diff --git a/.buildinfo b/.buildinfo new file mode 100644 index 00000000..a04eb55f --- /dev/null +++ b/.buildinfo @@ -0,0 +1,4 @@ +# Sphinx build info version 1 +# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. +config: fbf87ecf4aa3321a13aa764fc39e2bcd +tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 00000000..e69de29b diff --git a/_images/compiler_pipeline.svg b/_images/compiler_pipeline.svg new file mode 100644 index 00000000..64033a34 --- /dev/null +++ b/_images/compiler_pipeline.svg @@ -0,0 +1,561 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images/core_dem.png b/_images/core_dem.png new file mode 100644 index 00000000..aa4fa845 Binary files /dev/null and b/_images/core_dem.png differ diff --git a/_images/core_fluid.png b/_images/core_fluid.png new file mode 100644 index 00000000..27afe69c Binary files /dev/null and b/_images/core_fluid.png differ diff --git a/_images/core_graph_capture.png b/_images/core_graph_capture.png new file mode 100644 index 00000000..225fce78 Binary files /dev/null and b/_images/core_graph_capture.png differ diff --git a/_images/core_marching_cubes.png b/_images/core_marching_cubes.png new file mode 100644 index 00000000..e8994ccd Binary files /dev/null and b/_images/core_marching_cubes.png differ diff --git a/_images/core_mesh.png b/_images/core_mesh.png new file mode 100644 index 00000000..eb9e2288 Binary files /dev/null and b/_images/core_mesh.png differ diff --git a/_images/core_nvdb.png b/_images/core_nvdb.png new file mode 100644 index 00000000..2c3eca47 Binary files /dev/null and b/_images/core_nvdb.png differ diff --git a/_images/core_raycast.png b/_images/core_raycast.png new file mode 100644 index 00000000..e1c94924 Binary files /dev/null and b/_images/core_raycast.png differ diff --git a/_images/core_raymarch.png b/_images/core_raymarch.png new file mode 100644 index 00000000..433d094a Binary files /dev/null and b/_images/core_raymarch.png differ diff --git a/_images/core_sph.png b/_images/core_sph.png new file mode 100644 index 00000000..02392566 Binary files /dev/null and b/_images/core_sph.png differ diff --git a/_images/core_torch.png b/_images/core_torch.png new file mode 100644 index 00000000..d05efe38 Binary files /dev/null and b/_images/core_torch.png differ diff --git a/_images/core_wave.png b/_images/core_wave.png new file mode 100644 index 00000000..da9b2582 Binary files /dev/null and b/_images/core_wave.png differ diff --git a/_images/fem_apic_fluid.png b/_images/fem_apic_fluid.png new file mode 100644 index 00000000..619d010f Binary files /dev/null and b/_images/fem_apic_fluid.png differ diff --git a/_images/fem_convection_diffusion.png b/_images/fem_convection_diffusion.png new file mode 100644 index 00000000..c9d4cf5d Binary files /dev/null and b/_images/fem_convection_diffusion.png differ diff --git a/_images/fem_diffusion.png b/_images/fem_diffusion.png new file mode 100644 index 00000000..7d5d4518 Binary files /dev/null and b/_images/fem_diffusion.png differ diff --git a/_images/fem_diffusion_3d.png b/_images/fem_diffusion_3d.png new file mode 100644 index 00000000..76169a8b Binary files /dev/null and b/_images/fem_diffusion_3d.png differ diff --git a/_images/fem_mixed_elasticity.png b/_images/fem_mixed_elasticity.png new file mode 100644 index 00000000..7738cf3e Binary files /dev/null and b/_images/fem_mixed_elasticity.png differ diff --git a/_images/fem_navier_stokes.png b/_images/fem_navier_stokes.png new file mode 100644 index 00000000..ff8bb60f Binary files /dev/null and b/_images/fem_navier_stokes.png differ diff --git a/_images/fem_stokes.png b/_images/fem_stokes.png new file mode 100644 index 00000000..1a4a9357 Binary files /dev/null and b/_images/fem_stokes.png differ diff --git a/_images/fem_stokes_transfer.png b/_images/fem_stokes_transfer.png new file mode 100644 index 00000000..0f9cbb93 Binary files /dev/null and b/_images/fem_stokes_transfer.png differ diff --git a/_images/header.jpg b/_images/header.jpg new file mode 100644 index 00000000..bf4378a2 Binary files /dev/null and b/_images/header.jpg differ diff --git a/_images/joint_transforms.png b/_images/joint_transforms.png new file mode 100644 index 00000000..d39d1533 Binary files /dev/null and b/_images/joint_transforms.png differ diff --git a/_images/kernel_jacobian_ad.svg b/_images/kernel_jacobian_ad.svg new file mode 100644 index 00000000..beff1a3e --- /dev/null +++ b/_images/kernel_jacobian_ad.svg @@ -0,0 +1,1300 @@ + + + + + + + + 2024-06-10T12:09:07.177123 + image/svg+xml + + + Matplotlib v3.8.2, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images/kernel_jacobian_ad_subset.svg b/_images/kernel_jacobian_ad_subset.svg new file mode 100644 index 00000000..ee017900 --- /dev/null +++ b/_images/kernel_jacobian_ad_subset.svg @@ -0,0 +1,902 @@ + + + + + + + + 2024-06-10T14:54:41.664662 + image/svg+xml + + + Matplotlib v3.8.2, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images/optim_bounce.png b/_images/optim_bounce.png new file mode 100644 index 00000000..a7362b79 Binary files /dev/null and b/_images/optim_bounce.png differ diff --git a/_images/optim_cloth_throw.png b/_images/optim_cloth_throw.png new file mode 100644 index 00000000..eddd10b8 Binary files /dev/null and b/_images/optim_cloth_throw.png differ diff --git a/_images/optim_diffray.png b/_images/optim_diffray.png new file mode 100644 index 00000000..97d6a0a3 Binary files /dev/null and b/_images/optim_diffray.png differ diff --git a/_images/optim_drone.png b/_images/optim_drone.png new file mode 100644 index 00000000..e365cd6d Binary files /dev/null and b/_images/optim_drone.png differ diff --git a/_images/optim_inverse_kinematics.png b/_images/optim_inverse_kinematics.png new file mode 100644 index 00000000..52a25f0b Binary files /dev/null and b/_images/optim_inverse_kinematics.png differ diff --git a/_images/optim_spring_cage.png b/_images/optim_spring_cage.png new file mode 100644 index 00000000..ced3d729 Binary files /dev/null and b/_images/optim_spring_cage.png differ diff --git a/_images/optim_trajectory.png b/_images/optim_trajectory.png new file mode 100644 index 00000000..017e5972 Binary files /dev/null and b/_images/optim_trajectory.png differ diff --git a/_images/optim_walker.png b/_images/optim_walker.png new file mode 100644 index 00000000..0d0c4331 Binary files /dev/null and b/_images/optim_walker.png differ diff --git a/_images/profiling_nosync.png b/_images/profiling_nosync.png new file mode 100644 index 00000000..a32b7d41 Binary files /dev/null and b/_images/profiling_nosync.png differ diff --git a/_images/profiling_sync.png b/_images/profiling_sync.png new file mode 100644 index 00000000..c901bb10 Binary files /dev/null and b/_images/profiling_sync.png differ diff --git a/_images/sim_cartpole.png b/_images/sim_cartpole.png new file mode 100644 index 00000000..7eb8c888 Binary files /dev/null and b/_images/sim_cartpole.png differ diff --git a/_images/sim_cloth.png b/_images/sim_cloth.png new file mode 100644 index 00000000..38ac0e6f Binary files /dev/null and b/_images/sim_cloth.png differ diff --git a/_images/sim_granular.png b/_images/sim_granular.png new file mode 100644 index 00000000..9afa1df8 Binary files /dev/null and b/_images/sim_granular.png differ diff --git a/_images/sim_granular_collision_sdf.png b/_images/sim_granular_collision_sdf.png new file mode 100644 index 00000000..4cf0023c Binary files /dev/null and b/_images/sim_granular_collision_sdf.png differ diff --git a/_images/sim_jacobian_ik.png b/_images/sim_jacobian_ik.png new file mode 100644 index 00000000..2a0dd1a3 Binary files /dev/null and b/_images/sim_jacobian_ik.png differ diff --git a/_images/sim_quadruped.png b/_images/sim_quadruped.png new file mode 100644 index 00000000..be7e2548 Binary files /dev/null and b/_images/sim_quadruped.png differ diff --git a/_images/sim_rigid_chain.png b/_images/sim_rigid_chain.png new file mode 100644 index 00000000..07a88ddc Binary files /dev/null and b/_images/sim_rigid_chain.png differ diff --git a/_images/sim_rigid_contact.png b/_images/sim_rigid_contact.png new file mode 100644 index 00000000..6653ac5c Binary files /dev/null and b/_images/sim_rigid_contact.png differ diff --git a/_images/sim_rigid_force.png b/_images/sim_rigid_force.png new file mode 100644 index 00000000..7f152146 Binary files /dev/null and b/_images/sim_rigid_force.png differ diff --git a/_images/sim_rigid_gyroscopic.png b/_images/sim_rigid_gyroscopic.png new file mode 100644 index 00000000..2bcd7587 Binary files /dev/null and b/_images/sim_rigid_gyroscopic.png differ diff --git a/_images/sim_rigid_soft_contact.png b/_images/sim_rigid_soft_contact.png new file mode 100644 index 00000000..5cfa5c14 Binary files /dev/null and b/_images/sim_rigid_soft_contact.png differ diff --git a/_images/sim_soft_body.png b/_images/sim_soft_body.png new file mode 100644 index 00000000..20f8f4ea Binary files /dev/null and b/_images/sim_soft_body.png differ diff --git a/_images/tape.svg b/_images/tape.svg new file mode 100644 index 00000000..93e49b9c --- /dev/null +++ b/_images/tape.svg @@ -0,0 +1,218 @@ + + + + + + + + + + + + +cluster0 + + +Adder + + + + +cluster1 + + +Custom Scope + + + + + +kernel0 + + + + +add + + +a + + +c + + +b + + + + + +arr8663334912 + + +a + + + + + +kernel0:out_0->arr8663334912 + + + + + + + + +arr8663335424 + + +b + + + + + +arr8663335424->kernel0:in_0 + + + + + + + + +kernel1 + + + + +add + + +a + + +c + + +b + + + + + +arr8663335424->kernel1:in_1 + + + + + + + + +arr8663336960 + + +e + + + + + +arr8663336960->kernel0:in_1 + + + + + + + + +arr8663334912->kernel1:in_0 + + + + + + + + +kernel2 + + + + +add + + +a + + +c + + +b + + + + + +arr8663334912->kernel2:in_1 + + + + + + + + +arr8663336448 + + +c + + + + + +kernel1:out_0->arr8663336448 + + + + + + + + +arr8663336448->kernel2:in_0 + + + + + + + + +arr8663337984 + + +result + + + + + +kernel2:out_0->arr8663337984 + + + + + + + + diff --git a/_sources/basics.rst.txt b/_sources/basics.rst.txt new file mode 100644 index 00000000..2b042edf --- /dev/null +++ b/_sources/basics.rst.txt @@ -0,0 +1,313 @@ +Basics +====== + +.. currentmodule:: warp + +Initialization +-------------- + +When calling a Warp function like :func:`wp.launch() ` for the first time, +Warp will initialize itself and will print some startup information +about the compute devices available, driver versions, and the location for any +generated kernel code, e.g.: + +.. code:: bat + + Warp 1.2.0 initialized: + CUDA Toolkit 12.5, Driver 12.5 + Devices: + "cpu" : "x86_64" + "cuda:0" : "NVIDIA GeForce RTX 3090" (24 GiB, sm_86, mempool enabled) + "cuda:1" : "NVIDIA GeForce RTX 3090" (24 GiB, sm_86, mempool enabled) + CUDA peer access: + Supported fully (all-directional) + Kernel cache: + /home/nvidia/.cache/warp/1.2.0 + + +It's also possible to explicitly initialize Warp with the ``wp.init()`` method:: + + import warp as wp + + wp.init() + + +Kernels +------- + +In Warp, compute kernels are defined as Python functions and annotated with the ``@wp.kernel`` decorator:: + + @wp.kernel + def simple_kernel(a: wp.array(dtype=wp.vec3), + b: wp.array(dtype=wp.vec3), + c: wp.array(dtype=float)): + + # get thread index + tid = wp.tid() + + # load two vec3s + x = a[tid] + y = b[tid] + + # compute the dot product between vectors + r = wp.dot(x, y) + + # write result back to memory + c[tid] = r + +Because Warp kernels are compiled to native C++/CUDA code, all the function input arguments must be statically typed. This allows +Warp to generate fast code that executes at essentially native speeds. Because kernels may be run on either the CPU +or GPU, they cannot access arbitrary global state from the Python environment. Instead they must read and write data +through their input parameters such as arrays. + +Warp kernels functions have a one-to-one correspondence with CUDA kernels. +To launch a kernel with 1024 threads, we use :func:`wp.launch() `:: + + wp.launch(kernel=simple_kernel, # kernel to launch + dim=1024, # number of threads + inputs=[a, b, c], # parameters + device="cuda") # execution device + +Inside the kernel, we retrieve the *thread index* of the each thread using the :func:`wp.tid() ` built-in function:: + + # get thread index + i = wp.tid() + +Kernels can be launched with 1D, 2D, 3D, or 4D grids of threads. +To launch a 2D grid of threads to process a 1024x1024 image, we could write:: + + wp.launch(kernel=compute_image, dim=(1024, 1024), inputs=[img], device="cuda") + +We retrieve a 2D thread index inside the kernel by using multiple assignment when calling ``wp.tid()``: + +.. code-block:: python + + @wp.kernel + def compute_image(pixel_data: wp.array2d(dtype=wp.vec3)): + # get thread index + i, j = wp.tid() + +Arrays +------ + +Memory allocations are exposed via the ``wp.array`` type. Arrays wrap an underlying memory allocation that may live in +either host (CPU), or device (GPU) memory. Arrays are strongly typed and store a linear sequence of built-in values +(``float``, ``int``, ``vec3``, ``matrix33``, etc). + +Arrays can be allocated similar to PyTorch:: + + # allocate an uninitialized array of vec3s + v = wp.empty(shape=n, dtype=wp.vec3, device="cuda") + + # allocate a zero-initialized array of quaternions + q = wp.zeros(shape=n, dtype=wp.quat, device="cuda") + + # allocate and initialize an array from a NumPy array + # will be automatically transferred to the specified device + a = np.ones((10, 3), dtype=np.float32) + v = wp.from_numpy(a, dtype=wp.vec3, device="cuda") + +By default, Warp arrays that are initialized from external data (e.g.: NumPy, Lists, Tuples) will create a copy the data to new memory for the +device specified. However, it is possible for arrays to alias external memory using the ``copy=False`` parameter to the +array constructor provided the input is contiguous and on the same device. See the :doc:`/modules/interoperability` +section for more details on sharing memory with external frameworks. + +To read GPU array data back to CPU memory we can use :func:`array.numpy`:: + + # bring data from device back to host + view = device_array.numpy() + +This will automatically synchronize with the GPU to ensure that any outstanding work has finished, and will +copy the array back to CPU memory where it is passed to NumPy. +Calling :func:`array.numpy` on a CPU array will return a zero-copy NumPy view +onto the Warp data. + +Please see the :ref:`Arrays Reference ` for more details. + +User Functions +-------------- + +Users can write their own functions using the ``@wp.func`` decorator, for example:: + + @wp.func + def square(x: float): + return x*x + +Kernels can call user functions defined in the same module or defined in a different module. +As the example shows, return type hints for user functions are **optional**. + +Anything that can be done in a Warp kernel can also be done in a user function **with the exception** +of :func:`wp.tid() `. The thread index can be passed in through the arguments of a user function if it is required. + +Functions can accept arrays and structs as inputs: + +.. code-block:: python + + @wp.func + def lookup(foos: wp.array(dtype=wp.uint32), index: int): + return foos[index] + +Functions may also return multiple values: + +.. code-block:: python + + @wp.func + def multi_valued_func(a: wp.float32, b: wp.float32): + return a + b, a - b, a * b, a / b + + @wp.kernel + def test_multi_valued_kernel(test_data1: wp.array(dtype=wp.float32), test_data2: wp.array(dtype=wp.float32)): + tid = wp.tid() + d1, d2 = test_data1[tid], test_data2[tid] + a, b, c, d = multi_valued_func(d1, d2) + +User functions may also be overloaded by defining multiple function signatures with the same function name: + +.. code-block:: python + + @wp.func + def custom(x: int): + return x + 1 + + + @wp.func + def custom(x: float): + return x + 1.0 + + + @wp.func + def custom(x: wp.vec3): + return x + wp.vec3(1.0, 0.0, 0.0) + +See :ref:`Generic Functions` for details on using ``typing.Any`` in user function signatures. + +See :doc:`modules/differentiability` for details on how to define custom gradient functions, +custom replay functions, and custom native functions. + +User Structs +-------------- + +Users can define their own structures using the ``@wp.struct`` decorator, for example:: + + @wp.struct + class MyStruct: + + pos: wp.vec3 + vel: wp.vec3 + active: int + indices: wp.array(dtype=int) + + +Structs may be used as a ``dtype`` for ``wp.arrays``, and may be passed to kernels directly as arguments, +please see :ref:`Structs Reference ` for more details. + +.. note:: + + As with kernel parameters, all attributes of a struct must have valid type hints at class definition time. + +.. _Compilation Model: + +Compilation Model +----------------- + +Warp uses a Python->C++/CUDA compilation model that generates kernel code from Python function definitions. +All kernels belonging to a Python module are runtime compiled into dynamic libraries and PTX. +The result is then cached between application restarts for fast startup times. + +Note that compilation is triggered on the first kernel launch for that module. +Any kernels registered in the module with ``@wp.kernel`` will be included in the shared library. + +.. image:: ./img/compiler_pipeline.svg + +By default, status messages will be printed out after each module has been loaded indicating basic information: + +* The name of the module that was just loaded +* The first seven characters of the module hash +* The device on which the module is being loaded for +* How long it took to load the module in milliseconds +* Whether the module was compiled ``(compiled)``, loaded from the cache ``(cached)``, or was unable to be loaded ``(error)``. + +For debugging purposes, ``wp.config.verbose = True`` can be set to also get a printout when each module load begins. + +Here is an example illustrating the functionality of the kernel cache by running ``python3 -m warp.examples.sim.example_cartpole`` +twice. The first time, we see: + +.. code:: bat + + Warp 1.2.0 initialized: + CUDA Toolkit 12.5, Driver 12.5 + Devices: + "cpu" : "x86_64" + "cuda:0" : "NVIDIA GeForce RTX 3090" (24 GiB, sm_86, mempool enabled) + "cuda:1" : "NVIDIA GeForce RTX 3090" (24 GiB, sm_86, mempool enabled) + CUDA peer access: + Supported fully (all-directional) + Kernel cache: + /home/nvidia/.cache/warp/1.2.0 + Module warp.sim.collide 296dfb5 load on device 'cuda:0' took 17982.83 ms (compiled) + Module warp.sim.articulation b2cf0c2 load on device 'cuda:0' took 5686.67 ms (compiled) + Module warp.sim.integrator_euler b87aa18 load on device 'cuda:0' took 7753.78 ms (compiled) + Module warp.sim.integrator 036f39a load on device 'cuda:0' took 456.53 ms (compiled) + step took 0.06 ms + render took 4.63 ms + +The second time we run this example, we see that the module-loading messages now say ``(cached)`` and take much +less time to load since code compilation is skipped: + +.. code:: bat + + Warp 1.2.0 initialized: + CUDA Toolkit 12.5, Driver 12.5 + Devices: + "cpu" : "x86_64" + "cuda:0" : "NVIDIA GeForce RTX 3090" (24 GiB, sm_86, mempool enabled) + "cuda:1" : "NVIDIA GeForce RTX 3090" (24 GiB, sm_86, mempool enabled) + CUDA peer access: + Supported fully (all-directional) + Kernel cache: + /home/nvidia/.cache/warp/1.2.0 + Module warp.sim.collide 296dfb5 load on device 'cuda:0' took 9.07 ms (cached) + Module warp.sim.articulation b2cf0c2 load on device 'cuda:0' took 4.96 ms (cached) + Module warp.sim.integrator_euler b87aa18 load on device 'cuda:0' took 3.69 ms (cached) + Module warp.sim.integrator 036f39a load on device 'cuda:0' took 0.39 ms (cached) + step took 0.04 ms + render took 5.05 ms + +Language Details +---------------- + +To support GPU computation and differentiability, there are some differences from the CPython runtime. + +Built-in Types +^^^^^^^^^^^^^^ + +Warp supports a number of built-in math types similar to high-level shading languages, +e.g. ``vec2, vec3, vec4, mat22, mat33, mat44, quat, array``. +All built-in types have value semantics so that expressions such as ``a = b`` +generate a copy of the variable ``b`` rather than a reference. + +Strong Typing +^^^^^^^^^^^^^ + +Unlike Python, in Warp all variables must be typed. +Types are inferred from source expressions and function signatures using the Python typing extensions. +All kernel parameters must be annotated with the appropriate type, for example:: + + @wp.kernel + def simple_kernel(a: wp.array(dtype=vec3), + b: wp.array(dtype=vec3), + c: float): + +Tuple initialization is not supported, instead variables should be explicitly typed:: + + # invalid + a = (1.0, 2.0, 3.0) + + # valid + a = wp.vec3(1.0, 2.0, 3.0) + + +Limitations and Unsupported Features +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +See :doc:`limitations` for a list of Warp limitations and unsupported features. diff --git a/_sources/configuration.rst.txt b/_sources/configuration.rst.txt new file mode 100644 index 00000000..04ac2d2c --- /dev/null +++ b/_sources/configuration.rst.txt @@ -0,0 +1,163 @@ +.. _Configuration: + +Configuration +============= + +Warp has settings at the global, module, and kernel level that can be used to fine-tune the compilation and verbosity +of Warp programs. In cases in which a setting can be changed at multiple levels (e.g.: ``enable_backward``), +the setting at the more-specific scope takes precedence. + +Global Settings +--------------- + +To change a setting, prepend ``wp.config.`` to the name of the variable and assign a value to it. +Some settings may be changed on the fly, while others need to be set prior to calling ``wp.init()`` to take effect. + +For example, the location of the user kernel cache can be changed with: + +.. code-block:: python + + import os + + import warp as wp + + example_dir = os.path.dirname(os.path.realpath(__file__)) + + # set default cache directory before wp.init() + wp.config.kernel_cache_dir = os.path.join(example_dir, "tmp", "warpcache1") + + wp.init() + + +Basic Global Settings +^^^^^^^^^^^^^^^^^^^^^ + + ++------------------------------------------------+---------+-------------+--------------------------------------------------------------------------+ +| Field | Type |Default Value| Description | ++================================================+=========+=============+==========================================================================+ +|``verify_fp`` | Boolean | ``False`` | If ``True``, Warp will check that inputs and outputs are finite before | +| | | | and/or after various operations. **Has performance implications.** | ++------------------------------------------------+---------+-------------+--------------------------------------------------------------------------+ +|``verify_cuda`` | Boolean | ``False`` | If ``True``, Warp will check for CUDA errors after every launch and | +| | | | memory operation. CUDA error verification cannot be used during graph | +| | | | capture. **Has performance implications.** | ++------------------------------------------------+---------+-------------+--------------------------------------------------------------------------+ +|``print_launches`` | Boolean | ``False`` | If ``True``, Warp will print details of every kernel launch to standard | +| | | | out (e.g. launch dimensions, inputs, outputs, device, etc.). | +| | | | **Has performance implications.** | ++------------------------------------------------+---------+-------------+--------------------------------------------------------------------------+ +|``mode`` | String |``"release"``| Controls whether to compile Warp kernels in debug or release mode. | +| | | | Valid choices are ``"release"`` or ``"debug"``. | +| | | | **Has performance implications.** | ++------------------------------------------------+---------+-------------+--------------------------------------------------------------------------+ +|``max_unroll`` | Integer | Global | The maximum fixed-size loop to unroll. Note that ``max_unroll`` does not | +| | | setting | consider the total number of iterations in nested loops. This can result | +| | | | in a large amount of automatically generated code if each nested loop is | +| | | | below the ``max_unroll`` threshold. | ++------------------------------------------------+---------+-------------+--------------------------------------------------------------------------+ +|``verbose`` | Boolean | ``False`` | If ``True``, additional information will be printed to standard out | +| | | | during code generation, compilation, etc. | ++------------------------------------------------+---------+-------------+--------------------------------------------------------------------------+ +|``verbose_warnings`` | Boolean | ``False`` | If ``True``, Warp warnings will include extra information such as | +| | | | the source file and line number. | ++------------------------------------------------+---------+-------------+--------------------------------------------------------------------------+ +|``quiet`` | Boolean | ``False`` | If ``True``, Warp module initialization messages will be disabled. | +| | | | This setting does not affect error messages and warnings. | ++------------------------------------------------+---------+-------------+--------------------------------------------------------------------------+ +|``kernel_cache_dir`` | String | ``None`` | The path to the directory used for the user kernel cache. Subdirectories | +| | | | beginning with ``wp_`` will be created in this directory. If ``None``, | +| | | | a directory will be automatically determined using the value of the | +| | | | environment variable ``WARP_CACHE_PATH`` or the | +| | | | `appdirs.user_cache_directory `_ | +| | | | if ``WARP_CACHE_PATH`` is also not set. ``kernel_cache_dir`` will be | +| | | | updated to reflect the location of the cache directory used. | ++------------------------------------------------+---------+-------------+--------------------------------------------------------------------------+ +|``enable_backward`` | Boolean | ``True`` | If ``True``, backward passes of kernels will be compiled by default. | +| | | | Disabling this setting can reduce kernel compilation times. | ++------------------------------------------------+---------+-------------+--------------------------------------------------------------------------+ +|``enable_graph_capture_module_load_by_default`` | Boolean | ``True`` | If ``True``, ``wp.capture_begin()`` will call ``wp.force_load()`` to | +| | | | compile and load Warp kernels from all imported modules before graph | +| | | | capture if the ``force_module_load`` argument is not explicitly provided | +| | | | to ``wp.capture_begin()``. This setting is ignored if the CUDA driver | +| | | | supports CUDA 12.3 or newer. | ++------------------------------------------------+---------+-------------+--------------------------------------------------------------------------+ +|``enable_mempools_at_init`` | Boolean | ``False`` | If ``True``, ``wp.init()`` will enable pooled allocators on all CUDA | +| | | | devices that support memory pools. | +| | | | Pooled allocators are generally faster and can be used during CUDA graph | +| | | | capture. For the caveats, see CUDA Pooled Allocators documentation. | ++------------------------------------------------+---------+-------------+--------------------------------------------------------------------------+ + + +Advanced Global Settings +^^^^^^^^^^^^^^^^^^^^^^^^ + ++--------------------+---------+-------------+--------------------------------------------------------------------------+ +| Field | Type |Default Value| Description | ++====================+=========+=============+==========================================================================+ +|``cache_kernels`` | Boolean | ``True`` | If ``True``, kernels that have already been compiled from previous | +| | | | application launches will not be recompiled. | ++--------------------+---------+-------------+--------------------------------------------------------------------------+ +|``cuda_output`` | String | ``None`` | The preferred CUDA output format for kernels. Valid choices are ``None``,| +| | | | ``"ptx"``, and ``"cubin"``. If ``None``, a format will be determined | +| | | | automatically. | ++--------------------+---------+-------------+--------------------------------------------------------------------------+ +|``ptx_target_arch`` | Integer | 70 | The target architecture for PTX generation. | ++--------------------+---------+-------------+--------------------------------------------------------------------------+ +|``llvm_cuda`` | Boolean | ``False`` | If ``True``, Clang/LLVM will be used to compile CUDA code instead of | +| | | | NVTRC. | ++--------------------+---------+-------------+--------------------------------------------------------------------------+ + +Module Settings +--------------- + +Module-level settings to control runtime compilation and code generation may be changed by passing a dictionary of +option pairs to ``wp.set_module_options()``. + +For example, compilation of backward passes for the kernel in an entire module can be disabled with: + +.. code:: python + + wp.set_module_options({"enable_backward": False}) + +The options for a module can also be queried using ``wp.get_module_options()``. + ++--------------------+---------+-------------+--------------------------------------------------------------------------+ +| Field | Type |Default Value| Description | ++====================+=========+=============+==========================================================================+ +|``mode`` | String | Global | Controls whether to compile the module's kernels in debug or release | +| | | setting | mode by default. Valid choices are ``"release"`` or ``"debug"``. | ++--------------------+---------+-------------+--------------------------------------------------------------------------+ +|``max_unroll`` | Integer | Global | The maximum fixed-size loop to unroll. Note that ``max_unroll`` does not | +| | | setting | consider the total number of iterations in nested loops. This can result | +| | | | in a large amount of automatically generated code if each nested loop is | +| | | | below the ``max_unroll`` threshold. | ++--------------------+---------+-------------+--------------------------------------------------------------------------+ +|``enable_backward`` | Boolean | Global | If ``True``, backward passes of kernels will be compiled by default. | +| | | setting | Valid choices are ``"release"`` or ``"debug"``. | ++--------------------+---------+-------------+--------------------------------------------------------------------------+ +|``fast_math`` | Boolean | ``False`` | If ``True``, CUDA kernels will be compiled with the ``--use_fast_math`` | +| | | | compiler option, which enables some fast math operations that are faster | +| | | | but less accurate. | ++--------------------+---------+-------------+--------------------------------------------------------------------------+ +|``cuda_output`` | String | ``None`` | The preferred CUDA output format for kernels. Valid choices are ``None``,| +| | | | ``"ptx"``, and ``"cubin"``. If ``None``, a format will be determined | +| | | | automatically. The module-level setting takes precedence over the global | +| | | | setting. | ++--------------------+---------+-------------+--------------------------------------------------------------------------+ + +Kernel Settings +--------------- + +``enable_backward`` is currently the only setting that can also be configured on a per-kernel level. +Backward-pass compilation can be disabled by passing an argument into the ``@wp.kernel`` decorator +as in the following example: + +.. code-block:: python + + @wp.kernel(enable_backward=False) + def scale_2( + x: wp.array(dtype=float), + y: wp.array(dtype=float), + ): + y[0] = x[0] ** 2.0 diff --git a/_sources/debugging.rst.txt b/_sources/debugging.rst.txt new file mode 100644 index 00000000..4812ff0b --- /dev/null +++ b/_sources/debugging.rst.txt @@ -0,0 +1,89 @@ +Debugging +========= + +Printing Values +--------------- + +Often one of the best debugging methods is to simply print values from kernels. Warp supports printing all built-in +types using the ``print()`` function, e.g.:: + + v = wp.vec3(1.0, 2.0, 3.0) + + print(v) + +In addition, formatted C-style printing is available through the ``wp.printf()`` function, e.g.:: + + x = 1.0 + i = 2 + + wp.printf("A float value %f, an int value: %d", x, i) + +.. note:: Formatted printing is only available for scalar types (e.g.: ``int`` and ``float``) not vector types. + +Printing Launches +----------------- + +For complex applications it can be difficult to understand the order-of-operations that lead to a bug. To help diagnose +these issues Warp supports a simple option to print out all launches and arguments to the console:: + + wp.config.print_launches = True + + +Step-Through Debugging +---------------------- + +It is possible to attach IDE debuggers such as Visual Studio to Warp processes to step through generated kernel code. +Users should first compile the kernels in debug mode by setting:: + + wp.config.mode = "debug" + +This setting ensures that line numbers, and debug symbols are generated correctly. After launching the Python process, +the debugger should be attached, and a breakpoint inserted into the generated code. + +.. note:: Generated kernel code is not a 1:1 correspondence with the original Python code, but individual operations can still be replayed and variables inspected. + +Also see :github:`warp/tests/walkthrough_debug.py` for an example of how to debug Warp kernel code running on the CPU. + +Generated Code +-------------- + +Occasionally it can be useful to inspect the generated code for debugging or profiling. +The generated code for kernels is stored in a central cache location in the user's home directory, the cache location +is printed at startup when ``wp.init()`` is called, for example: + +.. code-block:: console + + Warp 0.8.1 initialized: + CUDA Toolkit: 11.8, Driver: 11.8 + Devices: + "cpu" | AMD64 Family 25 Model 33 Stepping 0, AuthenticAMD + "cuda:0" | NVIDIA GeForce RTX 3090 (sm_86) + "cuda:1" | NVIDIA GeForce RTX 2080 Ti (sm_75) + Kernel cache: C:\Users\LukasW\AppData\Local\NVIDIA Corporation\warp\Cache\0.8.1 + +The kernel cache has folders beginning with ``wp_`` that contain the generated C++/CUDA code and the compiled binaries +for each module that was compiled at runtime. +The name of each folder ends with a hexadecimal hash constructed from the module contents to avoid potential +conflicts when using multiple processes and to support the caching of runtime-defined kernels. + +Bounds Checking +--------------- + +Warp will perform bounds checking in debug build configurations to ensure that all array accesses lie within the defined +shape. + +CUDA Verification +----------------- + +It is possible to generate out-of-bounds memory access violations through poorly formed kernel code or inputs. In this +case the CUDA runtime will detect the violation and put the CUDA context into an error state. Subsequent kernel launches +may silently fail which can lead to hard to diagnose issues. + +If a CUDA error is suspected a simple verification method is to enable:: + + wp.config.verify_cuda = True + +This setting will check the CUDA context after every operation to ensure that it is still valid. If an error is +encountered it will raise an exception that often helps to narrow down the problematic kernel. + +.. note:: Verifying CUDA state at each launch requires synchronizing CPU and GPU which has a significant overhead. Users should ensure this setting is only used during debugging. diff --git a/_sources/faq.rst.txt b/_sources/faq.rst.txt new file mode 100644 index 00000000..2ab44721 --- /dev/null +++ b/_sources/faq.rst.txt @@ -0,0 +1,135 @@ +FAQ +=== + +How does Warp relate to other Python projects for GPU programming, e.g.: Numba, Taichi, cuPy, PyTorch, etc.? +------------------------------------------------------------------------------------------------------------ + +Warp is inspired by many of these projects, and is closely related to +Numba and Taichi which both expose kernel programming to Python. These +frameworks map to traditional GPU programming models, so many of the +high-level concepts are similar, however there are some functionality +and implementation differences. + +Compared to Numba, Warp supports a smaller subset of Python, but +offering auto-differentiation of kernel programs, which is useful for +machine learning. Compared to Taichi Warp uses C++/CUDA as an +intermediate representation, which makes it convenient to implement and +expose low-level routines. In addition, we are building in +data structures to support geometry processing (meshes, sparse volumes, +point clouds, USD data) as first-class citizens that are not exposed in +other runtimes. + +Warp does not offer a full tensor-based programming model like PyTorch +and JAX, but is designed to work well with these frameworks through data +sharing mechanisms like ``__cuda_array_interface__``. For computations +that map well to tensors (e.g.: neural-network inference) it makes sense +to use these existing tools. For problems with a lot of e.g.: sparsity, +conditional logic, heterogeneous workloads (like the ones we often find in +simulation and graphics), then the kernel-based programming model like +the one in Warp are often more convenient since users have control over +individual threads. + +Does Warp support all of the Python language? +--------------------------------------------- + +No, Warp supports a subset of Python that maps well to the GPU. Our goal +is to not have any performance cliffs so that users can expect +consistently good behavior from kernels that is close to native code. +Examples of unsupported concepts that don't map well to the GPU are +dynamic types, list comprehensions, exceptions, garbage collection, etc. + +When should I call ``wp.synchronize()``? +---------------------------------------- + +One of the common sources of confusion for new users is when calls to +``wp.synchronize()`` are necessary. The answer is “almost never”! +Synchronization is quite expensive and should generally be avoided +unless necessary. Warp naturally takes care of synchronization between +operations (e.g.: kernel launches, device memory copies). + +For example, the following requires no manual synchronization, as the +conversion to NumPy will automatically synchronize: + +.. code:: python + + # run some kernels + wp.launch(kernel_1, dim, [array_x, array_y], device="cuda") + wp.launch(kernel_2, dim, [array_y, array_z], device="cuda") + + # bring data back to host (and implicitly synchronize) + x = array_z.numpy() + +The *only* case where manual synchronization is needed is when copies +are being performed back to CPU asynchronously, e.g.: + +.. code:: python + + # copy data back to cpu from gpu, all copies will happen asynchronously to Python + wp.copy(cpu_array_1, gpu_array_1) + wp.copy(cpu_array_2, gpu_array_2) + wp.copy(cpu_array_3, gpu_array_3) + + # ensure that the copies have finished + wp.synchronize() + + # return a numpy wrapper around the cpu arrays, note there is no implicit synchronization here + a1 = cpu_array_1.numpy() + a2 = cpu_array_2.numpy() + a3 = cpu_array_3.numpy() + +For more information about asynchronous operations, please refer to the :doc:`concurrency documentation ` +and :ref:`synchronization guidance `. + +What happens when you differentiate a function like ``wp.abs(x)``? +------------------------------------------------------------------ + +Non-smooth functions such as :math:`y=|x|` do not have a single unique +gradient at :math:`x=0`, rather they have what is known as a +*subgradient*, which is formally the convex hull of directional +derivatives at that point. The way that Warp (and most +auto-differentiation frameworks) handles these points is to pick an +arbitrary gradient from this set, e.g.: for ``wp.abs()``, it will +arbitrarily choose the gradient to be 1.0 at the origin. You can find +the implementation for these functions in +`warp/native/builtin.h `_. + +Most optimizers (particularly ones that exploit stochasticity), are not +sensitive to the choice of which gradient to use from the subgradient, +although there are exceptions. + +Does Warp support multi-GPU programming? +---------------------------------------- + +Yes! Since version ``0.4.0`` we support allocating, launching, and +copying between multiple GPUs in a single process. We follow the naming +conventions of PyTorch and use aliases such as ``cuda:0``, ``cuda:1``, +``cpu`` to identify individual devices. + +Should I switch to Warp over IsaacGym/PhysX? +---------------------------------------------- + +Warp is not a replacement for IsaacGym, IsaacSim, or PhysX—while Warp +does offer some physical simulation capabilities, this is primarily aimed +at developers who need differentiable physics, rather than a fully +featured physics engine. Warp is also integrated with IsaacGym and is +great for performing auxiliary tasks such as reward and observation +computations for reinforcement learning. + +Why aren't assignments to Warp arrays supported outside of kernels? +------------------------------------------------------------------------ + +For best performance, reading and writing data that is living on the GPU can +only be performed inside Warp CUDA kernels. Otherwise individual element accesses +such as ``array[i] = 1.0`` in Python scope would require prohibitively slow device +synchronization and copies. + +We recommend to either initialize Warp arrays from other native arrays +(Python lists, NumPy arrays, etc.) or by launching a kernel to set its values. + +For the common use case of filling an array with a given value, we +also support the following forms: + +- ``wp.full(8, 1.23, dtype=float)``: initializes a new array of 8 float values set + to ``1.23``. +- ``arr.fill_(1.23)``: sets the content of an existing float array to ``1.23``. +- ``arr[:4].fill(1.23)``: sets the four first values of an existing float array to ``1.23``. diff --git a/_sources/index.rst.txt b/_sources/index.rst.txt new file mode 100644 index 00000000..a354e37d --- /dev/null +++ b/_sources/index.rst.txt @@ -0,0 +1,391 @@ +NVIDIA Warp Documentation +========================= + +Warp is a Python framework for writing high-performance simulation and graphics code. Warp takes +regular Python functions and JIT compiles them to efficient kernel code that can run on the CPU or GPU. + +Warp is designed for `spatial computing `_ +and comes with a rich set of primitives that make it easy to write +programs for physics simulation, perception, robotics, and geometry processing. In addition, Warp kernels +are differentiable and can be used as part of machine-learning pipelines with frameworks such as PyTorch and JAX. + +Below are some examples of simulations implemented using Warp: + +.. image:: ./img/header.jpg + +Quickstart +---------- + +The easiest way to install Warp is from `PyPI `_: + +.. code-block:: sh + + $ pip install warp-lang + +You can also use ```pip install warp-lang[extras]``` to install additional dependencies for running examples +and USD-related features. + +The binaries hosted on PyPI are currently built with the CUDA 12 runtime and therefore +require a minimum version of the CUDA driver of 525.60.13 (Linux x86-64) or 528.33 (Windows x86-64). + +If you require GPU support on a system with an older CUDA driver, you can build Warp from source or +install wheels built with the CUDA 11.8 runtime as described in :ref:`GitHub Installation`. + +Basic Example +------------- + +An example first program that computes the lengths of random 3D vectors is given below:: + + import warp as wp + import numpy as np + + num_points = 1024 + + @wp.kernel + def length(points: wp.array(dtype=wp.vec3), + lengths: wp.array(dtype=float)): + + # thread index + tid = wp.tid() + + # compute distance of each point from origin + lengths[tid] = wp.length(points[tid]) + + + # allocate an array of 3d points + points = wp.array(np.random.rand(num_points, 3), dtype=wp.vec3) + lengths = wp.zeros(num_points, dtype=float) + + # launch kernel + wp.launch(kernel=length, + dim=len(points), + inputs=[points, lengths]) + + print(lengths) + +Additional Examples +------------------- + +The `warp/examples `_ directory in +the Github repository contains a number of scripts categorized under subdirectories +that show how to implement various simulation methods using the Warp API. Most examples +will generate USD files containing time-sampled animations in the current working directory. +Before running examples, users should ensure that the ``usd-core``, ``matplotlib``, and ``pyglet`` packages are installed using:: + + pip install warp-lang[extras] + +These dependencies can also be manually installed using:: + + pip install usd-core matplotlib pyglet + +Examples can be run from the command-line as follows:: + + python -m warp.examples.. + +Most examples can be run on either the CPU or a CUDA-capable device, but a handful require a CUDA-capable device. These are marked at the top of the example script. + +USD files can be viewed or rendered inside NVIDIA +`Omniverse `_, +Pixar's UsdView, and Blender. Note that Preview in macOS is not +recommended as it has limited support for time-sampled animations. + +Built-in unit tests can be run from the command-line as follows:: + + python -m warp.tests + +warp/examples/core +^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :class: gallery + + * - .. image:: ./img/examples/core_dem.png + :target: https://github.com/NVIDIA/warp/tree/main/warp/examples/core/example_dem.py + - .. image:: ./img/examples/core_fluid.png + :target: https://github.com/NVIDIA/warp/tree/main/warp/examples/core/example_fluid.py + - .. image:: ./img/examples/core_graph_capture.png + :target: https://github.com/NVIDIA/warp/tree/main/warp/examples/core/example_graph_capture.py + - .. image:: ./img/examples/core_marching_cubes.png + :target: https://github.com/NVIDIA/warp/tree/main/warp/examples/core/example_marching_cubes.py + * - dem + - fluid + - graph capture + - marching cubes + * - .. image:: ./img/examples/core_mesh.png + :target: https://github.com/NVIDIA/warp/tree/main/warp/examples/core/example_mesh.py + - .. image:: ./img/examples/core_nvdb.png + :target: https://github.com/NVIDIA/warp/tree/main/warp/examples/core/example_nvdb.py + - .. image:: ./img/examples/core_raycast.png + :target: https://github.com/NVIDIA/warp/tree/main/warp/examples/core/example_raycast.py + - .. image:: ./img/examples/core_raymarch.png + :target: https://github.com/NVIDIA/warp/tree/main/warp/examples/core/example_raymarch.py + * - mesh + - nvdb + - raycast + - raymarch + * - .. image:: ./img/examples/core_sph.png + :target: https://github.com/NVIDIA/warp/tree/main/warp/examples/core/example_sph.py + - .. image:: ./img/examples/core_torch.png + :target: https://github.com/NVIDIA/warp/tree/main/warp/examples/core/example_torch.py + - .. image:: ./img/examples/core_wave.png + :target: https://github.com/NVIDIA/warp/tree/main/warp/examples/core/example_wave.py + - + * - sph + - torch + - wave + - + +warp/examples/fem +^^^^^^^^^^^^^^^^^ + +.. list-table:: + :class: gallery + + * - .. image:: ./img/examples/fem_apic_fluid.png + :target: https://github.com/NVIDIA/warp/tree/main/warp/examples/fem/example_apic_fluid.py + - .. image:: ./img/examples/fem_convection_diffusion.png + :target: https://github.com/NVIDIA/warp/tree/main/warp/examples/fem/example_convection_diffusion.py + - .. image:: ./img/examples/fem_diffusion_3d.png + :target: https://github.com/NVIDIA/warp/tree/main/warp/examples/fem/example_diffusion_3d.py + - .. image:: ./img/examples/fem_diffusion.png + :target: https://github.com/NVIDIA/warp/tree/main/warp/examples/fem/example_diffusion.py + * - apic fluid + - convection diffusion + - diffusion 3d + - diffusion + * - .. image:: ./img/examples/fem_mixed_elasticity.png + :target: https://github.com/NVIDIA/warp/tree/main/warp/examples/fem/example_mixed_elasticity.py + - .. image:: ./img/examples/fem_navier_stokes.png + :target: https://github.com/NVIDIA/warp/tree/main/warp/examples/fem/example_navier_stokes.py + - .. image:: ./img/examples/fem_stokes_transfer.png + :target: https://github.com/NVIDIA/warp/tree/main/warp/examples/fem/example_stokes_transfer.py + - .. image:: ./img/examples/fem_stokes.png + :target: https://github.com/NVIDIA/warp/tree/main/warp/examples/fem/example_stokes.py + * - mixed elasticity + - navier stokes + - stokes transfer + - stokes + +warp/examples/optim +^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :class: gallery + + * - .. image:: ./img/examples/optim_bounce.png + :target: https://github.com/NVIDIA/warp/tree/main/warp/examples/optim/example_bounce.py + - .. image:: ./img/examples/optim_cloth_throw.png + :target: https://github.com/NVIDIA/warp/tree/main/warp/examples/optim/example_cloth_throw.py + - .. image:: ./img/examples/optim_diffray.png + :target: https://github.com/NVIDIA/warp/tree/main/warp/examples/optim/example_diffray.py + - .. image:: ./img/examples/optim_drone.png + :target: https://github.com/NVIDIA/warp/tree/main/warp/examples/optim/example_drone.py + * - bounce + - cloth throw + - diffray + - drone + * - .. image:: ./img/examples/optim_inverse_kinematics.png + :target: https://github.com/NVIDIA/warp/tree/main/warp/examples/optim/example_inverse_kinematics.py + - .. image:: ./img/examples/optim_spring_cage.png + :target: https://github.com/NVIDIA/warp/tree/main/warp/examples/optim/example_spring_cage.py + - .. image:: ./img/examples/optim_trajectory.png + :target: https://github.com/NVIDIA/warp/tree/main/warp/examples/optim/example_trajectory.py + - .. image:: ./img/examples/optim_walker.png + :target: https://github.com/NVIDIA/warp/tree/main/warp/examples/optim/example_walker.py + * - inverse kinematics + - spring cage + - trajectory + - walker + +warp/examples/sim +^^^^^^^^^^^^^^^^^ + +.. list-table:: + :class: gallery + + * - .. image:: ./img/examples/sim_cartpole.png + :target: https://github.com/NVIDIA/warp/tree/main/warp/examples/sim/example_cartpole.py + - .. image:: ./img/examples/sim_cloth.png + :target: https://github.com/NVIDIA/warp/tree/main/warp/examples/sim/example_cloth.py + - .. image:: ./img/examples/sim_granular.png + :target: https://github.com/NVIDIA/warp/tree/main/warp/examples/sim/example_granular.py + - .. image:: ./img/examples/sim_granular_collision_sdf.png + :target: https://github.com/NVIDIA/warp/tree/main/warp/examples/sim/example_granular_collision_sdf.py + * - cartpole + - cloth + - granular + - granular collision sdf + * - .. image:: ./img/examples/sim_jacobian_ik.png + :target: https://github.com/NVIDIA/warp/tree/main/warp/examples/sim/example_jacobian_ik.py + - .. image:: ./img/examples/sim_quadruped.png + :target: https://github.com/NVIDIA/warp/tree/main/warp/examples/sim/example_quadruped.py + - .. image:: ./img/examples/sim_rigid_chain.png + :target: https://github.com/NVIDIA/warp/tree/main/warp/examples/sim/example_rigid_chain.py + - .. image:: ./img/examples/sim_rigid_contact.png + :target: https://github.com/NVIDIA/warp/tree/main/warp/examples/sim/example_rigid_contact.py + * - jacobian ik + - quadruped + - rigid chain + - rigid contact + * - .. image:: ./img/examples/sim_rigid_force.png + :target: https://github.com/NVIDIA/warp/tree/main/warp/examples/sim/example_rigid_force.py + - .. image:: ./img/examples/sim_rigid_gyroscopic.png + :target: https://github.com/NVIDIA/warp/tree/main/warp/examples/sim/example_rigid_gyroscopic.py + - .. image:: ./img/examples/sim_rigid_soft_contact.png + :target: https://github.com/NVIDIA/warp/tree/main/warp/examples/sim/example_rigid_soft_contact.py + - .. image:: ./img/examples/sim_soft_body.png + :target: https://github.com/NVIDIA/warp/tree/main/warp/examples/sim/example_soft_body.py + * - rigid force + - rigid gyroscopic + - rigid soft contact + - soft body + +Omniverse +--------- + +Omniverse extensions for Warp are available in the extension registry inside +Omniverse Kit or USD Composer. +The ``omni.warp.core`` extension installs Warp into the Omniverse Application's +Python environment, which allows users to import the module in their scripts and nodes. +The ``omni.warp`` extension provides a collection of OmniGraph nodes and sample +scenes demonstrating uses of Warp in OmniGraph. +Enabling the ``omni.warp`` extension automatically enables the ``omni.warp.core`` extension. + +Please see the +`Omniverse Warp Documentation `_ +for more details on how to use Warp in Omniverse. + + +Learn More +---------- + +Please see the following resources for additional background on Warp: + +- `Product Page `_ +- `GTC 2022 + Presentation `_ +- `GTC 2021 + Presentation `_ +- `SIGGRAPH Asia 2021 Differentiable Simulation + Course `_ +- `GTC 2024 Presentation `_ + +The underlying technology in Warp has been used in a number of research +projects at NVIDIA including the following publications: + +- Accelerated Policy Learning with Parallel Differentiable Simulation - + Xu, J., Makoviychuk, V., Narang, Y., Ramos, F., Matusik, W., Garg, + A., & Macklin, M. + `(2022) `__ +- DiSECt: Differentiable Simulator for Robotic Cutting - Heiden, E., + Macklin, M., Narang, Y., Fox, D., Garg, A., & Ramos, F + `(2021) `__ +- gradSim: Differentiable Simulation for System Identification and + Visuomotor Control - Murthy, J. Krishna, Miles Macklin, Florian + Golemo, Vikram Voleti, Linda Petrini, Martin Weiss, Breandan + Considine et + al. `(2021) `__ + +Support +------- + +Problems, questions, and feature requests can be opened on +`GitHub Issues `_. + +The Warp team also monitors the **#warp** channel on the public +`Omniverse Discord `_ server, come chat with us! + +Versioning +---------- + +Versions take the format X.Y.Z, similar to `Python itself `__: + +* Increments in X are reserved for major reworks of the project causing disruptive incompatibility (or reaching the 1.0 milestone). +* Increments in Y are for regular releases with a new set of features. +* Increments in Z are for bug fixes. In principle, there are no new features. Can be omitted if 0 or not relevant. + +This is similar to `Semantic Versioning `_ minor versions if well-documented and gradually introduced. + +Note that prior to 0.11.0, this schema was not strictly adhered to. + +License +------- + +Warp is provided under the NVIDIA Software License, please see +`LICENSE.md `__ for the full license text. + +Contributing +------------ + +Contributions and pull requests from the community are welcome and are taken under the +terms described in the **Feedback** section of `LICENSE.md `__. +`CONTRIBUTING.md `_ provides additional information on +how to open a pull request for Warp. + +Citing +------ + +If you use Warp in your research, please use the following citation: + +.. code:: bibtex + + @misc{warp2022, + title= {Warp: A High-performance Python Framework for GPU Simulation and Graphics}, + author = {Miles Macklin}, + month = {March}, + year = {2022}, + note= {NVIDIA GPU Technology Conference (GTC)}, + howpublished = {\url{https://github.com/nvidia/warp}} + } + +Full Table of Contents +---------------------- + +.. toctree:: + :maxdepth: 2 + :caption: User's Guide + + installation + basics + modules/devices + modules/differentiability + modules/generics + modules/interoperability + configuration + debugging + limitations + faq + +.. toctree:: + :maxdepth: 2 + :caption: Advanced Topics + + modules/allocators + modules/concurrency + profiling + +.. toctree:: + :maxdepth: 2 + :caption: Core Reference + + modules/runtime + modules/functions + +.. toctree:: + :maxdepth: 2 + :caption: Simulation Reference + + modules/sim + modules/sparse + modules/fem + modules/render + +.. toctree:: + :hidden: + :caption: Project Links + + GitHub + PyPI + Discord + +:ref:`Full Index ` diff --git a/_sources/installation.rst.txt b/_sources/installation.rst.txt new file mode 100644 index 00000000..eef093be --- /dev/null +++ b/_sources/installation.rst.txt @@ -0,0 +1,142 @@ +Installation +============ + +Python version 3.9 or newer is *recommended*. Warp can run on x86-64 and ARMv8 CPUs on Windows, Linux, and macOS. GPU support requires a CUDA-capable NVIDIA GPU and driver (minimum GeForce GTX 9xx). + +The easiest way to install Warp is from `PyPI `_: + +.. code-block:: sh + + $ pip install warp-lang + +.. _GitHub Installation: + +Installing from GitHub Releases +------------------------------- + +The binaries hosted on PyPI are currently built with the CUDA 12 runtime. +We also provide binaries built with the CUDA 11.8 runtime on the `GitHub Releases `_ page. +Copy the URL of the appropriate wheel file (``warp-lang-{ver}+cu11-py3-none-{platform}.whl``) and pass it to +the ``pip install`` command, e.g. + +.. list-table:: + :header-rows: 1 + + * - Platform + - Install Command + * - Linux aarch64 + - ``pip install https://github.com/NVIDIA/warp/releases/download/v1.3.1/warp_lang-1.3.1+cu11-py3-none-manylinux2014_aarch64.whl`` + * - Linux x86-64 + - ``pip install https://github.com/NVIDIA/warp/releases/download/v1.3.1/warp_lang-1.3.1+cu11-py3-none-manylinux2014_x86_64.whl`` + * - Windows x86-64 + - ``pip install https://github.com/NVIDIA/warp/releases/download/v1.3.1/warp_lang-1.3.1+cu11-py3-none-win_amd64.whl`` + +The ``--force-reinstall`` option may need to be used to overwrite a previous installation. + +CUDA Requirements +----------------- + +* Warp packages built with CUDA Toolkit 11.x require NVIDIA driver 470 or newer. +* Warp packages built with CUDA Toolkit 12.x require NVIDIA driver 525 or newer. + +This applies to pre-built packages distributed on PyPI and GitHub and also when building Warp from source. + +Note that building Warp with the ``--quick`` flag changes the driver requirements. +The quick build skips CUDA backward compatibility, so the minimum required driver is determined by the CUDA Toolkit version. +Refer to the `latest CUDA Toolkit release notes `_ +to find the minimum required driver for different CUDA Toolkit versions +(e.g., `this table from CUDA Toolkit 12.5 `_). + +Warp checks the installed driver during initialization and will report a warning if the driver is not suitable, e.g.: + +.. code-block:: text + + Warp UserWarning: + Insufficient CUDA driver version. + The minimum required CUDA driver version is 12.0, but the installed CUDA driver version is 11.8. + Visit https://github.com/NVIDIA/warp/blob/main/README.md#installing for guidance. + +This will make CUDA devices unavailable, but the CPU can still be used. + +To remedy the situation there are a few options: + +* Update the driver. +* Install a compatible pre-built Warp package. +* Build Warp from source using a CUDA Toolkit that's compatible with the installed driver. + +Dependencies +------------ + +Warp supports Python versions 3.7 onwards, with 3.9 or newer recommended for full functionality. Note that :ref:`some optional dependencies may not support the latest version of Python`. + +`NumPy `_ must be installed. + +The following optional dependencies are required to support certain features: + +* `usd-core `_: Required for some Warp examples, ``warp.sim.parse_usd()``, and ``warp.render.UsdRenderer``. +* `JAX `_: Required for JAX interoperability (see :ref:`jax-interop`). +* `PyTorch `_: Required for PyTorch interoperability (see :ref:`pytorch-interop`). +* `NVTX for Python `_: Required to use :class:`wp.ScopedTimer(use_nvtx=True) `. + +Building the Warp documentation requires: + +* `Sphinx `_ +* `Furo `_ +* `Sphinx-copybutton `_ + +Building from Source +-------------------- + +For developers who want to build the library themselves the following tools are required: + +* Microsoft Visual Studio (Windows), minimum version 2019 +* GCC (Linux), minimum version 9.4 +* `CUDA Toolkit `_, minimum version 11.5 +* `Git Large File Storage `_ + +After cloning the repository, users should run: + +.. code-block:: console + + $ python build_lib.py + +Upon success, the script will output platform-specific binary files in ``warp/bin/``. +The build script will look for the CUDA Toolkit in its default installation path. +This path can be overridden by setting the ``CUDA_PATH`` environment variable. Alternatively, +the path to the CUDA Toolkit can be passed to the build command as +``--cuda_path="..."``. After building, the Warp package should be installed using: + +.. code-block:: console + + $ pip install -e . + +The ``-e`` option is optional but ensures that subsequent modifications to the +library will be reflected in the Python package. + +.. _conda: + +Conda Environments +------------------ + +Some modules, such as ``usd-core``, don't support the latest Python version. +To manage running Warp and other projects on different Python versions one can +make use of an environment management system such as +`Conda `__. + +.. warning:: + + When building and running Warp in a different environment, make sure + the build environment has the same C++ runtime library version, or an older + one, than the execution environment. Otherwise Warp's shared libraries may end + up looking for a newer runtime library version than the one available in the + execution environment. For example, on Linux this error could occur:: + + OSError: <...>/libstdc++.so.6: version `GLIBCXX_3.4.30' not found (required by <...>/warp/warp/bin/warp.so) + + This can be solved by installing a newer C++ runtime version in the runtime + Conda environment using ``conda install -c conda-forge libstdcxx-ng=12.1`` or + newer. + + Alternatively, the build environment's C++ toolchain can be downgraded using + ``conda install -c conda-forge libstdcxx-ng=8.5``. Or, one can ``activate`` or + ``deactivate`` Conda environments as needed for building vs. running Warp. diff --git a/_sources/limitations.rst.txt b/_sources/limitations.rst.txt new file mode 100644 index 00000000..f0286d34 --- /dev/null +++ b/_sources/limitations.rst.txt @@ -0,0 +1,142 @@ +Limitations +=========== + +.. currentmodule:: warp + +This section summarizes various limitations and currently unsupported features in Warp. +Problems, questions, and feature requests can be opened on `GitHub Issues `_. + +Unsupported Features +-------------------- + +To achieve good performance on GPUs some dynamic language features are not supported: + +* Lambda functions +* List comprehensions +* Exceptions +* Recursion +* Runtime evaluation of expressions, e.g.: eval() +* Dynamic structures such as lists, sets, dictionaries, etc. + +Kernels and User Functions +-------------------------- + +* Strings cannot be passed into kernels. +* Short-circuit evaluation is not supported +* :func:`wp.atomic_add() ` does not support ``wp.int64``. +* :func:`wp.tid() ` cannot be called from user functions. +* Modifying the value of a :class:`wp.constant() ` during runtime will not trigger + recompilation of the affected kernels if the modules have already been loaded + (e.g. through a :func:`wp.launch() ` or a ``wp.load_module()``). +* A :class:`wp.constant() ` can suffer precision loss if used with ``wp.float64`` + as it is initially assigned to a ``wp.float32`` variable in the generated code. + +A limitation of Warp is that each dimension of the grid used to launch a kernel must be representable as a 32-bit +signed integer. Therefore, no single dimension of a grid should exceed :math:`2^{31}-1`. + +Warp also currently uses a fixed block size of 256 (CUDA) threads per block. +By default, Warp will try to process one element from the Warp grid in one CUDA thread. +This is not always possible for kernels launched with multi-dimensional grid bounds, as there are +`hardware limitations `_ +on CUDA block dimensions. + +Warp will automatically fall back to using +`grid-stride loops `_ when +it is not possible for a CUDA thread to process only one element from the Warp grid. +When this happens, some CUDA threads may process more than one element from the Warp grid. +Users can also set the ``max_blocks`` parameter to fine-tune the grid-striding behavior of kernels, even for kernels that are otherwise +able to process one Warp-grid element per CUDA thread. + +Differentiability +----------------- +Please see the :ref:`Limitations and Workarounds ` section in the Differentiability page for auto-differentiation limitations. + +Arrays +------ + +* Arrays can have a maximum of four dimensions. +* Each dimension of a Warp array cannot be greater than the maximum value representable by a 32-bit signed integer, + :math:`2^{31}-1`. +* There are currently no data types that support complex numbers. + +Structs +------- + +* Structs cannot have generic members, i.e. of type ``typing.Any``. + +Volumes +------- + +* The sparse-volume *topology* cannot be changed after the tiles for the :class:`Volume` have been allocated. + +Multiple Processes +------------------ + +* A CUDA context created in the parent process cannot be used in a *forked* child process. + Use the spawn start method instead, or avoid creating CUDA contexts in the parent process. +* There can be issues with using same user kernel cache directory when running with multiple processes. + A workaround is to use a separate cache directory for every process. + See the :ref:`Configuration` section for how the cache directory may be changed. + +Scalar Math Functions +--------------------- + +This section details some limitations and differences from CPython semantics for scalar math functions. + +Modulus Operator +"""""""""""""""" + +Deviation from Python behavior can occur when the modulus operator (``%``) is used with a negative dividend or divisor +(also see :func:`wp.mod() `). +The behavior of the modulus operator in a Warp kernel follows that of C++11: The sign of the result follows the sign of +*dividend*. In Python, the sign of the result follows the sign of the *divisor*: + +.. code-block:: python + + @wp.kernel + def modulus_test(): + # Kernel-scope behavior: + a = -3 % 2 # a is -1 + b = 3 % -2 # b is 1 + c = 3 % 0 # Undefined behavior + + # Python-scope behavior: + a = -3 % 2 # a is 1 + b = 3 % -2 # b is -1 + c = 3 % 0 # ZeroDivisionError + +Power Operator +"""""""""""""" + +The power operator (``**``) in Warp kernels only works on floating-point numbers (also see :func:`wp.pow() `). +In Python, the power operator can also be used on integers. + +Inverse Sine and Cosine +""""""""""""""""""""""" + +:func:`wp.asin() ` and :func:`wp.acos() ` automatically clamp the input to fall in the range [-1, 1]. +In Python, using :external+python:py:func:`math.asin` or :external+python:py:func:`math.acos` +with an input outside [-1, 1] raises a ``ValueError`` exception. + +Rounding +"""""""" + +:func:`wp.round() ` rounds halfway cases away from zero, but Python's +:external+python:py:func:`round` rounds halfway cases to the nearest even +choice (Banker's rounding). Use :func:`wp.rint() ` when Banker's rounding is +desired. Unlike Python, the return type in Warp of both of these rounding +functions is the same type as the input: + +.. code-block:: python + + @wp.kernel + def halfway_rounding_test(): + # Kernel-scope behavior: + a = wp.round(0.5) # a is 1.0 + b = wp.rint(0.5) # b is 0.0 + c = wp.round(1.5) # c is 2.0 + d = wp.rint(1.5) # d is 2.0 + + # Python-scope behavior: + a = round(0.5) # a is 0 + c = round(1.5) # c is 2 diff --git a/_sources/modules/allocators.rst.txt b/_sources/modules/allocators.rst.txt new file mode 100644 index 00000000..bedd2845 --- /dev/null +++ b/_sources/modules/allocators.rst.txt @@ -0,0 +1,262 @@ +Allocators +========== + +.. _mempool_allocators: + +Stream-Ordered Memory Pool Allocators +------------------------------------- + +Introduction +~~~~~~~~~~~~ + +Warp 0.14.0 added support for `stream-ordered memory pool allocators for CUDA arrays `_. As of Warp 0.15.0, these allocators are enabled by default on +all CUDA devices that support them. "Stream-ordered memory pool allocator" is quite a mouthful, so let's unpack it one bit at a time. + +Whenever you create an array, the memory needs to be allocated on the device: + +.. code:: python + + a = wp.empty(n, dtype=float, device="cuda:0") + b = wp.zeros(n, dtype=float, device="cuda:0") + c = wp.ones(n, dtype=float, device="cuda:0") + d = wp.full(n, 42.0, dtype=float, device="cuda:0") + +Each of the calls above allocates a block of device memory large enough to hold the array and optionally initializes the contents with +the specified values. ``wp.empty()`` is the only function that does not initialize the contents in any way, it just allocates the memory. + +Memory pool allocators grab a block of memory from a larger pool of reserved memory, which is generally faster than asking +the operating system for a brand new chunk of storage. This is an important benefit of these pooled allocators - they are faster. + +Stream-ordered means that each allocation is scheduled on a :ref:`CUDA stream`, which represents a sequence of instructions that execute in order on the GPU. The main benefit is that it allows memory to be allocated in CUDA graphs, which was previously not possible: + +.. code:: python + + with wp.ScopedCapture() as capture: + a = wp.zeros(n, dtype=float) + wp.launch(kernel, dim=a.size, inputs=[a]) + + wp.capture_launch(capture.graph) + +From now on, we will refer to these allocators as mempool allocators, for short. + + +Configuration +~~~~~~~~~~~~~ + +Mempool allocators are a feature of CUDA that is supported on most modern devices and operating systems. However, +there can be systems where they are not supported, such as certain virtual machine setups. Warp is designed with resiliency in mind, +so existing code written prior to the introduction of these new allocators should continue to function regardless of whether they +are supported by the underlying system or not. + +Warp's startup message gives the status of these allocators, for example: + +.. code-block:: text + + Warp 0.15.1 initialized: + CUDA Toolkit 11.5, Driver 12.2 + Devices: + "cpu" : "x86_64" + "cuda:0" : "NVIDIA GeForce RTX 4090" (24 GiB, sm_89, mempool enabled) + "cuda:1" : "NVIDIA GeForce RTX 3090" (24 GiB, sm_86, mempool enabled) + +Note the ``mempool enabled`` text next to each CUDA device. This means that memory pools are enabled on the device. Whenever you create +an array on that device, it will be allocated using the mempool allocator. If you see ``mempool supported``, it means that memory +pools are supported but were not enabled on startup. If you see ``mempool not supported``, it means that memory pools can't be used +on this device. + +There is a configuration flag that controls whether memory pools should be automatically enabled during ``wp.init()``: + +.. code:: python + + import warp as wp + + wp.config.enable_mempools_at_init = False + + wp.init() + +The flag defaults to ``True``, but can be set to ``False`` if desired. Changing this configuration flag after ``wp.init()`` is called has no effect. + +After ``wp.init()``, you can check if the memory pool is enabled on each device like this: + +.. code:: python + + if wp.is_mempool_enabled("cuda:0"): + ... + +You can also independently control enablement on each device: + +.. code:: python + + if wp.is_mempool_supported("cuda:0"): + wp.set_mempool_enabled("cuda:0", True) + +It's possible to temporarily enable or disable memory pools using a scoped manager: + +.. code:: python + + with wp.ScopedMempool("cuda:0", True): + a = wp.zeros(n, dtype=float, device="cuda:0") + + with wp.ScopedMempool("cuda:0", False): + b = wp.zeros(n, dtype=float, device="cuda:0") + +In the snippet above, array ``a`` will be allocated using the mempool allocator and array ``b`` will be allocated using the default allocator. + +In most cases, it shouldn't be necessary to fiddle with these enablement functions, but they are there if you need them. +By default, Warp will enable memory pools on startup if they are supported, which will bring the benefits of improved allocation speed automatically. +Most Warp code should continue to function with or without mempool allocators, with the exception of memory allocations +during graph capture, which will raise an exception if memory pools are not enabled. + +.. autofunction:: warp.is_mempool_supported +.. autofunction:: warp.is_mempool_enabled +.. autofunction:: warp.set_mempool_enabled + + +Allocation Performance +~~~~~~~~~~~~~~~~~~~~~~ + +Allocating and releasing memory are rather expensive operations that can add overhead to a program. We can't avoid them, since we need to allocate storage for our data somewhere, but there are some simple strategies that can reduce the overall impact of allocations on performance. + +Consider the following example: + +.. code:: python + + for i in range(100): + a = wp.zeros(n, dtype=float, device="cuda:0") + wp.launch(kernel, dim=a.size, inputs=[a], device="cuda:0") + +On each iteration of the loop, we allocate an array and run a kernel on the data. This program has 100 allocations and 100 deallocations. When we assign a new value to ``a``, the previous value gets garbage collected by Python, which triggers the deallocation. + +Reusing Memory +^^^^^^^^^^^^^^ + +If the size of the array remains fixed, consider reusing the memory on subsequent iterations. We can allocate the array only once and just re-initialize its contents on each iteration: + +.. code:: python + + # pre-allocate the array + a = wp.empty(n, dtype=float, device="cuda:0") + for i in range(100): + # reset the contents + a.zero_() + wp.launch(kernel, dim=a.size, inputs=[a], device="cuda:0") + +This works well if the array size does not change on each iteration. If the size changes but the upper bound is known, we can still pre-allocate a buffer large enough to store all the elements at any iteration. + +.. code:: python + + # pre-allocate a big enough buffer + buffer = wp.empty(MAX_N, dtype=float, device="cuda:0") + for i in range(100): + # get a buffer slice of size n <= MAX_N + n = get_size(i) + a = buffer[:n] + # reset the contents + a.zero_() + wp.launch(kernel, dim=a.size, inputs=[a], device="cuda:0") + +Reusing memory this way can improve performance, but may also add undesirable complexity to our code. The mempool allocators have a useful feature that can improve allocation performance without modifying our original code in any way. + +Release Threshold +^^^^^^^^^^^^^^^^^ + +The memory pool release threshold determines how much reserved memory the allocator should hold on to before releasing it back to the operating system. For programs that frequently allocate and release memory, setting a higher release threshold can improve the performance of allocations. + +By default, the release threshold is set to 0. Setting it to a higher number will reduce the cost of allocations if memory was previously acquired and returned to the pool. + +.. code:: python + + # set the release threshold to reduce re-allocation overhead + wp.set_mempool_release_threshold("cuda:0", 1024**3) + + for i in range(100): + a = wp.zeros(n, dtype=float, device="cuda:0") + wp.launch(kernel, dim=a.size, inputs=[a], device="cuda:0") + +Threshold values between 0 and 1 are interpreted as fractions of available memory. For example, 0.5 means half of the device's physical memory and 1.0 means all of the memory. Greater values are interpreted as an absolute number of bytes. For example, 1024**3 means one GiB of memory. + +This is a simple optimization that can improve the performance of programs without modifying the existing code in any way. + +.. autofunction:: warp.set_mempool_release_threshold + +Graph Allocations +~~~~~~~~~~~~~~~~~ + +Mempool allocators can be used in CUDA graphs, which means that you can capture Warp code that creates arrays: + +.. code:: python + + with wp.ScopedCapture() as capture: + a = wp.full(n, 42, dtype=float) + + wp.capture_launch(capture.graph) + + print(a) + +Capturing allocations is similar to capturing other operations like kernel launches or memory copies. During capture, the operations don't actually execute, but are recorded. To execute the captured operations, we must launch the graph using :func:`wp.capture_launch() `. This is important to keep in mind if you want to use an array that was allocated during graph capture. The array doesn't actually exist until the captured graph is launched. In the snippet above, we would get an error if we tried to print the array before calling :func:`wp.capture_launch() `. + +More generally, the ability to allocate memory during graph capture greatly increases the range of code that can be captured in a graph. This includes any code that creates temporary allocations. CUDA graphs can be used to re-run operations with minimal CPU overhead, which can yield dramatic performance improvements. + +.. _mempool_access: + +Memory Pool Access +~~~~~~~~~~~~~~~~~~ + +On multi-GPU systems that support :ref:`peer access`, we can enable directly accessing a memory pool from a different device: + +.. code:: python + + if wp.is_mempool_access_supported("cuda:0", "cuda:1"): + wp.set_mempool_access_enabled("cuda:0", "cuda:1", True): + +This will allow the memory pool of device ``cuda:0`` to be directly accessed on device ``cuda:1``. Memory pool access is directional, which means that enabling access to ``cuda:0`` from ``cuda:1`` does not automatically enable access to ``cuda:1`` from ``cuda:0``. + +The benefit of enabling memory pool access is that it allows direct memory transfers (DMA) between the devices. This is generally a faster way to copy data, since otherwise the transfer needs to be done using a CPU staging buffer. + +The drawback is that enabling memory pool access can slightly reduce the performance of allocations and deallocations. However, for applications that rely on copying memory between devices, there should be a net benefit. + +It's possible to temporarily enable or disable memory pool access using a scoped manager: + +.. code:: python + + with wp.ScopedMempoolAccess("cuda:0", "cuda:1", True): + a0 = wp.zeros(n, dtype=float, device="cuda:0") + a1 = wp.empty(n, dtype=float, device="cuda:1") + + # use direct memory transfer between GPUs + wp.copy(a1, a0) + +Note that memory pool access only applies to memory allocated using mempool allocators. For memory allocated using default CUDA allocators, we can enable CUDA peer access to get similar benefits. + +Because enabling memory pool access can have drawbacks, Warp does not automatically enable it, even if it's supported. Programs that don't require copying data between GPUs are therefore not affected in any way. + +.. autofunction:: warp.is_mempool_access_supported +.. autofunction:: warp.is_mempool_access_enabled +.. autofunction:: warp.set_mempool_access_enabled + +Limitations +~~~~~~~~~~~ + +Mempool-to-Mempool Copies Between GPUs During Graph Capture +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Copying data between different GPUs will fail during graph capture if the source and destination are allocated using mempool allocators and mempool access is not enabled between devices. Note that this only applies to capturing mempool-to-mempool copies in a graph; copies done outside of graph capture are not affected. Copies within the same mempool (i.e., same device) are also not affected. + +There are two workarounds. If mempool access is supported, you can simply enable mempool access between the devices prior to graph capture, as shown in :ref:`mempool_access`. + +If mempool access is not supported, you will need to pre-allocate the arrays involved in the copy using the default CUDA allocators. This will need to be done before capture begins: + +.. code:: python + + # pre-allocate the arrays with mempools disabled + with wp.ScopedMempool("cuda:0", False): + a0 = wp.zeros(n, dtype=float, device="cuda:0") + with wp.ScopedMempool("cuda:1", False): + a1 = wp.empty(n, dtype=float, device="cuda:1") + + with wp.ScopedCapture("cuda:1") as capture: + wp.copy(a1, a0) + + wp.capture_launch(capture.graph) + +This is due to a limitation in CUDA, which we envision being fixed in the future. diff --git a/_sources/modules/concurrency.rst.txt b/_sources/modules/concurrency.rst.txt new file mode 100644 index 00000000..356bec8d --- /dev/null +++ b/_sources/modules/concurrency.rst.txt @@ -0,0 +1,590 @@ +Concurrency +=========== + +.. currentmodule:: warp + +Asynchronous Operations +----------------------- + +Kernel Launches +~~~~~~~~~~~~~~~ + +Kernels launched on a CUDA device are asynchronous with respect to the host (CPU Python thread). Launching a kernel schedules +its execution on the CUDA device, but the :func:`wp.launch() ` function can return before the kernel execution +completes. This allows us to run some CPU computations while the CUDA kernel is executing, which is an +easy way to introduce parallelism into our programs. + +.. code:: python + + wp.launch(kernel1, dim=n, inputs=[a], device="cuda:0") + + # do some CPU work while the CUDA kernel is running + do_cpu_work() + +Kernels launched on different CUDA devices can execute concurrently. This can be used to tackle independent sub-tasks in parallel on different GPUs while using the CPU to do other useful work: + +.. code:: python + + # launch concurrent kernels on different devices + wp.launch(kernel1, dim=n, inputs=[a0], device="cuda:0") + wp.launch(kernel2, dim=n, inputs=[a1], device="cuda:1") + + # do CPU work while kernels are running on both GPUs + do_cpu_work() + +Launching kernels on the CPU is currently a synchronous operation. In other words, :func:`wp.launch() ` will return only after the kernel has finished executing on the CPU. To run a CUDA kernel and a CPU kernel concurrently, the CUDA kernel should be launched first: + +.. code:: python + + # schedule a kernel on a CUDA device + wp.launch(kernel1, ..., device="cuda:0") + + # run a kernel on the CPU while the CUDA kernel is running + wp.launch(kernel2, ..., device="cpu") + + +Graph Launches +~~~~~~~~~~~~~~ + +The concurrency rules for CUDA graph launches are similar to CUDA kernel launches, except that graphs are not available on the CPU. + +.. code:: python + + # capture work on cuda:0 in a graph + with wp.ScopedCapture(device="cuda:0") as capture0: + do_gpu0_work() + + # capture work on cuda:1 in a graph + with wp.ScopedCapture(device="cuda:1") as capture1: + do_gpu1_work() + + # launch captured graphs on the respective devices concurrently + wp.capture_launch(capture0.graph) + wp.capture_launch(capture1.graph) + + # do some CPU work while the CUDA graphs are running + do_cpu_work() + + +Array Creation +~~~~~~~~~~~~~~ + +Creating CUDA arrays is also asynchronous with respect to the host. It involves allocating memory on the device +and initializing it, which is done under the hood using a kernel launch or an asynchronous CUDA memset operation. + +.. code:: python + + a0 = wp.zeros(n, dtype=float, device="cuda:0") + b0 = wp.ones(n, dtype=float, device="cuda:0") + + a1 = wp.empty(n, dtype=float, device="cuda:1") + b1 = wp.full(n, 42.0, dtype=float, device="cuda:1") + +In this snippet, arrays ``a0`` and ``b0`` are created on device ``cuda:0`` and arrays ``a1`` and ``b1`` are created +on device ``cuda:1``. The operations on the same device are sequential, but each device executes them independently of the +other device, so they can run concurrently. + + +Array Copying +~~~~~~~~~~~~~ + +Copying arrays between devices can also be asynchronous, but there are some details to be aware of. + +Copying from host memory to a CUDA device and copying from a CUDA device to host memory is asynchronous only if the host array is pinned. +Pinned memory allows the CUDA driver to use direct memory transfers (DMA), which are generally faster and can be done without involving the CPU. +There are a couple of drawbacks to using pinned memory: allocation and deallocation is usually slower and there are system-specific limits +on how much pinned memory can be allocated on the system. For this reason, Warp CPU arrays are not pinned by default. You can request a pinned +allocation by passing the ``pinned=True`` flag when creating a CPU array. This is a good option for arrays that are used to copy data +between host and device, especially if asynchronous transfers are desired. + +.. code:: python + + h = wp.zeros(n, dtype=float, device="cpu") + p = wp.zeros(n, dtype=float, device="cpu", pinned=True) + d = wp.zeros(n, dtype=float, device="cuda:0") + + # host-to-device copy + wp.copy(d, h) # synchronous + wp.copy(d, p) # asynchronous + + # device-to-host copy + wp.copy(h, d) # synchronous + wp.copy(p, d) # asynchronous + + # wait for asynchronous operations to complete + wp.synchronize_device("cuda:0") + +Copying between CUDA arrays on the same device is always asynchronous with respect to the host, since it does not involve the CPU: + +.. code:: python + + a = wp.zeros(n, dtype=float, device="cuda:0") + b = wp.empty(n, dtype=float, device="cuda:0") + + # asynchronous device-to-device copy + wp.copy(a, b) + + # wait for transfer to complete + wp.synchronize_device("cuda:0") + +Copying between CUDA arrays on different devices is also asynchronous with respect to the host. Peer-to-peer transfers require +extra care, because CUDA devices are also asynchronous with respect to each other. When copying an array from one GPU to another, +the destination GPU is used to perform the copy, so we need to ensure that prior work on the source GPU completes before the transfer. + +.. code:: python + + a0 = wp.zeros(n, dtype=float, device="cuda:0") + a1 = wp.empty(n, dtype=float, device="cuda:1") + + # wait for outstanding work on the source device to complete to ensure the source array is ready + wp.synchronize_device("cuda:0") + + # asynchronous peer-to-peer copy + wp.copy(a1, a0) + + # wait for the copy to complete on the destination device + wp.synchronize_device("cuda:1") + +Note that peer-to-peer transfers can be accelerated using :ref:`memory pool access ` or :ref:`peer access `, which enables DMA transfers between CUDA devices on supported systems. + +.. _streams: + +Streams +------- + +A CUDA stream is a sequence of operations that execute in order on the GPU. Operations from different streams may run concurrently +and may be interleaved by the device scheduler. + +Warp automatically creates a stream for each CUDA device during initialization. This becomes the current stream for the device. +All kernel launches and memory operations issued on that device are placed on the current stream. + +Creating Streams +~~~~~~~~~~~~~~~~ + +A stream is tied to a particular CUDA device. New streams can be created using the :class:`wp.Stream ` constructor: + +.. code:: python + + s1 = wp.Stream("cuda:0") # create a stream on a specific CUDA device + s2 = wp.Stream() # create a stream on the default device + +If the device parameter is omitted, the default device will be used, which can be managed using :class:`wp.ScopedDevice `. + +For interoperation with external code, it is possible to pass a CUDA stream handle to wrap an external stream: + +.. code:: python + + s3 = wp.Stream("cuda:0", cuda_stream=stream_handle) + +The ``cuda_stream`` argument must be a native stream handle (``cudaStream_t`` or ``CUstream``) passed as a Python integer. +This mechanism is used internally for sharing streams with external frameworks like PyTorch or DLPack. The caller is responsible for ensuring +that the external stream does not get destroyed while it is referenced by a ``wp.Stream`` object. + +Using Streams +~~~~~~~~~~~~~ + +Use :class:`wp.ScopedStream ` to temporarily change the current stream on a device and schedule a sequence of operations on that stream: + +.. code:: python + + stream = wp.Stream("cuda:0") + + with wp.ScopedStream(stream): + a = wp.zeros(n, dtype=float) + b = wp.empty(n, dtype=float) + wp.launch(kernel, dim=n, inputs=[a]) + wp.copy(b, a) + +Since streams are tied to a particular device, :class:`wp.ScopedStream ` subsumes the functionality of :class:`wp.ScopedDevice `. That's why we don't need to explicitly specify the ``device`` argument to each of the calls. + +An important benefit of streams is that they can be used to overlap compute and data transfer operations on the same device, +which can improve the overall throughput of a program by doing those operations in parallel. + +.. code:: python + + with wp.ScopedDevice("cuda:0"): + a = wp.zeros(n, dtype=float) + b = wp.empty(n, dtype=float) + c = wp.ones(n, dtype=float, device="cpu", pinned=True) + + compute_stream = wp.Stream() + transfer_stream = wp.Stream() + + # asynchronous kernel launch on a stream + with wp.ScopedStream(compute_stream) + wp.launch(kernel, dim=a.size, inputs=[a]) + + # asynchronous host-to-device copy on another stream + with wp.ScopedStream(transfer_stream) + wp.copy(b, c) + +The :func:`wp.get_stream() ` function can be used to get the current stream on a device: + +.. code:: python + + s1 = wp.get_stream("cuda:0") # get the current stream on a specific device + s2 = wp.get_stream() # get the current stream on the default device + +The :func:`wp.set_stream() ` function can be used to set the current stream on a device: + +.. code:: python + + wp.set_stream(stream, device="cuda:0") # set the stream on a specific device + wp.set_stream(stream) # set the stream on the default device + +In general, we recommend using :class:`wp.ScopedStream ` rather than :func:`wp.set_stream() `. + +Synchronization +~~~~~~~~~~~~~~~ + +The :func:`wp.synchronize_stream() ` function can be used to block the host thread until the given stream completes: + +.. code:: python + + wp.synchronize_stream(stream) + +In a program that uses multiple streams, this gives a more fine-grained level of control over synchronization behavior +than :func:`wp.synchronize_device() `, which synchronizes all streams on the device. +For example, if a program has multiple compute and transfer streams, the host might only want to wait for one transfer stream +to complete, without waiting for the other streams. By synchronizing only one stream, we allow the others to continue running +concurrently with the host thread. + +.. _cuda_events: + +Events +~~~~~~ + +Functions like :func:`wp.synchronize_device() ` or :func:`wp.synchronize_stream() ` block the CPU thread until work completes on a CUDA device, but they're not intended to synchronize multiple CUDA streams with each other. + +CUDA events provide a mechanism for device-side synchronization between streams. +This kind of synchronization does not block the host thread, but it allows one stream to wait for work on another stream +to complete. + +Like streams, events are tied to a particular device: + +.. code:: python + + e1 = wp.Event("cuda:0") # create an event on a specific CUDA device + e2 = wp.Event() # create an event on the default device + +To wait for a stream to complete some work, we first record the event on that stream. Then we make another stream +wait on that event: + +.. code:: python + + stream1 = wp.Stream("cuda:0") + stream2 = wp.Stream("cuda:0") + event = wp.Event("cuda:0") + + stream1.record_event(event) + stream2.wait_event(event) + +Note that when recording events, the event must be from the same device as the recording stream. +When waiting for events, the waiting stream can be from another device. This allows using events to synchronize streams +on different GPUs. + +If the ``record_event()`` method is called without an event argument, a temporary event will be created, recorded, and returned: + +.. code:: python + + event = stream1.record_event() + stream2.wait_event(event) + +The ``wait_stream()`` method combines the acts of recording and waiting on an event in one call: + +.. code:: python + + stream2.wait_stream(stream1) + +Warp also provides global functions :func:`wp.record_event() `, :func:`wp.wait_event() `, and :func:`wp.wait_stream() ` which operate on the current +stream of the default device: + +.. code:: python + + wp.record_event(event) # record an event on the current stream + wp.wait_event(event) # make the current stream wait for an event + wp.wait_stream(stream) # make the current stream wait for another stream + +These variants are convenient to use inside of :class:`wp.ScopedStream ` and :class:`wp.ScopedDevice ` managers. + +Here is a more complete example with a producer stream that copies data into an array and a consumer stream +that uses the array in a kernel: + +.. code:: python + + with wp.ScopedDevice("cuda:0"): + a = wp.empty(n, dtype=float) + b = wp.ones(n, dtype=float, device="cpu", pinned=True) + + producer_stream = wp.Stream() + consumer_stream = wp.Stream() + + with wp.ScopedStream(producer_stream) + # asynchronous host-to-device copy + wp.copy(a, b) + + # record an event to create a synchronization point for the consumer stream + event = wp.record_event() + + # do some unrelated work in the producer stream + do_other_producer_work() + + with wp.ScopedStream(consumer_stream) + # do some unrelated work in the consumer stream + do_other_consumer_work() + + # wait for the producer copy to complete + wp.wait_event(event) + + # consume the array in a kernel + wp.launch(kernel, dim=a.size, inputs=[a]) + +The function :func:`wp.synchronize_event() ` can be used to block the host thread until a recorded event completes. This is useful when the host wants to wait for a specific synchronization point on a stream, while allowing subsequent stream operations to continue executing asynchronously. + +.. code:: python + + with wp.ScopedDevice("cpu"): + # CPU buffers for readback + a_host = wp.empty(N, dtype=float, pinned=True) + b_host = wp.empty(N, dtype=float, pinned=True) + + with wp.ScopedDevice("cuda:0"): + stream = wp.get_stream() + + # initialize first GPU array + a = wp.full(N, 17, dtype=float) + # asynchronous readback + wp.copy(a_host, a) + # record event + a_event = stream.record_event() + + # initialize second GPU array + b = wp.full(N, 42, dtype=float) + # asynchronous readback + wp.copy(b_host, b) + # record event + b_event = stream.record_event() + + # wait for first array readback to complete + wp.synchronize_event(a_event) + # process first array on the CPU + assert np.array_equal(a_host.numpy(), np.full(N, fill_value=17.0)) + + # wait for second array readback to complete + wp.synchronize_event(b_event) + # process second array on the CPU + assert np.array_equal(b_host.numpy(), np.full(N, fill_value=42.0)) + + +CUDA Default Stream +~~~~~~~~~~~~~~~~~~~ + +Warp avoids using the synchronous CUDA default stream, which is a special stream that synchronizes with all other streams +on the same device. This stream is currently only used during readback operations that are provided for convenience, such as ``array.numpy()`` and ``array.list()``. + +.. code:: python + + stream1 = wp.Stream("cuda:0") + stream2 = wp.Stream("cuda:0") + + with wp.ScopedStream(stream1): + a = wp.zeros(n, dtype=float) + + with wp.ScopedStream(stream2): + b = wp.ones(n, dtype=float) + + print(a) + print(b) + +In the snippet above, there are two arrays that are initialized on different CUDA streams. Printing those arrays triggers +a readback, which is done using the ``array.numpy()`` method. This readback happens on the synchronous CUDA default stream, +which means that no explicit synchronization is required. The reason for this is convenience - printing an array is useful +for debugging purposes, so it's nice not to worry about synchronization. + +The drawback of this approach is that the CUDA default stream (and any methods that use it) cannot be used during graph capture. +The regular :func:`wp.copy() ` function should be used to capture readback operations in a graph. + + +Explicit Streams Arguments +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Several Warp functions accept optional ``stream`` arguments. This allows directly specifying the stream without +using a :class:`wp.ScopedStream ` manager. There are benefits and drawbacks to both approaches, which will be discussed below. +Functions that accept stream arguments directly include :func:`wp.launch() `, :func:`wp.capture_launch() `, and :func:`wp.copy() `. + +To launch a kernel on a specific stream: + +.. code:: python + + wp.launch(kernel, dim=n, inputs=[...], stream=my_stream) + +When launching a kernel with an explicit ``stream`` argument, the ``device`` argument should be omitted, since the device is inferred +from the stream. If both ``stream`` and ``device`` are specified, the ``stream`` argument takes precedence. + +To launch a graph on a specific stream: + +.. code:: python + + wp.capture_launch(graph, stream=my_stream) + +For both kernel and graph launches, specifying the stream directly can be faster than using :class:`wp.ScopedStream `. +While :class:`wp.ScopedStream ` is useful for scheduling a sequence of operations on a specific stream, there is some overhead +in setting and restoring the current stream on the device. This overhead is negligible for larger workloads, +but performance-sensitive code may benefit from specifying the stream directly instead of using :class:`wp.ScopedStream `, especially +for a single kernel or graph launch. + +In addition to these performance considerations, specifying the stream directly can be useful when copying arrays between +two CUDA devices. By default, Warp uses the following rules to determine which stream will be used for the copy: + +- If the destination array is on a CUDA device, use the current stream on the destination device. +- Otherwise, if the source array is on a CUDA device, use the current stream on the source device. + +In the case of peer-to-peer copies, specifying the ``stream`` argument allows overriding these rules, and the copy can +be performed on a stream from any device. + +.. code:: python + + stream0 = wp.get_stream("cuda:0") + stream1 = wp.get_stream("cuda:1") + + a0 = wp.zeros(n, dtype=float, device="cuda:0") + a1 = wp.empty(n, dtype=float, device="cuda:1") + + # wait for the destination array to be ready + stream0.wait_stream(stream1) + + # use the source device stream to do the copy + wp.copy(a1, a0, stream=stream0) + +Notice that we use event synchronization to make the source stream wait for the destination stream prior to the copy. +This is due to the :ref:`stream-ordered memory pool allocators` introduced in Warp 0.14.0. The allocation of the +empty array ``a1`` is scheduled on stream ``stream1``. To avoid use-before-alloc errors, we need to wait until the +allocation completes before using that array on a different stream. + + +Stream Usage Guidance +~~~~~~~~~~~~~~~~~~~~~ + +Stream synchronization can be a tricky business, even for experienced CUDA developers. Consider the following code: + +.. code:: python + + a = wp.zeros(n, dtype=float, device="cuda:0") + + s = wp.Stream("cuda:0") + + wp.launch(kernel, dim=a.size, inputs=[a], stream=s) + +This snippet has a stream synchronization problem that is difficult to detect at first glance. +It's quite possible that the code will work just fine, but it introduces undefined behaviour, +which may lead to incorrect results that manifest only once in a while. The issue is that the kernel is launched +on stream ``s``, which is different than the stream used for creating array ``a``. The array is allocated and +initialized on the current stream of device ``cuda:0``, which means that it might not be ready when stream ``s`` +begins executing the kernel that consumes the array. + +The solution is to synchronize the streams, which can be done like this: + +.. code:: python + + a = wp.zeros(n, dtype=float, device="cuda:0") + + s = wp.Stream("cuda:0") + + # wait for the current stream on cuda:0 to finish initializing the array + s.wait_stream(wp.get_stream("cuda:0")) + + wp.launch(kernel, dim=a.size, inputs=[a], stream=s) + +The :class:`wp.ScopedStream ` manager is designed to alleviate this common problem. It synchronizes the new stream with the +previous stream on the device. Its behavior is equivalent to inserting the ``wait_stream()`` call as shown above. +With :class:`wp.ScopedStream `, we don't need to explicitly sync the new stream with the previous stream: + +.. code:: python + + a = wp.zeros(n, dtype=float, device="cuda:0") + + s = wp.Stream("cuda:0") + + with wp.ScopedStream(s): + wp.launch(kernel, dim=a.size, inputs=[a]) + +This makes :class:`wp.ScopedStream ` the recommended way of getting started with streams in Warp. Using explicit stream arguments +might be slightly more performant, but it requires more attention to stream synchronization mechanics. +If you are a stream novice, consider the following trajectory for integrating streams into your Warp programs: + +- Level 1: Don't. You don't need to use streams to use Warp. Avoiding streams is a perfectly valid and respectable way to live. Many interesting and sophisticated algorithms can be developed without fancy stream juggling. Often it's better to focus on solving a problem in a simple and elegant way, unencumbered by the vagaries of low-level stream management. +- Level 2: Use :class:`wp.ScopedStream `. It helps to avoid some common hard-to-catch issues. There's a little bit of overhead, but it should be negligible if the GPU workloads are large enough. Consider adding streams into your program as a form of targeted optimization, especially if some areas like memory transfers ("feeding the beast") are a known bottleneck. Streams are great for overlapping memory transfers with compute workloads. +- Level 3: Use explicit stream arguments for kernel launches, array copying, etc. This will be the most performant approach that can get you close to the speed of light. You will need to take care of all stream synchronization yourself, but the results can be rewarding in the benchmarks. + +.. _synchronization_guidance: + +Synchronization Guidance +------------------------ + +The general rule with synchronization is to use as little of it as possible, but not less. + +Excessive synchronization can severely limit the performance of programs. Synchronization means that a stream or thread +is waiting for something else to complete. While it's waiting, it's not doing any useful work, which means that any +outstanding work cannot start until the synchronization point is reached. This limits parallel execution, which is +often important for squeezing the most juice out of the collection of hardware components. + +On the other hand, insufficient synchronization can lead to errors or incorrect results if operations execute out-of-order. +A fast program is no good if it can't guarantee correct results. + +Host-side Synchronization +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Host-side synchronization blocks the host thread (Python) until GPU work completes. This is necessary when +you are waiting for some GPU work to complete so that you can access the results on the CPU. + +:func:`wp.synchronize() ` is the most heavy-handed synchronization function, since it synchronizes all the devices in the system. It is almost never the right function to call if performance is important. However, it can sometimes be useful when debugging synchronization-related issues. + +:func:`wp.synchronize_device(device) ` synchronizes a single device, which is generally better and faster. This synchronizes all the streams on the specified device, including streams created by Warp and those created by any other framework. + +:func:`wp.synchronize_stream(stream) ` synchronizes a single stream, which is better still. If the program uses multiple streams, you can wait for a specific one to finish without waiting for the others. This is handy if you have a readback stream that is copying data from the GPU to the CPU. You can wait for the transfer to complete and start processing it on the CPU while other streams are still chugging along on the GPU, in parallel with the host code. + +:func:`wp.synchronize_event(event) ` is the most specific host synchronization function. It blocks the host until an event previously recorded on a CUDA stream completes. This can be used to wait for a specific stream synchronization point to be reached, while allowing subsequent operations on that stream to continue asynchronously. + +Device-side Synchronization +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Device-side synchronization uses CUDA events to make one stream wait for a synchronization point recorded on another stream (:func:`wp.record_event() `, :func:`wp.wait_event() `, :func:`wp.wait_stream() `). + +These functions don't block the host thread, so the CPU can stay busy doing useful work, like preparing the next batch of data +to feed the beast. Events can be used to synchronize streams on the same device or even different CUDA devices, so you can +choreograph very sophisticated multi-stream and multi-device workloads that execute entirely on the available GPUs. +This allows keeping host-side synchronization to a minimum, perhaps only when reading back the final results. + +Synchronization and Graph Capture +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A CUDA graph captures a sequence of operations on a CUDA stream that can be replayed multiple times with low overhead. +During capture, certain CUDA functions are not allowed, which includes host-side synchronization functions. Using the synchronous +CUDA default stream is also not allowed. The only form of synchronization allowed in CUDA graphs is event-based synchronization. + +A CUDA graph capture must start and end on the same stream, but multiple streams can be used in the middle. This allows CUDA graphs to encompass multiple streams and even multiple GPUs. Events play a crucial role with multi-stream graph capture because they are used to fork and join new streams to the main capture stream, in addition to their regular synchronization duties. + +Here's an example of capturing a multi-GPU graph using a stream on each device: + +.. code:: python + + stream0 = wp.Stream("cuda:0") + stream1 = wp.Stream("cuda:1") + + # use stream0 as the main capture stream + with wp.ScopedCapture(stream=stream0) as capture: + + # fork stream1, which adds it to the set of streams being captured + stream1.wait_stream(stream0) + + # launch a kernel on stream0 + wp.launch(kernel, ..., stream=stream0) + + # launch a kernel on stream1 + wp.launch(kernel, ..., stream=stream1) + + # join stream1 + stream0.wait_stream(stream1) + + # launch the multi-GPU graph, which can execute the captured kernels concurrently + wp.capture_launch(capture.graph) diff --git a/_sources/modules/devices.rst.txt b/_sources/modules/devices.rst.txt new file mode 100644 index 00000000..856bbdaa --- /dev/null +++ b/_sources/modules/devices.rst.txt @@ -0,0 +1,254 @@ +Devices +======= + +Warp assigns unique string aliases to all supported compute devices in the system. There is currently a single CPU device exposed as ``"cpu"``. Each CUDA-capable GPU gets an alias of the form ``"cuda:i"``, where ``i`` is the CUDA device ordinal. This convention should be familiar to users of other popular frameworks like PyTorch. + +It is possible to explicitly target a specific device with each Warp API call using the ``device`` argument:: + + a = wp.zeros(n, device="cpu") + wp.launch(kernel, dim=a.size, inputs=[a], device="cpu") + + b = wp.zeros(n, device="cuda:0") + wp.launch(kernel, dim=b.size, inputs=[b], device="cuda:0") + + c = wp.zeros(n, device="cuda:1") + wp.launch(kernel, dim=c.size, inputs=[c], device="cuda:1") + +.. note:: + + A Warp CUDA device (``"cuda:i"``) corresponds to the primary CUDA context of device ``i``. + This is compatible with frameworks like PyTorch and other software that uses the CUDA Runtime API. + It makes interoperability easy because GPU resources like memory can be shared with Warp. + +.. autoclass:: warp.context.Device + :members: + :exclude-members: init_streams + +Default Device +-------------- + +To simplify writing code, Warp has the concept of **default device**. When the ``device`` argument is omitted from a Warp API call, the default device will be used. + +During Warp initialization, the default device is set to be ``"cuda:0"`` if CUDA is available. Otherwise, the default device is ``"cpu"``. + +The function ``wp.set_device()`` can be used to change the default device:: + + wp.set_device("cpu") + a = wp.zeros(n) + wp.launch(kernel, dim=a.size, inputs=[a]) + + wp.set_device("cuda:0") + b = wp.zeros(n) + wp.launch(kernel, dim=b.size, inputs=[b]) + + wp.set_device("cuda:1") + c = wp.zeros(n) + wp.launch(kernel, dim=c.size, inputs=[c]) + +.. note:: + + For CUDA devices, ``wp.set_device()`` does two things: it sets the Warp default device and it makes the device's CUDA context current. This helps to minimize the number of CUDA context switches in blocks of code targeting a single device. + +For PyTorch users, this function is similar to ``torch.cuda.set_device()``. It is still possible to specify a different device in individual API calls, like in this snippet:: + + # set default device + wp.set_device("cuda:0") + + # use default device + a = wp.zeros(n) + + # use explicit devices + b = wp.empty(n, device="cpu") + c = wp.empty(n, device="cuda:1") + + # use default device + wp.launch(kernel, dim=a.size, inputs=[a]) + + wp.copy(b, a) + wp.copy(c, a) + +Scoped Devices +-------------- + +Another way to manage the default device is using ``wp.ScopedDevice`` objects. They can be arbitrarily nested and restore the previous default device on exit:: + + with wp.ScopedDevice("cpu"): + # alloc and launch on "cpu" + a = wp.zeros(n) + wp.launch(kernel, dim=a.size, inputs=[a]) + + with wp.ScopedDevice("cuda:0"): + # alloc on "cuda:0" + b = wp.zeros(n) + + with wp.ScopedDevice("cuda:1"): + # alloc and launch on "cuda:1" + c = wp.zeros(n) + wp.launch(kernel, dim=c.size, inputs=[c]) + + # launch on "cuda:0" + wp.launch(kernel, dim=b.size, inputs=[b]) + +.. note:: + + For CUDA devices, ``wp.ScopedDevice`` makes the device's CUDA context current and restores the previous CUDA context on exit. This is handy when running Warp scripts as part of a bigger pipeline, because it avoids any side effects of changing the CUDA context in the enclosed code. + +Example: Using ``wp.ScopedDevice`` with multiple GPUs +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The following example shows how to allocate arrays and launch kernels on all available CUDA devices. + +.. code:: python + + import warp as wp + + + @wp.kernel + def inc(a: wp.array(dtype=float)): + tid = wp.tid() + a[tid] = a[tid] + 1.0 + + + # get all CUDA devices + devices = wp.get_cuda_devices() + device_count = len(devices) + + # number of launches + iters = 1000 + + # list of arrays, one per device + arrs = [] + + # loop over all devices + for device in devices: + # use a ScopedDevice to set the target device + with wp.ScopedDevice(device): + # allocate array + a = wp.zeros(250 * 1024 * 1024, dtype=float) + arrs.append(a) + + # launch kernels + for _ in range(iters): + wp.launch(inc, dim=a.size, inputs=[a]) + + # synchronize all devices + wp.synchronize() + + # print results + for i in range(device_count): + print(f"{arrs[i].device} -> {arrs[i].numpy()}") + + +Current CUDA Device +------------------- + +Warp uses the device alias ``"cuda"`` to target the current CUDA device. This allows external code to manage the CUDA device on which to execute Warp scripts. It is analogous to the PyTorch ``"cuda"`` device, which should be familiar to Torch users and simplify interoperation. + +In this snippet, we use PyTorch to manage the current CUDA device and invoke a Warp kernel on that device:: + + def example_function(): + # create a Torch tensor on the current CUDA device + t = torch.arange(10, dtype=torch.float32, device="cuda") + + a = wp.from_torch(t) + + # launch a Warp kernel on the current CUDA device + wp.launch(kernel, dim=a.size, inputs=[a], device="cuda") + + # use Torch to set the current CUDA device and run example_function() on that device + torch.cuda.set_device(0) + example_function() + + # use Torch to change the current CUDA device and re-run example_function() on that device + torch.cuda.set_device(1) + example_function() + +.. note:: + + Using the device alias ``"cuda"`` can be problematic if the code runs in an environment where another part of the code can unpredictably change the CUDA context. Using an explicit CUDA device like ``"cuda:i"`` is recommended to avoid such issues. + +Device Synchronization +---------------------- + +CUDA kernel launches and memory operations can execute asynchronously. This allows for overlapping compute and memory operations on different devices. Warp allows synchronizing the host with outstanding asynchronous operations on a specific device:: + + wp.synchronize_device("cuda:1") + +The ``wp.synchronize_device()`` function offers more fine-grained synchronization than ``wp.synchronize()``, as the latter waits for *all* devices to complete their work. + +Custom CUDA Contexts +-------------------- + +Warp is designed to work with arbitrary CUDA contexts so it can easily integrate into different workflows. + +Applications built on the CUDA Runtime API target the *primary context* of each device. The Runtime API hides CUDA context management under the hood. In Warp, device ``"cuda:i"`` represents the primary context of device ``i``, which aligns with the CUDA Runtime API. + +Applications built on the CUDA Driver API work with CUDA contexts directly and can create custom CUDA contexts on any device. Custom CUDA contexts can be created with specific affinity or interop features that benefit the application. Warp can work with these CUDA contexts as well. + +The special device alias ``"cuda"`` can be used to target the current CUDA context, whether this is a primary or custom context. + +In addition, Warp allows registering new device aliases for custom CUDA contexts, so that they can be explicitly targeted by name. If the ``CUcontext`` pointer is available, it can be used to create a new device alias like this:: + + wp.map_cuda_device("foo", ctypes.c_void_p(context_ptr)) + +Alternatively, if the custom CUDA context was made current by the application, the pointer can be omitted:: + + wp.map_cuda_device("foo") + +In either case, mapping the custom CUDA context allows us to target the context directly using the assigned alias:: + + with wp.ScopedDevice("foo"): + a = wp.zeros(n) + wp.launch(kernel, dim=a.size, inputs=[a]) + +.. _peer_access: + +CUDA Peer Access +---------------- + +CUDA allows direct memory access between different GPUs if the system hardware configuration supports it. Typically, the GPUs should be of the same type and a special interconnect may be required (e.g., NVLINK or PCIe topology). + +During initialization, Warp reports whether peer access is supported on multi-GPU systems: + +.. code:: text + + Warp 0.15.1 initialized: + CUDA Toolkit 11.5, Driver 12.2 + Devices: + "cpu" : "x86_64" + "cuda:0" : "NVIDIA L40" (48 GiB, sm_89, mempool enabled) + "cuda:1" : "NVIDIA L40" (48 GiB, sm_89, mempool enabled) + "cuda:2" : "NVIDIA L40" (48 GiB, sm_89, mempool enabled) + "cuda:3" : "NVIDIA L40" (48 GiB, sm_89, mempool enabled) + CUDA peer access: + Supported fully (all-directional) + +If the message reports that CUDA peer access is ``Supported fully``, it means that every CUDA device can access every other CUDA device in the system. If it says ``Supported partially``, it will be followed by the access matrix that shows which devices can access each other. If it says ``Not supported``, it means that access is not supported between any devices. + +In code, we can check support and enable peer access like this: + +.. code:: python + + if wp.is_peer_access_supported("cuda:0", "cuda:1"): + wp.set_peer_access_enabled("cuda:0", "cuda:1", True): + +This will allow the memory of device ``cuda:0`` to be directly accessed on device ``cuda:1``. Peer access is directional, which means that enabling access to ``cuda:0`` from ``cuda:1`` does not automatically enable access to ``cuda:1`` from ``cuda:0``. + +The benefit of enabling peer access is that it allows direct memory transfers (DMA) between the devices. This is generally a faster way to copy data, since otherwise the transfer needs to be done using a CPU staging buffer. + +The drawback is that enabling peer access can reduce the performance of allocations and deallocations. Programs that don't rely on peer-to-peer memory transfers should leave this setting disabled. + +It's possible to temporarily enable or disable peer access using a scoped manager: + +.. code:: python + + with wp.ScopedPeerAccess("cuda:0", "cuda:1", True): + ... + +.. note:: + + Peer access does not accelerate memory transfers between arrays allocated using the :ref:`stream-ordered memory pool allocators` introduced in Warp 0.14.0. To accelerate memory pool transfers, :ref:`memory pool access` should be enabled instead. + +.. autofunction:: warp.is_peer_access_supported +.. autofunction:: warp.is_peer_access_enabled +.. autofunction:: warp.set_peer_access_enabled diff --git a/_sources/modules/differentiability.rst.txt b/_sources/modules/differentiability.rst.txt new file mode 100644 index 00000000..f5c9bc09 --- /dev/null +++ b/_sources/modules/differentiability.rst.txt @@ -0,0 +1,1064 @@ +Differentiability +================= + +.. currentmodule:: warp + +By default, Warp generates a forward and backward (adjoint) version of each kernel definition. The backward version of a kernel can be used +to compute gradients of loss functions that can be back propagated to machine learning frameworks like PyTorch. + +Arrays that participate in the chain of computation which require gradients should be created with ``requires_grad=True``, for example:: + + a = wp.zeros(1024, dtype=wp.vec3, device="cuda", requires_grad=True) + +The ``wp.Tape`` class can then be used to record kernel launches, and replay them to compute the gradient of a scalar loss function with respect to the kernel inputs:: + + tape = wp.Tape() + + # forward pass + with tape: + wp.launch(kernel=compute1, inputs=[a, b], device="cuda") + wp.launch(kernel=compute2, inputs=[c, d], device="cuda") + wp.launch(kernel=loss, inputs=[d, l], device="cuda") + + # reverse pass + tape.backward(l) + +After the backward pass has completed, the gradients with respect to the inputs are available from the ``array.grad`` attribute:: + + # gradient of loss with respect to input a + print(a.grad) + +Note that gradients are accumulated on the participating buffers, so if you wish to reuse the same buffers for multiple backward passes you should first zero the gradients:: + + tape.zero() + +.. autoclass:: Tape + :members: + +Copying is Differentiable +######################### + +``wp.copy()``, ``wp.clone()``, and ``array.assign()`` are differentiable functions and can participate in the computation graph recorded on the tape. Consider the following examples and their +PyTorch equivalents (for comparison): + +``wp.copy()``:: + + @wp.kernel + def double(x: wp.array(dtype=float), y: wp.array(dtype=float)): + tid = wp.tid() + y[tid] = x[tid] * 2.0 + + x = wp.array(np.arange(3), dtype=float, requires_grad=True) + y = wp.zeros_like(x) + z = wp.zeros_like(x) + + tape = wp.Tape() + with tape: + wp.launch(double, dim=3, inputs=[x, y]) + wp.copy(z, y) + + tape.backward(grads={z: wp.ones_like(x)}) + + print(x.grad) + # [2. 2. 2.] + +Equivalently, in PyTorch:: + + x = torch.tensor(np.arange(3), dtype=torch.float32, requires_grad=True) + y = x * 2 + z = torch.zeros_like(y).copy_(y) + + z.sum().backward() + + print(x.grad) + # tensor([2., 2., 2.]) + +``wp.clone()``:: + + x = wp.array(np.arange(3), dtype=float, requires_grad=True) + y = wp.zeros_like(x) + + tape = wp.Tape() + with tape: + wp.launch(double, dim=3, inputs=[x, y]) + z = wp.clone(y, requires_grad=True) + + tape.backward(grads={z: wp.ones_like(x)}) + + print(x.grad) + # [2. 2. 2.] + +In PyTorch:: + + x = torch.tensor(np.arange(3), dtype=torch.float32, requires_grad=True) + y = x * 2 + z = torch.clone(y) + + z.sum().backward() + print(x.grad) + # tensor([2., 2., 2.]) + +.. note:: In PyTorch, one may clone a tensor x and detach it from the current computation graph by calling + ``x.clone().detach()``. The equivalent in Warp is ``wp.clone(x, requires_grad=False)``. + +``array.assign()``:: + + x = wp.array(np.arange(3), dtype=float, requires_grad=True) + y = wp.zeros_like(x) + z = wp.zeros_like(y) + + tape = wp.Tape() + with tape: + wp.launch(double, dim=3, inputs=[x], outputs=[y]) + z.assign(y) + + tape.backward(grads={z: wp.ones_like(x)}) + + print(x.grad) + # [2. 2. 2.] + +.. note:: ``array.assign()`` is equivalent to ``wp.copy()`` with an additional step that wraps the source array in a Warp array if it is not already a Warp array. + +Jacobians +######### + +To compute the Jacobian matrix :math:`J\in\mathbb{R}^{m\times n}` of a multi-valued function :math:`f: \mathbb{R}^n \to \mathbb{R}^m`, we can evaluate an entire row of the Jacobian in parallel by finding the Jacobian-vector product :math:`J^\top \mathbf{e}`. The vector :math:`\mathbf{e}\in\mathbb{R}^m` selects the indices in the output buffer to differentiate with respect to. +In Warp, instead of passing a scalar loss buffer to the ``tape.backward()`` method, we pass a dictionary ``grads`` mapping from the function output array to the selection vector :math:`\mathbf{e}` having the same type:: + + # compute the Jacobian for a function of single output + jacobian = np.empty((output_dim, input_dim), dtype=np.float32) + + # record computation + tape = wp.Tape() + with tape: + output_buffer = launch_kernels_to_be_differentiated(input_buffer) + + # compute each row of the Jacobian + for output_index in range(output_dim): + + # select which row of the Jacobian we want to compute + select_index = np.zeros(output_dim) + select_index[output_index] = 1.0 + e = wp.array(select_index, dtype=wp.float32) + + # pass input gradients to the output buffer to apply selection + tape.backward(grads={output_buffer: e}) + q_grad_i = tape.gradients[input_buffer] + jacobian[output_index, :] = q_grad_i.numpy() + + # zero gradient arrays for next row + tape.zero() + +When we run simulations independently in parallel, the Jacobian corresponding to the entire system dynamics is a block-diagonal matrix. In this case, we can compute the Jacobian in parallel for all environments by choosing a selection vector that has the output indices active for all environment copies. For example, to get the first rows of the Jacobians of all environments, :math:`\mathbf{e}=[\begin{smallmatrix}1 & 0 & 0 & \dots & 1 & 0 & 0 & \dots\end{smallmatrix}]^\top`, to compute the second rows, :math:`\mathbf{e}=[\begin{smallmatrix}0 & 1 & 0 & \dots & 0 & 1 & 0 & \dots\end{smallmatrix}]^\top`, etc.:: + + # compute the Jacobian for a function over multiple environments in parallel + jacobians = np.empty((num_envs, output_dim, input_dim), dtype=np.float32) + + # record computation + tape = wp.Tape() + with tape: + output_buffer = launch_kernels_to_be_differentiated(input_buffer) + + # compute each row of the Jacobian + for output_index in range(output_dim): + + # select which row of the Jacobian we want to compute + select_index = np.zeros(output_dim) + select_index[output_index] = 1.0 + + # assemble selection vector for all environments (can be precomputed) + e = wp.array(np.tile(select_index, num_envs), dtype=wp.float32) + tape.backward(grads={output_buffer: e}) + q_grad_i = tape.gradients[input_buffer] + jacobians[:, output_index, :] = q_grad_i.numpy().reshape(num_envs, input_dim) + + tape.zero() + + +Custom Gradient Functions +######################### + +Warp supports custom gradient function definitions for user-defined Warp functions. +This allows users to define code that should replace the automatically generated derivatives. + +To differentiate a function :math:`h(x) = f(g(x))` that has a nested call to function :math:`g(x)`, the chain rule is evaluated in the automatic differentiation of :math:`h(x)`: + +.. math:: + + h^\prime(x) = f^\prime({\color{green}{\underset{\textrm{replay}}{g(x)}}}) {\color{blue}{\underset{\textrm{grad}}{g^\prime(x)}}} + +This implies that a function to be compatible with the autodiff engine needs to provide an implementation of its forward version +:math:`\color{green}{g(x)}`, which we refer to as "replay" function (that matches the original function definition by default), +and its derivative :math:`\color{blue}{g^\prime(x)}`, referred to as "grad". + +Both the replay and the grad implementations can be customized by the user. They are defined as follows: + +.. list-table:: Customizing the replay and grad versions of function ``myfunc`` + :widths: 100 + :header-rows: 0 + + * - Forward Function + * - .. code-block:: python + + @wp.func + def myfunc(in1: InType1, ..., inN: InTypeN) -> OutType1, ..., OutTypeM: + return out1, ..., outM + + * - Custom Replay Function + * - .. code-block:: python + + @wp.func_replay(myfunc) + def replay_myfunc(in1: InType1, ..., inN: InTypeN) -> OutType1, ..., OutTypeM: + # Custom forward computations to be executed in the backward pass of a + # function calling `myfunc` go here + # Ensure the output variables match the original forward definition + return out1, ..., outM + + * - Custom Grad Function + * - .. code-block:: python + + @wp.func_grad(myfunc) + def adj_myfunc(in1: InType1, ..., inN: InTypeN, adj_out1: OutType1, ..., adj_outM: OutTypeM): + # Custom adjoint code goes here + # Update the partial derivatives for the inputs as follows: + wp.adjoint[in1] += ... + ... + wp.adjoint[inN] += ... + +.. note:: It is currently not possible to define custom replay or grad functions for functions that + have generic arguments, e.g. ``Any`` or ``wp.array(dtype=Any)``. Replay or grad functions that + themselves use generic arguments are also not yet supported. + +Example 1: Custom Grad Function +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In the following, we define a Warp function ``safe_sqrt`` that computes the square root of a number:: + + @wp.func + def safe_sqrt(x: float): + return wp.sqrt(x) + +To evaluate this function, we define a kernel that applies ``safe_sqrt`` to an array of input values:: + + @wp.kernel + def run_safe_sqrt(xs: wp.array(dtype=float), output: wp.array(dtype=float)): + i = wp.tid() + output[i] = safe_sqrt(xs[i]) + +Calling the kernel for an array of values ``[1.0, 2.0, 0.0]`` yields the expected outputs, the gradients are finite except for the zero input:: + + xs = wp.array([1.0, 2.0, 0.0], dtype=wp.float32, requires_grad=True) + ys = wp.zeros_like(xs) + + tape = wp.Tape() + with tape: + wp.launch(run_safe_sqrt, dim=len(xs), inputs=[xs], outputs=[ys]) + tape.backward(grads={ys: wp.array(np.ones(len(xs)), dtype=wp.float32)}) + + print("ys ", ys) + print("xs.grad", xs.grad) + + # ys [1. 1.4142135 0. ] + # xs.grad [0.5 0.35355338 inf] + +It is often desired to catch nonfinite gradients in the computation graph as they may cause the entire gradient computation to be nonfinite. +To do so, we can define a custom gradient function that replaces the adjoint function for ``safe_sqrt`` which is automatically generated by +decorating the custom gradient code via ``@wp.func_grad(safe_sqrt)``:: + + @wp.func_grad(safe_sqrt) + def adj_safe_sqrt(x: float, adj_ret: float): + if x > 0.0: + wp.adjoint[x] += 1.0 / (2.0 * wp.sqrt(x)) * adj_ret + +.. note:: The function signature of the custom grad code consists of the input arguments of the forward function plus the adjoint variables of the + forward function outputs. To access and modify the partial derivatives of the input arguments, we use the ``wp.adjoint`` dictionary. + The keys of this dictionary are the input arguments of the forward function, and the values are the partial derivatives of the forward function + output with respect to the input argument. + + +Example 2: Custom Replay Function +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In the following, we increment an array index in each thread via :func:`wp.atomic_add() ` and compute +the square root of an input array at the incremented index:: + + @wp.kernel + def test_add(counter: wp.array(dtype=int), input: wp.array(dtype=float), output: wp.array(dtype=float)): + idx = wp.atomic_add(counter, 0, 1) + output[idx] = wp.sqrt(input[idx]) + + def main(): + dim = 16 + use_reversible_increment = False + input = wp.array(np.arange(1, dim + 1), dtype=wp.float32, requires_grad=True) + counter = wp.zeros(1, dtype=wp.int32) + thread_ids = wp.zeros(dim, dtype=wp.int32) + output = wp.zeros(dim, dtype=wp.float32, requires_grad=True) + tape = wp.Tape() + with tape: + if use_reversible_increment: + wp.launch(test_add_diff, dim, inputs=[counter, thread_ids, input], outputs=[output]) + else: + wp.launch(test_add, dim, inputs=[counter, input], outputs=[output]) + + print("counter: ", counter.numpy()) + print("thread_ids: ", thread_ids.numpy()) + print("input: ", input.numpy()) + print("output: ", output.numpy()) + + tape.backward(grads={ + output: wp.array(np.ones(dim), dtype=wp.float32) + }) + print("input.grad: ", input.grad.numpy()) + + if __name__ == "__main__": + main() + +The output of the above code is: + +.. code-block:: js + + counter: [8] + thread_ids: [0 0 0 0 0 0 0 0] + input: [1. 2. 3. 4. 5. 6. 7. 8.] + output: [1. 1.4142135 1.7320508 2. 2.236068 2.4494898 2.6457512 2.828427] + input.grad: [4. 0. 0. 0. 0. 0. 0. 0.] + +The gradient of the input is incorrect because the backward pass involving the atomic operation ``wp.atomic_add()`` does not know which thread ID corresponds +to which input value. +The index returned by the adjoint of ``wp.atomic_add()`` is always zero so that the gradient the first entry of the input array, +i.e. :math:`\frac{1}{2\sqrt{1}} = 0.5`, is accumulated ``dim`` times (hence ``input.grad[0] == 4.0`` and all other entries zero). + +To fix this, we define a new Warp function ``reversible_increment()`` with a custom *replay* definition that stores the thread ID in a separate array:: + + @wp.func + def reversible_increment( + buf: wp.array(dtype=int), + buf_index: int, + value: int, + thread_values: wp.array(dtype=int), + tid: int + ): + next_index = wp.atomic_add(buf, buf_index, value) + # store which thread ID corresponds to which index for the backward pass + thread_values[tid] = next_index + return next_index + + + @wp.func_replay(reversible_increment) + def replay_reversible_increment( + buf: wp.array(dtype=int), + buf_index: int, + value: int, + thread_values: wp.array(dtype=int), + tid: int + ): + return thread_values[tid] + + +Instead of running ``reversible_increment()``, the custom replay code in ``replay_reversible_increment()`` is now executed +during forward phase in the backward pass of the function calling ``reversible_increment()``. +We first stored the array index to each thread ID in the forward pass, and now we retrieve the array index for each thread ID in the backward pass. +That way, the backward pass can reproduce the same addition operation as in the forward pass with exactly the same operands per thread. + +.. warning:: The function signature of the custom replay code must match the forward function signature. + +To use our function we write the following kernel:: + + @wp.kernel + def test_add_diff( + counter: wp.array(dtype=int), + thread_ids: wp.array(dtype=int), + input: wp.array(dtype=float), + output: wp.array(dtype=float) + ): + tid = wp.tid() + idx = reversible_increment(counter, 0, 1, thread_ids, tid) + output[idx] = wp.sqrt(input[idx]) + +Running the ``test_add_diff`` kernel via the previous ``main`` function with ``use_reversible_increment = True``, we now compute correct gradients +for the input array: + +.. code-block:: js + + counter: [8] + thread_ids: [0 1 2 3 4 5 6 7] + input: [1. 2. 3. 4. 5. 6. 7. 8.] + output: [1. 1.4142135 1.7320508 2. 2.236068 2.4494898 2.6457512 2.828427 ] + input.grad: [0.5 0.35355338 0.28867513 0.25 0.2236068 0.20412414 0.18898225 0.17677669] + +Custom Native Functions +####################### + +Users may insert native C++/CUDA code in Warp kernels using ``@func_native`` decorated functions. +These accept native code as strings that get compiled after code generation, and are called within ``@wp.kernel`` functions. +For example:: + + snippet = """ + __shared__ int sum[128]; + + sum[tid] = arr[tid]; + __syncthreads(); + + for (int stride = 64; stride > 0; stride >>= 1) { + if (tid < stride) { + sum[tid] += sum[tid + stride]; + } + __syncthreads(); + } + + if (tid == 0) { + out[0] = sum[0]; + } + """ + + @wp.func_native(snippet) + def reduce(arr: wp.array(dtype=int), out: wp.array(dtype=int), tid: int): + ... + + @wp.kernel + def reduce_kernel(arr: wp.array(dtype=int), out: wp.array(dtype=int)): + tid = wp.tid() + reduce(arr, out, tid) + + N = 128 + x = wp.array(np.arange(N, dtype=int), dtype=int, device=device) + out = wp.zeros(1, dtype=int, device=device) + + wp.launch(kernel=reduce_kernel, dim=N, inputs=[x, out], device=device) + +Notice the use of shared memory here: the Warp library does not expose shared memory as a feature, but the CUDA compiler will +readily accept the above snippet. This means CUDA features not exposed in Warp are still accessible in Warp scripts. +Warp kernels meant for the CPU won't be able to leverage CUDA features of course, but this same mechanism supports pure C++ snippets as well. + +Please bear in mind the following: the thread index in your snippet should be computed in a ``@wp.kernel`` and passed to your snippet, +as in the above example. This means your ``@wp.func_native`` function signature should include the variables used in your snippet, +as well as a thread index of type ``int``. The function body itself should be stubbed with ``...`` (the snippet will be inserted during compilation). + +Should you wish to record your native function on the tape and then subsequently rewind the tape, you must include an adjoint snippet +alongside your snippet as an additional input to the decorator, as in the following example:: + + snippet = """ + out[tid] = a * x[tid] + y[tid]; + """ + adj_snippet = """ + adj_a += x[tid] * adj_out[tid]; + adj_x[tid] += a * adj_out[tid]; + adj_y[tid] += adj_out[tid]; + """ + + @wp.func_native(snippet, adj_snippet) + def saxpy( + a: wp.float32, + x: wp.array(dtype=wp.float32), + y: wp.array(dtype=wp.float32), + out: wp.array(dtype=wp.float32), + tid: int, + ): + ... + + @wp.kernel + def saxpy_kernel( + a: wp.float32, + x: wp.array(dtype=wp.float32), + y: wp.array(dtype=wp.float32), + out: wp.array(dtype=wp.float32) + ): + tid = wp.tid() + saxpy(a, x, y, out, tid) + + N = 128 + a = 2.0 + x = wp.array(np.arange(N, dtype=np.float32), dtype=wp.float32, device=device, requires_grad=True) + y = wp.zeros_like(x1) + out = wp.array(np.arange(N, dtype=np.float32), dtype=wp.float32, device=device) + adj_out = wp.array(np.ones(N, dtype=np.float32), dtype=wp.float32, device=device) + + tape = wp.Tape() + + with tape: + wp.launch(kernel=saxpy_kernel, dim=N, inputs=[a, x, y], outputs=[out], device=device) + + tape.backward(grads={out: adj_out}) + +You may also include a custom replay snippet, to be executed as part of the adjoint (see `Custom Gradient Functions`_ for a full explanation). +Consider the following example:: + + def test_custom_replay_grad(): + num_threads = 8 + counter = wp.zeros(1, dtype=wp.int32) + thread_values = wp.zeros(num_threads, dtype=wp.int32) + inputs = wp.array(np.arange(num_threads, dtype=np.float32), requires_grad=True) + outputs = wp.zeros_like(inputs) + + snippet = """ + int next_index = atomicAdd(counter, 1); + thread_values[tid] = next_index; + """ + replay_snippet = "" + + @wp.func_native(snippet, replay_snippet=replay_snippet) + def reversible_increment( + counter: wp.array(dtype=int), thread_values: wp.array(dtype=int), tid: int + ): + ... + + @wp.kernel + def run_atomic_add( + input: wp.array(dtype=float), + counter: wp.array(dtype=int), + thread_values: wp.array(dtype=int), + output: wp.array(dtype=float), + ): + tid = wp.tid() + reversible_increment(counter, thread_values, tid) + idx = thread_values[tid] + output[idx] = input[idx] ** 2.0 + + tape = wp.Tape() + with tape: + wp.launch( + run_atomic_add, dim=num_threads, inputs=[inputs, counter, thread_values], outputs=[outputs] + ) + + tape.backward(grads={outputs: wp.array(np.ones(num_threads, dtype=np.float32))}) + +By default, ``snippet`` would be called in the backward pass, but in this case, we have a custom replay snippet defined, which is called instead. +In this case, ``replay_snippet`` is a no-op, which is all that we require, since ``thread_values`` are cached in the forward pass. +If we did not have a ``replay_snippet`` defined, ``thread_values`` would be overwritten with counter values that exceed the input array size in the backward pass. + +A native snippet may also include a return statement. If this is the case, you must specify the return type in the native function definition, as in the following example:: + + snippet = """ + float sq = x * x; + return sq; + """ + adj_snippet = """ + adj_x += 2.f * x * adj_ret; + """ + + @wp.func_native(snippet, adj_snippet) + def square(x: float) -> float: ... + + @wp.kernel + def square_kernel(input: wp.array(dtype=Any), output: wp.array(dtype=Any)): + tid = wp.tid() + x = input[tid] + output[tid] = square(x) + + N = 5 + x = wp.array(np.arange(N, dtype=float), dtype=float, requires_grad=True) + y = wp.zeros_like(x) + + tape = wp.Tape() + with tape: + wp.launch(kernel=square_kernel, dim=N, inputs=[x, y]) + + tape.backward(grads={y: wp.ones(N, dtype=float)}) + +Debugging Gradients +################### + +.. note:: + We are continuously expanding the debugging section to provide tools to help users debug gradient computations in upcoming Warp releases. + +Measuring Gradient Accuracy +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. currentmodule:: warp.autograd + +Warp provides utility functions to evaluate the partial Jacobian matrices for input/output argument pairs given to kernel launches. +:func:`jacobian` computes the Jacobian matrix of a kernel using Warp's automatic differentiation engine. +:func:`jacobian_fd` computes the Jacobian matrix of a kernel using finite differences. +:func:`gradcheck` compares the Jacobian matrices computed by the autodiff engine and finite differences to measure the accuracy of the gradients. +:func:`plot_kernel_jacobians` visualizes the Jacobian matrices returned by the :func:`jacobian` and :func:`jacobian_fd` functions. + +.. autofunction:: gradcheck + +.. autofunction:: gradcheck_tape + +.. autofunction:: jacobian + +.. autofunction:: jacobian_fd + +.. autofunction:: plot_kernel_jacobians + + +Example usage +""""""""""""" + +.. code-block:: python + + import warp as wp + import warp.autograd + + @wp.kernel + def my_kernel( + a: wp.array(dtype=float), b: wp.array(dtype=wp.vec3), + out1: wp.array(dtype=wp.vec2), out2: wp.array(dtype=wp.quat), + ): + tid = wp.tid() + ai, bi = a[tid], b[tid] + out1[tid] = wp.vec2(ai * wp.length(bi), -ai * wp.dot(bi, wp.vec3(0.1, 1.0, -0.1))) + out2[tid] = wp.normalize(wp.quat(ai, bi[0], bi[1], bi[2])) + + a = wp.array([2.0, -1.0], dtype=wp.float32, requires_grad=True) + b = wp.array([wp.vec3(3.0, 1.0, 2.0), wp.vec3(-4.0, -1.0, 0.0)], dtype=wp.vec3, requires_grad=True) + out1 = wp.zeros(2, dtype=wp.vec2, requires_grad=True) + out2 = wp.zeros(2, dtype=wp.quat, requires_grad=True) + + # compute the Jacobian matrices for all input/output pairs of the kernel using the autodiff engine + jacs = wp.autograd.jacobian( + my_kernel, dim=len(a), inputs=[a, b], outputs=[out1, out2], + plot_jacobians=True) + +.. image:: ../img/kernel_jacobian_ad.svg + +The ``jacs`` dictionary contains the Jacobian matrices as Warp arrays for all input/output pairs of the kernel. +The ``plot_jacobians`` argument visualizes the Jacobian matrices using the :func:`plot_kernel_jacobians` function. +The subplots show the Jacobian matrices for each input (column) and output (row) pair. +The major (thick) gridlines in these image plots separate the array elements of the respective Warp arrays. Since the kernel arguments ``b``, ``out1``, and ``out2`` are Warp arrays with vector-type elements, +the minor (thin, dashed) gridlines for the corresponding subplots indicate the vector elements. + + +Checking the gradient accuracy using the :func:`gradcheck` function: + +.. code-block:: python + + passed = wp.autograd.gradcheck( + my_kernel, dim=len(a), inputs=[a, b], outputs=[out1, out2], + plot_relative_error=False, plot_absolute_error=False, + raise_exception=False, show_summary=True) + + assert passed + +Output: + + .. list-table:: + :header-rows: 1 + + * - Input + - Output + - Max Abs Error + - Max Rel Error + - Pass + * - a + - out1 + - 1.5134811e-03 + - 4.0449476e-04 + - .. raw:: html + + PASS + * - a + - out2 + - 1.1073798e-04 + - 1.4098687e-03 + - .. raw:: html + + PASS + * - b + - out1 + - 9.8955631e-04 + - 4.6023726e-03 + - .. raw:: html + + PASS + * - b + - out2 + - 3.3494830e-04 + - 1.2789593e-02 + - .. raw:: html + + PASS + + .. raw:: html + + Gradient check for kernel my_kernel passed + + +Instead of evaluating Jacobians for all inputs and outputs of a kernel, we can also limit the computation to a specific subset of input/output pairs:: + + jacs = wp.autograd.jacobian( + my_kernel, dim=len(a), inputs=[a, b], outputs=[out1, out2], + plot_jacobians=True, + # select which input/output pairs to compute the Jacobian for + input_output_mask=[("a", "out1"), ("b", "out2")], + # limit the number of dimensions to query per output array + max_outputs_per_var=5, + ) + +.. image:: ../img/kernel_jacobian_ad_subset.svg + +The returned Jacobian matrices are now limited to the input/output pairs specified in the ``input_output_mask`` argument. +Furthermore, we limited the number of dimensions to evaluate the gradient for to 5 per output array using the ``max_outputs_per_var`` argument. +The corresponding non-evaluated Jacobian elements are set to ``NaN``. + +Furthermore, it is possible to check the gradients of multiple kernels recorded on a :class:`Tape` via the :func:`gradcheck_tape` function. Here, the inputs and outputs of the kernel launches are used to compute the Jacobian matrices for each kernel launch and compare them with finite differences:: + + tape = wp.Tape() + with tape: + wp.launch(my_kernel_1, dim=len(a), inputs=[a, b], outputs=[out1, c]) + wp.launch(my_kernel_2, dim=len(c), inputs=[c], outputs=[out2]) + + passed = wp.autograd.gradcheck_tape(tape, raise_exception=False, show_summary=True) + + assert passed + + +Visualizing Computation Graphs +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. currentmodule:: warp + +Computing gradients via automatic differentiation can be error-prone, where arrays sometimes miss the ``requires_grad`` setting, or the wrong arrays are passed between kernels. To help debug gradient computations, Warp provides a +:meth:`Tape.visualize` method that generates a graph visualization of the kernel launches recorded on the tape in the `GraphViz `_ dot format. +The visualization shows how the Warp arrays are used as inputs and outputs of the kernel launches. + +Example usage:: + + import warp as wp + + + @wp.kernel + def add(a: wp.array(dtype=float), b: wp.array(dtype=float), c: wp.array(dtype=float)): + tid = wp.tid() + c[tid] = a[tid] + b[tid] + + + tape = wp.Tape() + + a = wp.array([2.0], dtype=wp.float32) + b = wp.array([3.0], dtype=wp.float32, requires_grad=True) + c = wp.array([4.0], dtype=wp.float32) + d = c + e = wp.array([5.0], dtype=wp.float32, requires_grad=True) + + result = wp.zeros(1, dtype=wp.float32, requires_grad=True) + + with tape: + wp.launch(add, dim=1, inputs=[b, e], outputs=[a]) + + # ScopedTimer registers itself as a scope on the tape + with wp.ScopedTimer("Adder"): + + # we can also manually record scopes + tape.record_scope_begin("Custom Scope") + wp.launch(add, dim=1, inputs=[a, b], outputs=[c]) + tape.record_scope_end() + + wp.launch(add, dim=1, inputs=[d, a], outputs=[result]) + + + tape.visualize( + filename="tape.dot", + array_labels={a: "a", b: "b", c: "c", e: "e", result: "result"}, + ) + +This will generate a file `tape.dot` that can be visualized using the `GraphViz `_ toolset: + +.. code-block:: bash + + dot -Tsvg tape.dot -o tape.svg + +The resulting SVG image can be rendered in a web browser: + +.. image:: ../img/tape.svg + +The graph visualization shows the kernel launches as grey boxes with the ports below them indicating the input and output arguments. Arrays +are shown as ellipses, where gray ellipses indicate arrays that do not require gradients, and green ellipses indicate arrays that have ``requires_grad=True``. + +In the example above we can see that the array ``c`` does not have its ``requires_grad`` flag set, which means gradients will not be propagated through this path. + +.. note:: + Arrays can be labeled with custom names using the ``array_labels`` argument to the ``tape.visualize()`` method. + +Array Overwrite Tracking +^^^^^^^^^^^^^^^^^^^^^^^^^ + +It is a common mistake to inadvertently overwrite an array that participates in the computation graph. For example:: + + with tape as wp.Tape(): + + # step 1 + wp.launch(compute_forces, dim=n, inputs=[pos0, vel0], outputs=[force]) + wp.launch(simulate, dim=n, inputs=[pos0, vel0, force], outputs=[pos1, vel1]) + + # step 2 (error, we are overwriting previous forces) + wp.launch(compute_forces, dim=n, inputs=[pos1, vel1], outputs=[force]) + wp.launch(simulate, dim=n, inputs=[pos1, vel1, force], outputs=[pos2, vel2]) + + # compute loss + wp.launch(loss, dim=n, inputs=[pos2]) + + tape.backward(loss) + +Running the tape backwards will incorrectly compute the gradient of the loss with respect to ``pos0`` and ``vel0``, because ``force`` is overwritten in the second simulation step. +The adjoint of ``force`` with respect to ``pos1`` and ``vel1`` will be correct, because the stored value of ``force`` from the forward pass is still correct, but the adjoint of +``force`` with respect to ``pos0`` and ``vel0`` will be incorrect, because the ``force`` value used in this calculation was calculated in step 2, not step 1. The solution is to allocate +two force arrays, ``force0`` and ``force1``, so that we are not overwriting data that participates in the computation graph. + +This sort of problem boils down to a single pattern to be avoided: writing to an array after reading from it. This typically happens over consecutive kernel launches (A), but it might also happen within a single kernel (B). + +A: Inter-Kernel Overwrite:: + + import warp as wp + + @wp.kernel + def square_kernel(x: wp.array(dtype=float), y: wp.array(dtype=float)): + tid = wp.tid() + y[tid] = x[tid] * x[tid] + + @wp.kernel + def overwrite_kernel(z: wp.array(dtype=float), x: wp.array(dtype=float)): + tid = wp.tid() + x[tid] = z[tid] + + @wp.kernel + def loss_kernel(x: wp.array(dtype=float), loss: wp.array(dtype=float)): + tid = wp.tid() + wp.atomic_add(loss, 0, x[tid]) + + a = wp.array(np.array([1.0, 2.0, 3.0]), dtype=float, requires_grad=True) + b = wp.zeros_like(a) + c = wp.array(np.array([-1.0, -2.0, -3.0]), dtype=float, requires_grad=True) + loss = wp.zeros(1, dtype=float, requires_grad=True) + + tape = wp.Tape() + with tape: + wp.launch(square_kernel, a.shape, inputs=[a], outputs=[b]) + wp.launch(overwrite_kernel, c.shape, inputs=[c], outputs=[a]) + wp.launch(loss_kernel, a.shape, inputs=[a, loss]) + + tape.backward(loss) + + print(a.grad) + # prints [-2. -4. -6.] instead of [2. 4. 6.] + +B: Intra-Kernel Overwrite:: + + import warp as wp + + @wp.kernel + def readwrite_kernel(a: wp.array(dtype=float), b: wp.array(dtype=float)): + tid = wp.tid() + b[tid] = a[tid] * a[tid] + a[tid] = 1.0 + + @wp.kernel + def loss_kernel(x: wp.array(dtype=float), loss: wp.array(dtype=float)): + tid = wp.tid() + wp.atomic_add(loss, 0, x[tid]) + + a = wp.array(np.array([1.0, 2.0, 3.0]), dtype=float, requires_grad=True) + b = wp.zeros_like(a) + loss = wp.zeros(1, dtype=float, requires_grad=True) + + tape = wp.Tape() + with tape: + wp.launch(readwrite_kernel, dim=a.shape, inputs=[a, b]) + wp.launch(loss_kernel, a.shape, inputs=[b, loss]) + + tape.backward(loss) + + print(a.grad) + # prints [2. 2. 2.] instead of [2. 4. 6.] + +If ``wp.config.verify_autograd_array_access = True`` is set, Warp will automatically detect and report array overwrites, covering the above two cases as well as other problematic configurations. +It does so by flagging which kernel array arguments are read from and/or written to in each kernel function during compilation. At runtime, if an array is passed to a kernel argument marked with a read flag, +it is marked as having been read from. Later, if the same array is passed to a kernel argument marked with a write flag, a warning is printed +(recall the pattern we wish to avoid: *write* after *read*). + +.. note:: + Setting ``wp.config.verify_autograd_array_access = True`` will disable kernel caching and force the current module to rebuild. + +.. note:: + Though in-place operations such as ``x[tid] += 1.0`` are technically ``read -> write``, the Warp graph specifically accomodates adjoint accumulation in these cases, so we mark them as write operations. + +.. note:: + This feature does not yet support arrays packed in Warp structs. + +If you make use of :py:meth:`Tape.record_func` in your graph (and so provide your own adjoint callback), be sure to also call :py:meth:`array.mark_write()` and :py:meth:`array.mark_read()`, which will manually mark your arrays as having been written to or read from. + +.. _limitations_and_workarounds: + +Limitations and Workarounds +########################### + +Warp uses a source-code transformation approach to auto-differentiation. +In this approach, the backwards pass must keep a record of intermediate values computed during the forward pass. +This imposes some restrictions on what kernels can do if they are to remain differentiable. + +Dynamic Loops +^^^^^^^^^^^^^ +Currently, dynamic loops are not replayed or unrolled in the backward pass, meaning intermediate values that are +meant to be computed in the loop and may be necessary for adjoint calculations are not updated. + +In the following example, the correct gradient is computed because the ``x`` array adjoints do not depend on intermediate values of ``sum``:: + + @wp.kernel + def dynamic_loop_sum(x: wp.array(dtype=float), + loss: wp.array(dtype=float), + iters: int): + + sum = float(0.0) + + for i in range(iters): + sum += x[i] + + wp.atomic_add(loss, 0, sum) + + iters = 3 + x = wp.full(shape=iters, value=1.0, dtype=float, requires_grad=True) + loss = wp.zeros(1, dtype=float, requires_grad=True) + + with wp.Tape() as tape: + wp.launch(dynamic_loop_sum, dim=1, inputs=[x, loss, iters]) + + tape.backward(loss) + + print(x.grad) + # [1. 1. 1.] (correct) + +In contrast, in this example, the ``x`` array adjoints do depend on intermediate values of ``prod`` +(``adj_x[i] = adj_prod[i+1] * prod[i]``) so the gradients are not correctly computed:: + + @wp.kernel + def dynamic_loop_mult(x: wp.array(dtype=float), + loss: wp.array(dtype=float), + iters: int): + + prod = float(1.0) + + for i in range(iters): + prod *= x[i] + + wp.atomic_add(loss, 0, prod) + + iters = 3 + x = wp.full(shape=iters, value=2.0, dtype=float, requires_grad=True) + loss = wp.zeros(1, dtype=float, requires_grad=True) + + with wp.Tape() as tape: + wp.launch(dynamic_loop_mult, dim=1, inputs=[x, loss, iters]) + + tape.backward(loss) + + print(x.grad) + # [32. 8. 2.] (incorrect) + +We can fix the latter case by switching to a static loop (e.g. replacing ``range(iters)`` with ``range(3)``). Static loops are +automatically unrolled if the number of loop iterations is less than or equal to the ``max_unroll`` parameter set in ``wp.config`` +or at the module level with ``wp.set_module_options({"max_unroll": N})``, and so intermediate values in the loop are individually stored. +But in scenarios where this is not possible, you may consider allocating additional memory to store intermediate values in the dynamic loop. +For example, we can fix the above case like so:: + + @wp.kernel + def dynamic_loop_mult(x: wp.array(dtype=float), + prods: wp.array(dtype=float), + loss: wp.array(dtype=float), + iters: int): + + for i in range(iters): + prods[i+1] = x[i] * prods[i] + + wp.atomic_add(loss, 0, prods[iters]) + + iters = 3 + x = wp.full(shape=iters, value=2.0, dtype=float, requires_grad=True) + prods = wp.full(shape=(iters + 1), value=1.0, dtype=float, requires_grad=True) + loss = wp.zeros(1, dtype=float, requires_grad=True) + + with wp.Tape() as tape: + wp.launch(dynamic_loop_mult, dim=1, inputs=[x, prods, loss, iters]) + + tape.backward(loss) + + print(x.grad) + # [4. 4. 4] (correct) + +Even if an array's adjoints do not depend on `intermediate` local values in a dynamic loop, it may be that +the `final` value of a local variable is necessary for the adjoint computation. Consider the following scenario:: + + @wp.kernel + def dynamic_loop_sum(x: wp.array(dtype=float), + weights: wp.array(dtype=float), + loss: wp.array(dtype=float), + iters: int): + + sum = float(0.0) + norm = float(0.0) + + for i in range(iters): + w = weights[i] + norm += w + sum += x[i]*w + + l = sum / norm + wp.atomic_add(loss, 0, l) + + iters = 3 + x = wp.full(shape=iters, value=1.0, dtype=float, requires_grad=True) + weights = wp.full(shape=iters, value=1.0, dtype=float, requires_grad=True) + loss = wp.zeros(1, dtype=float, requires_grad=True) + + with wp.Tape() as tape: + wp.launch(dynamic_loop_sum, dim=1, inputs=[x, weights, loss, iters]) + + tape.backward(loss) + + print(x.grad) + # [inf inf inf] (incorrect) + +In the backward pass, when computing the adjoint for ``sum``, which is used to compute the adjoint for the ``x`` array, there is a division by zero: +``norm`` is not recomputed in the backward pass because dynamic loops are not replayed. This means that ``norm`` is 0.0 at the start of the adjoint calculation +rather than the value computed in the forward pass, 3.0. + +There is a different remedy for this particular scenario. One can force a dynamic loop to replay in the backward pass by migrating the body of the loop to +a Warp function:: + + @wp.func + def loop(x: wp.array(dtype=float), + weights: wp.array(dtype=float), + iters: int): + + sum = float(0.0) + norm = float(0.0) + + for i in range(iters): + w = weights[i] + norm += w + sum += x[i]*w + + return sum, norm + + @wp.kernel + def dynamic_loop_sum(x: wp.array(dtype=float), + weights: wp.array(dtype=float), + loss: wp.array(dtype=float), + iters: int): + + sum, norm = loop(x, weights, iters) + + l = sum / norm + wp.atomic_add(loss, 0, l) + + iters = 3 + x = wp.full(shape=iters, value=1.0, dtype=float, requires_grad=True) + weights = wp.full(shape=iters, value=0.5, dtype=float, requires_grad=True) + loss = wp.zeros(1, dtype=float, requires_grad=True) + + with wp.Tape() as tape: + wp.launch(dynamic_loop_sum, dim=1, inputs=[x, weights, loss, iters]) + + tape.backward(loss) + + print(x.grad) + # [.33 .33 .33] (correct) + +However, this only works because the ``x`` array adjoints do not require an intermediate +value for ``sum``; they only need the adjoint of ``sum``. In general this workaround is only valid for simple add/subtract operations such as +``+=`` or ``-=``. + +.. note:: + + In a subsequent release, we will enable users to force-unroll dynamic loops in some circumstances, thereby obviating these workarounds. diff --git a/_sources/modules/fem.rst.txt b/_sources/modules/fem.rst.txt new file mode 100644 index 00000000..319781c1 --- /dev/null +++ b/_sources/modules/fem.rst.txt @@ -0,0 +1,493 @@ +warp.fem +======== + +.. currentmodule:: warp.fem + +The ``warp.fem`` module is designed to facilitate solving physical systems described as differential +equations. For example, it can solve PDEs for diffusion, convection, fluid flow, and elasticity problems +using finite-element-based (FEM) Galerkin methods and allows users to quickly experiment with various FEM +formulations and discretization schemes. + +Integrands +---------- + +The core functionality of the FEM toolkit is the ability to integrate constant, linear, and bilinear forms +over various domains and using arbitrary interpolation basis. + +The main mechanism is the :py:func:`.integrand` decorator, for instance: :: + + @integrand + def linear_form( + s: Sample, + domain: Domain, + v: Field, + ): + x = domain(s) + return v(s) * wp.max(0.0, 1.0 - wp.length(x)) + + + @integrand + def diffusion_form(s: Sample, u: Field, v: Field, nu: float): + return nu * wp.dot( + grad(u, s), + grad(v, s), + ) + +Integrands are normal Warp kernels, meaning that they may contain arbitrary Warp functions. +However, they accept a few special parameters: + + - :class:`.Sample` contains information about the current integration sample point, such as the element index and coordinates in element. + - :class:`.Field` designates an abstract field, which will be replaced at call time by the actual field type such as a discrete field, :class:`.field.TestField` or :class:`.field.TrialField` defined over some :class:`.FunctionSpace`, + an :class:`.ImplicitField` wrapping an arbitrary function, or any other of the available :ref:`Fields`. + A field `u` can then be evaluated at a given sample `s` using the usual call operator as ``u(s)``. + Several other operators are available, such as the gradient :func:`.grad`; see the :ref:`Operators` section. + - :class:`.Domain` designates an abstract integration domain. Evaluating a domain at a sample `s` as ``domain(s)`` yields the corresponding world position, + and several operators are also provided domains, for example evaluating the normal at a given sample: :: + + @integrand + def boundary_form( + s: Sample, + domain: Domain, + u: Field, + ): + nor = normal(domain, s) + return wp.dot(u(s), nor) + +Integrands cannot be used directly with :func:`warp.launch`, but must be called through :func:`.integrate` or :func:`.interpolate` instead. +The :class:`.Sample` and :class:`.Domain` arguments of the root integrand (`integrand` parameter passed to :func:`integrate` or :func:`interpolate` call) will get automatically populated. +:class:`.Field` arguments must be passed as a dictionary in the `fields` parameter of the launcher function, and all other standard Warp types arguments must be +passed as a dictionary in the `values` parameter of the launcher function, for instance: :: + + integrate(diffusion_form, fields={"u": trial, "v": test}, values={"nu": viscosity}) + + +Basic Workflow +-------------- + +The typical steps for solving a linearized PDE with ``warp.fem`` are as follow: + + - Define a :class:`.Geometry` (grid, mesh, etc). At the moment, 2D and 3D regular grids, NanoVDB volumes, and triangle, quadrilateral, tetrahedron and hexahedron unstructured meshes are supported. + - Define one or more :class:`.FunctionSpace`, by equipping the geometry elements with shape functions. See :func:`.make_polynomial_space`. At the moment, continuous/discontinuous Lagrange (:math:`P_{k[d]}, Q_{k[d]}`) and Serendipity (:math:`S_k`) shape functions of order :math:`k \leq 3` are supported. + - Define an integration domain, for instance the geometry's cells (:class:`.Cells`) or boundary sides (:class:`.BoundarySides`). + - Integrate linear forms to build the system's right-hand-side. Define a test function over the function space using :func:`.make_test`, + a :class:`.Quadrature` formula (or let the module choose one based on the function space degree), and call :func:`.integrate` with the linear form integrand. + The result is a :class:`warp.array` containing the integration result for each of the function space degrees of freedom. + - Integrate bilinear forms to build the system's left-hand-side. Define a trial function over the function space using :func:`.make_trial`, + then call :func:`.integrate` with the bilinear form integrand. + The result is a :class:`warp.sparse.BsrMatrix` containing the integration result for each pair of test and trial function space degrees of freedom. + Note that the trial and test functions do not have to be defined over the same function space, so that Mixed FEM is supported. + - Solve the resulting linear system using the solver of your choice, for instance one of the built-in :ref:`iterative-linear-solvers`. + + +The following excerpt from the introductory example ``warp/examples/fem/example_diffusion.py`` outlines this procedure: :: + + # Grid geometry + geo = Grid2D(n=50, cell_size=2) + + # Domain and function spaces + domain = Cells(geometry=geo) + scalar_space = make_polynomial_space(geo, degree=3) + + # Right-hand-side (forcing term) + test = make_test(space=scalar_space, domain=domain) + rhs = integrate(linear_form, fields={"v": test}) + + # Weakly-imposed boundary conditions on Y sides + boundary = BoundarySides(geo) + bd_test = make_test(space=scalar_space, domain=boundary) + bd_trial = make_trial(space=scalar_space, domain=boundary) + bd_matrix = integrate(y_mass_form, fields={"u": bd_trial, "v": bd_test}) + + # Diffusion form + trial = make_trial(space=scalar_space, domain=domain) + matrix = integrate(diffusion_form, fields={"u": trial, "v": test}, values={"nu": viscosity}) + + # Assemble linear system (add diffusion and boundary condition matrices) + matrix += bd_matrix * boundary_strength + + # Solve linear system using Conjugate Gradient + x = wp.zeros_like(rhs) + bsr_cg(matrix, b=rhs, x=x) + + +.. note:: + The :func:`.integrate` function does not check that the passed integrands are actually linear or bilinear forms; it is up to the user to ensure that they are. + To solve non-linear PDEs, one can use an iterative procedure and pass the current value of the studied function :class:`.DiscreteField` argument to the integrand, in which + arbitrary operations are permitted. However, the result of the form must remain linear in the test and trial fields. + This strategy is demonstrated in the ``example_mixed_elasticity.py`` example. + +Introductory Examples +--------------------- + +``warp.fem`` ships with a list of examples in the ``warp/examples/fem`` directory demonstrating how to solve classical model problems. + + - ``example_diffusion.py``: 2D diffusion with homogeneous Neumann and Dirichlet boundary conditions + * ``example_diffusion_3d.py``: 3D variant of the diffusion problem + - ``example_convection_diffusion.py``: 2D convection-diffusion using semi-Lagrangian advection + * ``example_convection_diffusion_dg.py``: 2D convection-diffusion using Discontinuous Galerkin with upwind transport and Symmetric Interior Penalty + - ``example_burgers.py``: 2D inviscid Burgers using Discontinuous Galerkin with upwind transport and slope limiter + - ``example_stokes.py``: 2D incompressible Stokes flow using mixed :math:`P_k/P_{k-1}` or :math:`Q_k/P_{(k-1)d}` elements + - ``example_navier_stokes.py``: 2D Navier-Stokes flow using mixed :math:`P_k/P_{k-1}` elements + - ``example_mixed_elasticity.py``: 2D nonlinear elasticity using mixed continuous/discontinuous :math:`S_k/P_{(k-1)d}` elements + - ``example_magnetostatics.py``: 2D magnetostatics using a curl-curl formulation + +Advanced Usages +--------------- + +High-order (curved) geometries +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +It is possible to convert any :class:`.Geometry` (grids and explicit meshes) into a curved, high-order variant by deforming them +with an arbitrary-order displacement field using the :meth:`~.field.GeometryField.make_deformed_geometry` method. +The process looks as follows:: + + # Define a base geometry + base_geo = fem.Grid3D(res=resolution) + + # Define a displacement field on the base geometry + deformation_space = fem.make_polynomial_space(base_geo, degree=deformation_degree, dtype=wp.vec3) + deformation_field = deformation_space.make_field() + + # Populate the field value by interpolating an expression + fem.interpolate(deformation_field_expr, dest=deformation_field) + + # Construct the deformed geometry from the displacement field + deform_geo = deformation_field.make_deformed_geometry() + + # Define new function spaces on the deformed geometry + scalar_space = fem.make_polynomial_space(deformed_geo, degree=scalar_space_degree) + +See ``example_deformed_geometry.py`` for a complete example. +It is also possible to define the deformation field from an :class:`ImplicitField`, as done in ``example_magnetostatics.py``. + +Particle-based quadrature and position lookups +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The global :func:`.lookup` operator allows generating a :class:`.Sample` from an arbitraty position; this is illustrated in +the ``example_streamlines.py`` example for generating 3D streamlines by tracing through a velocity field. + +This operator is also leveraged by the :class:`.PicQuadrature` to provide a way to define Particle-In-Cell quadratures from a set or arbitrary particles, +making it possible to implement MPM-type methods. +The particles are automatically bucketed to the geometry cells when the quadrature is initialized. +This is illustrated by the ``example_stokes_transfer.py`` and ``example_apic_fluid.py`` examples. + +.. note:: + The global :func:`.lookup` operator is not currently supported for :class:`Quadmesh2D`, :class:`Hexmesh` and deformed geometries. + +Nonconforming fields +^^^^^^^^^^^^^^^^^^^^ + +Fields defined on a given :class:`.Geometry` cannot be directly used for integrating over a distinct geometry; +however, they may be wrapped in a :class:`.NonconformingField` for this purpose. +This is leveraged by the ``example_nonconforming_contact.py`` to simulate contacting bodies that are discretized separately. + +.. note:: + Currently :class:`.NonconformingField` does not support wrapping a trial field, so it is not yet possible to define + bilinear forms over different geometries. + +.. note:: + The mapping between the different geometries is position based, so a :class:`.NonconformingField` is not able to accurately capture discontinuous function spaces. + Moreover, the integration domain must support the :func:`.lookup` operator. + +Partitioning +^^^^^^^^^^^^ + +The FEM toolkit makes it possible to perform integration on a subset of the domain elements, +possibly re-indexing degrees of freedom so that the linear system contains the local ones only. +This is useful for distributed computation (see ``warp/examples/fem/example_diffusion_mgpu.py``), or simply to limit the simulation domain to a subset of active cells (see ``warp/examples/fem/example_stokes_transfer.py``). + +A partition of the simulation geometry can be defined using subclasses of :class:`.GeometryPartition` +such as :class:`.LinearGeometryPartition` or :class:`.ExplicitGeometryPartition`. + +Function spaces can then be partitioned according to the geometry partition using :func:`.make_space_partition`. +The resulting :class:`.SpacePartition` object allows translating between space-wide and partition-wide node indices, +and differentiating interior, frontier and exterior nodes. + +The :class:`.Subdomain` class can be used to integrate over a subset of elements while keeping the full set of degrees of freedom, +i.e, without reindexing; this is illustrated in the ``example_streamlines.py`` example to define inflow and outflow boundaries. + +Memory management +^^^^^^^^^^^^^^^^^ + +Several ``warp.fem`` functions require allocating temporary buffers to perform their computations. +If such functions are called many times in a tight loop, those many allocations and de-allocations may degrade performance, +though this is a lot less signifiant when :ref:`mempool_allocators` are in use. +To overcome this issue, a :class:`.cache.TemporaryStore` object may be created to persist and reuse temporary allocations across calls, +either globally using :func:`set_default_temporary_store` or at a per-function granularity using the corresponding argument. + +Visualization +------------- + +Most functions spaces define a :meth:`.FunctionSpace.cells_to_vtk` method that returns a list of VTK-compatible cell types and node indices. +This can be used to visualize discrete fields in VTK-aware viewers such as ``pyvista``, for instance:: + + import numpy as np + import pyvista + + import warp as wp + import warp.fem as fem + + + @fem.integrand + def ackley(s: fem.Sample, domain: fem.Domain): + x = domain(s) + return ( + -20.0 * wp.exp(-0.2 * wp.sqrt(0.5 * wp.length_sq(x))) + - wp.exp(0.5 * (wp.cos(2.0 * wp.pi * x[0]) + wp.cos(2.0 * wp.pi * x[1]))) + + wp.e + + 20.0 + ) + + + # Define field + geo = fem.Grid2D(res=wp.vec2i(64, 64), bounds_lo=wp.vec2(-4.0, -4.0), bounds_hi=wp.vec2(4.0, 4.0)) + space = fem.make_polynomial_space(geo, degree=3) + field = space.make_field() + fem.interpolate(ackley, dest=field) + + # Extract cells, nodes and values + cells, types = field.space.vtk_cells() + nodes = field.space.node_positions().numpy() + values = field.dof_values.numpy() + positions = np.hstack((nodes, values[:, np.newaxis])) + + # Visualize with pyvista + grid = pyvista.UnstructuredGrid(cells, types, positions) + grid.point_data["scalars"] = values + plotter = pyvista.Plotter() + plotter.add_mesh(grid) + plotter.show() + + + +.. _Operators: + +Operators +--------- +.. autofunction:: position(domain: Domain, s: Sample) +.. autofunction:: normal(domain: Domain, s: Sample) +.. autofunction:: lookup(domain: Domain, x) +.. autofunction:: measure(domain: Domain, s: Sample) +.. autofunction:: measure_ratio(domain: Domain, s: Sample) +.. autofunction:: deformation_gradient(domain: Domain, s: Sample) + +.. autofunction:: degree(f: Field) +.. autofunction:: inner(f: Field, s: Sample) +.. autofunction:: outer(f: Field, s: Sample) +.. autofunction:: grad(f: Field, s: Sample) +.. autofunction:: grad_outer(f: Field, s: Sample) +.. autofunction:: div(f: Field, s: Sample) +.. autofunction:: div_outer(f: Field, s: Sample) +.. autofunction:: at_node(f: Field, s: Sample) + +.. autofunction:: D(f: Field, s: Sample) +.. autofunction:: curl(f: Field, s: Sample) +.. autofunction:: jump(f: Field, s: Sample) +.. autofunction:: average(f: Field, s: Sample) +.. autofunction:: grad_jump(f: Field, s: Sample) +.. autofunction:: grad_average(f: Field, s: Sample) + +.. autofunction:: warp.fem.operator.operator + +Integration +----------- + +.. autofunction:: integrate +.. autofunction:: interpolate + +.. autofunction:: integrand + +.. class:: Sample + + Per-sample point context for evaluating fields and related operators in integrands. + +.. autoclass:: Field + +.. autoclass:: Domain + +Geometry +-------- + +.. autoclass:: Grid2D + :show-inheritance: + +.. autoclass:: Trimesh2D + :show-inheritance: + +.. autoclass:: Quadmesh2D + :show-inheritance: + +.. autoclass:: Grid3D + :show-inheritance: + +.. autoclass:: Tetmesh + :show-inheritance: + +.. autoclass:: Hexmesh + :show-inheritance: + +.. autoclass:: Nanogrid + :show-inheritance: + +.. autoclass:: LinearGeometryPartition + +.. autoclass:: ExplicitGeometryPartition + +.. autoclass:: Cells + :show-inheritance: + +.. autoclass:: Sides + :show-inheritance: + +.. autoclass:: BoundarySides + :show-inheritance: + +.. autoclass:: FrontierSides + :show-inheritance: + +.. autoclass:: Subdomain + :show-inheritance: + +.. autoclass:: Polynomial + :members: + +.. autoclass:: RegularQuadrature + :show-inheritance: + +.. autoclass:: NodalQuadrature + :show-inheritance: + +.. autoclass:: ExplicitQuadrature + :show-inheritance: + +.. autoclass:: PicQuadrature + :show-inheritance: + +Function Spaces +--------------- + +.. autofunction:: make_polynomial_space + +.. autofunction:: make_polynomial_basis_space + +.. autofunction:: make_collocated_function_space + +.. autofunction:: make_space_partition + +.. autofunction:: make_space_restriction + +.. autoclass:: ElementBasis + :members: + +.. autoclass:: SymmetricTensorMapper + :show-inheritance: + +.. autoclass:: SkewSymmetricTensorMapper + :show-inheritance: + +.. autoclass:: PointBasisSpace + :show-inheritance: + +.. _Fields: + +Fields +------ + +.. autofunction:: make_test + +.. autofunction:: make_trial + +.. autofunction:: make_discrete_field + +.. autoclass:: ImplicitField + :show-inheritance: + :members: values + +.. autoclass:: UniformField + :show-inheritance: + :members: value + +.. autoclass:: NonconformingField + :show-inheritance: + +.. autofunction:: make_restriction + +Boundary Conditions +------------------- + +.. autofunction:: normalize_dirichlet_projector + +.. autofunction:: project_linear_system + +Memory Management +----------------- + +.. autofunction:: set_default_temporary_store + +.. autofunction:: borrow_temporary + +.. autofunction:: borrow_temporary_like + + +Interfaces +---------- + +Interface classes are not meant to be constructed directly, but can be derived from extend the built-in functionality. + +.. autoclass:: Geometry + :members: cell_count, side_count, boundary_side_count + +.. autoclass:: GeometryPartition + :members: cell_count, side_count, boundary_side_count, frontier_side_count + +.. autoclass:: GeometryDomain + :members: element_kind, dimension, element_count + +.. autoclass:: Quadrature + :members: domain, total_point_count + +.. autoclass:: FunctionSpace + :members: dtype, topology, geometry, dimension, degree, trace, make_field + +.. autoclass:: SpaceTopology + :members: dimension, geometry, node_count, element_node_indices, trace + +.. autoclass:: BasisSpace + :members: topology, geometry, node_positions + +.. autoclass:: warp.fem.space.shape.ShapeFunction + +.. autoclass:: SpacePartition + :members: node_count, owned_node_count, interior_node_count, space_node_indices + +.. autoclass:: SpaceRestriction + :members: node_count + +.. autoclass:: DofMapper + +.. autoclass:: FieldLike + +.. autoclass:: DiscreteField + :show-inheritance: + :members: dof_values + +.. autoclass:: warp.fem.field.FieldRestriction + +.. autoclass:: warp.fem.field.GeometryField + :show-inheritance: + :members: trace, make_deformed_geometry + +.. autoclass:: warp.fem.field.SpaceField + :show-inheritance: + +.. autoclass:: warp.fem.field.TestField + :show-inheritance: + +.. autoclass:: warp.fem.field.TrialField + :show-inheritance: + +.. autoclass:: TemporaryStore + :members: clear + +.. autoclass:: warp.fem.cache.Temporary + :members: array, detach, release diff --git a/_sources/modules/functions.rst.txt b/_sources/modules/functions.rst.txt new file mode 100644 index 00000000..a451050c --- /dev/null +++ b/_sources/modules/functions.rst.txt @@ -0,0 +1,2181 @@ +.. + Autogenerated File - Do not edit. Run build_docs.py to generate. + +.. functions: +.. currentmodule:: warp + +Kernel Reference +================ + +Scalar Types +------------ +.. class:: int8 +.. class:: uint8 +.. class:: int16 +.. class:: uint16 +.. class:: int32 +.. class:: uint32 +.. class:: int64 +.. class:: uint64 +.. class:: float16 +.. class:: float32 +.. class:: float64 +.. class:: bool + + +Vector Types +------------ +.. class:: vec2b +.. class:: vec2ub +.. class:: vec2s +.. class:: vec2us +.. class:: vec2i +.. class:: vec2ui +.. class:: vec2l +.. class:: vec2ul +.. class:: vec2h +.. class:: vec2f +.. class:: vec2d +.. class:: vec3b +.. class:: vec3ub +.. class:: vec3s +.. class:: vec3us +.. class:: vec3i +.. class:: vec3ui +.. class:: vec3l +.. class:: vec3ul +.. class:: vec3h +.. class:: vec3f +.. class:: vec3d +.. class:: vec4b +.. class:: vec4ub +.. class:: vec4s +.. class:: vec4us +.. class:: vec4i +.. class:: vec4ui +.. class:: vec4l +.. class:: vec4ul +.. class:: vec4h +.. class:: vec4f +.. class:: vec4d +.. class:: mat22h +.. class:: mat22f +.. class:: mat22d +.. class:: mat33h +.. class:: mat33f +.. class:: mat33d +.. class:: mat44h +.. class:: mat44f +.. class:: mat44d +.. class:: quath +.. class:: quatf +.. class:: quatd +.. class:: transformh +.. class:: transformf +.. class:: transformd +.. class:: spatial_vectorh +.. class:: spatial_vectorf +.. class:: spatial_vectord +.. class:: spatial_matrixh +.. class:: spatial_matrixf +.. class:: spatial_matrixd + +Generic Types +------------- +.. class:: Int +.. class:: Float +.. class:: Scalar +.. class:: Vector +.. class:: Matrix +.. class:: Quaternion +.. class:: Transformation +.. class:: Array + + +Scalar Math +--------------- +.. py:function:: min(a: Scalar, b: Scalar) -> Scalar + + Return the minimum of two scalars. + + +.. py:function:: min(a: Vector[Any,Scalar], b: Vector[Any,Scalar]) -> Vector[Any,Scalar] + :noindex: + :nocontentsentry: + + Return the element-wise minimum of two vectors. + + +.. py:function:: min(a: Vector[Any,Scalar]) -> Scalar + :noindex: + :nocontentsentry: + + Return the minimum element of a vector ``a``. + + +.. py:function:: max(a: Scalar, b: Scalar) -> Scalar + + Return the maximum of two scalars. + + +.. py:function:: max(a: Vector[Any,Scalar], b: Vector[Any,Scalar]) -> Vector[Any,Scalar] + :noindex: + :nocontentsentry: + + Return the element-wise maximum of two vectors. + + +.. py:function:: max(a: Vector[Any,Scalar]) -> Scalar + :noindex: + :nocontentsentry: + + Return the maximum element of a vector ``a``. + + +.. py:function:: clamp(x: Scalar, low: Scalar, high: Scalar) -> Scalar + + Clamp the value of ``x`` to the range [low, high]. + + +.. py:function:: abs(x: Scalar) -> Scalar + + Return the absolute value of ``x``. + + +.. py:function:: abs(x: Vector[Any,Scalar]) -> Vector[Any,Scalar] + :noindex: + :nocontentsentry: + + Return the absolute values of the elements of ``x``. + + +.. py:function:: sign(x: Scalar) -> Scalar + + Return -1 if ``x`` < 0, return 1 otherwise. + + +.. py:function:: sign(x: Vector[Any,Scalar]) -> Scalar + :noindex: + :nocontentsentry: + + Return -1 for the negative elements of ``x``, and 1 otherwise. + + +.. py:function:: step(x: Scalar) -> Scalar + + Return 1.0 if ``x`` < 0.0, return 0.0 otherwise. + + +.. py:function:: nonzero(x: Scalar) -> Scalar + + Return 1.0 if ``x`` is not equal to zero, return 0.0 otherwise. + + +.. py:function:: sin(x: Float) -> Float + + Return the sine of ``x`` in radians. + + +.. py:function:: cos(x: Float) -> Float + + Return the cosine of ``x`` in radians. + + +.. py:function:: acos(x: Float) -> Float + + Return arccos of ``x`` in radians. Inputs are automatically clamped to [-1.0, 1.0]. + + +.. py:function:: asin(x: Float) -> Float + + Return arcsin of ``x`` in radians. Inputs are automatically clamped to [-1.0, 1.0]. + + +.. py:function:: sqrt(x: Float) -> Float + + Return the square root of ``x``, where ``x`` is positive. + + +.. py:function:: cbrt(x: Float) -> Float + + Return the cube root of ``x``. + + +.. py:function:: tan(x: Float) -> Float + + Return the tangent of ``x`` in radians. + + +.. py:function:: atan(x: Float) -> Float + + Return the arctangent of ``x`` in radians. + + +.. py:function:: atan2(y: Float, x: Float) -> Float + + Return the 2-argument arctangent, atan2, of the point ``(x, y)`` in radians. + + +.. py:function:: sinh(x: Float) -> Float + + Return the sinh of ``x``. + + +.. py:function:: cosh(x: Float) -> Float + + Return the cosh of ``x``. + + +.. py:function:: tanh(x: Float) -> Float + + Return the tanh of ``x``. + + +.. py:function:: degrees(x: Float) -> Float + + Convert ``x`` from radians into degrees. + + +.. py:function:: radians(x: Float) -> Float + + Convert ``x`` from degrees into radians. + + +.. py:function:: log(x: Float) -> Float + + Return the natural logarithm (base-e) of ``x``, where ``x`` is positive. + + +.. py:function:: log2(x: Float) -> Float + + Return the binary logarithm (base-2) of ``x``, where ``x`` is positive. + + +.. py:function:: log10(x: Float) -> Float + + Return the common logarithm (base-10) of ``x``, where ``x`` is positive. + + +.. py:function:: exp(x: Float) -> Float + + Return the value of the exponential function :math:`e^x`. + + +.. py:function:: pow(x: Float, y: Float) -> Float + + Return the result of ``x`` raised to power of ``y``. + + +.. py:function:: round(x: Float) -> Float + + Return the nearest integer value to ``x``, rounding halfway cases away from zero. + + This is the most intuitive form of rounding in the colloquial sense, but can be slower than other options like :func:`warp.rint()`. + Differs from :func:`numpy.round()`, which behaves the same way as :func:`numpy.rint()`. + + +.. py:function:: rint(x: Float) -> Float + + Return the nearest integer value to ``x``, rounding halfway cases to nearest even integer. + + It is generally faster than :func:`warp.round()`. Equivalent to :func:`numpy.rint()`. + + +.. py:function:: trunc(x: Float) -> Float + + Return the nearest integer that is closer to zero than ``x``. + + In other words, it discards the fractional part of ``x``. + It is similar to casting ``float(int(a))``, but preserves the negative sign when ``x`` is in the range [-0.0, -1.0). + Equivalent to :func:`numpy.trunc()` and :func:`numpy.fix()`. + + +.. py:function:: floor(x: Float) -> Float + + Return the largest integer that is less than or equal to ``x``. + + +.. py:function:: ceil(x: Float) -> Float + + Return the smallest integer that is greater than or equal to ``x``. + + +.. py:function:: frac(x: Float) -> Float + + Retrieve the fractional part of ``x``. + + In other words, it discards the integer part of ``x`` and is equivalent to ``x - trunc(x)``. + + +.. py:function:: isfinite(a: Scalar) -> bool + + Return ``True`` if ``a`` is a finite number, otherwise return ``False``. + + +.. py:function:: isfinite(a: Vector[Any,Scalar]) -> bool + :noindex: + :nocontentsentry: + + Return ``True`` if all elements of the vector ``a`` are finite, otherwise return ``False``. + + +.. py:function:: isfinite(a: Quaternion[Scalar]) -> bool + :noindex: + :nocontentsentry: + + Return ``True`` if all elements of the quaternion ``a`` are finite, otherwise return ``False``. + + +.. py:function:: isfinite(a: Matrix[Any,Any,Scalar]) -> bool + :noindex: + :nocontentsentry: + + Return ``True`` if all elements of the matrix ``a`` are finite, otherwise return ``False``. + + +.. py:function:: isnan(a: Scalar) -> bool + + Return ``True`` if ``a`` is NaN, otherwise return ``False``. + + +.. py:function:: isnan(a: Vector[Any,Scalar]) -> bool + :noindex: + :nocontentsentry: + + Return ``True`` if any element of the vector ``a`` is NaN, otherwise return ``False``. + + +.. py:function:: isnan(a: Quaternion[Scalar]) -> bool + :noindex: + :nocontentsentry: + + Return ``True`` if any element of the quaternion ``a`` is NaN, otherwise return ``False``. + + +.. py:function:: isnan(a: Matrix[Any,Any,Scalar]) -> bool + :noindex: + :nocontentsentry: + + Return ``True`` if any element of the matrix ``a`` is NaN, otherwise return ``False``. + + +.. py:function:: isinf(a: Scalar) -> bool + + Return ``True`` if ``a`` is positive or negative infinity, otherwise return ``False``. + + +.. py:function:: isinf(a: Vector[Any,Scalar]) -> bool + :noindex: + :nocontentsentry: + + Return ``True`` if any element of the vector ``a`` is positive or negative infinity, otherwise return ``False``. + + +.. py:function:: isinf(a: Quaternion[Scalar]) -> bool + :noindex: + :nocontentsentry: + + Return ``True`` if any element of the quaternion ``a`` is positive or negative infinity, otherwise return ``False``. + + +.. py:function:: isinf(a: Matrix[Any,Any,Scalar]) -> bool + :noindex: + :nocontentsentry: + + Return ``True`` if any element of the matrix ``a`` is positive or negative infinity, otherwise return ``False``. + + + + +Vector Math +--------------- +.. py:function:: dot(a: Vector[Any,Scalar], b: Vector[Any,Scalar]) -> Scalar + + Compute the dot product between two vectors. + + +.. py:function:: dot(a: Quaternion[Float], b: Quaternion[Float]) -> Float + :noindex: + :nocontentsentry: + + Compute the dot product between two quaternions. + + +.. py:function:: ddot(a: Matrix[Any,Any,Scalar], b: Matrix[Any,Any,Scalar]) -> Scalar + + Compute the double dot product between two matrices. + + +.. py:function:: argmin(a: Vector[Any,Scalar]) -> uint32 + + Return the index of the minimum element of a vector ``a``. [1]_ + + +.. py:function:: argmax(a: Vector[Any,Scalar]) -> uint32 + + Return the index of the maximum element of a vector ``a``. [1]_ + + +.. py:function:: outer(a: Vector[Any,Scalar], b: Vector[Any,Scalar]) -> Matrix[Any,Any,Scalar] + + Compute the outer product ``a*b^T`` for two vectors. + + +.. py:function:: cross(a: Vector[3,Scalar], b: Vector[3,Scalar]) -> Vector[3,Scalar] + + Compute the cross product of two 3D vectors. + + +.. py:function:: skew(vec: Vector[3,Scalar]) -> Matrix[3,3,Scalar] + + Compute the skew-symmetric 3x3 matrix for a 3D vector ``vec``. + + +.. py:function:: length(a: Vector[Any,Float]) -> Float + + Compute the length of a floating-point vector ``a``. + + +.. py:function:: length(a: Quaternion[Float]) -> Float + :noindex: + :nocontentsentry: + + Compute the length of a quaternion ``a``. + + +.. py:function:: length_sq(a: Vector[Any,Scalar]) -> Scalar + + Compute the squared length of a vector ``a``. + + +.. py:function:: length_sq(a: Quaternion[Scalar]) -> Scalar + :noindex: + :nocontentsentry: + + Compute the squared length of a quaternion ``a``. + + +.. py:function:: normalize(a: Vector[Any,Float]) -> Vector[Any,Float] + + Compute the normalized value of ``a``. If ``length(a)`` is 0 then the zero vector is returned. + + +.. py:function:: normalize(a: Quaternion[Float]) -> Quaternion[Float] + :noindex: + :nocontentsentry: + + Compute the normalized value of ``a``. If ``length(a)`` is 0, then the zero quaternion is returned. + + +.. py:function:: transpose(a: Matrix[Any,Any,Scalar]) -> Matrix[Any,Any,Scalar] + + Return the transpose of the matrix ``a``. + + +.. py:function:: inverse(a: Matrix[2,2,Float]) -> Matrix[Any,Any,Float] + + Return the inverse of a 2x2 matrix ``a``. + + +.. py:function:: inverse(a: Matrix[3,3,Float]) -> Matrix[Any,Any,Float] + :noindex: + :nocontentsentry: + + Return the inverse of a 3x3 matrix ``a``. + + +.. py:function:: inverse(a: Matrix[4,4,Float]) -> Matrix[Any,Any,Float] + :noindex: + :nocontentsentry: + + Return the inverse of a 4x4 matrix ``a``. + + +.. py:function:: determinant(a: Matrix[2,2,Float]) -> Float + + Return the determinant of a 2x2 matrix ``a``. + + +.. py:function:: determinant(a: Matrix[3,3,Float]) -> Float + :noindex: + :nocontentsentry: + + Return the determinant of a 3x3 matrix ``a``. + + +.. py:function:: determinant(a: Matrix[4,4,Float]) -> Float + :noindex: + :nocontentsentry: + + Return the determinant of a 4x4 matrix ``a``. + + +.. py:function:: trace(a: Matrix[Any,Any,Scalar]) -> Scalar + + Return the trace of the matrix ``a``. + + +.. py:function:: diag(vec: Vector[Any,Scalar]) -> Matrix[Any,Any,Scalar] + + Returns a matrix with the components of the vector ``vec`` on the diagonal. + + +.. py:function:: get_diag(mat: Matrix[Any,Any,Scalar]) -> Vector[Any,Scalar] + + Returns a vector containing the diagonal elements of the square matrix ``mat``. + + +.. py:function:: cw_mul(a: Vector[Any,Scalar], b: Vector[Any,Scalar]) -> Vector[Any,Scalar] + + Component-wise multiplication of two vectors. + + +.. py:function:: cw_mul(a: Matrix[Any,Any,Scalar], b: Matrix[Any,Any,Scalar]) -> Matrix[Any,Any,Scalar] + :noindex: + :nocontentsentry: + + Component-wise multiplication of two matrices. + + +.. py:function:: cw_div(a: Vector[Any,Scalar], b: Vector[Any,Scalar]) -> Vector[Any,Scalar] + + Component-wise division of two vectors. + + +.. py:function:: cw_div(a: Matrix[Any,Any,Scalar], b: Matrix[Any,Any,Scalar]) -> Matrix[Any,Any,Scalar] + :noindex: + :nocontentsentry: + + Component-wise division of two matrices. + + +.. py:function:: vector(*args: Scalar, length: int32, dtype: Scalar) -> Vector[Any,Scalar] + + Construct a vector of given length and dtype. + + +.. py:function:: matrix(pos: Vector[3,Float], rot: Quaternion[Float], scale: Vector[3,Float], dtype: Float) -> Matrix[4,4,Float] + + Construct a 4x4 transformation matrix that applies the transformations as + Translation(pos)*Rotation(rot)*Scaling(scale) when applied to column vectors, i.e.: y = (TRS)*x + + +.. py:function:: matrix(*args: Scalar, shape: Tuple[int, int], dtype: Scalar) -> Matrix[Any,Any,Scalar] + :noindex: + :nocontentsentry: + + Construct a matrix. If the positional ``arg_types`` are not given, then matrix will be zero-initialized. + + +.. py:function:: identity(n: int32, dtype: Scalar) -> Matrix[Any,Any,Scalar] + + Create an identity matrix with shape=(n,n) with the type given by ``dtype``. + + +.. py:function:: svd3(A: Matrix[3,3,Float], U: Matrix[3,3,Float], sigma: Vector[3,Float], V: Matrix[3,3,Scalar]) -> None + + Compute the SVD of a 3x3 matrix ``A``. The singular values are returned in ``sigma``, + while the left and right basis vectors are returned in ``U`` and ``V``. + + +.. py:function:: qr3(A: Matrix[3,3,Float], Q: Matrix[3,3,Float], R: Matrix[3,3,Float]) -> None + + Compute the QR decomposition of a 3x3 matrix ``A``. The orthogonal matrix is returned in ``Q``, + while the upper triangular matrix is returned in ``R``. + + +.. py:function:: eig3(A: Matrix[3,3,Float], Q: Matrix[3,3,Float], d: Vector[3,Float]) -> None + + Compute the eigendecomposition of a 3x3 matrix ``A``. The eigenvectors are returned as the columns of ``Q``, + while the corresponding eigenvalues are returned in ``d``. + + + + +Quaternion Math +--------------- +.. py:function:: quaternion(dtype: Float) -> Quaternion[Float] + + Construct a zero-initialized quaternion. Quaternions are laid out as + [ix, iy, iz, r], where ix, iy, iz are the imaginary part, and r the real part. + + +.. py:function:: quaternion(x: Float, y: Float, z: Float, w: Float) -> Quaternion[Float] + :noindex: + :nocontentsentry: + + Create a quaternion using the supplied components (type inferred from component type). + + +.. py:function:: quaternion(ijk: Vector[3,Float], real: Float, dtype: Float) -> Quaternion[Float] + :noindex: + :nocontentsentry: + + Create a quaternion using the supplied vector/scalar (type inferred from scalar type). + + +.. py:function:: quaternion(quat: Quaternion[Float], dtype: Float) -> Quaternion[Float] + :noindex: + :nocontentsentry: + + Construct a quaternion of type dtype from another quaternion of a different dtype. + + +.. py:function:: quat_identity(dtype: Float) -> quatf + + Construct an identity quaternion with zero imaginary part and real part of 1.0 + + +.. py:function:: quat_from_axis_angle(axis: Vector[3,Float], angle: Float) -> Quaternion[Float] + + Construct a quaternion representing a rotation of angle radians around the given axis. + + +.. py:function:: quat_to_axis_angle(quat: Quaternion[Float], axis: Vector[3,Float], angle: Float) -> None + + Extract the rotation axis and angle radians a quaternion represents. + + +.. py:function:: quat_from_matrix(mat: Matrix[3,3,Float]) -> Quaternion[Float] + + Construct a quaternion from a 3x3 matrix. + + +.. py:function:: quat_rpy(roll: Float, pitch: Float, yaw: Float) -> Quaternion[Float] + + Construct a quaternion representing a combined roll (z), pitch (x), yaw rotations (y) in radians. + + +.. py:function:: quat_inverse(quat: Quaternion[Float]) -> Quaternion[Float] + + Compute quaternion conjugate. + + +.. py:function:: quat_rotate(quat: Quaternion[Float], vec: Vector[3,Float]) -> Vector[3,Float] + + Rotate a vector by a quaternion. + + +.. py:function:: quat_rotate_inv(quat: Quaternion[Float], vec: Vector[3,Float]) -> Vector[3,Float] + + Rotate a vector by the inverse of a quaternion. + + +.. py:function:: quat_slerp(a: Quaternion[Float], b: Quaternion[Float], t: Float) -> Quaternion[Float] + + Linearly interpolate between two quaternions. + + +.. py:function:: quat_to_matrix(quat: Quaternion[Float]) -> Matrix[3,3,Float] + + Convert a quaternion to a 3x3 rotation matrix. + + + + +Transformations +--------------- +.. py:function:: transformation(pos: Vector[3,Float], rot: Quaternion[Float], dtype: Float) -> Transformation[Float] + + Construct a rigid-body transformation with translation part ``pos`` and rotation ``rot``. + + +.. py:function:: transform_identity(dtype: Float) -> transformf + + Construct an identity transform with zero translation and identity rotation. + + +.. py:function:: transform_get_translation(xform: Transformation[Float]) -> Vector[3,Float] + + Return the translational part of a transform ``xform``. + + +.. py:function:: transform_get_rotation(xform: Transformation[Float]) -> Quaternion[Float] + + Return the rotational part of a transform ``xform``. + + +.. py:function:: transform_multiply(a: Transformation[Float], b: Transformation[Float]) -> Transformation[Float] + + Multiply two rigid body transformations together. + + +.. py:function:: transform_point(xform: Transformation[Float], point: Vector[3,Float]) -> Vector[3,Float] + + Apply the transform to a point ``point`` treating the homogeneous coordinate as w=1 (translation and rotation). + + +.. py:function:: transform_point(mat: Matrix[4,4,Float], point: Vector[3,Float]) -> Vector[3,Float] + :noindex: + :nocontentsentry: + + Apply the transform to a point ``point`` treating the homogeneous coordinate as w=1. + + The transformation is applied treating ``point`` as a column vector, e.g.: ``y = mat*point``. + Note this is in contrast to some libraries, notably USD, which applies transforms to row vectors, ``y^T = point^T*mat^T``. + If the transform is coming from a library that uses row-vectors, then users should transpose the transformation + matrix before calling this method. + + +.. py:function:: transform_vector(xform: Transformation[Float], vec: Vector[3,Float]) -> Vector[3,Float] + + Apply the transform to a vector ``vec`` treating the homogeneous coordinate as w=0 (rotation only). + + +.. py:function:: transform_vector(mat: Matrix[4,4,Float], vec: Vector[3,Float]) -> Vector[3,Float] + :noindex: + :nocontentsentry: + + Apply the transform to a vector ``vec`` treating the homogeneous coordinate as w=0. + + The transformation is applied treating ``vec`` as a column vector, e.g.: ``y = mat*vec`` + note this is in contrast to some libraries, notably USD, which applies transforms to row vectors, ``y^T = vec^T*mat^T``. + If the transform is coming from a library that uses row-vectors, then users should transpose the transformation + matrix before calling this method. + + +.. py:function:: transform_inverse(xform: Transformation[Float]) -> Transformation[Float] + + Compute the inverse of the transformation ``xform``. + + + + +Spatial Math +--------------- +.. py:function:: spatial_vector(dtype: Float) + + Zero-initialize a 6D screw vector. + + +.. py:function:: spatial_vector(w: Vector[3,Float], v: Vector[3,Float], dtype: Float) + :noindex: + :nocontentsentry: + + Construct a 6D screw vector from two 3D vectors. + + +.. py:function:: spatial_vector(wx: Float, wy: Float, wz: Float, vx: Float, vy: Float, vz: Float, dtype: Float) + :noindex: + :nocontentsentry: + + Construct a 6D screw vector from six values. + + +.. py:function:: spatial_adjoint(r: Matrix[3,3,Float], s: Matrix[3,3,Float]) -> Matrix[6,6,Float] + + Construct a 6x6 spatial inertial matrix from two 3x3 diagonal blocks. + + +.. py:function:: spatial_dot(a: Vector[6,Float], b: Vector[6,Float]) -> Float + + Compute the dot product of two 6D screw vectors. + + +.. py:function:: spatial_cross(a: Vector[6,Float], b: Vector[6,Float]) -> Vector[6,Float] + + Compute the cross product of two 6D screw vectors. + + +.. py:function:: spatial_cross_dual(a: Vector[6,Float], b: Vector[6,Float]) -> Vector[6,Float] + + Compute the dual cross product of two 6D screw vectors. + + +.. py:function:: spatial_top(svec: Vector[6,Float]) -> Vector[3,Float] + + Return the top (first) part of a 6D screw vector. + + +.. py:function:: spatial_bottom(svec: Vector[6,Float]) -> Vector[3,Float] + + Return the bottom (second) part of a 6D screw vector. + + +.. py:function:: spatial_jacobian(S: Array[Vector[6,Float]], joint_parents: Array[int32], joint_qd_start: Array[int32], joint_start: int32, joint_count: int32, J_start: int32, J_out: Array[Float]) -> None + + +.. py:function:: spatial_mass(I_s: Array[Matrix[6,6,Float]], joint_start: int32, joint_count: int32, M_start: int32, M: Array[Float]) -> None + + + + +Utility +--------------- +.. py:function:: mlp(weights: Array[float32], bias: Array[float32], activation: Callable, index: int32, x: Array[float32], out: Array[float32]) -> None + + Evaluate a multi-layer perceptron (MLP) layer in the form: ``out = act(weights*x + bias)``. + + :param weights: A layer's network weights with dimensions ``(m, n)``. + :param bias: An array with dimensions ``(n)``. + :param activation: A ``wp.func`` function that takes a single scalar float as input and returns a scalar float as output + :param index: The batch item to process, typically each thread will process one item in the batch, in which case + index should be ``wp.tid()`` + :param x: The feature matrix with dimensions ``(n, b)`` + :param out: The network output with dimensions ``(m, b)`` + + :note: Feature and output matrices are transposed compared to some other frameworks such as PyTorch. + All matrices are assumed to be stored in flattened row-major memory layout (NumPy default). + + +.. py:function:: printf(fmt: str, *args: Any) -> None + + Allows printing formatted strings using C-style format specifiers. + + +.. py:function:: print(value: Any) -> None + + Print variable to stdout + + +.. py:function:: breakpoint() -> None + + Debugger breakpoint + + +.. py:function:: tid() -> int + + Return the current thread index for a 1D kernel launch. + + Note that this is the *global* index of the thread in the range [0, dim) + where dim is the parameter passed to kernel launch. + + This function may not be called from user-defined Warp functions. + + +.. py:function:: tid() -> Tuple[int, int] + :noindex: + :nocontentsentry: + + Return the current thread indices for a 2D kernel launch. + + Use ``i,j = wp.tid()`` syntax to retrieve the coordinates inside the kernel thread grid. + + This function may not be called from user-defined Warp functions. + + +.. py:function:: tid() -> Tuple[int, int, int] + :noindex: + :nocontentsentry: + + Return the current thread indices for a 3D kernel launch. + + Use ``i,j,k = wp.tid()`` syntax to retrieve the coordinates inside the kernel thread grid. + + This function may not be called from user-defined Warp functions. + + +.. py:function:: tid() -> Tuple[int, int, int, int] + :noindex: + :nocontentsentry: + + Return the current thread indices for a 4D kernel launch. + + Use ``i,j,k,l = wp.tid()`` syntax to retrieve the coordinates inside the kernel thread grid. + + This function may not be called from user-defined Warp functions. + + +.. py:function:: select(cond: bool, value_if_false: Any, value_if_true: Any) -> Any + + Select between two arguments, if ``cond`` is ``False`` then return ``value_if_false``, otherwise return ``value_if_true`` + + +.. py:function:: select(cond: int8, value_if_false: Any, value_if_true: Any) -> Any + :noindex: + :nocontentsentry: + + Select between two arguments, if ``cond`` is ``False`` then return ``value_if_false``, otherwise return ``value_if_true`` + + +.. py:function:: select(cond: uint8, value_if_false: Any, value_if_true: Any) -> Any + :noindex: + :nocontentsentry: + + Select between two arguments, if ``cond`` is ``False`` then return ``value_if_false``, otherwise return ``value_if_true`` + + +.. py:function:: select(cond: int16, value_if_false: Any, value_if_true: Any) -> Any + :noindex: + :nocontentsentry: + + Select between two arguments, if ``cond`` is ``False`` then return ``value_if_false``, otherwise return ``value_if_true`` + + +.. py:function:: select(cond: uint16, value_if_false: Any, value_if_true: Any) -> Any + :noindex: + :nocontentsentry: + + Select between two arguments, if ``cond`` is ``False`` then return ``value_if_false``, otherwise return ``value_if_true`` + + +.. py:function:: select(cond: int32, value_if_false: Any, value_if_true: Any) -> Any + :noindex: + :nocontentsentry: + + Select between two arguments, if ``cond`` is ``False`` then return ``value_if_false``, otherwise return ``value_if_true`` + + +.. py:function:: select(cond: uint32, value_if_false: Any, value_if_true: Any) -> Any + :noindex: + :nocontentsentry: + + Select between two arguments, if ``cond`` is ``False`` then return ``value_if_false``, otherwise return ``value_if_true`` + + +.. py:function:: select(cond: int64, value_if_false: Any, value_if_true: Any) -> Any + :noindex: + :nocontentsentry: + + Select between two arguments, if ``cond`` is ``False`` then return ``value_if_false``, otherwise return ``value_if_true`` + + +.. py:function:: select(cond: uint64, value_if_false: Any, value_if_true: Any) -> Any + :noindex: + :nocontentsentry: + + Select between two arguments, if ``cond`` is ``False`` then return ``value_if_false``, otherwise return ``value_if_true`` + + +.. py:function:: select(arr: Array[Any], value_if_false: Any, value_if_true: Any) -> Any + :noindex: + :nocontentsentry: + + Select between two arguments, if ``arr`` is null then return ``value_if_false``, otherwise return ``value_if_true`` + + +.. py:function:: atomic_add(arr: Array[Any], i: int32, value: Any) -> Any + + Atomically add ``value`` onto ``arr[i]`` and return the old value. + + +.. py:function:: atomic_add(arr: Array[Any], i: int32, j: int32, value: Any) -> Any + :noindex: + :nocontentsentry: + + Atomically add ``value`` onto ``arr[i,j]`` and return the old value. + + +.. py:function:: atomic_add(arr: Array[Any], i: int32, j: int32, k: int32, value: Any) -> Any + :noindex: + :nocontentsentry: + + Atomically add ``value`` onto ``arr[i,j,k]`` and return the old value. + + +.. py:function:: atomic_add(arr: Array[Any], i: int32, j: int32, k: int32, l: int32, value: Any) -> Any + :noindex: + :nocontentsentry: + + Atomically add ``value`` onto ``arr[i,j,k,l]`` and return the old value. + + +.. py:function:: atomic_add(arr: FabricArray[Any], i: int32, value: Any) -> Any + :noindex: + :nocontentsentry: + + Atomically add ``value`` onto ``arr[i]`` and return the old value. + + +.. py:function:: atomic_add(arr: FabricArray[Any], i: int32, j: int32, value: Any) -> Any + :noindex: + :nocontentsentry: + + Atomically add ``value`` onto ``arr[i,j]`` and return the old value. + + +.. py:function:: atomic_add(arr: FabricArray[Any], i: int32, j: int32, k: int32, value: Any) -> Any + :noindex: + :nocontentsentry: + + Atomically add ``value`` onto ``arr[i,j,k]`` and return the old value. + + +.. py:function:: atomic_add(arr: FabricArray[Any], i: int32, j: int32, k: int32, l: int32, value: Any) -> Any + :noindex: + :nocontentsentry: + + Atomically add ``value`` onto ``arr[i,j,k,l]`` and return the old value. + + +.. py:function:: atomic_add(arr: IndexedFabricArray[Any], i: int32, value: Any) -> Any + :noindex: + :nocontentsentry: + + Atomically add ``value`` onto ``arr[i]`` and return the old value. + + +.. py:function:: atomic_add(arr: IndexedFabricArray[Any], i: int32, j: int32, value: Any) -> Any + :noindex: + :nocontentsentry: + + Atomically add ``value`` onto ``arr[i,j]`` and return the old value. + + +.. py:function:: atomic_add(arr: IndexedFabricArray[Any], i: int32, j: int32, k: int32, value: Any) -> Any + :noindex: + :nocontentsentry: + + Atomically add ``value`` onto ``arr[i,j,k]`` and return the old value. + + +.. py:function:: atomic_add(arr: IndexedFabricArray[Any], i: int32, j: int32, k: int32, l: int32, value: Any) -> Any + :noindex: + :nocontentsentry: + + Atomically add ``value`` onto ``arr[i,j,k,l]`` and return the old value. + + +.. py:function:: atomic_sub(arr: Array[Any], i: int32, value: Any) -> Any + + Atomically subtract ``value`` onto ``arr[i]`` and return the old value. + + +.. py:function:: atomic_sub(arr: Array[Any], i: int32, j: int32, value: Any) -> Any + :noindex: + :nocontentsentry: + + Atomically subtract ``value`` onto ``arr[i,j]`` and return the old value. + + +.. py:function:: atomic_sub(arr: Array[Any], i: int32, j: int32, k: int32, value: Any) -> Any + :noindex: + :nocontentsentry: + + Atomically subtract ``value`` onto ``arr[i,j,k]`` and return the old value. + + +.. py:function:: atomic_sub(arr: Array[Any], i: int32, j: int32, k: int32, l: int32, value: Any) -> Any + :noindex: + :nocontentsentry: + + Atomically subtract ``value`` onto ``arr[i,j,k,l]`` and return the old value. + + +.. py:function:: atomic_sub(arr: FabricArray[Any], i: int32, value: Any) -> Any + :noindex: + :nocontentsentry: + + Atomically subtract ``value`` onto ``arr[i]`` and return the old value. + + +.. py:function:: atomic_sub(arr: FabricArray[Any], i: int32, j: int32, value: Any) -> Any + :noindex: + :nocontentsentry: + + Atomically subtract ``value`` onto ``arr[i,j]`` and return the old value. + + +.. py:function:: atomic_sub(arr: FabricArray[Any], i: int32, j: int32, k: int32, value: Any) -> Any + :noindex: + :nocontentsentry: + + Atomically subtract ``value`` onto ``arr[i,j,k]`` and return the old value. + + +.. py:function:: atomic_sub(arr: FabricArray[Any], i: int32, j: int32, k: int32, l: int32, value: Any) -> Any + :noindex: + :nocontentsentry: + + Atomically subtract ``value`` onto ``arr[i,j,k,l]`` and return the old value. + + +.. py:function:: atomic_sub(arr: IndexedFabricArray[Any], i: int32, value: Any) -> Any + :noindex: + :nocontentsentry: + + Atomically subtract ``value`` onto ``arr[i]`` and return the old value. + + +.. py:function:: atomic_sub(arr: IndexedFabricArray[Any], i: int32, j: int32, value: Any) -> Any + :noindex: + :nocontentsentry: + + Atomically subtract ``value`` onto ``arr[i,j]`` and return the old value. + + +.. py:function:: atomic_sub(arr: IndexedFabricArray[Any], i: int32, j: int32, k: int32, value: Any) -> Any + :noindex: + :nocontentsentry: + + Atomically subtract ``value`` onto ``arr[i,j,k]`` and return the old value. + + +.. py:function:: atomic_sub(arr: IndexedFabricArray[Any], i: int32, j: int32, k: int32, l: int32, value: Any) -> Any + :noindex: + :nocontentsentry: + + Atomically subtract ``value`` onto ``arr[i,j,k,l]`` and return the old value. + + +.. py:function:: atomic_min(arr: Array[Any], i: int32, value: Any) -> Any + + Compute the minimum of ``value`` and ``arr[i]``, atomically update the array, and return the old value. + + .. note:: The operation is only atomic on a per-component basis for vectors and matrices. + + +.. py:function:: atomic_min(arr: Array[Any], i: int32, j: int32, value: Any) -> Any + :noindex: + :nocontentsentry: + + Compute the minimum of ``value`` and ``arr[i,j]``, atomically update the array, and return the old value. + + .. note:: The operation is only atomic on a per-component basis for vectors and matrices. + + +.. py:function:: atomic_min(arr: Array[Any], i: int32, j: int32, k: int32, value: Any) -> Any + :noindex: + :nocontentsentry: + + Compute the minimum of ``value`` and ``arr[i,j,k]``, atomically update the array, and return the old value. + + .. note:: The operation is only atomic on a per-component basis for vectors and matrices. + + +.. py:function:: atomic_min(arr: Array[Any], i: int32, j: int32, k: int32, l: int32, value: Any) -> Any + :noindex: + :nocontentsentry: + + Compute the minimum of ``value`` and ``arr[i,j,k,l]``, atomically update the array, and return the old value. + + .. note:: The operation is only atomic on a per-component basis for vectors and matrices. + + +.. py:function:: atomic_min(arr: FabricArray[Any], i: int32, value: Any) -> Any + :noindex: + :nocontentsentry: + + Compute the minimum of ``value`` and ``arr[i]``, atomically update the array, and return the old value. + + .. note:: The operation is only atomic on a per-component basis for vectors and matrices. + + +.. py:function:: atomic_min(arr: FabricArray[Any], i: int32, j: int32, value: Any) -> Any + :noindex: + :nocontentsentry: + + Compute the minimum of ``value`` and ``arr[i,j]``, atomically update the array, and return the old value. + + .. note:: The operation is only atomic on a per-component basis for vectors and matrices. + + +.. py:function:: atomic_min(arr: FabricArray[Any], i: int32, j: int32, k: int32, value: Any) -> Any + :noindex: + :nocontentsentry: + + Compute the minimum of ``value`` and ``arr[i,j,k]``, atomically update the array, and return the old value. + + .. note:: The operation is only atomic on a per-component basis for vectors and matrices. + + +.. py:function:: atomic_min(arr: FabricArray[Any], i: int32, j: int32, k: int32, l: int32, value: Any) -> Any + :noindex: + :nocontentsentry: + + Compute the minimum of ``value`` and ``arr[i,j,k,l]``, atomically update the array, and return the old value. + + .. note:: The operation is only atomic on a per-component basis for vectors and matrices. + + +.. py:function:: atomic_min(arr: IndexedFabricArray[Any], i: int32, value: Any) -> Any + :noindex: + :nocontentsentry: + + Compute the minimum of ``value`` and ``arr[i]``, atomically update the array, and return the old value. + + .. note:: The operation is only atomic on a per-component basis for vectors and matrices. + + +.. py:function:: atomic_min(arr: IndexedFabricArray[Any], i: int32, j: int32, value: Any) -> Any + :noindex: + :nocontentsentry: + + Compute the minimum of ``value`` and ``arr[i,j]``, atomically update the array, and return the old value. + + .. note:: The operation is only atomic on a per-component basis for vectors and matrices. + + +.. py:function:: atomic_min(arr: IndexedFabricArray[Any], i: int32, j: int32, k: int32, value: Any) -> Any + :noindex: + :nocontentsentry: + + Compute the minimum of ``value`` and ``arr[i,j,k]``, atomically update the array, and return the old value. + + .. note:: The operation is only atomic on a per-component basis for vectors and matrices. + + +.. py:function:: atomic_min(arr: IndexedFabricArray[Any], i: int32, j: int32, k: int32, l: int32, value: Any) -> Any + :noindex: + :nocontentsentry: + + Compute the minimum of ``value`` and ``arr[i,j,k,l]``, atomically update the array, and return the old value. + + .. note:: The operation is only atomic on a per-component basis for vectors and matrices. + + +.. py:function:: atomic_max(arr: Array[Any], i: int32, value: Any) -> Any + + Compute the maximum of ``value`` and ``arr[i]``, atomically update the array, and return the old value. + + .. note:: The operation is only atomic on a per-component basis for vectors and matrices. + + +.. py:function:: atomic_max(arr: Array[Any], i: int32, j: int32, value: Any) -> Any + :noindex: + :nocontentsentry: + + Compute the maximum of ``value`` and ``arr[i,j]``, atomically update the array, and return the old value. + + .. note:: The operation is only atomic on a per-component basis for vectors and matrices. + + +.. py:function:: atomic_max(arr: Array[Any], i: int32, j: int32, k: int32, value: Any) -> Any + :noindex: + :nocontentsentry: + + Compute the maximum of ``value`` and ``arr[i,j,k]``, atomically update the array, and return the old value. + + .. note:: The operation is only atomic on a per-component basis for vectors and matrices. + + +.. py:function:: atomic_max(arr: Array[Any], i: int32, j: int32, k: int32, l: int32, value: Any) -> Any + :noindex: + :nocontentsentry: + + Compute the maximum of ``value`` and ``arr[i,j,k,l]``, atomically update the array, and return the old value. + + .. note:: The operation is only atomic on a per-component basis for vectors and matrices. + + +.. py:function:: atomic_max(arr: FabricArray[Any], i: int32, value: Any) -> Any + :noindex: + :nocontentsentry: + + Compute the maximum of ``value`` and ``arr[i]``, atomically update the array, and return the old value. + + .. note:: The operation is only atomic on a per-component basis for vectors and matrices. + + +.. py:function:: atomic_max(arr: FabricArray[Any], i: int32, j: int32, value: Any) -> Any + :noindex: + :nocontentsentry: + + Compute the maximum of ``value`` and ``arr[i,j]``, atomically update the array, and return the old value. + + .. note:: The operation is only atomic on a per-component basis for vectors and matrices. + + +.. py:function:: atomic_max(arr: FabricArray[Any], i: int32, j: int32, k: int32, value: Any) -> Any + :noindex: + :nocontentsentry: + + Compute the maximum of ``value`` and ``arr[i,j,k]``, atomically update the array, and return the old value. + + .. note:: The operation is only atomic on a per-component basis for vectors and matrices. + + +.. py:function:: atomic_max(arr: FabricArray[Any], i: int32, j: int32, k: int32, l: int32, value: Any) -> Any + :noindex: + :nocontentsentry: + + Compute the maximum of ``value`` and ``arr[i,j,k,l]``, atomically update the array, and return the old value. + + .. note:: The operation is only atomic on a per-component basis for vectors and matrices. + + +.. py:function:: atomic_max(arr: IndexedFabricArray[Any], i: int32, value: Any) -> Any + :noindex: + :nocontentsentry: + + Compute the maximum of ``value`` and ``arr[i]``, atomically update the array, and return the old value. + + .. note:: The operation is only atomic on a per-component basis for vectors and matrices. + + +.. py:function:: atomic_max(arr: IndexedFabricArray[Any], i: int32, j: int32, value: Any) -> Any + :noindex: + :nocontentsentry: + + Compute the maximum of ``value`` and ``arr[i,j]``, atomically update the array, and return the old value. + + .. note:: The operation is only atomic on a per-component basis for vectors and matrices. + + +.. py:function:: atomic_max(arr: IndexedFabricArray[Any], i: int32, j: int32, k: int32, value: Any) -> Any + :noindex: + :nocontentsentry: + + Compute the maximum of ``value`` and ``arr[i,j,k]``, atomically update the array, and return the old value. + + .. note:: The operation is only atomic on a per-component basis for vectors and matrices. + + +.. py:function:: atomic_max(arr: IndexedFabricArray[Any], i: int32, j: int32, k: int32, l: int32, value: Any) -> Any + :noindex: + :nocontentsentry: + + Compute the maximum of ``value`` and ``arr[i,j,k,l]``, atomically update the array, and return the old value. + + .. note:: The operation is only atomic on a per-component basis for vectors and matrices. + + +.. py:function:: lerp(a: Float, b: Float, t: Float) -> Float + + Linearly interpolate two values ``a`` and ``b`` using factor ``t``, computed as ``a*(1-t) + b*t`` + + +.. py:function:: lerp(a: Vector[Any,Float], b: Vector[Any,Float], t: Float) -> Vector[Any,Float] + :noindex: + :nocontentsentry: + + Linearly interpolate two values ``a`` and ``b`` using factor ``t``, computed as ``a*(1-t) + b*t`` + + +.. py:function:: lerp(a: Matrix[Any,Any,Float], b: Matrix[Any,Any,Float], t: Float) -> Matrix[Any,Any,Float] + :noindex: + :nocontentsentry: + + Linearly interpolate two values ``a`` and ``b`` using factor ``t``, computed as ``a*(1-t) + b*t`` + + +.. py:function:: lerp(a: Quaternion[Float], b: Quaternion[Float], t: Float) -> Quaternion[Float] + :noindex: + :nocontentsentry: + + Linearly interpolate two values ``a`` and ``b`` using factor ``t``, computed as ``a*(1-t) + b*t`` + + +.. py:function:: lerp(a: Transformation[Float], b: Transformation[Float], t: Float) -> Transformation[Float] + :noindex: + :nocontentsentry: + + Linearly interpolate two values ``a`` and ``b`` using factor ``t``, computed as ``a*(1-t) + b*t`` + + +.. py:function:: smoothstep(a: Float, b: Float, x: Float) -> Float + + Smoothly interpolate between two values ``a`` and ``b`` using a factor ``x``, + and return a result between 0 and 1 using a cubic Hermite interpolation after clamping. + + +.. py:function:: expect_near(a: Float, b: Float, tolerance: Float) -> None + + Prints an error to stdout if ``a`` and ``b`` are not closer than tolerance in magnitude + + +.. py:function:: expect_near(a: vec3f, b: vec3f, tolerance: float32) -> None + :noindex: + :nocontentsentry: + + Prints an error to stdout if any element of ``a`` and ``b`` are not closer than tolerance in magnitude + + + + +Geometry +--------------- +.. autoclass:: BvhQuery +.. py:function:: bvh_query_aabb(id: uint64, low: vec3f, high: vec3f) -> bvh_query_t + + Construct an axis-aligned bounding box query against a BVH object. + + This query can be used to iterate over all bounds inside a BVH. + + :param id: The BVH identifier + :param low: The lower bound of the bounding box in BVH space + :param high: The upper bound of the bounding box in BVH space + + +.. py:function:: bvh_query_ray(id: uint64, start: vec3f, dir: vec3f) -> bvh_query_t + + Construct a ray query against a BVH object. + + This query can be used to iterate over all bounds that intersect the ray. + + :param id: The BVH identifier + :param start: The start of the ray in BVH space + :param dir: The direction of the ray in BVH space + + +.. py:function:: bvh_query_next(query: bvh_query_t, index: int32) -> bool + + Move to the next bound returned by the query. + The index of the current bound is stored in ``index``, returns ``False`` if there are no more overlapping bound. + + +.. autoclass:: MeshQueryPoint +.. py:function:: mesh_query_point(id: uint64, point: vec3f, max_dist: float32) -> mesh_query_point_t + + Computes the closest point on the :class:`Mesh` with identifier ``id`` to the given ``point`` in space. + + Identifies the sign of the distance using additional ray-casts to determine if the point is inside or outside. + This method is relatively robust, but does increase computational cost. + See below for additional sign determination methods. + + :param id: The mesh identifier + :param point: The point in space to query + :param max_dist: Mesh faces above this distance will not be considered by the query + + +.. py:function:: mesh_query_point_no_sign(id: uint64, point: vec3f, max_dist: float32) -> mesh_query_point_t + + Computes the closest point on the :class:`Mesh` with identifier ``id`` to the given ``point`` in space. + + This method does not compute the sign of the point (inside/outside) which makes it faster than other point query methods. + + :param id: The mesh identifier + :param point: The point in space to query + :param max_dist: Mesh faces above this distance will not be considered by the query + + +.. py:function:: mesh_query_furthest_point_no_sign(id: uint64, point: vec3f, min_dist: float32) -> mesh_query_point_t + + Computes the furthest point on the mesh with identifier `id` to the given point in space. + + This method does not compute the sign of the point (inside/outside). + + :param id: The mesh identifier + :param point: The point in space to query + :param min_dist: Mesh faces below this distance will not be considered by the query + + +.. py:function:: mesh_query_point_sign_normal(id: uint64, point: vec3f, max_dist: float32, epsilon: float32) -> mesh_query_point_t + + Computes the closest point on the :class:`Mesh` with identifier ``id`` to the given ``point`` in space. + + Identifies the sign of the distance (inside/outside) using the angle-weighted pseudo normal. + This approach to sign determination is robust for well conditioned meshes that are watertight and non-self intersecting. + It is also comparatively fast to compute. + + :param id: The mesh identifier + :param point: The point in space to query + :param max_dist: Mesh faces above this distance will not be considered by the query + :param epsilon: Epsilon treating distance values as equal, when locating the minimum distance vertex/face/edge, as a + fraction of the average edge length, also for treating closest point as being on edge/vertex default 1e-3 + + +.. py:function:: mesh_query_point_sign_winding_number(id: uint64, point: vec3f, max_dist: float32, accuracy: float32, threshold: float32) -> mesh_query_point_t + + Computes the closest point on the :class:`Mesh` with identifier ``id`` to the given point in space. + + Identifies the sign using the winding number of the mesh relative to the query point. This method of sign determination is robust for poorly conditioned meshes + and provides a smooth approximation to sign even when the mesh is not watertight. This method is the most robust and accurate of the sign determination meshes + but also the most expensive. + + .. note:: The :class:`Mesh` object must be constructed with ``support_winding_number=True`` for this method to return correct results. + + :param id: The mesh identifier + :param point: The point in space to query + :param max_dist: Mesh faces above this distance will not be considered by the query + :param accuracy: Accuracy for computing the winding number with fast winding number method utilizing second-order dipole approximation, default 2.0 + :param threshold: The threshold of the winding number to be considered inside, default 0.5 + + +.. autoclass:: MeshQueryRay +.. py:function:: mesh_query_ray(id: uint64, start: vec3f, dir: vec3f, max_t: float32) -> mesh_query_ray_t + + Computes the closest ray hit on the :class:`Mesh` with identifier ``id``. + + :param id: The mesh identifier + :param start: The start point of the ray + :param dir: The ray direction (should be normalized) + :param max_t: The maximum distance along the ray to check for intersections + + +.. autoclass:: MeshQueryAABB +.. py:function:: mesh_query_aabb(id: uint64, low: vec3f, high: vec3f) -> mesh_query_aabb_t + + Construct an axis-aligned bounding box query against a :class:`Mesh`. + + This query can be used to iterate over all triangles inside a volume. + + :param id: The mesh identifier + :param low: The lower bound of the bounding box in mesh space + :param high: The upper bound of the bounding box in mesh space + + +.. py:function:: mesh_query_aabb_next(query: mesh_query_aabb_t, index: int32) -> bool + + Move to the next triangle overlapping the query bounding box. + + The index of the current face is stored in ``index``, returns ``False`` if there are no more overlapping triangles. + + +.. py:function:: mesh_eval_position(id: uint64, face: int32, bary_u: float32, bary_v: float32) -> vec3f + + Evaluates the position on the :class:`Mesh` given a face index and barycentric coordinates. + + +.. py:function:: mesh_eval_velocity(id: uint64, face: int32, bary_u: float32, bary_v: float32) -> vec3f + + Evaluates the velocity on the :class:`Mesh` given a face index and barycentric coordinates. + + +.. autoclass:: HashGridQuery +.. py:function:: hash_grid_query(id: uint64, point: vec3f, max_dist: float32) -> hash_grid_query_t + + Construct a point query against a :class:`HashGrid`. + + This query can be used to iterate over all neighboring point within a fixed radius from the query point. + + +.. py:function:: hash_grid_query_next(query: hash_grid_query_t, index: int32) -> bool + + Move to the next point in the hash grid query. + + The index of the current neighbor is stored in ``index``, returns ``False`` if there are no more neighbors. + + +.. py:function:: hash_grid_point_id(id: uint64, index: int32) -> int + + Return the index of a point in the :class:`HashGrid`. + + This can be used to reorder threads such that grid traversal occurs in a spatially coherent order. + + Returns -1 if the :class:`HashGrid` has not been reserved. + + +.. py:function:: intersect_tri_tri(v0: vec3f, v1: vec3f, v2: vec3f, u0: vec3f, u1: vec3f, u2: vec3f) -> int + + Tests for intersection between two triangles (v0, v1, v2) and (u0, u1, u2) using Moller's method. + + Returns > 0 if triangles intersect. + + +.. py:function:: mesh_get(id: uint64) -> Mesh + + Retrieves the mesh given its index. [1]_ + + +.. py:function:: mesh_eval_face_normal(id: uint64, face: int32) -> vec3f + + Evaluates the face normal the mesh given a face index. + + +.. py:function:: mesh_get_point(id: uint64, index: int32) -> vec3f + + Returns the point of the mesh given a index. + + +.. py:function:: mesh_get_velocity(id: uint64, index: int32) -> vec3f + + Returns the velocity of the mesh given a index. + + +.. py:function:: mesh_get_index(id: uint64, index: int32) -> int + + Returns the point-index of the mesh given a face-vertex index. + + +.. py:function:: closest_point_edge_edge(p1: vec3f, q1: vec3f, p2: vec3f, q2: vec3f, epsilon: float32) -> vec3f + + Finds the closest points between two edges. + + Returns barycentric weights to the points on each edge, as well as the closest distance between the edges. + + :param p1: First point of first edge + :param q1: Second point of first edge + :param p2: First point of second edge + :param q2: Second point of second edge + :param epsilon: Zero tolerance for determining if points in an edge are degenerate. + :param out: vec3 output containing (s,t,d), where `s` in [0,1] is the barycentric weight for the first edge, `t` is the barycentric weight for the second edge, and `d` is the distance between the two edges at these two closest points. + + + + +Volumes +--------------- +.. py:function:: volume_sample(id: uint64, uvw: vec3f, sampling_mode: int32, dtype: Any) -> Any + + Sample the volume of type `dtype` given by ``id`` at the volume local-space point ``uvw``. + + Interpolation should be :attr:`warp.Volume.CLOSEST` or :attr:`wp.Volume.LINEAR.` + + +.. py:function:: volume_sample_grad(id: uint64, uvw: vec3f, sampling_mode: int32, grad: Any, dtype: Any) -> Any + + Sample the volume given by ``id`` and its gradient at the volume local-space point ``uvw``. + + Interpolation should be :attr:`warp.Volume.CLOSEST` or :attr:`wp.Volume.LINEAR.` + + +.. py:function:: volume_lookup(id: uint64, i: int32, j: int32, k: int32, dtype: Any) -> Any + + Returns the value of voxel with coordinates ``i``, ``j``, ``k`` for a volume of type type `dtype`. + + If the voxel at this index does not exist, this function returns the background value. + + +.. py:function:: volume_store(id: uint64, i: int32, j: int32, k: int32, value: Any) -> None + + Store ``value`` at the voxel with coordinates ``i``, ``j``, ``k``. + + +.. py:function:: volume_sample_f(id: uint64, uvw: vec3f, sampling_mode: int32) -> float + + Sample the volume given by ``id`` at the volume local-space point ``uvw``. + + Interpolation should be :attr:`warp.Volume.CLOSEST` or :attr:`wp.Volume.LINEAR.` + + +.. py:function:: volume_sample_grad_f(id: uint64, uvw: vec3f, sampling_mode: int32, grad: vec3f) -> float + + Sample the volume and its gradient given by ``id`` at the volume local-space point ``uvw``. + + Interpolation should be :attr:`warp.Volume.CLOSEST` or :attr:`wp.Volume.LINEAR.` + + +.. py:function:: volume_lookup_f(id: uint64, i: int32, j: int32, k: int32) -> float + + Returns the value of voxel with coordinates ``i``, ``j``, ``k``. + + If the voxel at this index does not exist, this function returns the background value + + +.. py:function:: volume_store_f(id: uint64, i: int32, j: int32, k: int32, value: float32) -> None + + Store ``value`` at the voxel with coordinates ``i``, ``j``, ``k``. + + +.. py:function:: volume_sample_v(id: uint64, uvw: vec3f, sampling_mode: int32) -> vec3f + + Sample the vector volume given by ``id`` at the volume local-space point ``uvw``. + + Interpolation should be :attr:`warp.Volume.CLOSEST` or :attr:`wp.Volume.LINEAR.` + + +.. py:function:: volume_lookup_v(id: uint64, i: int32, j: int32, k: int32) -> vec3f + + Returns the vector value of voxel with coordinates ``i``, ``j``, ``k``. + + If the voxel at this index does not exist, this function returns the background value. + + +.. py:function:: volume_store_v(id: uint64, i: int32, j: int32, k: int32, value: vec3f) -> None + + Store ``value`` at the voxel with coordinates ``i``, ``j``, ``k``. + + +.. py:function:: volume_sample_i(id: uint64, uvw: vec3f) -> int + + Sample the :class:`int32` volume given by ``id`` at the volume local-space point ``uvw``. + + +.. py:function:: volume_lookup_i(id: uint64, i: int32, j: int32, k: int32) -> int + + Returns the :class:`int32` value of voxel with coordinates ``i``, ``j``, ``k``. + + If the voxel at this index does not exist, this function returns the background value. + + +.. py:function:: volume_store_i(id: uint64, i: int32, j: int32, k: int32, value: int32) -> None + + Store ``value`` at the voxel with coordinates ``i``, ``j``, ``k``. + + +.. py:function:: volume_sample_index(id: uint64, uvw: vec3f, sampling_mode: int32, voxel_data: Array[Any], background: Any) -> Any + + Sample the volume given by ``id`` at the volume local-space point ``uvw``. + + Values for allocated voxels are read from the ``voxel_data`` array, and `background` is used as the value of non-existing voxels. + Interpolation should be :attr:`warp.Volume.CLOSEST` or :attr:`wp.Volume.LINEAR`. + This function is available for both index grids and classical volumes. + + + +.. py:function:: volume_sample_grad_index(id: uint64, uvw: vec3f, sampling_mode: int32, voxel_data: Array[Any], background: Any, grad: Any) -> Any + + Sample the volume given by ``id`` and its gradient at the volume local-space point ``uvw``. + + Values for allocated voxels are read from the ``voxel_data`` array, and `background` is used as the value of non-existing voxels. + Interpolation should be :attr:`warp.Volume.CLOSEST` or :attr:`wp.Volume.LINEAR`. + This function is available for both index grids and classical volumes. + + + +.. py:function:: volume_lookup_index(id: uint64, i: int32, j: int32, k: int32) -> int32 + + Returns the index associated to the voxel with coordinates ``i``, ``j``, ``k``. + + If the voxel at this index does not exist, this function returns -1. + This function is available for both index grids and classical volumes. + + + +.. py:function:: volume_index_to_world(id: uint64, uvw: vec3f) -> vec3f + + Transform a point ``uvw`` defined in volume index space to world space given the volume's intrinsic affine transformation. + + +.. py:function:: volume_world_to_index(id: uint64, xyz: vec3f) -> vec3f + + Transform a point ``xyz`` defined in volume world space to the volume's index space given the volume's intrinsic affine transformation. + + +.. py:function:: volume_index_to_world_dir(id: uint64, uvw: vec3f) -> vec3f + + Transform a direction ``uvw`` defined in volume index space to world space given the volume's intrinsic affine transformation. + + +.. py:function:: volume_world_to_index_dir(id: uint64, xyz: vec3f) -> vec3f + + Transform a direction ``xyz`` defined in volume world space to the volume's index space given the volume's intrinsic affine transformation. + + + + +Random +--------------- +.. py:function:: rand_init(seed: int32) -> uint32 + + Initialize a new random number generator given a user-defined seed. Returns a 32-bit integer representing the RNG state. + + +.. py:function:: rand_init(seed: int32, offset: int32) -> uint32 + :noindex: + :nocontentsentry: + + Initialize a new random number generator given a user-defined seed and an offset. + + This alternative constructor can be useful in parallel programs, where a kernel as a whole should share a seed, + but each thread should generate uncorrelated values. In this case usage should be ``r = rand_init(seed, tid)`` + + +.. py:function:: randi(state: uint32) -> int + + Return a random integer in the range [0, 2^32). + + +.. py:function:: randi(state: uint32, low: int32, high: int32) -> int + :noindex: + :nocontentsentry: + + Return a random integer between [low, high). + + +.. py:function:: randf(state: uint32) -> float + + Return a random float between [0.0, 1.0). + + +.. py:function:: randf(state: uint32, low: float32, high: float32) -> float + :noindex: + :nocontentsentry: + + Return a random float between [low, high). + + +.. py:function:: randn(state: uint32) -> float + + Sample a normal distribution. + + +.. py:function:: sample_cdf(state: uint32, cdf: Array[float32]) -> int + + Inverse-transform sample a cumulative distribution function. + + +.. py:function:: sample_triangle(state: uint32) -> vec2f + + Uniformly sample a triangle. Returns sample barycentric coordinates. + + +.. py:function:: sample_unit_ring(state: uint32) -> vec2f + + Uniformly sample a ring in the xy plane. + + +.. py:function:: sample_unit_disk(state: uint32) -> vec2f + + Uniformly sample a disk in the xy plane. + + +.. py:function:: sample_unit_sphere_surface(state: uint32) -> vec3f + + Uniformly sample a unit sphere surface. + + +.. py:function:: sample_unit_sphere(state: uint32) -> vec3f + + Uniformly sample a unit sphere. + + +.. py:function:: sample_unit_hemisphere_surface(state: uint32) -> vec3f + + Uniformly sample a unit hemisphere surface. + + +.. py:function:: sample_unit_hemisphere(state: uint32) -> vec3f + + Uniformly sample a unit hemisphere. + + +.. py:function:: sample_unit_square(state: uint32) -> vec2f + + Uniformly sample a unit square. + + +.. py:function:: sample_unit_cube(state: uint32) -> vec3f + + Uniformly sample a unit cube. + + +.. py:function:: poisson(state: uint32, lam: float32) -> uint32 + + Generate a random sample from a Poisson distribution. + + :param state: RNG state + :param lam: The expected value of the distribution + + +.. py:function:: noise(state: uint32, x: float32) -> float + + Non-periodic Perlin-style noise in 1D. + + +.. py:function:: noise(state: uint32, xy: vec2f) -> float + :noindex: + :nocontentsentry: + + Non-periodic Perlin-style noise in 2D. + + +.. py:function:: noise(state: uint32, xyz: vec3f) -> float + :noindex: + :nocontentsentry: + + Non-periodic Perlin-style noise in 3D. + + +.. py:function:: noise(state: uint32, xyzt: vec4f) -> float + :noindex: + :nocontentsentry: + + Non-periodic Perlin-style noise in 4D. + + +.. py:function:: pnoise(state: uint32, x: float32, px: int32) -> float + + Periodic Perlin-style noise in 1D. + + +.. py:function:: pnoise(state: uint32, xy: vec2f, px: int32, py: int32) -> float + :noindex: + :nocontentsentry: + + Periodic Perlin-style noise in 2D. + + +.. py:function:: pnoise(state: uint32, xyz: vec3f, px: int32, py: int32, pz: int32) -> float + :noindex: + :nocontentsentry: + + Periodic Perlin-style noise in 3D. + + +.. py:function:: pnoise(state: uint32, xyzt: vec4f, px: int32, py: int32, pz: int32, pt: int32) -> float + :noindex: + :nocontentsentry: + + Periodic Perlin-style noise in 4D. + + +.. py:function:: curlnoise(state: uint32, xy: vec2f, octaves: uint32, lacunarity: float32, gain: float32) -> vec2f + + Divergence-free vector field based on the gradient of a Perlin noise function. [1]_ + + +.. py:function:: curlnoise(state: uint32, xyz: vec3f, octaves: uint32, lacunarity: float32, gain: float32) -> vec3f + :noindex: + :nocontentsentry: + + Divergence-free vector field based on the curl of three Perlin noise functions. [1]_ + + +.. py:function:: curlnoise(state: uint32, xyzt: vec4f, octaves: uint32, lacunarity: float32, gain: float32) -> vec3f + :noindex: + :nocontentsentry: + + Divergence-free vector field based on the curl of three Perlin noise functions. [1]_ + + + + +Other +--------------- +.. py:function:: lower_bound(arr: Array[Scalar], value: Scalar) -> int + + Search a sorted array ``arr`` for the closest element greater than or equal to ``value``. + + +.. py:function:: lower_bound(arr: Array[Scalar], arr_begin: int32, arr_end: int32, value: Scalar) -> int + :noindex: + :nocontentsentry: + + Search a sorted array ``arr`` in the range [arr_begin, arr_end) for the closest element greater than or equal to ``value``. + + +.. py:function:: bit_and(a: Int, b: Int) -> Int + + +.. py:function:: bit_or(a: Int, b: Int) -> Int + + +.. py:function:: bit_xor(a: Int, b: Int) -> Int + + +.. py:function:: lshift(a: Int, b: Int) -> Int + + +.. py:function:: rshift(a: Int, b: Int) -> Int + + +.. py:function:: invert(a: Int) -> Int + + + + +Operators +--------------- +.. py:function:: add(a: Scalar, b: Scalar) -> Scalar + + +.. py:function:: add(a: Vector[Any,Scalar], b: Vector[Any,Scalar]) -> Vector[Any,Scalar] + :noindex: + :nocontentsentry: + + +.. py:function:: add(a: Quaternion[Scalar], b: Quaternion[Scalar]) -> Quaternion[Scalar] + :noindex: + :nocontentsentry: + + +.. py:function:: add(a: Matrix[Any,Any,Scalar], b: Matrix[Any,Any,Scalar]) -> Matrix[Any,Any,Scalar] + :noindex: + :nocontentsentry: + + +.. py:function:: add(a: Transformation[Scalar], b: Transformation[Scalar]) -> Transformation[Scalar] + :noindex: + :nocontentsentry: + + +.. py:function:: sub(a: Scalar, b: Scalar) -> Scalar + + +.. py:function:: sub(a: Vector[Any,Scalar], b: Vector[Any,Scalar]) -> Vector[Any,Scalar] + :noindex: + :nocontentsentry: + + +.. py:function:: sub(a: Matrix[Any,Any,Scalar], b: Matrix[Any,Any,Scalar]) -> Matrix[Any,Any,Scalar] + :noindex: + :nocontentsentry: + + +.. py:function:: sub(a: Quaternion[Scalar], b: Quaternion[Scalar]) -> Quaternion[Scalar] + :noindex: + :nocontentsentry: + + +.. py:function:: sub(a: Transformation[Scalar], b: Transformation[Scalar]) -> Transformation[Scalar] + :noindex: + :nocontentsentry: + + +.. py:function:: mul(a: Scalar, b: Scalar) -> Scalar + + +.. py:function:: mul(a: Vector[Any,Scalar], b: Scalar) -> Vector[Any,Scalar] + :noindex: + :nocontentsentry: + + +.. py:function:: mul(a: Scalar, b: Vector[Any,Scalar]) -> Vector[Any,Scalar] + :noindex: + :nocontentsentry: + + +.. py:function:: mul(a: Quaternion[Scalar], b: Scalar) -> Quaternion[Scalar] + :noindex: + :nocontentsentry: + + +.. py:function:: mul(a: Scalar, b: Quaternion[Scalar]) -> Quaternion[Scalar] + :noindex: + :nocontentsentry: + + +.. py:function:: mul(a: Quaternion[Scalar], b: Quaternion[Scalar]) -> Quaternion[Scalar] + :noindex: + :nocontentsentry: + + +.. py:function:: mul(a: Scalar, b: Matrix[Any,Any,Scalar]) -> Matrix[Any,Any,Scalar] + :noindex: + :nocontentsentry: + + +.. py:function:: mul(a: Matrix[Any,Any,Scalar], b: Scalar) -> Matrix[Any,Any,Scalar] + :noindex: + :nocontentsentry: + + +.. py:function:: mul(a: Matrix[Any,Any,Scalar], b: Vector[Any,Scalar]) -> Vector[Any,Scalar] + :noindex: + :nocontentsentry: + + +.. py:function:: mul(a: Vector[Any,Scalar], b: Matrix[Any,Any,Scalar]) -> Vector[Any,Scalar] + :noindex: + :nocontentsentry: + + +.. py:function:: mul(a: Matrix[Any,Any,Scalar], b: Matrix[Any,Any,Scalar]) -> Matrix[Any,Any,Scalar] + :noindex: + :nocontentsentry: + + +.. py:function:: mul(a: Transformation[Scalar], b: Transformation[Scalar]) -> Transformation[Scalar] + :noindex: + :nocontentsentry: + + +.. py:function:: mul(a: Scalar, b: Transformation[Scalar]) -> Transformation[Scalar] + :noindex: + :nocontentsentry: + + +.. py:function:: mul(a: Transformation[Scalar], b: Scalar) -> Transformation[Scalar] + :noindex: + :nocontentsentry: + + +.. py:function:: mod(a: Scalar, b: Scalar) -> Scalar + + Modulo operation using truncated division. + + +.. py:function:: div(a: Scalar, b: Scalar) -> Scalar + + +.. py:function:: div(a: Vector[Any,Scalar], b: Scalar) -> Vector[Any,Scalar] + :noindex: + :nocontentsentry: + + +.. py:function:: div(a: Scalar, b: Vector[Any,Scalar]) -> Vector[Any,Scalar] + :noindex: + :nocontentsentry: + + +.. py:function:: div(a: Matrix[Any,Any,Scalar], b: Scalar) -> Matrix[Any,Any,Scalar] + :noindex: + :nocontentsentry: + + +.. py:function:: div(a: Scalar, b: Matrix[Any,Any,Scalar]) -> Matrix[Any,Any,Scalar] + :noindex: + :nocontentsentry: + + +.. py:function:: div(a: Quaternion[Scalar], b: Scalar) -> Quaternion[Scalar] + :noindex: + :nocontentsentry: + + +.. py:function:: div(a: Scalar, b: Quaternion[Scalar]) -> Quaternion[Scalar] + :noindex: + :nocontentsentry: + + +.. py:function:: floordiv(a: Scalar, b: Scalar) -> Scalar + + +.. py:function:: pos(x: Scalar) -> Scalar + + +.. py:function:: pos(x: Vector[Any,Scalar]) -> Vector[Any,Scalar] + :noindex: + :nocontentsentry: + + +.. py:function:: pos(x: Quaternion[Scalar]) -> Quaternion[Scalar] + :noindex: + :nocontentsentry: + + +.. py:function:: pos(x: Matrix[Any,Any,Scalar]) -> Matrix[Any,Any,Scalar] + :noindex: + :nocontentsentry: + + +.. py:function:: neg(x: Scalar) -> Scalar + + +.. py:function:: neg(x: Vector[Any,Scalar]) -> Vector[Any,Scalar] + :noindex: + :nocontentsentry: + + +.. py:function:: neg(x: Quaternion[Scalar]) -> Quaternion[Scalar] + :noindex: + :nocontentsentry: + + +.. py:function:: neg(x: Matrix[Any,Any,Scalar]) -> Matrix[Any,Any,Scalar] + :noindex: + :nocontentsentry: + + +.. py:function:: unot(a: bool) -> bool + + +.. py:function:: unot(a: int8) -> bool + :noindex: + :nocontentsentry: + + +.. py:function:: unot(a: uint8) -> bool + :noindex: + :nocontentsentry: + + +.. py:function:: unot(a: int16) -> bool + :noindex: + :nocontentsentry: + + +.. py:function:: unot(a: uint16) -> bool + :noindex: + :nocontentsentry: + + +.. py:function:: unot(a: int32) -> bool + :noindex: + :nocontentsentry: + + +.. py:function:: unot(a: uint32) -> bool + :noindex: + :nocontentsentry: + + +.. py:function:: unot(a: int64) -> bool + :noindex: + :nocontentsentry: + + +.. py:function:: unot(a: uint64) -> bool + :noindex: + :nocontentsentry: + + +.. py:function:: unot(a: Array[Any]) -> bool + :noindex: + :nocontentsentry: + + +.. rubric:: Footnotes +.. [1] Function gradients have not been implemented for backpropagation. diff --git a/_sources/modules/generics.rst.txt b/_sources/modules/generics.rst.txt new file mode 100644 index 00000000..7298c189 --- /dev/null +++ b/_sources/modules/generics.rst.txt @@ -0,0 +1,306 @@ +Generics +======== + +Warp supports writing generic kernels and functions, which act as templates that can be instantiated with different concrete types. +This allows you to write code once and reuse it with multiple data types. +The concepts discussed on this page also apply to :ref:`Runtime Kernel Creation`. + +Generic Kernels +--------------- + +Generic kernel definition syntax is the same as regular kernels, but you can use ``typing.Any`` in place of concrete types: + +.. code:: python + + from typing import Any + + # generic kernel definition using Any as a placeholder for concrete types + @wp.kernel + def scale(x: wp.array(dtype=Any), s: Any): + i = wp.tid() + x[i] = s * x[i] + + data = [1, 2, 3, 4, 5, 6, 7, 8, 9] + n = len(data) + + x16 = wp.array(data, dtype=wp.float16) + x32 = wp.array(data, dtype=wp.float32) + x64 = wp.array(data, dtype=wp.float64) + + # run the generic kernel with different data types + wp.launch(scale, dim=n, inputs=[x16, wp.float16(3)]) + wp.launch(scale, dim=n, inputs=[x32, wp.float32(3)]) + wp.launch(scale, dim=n, inputs=[x64, wp.float64(3)]) + + print(x16) + print(x32) + print(x64) + +Under the hood, Warp will automatically generate new instances of the generic kernel to match the given argument types. + + +Type Inference +~~~~~~~~~~~~~~ + +When a generic kernel is being launched, Warp infers the concrete types from the arguments. ``wp.launch()`` handles generic kernels without any special syntax, but we should be mindful of the data types passed as arguments to make sure that the correct types are inferred. + + - Scalars can be passed as regular Python numeric values (e.g., ``42`` or ``0.5``). Python integers are interpreted as ``wp.int32`` and Python floating point values are interpreted as ``wp.float32``. To specify a different data type and to avoid ambiguity, Warp data types should be used instead (e.g., ``wp.int64(42)`` or ``wp.float16(0.5)``). + + - Vectors and matrices should be passed as Warp types rather than tuples or lists (e.g., ``wp.vec3f(1.0, 2.0, 3.0)`` or ``wp.mat22h([[1.0, 0.0], [0.0, 1.0]])``). + + - Warp arrays and structs can be passed normally. + +.. _implicit_instantiation: + +Implicit Instantiation +~~~~~~~~~~~~~~~~~~~~~~ + +When you launch a generic kernel with a new set of data types, Warp automatically creates a new instance of this kernel with the given types. This is convenient, but there are some downsides to this implicit instantiation. + +Consider these three generic kernel launches: + +.. code:: python + + wp.launch(scale, dim=n, inputs=[x16, wp.float16(3)]) + wp.launch(scale, dim=n, inputs=[x32, wp.float32(3)]) + wp.launch(scale, dim=n, inputs=[x64, wp.float64(3)]) + +During each one of these launches, a new kernel instance is being generated, which forces the module to be reloaded. You might see something like this in the output: + +.. code:: text + + Module __main__ load on device 'cuda:0' took 170.37 ms + Module __main__ load on device 'cuda:0' took 171.43 ms + Module __main__ load on device 'cuda:0' took 179.49 ms + +This leads to a couple of potential problems: + + - The overhead of repeatedly rebuilding the modules can impact the overall performance of the program. + + - Module reloading during graph capture is not allowed on older CUDA drivers, which will cause captures to fail. + +Explicit instantiation can be used to overcome these issues. + + +.. _explicit_instantiation: + +Explicit Instantiation +~~~~~~~~~~~~~~~~~~~~~~ + +Warp allows explicitly declaring instances of generic kernels with different types. One way is to use the ``@wp.overload`` decorator: + +.. code:: python + + @wp.overload + def scale(x: wp.array(dtype=wp.float16), s: wp.float16): + ... + + @wp.overload + def scale(x: wp.array(dtype=wp.float32), s: wp.float32): + ... + + @wp.overload + def scale(x: wp.array(dtype=wp.float64), s: wp.float64): + ... + + wp.launch(scale, dim=n, inputs=[x16, wp.float16(3)]) + wp.launch(scale, dim=n, inputs=[x32, wp.float32(3)]) + wp.launch(scale, dim=n, inputs=[x64, wp.float64(3)]) + +The ``@wp.overload`` decorator allows re-declaring generic kernels without repeating the kernel code. The kernel body is just replaced with the ellipsis (``...``). Warp keeps track of known overloads for each kernel, so if an overload exists it will not be instantiated again. If all the overloads are declared prior to kernel launches, the module will only load once with all the kernel instances in place. + +We can also use ``wp.overload()`` as a function for a slightly more concise syntax. We just need to specify the generic kernel and a list of concrete argument types: + +.. code:: python + + wp.overload(scale, [wp.array(dtype=wp.float16), wp.float16]) + wp.overload(scale, [wp.array(dtype=wp.float32), wp.float32]) + wp.overload(scale, [wp.array(dtype=wp.float64), wp.float64]) + +Instead of an argument list, a dictionary can also be provided: + +.. code:: python + + wp.overload(scale, {"x": wp.array(dtype=wp.float16), "s": wp.float16}) + wp.overload(scale, {"x": wp.array(dtype=wp.float32), "s": wp.float32}) + wp.overload(scale, {"x": wp.array(dtype=wp.float64), "s": wp.float64}) + +A dictionary might be preferred for readability. With dictionaries, only generic arguments need to be specified, which can be even more concise when overloading kernels where some of the arguments are not generic. + +We can easily create overloads in a single loop, like this: + +.. code:: python + + for T in [wp.float16, wp.float32, wp.float64]: + wp.overload(scale, [wp.array(dtype=T), T]) + +Finally, the ``wp.overload()`` function returns the concrete kernel instance, which can be saved in a variable: + +.. code:: python + + scale_f16 = wp.overload(scale, [wp.array(dtype=wp.float16), wp.float16]) + scale_f32 = wp.overload(scale, [wp.array(dtype=wp.float32), wp.float32]) + scale_f64 = wp.overload(scale, [wp.array(dtype=wp.float64), wp.float64]) + +These instances are treated as regular kernels, not generic. This means that launches should be faster, because Warp doesn't need to infer data types from the arguments like it does when launching generic kernels. The typing requirements for kernel arguments are also more relaxed than with generic kernels, because Warp can convert scalars, vectors, and matrices to the known required types. + +.. code:: python + + # launch concrete kernel instances + wp.launch(scale_f16, dim=n, inputs=[x16, 3]) + wp.launch(scale_f32, dim=n, inputs=[x32, 3]) + wp.launch(scale_f64, dim=n, inputs=[x64, 3]) + +.. _Generic Functions: + +Generic Functions +----------------- + +Like Warp kernels, we can also define generic Warp functions: + +.. code:: python + + # generic function + @wp.func + def f(x: Any): + return x * x + + # use generic function in a regular kernel + @wp.kernel + def square_float(a: wp.array(dtype=float)): + i = wp.tid() + a[i] = f(a[i]) + + # use generic function in a generic kernel + @wp.kernel + def square_any(a: wp.array(dtype=Any)): + i = wp.tid() + a[i] = f(a[i]) + + data = [1, 2, 3, 4, 5, 6, 7, 8, 9] + n = len(data) + + af = wp.array(data, dtype=float) + ai = wp.array(data, dtype=int) + + # launch regular kernel + wp.launch(square_float, dim=n, inputs=[af]) + + # launch generic kernel + wp.launch(square_any, dim=n, inputs=[af]) + wp.launch(square_any, dim=n, inputs=[ai]) + +A generic function can be used in regular and generic kernels. It's not necessary to explicitly overload generic functions. All required function overloads are generated automatically when those functions are used in kernels. + + +type() Operator +--------------- + +Consider the following generic function: + +.. code:: python + + @wp.func + def triple(x: Any): + return 3 * x + +Using numeric literals like ``3`` is problematic in generic expressions due to Warp's strict typing rules. Operands in arithmetic expressions must have the same data types, but integer literals are always treated as ``wp.int32``. This function will fail to compile if ``x`` has a data type other than ``wp.int32``, which means that it's not generic at all. + +The ``type()`` operator comes to the rescue here. The ``type()`` operator returns the type of its argument, which is handy in generic functions or kernels where the data types are not known in advance. We can rewrite the function like this to make it work with a wider range of types: + +.. code:: python + + @wp.func + def triple(x: Any): + return type(x)(3) * x + +The ``type()`` operator is useful for type conversions in Warp kernels and functions. For example, here is a simple generic ``arange()`` kernel: + +.. code:: python + + @wp.kernel + def arange(a: wp.array(dtype=Any)): + i = wp.tid() + a[i] = type(a[0])(i) + + n = 10 + ai = wp.empty(n, dtype=wp.int32) + af = wp.empty(n, dtype=wp.float32) + + wp.launch(arange, dim=n, inputs=[ai]) + wp.launch(arange, dim=n, inputs=[af]) + +``wp.tid()`` returns an integer, but the value gets converted to the array's data type before storing it in the array. Alternatively, we could write our ``arange()`` kernel like this: + +.. code:: python + + @wp.kernel + def arange(a: wp.array(dtype=Any)): + i = wp.tid() + a[i] = a.dtype(i) + +This variant uses the ``array.dtype()`` operator, which returns the type of the array's contents. + + +Limitations and Rough Edges +--------------------------- + +Warp generics are still in development and there are some limitations. + +Module Reloading Behavior +~~~~~~~~~~~~~~~~~~~~~~~~~ + +As mentioned in the :ref:`implicit instantiation ` section, launching new kernel overloads triggers the recompilation of the kernel module. This adds overhead and doesn't play well with Warp's current kernel caching strategy. Kernel caching relies on hashing the contents of the module, which includes all the concrete kernels and functions encountered in the Python program so far. Whenever a new kernel or a new instance of a generic kernel is added, the module needs to be reloaded. Re-running the Python program leads to the same sequence of kernels being added to the module, which means that implicit instantiation of generic kernels will trigger the same module reloading on every run. This is clearly not ideal, and we intend to improve this behavior in the future. + +Using :ref:`explicit instantiation ` is usually a good workaround for this, as long as the overloads are added in the same order before any kernel launches. + +Note that this issue is not specific to generic kernels. Adding new regular kernels to a module can also trigger repetitive module reloading if the kernel definitions are intermixed with kernel launches. For example: + +.. code:: python + + @wp.kernel + def foo(x: float): + wp.print(x) + + wp.launch(foo, dim=1, inputs=[17]) + + @wp.kernel + def bar(x: float): + wp.print(x) + + wp.launch(bar, dim=1, inputs=[42]) + +This code will also trigger module reloading during each kernel launch, even though it doesn't use generics at all: + +.. code:: text + + Module __main__ load on device 'cuda:0' took 155.73 ms + 17 + Module __main__ load on device 'cuda:0' took 164.83 ms + 42 + + +Graph Capture +~~~~~~~~~~~~~ + +Module reloading is not allowed during graph capture in CUDA 12.2 or older. Kernel instantiation can trigger module reloading, which will cause graph capture to fail on drivers that don't support newer versions of CUDA. The workaround, again, is to explicitly declare the required overloads before capture begins. + + +Type Variables +~~~~~~~~~~~~~~ + +Warp's ``type()`` operator is similar in principle to Python's ``type()`` function, but it's currently not possible to use types as variables in Warp kernels and functions. For example, the following is currently `not` allowed: + +.. code:: python + + @wp.func + def triple(x: Any): + # TODO: + T = type(x) + return T(3) * x + + +Kernel Overloading Restrictions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +It's currently not possible to define multiple kernels with the same name but different argument counts, but this restriction may be lifted in the future. diff --git a/_sources/modules/interoperability.rst.txt b/_sources/modules/interoperability.rst.txt new file mode 100644 index 00000000..eb2217c1 --- /dev/null +++ b/_sources/modules/interoperability.rst.txt @@ -0,0 +1,509 @@ +Interoperability +================ + +Warp can interoperate with other Python-based frameworks such as NumPy through standard interface protocols. + +Warp supports passing external arrays to kernels directly, as long as they implement the ``__array__``, ``__array_interface__``, or ``__cuda_array_interface__`` protocols. This works with many common frameworks like NumPy, CuPy, or PyTorch. + +For example, we can use NumPy arrays directly when launching Warp kernels on the CPU: + +.. code:: python + + import numpy as np + import warp as wp + + @wp.kernel + def saxpy(x: wp.array(dtype=float), y: wp.array(dtype=float), a: float): + i = wp.tid() + y[i] = a * x[i] + y[i] + + x = np.arange(n, dtype=np.float32) + y = np.ones(n, dtype=np.float32) + + wp.launch(saxpy, dim=n, inputs=[x, y, 1.0], device="cpu") + +Likewise, we can use CuPy arrays on a CUDA device: + +.. code:: python + + import cupy as cp + + with cp.cuda.Device(0): + x = cp.arange(n, dtype=cp.float32) + y = cp.ones(n, dtype=cp.float32) + + wp.launch(saxpy, dim=n, inputs=[x, y, 1.0], device="cuda:0") + +Note that with CUDA arrays, it's important to ensure that the device on which the arrays reside is the same as the device on which the kernel is launched. + +PyTorch supports both CPU and GPU tensors and both kinds can be passed to Warp kernels on the appropriate device. + +.. code:: python + + import random + import torch + + if random.choice([False, True]): + device = "cpu" + else: + device = "cuda:0" + + x = torch.arange(n, dtype=torch.float32, device=device) + y = torch.ones(n, dtype=torch.float32, device=device) + + wp.launch(saxpy, dim=n, inputs=[x, y, 1.0], device=device) + +NumPy +----- + +Warp arrays may be converted to a NumPy array through the ``warp.array.numpy()`` method. When the Warp array lives on +the ``cpu`` device this will return a zero-copy view onto the underlying Warp allocation. If the array lives on a +``cuda`` device then it will first be copied back to a temporary buffer and copied to NumPy. + +Warp CPU arrays also implement the ``__array_interface__`` protocol and so can be used to construct NumPy arrays +directly:: + + w = wp.array([1.0, 2.0, 3.0], dtype=float, device="cpu") + a = np.array(w) + print(a) + > [1. 2. 3.] + +Data type conversion utilities are also available for convenience: + +.. code:: python + + warp_type = wp.float32 + ... + numpy_type = wp.dtype_to_numpy(warp_type) + ... + a = wp.zeros(n, dtype=warp_type) + b = np.zeros(n, dtype=numpy_type) + +To create Warp arrays from NumPy arrays, use :func:`warp.from_numpy` +or pass the NumPy array as the ``data`` argument of the :class:`warp.array` constructor directly. + +.. autofunction:: warp.from_numpy +.. autofunction:: warp.dtype_from_numpy +.. autofunction:: warp.dtype_to_numpy + +.. _pytorch-interop: + +PyTorch +------- + +Warp provides helper functions to convert arrays to/from PyTorch:: + + w = wp.array([1.0, 2.0, 3.0], dtype=float, device="cpu") + + # convert to Torch tensor + t = wp.to_torch(w) + + # convert from Torch tensor + w = wp.from_torch(t) + +These helper functions allow the conversion of Warp arrays to/from PyTorch tensors without copying the underlying data. +At the same time, if available, gradient arrays and tensors are converted to/from PyTorch autograd tensors, allowing the use of Warp arrays +in PyTorch autograd computations. + +.. autofunction:: warp.from_torch +.. autofunction:: warp.to_torch +.. autofunction:: warp.device_from_torch +.. autofunction:: warp.device_to_torch +.. autofunction:: warp.dtype_from_torch +.. autofunction:: warp.dtype_to_torch + +To convert a PyTorch CUDA stream to a Warp CUDA stream and vice versa, Warp provides the following functions: + +.. autofunction:: warp.stream_from_torch +.. autofunction:: warp.stream_to_torch + +Example: Optimization using ``warp.from_torch()`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +An example usage of minimizing a loss function over an array of 2D points written in Warp via PyTorch's Adam optimizer +using :func:`warp.from_torch` is as follows:: + + import warp as wp + import torch + + + @wp.kernel() + def loss(xs: wp.array(dtype=float, ndim=2), l: wp.array(dtype=float)): + tid = wp.tid() + wp.atomic_add(l, 0, xs[tid, 0] ** 2.0 + xs[tid, 1] ** 2.0) + + # indicate requires_grad so that Warp can accumulate gradients in the grad buffers + xs = torch.randn(100, 2, requires_grad=True) + l = torch.zeros(1, requires_grad=True) + opt = torch.optim.Adam([xs], lr=0.1) + + wp_xs = wp.from_torch(xs) + wp_l = wp.from_torch(l) + + tape = wp.Tape() + with tape: + # record the loss function kernel launch on the tape + wp.launch(loss, dim=len(xs), inputs=[wp_xs], outputs=[wp_l], device=wp_xs.device) + + for i in range(500): + tape.zero() + tape.backward(loss=wp_l) # compute gradients + # now xs.grad will be populated with the gradients computed by Warp + opt.step() # update xs (and thereby wp_xs) + + # these lines are only needed for evaluating the loss + # (the optimization just needs the gradient, not the loss value) + wp_l.zero_() + wp.launch(loss, dim=len(xs), inputs=[wp_xs], outputs=[wp_l], device=wp_xs.device) + print(f"{i}\tloss: {l.item()}") + +Example: Optimization using ``warp.to_torch`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Less code is needed when we declare the optimization variables directly in Warp and use :func:`warp.to_torch` to convert them to PyTorch tensors. +Here, we revisit the same example from above where now only a single conversion to a torch tensor is needed to supply Adam with the optimization variables:: + + import warp as wp + import numpy as np + import torch + + + @wp.kernel() + def loss(xs: wp.array(dtype=float, ndim=2), l: wp.array(dtype=float)): + tid = wp.tid() + wp.atomic_add(l, 0, xs[tid, 0] ** 2.0 + xs[tid, 1] ** 2.0) + + # initialize the optimization variables in Warp + xs = wp.array(np.random.randn(100, 2), dtype=wp.float32, requires_grad=True) + l = wp.zeros(1, dtype=wp.float32, requires_grad=True) + # just a single wp.to_torch call is needed, Adam optimizes using the Warp array gradients + opt = torch.optim.Adam([wp.to_torch(xs)], lr=0.1) + + tape = wp.Tape() + with tape: + wp.launch(loss, dim=len(xs), inputs=[xs], outputs=[l], device=xs.device) + + for i in range(500): + tape.zero() + tape.backward(loss=l) + opt.step() + + l.zero_() + wp.launch(loss, dim=len(xs), inputs=[xs], outputs=[l], device=xs.device) + print(f"{i}\tloss: {l.numpy()[0]}") + +Example: Optimization using ``torch.autograd.function`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +One can insert Warp kernel launches in a PyTorch graph by defining a :class:`torch.autograd.Function` class, which +requires forward and backward functions to be defined. After mapping incoming torch arrays to Warp arrays, a Warp kernel +may be launched in the usual way. In the backward pass, the same kernel's adjoint may be launched by +setting ``adjoint = True`` in :func:`wp.launch() `. Alternatively, the user may choose to rely on Warp's tape. +In the following example, we demonstrate how Warp may be used to evaluate the Rosenbrock function in an optimization context:: + + import warp as wp + import numpy as np + import torch + + pvec2 = wp.types.vector(length=2, dtype=wp.float32) + + # Define the Rosenbrock function + @wp.func + def rosenbrock(x: float, y: float): + return (1.0 - x) ** 2.0 + 100.0 * (y - x**2.0) ** 2.0 + + @wp.kernel + def eval_rosenbrock( + xs: wp.array(dtype=pvec2), + # outputs + z: wp.array(dtype=float), + ): + i = wp.tid() + x = xs[i] + z[i] = rosenbrock(x[0], x[1]) + + + class Rosenbrock(torch.autograd.Function): + @staticmethod + def forward(ctx, xy, num_points): + ctx.xy = wp.from_torch(xy, dtype=pvec2, requires_grad=True) + ctx.num_points = num_points + + # allocate output + ctx.z = wp.zeros(num_points, requires_grad=True) + + wp.launch( + kernel=eval_rosenbrock, + dim=ctx.num_points, + inputs=[ctx.xy], + outputs=[ctx.z] + ) + + return wp.to_torch(ctx.z) + + @staticmethod + def backward(ctx, adj_z): + # map incoming Torch grads to our output variables + ctx.z.grad = wp.from_torch(adj_z) + + wp.launch( + kernel=eval_rosenbrock, + dim=ctx.num_points, + inputs=[ctx.xy], + outputs=[ctx.z], + adj_inputs=[ctx.xy.grad], + adj_outputs=[ctx.z.grad], + adjoint=True + ) + + # return adjoint w.r.t. inputs + return (wp.to_torch(ctx.xy.grad), None) + + + num_points = 1500 + learning_rate = 5e-2 + + torch_device = wp.device_to_torch(wp.get_device()) + + rng = np.random.default_rng(42) + xy = torch.tensor(rng.normal(size=(num_points, 2)), dtype=torch.float32, requires_grad=True, device=torch_device) + opt = torch.optim.Adam([xy], lr=learning_rate) + + for _ in range(10000): + # step + opt.zero_grad() + z = Rosenbrock.apply(xy, num_points) + z.backward(torch.ones_like(z)) + + opt.step() + + # minimum at (1, 1) + xy_np = xy.numpy(force=True) + print(np.mean(xy_np, axis=0)) + +Note that if Warp code is wrapped in a torch.autograd.function that gets called in ``torch.compile()``, it will automatically +exclude that function from compiler optimizations. If your script uses ``torch.compile()``, we recommend using Pytorch version 2.3.0+, +which has improvements that address this scenario. + +Performance Notes +^^^^^^^^^^^^^^^^^ + +The ``wp.from_torch()`` function creates a Warp array object that shares data with a PyTorch tensor. Although this function does not copy the data, there is always some CPU overhead during the conversion. If these conversions happen frequently, the overall program performance may suffer. As a general rule, it's good to avoid repeated conversions of the same tensor. Instead of: + +.. code:: python + + x_t = torch.arange(n, dtype=torch.float32, device=device) + y_t = torch.ones(n, dtype=torch.float32, device=device) + + for i in range(10): + x_w = wp.from_torch(x_t) + y_w = wp.from_torch(y_t) + wp.launch(saxpy, dim=n, inputs=[x_w, y_w, 1.0], device=device) + +Try converting the arrays only once and reuse them: + +.. code:: python + + x_t = torch.arange(n, dtype=torch.float32, device=device) + y_t = torch.ones(n, dtype=torch.float32, device=device) + + x_w = wp.from_torch(x_t) + y_w = wp.from_torch(y_t) + + for i in range(10): + wp.launch(saxpy, dim=n, inputs=[x_w, y_w, 1.0], device=device) + +If reusing arrays is not possible (e.g., a new PyTorch tensor is constructed on every iteration), passing ``return_ctype=True`` to ``wp.from_torch()`` should yield faster performance. Setting this argument to True avoids constructing a ``wp.array`` object and instead returns a low-level array descriptor. This descriptor is a simple C structure that can be passed to Warp kernels instead of a ``wp.array``, but cannot be used in other places that require a ``wp.array``. + +.. code:: python + + for n in range(1, 10): + # get Torch tensors for this iteration + x_t = torch.arange(n, dtype=torch.float32, device=device) + y_t = torch.ones(n, dtype=torch.float32, device=device) + + # get Warp array descriptors + x_ctype = wp.from_torch(x_t, return_ctype=True) + y_ctype = wp.from_torch(y_t, return_ctype=True) + + wp.launch(saxpy, dim=n, inputs=[x_ctype, y_ctype, 1.0], device=device) + +An alternative approach is to pass the PyTorch tensors to Warp kernels directly. This avoids constructing temporary Warp arrays by leveraging standard array interfaces (like ``__cuda_array_interface__``) supported by both PyTorch and Warp. The main advantage of this approach is convenience, since there is no need to call any conversion functions. The main limitation is that it does not handle gradients, because gradient information is not included in the standard array interfaces. This technique is therefore most suitable for algorithms that do not involve differentiation. + +.. code:: python + + x = torch.arange(n, dtype=torch.float32, device=device) + y = torch.ones(n, dtype=torch.float32, device=device) + + for i in range(10): + wp.launch(saxpy, dim=n, inputs=[x, y, 1.0], device=device) + +.. code:: shell + + python -m warp.examples.benchmarks.benchmark_interop_torch + +Sample output: + +.. code:: + + 5095 ms from_torch(...) + 2113 ms from_torch(..., return_ctype=True) + 2950 ms direct from torch + +The default ``wp.from_torch()`` conversion is the slowest. Passing ``return_ctype=True`` is the fastest, because it skips creating temporary Warp array objects. Passing PyTorch tensors to Warp kernels directly falls somewhere in between. It skips creating temporary Warp arrays, but accessing the ``__cuda_array_interface__`` attributes of PyTorch tensors adds overhead because they are initialized on-demand. + + +CuPy/Numba +---------- + +Warp GPU arrays support the ``__cuda_array_interface__`` protocol for sharing data with other Python GPU frameworks. This allows frameworks like CuPy to use Warp GPU arrays directly. + +Likewise, Warp arrays can be created from any object that exposes the ``__cuda_array_interface__``. Such objects can also be passed to Warp kernels directly without creating a Warp array object. + +.. _jax-interop: + +JAX +--- + +Interoperability with JAX arrays is supported through the following methods. +Internally these use the DLPack protocol to exchange data in a zero-copy way with JAX:: + + warp_array = wp.from_jax(jax_array) + jax_array = wp.to_jax(warp_array) + +It may be preferable to use the :ref:`DLPack` protocol directly for better performance and control over stream synchronization behaviour. + +.. autofunction:: warp.from_jax +.. autofunction:: warp.to_jax +.. autofunction:: warp.device_from_jax +.. autofunction:: warp.device_to_jax +.. autofunction:: warp.dtype_from_jax +.. autofunction:: warp.dtype_to_jax + + +Using Warp kernels as JAX primitives +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. note:: + This is an experimental feature under development. + +Warp kernels can be used as JAX primitives, which can be used to call Warp kernels inside of jitted JAX functions:: + + import warp as wp + import jax + import jax.numpy as jp + + # import experimental feature + from warp.jax_experimental import jax_kernel + + @wp.kernel + def triple_kernel(input: wp.array(dtype=float), output: wp.array(dtype=float)): + tid = wp.tid() + output[tid] = 3.0 * input[tid] + + # create a Jax primitive from a Warp kernel + jax_triple = jax_kernel(triple_kernel) + + # use the Warp kernel in a Jax jitted function + @jax.jit + def f(): + x = jp.arange(0, 64, dtype=jp.float32) + return jax_triple(x) + + print(f()) + +Since this is an experimental feature, there are some limitations: + + - All kernel arguments must be arrays. + - Kernel launch dimensions are inferred from the shape of the first argument. + - Input arguments are followed by output arguments in the Warp kernel definition. + - There must be at least one input argument and at least one output argument. + - Output shapes must match the launch dimensions (i.e., output shapes must match the shape of the first argument). + - All arrays must be contiguous. + - Only the CUDA backend is supported. + +Here is an example of an operation with three inputs and two outputs:: + + import warp as wp + import jax + import jax.numpy as jp + + # import experimental feature + from warp.jax_experimental import jax_kernel + + # kernel with multiple inputs and outputs + @wp.kernel + def multiarg_kernel( + # inputs + a: wp.array(dtype=float), + b: wp.array(dtype=float), + c: wp.array(dtype=float), + # outputs + ab: wp.array(dtype=float), + bc: wp.array(dtype=float), + ): + tid = wp.tid() + ab[tid] = a[tid] + b[tid] + bc[tid] = b[tid] + c[tid] + + # create a Jax primitive from a Warp kernel + jax_multiarg = jax_kernel(multiarg_kernel) + + # use the Warp kernel in a Jax jitted function with three inputs and two outputs + @jax.jit + def f(): + a = jp.full(64, 1, dtype=jp.float32) + b = jp.full(64, 2, dtype=jp.float32) + c = jp.full(64, 3, dtype=jp.float32) + return jax_multiarg(a, b, c) + + x, y = f() + + print(x) + print(y) + +.. _DLPack: + +DLPack +------ + +Warp supports the DLPack protocol included in the Python Array API standard v2022.12. +See the `Python Specification for DLPack `_ for reference. + +The canonical way to import an external array into Warp is using the ``warp.from_dlpack()`` function:: + + warp_array = wp.from_dlpack(external_array) + +The external array can be a PyTorch tensor, Jax array, or any other array type compatible with this version of the DLPack protocol. +For CUDA arrays, this approach requires the producer to perform stream synchronization which ensures that operations on the array +are ordered correctly. The ``warp.from_dlpack()`` function asks the producer to synchronize the current Warp stream on the device where +the array resides. Thus it should be safe to use the array in Warp kernels on that device without any additional synchronization. + +The canonical way to export a Warp array to an external framework is to use the ``from_dlpack()`` function in that framework:: + + jax_array = jax.dlpack.from_dlpack(warp_array) + torch_tensor = torch.utils.dlpack.from_dlpack(warp_array) + +For CUDA arrays, this will synchronize the current stream of the consumer framework with the current Warp stream on the array's device. +Thus it should be safe to use the wrapped array in the consumer framework, even if the array was previously used in a Warp kernel +on the device. + +Alternatively, arrays can be shared by explicitly creating PyCapsules using a ``to_dlpack()`` function provided by the producer framework. +This approach may be used for older versions of frameworks that do not support the v2022.12 standard:: + + warp_array1 = wp.from_dlpack(jax.dlpack.to_dlpack(jax_array)) + warp_array2 = wp.from_dlpack(torch.utils.dlpack.to_dlpack(torch_tensor)) + + jax_array = jax.dlpack.from_dlpack(wp.to_dlpack(warp_array)) + torch_tensor = torch.utils.dlpack.from_dlpack(wp.to_dlpack(warp_array)) + +This approach is generally faster because it skips any stream synchronization, but another solution must be used to ensure correct +ordering of operations. In situations where no synchronization is required, using this approach can yield better performance. +This may be a good choice in situations like these: + + - The external framework is using the synchronous CUDA default stream. + - Warp and the external framework are using the same CUDA stream. + - Another synchronization mechanism is already in place. + +.. autofunction:: warp.from_dlpack +.. autofunction:: warp.to_dlpack diff --git a/_sources/modules/render.rst.txt b/_sources/modules/render.rst.txt new file mode 100644 index 00000000..c275e4a9 --- /dev/null +++ b/_sources/modules/render.rst.txt @@ -0,0 +1,50 @@ +warp.render +=========== + +.. currentmodule:: warp.render + +The ``warp.render`` module provides a set of renderers that can be used for visualizing scenes involving shapes of various types. + +Built on top of these stand-alone renderers, the ``warp.sim.render`` module provides renderers that can be used to +visualize scenes directly from ``warp.sim.ModelBuilder`` objects and update them from ``warp.sim.State`` objects. + +Standalone Renderers +-------------------- + +The ``OpenGLRenderer`` provides an interactive renderer to play back animations in real time, the ``UsdRenderer`` +provides a renderer that exports the scene to a USD file that can be rendered in a renderer of your choice. + +.. autoclass:: UsdRenderer + :members: + + +.. autoclass:: OpenGLRenderer + :members: + +Simulation Renderers +-------------------- + +Based on these renderers from ``warp.render``, the ``SimRendererUsd`` (which equals ``SimRenderer``) and +``SimRendererOpenGL`` classes from ``warp.sim.render`` are derived to populate the renderers directly from +``warp.sim.ModelBuilder`` scenes and update them from ``warp.sim.State`` objects. + +.. currentmodule:: warp.sim.render + +.. autoclass:: SimRendererUsd + :members: + +.. autoclass:: SimRendererOpenGL + :members: + +CUDA Graphics Interface +----------------------- + +Warp provides a CUDA graphics interface that allows you to access OpenGL buffers from CUDA kernels. +This is useful for manipulating OpenGL array buffers without having to copy them back and forth between the CPU and GPU. + +See the `CUDA documentation on OpenGL Interoperability `_ for more information. + +.. currentmodule:: warp.context + +.. autoclass:: RegisteredGLBuffer + :members: diff --git a/_sources/modules/runtime.rst.txt b/_sources/modules/runtime.rst.txt new file mode 100644 index 00000000..95767c26 --- /dev/null +++ b/_sources/modules/runtime.rst.txt @@ -0,0 +1,1197 @@ +Runtime Reference +================= + +.. currentmodule:: warp + +This section describes the Warp Python runtime API, how to manage memory, launch kernels, and high-level functionality +for dealing with objects such as meshes and volumes. The APIs described in this section are intended to be used at +the *Python Scope* and run inside the CPython interpreter. For a comprehensive list of functions available at +the *Kernel Scope*, please see the :doc:`functions` section. + +Kernels +------- + +Kernels are launched with the :func:`wp.launch() ` function on a specific device (CPU/GPU):: + + wp.launch(simple_kernel, dim=1024, inputs=[a, b, c], device="cuda") + +Note that all the kernel inputs must live on the target device or a runtime exception will be raised. +Kernels may be launched with multi-dimensional grid bounds. In this case, threads are not assigned a single index, +but a coordinate in an n-dimensional grid, e.g.:: + + wp.launch(complex_kernel, dim=(128, 128, 3), ...) + +Launches a 3D grid of threads with dimension 128 x 128 x 3. To retrieve the 3D index for each thread, use the following syntax:: + + i,j,k = wp.tid() + +.. note:: + Currently, kernels launched on CPU devices will be executed in serial. + Kernels launched on CUDA devices will be launched in parallel with a fixed block-size. + +In the Warp :ref:`Compilation Model`, kernels are just-in-time compiled into dynamic libraries and PTX using +C++/CUDA as an intermediate representation. +To avoid excessive runtime recompilation of kernel code, these files are stored in a cache directory +named with a module-dependent hash to allow for the reuse of previously compiled modules. +The location of the kernel cache is printed when Warp is initialized. +:func:`wp.clear_kernel_cache() ` can be used to clear the kernel cache of previously +generated compilation artifacts as Warp does not automatically try to keep the cache below a certain size. + +.. autofunction:: launch +.. autofunction:: clear_kernel_cache + +.. _Runtime Kernel Creation: + +Runtime Kernel Creation +####################### + +It is often desirable to specialize kernels for different types, constants, or functions at runtime. +We can achieve this through the use of runtime kernel specialization using Python closures. + +For example, we might require a variety of kernels that execute particular functions for each item in an array. +We might also want this function call to be valid for a variety of data types. Making use of closure and generics, we can generate +these kernels using a single kernel definition:: + + def make_kernel(func, dtype): + def closure_kernel_fn(data: wp.array(dtype=dtype), out: wp.array(dtype=dtype)): + tid = wp.tid() + out[tid] = func(data[tid]) + + return wp.Kernel(closure_kernel_fn) + +In practice, we might use our kernel generator, ``make_kernel()`` as follows:: + + @wp.func + def sqr(x: Any) -> Any: + return x * x + + @wp.func + def cube(x: Any) -> Any: + return sqr(x) * x + + sqr_float = make_kernel(sqr, wp.float32) + cube_double = make_kernel(cube, wp.float64) + + arr = [1.0, 2.0, 3.0] + N = len(arr) + + data_float = wp.array(arr, dtype=wp.float32, device=device) + data_double = wp.array(arr, dtype=wp.float64, device=device) + + out_float = wp.zeros(N, dtype=wp.float32, device=device) + out_double = wp.zeros(N, dtype=wp.float64, device=device) + + wp.launch(sqr_float, dim=N, inputs=[data_float], outputs=[out_float], device=device) + wp.launch(cube_double, dim=N, inputs=[data_double], outputs=[out_double], device=device) + +We can specialize kernel definitions over Warp constants similarly. The following generates kernels that add a specified constant +to a generic-typed array value:: + + def make_add_kernel(key, constant): + def closure_kernel_fn(data: wp.array(dtype=Any), out: wp.array(dtype=Any)): + tid = wp.tid() + out[tid] = data[tid] + constant + + return wp.Kernel(closure_kernel_fn, key=key) + + add_ones_int = make_add_kernel("add_one", wp.constant(1)) + add_ones_vec3 = make_add_kernel("add_ones_vec3", wp.constant(wp.vec3(1.0, 1.0, 1.0))) + + a = wp.zeros(2, dtype=int) + b = wp.zeros(2, dtype=wp.vec3) + + a_out = wp.zeros_like(a) + b_out = wp.zeros_like(b) + + wp.launch(add_ones_int, dim=a.size, inputs=[a], outputs=[a_out], device=device) + wp.launch(add_ones_vec3, dim=b.size, inputs=[b], outputs=[b_out], device=device) + + +.. _Arrays: + +Arrays +------ + +Arrays are the fundamental memory abstraction in Warp; they are created through the following global constructors: :: + + wp.empty(shape=1024, dtype=wp.vec3, device="cpu") + wp.zeros(shape=1024, dtype=float, device="cuda") + wp.full(shape=1024, value=10, dtype=int, device="cuda") + + +Arrays can also be constructed directly from ``numpy`` ndarrays as follows: :: + + r = np.random.rand(1024) + + # copy to Warp owned array + a = wp.array(r, dtype=float, device="cpu") + + # return a Warp array wrapper around the NumPy data (zero-copy) + a = wp.array(r, dtype=float, copy=False, device="cpu") + + # return a Warp copy of the array data on the GPU + a = wp.array(r, dtype=float, device="cuda") + +Note that for multi-dimensional data the ``dtype`` parameter must be specified explicitly, e.g.: :: + + r = np.random.rand((1024, 3)) + + # initialize as an array of vec3 objects + a = wp.array(r, dtype=wp.vec3, device="cuda") + +If the shapes are incompatible, an error will be raised. + +Warp arrays can also be constructed from objects that define the ``__cuda_array_interface__`` attribute. For example: :: + + import cupy + import warp as wp + + device = wp.get_cuda_device() + + r = cupy.arange(10) + + # return a Warp array wrapper around the cupy data (zero-copy) + a = wp.array(r, device=device) + +Arrays can be moved between devices using the ``array.to()`` method: :: + + host_array = wp.array(a, dtype=float, device="cpu") + + # allocate and copy to GPU + device_array = host_array.to("cuda") + +Additionally, arrays can be copied directly between memory spaces: :: + + src_array = wp.array(a, dtype=float, device="cpu") + dest_array = wp.empty_like(host_array) + + # copy from source CPU buffer to GPU + wp.copy(dest_array, src_array) + +.. autoclass:: array + :members: + :undoc-members: + :exclude-members: vars + + +Multi-dimensional Arrays +######################## + +Multi-dimensional arrays can be constructed by passing a tuple of sizes for each dimension, e.g.: the following constructs a 2d array of size 1024x16:: + + wp.zeros(shape=(1024, 16), dtype=float, device="cuda") + +When passing multi-dimensional arrays to kernels users must specify the expected array dimension inside the kernel signature, +e.g. to pass a 2d array to a kernel the number of dims is specified using the ``ndim=2`` parameter:: + + @wp.kernel + def test(input: wp.array(dtype=float, ndim=2)): + +Type-hint helpers are provided for common array sizes, e.g.: ``array2d()``, ``array3d()``, which are equivalent to calling ``array(..., ndim=2)```, etc. To index a multi-dimensional array use a the following kernel syntax:: + + # returns a float from the 2d array + value = input[i,j] + +To create an array slice use the following syntax, where the number of indices is less than the array dimensions:: + + # returns an 1d array slice representing a row of the 2d array + row = input[i] + +Slice operators can be concatenated, e.g.: ``s = array[i][j][k]``. Slices can be passed to ``wp.func`` user functions provided +the function also declares the expected array dimension. Currently only single-index slicing is supported. + +.. note:: + Currently Warp limits arrays to 4 dimensions maximum. This is in addition to the contained datatype, which may be 1-2 dimensional for vector and matrix types such as ``vec3``, and ``mat33``. + + +The following construction methods are provided for allocating zero-initialized and empty (non-initialized) arrays: + +.. autofunction:: zeros +.. autofunction:: zeros_like +.. autofunction:: ones +.. autofunction:: ones_like +.. autofunction:: full +.. autofunction:: full_like +.. autofunction:: empty +.. autofunction:: empty_like +.. autofunction:: copy +.. autofunction:: clone + +Matrix Multiplication +##################### + +Warp 2D array multiplication is built on NVIDIA's `CUTLASS `_ library, +which enables fast matrix multiplication of large arrays on the GPU. + +If no GPU is detected, matrix multiplication falls back to Numpy's implementation on the CPU. + +Matrix multiplication is fully differentiable, and can be recorded on the tape like so:: + + tape = wp.Tape() + with tape: + wp.matmul(A, B, C, D, device=device) + wp.launch(loss_kernel, dim=(m, n), inputs=[D, loss], device=device) + + tape.backward(loss=loss) + A_grad = A.grad.numpy() + +Using the ``@`` operator (``D = A @ B``) will default to the same CUTLASS algorithm used in ``wp.matmul``. + +.. autofunction:: matmul + +.. autofunction:: batched_matmul + +Data Types +---------- + +Scalar Types +############ + +The following scalar storage types are supported for array structures: + ++---------+------------------------+ +| bool | boolean | ++---------+------------------------+ +| int8 | signed byte | ++---------+------------------------+ +| uint8 | unsigned byte | ++---------+------------------------+ +| int16 | signed short | ++---------+------------------------+ +| uint16 | unsigned short | ++---------+------------------------+ +| int32 | signed integer | ++---------+------------------------+ +| uint32 | unsigned integer | ++---------+------------------------+ +| int64 | signed long integer | ++---------+------------------------+ +| uint64 | unsigned long integer | ++---------+------------------------+ +| float16 | half-precision float | ++---------+------------------------+ +| float32 | single-precision float | ++---------+------------------------+ +| float64 | double-precision float | ++---------+------------------------+ + +Warp supports ``float`` and ``int`` as aliases for ``wp.float32`` and ``wp.int32`` respectively. + +.. _vec: + +Vectors +####### + +Warp provides built-in math and geometry types for common simulation and graphics problems. +A full reference for operators and functions for these types is available in the :doc:`/modules/functions`. + +Warp supports vectors of numbers with an arbitrary length/numeric type. The built-in concrete types are as follows: + ++-----------------------+------------------------------------------------+ +| vec2 vec3 vec4 | 2D, 3D, 4D vector of single-precision floats | ++-----------------------+------------------------------------------------+ +| vec2b vec3b vec4b | 2D, 3D, 4D vector of signed bytes | ++-----------------------+------------------------------------------------+ +| vec2ub vec3ub vec4ub | 2D, 3D, 4D vector of unsigned bytes | ++-----------------------+------------------------------------------------+ +| vec2s vec3s vec4s | 2D, 3D, 4D vector of signed shorts | ++-----------------------+------------------------------------------------+ +| vec2us vec3us vec4us | 2D, 3D, 4D vector of unsigned shorts | ++-----------------------+------------------------------------------------+ +| vec2i vec3i vec4i | 2D, 3D, 4D vector of signed integers | ++-----------------------+------------------------------------------------+ +| vec2ui vec3ui vec4ui | 2D, 3D, 4D vector of unsigned integers | ++-----------------------+------------------------------------------------+ +| vec2l vec3l vec4l | 2D, 3D, 4D vector of signed long integers | ++-----------------------+------------------------------------------------+ +| vec2ul vec3ul vec4ul | 2D, 3D, 4D vector of unsigned long integers | ++-----------------------+------------------------------------------------+ +| vec2h vec3h vec4h | 2D, 3D, 4D vector of half-precision floats | ++-----------------------+------------------------------------------------+ +| vec2f vec3f vec4f | 2D, 3D, 4D vector of single-precision floats | ++-----------------------+------------------------------------------------+ +| vec2d vec3d vec4d | 2D, 3D, 4D vector of double-precision floats | ++-----------------------+------------------------------------------------+ +| spatial_vector | 6D vector of single-precision floats | ++-----------------------+------------------------------------------------+ +| spatial_vectorf | 6D vector of single-precision floats | ++-----------------------+------------------------------------------------+ +| spatial_vectord | 6D vector of double-precision floats | ++-----------------------+------------------------------------------------+ +| spatial_vectorh | 6D vector of half-precision floats | ++-----------------------+------------------------------------------------+ + +Vectors support most standard linear algebra operations, e.g.: :: + + @wp.kernel + def compute( ... ): + + # basis vectors + a = wp.vec3(1.0, 0.0, 0.0) + b = wp.vec3(0.0, 1.0, 0.0) + + # take the cross product + c = wp.cross(a, b) + + # compute + r = wp.dot(c, c) + + ... + + +It's possible to declare additional vector types with different lengths and data types. This is done in outside of kernels in *Python scope* using ``warp.types.vector()``, for example: :: + + # declare a new vector type for holding 5 double precision floats: + vec5d = wp.types.vector(length=5, dtype=wp.float64) + +Once declared, the new type can be used when allocating arrays or inside kernels: :: + + # create an array of vec5d + arr = wp.zeros(10, dtype=vec5d) + + # use inside a kernel + @wp.kernel + def compute( ... ): + + # zero initialize a custom named vector type + v = vec5d() + ... + + # component-wise initialize a named vector type + v = vec5d(wp.float64(1.0), + wp.float64(2.0), + wp.float64(3.0), + wp.float64(4.0), + wp.float64(5.0)) + ... + +In addition, it's possible to directly create *anonymously* typed instances of these vectors without declaring their type in advance. In this case the type will be inferred by the constructor arguments. For example: :: + + @wp.kernel + def compute( ... ): + + # zero initialize vector of 5 doubles: + v = wp.vector(dtype=wp.float64, length=5) + + # scalar initialize a vector of 5 doubles to the same value: + v = wp.vector(wp.float64(1.0), length=5) + + # component-wise initialize a vector of 5 doubles + v = wp.vector(wp.float64(1.0), + wp.float64(2.0), + wp.float64(3.0), + wp.float64(4.0), + wp.float64(5.0)) + + +These can be used with all the standard vector arithmetic operators, e.g.: ``+``, ``-``, scalar multiplication, and can also be transformed using matrices with compatible dimensions, potentially returning vectors with a different length. + +.. _mat: + +Matrices +######## + +Matrices with arbitrary shapes/numeric types are also supported. The built-in concrete matrix types are as follows: + ++--------------------------+-------------------------------------------------+ +| mat22 mat33 mat44 | 2x2, 3x3, 4x4 matrix of single-precision floats | ++--------------------------+-------------------------------------------------+ +| mat22f mat33f mat44f | 2x2, 3x3, 4x4 matrix of single-precision floats | ++--------------------------+-------------------------------------------------+ +| mat22d mat33d mat44d | 2x2, 3x3, 4x4 matrix of double-precision floats | ++--------------------------+-------------------------------------------------+ +| mat22h mat33h mat44h | 2x2, 3x3, 4x4 matrix of half-precision floats | ++--------------------------+-------------------------------------------------+ +| spatial_matrix | 6x6 matrix of single-precision floats | ++--------------------------+-------------------------------------------------+ +| spatial_matrixf | 6x6 matrix of single-precision floats | ++--------------------------+-------------------------------------------------+ +| spatial_matrixd | 6x6 matrix of double-precision floats | ++--------------------------+-------------------------------------------------+ +| spatial_matrixh | 6x6 matrix of half-precision floats | ++--------------------------+-------------------------------------------------+ + +Matrices are stored in row-major format and support most standard linear algebra operations: :: + + @wp.kernel + def compute( ... ): + + # initialize matrix + m = wp.mat22(1.0, 2.0, + 3.0, 4.0) + + # compute inverse + minv = wp.inverse(m) + + # transform vector + v = minv * wp.vec2(0.5, 0.3) + + ... + + +In a similar manner to vectors, it's possible to declare new matrix types with arbitrary shapes and data types using ``wp.types.matrix()``, for example: :: + + # declare a new 3x2 half precision float matrix type: + mat32h = wp.types.matrix(shape=(3,2), dtype=wp.float64) + + # create an array of this type + a = wp.zeros(10, dtype=mat32h) + +These can be used inside a kernel:: + + @wp.kernel + def compute( ... ): + ... + + # initialize a mat32h matrix + m = mat32h(wp.float16(1.0), wp.float16(2.0), + wp.float16(3.0), wp.float16(4.0), + wp.float16(5.0), wp.float16(6.0)) + + # declare a 2 component half precision vector + v2 = wp.vec2h(wp.float16(1.0), wp.float16(1.0)) + + # multiply by the matrix, returning a 3 component vector: + v3 = m * v2 + ... + +It's also possible to directly create anonymously typed instances inside kernels where the type is inferred from constructor arguments as follows:: + + @wp.kernel + def compute( ... ): + ... + + # create a 3x2 half precision matrix from components (row major ordering): + m = wp.matrix( + wp.float16(1.0), wp.float16(2.0), + wp.float16(1.0), wp.float16(2.0), + wp.float16(1.0), wp.float16(2.0), + shape=(3,2)) + + # zero initialize a 3x2 half precision matrix: + m = wp.matrix(wp.float16(0.0),shape=(3,2)) + + # create a 5x5 double precision identity matrix: + m = wp.identity(n=5, dtype=wp.float64) + +As with vectors, you can do standard matrix arithmetic with these variables, along with multiplying matrices with compatible shapes and potentially returning a matrix with a new shape. + +.. _quat: + +Quaternions +########### + +Warp supports quaternions with the layout ``i, j, k, w`` where ``w`` is the real part. Here are the built-in concrete quaternion types: + ++-----------------+--------------------------------------------+ +| quat | Single-precision floating point quaternion | ++-----------------+--------------------------------------------+ +| quatf | Single-precision floating point quaternion | ++-----------------+--------------------------------------------+ +| quatd | Double-precision floating point quaternion | ++-----------------+--------------------------------------------+ +| quath | Half-precision floating point quaternion | ++-----------------+--------------------------------------------+ + +Quaternions can be used to transform vectors as follows:: + + @wp.kernel + def compute( ... ): + ... + + # construct a 30 degree rotation around the x-axis + q = wp.quat_from_axis_angle(wp.vec3(1.0, 0.0, 0.0), wp.degrees(30.0)) + + # rotate an axis by this quaternion + v = wp.quat_rotate(q, wp.vec3(0.0, 1.0, 0.0)) + + +As with vectors and matrices, you can declare quaternion types with an arbitrary numeric type like so:: + + quatd = wp.types.quaternion(dtype=wp.float64) + +You can also create identity quaternion and anonymously typed instances inside a kernel like so:: + + @wp.kernel + def compute( ... ): + ... + + # create a double precision identity quaternion: + qd = wp.quat_identity(dtype=wp.float64) + + # precision defaults to wp.float32 so this creates a single precision identity quaternion: + qf = wp.quat_identity() + + # create a half precision quaternion from components, or a vector/scalar: + qh = wp.quaternion(wp.float16(0.0), + wp.float16(0.0), + wp.float16(0.0), + wp.float16(1.0)) + + + qh = wp.quaternion( + wp.vector(wp.float16(0.0),wp.float16(0.0),wp.float16(0.0)), + wp.float16(1.0)) + +.. _transform: + +Transforms +########## + +Transforms are 7D vectors of floats representing a spatial rigid body transformation in format (p, q) where p is a 3D vector, and q is a quaternion. + ++-----------------+--------------------------------------------+ +| transform | Single-precision floating point transform | ++-----------------+--------------------------------------------+ +| transformf | Single-precision floating point transform | ++-----------------+--------------------------------------------+ +| transformd | Double-precision floating point transform | ++-----------------+--------------------------------------------+ +| transformh | Half-precision floating point transform | ++-----------------+--------------------------------------------+ + +Transforms can be constructed inside kernels from translation and rotation parts:: + + @wp.kernel + def compute( ... ): + ... + + # create a transform from a vector/quaternion: + t = wp.transform( + wp.vec3(1.0, 2.0, 3.0), + wp.quat_from_axis_angle(wp.vec3(0.0, 1.0, 0.0), wp.degrees(30.0))) + + # transform a point + p = wp.transform_point(t, wp.vec3(10.0, 0.5, 1.0)) + + # transform a vector (ignore translation) + p = wp.transform_vector(t, wp.vec3(10.0, 0.5, 1.0)) + + +As with vectors and matrices, you can declare transform types with an arbitrary numeric type using ``wp.types.transformation()``, for example:: + + transformd = wp.types.transformation(dtype=wp.float64) + +You can also create identity transforms and anonymously typed instances inside a kernel like so:: + + @wp.kernel + def compute( ... ): + + # create double precision identity transform: + qd = wp.transform_identity(dtype=wp.float64) + +.. _Structs: + +Structs +####### + +Users can define custom structure types using the ``@wp.struct`` decorator as follows:: + + @wp.struct + class MyStruct: + + param1: int + param2: float + param3: wp.array(dtype=wp.vec3) + +Struct attributes must be annotated with their respective type. They can be constructed in Python scope and then passed to kernels as arguments:: + + @wp.kernel + def compute(args: MyStruct): + + tid = wp.tid() + + print(args.param1) + print(args.param2) + print(args.param3[tid]) + + # construct an instance of the struct in Python + s = MyStruct() + s.param1 = 10 + s.param2 = 2.5 + s.param3 = wp.zeros(shape=10, dtype=wp.vec3) + + # pass to our compute kernel + wp.launch(compute, dim=10, inputs=[s]) + +An array of structs can be zero-initialized as follows:: + + a = wp.zeros(shape=10, dtype=MyStruct) + +An array of structs can also be initialized from a list of struct objects:: + + a = wp.array([MyStruct(), MyStruct(), MyStruct()], dtype=MyStruct) + +Example: Using a struct in gradient computation +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code:: python + + import numpy as np + import warp as wp + + @wp.struct + class TestStruct: + x: wp.vec3 + a: wp.array(dtype=wp.vec3) + b: wp.array(dtype=wp.vec3) + + @wp.kernel + def test_kernel(s: TestStruct): + tid = wp.tid() + + s.b[tid] = s.a[tid] + s.x + + @wp.kernel + def loss_kernel(s: TestStruct, loss: wp.array(dtype=float)): + tid = wp.tid() + + v = s.b[tid] + wp.atomic_add(loss, 0, float(tid + 1) * (v[0] + 2.0 * v[1] + 3.0 * v[2])) + + # create struct + ts = TestStruct() + + # set members + ts.x = wp.vec3(1.0, 2.0, 3.0) + ts.a = wp.array(np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]), dtype=wp.vec3, requires_grad=True) + ts.b = wp.zeros(2, dtype=wp.vec3, requires_grad=True) + + loss = wp.zeros(1, dtype=float, requires_grad=True) + + tape = wp.Tape() + with tape: + wp.launch(test_kernel, dim=2, inputs=[ts]) + wp.launch(loss_kernel, dim=2, inputs=[ts, loss]) + + tape.backward(loss) + + print(loss) + print(ts.a) + + +Type Conversions +################ + +Warp is particularly strict regarding type conversions and does not perform *any* implicit conversion between numeric types. +The user is responsible for ensuring types for most arithmetic operators match, e.g.: ``x = float(0.0) + int(4)`` will result in an error. +This can be surprising for users that are accustomed to C-style conversions but avoids a class of common bugs that result from implicit conversions. + +.. note:: + Warp does not currently perform implicit type conversions between numeric types. + Users should explicitly cast variables to compatible types using constructors like + ``int()``, ``float()``, ``wp.float16()``, ``wp.uint8()``, etc. + +Constants +--------- + +In general, Warp kernels cannot access variables in the global Python interpreter state. One exception to this is for compile-time constants, which may be declared globally (or as class attributes) and folded into the kernel definition. + +Constants are defined using the ``wp.constant()`` function. An example is shown below:: + + TYPE_SPHERE = wp.constant(0) + TYPE_CUBE = wp.constant(1) + TYPE_CAPSULE = wp.constant(2) + + @wp.kernel + def collide(geometry: wp.array(dtype=int)): + + t = geometry[wp.tid()] + + if (t == TYPE_SPHERE): + print("sphere") + if (t == TYPE_CUBE): + print("cube") + if (t == TYPE_CAPSULE): + print("capsule") + + +.. autoclass:: constant + +Predefined Constants +#################### + +For convenience, Warp has a number of predefined mathematical constants that +may be used both inside and outside Warp kernels. +The constants in the following table also have lowercase versions defined, +e.g. ``wp.E`` and ``wp.e`` are equivalent. + +================ ========================= +Name Value +================ ========================= +wp.E 2.71828182845904523536 +wp.LOG2E 1.44269504088896340736 +wp.LOG10E 0.43429448190325182765 +wp.LN2 0.69314718055994530942 +wp.LN10 2.30258509299404568402 +wp.PHI 1.61803398874989484820 +wp.PI 3.14159265358979323846 +wp.HALF_PI 1.57079632679489661923 +wp.TAU 6.28318530717958647692 +wp.INF math.inf +wp.NAN float('nan') +================ ========================= + +The ``wp.NAN`` constant may only be used with floating-point types. +Comparisons involving ``wp.NAN`` follow the IEEE 754 standard, +e.g. ``wp.float32(wp.NAN) == wp.float32(wp.NAN)`` returns ``False``. +The :func:`wp.isnan() ` built-in function can be used to determine whether a +value is a NaN (or if a vector, matrix, or quaternion contains a NaN entry). + +The following example shows how positive and negative infinity +can be used with floating-point types in Warp using the ``wp.inf`` constant: + +.. code-block:: python + + @wp.kernel + def test_infinity(outputs: wp.array(dtype=wp.float32)): + outputs[0] = wp.float32(wp.inf) # inf + outputs[1] = wp.float32(-wp.inf) # -inf + outputs[2] = wp.float32(2.0 * wp.inf) # inf + outputs[3] = wp.float32(-2.0 * wp.inf) # -inf + outputs[4] = wp.float32(2.0 / 0.0) # inf + outputs[5] = wp.float32(-2.0 / 0.0) # -inf + +Operators +---------- + +Boolean Operators +################# + ++--------------+--------------------------------------+ +| a and b | True if a and b are True | ++--------------+--------------------------------------+ +| a or b | True if a or b is True | ++--------------+--------------------------------------+ +| not a | True if a is False, otherwise False | ++--------------+--------------------------------------+ + +.. note:: + Expressions such as ``if (a and b):`` currently do not perform short-circuit evaluation. + In this case ``b`` will also be evaluated even when ``a`` is ``False``. + Users should take care to ensure that secondary conditions are safe to evaluate (e.g.: do not index out of bounds) in all cases. + + +Comparison Operators +#################### + ++----------+---------------------------------------+ +| a > b | True if a strictly greater than b | ++----------+---------------------------------------+ +| a < b | True if a strictly less than b | ++----------+---------------------------------------+ +| a >= b | True if a greater than or equal to b | ++----------+---------------------------------------+ +| a <= b | True if a less than or equal to b | ++----------+---------------------------------------+ +| a == b | True if a equals b | ++----------+---------------------------------------+ +| a != b | True if a not equal to b | ++----------+---------------------------------------+ + +Arithmetic Operators +#################### + ++-----------+--------------------------+ +| a + b | Addition | ++-----------+--------------------------+ +| a - b | Subtraction | ++-----------+--------------------------+ +| a * b | Multiplication | ++-----------+--------------------------+ +| a / b | Floating point division | ++-----------+--------------------------+ +| a // b | Floored division | ++-----------+--------------------------+ +| a ** b | Exponentiation | ++-----------+--------------------------+ +| a % b | Modulus | ++-----------+--------------------------+ + +.. note:: + Since implicit conversions are not performed arguments types to operators should match. + Users should use type constructors, e.g.: ``float()``, ``int()``, ``wp.int64()``, etc. to cast variables + to the correct type. Also note that the multiplication expression ``a * b`` is used to represent scalar + multiplication and matrix multiplication. The ``@`` operator is not currently supported. + +Graphs +----------- + +Launching kernels from Python introduces significant additional overhead compared to C++ or native programs. +To address this, Warp exposes the concept of `CUDA graphs `_ +to allow recording large batches of kernels and replaying them with very little CPU overhead. + +To record a series of kernel launches use the :func:`wp.capture_begin() ` and +:func:`wp.capture_end() ` API as follows: + +.. code:: python + + # begin capture + wp.capture_begin(device="cuda") + + try: + # record launches + for i in range(100): + wp.launch(kernel=compute1, inputs=[a, b], device="cuda") + finally: + # end capture and return a graph object + graph = wp.capture_end(device="cuda") + +We strongly recommend the use of the the try-finally pattern when capturing graphs because the `finally` +statement will ensure :func:`wp.capture_end ` gets called, even if an exception occurs during +capture, which would otherwise trap the stream in a capturing state. + +Once a graph has been constructed it can be executed: :: + + wp.capture_launch(graph) + +The :class:`wp.ScopedCapture ` context manager can be used to simplify the code and +ensure that :func:`wp.capture_end ` is called regardless of exceptions: + +.. code:: python + + with wp.ScopedCapture(device="cuda") as capture: + # record launches + for i in range(100): + wp.launch(kernel=compute1, inputs=[a, b], device="cuda") + + wp.capture_launch(capture.graph) + +Note that only launch calls are recorded in the graph, any Python executed outside of the kernel code will not be recorded. +Typically it is only beneficial to use CUDA graphs when the graph will be reused or launched multiple times. + +.. autofunction:: capture_begin +.. autofunction:: capture_end +.. autofunction:: capture_launch + +.. autoclass:: ScopedCapture + :members: + + +Meshes +------ + +Warp provides a ``wp.Mesh`` class to manage triangle mesh data. To create a mesh users provide a points, indices and optionally a velocity array:: + + mesh = wp.Mesh(points, indices, velocities) + +.. note:: + Mesh objects maintain references to their input geometry buffers. All buffers should live on the same device. + +Meshes can be passed to kernels using their ``id`` attribute which uniquely identifies the mesh by a unique ``uint64`` value. +Once inside a kernel you can perform geometric queries against the mesh such as ray-casts or closest point lookups:: + + @wp.kernel + def raycast(mesh: wp.uint64, + ray_origin: wp.array(dtype=wp.vec3), + ray_dir: wp.array(dtype=wp.vec3), + ray_hit: wp.array(dtype=wp.vec3)): + + tid = wp.tid() + + t = float(0.0) # hit distance along ray + u = float(0.0) # hit face barycentric u + v = float(0.0) # hit face barycentric v + sign = float(0.0) # hit face sign + n = wp.vec3() # hit face normal + f = int(0) # hit face index + + color = wp.vec3() + + # ray cast against the mesh + if wp.mesh_query_ray(mesh, ray_origin[tid], ray_dir[tid], 1.e+6, t, u, v, sign, n, f): + + # if we got a hit then set color to the face normal + color = n*0.5 + wp.vec3(0.5, 0.5, 0.5) + + ray_hit[tid] = color + + +Users may update mesh vertex positions at runtime simply by modifying the points buffer. +After modifying point locations users should call ``Mesh.refit()`` to rebuild the bounding volume hierarchy (BVH) structure and ensure that queries work correctly. + +.. note:: + Updating Mesh topology (indices) at runtime is not currently supported. Users should instead recreate a new Mesh object. + +.. autoclass:: Mesh + :members: + +Hash Grids +---------- + +Many particle-based simulation methods such as the Discrete Element Method (DEM), or Smoothed Particle Hydrodynamics (SPH), involve iterating over spatial neighbors to compute force interactions. Hash grids are a well-established data structure to accelerate these nearest neighbor queries, and particularly well-suited to the GPU. + +To support spatial neighbor queries Warp provides a ``HashGrid`` object that may be created as follows:: + + grid = wp.HashGrid(dim_x=128, dim_y=128, dim_z=128, device="cuda") + + grid.build(points=p, radius=r) + +``p`` is an array of ``wp.vec3`` point positions, and ``r`` is the radius to use when building the grid. +Neighbors can then be iterated over inside the kernel code using :func:`wp.hash_grid_query() ` +and :func:`wp.hash_grid_query_next() ` as follows: + +.. code:: python + + @wp.kernel + def sum(grid : wp.uint64, + points: wp.array(dtype=wp.vec3), + output: wp.array(dtype=wp.vec3), + radius: float): + + tid = wp.tid() + + # query point + p = points[tid] + + # create grid query around point + query = wp.hash_grid_query(grid, p, radius) + index = int(0) + + sum = wp.vec3() + + while(wp.hash_grid_query_next(query, index)): + + neighbor = points[index] + + # compute distance to neighbor point + dist = wp.length(p-neighbor) + if (dist <= radius): + sum += neighbor + + output[tid] = sum + +.. note:: + The ``HashGrid`` query will give back all points in *cells* that fall inside the query radius. + When there are hash conflicts it means that some points outside of query radius will be returned, and users should + check the distance themselves inside their kernels. The reason the query doesn't do the check itself for each + returned point is because it's common for kernels to compute the distance themselves, so it would redundant to + check/compute the distance twice. + + +.. autoclass:: HashGrid + :members: + +Volumes +------- + +Sparse volumes are incredibly useful for representing grid data over large domains, such as signed distance fields +(SDFs) for complex objects, or velocities for large-scale fluid flow. Warp supports reading sparse volumetric grids +stored using the `NanoVDB `_ standard. Users can access voxels directly +or use built-in closest-point or trilinear interpolation to sample grid data from world or local space. + +Volume objects can be created directly from Warp arrays containing a NanoVDB grid, from the contents of a +standard ``.nvdb`` file using :func:`load_from_nvdb() `, +from an uncompressed in-memory buffer using :func:`load_from_address() `, +or from a dense 3D NumPy array using :func:`load_from_numpy() `. + +Volumes can also be created using :func:`allocate() `, +:func:`allocate_by_tiles() ` or :func:`allocate_by_voxels() `. +The values for a Volume object can be modified in a Warp kernel using :func:`wp.volume_store() `. + +.. note:: + Warp does not currently support modifying the topology of sparse volumes at runtime. + +Below we give an example of creating a Volume object from an existing NanoVDB file:: + + # open NanoVDB file on disk + file = open("mygrid.nvdb", "rb") + + # create Volume object + volume = wp.Volume.load_from_nvdb(file, device="cpu") + +.. note:: + Files written by the NanoVDB library, commonly marked by the ``.nvdb`` extension, can contain multiple grids with + various compression methods, but a :class:`Volume` object represents a single NanoVDB grid. + The first grid is loaded by default, then Warp volumes corresponding to the other grids in the file can be created + using repeated calls to :func:`load_next_grid() `. + NanoVDB's uncompressed and zip-compressed file formats are supported out-of-the-box, blosc compressed files require + the `blosc` Python package to be installed. + +To sample the volume inside a kernel we pass a reference to it by ID, and use the built-in sampling modes:: + + @wp.kernel + def sample_grid(volume: wp.uint64, + points: wp.array(dtype=wp.vec3), + samples: wp.array(dtype=float)): + + tid = wp.tid() + + # load sample point in world-space + p = points[tid] + + # transform position to the volume's local-space + q = wp.volume_world_to_index(volume, p) + + # sample volume with trilinear interpolation + f = wp.volume_sample(volume, q, wp.Volume.LINEAR, dtype=float) + + # write result + samples[tid] = f + +Warp also supports NanoVDB index grids, which provide a memory-efficient linearization of voxel indices that can refer +to values in arbitrarily shaped arrays:: + + @wp.kernel + def sample_index_grid(volume: wp.uint64, + points: wp.array(dtype=wp.vec3), + voxel_values: wp.array(dtype=Any)): + + tid = wp.tid() + + # load sample point in world-space + p = points[tid] + + # transform position to the volume's local-space + q = wp.volume_world_to_index(volume, p) + + # sample volume with trilinear interpolation + background_value = voxel_values.dtype(0.0) + f = wp.volume_sample_index(volume, q, wp.Volume.LINEAR, voxel_values, background_value) + +The coordinates of all indexable voxels can be recovered using :func:`get_voxels() `. +NanoVDB grids may also contains embedded *blind* data arrays; those can be accessed with the +:func:`feature_array() ` function. + +.. autoclass:: Volume + :members: + :undoc-members: + +.. seealso:: `Reference `__ for the volume functions available in kernels. + + +Bounding Value Hierarchies (BVH) +-------------------------------- + +The :class:`wp.Bvh ` class can be used to create a BVH for a group of bounding volumes. This object can then be traversed +to determine which parts are intersected by a ray using :func:`bvh_query_ray` and which parts overlap +with a certain bounding volume using :func:`bvh_query_aabb`. + +The following snippet demonstrates how to create a :class:`wp.Bvh ` object from 100 random bounding volumes: + +.. code:: python + + rng = np.random.default_rng(123) + + num_bounds = 100 + lowers = rng.random(size=(num_bounds, 3)) * 5.0 + uppers = lowers + rng.random(size=(num_bounds, 3)) * 5.0 + + device_lowers = wp.array(lowers, dtype=wp.vec3, device="cuda:0") + device_uppers = wp.array(uppers, dtype=wp.vec3, device="cuda:0") + + bvh = wp.Bvh(device_lowers, device_uppers) + +.. autoclass:: Bvh + :members: + +Example: BVH Ray Traversal +########################## + +An example of performing a ray traversal on the data structure is as follows: + +.. code:: python + + @wp.kernel + def bvh_query_ray( + bvh_id: wp.uint64, + start: wp.vec3, + dir: wp.vec3, + bounds_intersected: wp.array(dtype=wp.bool), + ): + query = wp.bvh_query_ray(bvh_id, start, dir) + bounds_nr = wp.int32(0) + + while wp.bvh_query_next(query, bounds_nr): + # The ray intersects the volume with index bounds_nr + bounds_intersected[bounds_nr] = True + + + bounds_intersected = wp.zeros(shape=(num_bounds), dtype=wp.bool, device="cuda:0") + query_start = wp.vec3(0.0, 0.0, 0.0) + query_dir = wp.normalize(wp.vec3(1.0, 1.0, 1.0)) + + wp.launch( + kernel=bvh_query_ray, + dim=1, + inputs=[bvh.id, query_start, query_dir, bounds_intersected], + device="cuda:0", + ) + +The Warp kernel ``bvh_query_ray`` is launched with a single thread, provided the unique :class:`uint64` +identifier of the :class:`wp.Bvh ` object, parameters describing the ray, and an array to store the results. +In ``bvh_query_ray``, :func:`wp.bvh_query_ray() ` is called once to obtain an object that is stored in the +variable ``query``. An integer is also allocated as ``bounds_nr`` to store the volume index of the traversal. +A while statement is used for the actual traversal using :func:`wp.bvh_query_next() `, +which returns ``True`` as long as there are intersecting bounds. + +Example: BVH Volume Traversal +############################# + +Similar to the ray-traversal example, we can perform volume traversal to find the volumes that are fully contained +within a specified bounding box. + +.. code:: python + + @wp.kernel + def bvh_query_aabb( + bvh_id: wp.uint64, + lower: wp.vec3, + upper: wp.vec3, + bounds_intersected: wp.array(dtype=wp.bool), + ): + query = wp.bvh_query_aabb(bvh_id, lower, upper) + bounds_nr = wp.int32(0) + + while wp.bvh_query_next(query, bounds_nr): + # The volume with index bounds_nr is fully contained + # in the (lower,upper) bounding box + bounds_intersected[bounds_nr] = True + + + bounds_intersected = wp.zeros(shape=(num_bounds), dtype=wp.bool, device="cuda:0") + query_lower = wp.vec3(4.0, 4.0, 4.0) + query_upper = wp.vec3(6.0, 6.0, 6.0) + + wp.launch( + kernel=bvh_query_aabb, + dim=1, + inputs=[bvh.id, query_lower, query_upper, bounds_intersected], + device="cuda:0", + ) + +The kernel is nearly identical to the ray-traversal example, except we obtain ``query`` using +:func:`wp.bvh_query_aabb() `. + +Marching Cubes +-------------- + +The :class:`wp.MarchingCubes ` class can be used to extract a 2-D mesh approximating an +isosurface of a 3-D scalar field. The resulting triangle mesh can be saved to a USD +file using the :class:`warp.renderer.UsdRenderer`. + +See :github:`warp/examples/core/example_marching_cubes.py` for a usage example. + +.. autoclass:: MarchingCubes + :members: + +Profiling +--------- + +``wp.ScopedTimer`` objects can be used to gain some basic insight into the performance of Warp applications: + +.. code:: python + + with wp.ScopedTimer("grid build"): + self.grid.build(self.x, self.point_radius) + +This results in a printout at runtime to the standard output stream like: + +.. code:: console + + grid build took 0.06 ms + +See :doc:`../profiling` documentation for more information. + +.. autoclass:: warp.ScopedTimer + :noindex: diff --git a/_sources/modules/sim.rst.txt b/_sources/modules/sim.rst.txt new file mode 100644 index 00000000..f6d6ce06 --- /dev/null +++ b/_sources/modules/sim.rst.txt @@ -0,0 +1,190 @@ +warp.sim +======== + +.. currentmodule:: warp.sim + +Warp includes a simulation module ``warp.sim`` that includes many common physical simulation models and integrators for explicit and implicit time-stepping. + +Model +----- + +.. autoclass:: ModelBuilder + :members: + +.. autoclass:: Model + :members: + +.. autoclass:: ModelShapeMaterials + :members: + +.. autoclass:: ModelShapeGeometry + :members: + +.. autoclass:: JointAxis + :members: + +.. autoclass:: Mesh + :members: + +.. autoclass:: SDF + :members: + +.. _Joint types: + +Joint types +^^^^^^^^^^^^^^ + +.. data:: JOINT_PRISMATIC + + Prismatic (slider) joint + +.. data:: JOINT_REVOLUTE + + Revolute (hinge) joint + +.. data:: JOINT_BALL + + Ball (spherical) joint with quaternion state representation + +.. data:: JOINT_FIXED + + Fixed (static) joint + +.. data:: JOINT_FREE + + Free (floating) joint + +.. data:: JOINT_COMPOUND + + Compound joint with 3 rotational degrees of freedom + +.. data:: JOINT_UNIVERSAL + + Universal joint with 2 rotational degrees of freedom + +.. data:: JOINT_DISTANCE + + Distance joint that keeps two bodies at a distance within its joint limits (only supported in :class:`XPBDIntegrator` at the moment) + +.. data:: JOINT_D6 + + Generic D6 joint with up to 3 translational and 3 rotational degrees of freedom + +.. _Joint modes: + +Joint control modes +^^^^^^^^^^^^^^^^^^^ + +Joint modes control the behavior of how the joint control input :attr:`Control.joint_act` affects the torque applied at a given joint axis. +By default, it behaves as a direct force application via :data:`JOINT_MODE_FORCE`. Other modes can be used to implement joint position or velocity drives: + +.. data:: JOINT_MODE_FORCE + + This is the default control mode where the control input is the torque :math:`\tau` applied at the joint axis. + +.. data:: JOINT_MODE_TARGET_POSITION + + The control input is the target position :math:`\mathbf{q}_{\text{target}}` which is achieved via PD control of torque :math:`\tau` where the proportional and derivative gains are set by :attr:`Model.joint_target_ke` and :attr:`Model.joint_target_kd`: + + .. math:: + + \tau = k_e (\mathbf{q}_{\text{target}} - \mathbf{q}) - k_d \mathbf{\dot{q}} + +.. data:: JOINT_MODE_TARGET_VELOCITY + + The control input is the target velocity :math:`\mathbf{\dot{q}}_{\text{target}}` which is achieved via a controller of torque :math:`\tau` that brings the velocity at the joint axis to the target through proportional gain :attr:`Model.joint_target_ke`: + + .. math:: + + \tau = k_e (\mathbf{\dot{q}}_{\text{target}} - \mathbf{\dot{q}}) + +State +-------------- + +.. autoclass:: State + :members: + +Control +-------------- + +.. autoclass:: Control + :members: + +.. _FK-IK: + +Forward / Inverse Kinematics +---------------------------- + +Articulated rigid-body mechanisms are kinematically described by the joints that connect the bodies as well as the relative relative transform from the parent and child body to the respective anchor frames of the joint in the parent and child body: + +.. image:: /img/joint_transforms.png + :width: 400 + :align: center + +.. list-table:: Variable names in the kernels from articulation.py + :widths: 10 90 + :header-rows: 1 + + * - Symbol + - Description + * - x_wp + - World transform of the parent body (stored at :attr:`State.body_q`) + * - x_wc + - World transform of the child body (stored at :attr:`State.body_q`) + * - x_pj + - Transform from the parent body to the joint parent anchor frame (defined by :attr:`Model.joint_X_p`) + * - x_cj + - Transform from the child body to the joint child anchor frame (defined by :attr:`Model.joint_X_c`) + * - x_j + - Joint transform from the joint parent anchor frame to the joint child anchor frame + +In the forward kinematics, the joint transform is determined by the joint coordinates (generalized joint positions :attr:`State.joint_q` and velocities :attr:`State.joint_qd`). +Given the parent body's world transform :math:`x_{wp}` and the joint transform :math:`x_{j}`, the child body's world transform :math:`x_{wc}` is computed as: + +.. math:: + x_{wc} = x_{wp} \cdot x_{pj} \cdot x_{j} \cdot x_{cj}^{-1}. + +.. autofunction:: eval_fk + +.. autofunction:: eval_ik + +Integrators +----------- + +.. autoclass:: Integrator + :members: + +.. autoclass:: SemiImplicitIntegrator + :members: + +.. autoclass:: XPBDIntegrator + :members: + +.. autoclass:: FeatherstoneIntegrator + :members: + +Importers +--------- + +Warp sim supports the loading of simulation models from URDF, MuJoCo (MJCF), and USD Physics files. + +.. autofunction:: parse_urdf + +.. autofunction:: parse_mjcf + +.. autofunction:: parse_usd + +.. autofunction:: resolve_usd_from_url + +Utility Functions +----------------- + +Common utility functions used in simulators. + +.. autofunction:: velocity_at_point + +.. autofunction:: quat_to_euler + +.. autofunction:: quat_from_euler + +.. autofunction:: load_mesh diff --git a/_sources/modules/sparse.rst.txt b/_sources/modules/sparse.rst.txt new file mode 100644 index 00000000..f56493a3 --- /dev/null +++ b/_sources/modules/sparse.rst.txt @@ -0,0 +1,36 @@ +warp.sparse +=============================== + +.. currentmodule:: warp.sparse + +.. + .. toctree:: + :maxdepth: 2 + +Warp includes a sparse linear algebra module ``warp.sparse`` that implements some common sparse matrix operations that arise in simulation. + +Sparse Matrices +------------------------- + +Currently `warp.sparse` supports Block Sparse Row (BSR) matrices, the BSR format can also be used to represent Compressed Sparse Row (CSR) matrices as a special case with a 1x1 block size. + +Overloaded Python mathematical operators are supported for sparse matrix addition (`+`), subtraction (`-`), multiplication by a scalar (`*`) and matrix-matrix or matrix-vector multiplication (`@`), +including in-place variants where possible. + +.. automodule:: warp.sparse + :members: + +.. _iterative-linear-solvers: + +Iterative Linear Solvers +------------------------ + +.. currentmodule:: warp.optim.linear + +Warp provides a few common iterative linear solvers (:func:`cg`, :func:`cr`, :func:`bicgstab`, :func:`gmres`) with optional preconditioning. + +.. note:: While primarily intended to work with sparse matrices, those solvers also accept dense linear operators provided as 2D Warp arrays. + It is also possible to provide custom operators, see :class:`LinearOperator`. + +.. automodule:: warp.optim.linear + :members: diff --git a/_sources/profiling.rst.txt b/_sources/profiling.rst.txt new file mode 100644 index 00000000..178b99aa --- /dev/null +++ b/_sources/profiling.rst.txt @@ -0,0 +1,277 @@ +Profiling +========= + +ScopedTimer +----------- + +``wp.ScopedTimer`` objects can be used to gain some basic insight into the performance of Warp applications: + +.. code:: python + + @wp.kernel + def inc_loop(a: wp.array(dtype=float), num_iters: int): + i = wp.tid() + for j in range(num_iters): + a[i] += 1.0 + + n = 10_000_000 + devices = wp.get_cuda_devices() + + # pre-allocate host arrays for readback + host_arrays = [ + wp.empty(n, dtype=float, device="cpu", pinned=True) for _ in devices + ] + + # code for profiling + with wp.ScopedTimer("Demo"): + for i, device in enumerate(devices): + a = wp.zeros(n, dtype=float, device=device) + wp.launch(inc_loop, dim=n, inputs=[a, 500], device=device) + wp.launch(inc_loop, dim=n, inputs=[a, 1000], device=device) + wp.launch(inc_loop, dim=n, inputs=[a, 1500], device=device) + wp.copy(host_arrays[i], a) + +The only required argument for the ``ScopedTimer`` constructor is a string label, which can be used to distinguish multiple timed code sections when reading the output. The snippet above will print a message like this: + +.. code:: console + + Demo took 0.52 ms + +By default, ``ScopedTimer`` measures the elapsed time on the CPU and does not introduce any CUDA synchronization. Since most CUDA operations are asynchronous, the result does not include the time spent executing kernels and memory transfers on the CUDA device. It's still a useful measurement, because it shows how long it took to schedule the CUDA operations on the CPU. + +To get the total amount of time including the device executions time, create the ``ScopedTimer`` with the ``synchronize=True`` flag. This is equivalent to calling ``wp.synchronize()`` before and after the timed section of code. Synchronizing at the beginning ensures that all prior CUDA work has completed prior to starting the timer. Synchronizing at the end ensures that all timed work finishes before stopping the timer. With the example above, the result might look like this: + +.. code:: console + + Demo took 4.91 ms + +The timing values will vary slightly from run to run and will depend on the system hardware and current load. The sample results presented here were obtained on a system with one RTX 4090 GPU, one RTX 3090 GPU, and an AMD Ryzen Threadripper Pro 5965WX CPU. For each GPU, the code allocates and initializes an array with 10 million floating point elements. It then launches the ``inc_loop`` kernel three times on the array. The kernel increments each array element a given number of times - 500, 1000, and 1500. Finally, the code copies the array contents to the CPU. + +Profiling complex programs with many asynchronous and concurrent operations can be tricky. Profiling tools like `NVIDIA Nsight Systems `_ can present the results in a visual way and capture a plethora of timing information for deeper study. For profiling tools capable of visualizing NVTX ranges, ``ScopedTimer`` can be created with the ``use_nvtx=True`` argument. This will mark the CPU execution range on the timeline for easier visual inspection. The color can be customized using the ``color`` argument, as shown below: + +.. code:: python + + with wp.ScopedTimer("Demo", use_nvtx=True, color="yellow"): + ... + +To use NVTX integration, you will need to install the `NVIDIA NVTX Python package `_. + +.. code:: + + pip install nvtx + +The package allows you to insert custom NVTX ranges into your code (``nvtx.annotate``) and customize the `colors `_. + +Here is what the demo code looks like in Nsight Systems (click to enlarge the image): + +.. image:: ./img/profiling_nosync.png + :width: 95% + :align: center + +There are a few noteworthy observations we can make from this capture. Scheduling and launching the work on the CPU takes about half a millisecond, as shown in the `NVTX / Start & End` row. This time also includes the allocation of arrays on both CUDA devices. We can see that the execution on each device is asynchronous with respect to the host, since CUDA operations start running before the yellow `Demo` NVTX range finishes. We can also see that the operations on different CUDA devices execute concurrently, including kernels and memory transfers. The kernels run faster on the first CUDA device (RTX 4090) than the second device (RTX 3090). Memory transfers take about the same time on each device. Using pinned CPU arrays for the transfer destinations allows the transfers to run asynchronously without involving the CPU. + +Check out the :doc:`concurrency documentation ` for more information about asynchronous operations. + +Note that synchronization was not enabled in this run, so the NVTX range only spans the CPU operations used to schedule the CUDA work. When synchronization is enabled, the timer will wait for all CUDA work to complete, so the NVTX range will span the synchronization of both devices: + +.. code:: python + + with wp.ScopedTimer("Demo", use_nvtx=True, color="yellow", synchronize=True): + ... + +.. image:: ./img/profiling_sync.png + :width: 95% + :align: center + + +CUDA Activity Profiling +----------------------- + +``ScopedTimer`` supports timing individual CUDA activities like kernels and memory operations. This is done by measuring the time taken between :ref:`CUDA events ` on the device. To get information about CUDA activities, pass the ``cuda_filter`` argument to the ``ScopedTimer`` constructor. The ``cuda_filter`` can be a bitwise combination of the following values: + +.. list-table:: CUDA profiling flags + :widths: 25 50 + :header-rows: 0 + + * - ``wp.TIMING_KERNELS`` + - Warp kernels (this includes all kernels written in Python as ``@wp.kernel``) + * - ``wp.TIMING_KERNELS_BUILTIN`` + - Builtin kernels (this includes kernels used by the Warp library under the hood) + * - ``wp.TIMING_MEMCPY`` + - CUDA memory transfers (host-to-device, device-to-host, device-to-device, and peer-to-peer) + * - ``wp.TIMING_MEMSET`` + - CUDA memset operations (e.g., zeroing out memory in ``wp.zeros()``) + * - ``wp.TIMING_GRAPH`` + - CUDA graph launches + * - ``wp.TIMING_ALL`` + - Combines all of the above for convenience. + +When a non-zero ``cuda_filter`` is specified, Warp will inject CUDA events for timing purposes and report the results when the ``ScopeTimer`` finishes. This adds some overhead to the code, so should be used only during profiling. + +CUDA event timing resolution is about 0.5 microseconds. The reported execution time of short operations will likely be longer than the operations actually took on the device. This is due to the timing resolution and the overhead of added instrumentation code. For more precise analysis of short operations, a tool like Nsight Systems can report more accurate data. + +Enabling CUDA profiling with the demo code can be done like this: + +.. code:: python + + with wp.ScopedTimer("Demo", cuda_filter=wp.TIMING_ALL): + ... + +This adds additional information to the output: + +.. code:: + + CUDA timeline: + ----------------+---------+------------------------ + Time | Device | Activity + ----------------+---------+------------------------ + 0.021504 ms | cuda:0 | memset + 0.163840 ms | cuda:0 | forward kernel inc_loop + 0.306176 ms | cuda:0 | forward kernel inc_loop + 0.451584 ms | cuda:0 | forward kernel inc_loop + 2.455520 ms | cuda:0 | memcpy DtoH + 0.051200 ms | cuda:1 | memset + 0.374784 ms | cuda:1 | forward kernel inc_loop + 0.707584 ms | cuda:1 | forward kernel inc_loop + 1.042432 ms | cuda:1 | forward kernel inc_loop + 2.136096 ms | cuda:1 | memcpy DtoH + + CUDA activity summary: + ----------------+---------+------------------------ + Total time | Count | Activity + ----------------+---------+------------------------ + 0.072704 ms | 2 | memset + 3.046400 ms | 6 | forward kernel inc_loop + 4.591616 ms | 2 | memcpy DtoH + + CUDA device summary: + ----------------+---------+------------------------ + Total time | Count | Device + ----------------+---------+------------------------ + 3.398624 ms | 5 | cuda:0 + 4.312096 ms | 5 | cuda:1 + Demo took 0.92 ms + +The first section is the `CUDA timeline`, which lists all captured activities in issue order. We see a `memset` on device ``cuda:0``, which corresponds to clearing the memory in ``wp.zeros()``. This is followed by three launches of the ``inc_loop`` kernel on ``cuda:0`` and a memory transfer from device to host issued by ``wp.copy()``. The remaining entries repeat similar operations on device ``cuda:1``. + +The next section is the `CUDA activity summary`, which reports the cumulative time taken by each activity type. Here, the `memsets`, kernel launches, and memory transfer operations are grouped together. This is a good way to see where time is being spent overall. The `memsets` are quite fast. The ``inc_loop`` kernel launches took about three milliseconds of combined GPU time. The memory transfers took the longest, over four milliseconds. + +The `CUDA device summary` shows the total time taken per device. We see that device ``cuda:0`` took about 3.4 ms to complete the tasks and device ``cuda:1`` took about 4.3 ms. This summary can be used to asses the workload distribution in multi-GPU applications. + +The final line shows the time taken by the CPU, as with the default ``ScopedTimer`` options (without synchronization in this case). + +Customizing the output +~~~~~~~~~~~~~~~~~~~~~~ + +It is possible to customize how the activity timing results are reported. The function :func:`warp.timing_print` is used by default. To use a different reporting function, pass it as the ``report_func`` argument to ``ScopedTimer``. The custom report function should take a list of :class:`warp.TimingResult` objects as the first argument. Each result in the list corresponds to a single activity and the list represents the complete recorded timeline. By manually traversing the list, you can customize the formatting of the output, apply custom sorting rules, and aggregate the results as desired. The second argument is a string indent that should be printed at the beginning of each line. This is for compatibility with ``ScopedTimer`` indenting rules used with nested timers. + +Here is an example of a custom reporting function, which aggregates the total time spend in forward and backward kernels: + +.. code:: python + + def print_custom_report(results, indent=""): + forward_time = 0 + backward_time = 0 + + for r in results: + # aggregate all forward kernels + if r.name.startswith("forward kernel"): + forward_time += r.elapsed + # aggregate all backward kernels + elif r.name.startswith("backward kernel"): + backward_time += r.elapsed + + print(f"{indent}Forward kernels : {forward_time:.6f} ms") + print(f"{indent}Backward kernels : {backward_time:.6f} ms") + +Let's apply it to one of the Warp examples: + +.. code:: python + + from warp.examples.optim.example_cloth_throw import Example + + example = Example(None) + example.use_graph = False # disable graphs so we get timings for individual kernels + + with wp.ScopedTimer("Example", cuda_filter=wp.TIMING_KERNEL, report_func=print_custom_report): + for iteration in range(5): + example.step() + +This produces a report like this: + +.. code:: + + Forward kernels : 187.098367 ms + Backward kernels : 245.070177 ms + + +Using the activity timing functions directly +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +It is also possible to capture activity timings without using the ``ScopedTimer`` at all. Simply call :func:`warp.timing_begin` to start recording activity timings and :func:`warp.timing_end` to stop and get a list of recorded activities. You can use :func:`warp.timing_print` to print the default activity report or generate your own report from the list of results. + +.. code:: python + + wp.timing_begin(cuda_filter=wp.TIMING_ALL) + ... + results = wp.timing_end() + + wp.timing_print(results) + + +Limitations +~~~~~~~~~~~ + +Currently, detailed activity timing is only available for CUDA devices, but support for CPU timing may be added in the future. + +The activity profiling only records activities initiated using the Warp API. It does not capture CUDA activity initiated by other frameworks. A profiling tool like Nsight Systems can be used to examine whole program activities. + + +Using CUDA Events +----------------- + +CUDA events can be used for timing purposes outside of the ``ScopedTimer``. Here is an example: + +.. code:: python + + with wp.ScopedDevice("cuda:0") as device: + + # ensure the module is loaded + wp.load_module(device=device) + + # create events with enabled timing + e1 = wp.Event(enable_timing=True) + e2 = wp.Event(enable_timing=True) + + n = 10000000 + + # start timing... + wp.record_event(e1) + + a = wp.zeros(n, dtype=float) + wp.launch(inc, dim=n, inputs=[a]) + + # ...end timing + wp.record_event(e2) + + # get elapsed time between the two events + elapsed = wp.get_event_elapsed_time(e1, e2) + print(elapsed) + +The events must be created with the flag ``enable_timing=True``. The first event is recorded at the start of the timed code and the second event is recorded at the end. The function :func:`warp.get_event_elapsed_time()` is used to compute the time difference between the two events. We must ensure that both events have completed on the device before calling :func:`warp.get_event_elapsed_time()`. By default, this function will synchronize on the second event using :func:`warp.synchronize_event()`. If that is not desired, the user may pass the ``synchronize=False`` flag and must use some other means of ensuring that both events have completed prior to calling the function. + +Note that timing very short operations may yield inflated results, due to the timing resolution of CUDA events and the overhead of the profiling code. In most cases, CUDA activity profiling with ``ScopedTimer`` will have less overhead and better precision. For the most accurate results, a profiling tool such as Nsight Systems should be used. The main benefit of using the manual event timing API is that it allows timing arbitrary sections of code rather than individual activities. + +Profiling API Reference +----------------------- + +.. autoclass:: warp.ScopedTimer + +.. autofunction:: warp.get_event_elapsed_time +.. autofunction:: warp.synchronize_event + +.. autoclass:: warp.TimingResult + +.. autofunction:: warp.timing_begin +.. autofunction:: warp.timing_end +.. autofunction:: warp.timing_print diff --git a/_static/basic.css b/_static/basic.css new file mode 100644 index 00000000..f316efcb --- /dev/null +++ b/_static/basic.css @@ -0,0 +1,925 @@ +/* + * basic.css + * ~~~~~~~~~ + * + * Sphinx stylesheet -- basic theme. + * + * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +/* -- main layout ----------------------------------------------------------- */ + +div.clearer { + clear: both; +} + +div.section::after { + display: block; + content: ''; + clear: left; +} + +/* -- relbar ---------------------------------------------------------------- */ + +div.related { + width: 100%; + font-size: 90%; +} + +div.related h3 { + display: none; +} + +div.related ul { + margin: 0; + padding: 0 0 0 10px; + list-style: none; +} + +div.related li { + display: inline; +} + +div.related li.right { + float: right; + margin-right: 5px; +} + +/* -- sidebar --------------------------------------------------------------- */ + +div.sphinxsidebarwrapper { + padding: 10px 5px 0 10px; +} + +div.sphinxsidebar { + float: left; + width: 230px; + margin-left: -100%; + font-size: 90%; + word-wrap: break-word; + overflow-wrap : break-word; +} + +div.sphinxsidebar ul { + list-style: none; +} + +div.sphinxsidebar ul ul, +div.sphinxsidebar ul.want-points { + margin-left: 20px; + list-style: square; +} + +div.sphinxsidebar ul ul { + margin-top: 0; + margin-bottom: 0; +} + +div.sphinxsidebar form { + margin-top: 10px; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + +div.sphinxsidebar #searchbox form.search { + overflow: hidden; +} + +div.sphinxsidebar #searchbox input[type="text"] { + float: left; + width: 80%; + padding: 0.25em; + box-sizing: border-box; +} + +div.sphinxsidebar #searchbox input[type="submit"] { + float: left; + width: 20%; + border-left: none; + padding: 0.25em; + box-sizing: border-box; +} + + +img { + border: 0; + max-width: 100%; +} + +/* -- search page ----------------------------------------------------------- */ + +ul.search { + margin: 10px 0 0 20px; + padding: 0; +} + +ul.search li { + padding: 5px 0 5px 20px; + background-image: url(file.png); + background-repeat: no-repeat; + background-position: 0 7px; +} + +ul.search li a { + font-weight: bold; +} + +ul.search li p.context { + color: #888; + margin: 2px 0 0 30px; + text-align: left; +} + +ul.keywordmatches li.goodmatch a { + font-weight: bold; +} + +/* -- index page ------------------------------------------------------------ */ + +table.contentstable { + width: 90%; + margin-left: auto; + margin-right: auto; +} + +table.contentstable p.biglink { + line-height: 150%; +} + +a.biglink { + font-size: 1.3em; +} + +span.linkdescr { + font-style: italic; + padding-top: 5px; + font-size: 90%; +} + +/* -- general index --------------------------------------------------------- */ + +table.indextable { + width: 100%; +} + +table.indextable td { + text-align: left; + vertical-align: top; +} + +table.indextable ul { + margin-top: 0; + margin-bottom: 0; + list-style-type: none; +} + +table.indextable > tbody > tr > td > ul { + padding-left: 0em; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +div.modindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +div.genindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +/* -- domain module index --------------------------------------------------- */ + +table.modindextable td { + padding: 2px; + border-collapse: collapse; +} + +/* -- general body styles --------------------------------------------------- */ + +div.body { + min-width: 360px; + max-width: 800px; +} + +div.body p, div.body dd, div.body li, div.body blockquote { + -moz-hyphens: auto; + -ms-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; +} + +a.headerlink { + visibility: hidden; +} + +a:visited { + color: #551A8B; +} + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink, +caption:hover > a.headerlink, +p.caption:hover > a.headerlink, +div.code-block-caption:hover > a.headerlink { + visibility: visible; +} + +div.body p.caption { + text-align: inherit; +} + +div.body td { + text-align: left; +} + +.first { + margin-top: 0 !important; +} + +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +img.align-left, figure.align-left, .figure.align-left, object.align-left { + clear: left; + float: left; + margin-right: 1em; +} + +img.align-right, figure.align-right, .figure.align-right, object.align-right { + clear: right; + float: right; + margin-left: 1em; +} + +img.align-center, figure.align-center, .figure.align-center, object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + +img.align-default, figure.align-default, .figure.align-default { + display: block; + margin-left: auto; + margin-right: auto; +} + +.align-left { + text-align: left; +} + +.align-center { + text-align: center; +} + +.align-default { + text-align: center; +} + +.align-right { + text-align: right; +} + +/* -- sidebars -------------------------------------------------------------- */ + +div.sidebar, +aside.sidebar { + margin: 0 0 0.5em 1em; + border: 1px solid #ddb; + padding: 7px; + background-color: #ffe; + width: 40%; + float: right; + clear: right; + overflow-x: auto; +} + +p.sidebar-title { + font-weight: bold; +} + +nav.contents, +aside.topic, +div.admonition, div.topic, blockquote { + clear: left; +} + +/* -- topics ---------------------------------------------------------------- */ + +nav.contents, +aside.topic, +div.topic { + border: 1px solid #ccc; + padding: 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* -- admonitions ----------------------------------------------------------- */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; +} + +div.body p.centered { + text-align: center; + margin-top: 25px; +} + +/* -- content of sidebars/topics/admonitions -------------------------------- */ + +div.sidebar > :last-child, +aside.sidebar > :last-child, +nav.contents > :last-child, +aside.topic > :last-child, +div.topic > :last-child, +div.admonition > :last-child { + margin-bottom: 0; +} + +div.sidebar::after, +aside.sidebar::after, +nav.contents::after, +aside.topic::after, +div.topic::after, +div.admonition::after, +blockquote::after { + display: block; + content: ''; + clear: both; +} + +/* -- tables ---------------------------------------------------------------- */ + +table.docutils { + margin-top: 10px; + margin-bottom: 10px; + border: 0; + border-collapse: collapse; +} + +table.align-center { + margin-left: auto; + margin-right: auto; +} + +table.align-default { + margin-left: auto; + margin-right: auto; +} + +table caption span.caption-number { + font-style: italic; +} + +table caption span.caption-text { +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 5px; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +th { + text-align: left; + padding-right: 5px; +} + +table.citation { + border-left: solid 1px gray; + margin-left: 1px; +} + +table.citation td { + border-bottom: none; +} + +th > :first-child, +td > :first-child { + margin-top: 0px; +} + +th > :last-child, +td > :last-child { + margin-bottom: 0px; +} + +/* -- figures --------------------------------------------------------------- */ + +div.figure, figure { + margin: 0.5em; + padding: 0.5em; +} + +div.figure p.caption, figcaption { + padding: 0.3em; +} + +div.figure p.caption span.caption-number, +figcaption span.caption-number { + font-style: italic; +} + +div.figure p.caption span.caption-text, +figcaption span.caption-text { +} + +/* -- field list styles ----------------------------------------------------- */ + +table.field-list td, table.field-list th { + border: 0 !important; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +.field-name { + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; +} + +/* -- hlist styles ---------------------------------------------------------- */ + +table.hlist { + margin: 1em 0; +} + +table.hlist td { + vertical-align: top; +} + +/* -- object description styles --------------------------------------------- */ + +.sig { + font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; +} + +.sig-name, code.descname { + background-color: transparent; + font-weight: bold; +} + +.sig-name { + font-size: 1.1em; +} + +code.descname { + font-size: 1.2em; +} + +.sig-prename, code.descclassname { + background-color: transparent; +} + +.optional { + font-size: 1.3em; +} + +.sig-paren { + font-size: larger; +} + +.sig-param.n { + font-style: italic; +} + +/* C++ specific styling */ + +.sig-inline.c-texpr, +.sig-inline.cpp-texpr { + font-family: unset; +} + +.sig.c .k, .sig.c .kt, +.sig.cpp .k, .sig.cpp .kt { + color: #0033B3; +} + +.sig.c .m, +.sig.cpp .m { + color: #1750EB; +} + +.sig.c .s, .sig.c .sc, +.sig.cpp .s, .sig.cpp .sc { + color: #067D17; +} + + +/* -- other body styles ----------------------------------------------------- */ + +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; +} + +:not(li) > ol > li:first-child > :first-child, +:not(li) > ul > li:first-child > :first-child { + margin-top: 0px; +} + +:not(li) > ol > li:last-child > :last-child, +:not(li) > ul > li:last-child > :last-child { + margin-bottom: 0px; +} + +ol.simple ol p, +ol.simple ul p, +ul.simple ol p, +ul.simple ul p { + margin-top: 0; +} + +ol.simple > li:not(:first-child) > p, +ul.simple > li:not(:first-child) > p { + margin-top: 0; +} + +ol.simple p, +ul.simple p { + margin-bottom: 0; +} + +aside.footnote > span, +div.citation > span { + float: left; +} +aside.footnote > span:last-of-type, +div.citation > span:last-of-type { + padding-right: 0.5em; +} +aside.footnote > p { + margin-left: 2em; +} +div.citation > p { + margin-left: 4em; +} +aside.footnote > p:last-of-type, +div.citation > p:last-of-type { + margin-bottom: 0em; +} +aside.footnote > p:last-of-type:after, +div.citation > p:last-of-type:after { + content: ""; + clear: both; +} + +dl.field-list { + display: grid; + grid-template-columns: fit-content(30%) auto; +} + +dl.field-list > dt { + font-weight: bold; + word-break: break-word; + padding-left: 0.5em; + padding-right: 5px; +} + +dl.field-list > dd { + padding-left: 0.5em; + margin-top: 0em; + margin-left: 0em; + margin-bottom: 0em; +} + +dl { + margin-bottom: 15px; +} + +dd > :first-child { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +.sig dd { + margin-top: 0px; + margin-bottom: 0px; +} + +.sig dl { + margin-top: 0px; + margin-bottom: 0px; +} + +dl > dd:last-child, +dl > dd:last-child > :last-child { + margin-bottom: 0; +} + +dt:target, span.highlighted { + background-color: #fbe54e; +} + +rect.highlighted { + fill: #fbe54e; +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +.versionmodified { + font-style: italic; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +.footnote:target { + background-color: #ffa; +} + +.line-block { + display: block; + margin-top: 1em; + margin-bottom: 1em; +} + +.line-block .line-block { + margin-top: 0; + margin-bottom: 0; + margin-left: 1.5em; +} + +.guilabel, .menuselection { + font-family: sans-serif; +} + +.accelerator { + text-decoration: underline; +} + +.classifier { + font-style: oblique; +} + +.classifier:before { + font-style: normal; + margin: 0 0.5em; + content: ":"; + display: inline-block; +} + +abbr, acronym { + border-bottom: dotted 1px; + cursor: help; +} + +.translated { + background-color: rgba(207, 255, 207, 0.2) +} + +.untranslated { + background-color: rgba(255, 207, 207, 0.2) +} + +/* -- code displays --------------------------------------------------------- */ + +pre { + overflow: auto; + overflow-y: hidden; /* fixes display issues on Chrome browsers */ +} + +pre, div[class*="highlight-"] { + clear: both; +} + +span.pre { + -moz-hyphens: none; + -ms-hyphens: none; + -webkit-hyphens: none; + hyphens: none; + white-space: nowrap; +} + +div[class*="highlight-"] { + margin: 1em 0; +} + +td.linenos pre { + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + display: block; +} + +table.highlighttable tbody { + display: block; +} + +table.highlighttable tr { + display: flex; +} + +table.highlighttable td { + margin: 0; + padding: 0; +} + +table.highlighttable td.linenos { + padding-right: 0.5em; +} + +table.highlighttable td.code { + flex: 1; + overflow: hidden; +} + +.highlight .hll { + display: block; +} + +div.highlight pre, +table.highlighttable pre { + margin: 0; +} + +div.code-block-caption + div { + margin-top: 0; +} + +div.code-block-caption { + margin-top: 1em; + padding: 2px 5px; + font-size: small; +} + +div.code-block-caption code { + background-color: transparent; +} + +table.highlighttable td.linenos, +span.linenos, +div.highlight span.gp { /* gp: Generic.Prompt */ + user-select: none; + -webkit-user-select: text; /* Safari fallback only */ + -webkit-user-select: none; /* Chrome/Safari */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* IE10+ */ +} + +div.code-block-caption span.caption-number { + padding: 0.1em 0.3em; + font-style: italic; +} + +div.code-block-caption span.caption-text { +} + +div.literal-block-wrapper { + margin: 1em 0; +} + +code.xref, a code { + background-color: transparent; + font-weight: bold; +} + +h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { + background-color: transparent; +} + +.viewcode-link { + float: right; +} + +.viewcode-back { + float: right; + font-family: sans-serif; +} + +div.viewcode-block:target { + margin: -1px -10px; + padding: 0 10px; +} + +/* -- math display ---------------------------------------------------------- */ + +img.math { + vertical-align: middle; +} + +div.body div.math p { + text-align: center; +} + +span.eqno { + float: right; +} + +span.eqno a.headerlink { + position: absolute; + z-index: 1; +} + +div.math:hover a.headerlink { + visibility: visible; +} + +/* -- printout stylesheet --------------------------------------------------- */ + +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0 !important; + width: 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + #top-link { + display: none; + } +} \ No newline at end of file diff --git a/_static/check-solid.svg b/_static/check-solid.svg new file mode 100644 index 00000000..92fad4b5 --- /dev/null +++ b/_static/check-solid.svg @@ -0,0 +1,4 @@ + + + + diff --git a/_static/clipboard.min.js b/_static/clipboard.min.js new file mode 100644 index 00000000..54b3c463 --- /dev/null +++ b/_static/clipboard.min.js @@ -0,0 +1,7 @@ +/*! + * clipboard.js v2.0.8 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */ +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ClipboardJS=e():t.ClipboardJS=e()}(this,function(){return n={686:function(t,e,n){"use strict";n.d(e,{default:function(){return o}});var e=n(279),i=n.n(e),e=n(370),u=n.n(e),e=n(817),c=n.n(e);function a(t){try{return document.execCommand(t)}catch(t){return}}var f=function(t){t=c()(t);return a("cut"),t};var l=function(t){var e,n,o,r=1 + + + + diff --git a/_static/copybutton.css b/_static/copybutton.css new file mode 100644 index 00000000..f1916ec7 --- /dev/null +++ b/_static/copybutton.css @@ -0,0 +1,94 @@ +/* Copy buttons */ +button.copybtn { + position: absolute; + display: flex; + top: .3em; + right: .3em; + width: 1.7em; + height: 1.7em; + opacity: 0; + transition: opacity 0.3s, border .3s, background-color .3s; + user-select: none; + padding: 0; + border: none; + outline: none; + border-radius: 0.4em; + /* The colors that GitHub uses */ + border: #1b1f2426 1px solid; + background-color: #f6f8fa; + color: #57606a; +} + +button.copybtn.success { + border-color: #22863a; + color: #22863a; +} + +button.copybtn svg { + stroke: currentColor; + width: 1.5em; + height: 1.5em; + padding: 0.1em; +} + +div.highlight { + position: relative; +} + +/* Show the copybutton */ +.highlight:hover button.copybtn, button.copybtn.success { + opacity: 1; +} + +.highlight button.copybtn:hover { + background-color: rgb(235, 235, 235); +} + +.highlight button.copybtn:active { + background-color: rgb(187, 187, 187); +} + +/** + * A minimal CSS-only tooltip copied from: + * https://codepen.io/mildrenben/pen/rVBrpK + * + * To use, write HTML like the following: + * + *

Short

+ */ + .o-tooltip--left { + position: relative; + } + + .o-tooltip--left:after { + opacity: 0; + visibility: hidden; + position: absolute; + content: attr(data-tooltip); + padding: .2em; + font-size: .8em; + left: -.2em; + background: grey; + color: white; + white-space: nowrap; + z-index: 2; + border-radius: 2px; + transform: translateX(-102%) translateY(0); + transition: opacity 0.2s cubic-bezier(0.64, 0.09, 0.08, 1), transform 0.2s cubic-bezier(0.64, 0.09, 0.08, 1); +} + +.o-tooltip--left:hover:after { + display: block; + opacity: 1; + visibility: visible; + transform: translateX(-100%) translateY(0); + transition: opacity 0.2s cubic-bezier(0.64, 0.09, 0.08, 1), transform 0.2s cubic-bezier(0.64, 0.09, 0.08, 1); + transition-delay: .5s; +} + +/* By default the copy button shouldn't show up when printing a page */ +@media print { + button.copybtn { + display: none; + } +} diff --git a/_static/copybutton.js b/_static/copybutton.js new file mode 100644 index 00000000..ff4aa329 --- /dev/null +++ b/_static/copybutton.js @@ -0,0 +1,248 @@ +// Localization support +const messages = { + 'en': { + 'copy': 'Copy', + 'copy_to_clipboard': 'Copy to clipboard', + 'copy_success': 'Copied!', + 'copy_failure': 'Failed to copy', + }, + 'es' : { + 'copy': 'Copiar', + 'copy_to_clipboard': 'Copiar al portapapeles', + 'copy_success': '¡Copiado!', + 'copy_failure': 'Error al copiar', + }, + 'de' : { + 'copy': 'Kopieren', + 'copy_to_clipboard': 'In die Zwischenablage kopieren', + 'copy_success': 'Kopiert!', + 'copy_failure': 'Fehler beim Kopieren', + }, + 'fr' : { + 'copy': 'Copier', + 'copy_to_clipboard': 'Copier dans le presse-papier', + 'copy_success': 'Copié !', + 'copy_failure': 'Échec de la copie', + }, + 'ru': { + 'copy': 'Скопировать', + 'copy_to_clipboard': 'Скопировать в буфер', + 'copy_success': 'Скопировано!', + 'copy_failure': 'Не удалось скопировать', + }, + 'zh-CN': { + 'copy': '复制', + 'copy_to_clipboard': '复制到剪贴板', + 'copy_success': '复制成功!', + 'copy_failure': '复制失败', + }, + 'it' : { + 'copy': 'Copiare', + 'copy_to_clipboard': 'Copiato negli appunti', + 'copy_success': 'Copiato!', + 'copy_failure': 'Errore durante la copia', + } +} + +let locale = 'en' +if( document.documentElement.lang !== undefined + && messages[document.documentElement.lang] !== undefined ) { + locale = document.documentElement.lang +} + +let doc_url_root = DOCUMENTATION_OPTIONS.URL_ROOT; +if (doc_url_root == '#') { + doc_url_root = ''; +} + +/** + * SVG files for our copy buttons + */ +let iconCheck = ` + ${messages[locale]['copy_success']} + + +` + +// If the user specified their own SVG use that, otherwise use the default +let iconCopy = ``; +if (!iconCopy) { + iconCopy = ` + ${messages[locale]['copy_to_clipboard']} + + + +` +} + +/** + * Set up copy/paste for code blocks + */ + +const runWhenDOMLoaded = cb => { + if (document.readyState != 'loading') { + cb() + } else if (document.addEventListener) { + document.addEventListener('DOMContentLoaded', cb) + } else { + document.attachEvent('onreadystatechange', function() { + if (document.readyState == 'complete') cb() + }) + } +} + +const codeCellId = index => `codecell${index}` + +// Clears selected text since ClipboardJS will select the text when copying +const clearSelection = () => { + if (window.getSelection) { + window.getSelection().removeAllRanges() + } else if (document.selection) { + document.selection.empty() + } +} + +// Changes tooltip text for a moment, then changes it back +// We want the timeout of our `success` class to be a bit shorter than the +// tooltip and icon change, so that we can hide the icon before changing back. +var timeoutIcon = 2000; +var timeoutSuccessClass = 1500; + +const temporarilyChangeTooltip = (el, oldText, newText) => { + el.setAttribute('data-tooltip', newText) + el.classList.add('success') + // Remove success a little bit sooner than we change the tooltip + // So that we can use CSS to hide the copybutton first + setTimeout(() => el.classList.remove('success'), timeoutSuccessClass) + setTimeout(() => el.setAttribute('data-tooltip', oldText), timeoutIcon) +} + +// Changes the copy button icon for two seconds, then changes it back +const temporarilyChangeIcon = (el) => { + el.innerHTML = iconCheck; + setTimeout(() => {el.innerHTML = iconCopy}, timeoutIcon) +} + +const addCopyButtonToCodeCells = () => { + // If ClipboardJS hasn't loaded, wait a bit and try again. This + // happens because we load ClipboardJS asynchronously. + if (window.ClipboardJS === undefined) { + setTimeout(addCopyButtonToCodeCells, 250) + return + } + + // Add copybuttons to all of our code cells + const COPYBUTTON_SELECTOR = 'div.highlight pre'; + const codeCells = document.querySelectorAll(COPYBUTTON_SELECTOR) + codeCells.forEach((codeCell, index) => { + const id = codeCellId(index) + codeCell.setAttribute('id', id) + + const clipboardButton = id => + `` + codeCell.insertAdjacentHTML('afterend', clipboardButton(id)) + }) + +function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} + +/** + * Removes excluded text from a Node. + * + * @param {Node} target Node to filter. + * @param {string} exclude CSS selector of nodes to exclude. + * @returns {DOMString} Text from `target` with text removed. + */ +function filterText(target, exclude) { + const clone = target.cloneNode(true); // clone as to not modify the live DOM + if (exclude) { + // remove excluded nodes + clone.querySelectorAll(exclude).forEach(node => node.remove()); + } + return clone.innerText; +} + +// Callback when a copy button is clicked. Will be passed the node that was clicked +// should then grab the text and replace pieces of text that shouldn't be used in output +function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onlyCopyPromptLines = true, removePrompts = true, copyEmptyLines = true, lineContinuationChar = "", hereDocDelim = "") { + var regexp; + var match; + + // Do we check for line continuation characters and "HERE-documents"? + var useLineCont = !!lineContinuationChar + var useHereDoc = !!hereDocDelim + + // create regexp to capture prompt and remaining line + if (isRegexp) { + regexp = new RegExp('^(' + copybuttonPromptText + ')(.*)') + } else { + regexp = new RegExp('^(' + escapeRegExp(copybuttonPromptText) + ')(.*)') + } + + const outputLines = []; + var promptFound = false; + var gotLineCont = false; + var gotHereDoc = false; + const lineGotPrompt = []; + for (const line of textContent.split('\n')) { + match = line.match(regexp) + if (match || gotLineCont || gotHereDoc) { + promptFound = regexp.test(line) + lineGotPrompt.push(promptFound) + if (removePrompts && promptFound) { + outputLines.push(match[2]) + } else { + outputLines.push(line) + } + gotLineCont = line.endsWith(lineContinuationChar) & useLineCont + if (line.includes(hereDocDelim) & useHereDoc) + gotHereDoc = !gotHereDoc + } else if (!onlyCopyPromptLines) { + outputLines.push(line) + } else if (copyEmptyLines && line.trim() === '') { + outputLines.push(line) + } + } + + // If no lines with the prompt were found then just use original lines + if (lineGotPrompt.some(v => v === true)) { + textContent = outputLines.join('\n'); + } + + // Remove a trailing newline to avoid auto-running when pasting + if (textContent.endsWith("\n")) { + textContent = textContent.slice(0, -1) + } + return textContent +} + + +var copyTargetText = (trigger) => { + var target = document.querySelector(trigger.attributes['data-clipboard-target'].value); + + // get filtered text + let exclude = '.linenos'; + + let text = filterText(target, exclude); + return formatCopyText(text, '>>> |\\.\\.\\. |\\$ ', true, true, true, true, '', '') +} + + // Initialize with a callback so we can modify the text before copy + const clipboard = new ClipboardJS('.copybtn', {text: copyTargetText}) + + // Update UI with error/success messages + clipboard.on('success', event => { + clearSelection() + temporarilyChangeTooltip(event.trigger, messages[locale]['copy'], messages[locale]['copy_success']) + temporarilyChangeIcon(event.trigger) + }) + + clipboard.on('error', event => { + temporarilyChangeTooltip(event.trigger, messages[locale]['copy'], messages[locale]['copy_failure']) + }) +} + +runWhenDOMLoaded(addCopyButtonToCodeCells) \ No newline at end of file diff --git a/_static/copybutton_funcs.js b/_static/copybutton_funcs.js new file mode 100644 index 00000000..dbe1aaad --- /dev/null +++ b/_static/copybutton_funcs.js @@ -0,0 +1,73 @@ +function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} + +/** + * Removes excluded text from a Node. + * + * @param {Node} target Node to filter. + * @param {string} exclude CSS selector of nodes to exclude. + * @returns {DOMString} Text from `target` with text removed. + */ +export function filterText(target, exclude) { + const clone = target.cloneNode(true); // clone as to not modify the live DOM + if (exclude) { + // remove excluded nodes + clone.querySelectorAll(exclude).forEach(node => node.remove()); + } + return clone.innerText; +} + +// Callback when a copy button is clicked. Will be passed the node that was clicked +// should then grab the text and replace pieces of text that shouldn't be used in output +export function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onlyCopyPromptLines = true, removePrompts = true, copyEmptyLines = true, lineContinuationChar = "", hereDocDelim = "") { + var regexp; + var match; + + // Do we check for line continuation characters and "HERE-documents"? + var useLineCont = !!lineContinuationChar + var useHereDoc = !!hereDocDelim + + // create regexp to capture prompt and remaining line + if (isRegexp) { + regexp = new RegExp('^(' + copybuttonPromptText + ')(.*)') + } else { + regexp = new RegExp('^(' + escapeRegExp(copybuttonPromptText) + ')(.*)') + } + + const outputLines = []; + var promptFound = false; + var gotLineCont = false; + var gotHereDoc = false; + const lineGotPrompt = []; + for (const line of textContent.split('\n')) { + match = line.match(regexp) + if (match || gotLineCont || gotHereDoc) { + promptFound = regexp.test(line) + lineGotPrompt.push(promptFound) + if (removePrompts && promptFound) { + outputLines.push(match[2]) + } else { + outputLines.push(line) + } + gotLineCont = line.endsWith(lineContinuationChar) & useLineCont + if (line.includes(hereDocDelim) & useHereDoc) + gotHereDoc = !gotHereDoc + } else if (!onlyCopyPromptLines) { + outputLines.push(line) + } else if (copyEmptyLines && line.trim() === '') { + outputLines.push(line) + } + } + + // If no lines with the prompt were found then just use original lines + if (lineGotPrompt.some(v => v === true)) { + textContent = outputLines.join('\n'); + } + + // Remove a trailing newline to avoid auto-running when pasting + if (textContent.endsWith("\n")) { + textContent = textContent.slice(0, -1) + } + return textContent +} diff --git a/_static/custom.css b/_static/custom.css new file mode 100644 index 00000000..39672c9d --- /dev/null +++ b/_static/custom.css @@ -0,0 +1,56 @@ +/* hides the TOC caption from the main page since we already have an H1 heading */ +.section .caption-text { + display: none; +} + +/* note title text in white */ +.admonition p.admonition-title { + color: white; + font-weight: 700; +} + +/* .admonition.note { + background: #e5f4d8; +} */ + +/* left align tables */ +table.docutils { + margin-left: 1em; + font-family: monospace; +} + +/* inline code snippets #E74C3C, var(--color-link), #4e9a06 */ +code.literal { + color: #4e9a06; + font-family: monospace; + font-size: var(--font-size-normal); +} + +.sidebar-brand-text { + text-align: center; + margin: 0 0; +} + +article div.gallery { + overflow-x: unset; +} + +article table.gallery { + margin: 0 auto; + text-align: center; +} + +article table.gallery td { + padding: 0; +} + +article table.gallery img { + margin: 8px; + max-width: 160px; + vertical-align: middle; +} + + + +/* normalize size across Firefox / Chrome */ +html { font-size: 100%; } diff --git a/_static/debug.css b/_static/debug.css new file mode 100644 index 00000000..74d4aec3 --- /dev/null +++ b/_static/debug.css @@ -0,0 +1,69 @@ +/* + This CSS file should be overridden by the theme authors. It's + meant for debugging and developing the skeleton that this theme provides. +*/ +body { + font-family: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, + "Apple Color Emoji", "Segoe UI Emoji"; + background: lavender; +} +.sb-announcement { + background: rgb(131, 131, 131); +} +.sb-announcement__inner { + background: black; + color: white; +} +.sb-header { + background: lightskyblue; +} +.sb-header__inner { + background: royalblue; + color: white; +} +.sb-header-secondary { + background: lightcyan; +} +.sb-header-secondary__inner { + background: cornflowerblue; + color: white; +} +.sb-sidebar-primary { + background: lightgreen; +} +.sb-main { + background: blanchedalmond; +} +.sb-main__inner { + background: antiquewhite; +} +.sb-header-article { + background: lightsteelblue; +} +.sb-article-container { + background: snow; +} +.sb-article-main { + background: white; +} +.sb-footer-article { + background: lightpink; +} +.sb-sidebar-secondary { + background: lightgoldenrodyellow; +} +.sb-footer-content { + background: plum; +} +.sb-footer-content__inner { + background: palevioletred; +} +.sb-footer { + background: pink; +} +.sb-footer__inner { + background: salmon; +} +.sb-article { + background: white; +} diff --git a/_static/doctools.js b/_static/doctools.js new file mode 100644 index 00000000..4d67807d --- /dev/null +++ b/_static/doctools.js @@ -0,0 +1,156 @@ +/* + * doctools.js + * ~~~~~~~~~~~ + * + * Base JavaScript utilities for all Sphinx HTML documentation. + * + * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ +"use strict"; + +const BLACKLISTED_KEY_CONTROL_ELEMENTS = new Set([ + "TEXTAREA", + "INPUT", + "SELECT", + "BUTTON", +]); + +const _ready = (callback) => { + if (document.readyState !== "loading") { + callback(); + } else { + document.addEventListener("DOMContentLoaded", callback); + } +}; + +/** + * Small JavaScript module for the documentation. + */ +const Documentation = { + init: () => { + Documentation.initDomainIndexTable(); + Documentation.initOnKeyListeners(); + }, + + /** + * i18n support + */ + TRANSLATIONS: {}, + PLURAL_EXPR: (n) => (n === 1 ? 0 : 1), + LOCALE: "unknown", + + // gettext and ngettext don't access this so that the functions + // can safely bound to a different name (_ = Documentation.gettext) + gettext: (string) => { + const translated = Documentation.TRANSLATIONS[string]; + switch (typeof translated) { + case "undefined": + return string; // no translation + case "string": + return translated; // translation exists + default: + return translated[0]; // (singular, plural) translation tuple exists + } + }, + + ngettext: (singular, plural, n) => { + const translated = Documentation.TRANSLATIONS[singular]; + if (typeof translated !== "undefined") + return translated[Documentation.PLURAL_EXPR(n)]; + return n === 1 ? singular : plural; + }, + + addTranslations: (catalog) => { + Object.assign(Documentation.TRANSLATIONS, catalog.messages); + Documentation.PLURAL_EXPR = new Function( + "n", + `return (${catalog.plural_expr})` + ); + Documentation.LOCALE = catalog.locale; + }, + + /** + * helper function to focus on search bar + */ + focusSearchBar: () => { + document.querySelectorAll("input[name=q]")[0]?.focus(); + }, + + /** + * Initialise the domain index toggle buttons + */ + initDomainIndexTable: () => { + const toggler = (el) => { + const idNumber = el.id.substr(7); + const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`); + if (el.src.substr(-9) === "minus.png") { + el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`; + toggledRows.forEach((el) => (el.style.display = "none")); + } else { + el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`; + toggledRows.forEach((el) => (el.style.display = "")); + } + }; + + const togglerElements = document.querySelectorAll("img.toggler"); + togglerElements.forEach((el) => + el.addEventListener("click", (event) => toggler(event.currentTarget)) + ); + togglerElements.forEach((el) => (el.style.display = "")); + if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler); + }, + + initOnKeyListeners: () => { + // only install a listener if it is really needed + if ( + !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS && + !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS + ) + return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + // bail with special keys + if (event.altKey || event.ctrlKey || event.metaKey) return; + + if (!event.shiftKey) { + switch (event.key) { + case "ArrowLeft": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const prevLink = document.querySelector('link[rel="prev"]'); + if (prevLink && prevLink.href) { + window.location.href = prevLink.href; + event.preventDefault(); + } + break; + case "ArrowRight": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const nextLink = document.querySelector('link[rel="next"]'); + if (nextLink && nextLink.href) { + window.location.href = nextLink.href; + event.preventDefault(); + } + break; + } + } + + // some keyboard layouts may need Shift to get / + switch (event.key) { + case "/": + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; + Documentation.focusSearchBar(); + event.preventDefault(); + } + }); + }, +}; + +// quick alias for translations +const _ = Documentation.gettext; + +_ready(Documentation.init); diff --git a/_static/documentation_options.js b/_static/documentation_options.js new file mode 100644 index 00000000..b1151d07 --- /dev/null +++ b/_static/documentation_options.js @@ -0,0 +1,13 @@ +const DOCUMENTATION_OPTIONS = { + VERSION: '1.3.3', + LANGUAGE: 'en', + COLLAPSE_INDEX: false, + BUILDER: 'html', + FILE_SUFFIX: '.html', + LINK_SUFFIX: '.html', + HAS_SOURCE: true, + SOURCELINK_SUFFIX: '.txt', + NAVIGATION_WITH_KEYS: false, + SHOW_SEARCH_SUMMARY: true, + ENABLE_SEARCH_SHORTCUTS: true, +}; \ No newline at end of file diff --git a/_static/file.png b/_static/file.png new file mode 100644 index 00000000..a858a410 Binary files /dev/null and b/_static/file.png differ diff --git a/_static/language_data.js b/_static/language_data.js new file mode 100644 index 00000000..367b8ed8 --- /dev/null +++ b/_static/language_data.js @@ -0,0 +1,199 @@ +/* + * language_data.js + * ~~~~~~~~~~~~~~~~ + * + * This script contains the language-specific data used by searchtools.js, + * namely the list of stopwords, stemmer, scorer and splitter. + * + * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +var stopwords = ["a", "and", "are", "as", "at", "be", "but", "by", "for", "if", "in", "into", "is", "it", "near", "no", "not", "of", "on", "or", "such", "that", "the", "their", "then", "there", "these", "they", "this", "to", "was", "will", "with"]; + + +/* Non-minified version is copied as a separate JS file, if available */ + +/** + * Porter Stemmer + */ +var Stemmer = function() { + + var step2list = { + ational: 'ate', + tional: 'tion', + enci: 'ence', + anci: 'ance', + izer: 'ize', + bli: 'ble', + alli: 'al', + entli: 'ent', + eli: 'e', + ousli: 'ous', + ization: 'ize', + ation: 'ate', + ator: 'ate', + alism: 'al', + iveness: 'ive', + fulness: 'ful', + ousness: 'ous', + aliti: 'al', + iviti: 'ive', + biliti: 'ble', + logi: 'log' + }; + + var step3list = { + icate: 'ic', + ative: '', + alize: 'al', + iciti: 'ic', + ical: 'ic', + ful: '', + ness: '' + }; + + var c = "[^aeiou]"; // consonant + var v = "[aeiouy]"; // vowel + var C = c + "[^aeiouy]*"; // consonant sequence + var V = v + "[aeiou]*"; // vowel sequence + + var mgr0 = "^(" + C + ")?" + V + C; // [C]VC... is m>0 + var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1 + var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1 + var s_v = "^(" + C + ")?" + v; // vowel in stem + + this.stemWord = function (w) { + var stem; + var suffix; + var firstch; + var origword = w; + + if (w.length < 3) + return w; + + var re; + var re2; + var re3; + var re4; + + firstch = w.substr(0,1); + if (firstch == "y") + w = firstch.toUpperCase() + w.substr(1); + + // Step 1a + re = /^(.+?)(ss|i)es$/; + re2 = /^(.+?)([^s])s$/; + + if (re.test(w)) + w = w.replace(re,"$1$2"); + else if (re2.test(w)) + w = w.replace(re2,"$1$2"); + + // Step 1b + re = /^(.+?)eed$/; + re2 = /^(.+?)(ed|ing)$/; + if (re.test(w)) { + var fp = re.exec(w); + re = new RegExp(mgr0); + if (re.test(fp[1])) { + re = /.$/; + w = w.replace(re,""); + } + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1]; + re2 = new RegExp(s_v); + if (re2.test(stem)) { + w = stem; + re2 = /(at|bl|iz)$/; + re3 = new RegExp("([^aeiouylsz])\\1$"); + re4 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re2.test(w)) + w = w + "e"; + else if (re3.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + else if (re4.test(w)) + w = w + "e"; + } + } + + // Step 1c + re = /^(.+?)y$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(s_v); + if (re.test(stem)) + w = stem + "i"; + } + + // Step 2 + re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step2list[suffix]; + } + + // Step 3 + re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step3list[suffix]; + } + + // Step 4 + re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; + re2 = /^(.+?)(s|t)(ion)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + if (re.test(stem)) + w = stem; + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1] + fp[2]; + re2 = new RegExp(mgr1); + if (re2.test(stem)) + w = stem; + } + + // Step 5 + re = /^(.+?)e$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + re2 = new RegExp(meq1); + re3 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) + w = stem; + } + re = /ll$/; + re2 = new RegExp(mgr1); + if (re.test(w) && re2.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + + // and turn initial Y back to y + if (firstch == "y") + w = firstch.toLowerCase() + w.substr(1); + return w; + } +} + diff --git a/_static/logo-dark-mode.png b/_static/logo-dark-mode.png new file mode 100644 index 00000000..ce001656 Binary files /dev/null and b/_static/logo-dark-mode.png differ diff --git a/_static/logo-light-mode.png b/_static/logo-light-mode.png new file mode 100644 index 00000000..3f2fd316 Binary files /dev/null and b/_static/logo-light-mode.png differ diff --git a/_static/minus.png b/_static/minus.png new file mode 100644 index 00000000..d96755fd Binary files /dev/null and b/_static/minus.png differ diff --git a/_static/plus.png b/_static/plus.png new file mode 100644 index 00000000..7107cec9 Binary files /dev/null and b/_static/plus.png differ diff --git a/_static/pygments.css b/_static/pygments.css new file mode 100644 index 00000000..02b4b128 --- /dev/null +++ b/_static/pygments.css @@ -0,0 +1,258 @@ +.highlight pre { line-height: 125%; } +.highlight td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +.highlight span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +.highlight td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +.highlight span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +.highlight .hll { background-color: #ffffcc } +.highlight { background: #f8f8f8; } +.highlight .c { color: #8f5902; font-style: italic } /* Comment */ +.highlight .err { color: #a40000; border: 1px solid #ef2929 } /* Error */ +.highlight .g { color: #000000 } /* Generic */ +.highlight .k { color: #204a87; font-weight: bold } /* Keyword */ +.highlight .l { color: #000000 } /* Literal */ +.highlight .n { color: #000000 } /* Name */ +.highlight .o { color: #ce5c00; font-weight: bold } /* Operator */ +.highlight .x { color: #000000 } /* Other */ +.highlight .p { color: #000000; font-weight: bold } /* Punctuation */ +.highlight .ch { color: #8f5902; font-style: italic } /* Comment.Hashbang */ +.highlight .cm { color: #8f5902; font-style: italic } /* Comment.Multiline */ +.highlight .cp { color: #8f5902; font-style: italic } /* Comment.Preproc */ +.highlight .cpf { color: #8f5902; font-style: italic } /* Comment.PreprocFile */ +.highlight .c1 { color: #8f5902; font-style: italic } /* Comment.Single */ +.highlight .cs { color: #8f5902; font-style: italic } /* Comment.Special */ +.highlight .gd { color: #a40000 } /* Generic.Deleted */ +.highlight .ge { color: #000000; font-style: italic } /* Generic.Emph */ +.highlight .ges { color: #000000; font-weight: bold; font-style: italic } /* Generic.EmphStrong */ +.highlight .gr { color: #ef2929 } /* Generic.Error */ +.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ +.highlight .gi { color: #00A000 } /* Generic.Inserted */ +.highlight .go { color: #000000; font-style: italic } /* Generic.Output */ +.highlight .gp { color: #8f5902 } /* Generic.Prompt */ +.highlight .gs { color: #000000; font-weight: bold } /* Generic.Strong */ +.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +.highlight .gt { color: #a40000; font-weight: bold } /* Generic.Traceback */ +.highlight .kc { color: #204a87; font-weight: bold } /* Keyword.Constant */ +.highlight .kd { color: #204a87; font-weight: bold } /* Keyword.Declaration */ +.highlight .kn { color: #204a87; font-weight: bold } /* Keyword.Namespace */ +.highlight .kp { color: #204a87; font-weight: bold } /* Keyword.Pseudo */ +.highlight .kr { color: #204a87; font-weight: bold } /* Keyword.Reserved */ +.highlight .kt { color: #204a87; font-weight: bold } /* Keyword.Type */ +.highlight .ld { color: #000000 } /* Literal.Date */ +.highlight .m { color: #0000cf; font-weight: bold } /* Literal.Number */ +.highlight .s { color: #4e9a06 } /* Literal.String */ +.highlight .na { color: #c4a000 } /* Name.Attribute */ +.highlight .nb { color: #204a87 } /* Name.Builtin */ +.highlight .nc { color: #000000 } /* Name.Class */ +.highlight .no { color: #000000 } /* Name.Constant */ +.highlight .nd { color: #5c35cc; font-weight: bold } /* Name.Decorator */ +.highlight .ni { color: #ce5c00 } /* Name.Entity */ +.highlight .ne { color: #cc0000; font-weight: bold } /* Name.Exception */ +.highlight .nf { color: #000000 } /* Name.Function */ +.highlight .nl { color: #f57900 } /* Name.Label */ +.highlight .nn { color: #000000 } /* Name.Namespace */ +.highlight .nx { color: #000000 } /* Name.Other */ +.highlight .py { color: #000000 } /* Name.Property */ +.highlight .nt { color: #204a87; font-weight: bold } /* Name.Tag */ +.highlight .nv { color: #000000 } /* Name.Variable */ +.highlight .ow { color: #204a87; font-weight: bold } /* Operator.Word */ +.highlight .pm { color: #000000; font-weight: bold } /* Punctuation.Marker */ +.highlight .w { color: #f8f8f8 } /* Text.Whitespace */ +.highlight .mb { color: #0000cf; font-weight: bold } /* Literal.Number.Bin */ +.highlight .mf { color: #0000cf; font-weight: bold } /* Literal.Number.Float */ +.highlight .mh { color: #0000cf; font-weight: bold } /* Literal.Number.Hex */ +.highlight .mi { color: #0000cf; font-weight: bold } /* Literal.Number.Integer */ +.highlight .mo { color: #0000cf; font-weight: bold } /* Literal.Number.Oct */ +.highlight .sa { color: #4e9a06 } /* Literal.String.Affix */ +.highlight .sb { color: #4e9a06 } /* Literal.String.Backtick */ +.highlight .sc { color: #4e9a06 } /* Literal.String.Char */ +.highlight .dl { color: #4e9a06 } /* Literal.String.Delimiter */ +.highlight .sd { color: #8f5902; font-style: italic } /* Literal.String.Doc */ +.highlight .s2 { color: #4e9a06 } /* Literal.String.Double */ +.highlight .se { color: #4e9a06 } /* Literal.String.Escape */ +.highlight .sh { color: #4e9a06 } /* Literal.String.Heredoc */ +.highlight .si { color: #4e9a06 } /* Literal.String.Interpol */ +.highlight .sx { color: #4e9a06 } /* Literal.String.Other */ +.highlight .sr { color: #4e9a06 } /* Literal.String.Regex */ +.highlight .s1 { color: #4e9a06 } /* Literal.String.Single */ +.highlight .ss { color: #4e9a06 } /* Literal.String.Symbol */ +.highlight .bp { color: #3465a4 } /* Name.Builtin.Pseudo */ +.highlight .fm { color: #000000 } /* Name.Function.Magic */ +.highlight .vc { color: #000000 } /* Name.Variable.Class */ +.highlight .vg { color: #000000 } /* Name.Variable.Global */ +.highlight .vi { color: #000000 } /* Name.Variable.Instance */ +.highlight .vm { color: #000000 } /* Name.Variable.Magic */ +.highlight .il { color: #0000cf; font-weight: bold } /* Literal.Number.Integer.Long */ +@media not print { +body[data-theme="dark"] .highlight pre { line-height: 125%; } +body[data-theme="dark"] .highlight td.linenos .normal { color: #aaaaaa; background-color: transparent; padding-left: 5px; padding-right: 5px; } +body[data-theme="dark"] .highlight span.linenos { color: #aaaaaa; background-color: transparent; padding-left: 5px; padding-right: 5px; } +body[data-theme="dark"] .highlight td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +body[data-theme="dark"] .highlight span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +body[data-theme="dark"] .highlight .hll { background-color: #404040 } +body[data-theme="dark"] .highlight { background: #202020; color: #d0d0d0 } +body[data-theme="dark"] .highlight .c { color: #ababab; font-style: italic } /* Comment */ +body[data-theme="dark"] .highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */ +body[data-theme="dark"] .highlight .esc { color: #d0d0d0 } /* Escape */ +body[data-theme="dark"] .highlight .g { color: #d0d0d0 } /* Generic */ +body[data-theme="dark"] .highlight .k { color: #6ebf26; font-weight: bold } /* Keyword */ +body[data-theme="dark"] .highlight .l { color: #d0d0d0 } /* Literal */ +body[data-theme="dark"] .highlight .n { color: #d0d0d0 } /* Name */ +body[data-theme="dark"] .highlight .o { color: #d0d0d0 } /* Operator */ +body[data-theme="dark"] .highlight .x { color: #d0d0d0 } /* Other */ +body[data-theme="dark"] .highlight .p { color: #d0d0d0 } /* Punctuation */ +body[data-theme="dark"] .highlight .ch { color: #ababab; font-style: italic } /* Comment.Hashbang */ +body[data-theme="dark"] .highlight .cm { color: #ababab; font-style: italic } /* Comment.Multiline */ +body[data-theme="dark"] .highlight .cp { color: #ff3a3a; font-weight: bold } /* Comment.Preproc */ +body[data-theme="dark"] .highlight .cpf { color: #ababab; font-style: italic } /* Comment.PreprocFile */ +body[data-theme="dark"] .highlight .c1 { color: #ababab; font-style: italic } /* Comment.Single */ +body[data-theme="dark"] .highlight .cs { color: #e50808; font-weight: bold; background-color: #520000 } /* Comment.Special */ +body[data-theme="dark"] .highlight .gd { color: #ff3a3a } /* Generic.Deleted */ +body[data-theme="dark"] .highlight .ge { color: #d0d0d0; font-style: italic } /* Generic.Emph */ +body[data-theme="dark"] .highlight .ges { color: #d0d0d0; font-weight: bold; font-style: italic } /* Generic.EmphStrong */ +body[data-theme="dark"] .highlight .gr { color: #ff3a3a } /* Generic.Error */ +body[data-theme="dark"] .highlight .gh { color: #ffffff; font-weight: bold } /* Generic.Heading */ +body[data-theme="dark"] .highlight .gi { color: #589819 } /* Generic.Inserted */ +body[data-theme="dark"] .highlight .go { color: #cccccc } /* Generic.Output */ +body[data-theme="dark"] .highlight .gp { color: #aaaaaa } /* Generic.Prompt */ +body[data-theme="dark"] .highlight .gs { color: #d0d0d0; font-weight: bold } /* Generic.Strong */ +body[data-theme="dark"] .highlight .gu { color: #ffffff; text-decoration: underline } /* Generic.Subheading */ +body[data-theme="dark"] .highlight .gt { color: #ff3a3a } /* Generic.Traceback */ +body[data-theme="dark"] .highlight .kc { color: #6ebf26; font-weight: bold } /* Keyword.Constant */ +body[data-theme="dark"] .highlight .kd { color: #6ebf26; font-weight: bold } /* Keyword.Declaration */ +body[data-theme="dark"] .highlight .kn { color: #6ebf26; font-weight: bold } /* Keyword.Namespace */ +body[data-theme="dark"] .highlight .kp { color: #6ebf26 } /* Keyword.Pseudo */ +body[data-theme="dark"] .highlight .kr { color: #6ebf26; font-weight: bold } /* Keyword.Reserved */ +body[data-theme="dark"] .highlight .kt { color: #6ebf26; font-weight: bold } /* Keyword.Type */ +body[data-theme="dark"] .highlight .ld { color: #d0d0d0 } /* Literal.Date */ +body[data-theme="dark"] .highlight .m { color: #51b2fd } /* Literal.Number */ +body[data-theme="dark"] .highlight .s { color: #ed9d13 } /* Literal.String */ +body[data-theme="dark"] .highlight .na { color: #bbbbbb } /* Name.Attribute */ +body[data-theme="dark"] .highlight .nb { color: #2fbccd } /* Name.Builtin */ +body[data-theme="dark"] .highlight .nc { color: #71adff; text-decoration: underline } /* Name.Class */ +body[data-theme="dark"] .highlight .no { color: #40ffff } /* Name.Constant */ +body[data-theme="dark"] .highlight .nd { color: #ffa500 } /* Name.Decorator */ +body[data-theme="dark"] .highlight .ni { color: #d0d0d0 } /* Name.Entity */ +body[data-theme="dark"] .highlight .ne { color: #bbbbbb } /* Name.Exception */ +body[data-theme="dark"] .highlight .nf { color: #71adff } /* Name.Function */ +body[data-theme="dark"] .highlight .nl { color: #d0d0d0 } /* Name.Label */ +body[data-theme="dark"] .highlight .nn { color: #71adff; text-decoration: underline } /* Name.Namespace */ +body[data-theme="dark"] .highlight .nx { color: #d0d0d0 } /* Name.Other */ +body[data-theme="dark"] .highlight .py { color: #d0d0d0 } /* Name.Property */ +body[data-theme="dark"] .highlight .nt { color: #6ebf26; font-weight: bold } /* Name.Tag */ +body[data-theme="dark"] .highlight .nv { color: #40ffff } /* Name.Variable */ +body[data-theme="dark"] .highlight .ow { color: #6ebf26; font-weight: bold } /* Operator.Word */ +body[data-theme="dark"] .highlight .pm { color: #d0d0d0 } /* Punctuation.Marker */ +body[data-theme="dark"] .highlight .w { color: #666666 } /* Text.Whitespace */ +body[data-theme="dark"] .highlight .mb { color: #51b2fd } /* Literal.Number.Bin */ +body[data-theme="dark"] .highlight .mf { color: #51b2fd } /* Literal.Number.Float */ +body[data-theme="dark"] .highlight .mh { color: #51b2fd } /* Literal.Number.Hex */ +body[data-theme="dark"] .highlight .mi { color: #51b2fd } /* Literal.Number.Integer */ +body[data-theme="dark"] .highlight .mo { color: #51b2fd } /* Literal.Number.Oct */ +body[data-theme="dark"] .highlight .sa { color: #ed9d13 } /* Literal.String.Affix */ +body[data-theme="dark"] .highlight .sb { color: #ed9d13 } /* Literal.String.Backtick */ +body[data-theme="dark"] .highlight .sc { color: #ed9d13 } /* Literal.String.Char */ +body[data-theme="dark"] .highlight .dl { color: #ed9d13 } /* Literal.String.Delimiter */ +body[data-theme="dark"] .highlight .sd { color: #ed9d13 } /* Literal.String.Doc */ +body[data-theme="dark"] .highlight .s2 { color: #ed9d13 } /* Literal.String.Double */ +body[data-theme="dark"] .highlight .se { color: #ed9d13 } /* Literal.String.Escape */ +body[data-theme="dark"] .highlight .sh { color: #ed9d13 } /* Literal.String.Heredoc */ +body[data-theme="dark"] .highlight .si { color: #ed9d13 } /* Literal.String.Interpol */ +body[data-theme="dark"] .highlight .sx { color: #ffa500 } /* Literal.String.Other */ +body[data-theme="dark"] .highlight .sr { color: #ed9d13 } /* Literal.String.Regex */ +body[data-theme="dark"] .highlight .s1 { color: #ed9d13 } /* Literal.String.Single */ +body[data-theme="dark"] .highlight .ss { color: #ed9d13 } /* Literal.String.Symbol */ +body[data-theme="dark"] .highlight .bp { color: #2fbccd } /* Name.Builtin.Pseudo */ +body[data-theme="dark"] .highlight .fm { color: #71adff } /* Name.Function.Magic */ +body[data-theme="dark"] .highlight .vc { color: #40ffff } /* Name.Variable.Class */ +body[data-theme="dark"] .highlight .vg { color: #40ffff } /* Name.Variable.Global */ +body[data-theme="dark"] .highlight .vi { color: #40ffff } /* Name.Variable.Instance */ +body[data-theme="dark"] .highlight .vm { color: #40ffff } /* Name.Variable.Magic */ +body[data-theme="dark"] .highlight .il { color: #51b2fd } /* Literal.Number.Integer.Long */ +@media (prefers-color-scheme: dark) { +body:not([data-theme="light"]) .highlight pre { line-height: 125%; } +body:not([data-theme="light"]) .highlight td.linenos .normal { color: #aaaaaa; background-color: transparent; padding-left: 5px; padding-right: 5px; } +body:not([data-theme="light"]) .highlight span.linenos { color: #aaaaaa; background-color: transparent; padding-left: 5px; padding-right: 5px; } +body:not([data-theme="light"]) .highlight td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +body:not([data-theme="light"]) .highlight span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +body:not([data-theme="light"]) .highlight .hll { background-color: #404040 } +body:not([data-theme="light"]) .highlight { background: #202020; color: #d0d0d0 } +body:not([data-theme="light"]) .highlight .c { color: #ababab; font-style: italic } /* Comment */ +body:not([data-theme="light"]) .highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */ +body:not([data-theme="light"]) .highlight .esc { color: #d0d0d0 } /* Escape */ +body:not([data-theme="light"]) .highlight .g { color: #d0d0d0 } /* Generic */ +body:not([data-theme="light"]) .highlight .k { color: #6ebf26; font-weight: bold } /* Keyword */ +body:not([data-theme="light"]) .highlight .l { color: #d0d0d0 } /* Literal */ +body:not([data-theme="light"]) .highlight .n { color: #d0d0d0 } /* Name */ +body:not([data-theme="light"]) .highlight .o { color: #d0d0d0 } /* Operator */ +body:not([data-theme="light"]) .highlight .x { color: #d0d0d0 } /* Other */ +body:not([data-theme="light"]) .highlight .p { color: #d0d0d0 } /* Punctuation */ +body:not([data-theme="light"]) .highlight .ch { color: #ababab; font-style: italic } /* Comment.Hashbang */ +body:not([data-theme="light"]) .highlight .cm { color: #ababab; font-style: italic } /* Comment.Multiline */ +body:not([data-theme="light"]) .highlight .cp { color: #ff3a3a; font-weight: bold } /* Comment.Preproc */ +body:not([data-theme="light"]) .highlight .cpf { color: #ababab; font-style: italic } /* Comment.PreprocFile */ +body:not([data-theme="light"]) .highlight .c1 { color: #ababab; font-style: italic } /* Comment.Single */ +body:not([data-theme="light"]) .highlight .cs { color: #e50808; font-weight: bold; background-color: #520000 } /* Comment.Special */ +body:not([data-theme="light"]) .highlight .gd { color: #ff3a3a } /* Generic.Deleted */ +body:not([data-theme="light"]) .highlight .ge { color: #d0d0d0; font-style: italic } /* Generic.Emph */ +body:not([data-theme="light"]) .highlight .ges { color: #d0d0d0; font-weight: bold; font-style: italic } /* Generic.EmphStrong */ +body:not([data-theme="light"]) .highlight .gr { color: #ff3a3a } /* Generic.Error */ +body:not([data-theme="light"]) .highlight .gh { color: #ffffff; font-weight: bold } /* Generic.Heading */ +body:not([data-theme="light"]) .highlight .gi { color: #589819 } /* Generic.Inserted */ +body:not([data-theme="light"]) .highlight .go { color: #cccccc } /* Generic.Output */ +body:not([data-theme="light"]) .highlight .gp { color: #aaaaaa } /* Generic.Prompt */ +body:not([data-theme="light"]) .highlight .gs { color: #d0d0d0; font-weight: bold } /* Generic.Strong */ +body:not([data-theme="light"]) .highlight .gu { color: #ffffff; text-decoration: underline } /* Generic.Subheading */ +body:not([data-theme="light"]) .highlight .gt { color: #ff3a3a } /* Generic.Traceback */ +body:not([data-theme="light"]) .highlight .kc { color: #6ebf26; font-weight: bold } /* Keyword.Constant */ +body:not([data-theme="light"]) .highlight .kd { color: #6ebf26; font-weight: bold } /* Keyword.Declaration */ +body:not([data-theme="light"]) .highlight .kn { color: #6ebf26; font-weight: bold } /* Keyword.Namespace */ +body:not([data-theme="light"]) .highlight .kp { color: #6ebf26 } /* Keyword.Pseudo */ +body:not([data-theme="light"]) .highlight .kr { color: #6ebf26; font-weight: bold } /* Keyword.Reserved */ +body:not([data-theme="light"]) .highlight .kt { color: #6ebf26; font-weight: bold } /* Keyword.Type */ +body:not([data-theme="light"]) .highlight .ld { color: #d0d0d0 } /* Literal.Date */ +body:not([data-theme="light"]) .highlight .m { color: #51b2fd } /* Literal.Number */ +body:not([data-theme="light"]) .highlight .s { color: #ed9d13 } /* Literal.String */ +body:not([data-theme="light"]) .highlight .na { color: #bbbbbb } /* Name.Attribute */ +body:not([data-theme="light"]) .highlight .nb { color: #2fbccd } /* Name.Builtin */ +body:not([data-theme="light"]) .highlight .nc { color: #71adff; text-decoration: underline } /* Name.Class */ +body:not([data-theme="light"]) .highlight .no { color: #40ffff } /* Name.Constant */ +body:not([data-theme="light"]) .highlight .nd { color: #ffa500 } /* Name.Decorator */ +body:not([data-theme="light"]) .highlight .ni { color: #d0d0d0 } /* Name.Entity */ +body:not([data-theme="light"]) .highlight .ne { color: #bbbbbb } /* Name.Exception */ +body:not([data-theme="light"]) .highlight .nf { color: #71adff } /* Name.Function */ +body:not([data-theme="light"]) .highlight .nl { color: #d0d0d0 } /* Name.Label */ +body:not([data-theme="light"]) .highlight .nn { color: #71adff; text-decoration: underline } /* Name.Namespace */ +body:not([data-theme="light"]) .highlight .nx { color: #d0d0d0 } /* Name.Other */ +body:not([data-theme="light"]) .highlight .py { color: #d0d0d0 } /* Name.Property */ +body:not([data-theme="light"]) .highlight .nt { color: #6ebf26; font-weight: bold } /* Name.Tag */ +body:not([data-theme="light"]) .highlight .nv { color: #40ffff } /* Name.Variable */ +body:not([data-theme="light"]) .highlight .ow { color: #6ebf26; font-weight: bold } /* Operator.Word */ +body:not([data-theme="light"]) .highlight .pm { color: #d0d0d0 } /* Punctuation.Marker */ +body:not([data-theme="light"]) .highlight .w { color: #666666 } /* Text.Whitespace */ +body:not([data-theme="light"]) .highlight .mb { color: #51b2fd } /* Literal.Number.Bin */ +body:not([data-theme="light"]) .highlight .mf { color: #51b2fd } /* Literal.Number.Float */ +body:not([data-theme="light"]) .highlight .mh { color: #51b2fd } /* Literal.Number.Hex */ +body:not([data-theme="light"]) .highlight .mi { color: #51b2fd } /* Literal.Number.Integer */ +body:not([data-theme="light"]) .highlight .mo { color: #51b2fd } /* Literal.Number.Oct */ +body:not([data-theme="light"]) .highlight .sa { color: #ed9d13 } /* Literal.String.Affix */ +body:not([data-theme="light"]) .highlight .sb { color: #ed9d13 } /* Literal.String.Backtick */ +body:not([data-theme="light"]) .highlight .sc { color: #ed9d13 } /* Literal.String.Char */ +body:not([data-theme="light"]) .highlight .dl { color: #ed9d13 } /* Literal.String.Delimiter */ +body:not([data-theme="light"]) .highlight .sd { color: #ed9d13 } /* Literal.String.Doc */ +body:not([data-theme="light"]) .highlight .s2 { color: #ed9d13 } /* Literal.String.Double */ +body:not([data-theme="light"]) .highlight .se { color: #ed9d13 } /* Literal.String.Escape */ +body:not([data-theme="light"]) .highlight .sh { color: #ed9d13 } /* Literal.String.Heredoc */ +body:not([data-theme="light"]) .highlight .si { color: #ed9d13 } /* Literal.String.Interpol */ +body:not([data-theme="light"]) .highlight .sx { color: #ffa500 } /* Literal.String.Other */ +body:not([data-theme="light"]) .highlight .sr { color: #ed9d13 } /* Literal.String.Regex */ +body:not([data-theme="light"]) .highlight .s1 { color: #ed9d13 } /* Literal.String.Single */ +body:not([data-theme="light"]) .highlight .ss { color: #ed9d13 } /* Literal.String.Symbol */ +body:not([data-theme="light"]) .highlight .bp { color: #2fbccd } /* Name.Builtin.Pseudo */ +body:not([data-theme="light"]) .highlight .fm { color: #71adff } /* Name.Function.Magic */ +body:not([data-theme="light"]) .highlight .vc { color: #40ffff } /* Name.Variable.Class */ +body:not([data-theme="light"]) .highlight .vg { color: #40ffff } /* Name.Variable.Global */ +body:not([data-theme="light"]) .highlight .vi { color: #40ffff } /* Name.Variable.Instance */ +body:not([data-theme="light"]) .highlight .vm { color: #40ffff } /* Name.Variable.Magic */ +body:not([data-theme="light"]) .highlight .il { color: #51b2fd } /* Literal.Number.Integer.Long */ +} +} \ No newline at end of file diff --git a/_static/scripts/furo-extensions.js b/_static/scripts/furo-extensions.js new file mode 100644 index 00000000..e69de29b diff --git a/_static/scripts/furo.js b/_static/scripts/furo.js new file mode 100644 index 00000000..0abb2afa --- /dev/null +++ b/_static/scripts/furo.js @@ -0,0 +1,3 @@ +/*! For license information please see furo.js.LICENSE.txt */ +(()=>{var t={856: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(856),e=n.n(t),o=null,r=null,c=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;(function(t){const e=Math.floor(r.getBoundingClientRect().top);console.log(`headerTop: ${e}`),0==e&&t!=e?r.classList.add("scrolled"):r.classList.remove("scrolled")})(n=t),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+2.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/_static/scripts/furo.js.LICENSE.txt b/_static/scripts/furo.js.LICENSE.txt new file mode 100644 index 00000000..1632189c --- /dev/null +++ b/_static/scripts/furo.js.LICENSE.txt @@ -0,0 +1,7 @@ +/*! + * gumshoejs v5.1.2 (patched by @pradyunsg) + * A simple, framework-agnostic scrollspy script. + * (c) 2019 Chris Ferdinandi + * MIT License + * http://github.com/cferdinandi/gumshoe + */ diff --git a/_static/scripts/furo.js.map b/_static/scripts/furo.js.map new file mode 100644 index 00000000..80ea12b8 --- /dev/null +++ b/_static/scripts/furo.js.map @@ -0,0 +1 @@ +{"version":3,"file":"scripts/furo.js","mappings":";iCAAA,MAQWA,SAWS,IAAX,EAAAC,EACH,EAAAA,EACkB,oBAAXC,OACLA,OACAC,KAbO,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,EAVgB,CAWrC,EAOIK,EAAe,SAAUR,GAC3B,IAAIS,EAAW,EACf,GAAIT,EAAKU,aACP,KAAOV,GACLS,GAAYT,EAAKW,UACjBX,EAAOA,EAAKU,aAGhB,OAAOD,GAAY,EAAIA,EAAW,CACpC,EAMIG,EAAe,SAAUC,GACvBA,GACFA,EAASC,MAAK,SAAUC,EAAOC,GAG7B,OAFcR,EAAaO,EAAME,SACnBT,EAAaQ,EAAMC,UACF,EACxB,CACT,GAEJ,EAwCIC,EAAW,SAAUlB,EAAME,EAAUiB,GACvC,IAAIC,EAASpB,EAAKqB,wBACd1B,EAnCU,SAAUO,GAExB,MAA+B,mBAApBA,EAASP,OACX2B,WAAWpB,EAASP,UAItB2B,WAAWpB,EAASP,OAC7B,CA2Be4B,CAAUrB,GACvB,OAAIiB,EAEAK,SAASJ,EAAOD,OAAQ,KACvB/B,EAAOqC,aAAeC,SAASC,gBAAgBC,cAG7CJ,SAASJ,EAAOS,IAAK,KAAOlC,CACrC,EAMImC,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,aAkC7B,EAmBIU,EAAY,SAAUzB,EAAUX,GAClC,IAAIqC,EAAO1B,EAASA,EAAS2B,OAAS,GACtC,GAbgB,SAAUC,EAAMvC,GAChC,SAAI4B,MAAgBZ,EAASuB,EAAKxB,QAASf,GAAU,GAEvD,CAUMwC,CAAYH,EAAMrC,GAAW,OAAOqC,EACxC,IAAK,IAAII,EAAI9B,EAAS2B,OAAS,EAAGG,GAAK,EAAGA,IACxC,GAAIzB,EAASL,EAAS8B,GAAG1B,QAASf,GAAW,OAAOW,EAAS8B,EAEjE,EAOIC,EAAmB,SAAUC,EAAK3C,GAEpC,GAAKA,EAAST,QAAWoD,EAAIC,WAA7B,CAGA,IAAIC,EAAKF,EAAIC,WAAWE,QAAQ,MAC3BD,IAGLA,EAAGE,UAAUC,OAAOhD,EAASR,aAG7BkD,EAAiBG,EAAI7C,GAV0B,CAWjD,EAOIiD,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,IAjBM,CAmBpB,EAOIoD,EAAiB,SAAUT,EAAK3C,GAElC,GAAKA,EAAST,OAAd,CAGA,IAAIsD,EAAKF,EAAIC,WAAWE,QAAQ,MAC3BD,IAGLA,EAAGE,UAAUM,IAAIrD,EAASR,aAG1B4D,EAAeP,EAAI7C,GAVS,CAW9B,EA6LA,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,GAEb,IAGAL,EAAaC,EACf,EAKAgD,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,IAjBM,CAmBpB,CAqEIuE,CAASD,EAAQtE,GAGjByD,EAAUa,GAfJb,IACFR,EAAWQ,EAASzD,GACpByD,EAAU,KAchB,GAMIe,EAAgB,SAAUvE,GAExByD,GACFxE,EAAOuF,qBAAqBf,GAI9BA,EAAUxE,EAAOwF,sBAAsBf,EAAWgB,OACpD,EAMIC,EAAgB,SAAU3E,GAExByD,GACFxE,EAAOuF,qBAAqBf,GAI9BA,EAAUxE,EAAOwF,uBAAsB,WACrChE,EAAaC,GACbgD,EAAWgB,QACb,GACF,EAkDA,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,IACb,EAOEA,EA3XS,WACX,IAAI+E,EAAS,CAAC,EAOd,OANAlB,MAAMC,UAAUC,QAAQC,KAAKgB,WAAW,SAAUC,GAChD,IAAK,IAAIC,KAAOD,EAAK,CACnB,IAAKA,EAAIE,eAAeD,GAAM,OAC9BH,EAAOG,GAAOD,EAAIC,EACpB,CACF,IACOH,CACT,CAkXeK,CAAOhG,EAAUmE,GAAW,CAAC,GAGxCI,EAAW0B,QAGX1B,EAAWgB,SAGXzF,EAAOoG,iBAAiB,SAAUd,GAAe,GAC7CxE,EAASN,QACXR,EAAOoG,iBAAiB,SAAUV,GAAe,GAS9CjB,CACT,CAOF,CArcW4B,CAAQvG,EAChB,UAFM,SAEN,uBCXDwG,EAA2B,CAAC,EAGhC,SAASC,EAAoBC,GAE5B,IAAIC,EAAeH,EAAyBE,GAC5C,QAAqBE,IAAjBD,EACH,OAAOA,EAAaE,QAGrB,IAAIC,EAASN,EAAyBE,GAAY,CAGjDG,QAAS,CAAC,GAOX,OAHAE,EAAoBL,GAAU1B,KAAK8B,EAAOD,QAASC,EAAQA,EAAOD,QAASJ,GAGpEK,EAAOD,OACf,CCrBAJ,EAAoBO,EAAKF,IACxB,IAAIG,EAASH,GAAUA,EAAOI,WAC7B,IAAOJ,EAAiB,QACxB,IAAM,EAEP,OADAL,EAAoBU,EAAEF,EAAQ,CAAEG,EAAGH,IAC5BA,CAAM,ECLdR,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,IAE1E,ECNDO,EAAoBxG,EAAI,WACvB,GAA0B,iBAAf0H,WAAyB,OAAOA,WAC3C,IACC,OAAOxH,MAAQ,IAAIyH,SAAS,cAAb,EAChB,CAAE,MAAOC,GACR,GAAsB,iBAAX3H,OAAqB,OAAOA,MACxC,CACA,CAPuB,GCAxBuG,EAAoBa,EAAI,CAACrB,EAAK6B,IAAUP,OAAOzC,UAAUqB,eAAenB,KAAKiB,EAAK6B,4CCK9EC,EAAY,KACZC,EAAS,KACTC,EAAgBzF,SAASC,gBAAgByF,UAC7C,MAAMC,EAAmB,GA8EzB,SAASC,IACP,MAAMC,EAAeC,aAAaC,QAAQ,UAAY,OAZxD,IAAkBC,EACH,WADGA,EAaItI,OAAOuI,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,QAGThG,SAASS,KAAK4F,QAAQC,MAAQN,EAC9BF,aAAaS,QAAQ,QAASP,GAC9BG,QAAQK,IAAI,cAAcR,UA0B5B,CAkDA,SAASnC,KART,WAEE,MAAM4C,EAAUzG,SAAS0G,uBAAuB,gBAChDrE,MAAMsE,KAAKF,GAASlE,SAASqE,IAC3BA,EAAI9C,iBAAiB,QAAS8B,EAAe,GAEjD,CAGEiB,GA9CF,WAEE,IAAIC,EAA6B,EAC7BC,GAAU,EAEdrJ,OAAOoG,iBAAiB,UAAU,SAAUuB,GAC1CyB,EAA6BpJ,OAAOsJ,QAE/BD,IACHrJ,OAAOwF,uBAAsB,WAzDnC,IAAuB+D,GAxDvB,SAAgCA,GAC9B,MAAMC,EAAY7G,KAAK8G,MAAM3B,EAAO7F,wBAAwBQ,KAE5DgG,QAAQK,IAAI,cAAcU,KACT,GAAbA,GAAkBD,GAAaC,EACjC1B,EAAOjE,UAAUM,IAAI,YAErB2D,EAAOjE,UAAUC,OAAO,WAE5B,EAgDE4F,CADqBH,EA0DDH,GAvGtB,SAAmCG,GAC7BA,EAAYtB,EACd3F,SAASC,gBAAgBsB,UAAUC,OAAO,oBAEtCyF,EAAYxB,EACdzF,SAASC,gBAAgBsB,UAAUM,IAAI,oBAC9BoF,EAAYxB,GACrBzF,SAASC,gBAAgBsB,UAAUC,OAAO,oBAG9CiE,EAAgBwB,CAClB,CAoCEI,CAA0BJ,GAlC5B,SAA6BA,GACT,OAAd1B,IAKa,GAAb0B,EACF1B,EAAU+B,SAAS,EAAG,GAGtBjH,KAAKC,KAAK2G,IACV5G,KAAK8G,MAAMnH,SAASC,gBAAgBS,aAAehD,OAAOqC,aAE1DwF,EAAU+B,SAAS,EAAG/B,EAAU7E,cAGhBV,SAASuH,cAAc,mBAc3C,CAKEC,CAAoBP,GAwDdF,GAAU,CACZ,IAEAA,GAAU,EAEd,IACArJ,OAAO+J,QACT,CA6BEC,GA1BkB,OAAdnC,GAKJ,IAAI,IAAJ,CAAY,cAAe,CACzBrH,QAAQ,EACRyJ,WAAW,EACX9J,SAAU,iBACVI,OAAQ,KACN,IAAI2J,EAAMhI,WAAWiI,iBAAiB7H,SAASC,iBAAiB6H,UAChE,OAAOtC,EAAO7F,wBAAwBoI,OAAS,IAAMH,EAAM,CAAC,GAiBlE,CAcA5H,SAAS8D,iBAAiB,oBAT1B,WACE9D,SAASS,KAAKW,WAAWG,UAAUC,OAAO,SAE1CgE,EAASxF,SAASuH,cAAc,UAChChC,EAAYvF,SAASuH,cAAc,eAEnC1D,GACF","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 = document.documentElement.scrollTop;\nconst GO_TO_TOP_OFFSET = 64;\n\nfunction scrollHandlerForHeader(positionY) {\n const headerTop = Math.floor(header.getBoundingClientRect().top);\n\n console.log(`headerTop: ${headerTop}`);\n if (headerTop == 0 && positionY != headerTop) {\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(positionY);\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 + 2.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","headerTop","floor","scrollHandlerForHeader","scrollHandlerForBackToTop","scrollTo","querySelector","scrollHandlerForTOC","scroll","setupScrollHandler","recursive","rem","getComputedStyle","fontSize","height"],"sourceRoot":""} \ No newline at end of file diff --git a/_static/searchtools.js b/_static/searchtools.js new file mode 100644 index 00000000..b08d58c9 --- /dev/null +++ b/_static/searchtools.js @@ -0,0 +1,620 @@ +/* + * searchtools.js + * ~~~~~~~~~~~~~~~~ + * + * Sphinx JavaScript utilities for the full-text search. + * + * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ +"use strict"; + +/** + * Simple result scoring code. + */ +if (typeof Scorer === "undefined") { + var Scorer = { + // Implement the following function to further tweak the score for each result + // The function takes a result array [docname, title, anchor, descr, score, filename] + // and returns the new score. + /* + score: result => { + const [docname, title, anchor, descr, score, filename] = result + return score + }, + */ + + // query matches the full name of an object + objNameMatch: 11, + // or matches in the last dotted part of the object name + objPartialMatch: 6, + // Additive scores depending on the priority of the object + objPrio: { + 0: 15, // used to be importantResults + 1: 5, // used to be objectResults + 2: -5, // used to be unimportantResults + }, + // Used when the priority is not in the mapping. + objPrioDefault: 0, + + // query found in title + title: 15, + partialTitle: 7, + // query found in terms + term: 5, + partialTerm: 2, + }; +} + +const _removeChildren = (element) => { + while (element && element.lastChild) element.removeChild(element.lastChild); +}; + +/** + * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping + */ +const _escapeRegExp = (string) => + string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string + +const _displayItem = (item, searchTerms, highlightTerms) => { + const docBuilder = DOCUMENTATION_OPTIONS.BUILDER; + const docFileSuffix = DOCUMENTATION_OPTIONS.FILE_SUFFIX; + const docLinkSuffix = DOCUMENTATION_OPTIONS.LINK_SUFFIX; + const showSearchSummary = DOCUMENTATION_OPTIONS.SHOW_SEARCH_SUMMARY; + const contentRoot = document.documentElement.dataset.content_root; + + const [docName, title, anchor, descr, score, _filename] = item; + + let listItem = document.createElement("li"); + let requestUrl; + let linkUrl; + if (docBuilder === "dirhtml") { + // dirhtml builder + let dirname = docName + "/"; + if (dirname.match(/\/index\/$/)) + dirname = dirname.substring(0, dirname.length - 6); + else if (dirname === "index/") dirname = ""; + requestUrl = contentRoot + dirname; + linkUrl = requestUrl; + } else { + // normal html builders + requestUrl = contentRoot + docName + docFileSuffix; + linkUrl = docName + docLinkSuffix; + } + let linkEl = listItem.appendChild(document.createElement("a")); + linkEl.href = linkUrl + anchor; + linkEl.dataset.score = score; + linkEl.innerHTML = title; + if (descr) { + listItem.appendChild(document.createElement("span")).innerHTML = + " (" + descr + ")"; + // highlight search terms in the description + if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js + highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted")); + } + else if (showSearchSummary) + fetch(requestUrl) + .then((responseData) => responseData.text()) + .then((data) => { + if (data) + listItem.appendChild( + Search.makeSearchSummary(data, searchTerms, anchor) + ); + // highlight search terms in the summary + if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js + highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted")); + }); + Search.output.appendChild(listItem); +}; +const _finishSearch = (resultCount) => { + Search.stopPulse(); + Search.title.innerText = _("Search Results"); + if (!resultCount) + Search.status.innerText = Documentation.gettext( + "Your search did not match any documents. Please make sure that all words are spelled correctly and that you've selected enough categories." + ); + else + Search.status.innerText = _( + "Search finished, found ${resultCount} page(s) matching the search query." + ).replace('${resultCount}', resultCount); +}; +const _displayNextItem = ( + results, + resultCount, + searchTerms, + highlightTerms, +) => { + // results left, load the summary and display it + // this is intended to be dynamic (don't sub resultsCount) + if (results.length) { + _displayItem(results.pop(), searchTerms, highlightTerms); + setTimeout( + () => _displayNextItem(results, resultCount, searchTerms, highlightTerms), + 5 + ); + } + // search finished, update title and status message + else _finishSearch(resultCount); +}; +// Helper function used by query() to order search results. +// Each input is an array of [docname, title, anchor, descr, score, filename]. +// Order the results by score (in opposite order of appearance, since the +// `_displayNextItem` function uses pop() to retrieve items) and then alphabetically. +const _orderResultsByScoreThenName = (a, b) => { + const leftScore = a[4]; + const rightScore = b[4]; + if (leftScore === rightScore) { + // same score: sort alphabetically + const leftTitle = a[1].toLowerCase(); + const rightTitle = b[1].toLowerCase(); + if (leftTitle === rightTitle) return 0; + return leftTitle > rightTitle ? -1 : 1; // inverted is intentional + } + return leftScore > rightScore ? 1 : -1; +}; + +/** + * Default splitQuery function. Can be overridden in ``sphinx.search`` with a + * custom function per language. + * + * The regular expression works by splitting the string on consecutive characters + * that are not Unicode letters, numbers, underscores, or emoji characters. + * This is the same as ``\W+`` in Python, preserving the surrogate pair area. + */ +if (typeof splitQuery === "undefined") { + var splitQuery = (query) => query + .split(/[^\p{Letter}\p{Number}_\p{Emoji_Presentation}]+/gu) + .filter(term => term) // remove remaining empty strings +} + +/** + * Search Module + */ +const Search = { + _index: null, + _queued_query: null, + _pulse_status: -1, + + htmlToText: (htmlString, anchor) => { + const htmlElement = new DOMParser().parseFromString(htmlString, 'text/html'); + for (const removalQuery of [".headerlink", "script", "style"]) { + htmlElement.querySelectorAll(removalQuery).forEach((el) => { el.remove() }); + } + if (anchor) { + const anchorContent = htmlElement.querySelector(`[role="main"] ${anchor}`); + if (anchorContent) return anchorContent.textContent; + + console.warn( + `Anchored content block not found. Sphinx search tries to obtain it via DOM query '[role=main] ${anchor}'. Check your theme or template.` + ); + } + + // if anchor not specified or not found, fall back to main content + const docContent = htmlElement.querySelector('[role="main"]'); + if (docContent) return docContent.textContent; + + console.warn( + "Content block not found. Sphinx search tries to obtain it via DOM query '[role=main]'. Check your theme or template." + ); + return ""; + }, + + init: () => { + const query = new URLSearchParams(window.location.search).get("q"); + document + .querySelectorAll('input[name="q"]') + .forEach((el) => (el.value = query)); + if (query) Search.performSearch(query); + }, + + loadIndex: (url) => + (document.body.appendChild(document.createElement("script")).src = url), + + setIndex: (index) => { + Search._index = index; + if (Search._queued_query !== null) { + const query = Search._queued_query; + Search._queued_query = null; + Search.query(query); + } + }, + + hasIndex: () => Search._index !== null, + + deferQuery: (query) => (Search._queued_query = query), + + stopPulse: () => (Search._pulse_status = -1), + + startPulse: () => { + if (Search._pulse_status >= 0) return; + + const pulse = () => { + Search._pulse_status = (Search._pulse_status + 1) % 4; + Search.dots.innerText = ".".repeat(Search._pulse_status); + if (Search._pulse_status >= 0) window.setTimeout(pulse, 500); + }; + pulse(); + }, + + /** + * perform a search for something (or wait until index is loaded) + */ + performSearch: (query) => { + // create the required interface elements + const searchText = document.createElement("h2"); + searchText.textContent = _("Searching"); + const searchSummary = document.createElement("p"); + searchSummary.classList.add("search-summary"); + searchSummary.innerText = ""; + const searchList = document.createElement("ul"); + searchList.classList.add("search"); + + const out = document.getElementById("search-results"); + Search.title = out.appendChild(searchText); + Search.dots = Search.title.appendChild(document.createElement("span")); + Search.status = out.appendChild(searchSummary); + Search.output = out.appendChild(searchList); + + const searchProgress = document.getElementById("search-progress"); + // Some themes don't use the search progress node + if (searchProgress) { + searchProgress.innerText = _("Preparing search..."); + } + Search.startPulse(); + + // index already loaded, the browser was quick! + if (Search.hasIndex()) Search.query(query); + else Search.deferQuery(query); + }, + + _parseQuery: (query) => { + // stem the search terms and add them to the correct list + const stemmer = new Stemmer(); + const searchTerms = new Set(); + const excludedTerms = new Set(); + const highlightTerms = new Set(); + const objectTerms = new Set(splitQuery(query.toLowerCase().trim())); + splitQuery(query.trim()).forEach((queryTerm) => { + const queryTermLower = queryTerm.toLowerCase(); + + // maybe skip this "word" + // stopwords array is from language_data.js + if ( + stopwords.indexOf(queryTermLower) !== -1 || + queryTerm.match(/^\d+$/) + ) + return; + + // stem the word + let word = stemmer.stemWord(queryTermLower); + // select the correct list + if (word[0] === "-") excludedTerms.add(word.substr(1)); + else { + searchTerms.add(word); + highlightTerms.add(queryTermLower); + } + }); + + if (SPHINX_HIGHLIGHT_ENABLED) { // set in sphinx_highlight.js + localStorage.setItem("sphinx_highlight_terms", [...highlightTerms].join(" ")) + } + + // console.debug("SEARCH: searching for:"); + // console.info("required: ", [...searchTerms]); + // console.info("excluded: ", [...excludedTerms]); + + return [query, searchTerms, excludedTerms, highlightTerms, objectTerms]; + }, + + /** + * execute search (requires search index to be loaded) + */ + _performSearch: (query, searchTerms, excludedTerms, highlightTerms, objectTerms) => { + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const titles = Search._index.titles; + const allTitles = Search._index.alltitles; + const indexEntries = Search._index.indexentries; + + // Collect multiple result groups to be sorted separately and then ordered. + // Each is an array of [docname, title, anchor, descr, score, filename]. + const normalResults = []; + const nonMainIndexResults = []; + + _removeChildren(document.getElementById("search-progress")); + + const queryLower = query.toLowerCase().trim(); + for (const [title, foundTitles] of Object.entries(allTitles)) { + if (title.toLowerCase().trim().includes(queryLower) && (queryLower.length >= title.length/2)) { + for (const [file, id] of foundTitles) { + const score = Math.round(Scorer.title * queryLower.length / title.length); + const boost = titles[file] === title ? 1 : 0; // add a boost for document titles + normalResults.push([ + docNames[file], + titles[file] !== title ? `${titles[file]} > ${title}` : title, + id !== null ? "#" + id : "", + null, + score + boost, + filenames[file], + ]); + } + } + } + + // search for explicit entries in index directives + for (const [entry, foundEntries] of Object.entries(indexEntries)) { + if (entry.includes(queryLower) && (queryLower.length >= entry.length/2)) { + for (const [file, id, isMain] of foundEntries) { + const score = Math.round(100 * queryLower.length / entry.length); + const result = [ + docNames[file], + titles[file], + id ? "#" + id : "", + null, + score, + filenames[file], + ]; + if (isMain) { + normalResults.push(result); + } else { + nonMainIndexResults.push(result); + } + } + } + } + + // lookup as object + objectTerms.forEach((term) => + normalResults.push(...Search.performObjectSearch(term, objectTerms)) + ); + + // lookup as search terms in fulltext + normalResults.push(...Search.performTermsSearch(searchTerms, excludedTerms)); + + // let the scorer override scores with a custom scoring function + if (Scorer.score) { + normalResults.forEach((item) => (item[4] = Scorer.score(item))); + nonMainIndexResults.forEach((item) => (item[4] = Scorer.score(item))); + } + + // Sort each group of results by score and then alphabetically by name. + normalResults.sort(_orderResultsByScoreThenName); + nonMainIndexResults.sort(_orderResultsByScoreThenName); + + // Combine the result groups in (reverse) order. + // Non-main index entries are typically arbitrary cross-references, + // so display them after other results. + let results = [...nonMainIndexResults, ...normalResults]; + + // remove duplicate search results + // note the reversing of results, so that in the case of duplicates, the highest-scoring entry is kept + let seen = new Set(); + results = results.reverse().reduce((acc, result) => { + let resultStr = result.slice(0, 4).concat([result[5]]).map(v => String(v)).join(','); + if (!seen.has(resultStr)) { + acc.push(result); + seen.add(resultStr); + } + return acc; + }, []); + + return results.reverse(); + }, + + query: (query) => { + const [searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms] = Search._parseQuery(query); + const results = Search._performSearch(searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms); + + // for debugging + //Search.lastresults = results.slice(); // a copy + // console.info("search results:", Search.lastresults); + + // print the results + _displayNextItem(results, results.length, searchTerms, highlightTerms); + }, + + /** + * search for object names + */ + performObjectSearch: (object, objectTerms) => { + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const objects = Search._index.objects; + const objNames = Search._index.objnames; + const titles = Search._index.titles; + + const results = []; + + const objectSearchCallback = (prefix, match) => { + const name = match[4] + const fullname = (prefix ? prefix + "." : "") + name; + const fullnameLower = fullname.toLowerCase(); + if (fullnameLower.indexOf(object) < 0) return; + + let score = 0; + const parts = fullnameLower.split("."); + + // check for different match types: exact matches of full name or + // "last name" (i.e. last dotted part) + if (fullnameLower === object || parts.slice(-1)[0] === object) + score += Scorer.objNameMatch; + else if (parts.slice(-1)[0].indexOf(object) > -1) + score += Scorer.objPartialMatch; // matches in last name + + const objName = objNames[match[1]][2]; + const title = titles[match[0]]; + + // If more than one term searched for, we require other words to be + // found in the name/title/description + const otherTerms = new Set(objectTerms); + otherTerms.delete(object); + if (otherTerms.size > 0) { + const haystack = `${prefix} ${name} ${objName} ${title}`.toLowerCase(); + if ( + [...otherTerms].some((otherTerm) => haystack.indexOf(otherTerm) < 0) + ) + return; + } + + let anchor = match[3]; + if (anchor === "") anchor = fullname; + else if (anchor === "-") anchor = objNames[match[1]][1] + "-" + fullname; + + const descr = objName + _(", in ") + title; + + // add custom score for some objects according to scorer + if (Scorer.objPrio.hasOwnProperty(match[2])) + score += Scorer.objPrio[match[2]]; + else score += Scorer.objPrioDefault; + + results.push([ + docNames[match[0]], + fullname, + "#" + anchor, + descr, + score, + filenames[match[0]], + ]); + }; + Object.keys(objects).forEach((prefix) => + objects[prefix].forEach((array) => + objectSearchCallback(prefix, array) + ) + ); + return results; + }, + + /** + * search for full-text terms in the index + */ + performTermsSearch: (searchTerms, excludedTerms) => { + // prepare search + const terms = Search._index.terms; + const titleTerms = Search._index.titleterms; + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const titles = Search._index.titles; + + const scoreMap = new Map(); + const fileMap = new Map(); + + // perform the search on the required terms + searchTerms.forEach((word) => { + const files = []; + const arr = [ + { files: terms[word], score: Scorer.term }, + { files: titleTerms[word], score: Scorer.title }, + ]; + // add support for partial matches + if (word.length > 2) { + const escapedWord = _escapeRegExp(word); + if (!terms.hasOwnProperty(word)) { + Object.keys(terms).forEach((term) => { + if (term.match(escapedWord)) + arr.push({ files: terms[term], score: Scorer.partialTerm }); + }); + } + if (!titleTerms.hasOwnProperty(word)) { + Object.keys(titleTerms).forEach((term) => { + if (term.match(escapedWord)) + arr.push({ files: titleTerms[term], score: Scorer.partialTitle }); + }); + } + } + + // no match but word was a required one + if (arr.every((record) => record.files === undefined)) return; + + // found search word in contents + arr.forEach((record) => { + if (record.files === undefined) return; + + let recordFiles = record.files; + if (recordFiles.length === undefined) recordFiles = [recordFiles]; + files.push(...recordFiles); + + // set score for the word in each file + recordFiles.forEach((file) => { + if (!scoreMap.has(file)) scoreMap.set(file, {}); + scoreMap.get(file)[word] = record.score; + }); + }); + + // create the mapping + files.forEach((file) => { + if (!fileMap.has(file)) fileMap.set(file, [word]); + else if (fileMap.get(file).indexOf(word) === -1) fileMap.get(file).push(word); + }); + }); + + // now check if the files don't contain excluded terms + const results = []; + for (const [file, wordList] of fileMap) { + // check if all requirements are matched + + // as search terms with length < 3 are discarded + const filteredTermCount = [...searchTerms].filter( + (term) => term.length > 2 + ).length; + if ( + wordList.length !== searchTerms.size && + wordList.length !== filteredTermCount + ) + continue; + + // ensure that none of the excluded terms is in the search result + if ( + [...excludedTerms].some( + (term) => + terms[term] === file || + titleTerms[term] === file || + (terms[term] || []).includes(file) || + (titleTerms[term] || []).includes(file) + ) + ) + break; + + // select one (max) score for the file. + const score = Math.max(...wordList.map((w) => scoreMap.get(file)[w])); + // add result to the result list + results.push([ + docNames[file], + titles[file], + "", + null, + score, + filenames[file], + ]); + } + return results; + }, + + /** + * helper function to return a node containing the + * search summary for a given text. keywords is a list + * of stemmed words. + */ + makeSearchSummary: (htmlText, keywords, anchor) => { + const text = Search.htmlToText(htmlText, anchor); + if (text === "") return null; + + const textLower = text.toLowerCase(); + const actualStartPosition = [...keywords] + .map((k) => textLower.indexOf(k.toLowerCase())) + .filter((i) => i > -1) + .slice(-1)[0]; + const startWithContext = Math.max(actualStartPosition - 120, 0); + + const top = startWithContext === 0 ? "" : "..."; + const tail = startWithContext + 240 < text.length ? "..." : ""; + + let summary = document.createElement("p"); + summary.classList.add("context"); + summary.textContent = top + text.substr(startWithContext, 240).trim() + tail; + + return summary; + }, +}; + +_ready(Search.init); diff --git a/_static/skeleton.css b/_static/skeleton.css new file mode 100644 index 00000000..467c878c --- /dev/null +++ b/_static/skeleton.css @@ -0,0 +1,296 @@ +/* Some sane resets. */ +html { + height: 100%; +} + +body { + margin: 0; + min-height: 100%; +} + +/* All the flexbox magic! */ +body, +.sb-announcement, +.sb-content, +.sb-main, +.sb-container, +.sb-container__inner, +.sb-article-container, +.sb-footer-content, +.sb-header, +.sb-header-secondary, +.sb-footer { + display: flex; +} + +/* These order things vertically */ +body, +.sb-main, +.sb-article-container { + flex-direction: column; +} + +/* Put elements in the center */ +.sb-header, +.sb-header-secondary, +.sb-container, +.sb-content, +.sb-footer, +.sb-footer-content { + justify-content: center; +} +/* Put elements at the ends */ +.sb-article-container { + justify-content: space-between; +} + +/* These elements grow. */ +.sb-main, +.sb-content, +.sb-container, +article { + flex-grow: 1; +} + +/* Because padding making this wider is not fun */ +article { + box-sizing: border-box; +} + +/* The announcements element should never be wider than the page. */ +.sb-announcement { + max-width: 100%; +} + +.sb-sidebar-primary, +.sb-sidebar-secondary { + flex-shrink: 0; + width: 17rem; +} + +.sb-announcement__inner { + justify-content: center; + + box-sizing: border-box; + height: 3rem; + + overflow-x: auto; + white-space: nowrap; +} + +/* Sidebars, with checkbox-based toggle */ +.sb-sidebar-primary, +.sb-sidebar-secondary { + position: fixed; + height: 100%; + top: 0; +} + +.sb-sidebar-primary { + left: -17rem; + transition: left 250ms ease-in-out; +} +.sb-sidebar-secondary { + right: -17rem; + transition: right 250ms ease-in-out; +} + +.sb-sidebar-toggle { + display: none; +} +.sb-sidebar-overlay { + position: fixed; + top: 0; + width: 0; + height: 0; + + transition: width 0ms ease 250ms, height 0ms ease 250ms, opacity 250ms ease; + + opacity: 0; + background-color: rgba(0, 0, 0, 0.54); +} + +#sb-sidebar-toggle--primary:checked + ~ .sb-sidebar-overlay[for="sb-sidebar-toggle--primary"], +#sb-sidebar-toggle--secondary:checked + ~ .sb-sidebar-overlay[for="sb-sidebar-toggle--secondary"] { + width: 100%; + height: 100%; + opacity: 1; + transition: width 0ms ease, height 0ms ease, opacity 250ms ease; +} + +#sb-sidebar-toggle--primary:checked ~ .sb-container .sb-sidebar-primary { + left: 0; +} +#sb-sidebar-toggle--secondary:checked ~ .sb-container .sb-sidebar-secondary { + right: 0; +} + +/* Full-width mode */ +.drop-secondary-sidebar-for-full-width-content + .hide-when-secondary-sidebar-shown { + display: none !important; +} +.drop-secondary-sidebar-for-full-width-content .sb-sidebar-secondary { + display: none !important; +} + +/* Mobile views */ +.sb-page-width { + width: 100%; +} + +.sb-article-container, +.sb-footer-content__inner, +.drop-secondary-sidebar-for-full-width-content .sb-article, +.drop-secondary-sidebar-for-full-width-content .match-content-width { + width: 100vw; +} + +.sb-article, +.match-content-width { + padding: 0 1rem; + box-sizing: border-box; +} + +@media (min-width: 32rem) { + .sb-article, + .match-content-width { + padding: 0 2rem; + } +} + +/* Tablet views */ +@media (min-width: 42rem) { + .sb-article-container { + width: auto; + } + .sb-footer-content__inner, + .drop-secondary-sidebar-for-full-width-content .sb-article, + .drop-secondary-sidebar-for-full-width-content .match-content-width { + width: 42rem; + } + .sb-article, + .match-content-width { + width: 42rem; + } +} +@media (min-width: 46rem) { + .sb-footer-content__inner, + .drop-secondary-sidebar-for-full-width-content .sb-article, + .drop-secondary-sidebar-for-full-width-content .match-content-width { + width: 46rem; + } + .sb-article, + .match-content-width { + width: 46rem; + } +} +@media (min-width: 50rem) { + .sb-footer-content__inner, + .drop-secondary-sidebar-for-full-width-content .sb-article, + .drop-secondary-sidebar-for-full-width-content .match-content-width { + width: 50rem; + } + .sb-article, + .match-content-width { + width: 50rem; + } +} + +/* Tablet views */ +@media (min-width: 59rem) { + .sb-sidebar-secondary { + position: static; + } + .hide-when-secondary-sidebar-shown { + display: none !important; + } + .sb-footer-content__inner, + .drop-secondary-sidebar-for-full-width-content .sb-article, + .drop-secondary-sidebar-for-full-width-content .match-content-width { + width: 59rem; + } + .sb-article, + .match-content-width { + width: 42rem; + } +} +@media (min-width: 63rem) { + .sb-footer-content__inner, + .drop-secondary-sidebar-for-full-width-content .sb-article, + .drop-secondary-sidebar-for-full-width-content .match-content-width { + width: 63rem; + } + .sb-article, + .match-content-width { + width: 46rem; + } +} +@media (min-width: 67rem) { + .sb-footer-content__inner, + .drop-secondary-sidebar-for-full-width-content .sb-article, + .drop-secondary-sidebar-for-full-width-content .match-content-width { + width: 67rem; + } + .sb-article, + .match-content-width { + width: 50rem; + } +} + +/* Desktop views */ +@media (min-width: 76rem) { + .sb-sidebar-primary { + position: static; + } + .hide-when-primary-sidebar-shown { + display: none !important; + } + .sb-footer-content__inner, + .drop-secondary-sidebar-for-full-width-content .sb-article, + .drop-secondary-sidebar-for-full-width-content .match-content-width { + width: 59rem; + } + .sb-article, + .match-content-width { + width: 42rem; + } +} + +/* Full desktop views */ +@media (min-width: 80rem) { + .sb-article, + .match-content-width { + width: 46rem; + } + .sb-footer-content__inner, + .drop-secondary-sidebar-for-full-width-content .sb-article, + .drop-secondary-sidebar-for-full-width-content .match-content-width { + width: 63rem; + } +} + +@media (min-width: 84rem) { + .sb-article, + .match-content-width { + width: 50rem; + } + .sb-footer-content__inner, + .drop-secondary-sidebar-for-full-width-content .sb-article, + .drop-secondary-sidebar-for-full-width-content .match-content-width { + width: 67rem; + } +} + +@media (min-width: 88rem) { + .sb-footer-content__inner, + .drop-secondary-sidebar-for-full-width-content .sb-article, + .drop-secondary-sidebar-for-full-width-content .match-content-width { + width: 67rem; + } + .sb-page-width { + width: 88rem; + } +} diff --git a/_static/sphinx_highlight.js b/_static/sphinx_highlight.js new file mode 100644 index 00000000..8a96c69a --- /dev/null +++ b/_static/sphinx_highlight.js @@ -0,0 +1,154 @@ +/* Highlighting utilities for Sphinx HTML documentation. */ +"use strict"; + +const SPHINX_HIGHLIGHT_ENABLED = true + +/** + * highlight a given string on a node by wrapping it in + * span elements with the given class name. + */ +const _highlight = (node, addItems, text, className) => { + if (node.nodeType === Node.TEXT_NODE) { + const val = node.nodeValue; + const parent = node.parentNode; + const pos = val.toLowerCase().indexOf(text); + if ( + pos >= 0 && + !parent.classList.contains(className) && + !parent.classList.contains("nohighlight") + ) { + let span; + + const closestNode = parent.closest("body, svg, foreignObject"); + const isInSVG = closestNode && closestNode.matches("svg"); + if (isInSVG) { + span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); + } else { + span = document.createElement("span"); + span.classList.add(className); + } + + span.appendChild(document.createTextNode(val.substr(pos, text.length))); + const rest = document.createTextNode(val.substr(pos + text.length)); + parent.insertBefore( + span, + parent.insertBefore( + rest, + node.nextSibling + ) + ); + node.nodeValue = val.substr(0, pos); + /* There may be more occurrences of search term in this node. So call this + * function recursively on the remaining fragment. + */ + _highlight(rest, addItems, text, className); + + if (isInSVG) { + const rect = document.createElementNS( + "http://www.w3.org/2000/svg", + "rect" + ); + const bbox = parent.getBBox(); + rect.x.baseVal.value = bbox.x; + rect.y.baseVal.value = bbox.y; + rect.width.baseVal.value = bbox.width; + rect.height.baseVal.value = bbox.height; + rect.setAttribute("class", className); + addItems.push({ parent: parent, target: rect }); + } + } + } else if (node.matches && !node.matches("button, select, textarea")) { + node.childNodes.forEach((el) => _highlight(el, addItems, text, className)); + } +}; +const _highlightText = (thisNode, text, className) => { + let addItems = []; + _highlight(thisNode, addItems, text, className); + addItems.forEach((obj) => + obj.parent.insertAdjacentElement("beforebegin", obj.target) + ); +}; + +/** + * Small JavaScript module for the documentation. + */ +const SphinxHighlight = { + + /** + * highlight the search words provided in localstorage in the text + */ + highlightSearchWords: () => { + if (!SPHINX_HIGHLIGHT_ENABLED) return; // bail if no highlight + + // get and clear terms from localstorage + const url = new URL(window.location); + const highlight = + localStorage.getItem("sphinx_highlight_terms") + || url.searchParams.get("highlight") + || ""; + localStorage.removeItem("sphinx_highlight_terms") + url.searchParams.delete("highlight"); + window.history.replaceState({}, "", url); + + // get individual terms from highlight string + const terms = highlight.toLowerCase().split(/\s+/).filter(x => x); + if (terms.length === 0) return; // nothing to do + + // There should never be more than one element matching "div.body" + const divBody = document.querySelectorAll("div.body"); + const body = divBody.length ? divBody[0] : document.querySelector("body"); + window.setTimeout(() => { + terms.forEach((term) => _highlightText(body, term, "highlighted")); + }, 10); + + const searchBox = document.getElementById("searchbox"); + if (searchBox === null) return; + searchBox.appendChild( + document + .createRange() + .createContextualFragment( + '" + ) + ); + }, + + /** + * helper function to hide the search marks again + */ + hideSearchWords: () => { + document + .querySelectorAll("#searchbox .highlight-link") + .forEach((el) => el.remove()); + document + .querySelectorAll("span.highlighted") + .forEach((el) => el.classList.remove("highlighted")); + localStorage.removeItem("sphinx_highlight_terms") + }, + + initEscapeListener: () => { + // only install a listener if it is really needed + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + // bail with special keys + if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) return; + if (DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS && (event.key === "Escape")) { + SphinxHighlight.hideSearchWords(); + event.preventDefault(); + } + }); + }, +}; + +_ready(() => { + /* Do not call highlightSearchWords() when we are on the search page. + * It will highlight words from the *previous* search query. + */ + if (typeof Search === "undefined") SphinxHighlight.highlightSearchWords(); + SphinxHighlight.initEscapeListener(); +}); diff --git a/_static/styles/furo-extensions.css b/_static/styles/furo-extensions.css new file mode 100644 index 00000000..82295876 --- /dev/null +++ b/_static/styles/furo-extensions.css @@ -0,0 +1,2 @@ +#furo-sidebar-ad-placement{padding:var(--sidebar-item-spacing-vertical) var(--sidebar-item-spacing-horizontal)}#furo-sidebar-ad-placement .ethical-sidebar{background:var(--color-background-secondary);border:none;box-shadow:none}#furo-sidebar-ad-placement .ethical-sidebar:hover{background:var(--color-background-hover)}#furo-sidebar-ad-placement .ethical-sidebar a{color:var(--color-foreground-primary)}#furo-sidebar-ad-placement .ethical-callout a{color:var(--color-foreground-secondary)!important}#furo-readthedocs-versions{background:transparent;display:block;position:static;width:100%}#furo-readthedocs-versions .rst-versions{background:#1a1c1e}#furo-readthedocs-versions .rst-current-version{background:var(--color-sidebar-item-background);cursor:unset}#furo-readthedocs-versions .rst-current-version:hover{background:var(--color-sidebar-item-background)}#furo-readthedocs-versions .rst-current-version .fa-book{color:var(--color-foreground-primary)}#furo-readthedocs-versions>.rst-other-versions{padding:0}#furo-readthedocs-versions>.rst-other-versions small{opacity:1}#furo-readthedocs-versions .injected .rst-versions{position:unset}#furo-readthedocs-versions:focus-within,#furo-readthedocs-versions:hover{box-shadow:0 0 0 1px var(--color-sidebar-background-border)}#furo-readthedocs-versions:focus-within .rst-current-version,#furo-readthedocs-versions:hover .rst-current-version{background:#1a1c1e;font-size:inherit;height:auto;line-height:inherit;padding:12px;text-align:right}#furo-readthedocs-versions:focus-within .rst-current-version .fa-book,#furo-readthedocs-versions:hover .rst-current-version .fa-book{color:#fff;float:left}#furo-readthedocs-versions:focus-within .fa-caret-down,#furo-readthedocs-versions:hover .fa-caret-down{display:none}#furo-readthedocs-versions:focus-within .injected,#furo-readthedocs-versions:focus-within .rst-current-version,#furo-readthedocs-versions:focus-within .rst-other-versions,#furo-readthedocs-versions:hover .injected,#furo-readthedocs-versions:hover .rst-current-version,#furo-readthedocs-versions:hover .rst-other-versions{display:block}#furo-readthedocs-versions:focus-within>.rst-current-version,#furo-readthedocs-versions:hover>.rst-current-version{display:none}.highlight:hover button.copybtn{color:var(--color-code-foreground)}.highlight button.copybtn{align-items:center;background-color:var(--color-code-background);border:none;color:var(--color-background-item);cursor:pointer;height:1.25em;right:.5rem;top:.625rem;transition:color .3s,opacity .3s;width:1.25em}.highlight button.copybtn:hover{background-color:var(--color-code-background);color:var(--color-brand-content)}.highlight button.copybtn:after{background-color:transparent;color:var(--color-code-foreground);display:none}.highlight button.copybtn.success{color:#22863a;transition:color 0ms}.highlight button.copybtn.success:after{display:block}.highlight button.copybtn svg{padding:0}body{--sd-color-primary:var(--color-brand-primary);--sd-color-primary-highlight:var(--color-brand-content);--sd-color-primary-text:var(--color-background-primary);--sd-color-shadow:rgba(0,0,0,.05);--sd-color-card-border:var(--color-card-border);--sd-color-card-border-hover:var(--color-brand-content);--sd-color-card-background:var(--color-card-background);--sd-color-card-text:var(--color-foreground-primary);--sd-color-card-header:var(--color-card-marginals-background);--sd-color-card-footer:var(--color-card-marginals-background);--sd-color-tabs-label-active:var(--color-brand-content);--sd-color-tabs-label-hover:var(--color-foreground-muted);--sd-color-tabs-label-inactive:var(--color-foreground-muted);--sd-color-tabs-underline-active:var(--color-brand-content);--sd-color-tabs-underline-hover:var(--color-foreground-border);--sd-color-tabs-underline-inactive:var(--color-background-border);--sd-color-tabs-overline:var(--color-background-border);--sd-color-tabs-underline:var(--color-background-border)}.sd-tab-content{box-shadow:0 -2px var(--sd-color-tabs-overline),0 1px var(--sd-color-tabs-underline)}.sd-card{box-shadow:0 .1rem .25rem var(--sd-color-shadow),0 0 .0625rem rgba(0,0,0,.1)}.sd-shadow-sm{box-shadow:0 .1rem .25rem var(--sd-color-shadow),0 0 .0625rem rgba(0,0,0,.1)!important}.sd-shadow-md{box-shadow:0 .3rem .75rem var(--sd-color-shadow),0 0 .0625rem rgba(0,0,0,.1)!important}.sd-shadow-lg{box-shadow:0 .6rem 1.5rem var(--sd-color-shadow),0 0 .0625rem rgba(0,0,0,.1)!important}.sd-card-hover:hover{transform:none}.sd-cards-carousel{gap:.25rem;padding:.25rem}body{--tabs--label-text:var(--color-foreground-muted);--tabs--label-text--hover:var(--color-foreground-muted);--tabs--label-text--active:var(--color-brand-content);--tabs--label-text--active--hover:var(--color-brand-content);--tabs--label-background:transparent;--tabs--label-background--hover:transparent;--tabs--label-background--active:transparent;--tabs--label-background--active--hover:transparent;--tabs--padding-x:0.25em;--tabs--margin-x:1em;--tabs--border:var(--color-background-border);--tabs--label-border:transparent;--tabs--label-border--hover:var(--color-foreground-muted);--tabs--label-border--active:var(--color-brand-content);--tabs--label-border--active--hover:var(--color-brand-content)}[role=main] .container{max-width:none;padding-left:0;padding-right:0}.shadow.docutils{border:none;box-shadow:0 .2rem .5rem rgba(0,0,0,.05),0 0 .0625rem rgba(0,0,0,.1)!important}.sphinx-bs .card{background-color:var(--color-background-secondary);color:var(--color-foreground)} +/*# sourceMappingURL=furo-extensions.css.map*/ \ No newline at end of file diff --git a/_static/styles/furo-extensions.css.map b/_static/styles/furo-extensions.css.map new file mode 100644 index 00000000..c26eac7f --- /dev/null +++ b/_static/styles/furo-extensions.css.map @@ -0,0 +1 @@ +{"version":3,"file":"styles/furo-extensions.css","mappings":"AAGA,2BACE,oFACA,4CAKE,6CAHA,YACA,eAEA,CACA,kDACE,yCAEF,8CACE,sCAEJ,8CACE,kDAEJ,2BAGE,uBACA,cAHA,gBACA,UAEA,CAGA,yCACE,mBAEF,gDAEE,gDADA,YACA,CACA,sDACE,gDACF,yDACE,sCAEJ,+CACE,UACA,qDACE,UAGF,mDACE,eAEJ,yEAEE,4DAEA,mHASE,mBAPA,kBAEA,YADA,oBAGA,aADA,gBAIA,CAEA,qIAEE,WADA,UACA,CAEJ,uGACE,aAEF,iUAGE,cAEF,mHACE,aC1EJ,gCACE,mCAEF,0BAEE,mBAUA,8CACA,YAFA,mCAKA,eAZA,cAIA,YADA,YAYA,iCAdA,YAcA,CAEA,gCAEE,8CADA,gCACA,CAEF,gCAGE,6BADA,mCADA,YAEA,CAEF,kCAEE,cADA,oBACA,CACA,wCACE,cAEJ,8BACE,UCzCN,KAEE,6CAA8C,CAC9C,uDAAwD,CACxD,uDAAwD,CAGxD,iCAAsC,CAGtC,+CAAgD,CAChD,uDAAwD,CACxD,uDAAwD,CACxD,oDAAqD,CACrD,6DAA8D,CAC9D,6DAA8D,CAG9D,uDAAwD,CACxD,yDAA0D,CAC1D,4DAA6D,CAC7D,2DAA4D,CAC5D,8DAA+D,CAC/D,iEAAkE,CAClE,uDAAwD,CACxD,wDAAyD,CAG3D,gBACE,qFAGF,SACE,6EAEF,cACE,uFAEF,cACE,uFAEF,cACE,uFAGF,qBACE,eAEF,mBACE,WACA,eChDF,KACE,gDAAiD,CACjD,uDAAwD,CACxD,qDAAsD,CACtD,4DAA6D,CAC7D,oCAAqC,CACrC,2CAA4C,CAC5C,4CAA6C,CAC7C,mDAAoD,CACpD,wBAAyB,CACzB,oBAAqB,CACrB,6CAA8C,CAC9C,gCAAiC,CACjC,yDAA0D,CAC1D,uDAAwD,CACxD,8DAA+D,CCbjE,uBACE,eACA,eACA,gBAGF,iBACE,YACA,+EAGF,iBACE,mDACA","sources":["webpack:///./src/furo/assets/styles/extensions/_readthedocs.sass","webpack:///./src/furo/assets/styles/extensions/_copybutton.sass","webpack:///./src/furo/assets/styles/extensions/_sphinx-design.sass","webpack:///./src/furo/assets/styles/extensions/_sphinx-inline-tabs.sass","webpack:///./src/furo/assets/styles/extensions/_sphinx-panels.sass"],"sourcesContent":["// This file contains the styles used for tweaking how ReadTheDoc's embedded\n// contents would show up inside the theme.\n\n#furo-sidebar-ad-placement\n padding: var(--sidebar-item-spacing-vertical) var(--sidebar-item-spacing-horizontal)\n .ethical-sidebar\n // Remove the border and box-shadow.\n border: none\n box-shadow: none\n // Manage the background colors.\n background: var(--color-background-secondary)\n &:hover\n background: var(--color-background-hover)\n // Ensure the text is legible.\n a\n color: var(--color-foreground-primary)\n\n .ethical-callout a\n color: var(--color-foreground-secondary) !important\n\n#furo-readthedocs-versions\n position: static\n width: 100%\n background: transparent\n display: block\n\n // Make the background color fit with the theme's aesthetic.\n .rst-versions\n background: rgb(26, 28, 30)\n\n .rst-current-version\n cursor: unset\n background: var(--color-sidebar-item-background)\n &:hover\n background: var(--color-sidebar-item-background)\n .fa-book\n color: var(--color-foreground-primary)\n\n > .rst-other-versions\n padding: 0\n small\n opacity: 1\n\n .injected\n .rst-versions\n position: unset\n\n &:hover,\n &:focus-within\n box-shadow: 0 0 0 1px var(--color-sidebar-background-border)\n\n .rst-current-version\n // Undo the tweaks done in RTD's CSS\n font-size: inherit\n line-height: inherit\n height: auto\n text-align: right\n padding: 12px\n\n // Match the rest of the body\n background: #1a1c1e\n\n .fa-book\n float: left\n color: white\n\n .fa-caret-down\n display: none\n\n .rst-current-version,\n .rst-other-versions,\n .injected\n display: block\n\n > .rst-current-version\n display: none\n",".highlight\n &:hover button.copybtn\n color: var(--color-code-foreground)\n\n button.copybtn\n // Align things correctly\n align-items: center\n\n height: 1.25em\n width: 1.25em\n\n top: 0.625rem // $code-spacing-vertical\n right: 0.5rem\n\n // Make it look better\n color: var(--color-background-item)\n background-color: var(--color-code-background)\n border: none\n\n // Change to cursor to make it obvious that you can click on it\n cursor: pointer\n\n // Transition smoothly, for aesthetics\n transition: color 300ms, opacity 300ms\n\n &:hover\n color: var(--color-brand-content)\n background-color: var(--color-code-background)\n\n &::after\n display: none\n color: var(--color-code-foreground)\n background-color: transparent\n\n &.success\n transition: color 0ms\n color: #22863a\n &::after\n display: block\n\n svg\n padding: 0\n","body\n // Colors\n --sd-color-primary: var(--color-brand-primary)\n --sd-color-primary-highlight: var(--color-brand-content)\n --sd-color-primary-text: var(--color-background-primary)\n\n // Shadows\n --sd-color-shadow: rgba(0, 0, 0, 0.05)\n\n // Cards\n --sd-color-card-border: var(--color-card-border)\n --sd-color-card-border-hover: var(--color-brand-content)\n --sd-color-card-background: var(--color-card-background)\n --sd-color-card-text: var(--color-foreground-primary)\n --sd-color-card-header: var(--color-card-marginals-background)\n --sd-color-card-footer: var(--color-card-marginals-background)\n\n // Tabs\n --sd-color-tabs-label-active: var(--color-brand-content)\n --sd-color-tabs-label-hover: var(--color-foreground-muted)\n --sd-color-tabs-label-inactive: var(--color-foreground-muted)\n --sd-color-tabs-underline-active: var(--color-brand-content)\n --sd-color-tabs-underline-hover: var(--color-foreground-border)\n --sd-color-tabs-underline-inactive: var(--color-background-border)\n --sd-color-tabs-overline: var(--color-background-border)\n --sd-color-tabs-underline: var(--color-background-border)\n\n// Tabs\n.sd-tab-content\n box-shadow: 0 -2px var(--sd-color-tabs-overline), 0 1px var(--sd-color-tabs-underline)\n\n// Shadows\n.sd-card // Have a shadow by default\n box-shadow: 0 0.1rem 0.25rem var(--sd-color-shadow), 0 0 0.0625rem rgba(0, 0, 0, 0.1)\n\n.sd-shadow-sm\n box-shadow: 0 0.1rem 0.25rem var(--sd-color-shadow), 0 0 0.0625rem rgba(0, 0, 0, 0.1) !important\n\n.sd-shadow-md\n box-shadow: 0 0.3rem 0.75rem var(--sd-color-shadow), 0 0 0.0625rem rgba(0, 0, 0, 0.1) !important\n\n.sd-shadow-lg\n box-shadow: 0 0.6rem 1.5rem var(--sd-color-shadow), 0 0 0.0625rem rgba(0, 0, 0, 0.1) !important\n\n// Cards\n.sd-card-hover:hover // Don't change scale on hover\n transform: none\n\n.sd-cards-carousel // Have a bit of gap in the carousel by default\n gap: 0.25rem\n padding: 0.25rem\n","// This file contains styles to tweak sphinx-inline-tabs to work well with Furo.\n\nbody\n --tabs--label-text: var(--color-foreground-muted)\n --tabs--label-text--hover: var(--color-foreground-muted)\n --tabs--label-text--active: var(--color-brand-content)\n --tabs--label-text--active--hover: var(--color-brand-content)\n --tabs--label-background: transparent\n --tabs--label-background--hover: transparent\n --tabs--label-background--active: transparent\n --tabs--label-background--active--hover: transparent\n --tabs--padding-x: 0.25em\n --tabs--margin-x: 1em\n --tabs--border: var(--color-background-border)\n --tabs--label-border: transparent\n --tabs--label-border--hover: var(--color-foreground-muted)\n --tabs--label-border--active: var(--color-brand-content)\n --tabs--label-border--active--hover: var(--color-brand-content)\n","// This file contains styles to tweak sphinx-panels to work well with Furo.\n\n// sphinx-panels includes Bootstrap 4, which uses .container which can conflict\n// with docutils' `.. container::` directive.\n[role=\"main\"] .container\n max-width: initial\n padding-left: initial\n padding-right: initial\n\n// Make the panels look nicer!\n.shadow.docutils\n border: none\n box-shadow: 0 0.2rem 0.5rem rgba(0, 0, 0, 0.05), 0 0 0.0625rem rgba(0, 0, 0, 0.1) !important\n\n// Make panel colors respond to dark mode\n.sphinx-bs .card\n background-color: var(--color-background-secondary)\n color: var(--color-foreground)\n"],"names":[],"sourceRoot":""} \ No newline at end of file diff --git a/_static/styles/furo.css b/_static/styles/furo.css new file mode 100644 index 00000000..21836d6a --- /dev/null +++ b/_static/styles/furo.css @@ -0,0 +1,2 @@ +/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}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{height:1px!important;margin:-1px!important;overflow:hidden!important;padding:0!important;position:absolute!important;width:1px!important;clip:rect(0,0,0,0)!important;background:var(--color-background-primary);border:0!important;color:var(--color-foreground-primary);white-space:nowrap!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-stack--headings:var(--font-stack);--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:#6b6f76;--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:#0a4bff;--color-brand-content:#2757dd;--color-brand-visited:#872ee0;--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-api-added:#21632c;--color-api-added-border:#38a84d;--color-api-changed:#046172;--color-api-changed-border:#06a1bc;--color-api-deprecated:#605706;--color-api-deprecated-border:#f0d90f;--color-api-removed:#b30000;--color-api-removed-border:#ff5c5c;--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-underline:var(--color-background-border);--color-link--hover:var(--color-brand-content);--color-link-underline--hover:var(--color-foreground-border);--color-link--visited:var(--color-brand-visited);--color-link-underline--visited:var(--color-background-border);--color-link--visited--hover:var(--color-brand-visited);--color-link-underline--visited--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:#cfd0d0;--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:#3d94ff;--color-brand-content:#5ca5ff;--color-brand-visited:#b27aeb;--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-api-added:#3db854;--color-api-added-border:#267334;--color-api-changed:#09b0ce;--color-api-changed-border:#056d80;--color-api-deprecated:#b1a10b;--color-api-deprecated-border:#6e6407;--color-api-removed:#ff7575;--color-api-removed-border:#b03b3b;--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:#cfd0d0;--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:#3d94ff;--color-brand-content:#5ca5ff;--color-brand-visited:#b27aeb;--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-api-added:#3db854;--color-api-added-border:#267334;--color-api-changed:#09b0ce;--color-api-changed-border:#056d80;--color-api-deprecated:#b1a10b;--color-api-deprecated-border:#6e6407;--color-api-removed:#ff7575;--color-api-removed-border:#b03b3b;--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-light{display:block}@media(prefers-color-scheme:dark){body[data-theme=auto] .theme-toggle svg.theme-icon-when-auto-dark{display:block}body[data-theme=auto] .theme-toggle svg.theme-icon-when-auto-light{display:none}}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-family:var(--font-stack--headings);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:visited{color:var(--color-link--visited);text-decoration-color:var(--color-link-underline--visited)}a:visited:hover{color:var(--color-link--visited--hover);text-decoration-color:var(--color-link-underline--visited--hover)}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--hover);text-decoration-color:var(--color-link-underline--hover)}a.muted-link:hover:visited{color:var(--color-link--visited--hover);text-decoration-color:var(--color-link-underline--visited--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{height:100%}.skip-to-content,body,html{background:var(--color-background-primary);color:var(--color-foreground-primary)}.skip-to-content{border-radius:1rem;left:.25rem;padding:1rem;position:fixed;top:.25rem;transform:translateY(-200%);transition:transform .3s ease-in-out;z-index:40}.skip-to-content:focus-within{transform:translateY(0)}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{display:flex}.theme-toggle{background:transparent;border:none;cursor:pointer;display:flex;padding:0}.theme-toggle svg{color:var(--color-foreground-primary);display:none;height:1.25rem;width:1.25rem}.theme-toggle-header{align-items:center;display:flex;justify-content:center}.nav-overlay-icon,.toc-overlay-icon{cursor:pointer;display:none}.nav-overlay-icon .icon,.toc-overlay-icon .icon{color:var(--color-foreground-secondary);height:1.5rem;width:1.5rem}.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,.content-icon-container .view-this-page svg{color:inherit;height:1.25rem;width:1.25rem}.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{height:1rem;width:1rem;fill:currentColor;display:inline-block}.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}.theme-toggle-header,.toc-header-icon{display:flex}.theme-toggle-content,.toc-content-icon{display:none}.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.5rem;width:1.5rem}:target{scroll-margin-top:calc(var(--header-height) + 2.5rem)}.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:4.25rem}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}div.deprecated,div.versionadded,div.versionchanged,div.versionremoved{border-left:.1875rem solid;border-radius:.125rem;padding-left:.75rem}div.deprecated p,div.versionadded p,div.versionchanged p,div.versionremoved p{margin-bottom:.125rem;margin-top:.125rem}div.versionadded{border-color:var(--color-api-added-border)}div.versionadded .versionmodified{color:var(--color-api-added)}div.versionchanged{border-color:var(--color-api-changed-border)}div.versionchanged .versionmodified{color:var(--color-api-changed)}div.deprecated{border-color:var(--color-api-deprecated-border)}div.deprecated .versionmodified{color:var(--color-api-deprecated)}div.versionremoved{border-color:var(--color-api-removed-border)}div.versionremoved .versionmodified{color:var(--color-api-removed)}.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>:not(span),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:2.5rem}@media(max-width:67em){:target{scroll-margin-top:calc(2.5rem + var(--header-height))}section>span:target{scroll-margin-top:calc(2.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);color:var(--color-sidebar-link-text)}.sidebar-tree .reference.external:after{color:var(--color-sidebar-link-text);content:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23607D8B' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' viewBox='0 0 24 24'%3E%3Cpath stroke='none' d='M0 0h24v24H0z'/%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 a.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/_static/styles/furo.css.map b/_static/styles/furo.css.map new file mode 100644 index 00000000..0ee3acbe --- /dev/null +++ b/_static/styles/furo.css.map @@ -0,0 +1 @@ +{"version":3,"file":"styles/furo.css","mappings":"AAAA,2EAA2E,CAU3E,KACE,gBAAiB,CACjB,6BACF,CASA,KACE,QACF,CAMA,KACE,aACF,CAOA,GACE,aAAc,CACd,cACF,CAUA,GACE,sBAAuB,CACvB,QAAS,CACT,gBACF,CAOA,IACE,+BAAiC,CACjC,aACF,CASA,EACE,4BACF,CAOA,YACE,kBAAmB,CACnB,yBAA0B,CAC1B,gCACF,CAMA,SAEE,kBACF,CAOA,cAGE,+BAAiC,CACjC,aACF,CAeA,QAEE,aAAc,CACd,aAAc,CACd,iBAAkB,CAClB,uBACF,CAEA,IACE,aACF,CAEA,IACE,SACF,CASA,IACE,iBACF,CAUA,sCAKE,mBAAoB,CACpB,cAAe,CACf,gBAAiB,CACjB,QACF,CAOA,aAEE,gBACF,CAOA,cAEE,mBACF,CAMA,gDAIE,yBACF,CAMA,wHAIE,iBAAkB,CAClB,SACF,CAMA,4GAIE,6BACF,CAMA,SACE,0BACF,CASA,OACE,qBAAsB,CACtB,aAAc,CACd,aAAc,CACd,cAAe,CACf,SAAU,CACV,kBACF,CAMA,SACE,uBACF,CAMA,SACE,aACF,CAOA,6BAEE,qBAAsB,CACtB,SACF,CAMA,kFAEE,WACF,CAOA,cACE,4BAA6B,CAC7B,mBACF,CAMA,yCACE,uBACF,CAOA,6BACE,yBAA0B,CAC1B,YACF,CASA,QACE,aACF,CAMA,QACE,iBACF,CAiBA,kBACE,YACF,CCvVA,aAcE,kEACE,uBAOF,WACE,iDAMF,gCACE,wBAEF,qCAEE,uBADA,uBACA,CAEF,SACE,wBAtBA,CCpBJ,iBAGE,qBAEA,sBACA,0BAFA,oBAHA,4BACA,oBAKA,6BAIA,2CAFA,mBACA,sCAFA,4BAGA,CAEF,gBACE,aCTF,KCGE,mHAEA,wGAEA,wCAAyC,CAEzC,wBAAyB,CACzB,wBAAyB,CACzB,4BAA6B,CAC7B,yBAA0B,CAC1B,2BAA4B,CAG5B,sDAAuD,CACvD,gDAAiD,CACjD,wDAAyD,CAGzD,0CAA2C,CAC3C,gDAAiD,CACjD,gDAAiD,CAKjD,gCAAiC,CACjC,sCAAuC,CAGvC,2CAA4C,CAG5C,uCAAwC,CCjCxC,+FAGA,uBAAwB,CAGxB,iCAAkC,CAClC,kCAAmC,CAEnC,+BAAgC,CAChC,sCAAuC,CACvC,sCAAuC,CACvC,qGAIA,mDAAoD,CAEpD,mCAAoC,CACpC,8CAA+C,CAC/C,gDAAiD,CACjD,kCAAmC,CACnC,6DAA8D,CAG9D,6BAA8B,CAC9B,6BAA8B,CAC9B,+BAAgC,CAChC,kCAAmC,CACnC,kCAAmC,CCPjC,+jBCYA,iqCAZF,iaCVA,8KAOA,4SAWA,4SAUA,0CACA,gEAGA,0CAGA,gEAGA,yCACA,+DAIA,4CACA,kEAGA,wCAUA,8DACA,uCAGA,4DACA,sCACA,2DAGA,4CACA,kEACA,uCAGA,6DACA,2GAGA,sHAEA,yFAEA,+CACA,+EAGA,4MAOA,gCACA,sHAIA,kCACA,uEACA,gEACA,4DACA,kEAGA,2DACA,sDACA,0CACA,8CACA,wGAGA,0BACA,iCAGA,+DACA,+BACA,sCACA,+DAEA,kGACA,oCACA,yDACA,sCL7HF,kCAEA,sDAIA,0CK2HE,kEAIA,oDACA,sDAGA,oCACA,oEAEA,0DACA,qDAIA,oDACA,6DAIA,iEAIA,2DAIA,2DAGA,4DACA,gEAIA,gEAEA,gFAEA,oNASA,qDLxKE,gFAGE,4DAIF,oEKkHF,yEAEA,6DAGA,0DAEA,uDACA,qDACA,wDAIA,6DAIA,yDACA,2DAIA,uCAGA,wCACA,sDAGA,+CAGA,6DAEA,iDACA,+DAEA,wDAEA,sEAMA,0DACA,sBACA,mEL9JI,wEAEA,iCACE,+BAMN,wEAGA,iCACE,kFAEA,uEAIF,gEACE,8BAGF,qEMvDA,sCAKA,wFAKA,iCAIA,0BAWA,iCACA,4BACA,mCAGA,+BAEA,sCACA,4BAEA,mCAEA,sCAKA,sDAIA,gCAEA,gEAQF,wCAME,sBACA,kCAKA,uBAEA,gEAIA,2BAIA,mCAEA,qCACA,iCAGE,+BACA,wEAEE,iCACA,kFAGF,6BACA,0CACF,kCAEE,8BACE,8BACA,qEAEE,sCACA,wFCnFN,iCAGF,2DAEE,4BACA,oCAGA,mIAGA,4HACE,gEAMJ,+CAGE,sBACA,yCAEF,uBAEE,sEAKA,gDACA,kEAGA,iFAGE,YAGF,EACA,4HAQF,mBACE,6BACA,mBACA,wCACA,wCACA,2CAIA,eAGA,mBAKE,mBAGA,CAJA,uCACA,iBAFF,gBACE,CAKE,mBACA,mBAGJ,oBAIF,+BAGE,kDACA,OADA,kBAGA,CAFA,gBAEA,mBACA,oBAEA,sCACA,OAGF,cAHE,WAGF,GAEE,oBACA,CAHF,gBAGE,CC9Gc,YDiHd,+CAIF,SAEE,CAPF,UACE,wBAMA,4BAEA,GAGA,uBACA,CAJA,yBAGA,CACA,iDAKA,2CAGA,2DAQA,iBACA,uCAGA,kEAKE,SAKJ,8BACE,yDACA,2BAEA,oBACA,8BAEA,yDAEE,4BAEJ,uCACE,CACA,iEAGA,CAEA,wCACE,uBACA,kDAEA,0DAEE,CAJF,oBAIE,0GAWN,aACE,CAHA,YAGA,4HASA,+CAGF,sBACE,WACA,WAQA,4BAFF,0CAEE,CARA,qCAsBA,CAdA,iBAEA,kBACE,aADF,4BACE,WAMF,2BAGF,qCAEE,CAXE,UAWF,+BAGA,uBAEA,SAEA,0CAIE,CANF,qCAEA,CAIE,2DACE,gBAIN,+CAIA,CAEA,kDAKE,CAPF,8BAEA,CAOE,YACA,CAjBI,2BAGN,CAHM,WAcJ,UAGA,CAEA,2GAIF,iCAGE,8BAIA,qBACA,oBACF,uBAOI,0CAIA,CATF,6DAKE,CALF,sBASE,qCAKF,CACE,cACA,CAFF,sBAEE,CACA,+BAEA,qBAEE,WAKN,aACE,sCAGA,mBAEA,6BAMA,kCACA,CAJA,sBACA,aAEA,CAJA,eACA,MAIA,2FAEA,UAGA,YACA,sBACE,8BAEA,CALF,aACA,WAIE,OACA,oBAEF,uBACE,WAEF,YAFE,UAEF,eAgBA,kBACE,CAhBA,qDAQF,qCAGF,CAGI,YACF,CAJF,2BAGI,CAEA,eACA,qBAGA,mEAEA,qBACA,8BAIA,kBADF,kBACE,yBAEJ,oCAGI,qDAGA,CACA,8BAMF,oCACE,+CACF,gCAIA,YACE,yBAGA,2BAGA,mCAFA,cAEA,CAHA,YACA,CAEA,4BAIE,qCACA,cAFA,4BAEA,wCACE,CADF,aACE,sBAEA,mDAEN,CAFM,YAEN,iDAEE,uCAKA,+DAIA,kBAIA,CAJA,sBAIA,mBACA,0BACF,yBAEE,YAEJ,CAFI,YAQJ,UAFI,kBAEJ,CAJE,gBAEE,CAJJ,iBAMA,yFAOI,aEjbJ,eACE,cACA,iBAEA,aAFA,iBAEA,6BAEA,kCACA,mBAKA,gCAGA,CARA,QAEA,CAGA,UALA,qBAEA,qDAGA,CALA,OAQA,4BACE,cAGF,2BACE,gCAEJ,CAHE,UAGF,8CAGE,CAHF,UAGE,wCAGA,qBACA,CAFA,UAEA,6CAGA,yCAIA,sBAHA,UAGA,kCACE,OACA,CADA,KACA,cASA,2CAFF,kBACA,CACE,wEACA,CARA,YACA,CAKF,mBAFF,MACE,CAIE,gBAJF,iCADF,eALI,oBACA,CAKF,SAIE,2BADA,UACA,kBAEJ,WACE,kDACA,mBACE,kDACA,0EACA,uDAKJ,aACE,mDAII,CAJJ,6CAII,2BACA,uCACE,kEACA,+CACE,aACA,WADA,oBACA,CADA,UACA,4FALJ,4BAEE,mBADF,0CACE,CAFF,eACA,MACE,0DACA,wCACE,sGACA,WANN,yBACE,uCACA,CAFF,UAEE,2CACE,wFACA,cACE,kEACA,mEANN,yBACE,4DACA,sBACE,+EAEE,iEACA,qEANN,sCACE,CAGE,iBAHF,gBAGE,qBACE,CAJJ,uBACA,gDACE,wDACA,6DAHF,2CACA,CADA,gBACA,eACE,CAGE,sBANN,8BACE,CAII,iBAFF,4DACA,WACE,YADF,uCACE,6EACA,2BANN,8CACE,kDACA,0CACE,8BACA,yFACE,sBACA,sFALJ,mEACA,sBACE,kEACA,6EACE,uCACA,kEALJ,qGAEE,kEACA,6EACE,uCACA,kEALJ,8CACA,uDACE,sEACA,2EACE,sCACA,iEALJ,mGACA,qCACE,oDACA,0DACE,6GACA,gDAGR,yDCrEA,sEACE,CACA,6GACE,gEACF,iGAIF,wFACE,qDAGA,mGAEE,2CAEF,4FACE,gCACF,wGACE,8DAEE,6FAIA,iJAKN,6GACE,gDAKF,yDACA,qCAGA,6BACA,kBACA,qDAKA,oCAEA,+DAGA,2CAGE,oDAIA,oEAEE,qBAGJ,wDAEE,uCAEF,kEAGA,8CAEA,uDAIF,gEAIE,6BACA,gEAIA,+CACE,0EAIF,sDAEE,+DAGF,sCACA,8BACE,oCAEJ,wBACE,4FAEE,gBAEJ,yGAGI,kBAGJ,CCnHE,2MCFF,oBAGE,wGAKA,iCACE,CADF,wBACE,8GAQA,mBCjBJ,2GAIE,mBACA,6HAMA,YACE,mIAYF,eACA,CAHF,YAGE,4FAGE,8BAKF,uBAkBE,sCACA,CADA,qBAbA,wCAIA,CALF,8BACE,CADF,gBAKE,wCACA,CAOA,kDACA,CACA,kCAKF,6BAGA,4CACE,kDACA,eAGF,cACE,aACA,iBACA,yBACA,8BACA,WAGJ,2BACE,cAGA,+BACA,CAHA,eAGA,wCACA,YACA,iBACA,uEAGA,0BACA,2CAEA,8EAGI,qBACA,CAFF,kBAEE,kBAGN,0CAGE,mCAGA,4BAIA,gEACE,qCACA,8BAEA,gBACA,+CACA,iCAEF,iCAEE,gEACA,qCAGF,8BAEE,+BAIA,yCAEE,qBADA,gBACA,yBAKF,eACA,CAFF,YACE,CACA,iBACA,qDAEA,mDCvIJ,2FAOE,iCACA,CAEA,eACA,CAHA,kBAEA,CAFA,wBAGA,8BACA,eACE,CAFF,YAEE,0BACA,8CAGA,oBACE,oCAGA,kBACE,8DAEA,iBAEN,UACE,8BAIJ,+CAEE,qDAEF,kDAIE,YAEF,CAFE,YAEF,CCpCE,mFADA,kBAKE,CAJF,IAGA,aACE,mCAGA,iDACE,+BAEJ,wBAEE,mBAMA,6CAEF,CAJE,mBAEA,CAEF,kCAGE,CARF,kBACE,CAHA,eAUA,YACA,mBACA,CADA,UACA,wCC9BF,oBDkCE,wBCnCJ,uCACE,+BACA,+DACA,sBAGA,qBCDA,6CAIE,CAPF,uBAGA,CDGE,oBACF,yDAEE,CCDE,2CAGF,CAJA,kCACE,CDJJ,YACE,CAIA,eCTF,CDKE,uBCMA,gCACE,YAEF,oCAEE,wBACA,0BAIF,iBAEA,cADF,UACE,uBAEA,iCAEA,wCAEA,6CAMA,CAYF,gCATI,4BASJ,CAZE,mCAEE,iCAUJ,4BAGE,4DADA,+BACA,CAHF,qBAGE,sCACE,OAEF,iBAHA,SAGA,iHACE,2DAKF,CANA,8EAMA,uSAEE,kBAEF,+FACE,yCCjEJ,WACA,yBAGA,uBACA,gBAEA,uCAIA,CAJA,iCAIA,uCAGA,UACE,gBACA,qBAEA,0CClBJ,gBACE,KAGF,qBACE,YAGF,CAHE,cAGF,gCAEE,mBACA,iEAEA,oCACA,wCAEA,sBACA,WAEA,CAFA,YAEA,8EAEA,mCAFA,iBAEA,6BAIA,wEAKA,sDAIE,CARF,mDAIA,CAIE,cAEF,8CAIA,oBAFE,iBAEF,8CAGE,eAEF,CAFE,YAEF,OAEE,kBAGJ,CAJI,eACA,CAFF,mBAKF,yCCjDE,oBACA,CAFA,iBAEA,uCAKE,iBACA,qCAGA,mBCZJ,CDWI,gBCXJ,6BAEE,eACA,sBAGA,eAEA,sBACA,oDACA,iGAMA,gBAFE,YAEF,8FAME,iJClBF,YACA,gNAUE,6BAEF,oTAcI,kBACF,gHAIA,qBACE,eACF,qDACE,kBACF,6DACE,4BCxCJ,oBAEF,qCAEI,+CAGF,uBACE,uDAGJ,oBAiBI,kDACF,CAhBA,+CAaA,CAbA,oBAaA,0FAEE,CAFF,gGAdA,cACA,iBAaA,0BAGA,mQAIA,oNAEE,iBAGJ,CAHI,gBAFF,gBAKF,8CAYI,CAZJ,wCAYI,sVACE,iCAGA,uEAHA,QAGA,qXAKJ,iDAGF,CARM,+CACE,iDAIN,CALI,gBAQN,mHACE,gBAGF,2DACE,0EAOA,0EAGF,gBAEE,6DC/EA,kDACA,gCACA,qDAGA,qBACA,qDCFA,cACA,eAEA,yBAGF,sBAEE,iBACA,sNAWA,iBACE,kBACA,wRAgBA,kBAEA,iOAgBA,uCACE,uEAEA,kBAEF,qUAuBE,iDAIJ,CACA,geCxFF,4BAEE,CAQA,6JACA,iDAIA,sEAGA,mDAOF,iDAGE,4DAIA,8CACA,qDAEE,eAFF,cAEE,oBAEF,uBAFE,kCAGA,eACA,iBACA,mBAIA,mDACA,CAHA,uCAEA,CAJA,0CACA,CAIA,gBAJA,gBACA,oBADA,gBAIA,wBAEJ,gBAGE,6BACA,YAHA,iBAGA,gCACA,iEAEA,6CACA,sDACA,0BADA,wBACA,0BACA,oIAIA,mBAFA,YAEA,qBACA,0CAIE,uBAEF,CAHA,yBACE,CAEF,iDACE,mFAKJ,oCACE,CANE,aAKJ,CACE,qEAIA,YAFA,WAEA,CAHA,aACA,CAEA,gBACE,4BACA,sBADA,aACA,gCAMF,oCACA,yDACA,2CAEA,qBAGE,kBAEA,CACA,mCAIF,CARE,YACA,CAOF,iCAEE,CAPA,oBACA,CAQA,oBACE,uDAEJ,sDAGA,CAHA,cAGA,0BACE,oDAIA,oCACA,4BACA,sBAGA,cAEA,oFAGA,sBAEA,yDACE,CAIF,iBAJE,wBAIF,6CAHE,6CAKA,eACA,aACA,CADA,cACA,yCAGJ,kBACE,CAKA,iDAEA,CARF,aACE,4CAGA,kBAIA,wEAGA,wDAGA,kCAOA,iDAGA,CAPF,WAEE,sCAEA,CAJF,2CACE,CAMA,qCACA,+BARF,kBACE,qCAOA,iBAsBA,sBACE,CAvBF,WAKA,CACE,0DAIF,CALA,uDACE,CANF,sBAqBA,4CACA,CALA,gRAIA,YAEE,6CAEN,mCAEE,+CASA,6EAIA,4BChNA,SDmNA,qFCnNA,gDACA,sCAGA,qCACA,sDACA,CAKA,kDAGA,CARA,0CAQA,kBAGA,YACA,sBACA,iBAFA,gBADF,YACE,CAHA,SAKA,kBAEA,SAFA,iBAEA,uEAGA,CAEE,6CAFF,oCAgBI,CAdF,yBACE,qBACF,CAGF,oBACE,CAIF,WACE,CALA,2CAGA,uBACF,CACE,mFAGE,CALF,qBAEA,UAGE,gCAIF,sDAEA,CALE,oCAKF,yCC7CJ,oCACE,CD+CA,yXAQE,sCCrDJ,wCAGA,oCACE","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/_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 color: var(--color-foreground-primary)\n background: var(--color-background-primary)\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-light\n display: block\n\n @media (prefers-color-scheme: dark)\n .theme-toggle svg.theme-icon-when-auto-dark\n display: block\n .theme-toggle svg.theme-icon-when-auto-light\n display: none\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 --font-stack--headings: var(--font-stack);\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: #6b6f76; // 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: #0a4bff;\n --color-brand-content: #2757dd;\n --color-brand-visited: #872ee0;\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\n --color-api-added: #21632c;\n --color-api-added-border: #38a84d;\n --color-api-changed: #046172;\n --color-api-changed-border: #06a1bc;\n --color-api-deprecated: #605706;\n --color-api-deprecated-border: #f0d90f;\n --color-api-removed: #b30000;\n --color-api-removed-border: #ff5c5c;\n\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-underline: var(--color-background-border);\n --color-link--hover: var(--color-brand-content);\n --color-link-underline--hover: var(--color-foreground-border);\n\n --color-link--visited: var(--color-brand-visited);\n --color-link-underline--visited: var(--color-background-border);\n --color-link--visited--hover: var(--color-brand-visited);\n --color-link-underline--visited--hover: var(--color-foreground-border);\n}\n\n@mixin colors-dark {\n --color-problematic: #ee5151;\n\n // Base Colors\n --color-foreground-primary: #cfd0d0; // 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: #3d94ff;\n --color-brand-content: #5ca5ff;\n --color-brand-visited: #b27aeb;\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 --color-api-added: #3db854;\n --color-api-added-border: #267334;\n --color-api-changed: #09b0ce;\n --color-api-changed-border: #056d80;\n --color-api-deprecated: #b1a10b;\n --color-api-deprecated-border: #6e6407;\n --color-api-removed: #ff7575;\n --color-api-removed-border: #b03b3b;\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-family: var(--font-stack--headings)\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 &:visited\n color: var(--color-link--visited)\n text-decoration-color: var(--color-link-underline--visited)\n &:hover\n color: var(--color-link--visited--hover)\n text-decoration-color: var(--color-link-underline--visited--hover)\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--hover)\n text-decoration-color: var(--color-link-underline--hover)\n &:visited\n color: var(--color-link--visited--hover)\n text-decoration-color: var(--color-link-underline--visited--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\n.skip-to-content\n position: fixed\n padding: 1rem\n border-radius: 1rem\n left: 0.25rem\n top: 0.25rem\n z-index: 40\n background: var(--color-background-primary)\n color: var(--color-foreground-primary)\n\n transform: translateY(-200%)\n transition: transform 300ms ease-in-out\n\n &:focus-within\n transform: translateY(0%)\n\narticle\n color: var(--color-content-foreground)\n background: var(--color-content-background)\n overflow-wrap: break-word\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 display: flex\n\n.theme-toggle\n display: flex\n cursor: pointer\n border: none\n padding: 0\n background: transparent\n\n.theme-toggle svg\n height: 1.25rem\n width: 1.25rem\n color: var(--color-foreground-primary)\n display: none\n\n.theme-toggle-header\n display: flex\n align-items: center\n justify-content: center\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: 1.5rem\n width: 1.5rem\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, .view-this-page\n svg\n color: inherit\n height: 1.25rem\n width: 1.25rem\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, .theme-toggle-header\n display: flex\n .toc-content-icon, .theme-toggle-content\n display: none\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.5rem\n width: 1.5rem\n\n // Add a scroll margin for the content\n :target\n scroll-margin-top: calc(var(--header-height) + 2.5rem)\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\n.admonition p.admonition-title,\np.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: 4.25rem\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\ndiv.versionadded,\ndiv.versionchanged,\ndiv.deprecated,\ndiv.versionremoved\n border-left: 0.1875rem solid\n border-radius: 0.125rem\n\n padding-left: 0.75rem\n\n p\n margin-top: 0.125rem\n margin-bottom: 0.125rem\n\ndiv.versionadded\n border-color: var(--color-api-added-border)\n .versionmodified\n color: var(--color-api-added)\n\ndiv.versionchanged\n border-color: var(--color-api-changed-border)\n .versionmodified\n color: var(--color-api-changed)\n\ndiv.deprecated\n border-color: var(--color-api-deprecated-border)\n .versionmodified\n color: var(--color-api-deprecated)\n\ndiv.versionremoved\n border-color: var(--color-api-removed-border)\n .versionmodified\n color: var(--color-api-removed)\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 pre.literal-block &\n font-size: inherit\n padding: 0\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 > *:not(span),\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: 2.5rem\n\n@media (max-width: $full-width - $sidebar-width)\n :target\n scroll-margin-top: calc(2.5rem + var(--header-height))\n\n // When a heading is selected\n section > span:target\n scroll-margin-top: calc(2.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","// 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 color: var(--color-sidebar-link-text)\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