URI:
       docs: regenerate html documentation - sphere - GPU-based 3D discrete element method algorithm with optional fluid coupling
  HTML git clone git://src.adamsgaard.dk/sphere
   DIR Log
   DIR Files
   DIR Refs
   DIR LICENSE
       ---
   DIR commit 8db43a4f44d5a40467db9fd613706de973506ece
   DIR parent 3cf967ad1b0c4a985fa1a5c1fc3d99755b775b99
  HTML Author: Anders Damsgaard <anders@adamsgaard.dk>
       Date:   Mon,  6 Jul 2026 09:20:27 +0000
       
       docs: regenerate html documentation
       
       Diffstat:
         M doc/html/.buildinfo                 |       4 ++--
         A doc/html/_sources/cfd.rst.txt       |     587 +++++++++++++++++++++++++++++++
         A doc/html/_sources/dem.rst.txt       |     217 +++++++++++++++++++++++++++++++
         A doc/html/_sources/index.rst.txt     |      37 +++++++++++++++++++++++++++++++
         A doc/html/_sources/introduction.rst… |      49 +++++++++++++++++++++++++++++++
         A doc/html/_sources/python_api.rst.t… |      41 +++++++++++++++++++++++++++++++
         A doc/html/_sources/quickstart.rst.t… |      77 +++++++++++++++++++++++++++++++
         A doc/html/_sources/sphere_internals… |      81 ++++++++++++++++++++++++++++++
         A doc/html/_static/base-stemmer.js    |     476 +++++++++++++++++++++++++++++++
         M doc/html/_static/basic.css          |     326 ++++++++++++++++++++++---------
         A doc/html/_static/classic.css        |     263 +++++++++++++++++++++++++++++++
         M doc/html/_static/doctools.js        |     382 +++++++++----------------------
         A doc/html/_static/documentation_opt… |      14 ++++++++++++++
         A doc/html/_static/english-stemmer.js |    1066 ++++++++++++++++++++++++++++++
         A doc/html/_static/language_data.js   |      13 +++++++++++++
         M doc/html/_static/pygments.css       |      74 +++++++++++++++++--------------
         M doc/html/_static/searchtools.js     |     949 ++++++++++++++++++-------------
         M doc/html/_static/sidebar.js         |     163 +++++++------------------------
         A doc/html/_static/sphinx_highlight.… |     159 +++++++++++++++++++++++++++++++
         M doc/html/cfd.html                   |     116 ++++++++++++++++---------------
         M doc/html/dem.html                   |      96 +++++++++++++++++--------------
         M doc/html/genindex.html              |      68 ++++++++++++++++++-------------
         M doc/html/index.html                 |     112 ++++++++++++++++++-------------
         M doc/html/introduction.html          |     307 ++++++++-----------------------
         M doc/html/objects.inv                |       0 
         M doc/html/py-modindex.html           |      45 ++++++++++++++++---------------
         M doc/html/python_api.html            |    1923 +++++++++++++++++--------------
         A doc/html/quickstart.html            |     200 +++++++++++++++++++++++++++++++
         M doc/html/search.html                |      65 +++++++++++++++++--------------
         M doc/html/searchindex.js             |       4 ++--
         M doc/html/sphere_internals.html      |     272 ++++++++++++-------------------
       
       31 files changed, 5762 insertions(+), 2424 deletions(-)
       ---
   DIR diff --git a/doc/html/.buildinfo b/doc/html/.buildinfo
       @@ -1,4 +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: c4e84a3d2c04280d43617251d14cbd96
       +# This file records the configuration used when building these files. When it is not found, a full rebuild will be done.
       +config: 0da0288272aac7495bdc94873a9cae20
        tags: 645f666f9bcd5a90fca523b33c5a78b7
   DIR diff --git a/doc/html/_sources/cfd.rst.txt b/doc/html/_sources/cfd.rst.txt
       @@ -0,0 +1,587 @@
       +Fluid simulation and particle-fluid interaction
       +===============================================
       +``sphere`` can simulate mixtures of particles and a Newtonian fluid. The fluid
       +solvers share the same source code across the CUDA and OpenMP CPU backends,
       +using an Eulerian continuum approach coupled to the DEM particles.
       +
       +The following sections will describe the theoretical background, as well as the
       +solution procedure and the numerical implementation.
       +
       +Derivation of the Navier Stokes equations with porosity
       +-------------------------------------------------------
       +Following the outline presented by `Limache and Idelsohn (2006)`_, the
       +continuity equation for an incompressible fluid material is given by:
       +
       +.. math::
       +    \nabla \cdot \boldsymbol{v} = 0
       +
       +and the momentum equation:
       +
       +.. math::
       +    \rho \frac{\partial \boldsymbol{v}}{\partial t}
       +    + \rho (\boldsymbol{v} \cdot \nabla \boldsymbol{v})
       +    = \nabla \cdot \boldsymbol{\sigma}
       +    - \boldsymbol{f}^i
       +    + \rho \boldsymbol{g}
       +
       +Here, :math:`\boldsymbol{v}` is the fluid velocity, :math:`\rho` is the
       +fluid density, :math:`\boldsymbol{\sigma}` is the `Cauchy stress tensor`_,
       +:math:`\boldsymbol{f}^i` is the particle-fluid interaction vector and
       +:math:`\boldsymbol{g}` is the gravitational acceleration. For incompressible
       +Newtonian fluids, the Cauchy stress is given by:
       +
       +.. math::
       +    \boldsymbol{\sigma} = -p \boldsymbol{I} + \boldsymbol{\tau}
       +
       +:math:`p` is the fluid pressure, :math:`\boldsymbol{I}` is the identity
       +tensor, and :math:`\boldsymbol{\tau}` is the deviatoric stress tensor, given
       +by:
       +
       +.. math::
       +    \boldsymbol{\tau} =
       +    \mu_f \nabla \boldsymbol{v}
       +    + \mu_f (\nabla \boldsymbol{v})^T
       +
       +By using the following vector identities:
       +
       +.. math::
       +    \nabla \cdot (p \boldsymbol{I}) = \nabla p
       +
       +    \nabla \cdot (\nabla \boldsymbol{v}) = \nabla^2 \boldsymbol{v}
       +
       +    \nabla \cdot (\nabla \boldsymbol{v})^T
       +    = \nabla (\nabla \cdot \boldsymbol{v})
       +
       +the deviatoric component of the Cauchy stress tensor simplifies to the
       +following, assuming that spatial variations in the viscosity can be neglected:
       +
       +.. math::
       +    = -\nabla p
       +    + \mu_f \nabla^2 \boldsymbol{v}
       +
       +Since we are dealing with fluid flow in a porous medium, additional terms are
       +introduced to the equations for conservation of mass and momentum. In the
       +following, the equations are derived for the first spatial component. The
       +solution for the other components is trivial.
       +
       +The porosity value (in the saturated porous medium the volumetric fraction of
       +the fluid phase) denoted :math:`\phi` is incorporated in the continuity and
       +momentum equations. The continuity equation becomes:
       +
       +.. math::
       +    \frac{\partial \phi}{\partial t}
       +    + \nabla \cdot (\phi \boldsymbol{v}) = 0
       +
       +For the :math:`x` component, the Lagrangian formulation of the momentum equation
       +with a body force :math:`\boldsymbol{f}` becomes:
       +
       +.. math::
       +    \frac{D (\phi v_x)}{D t}
       +    = \frac{1}{\rho} \left[ \nabla \cdot (\phi \boldsymbol{\sigma}) \right]_x
       +    - \frac{1}{\rho} f^i_x
       +    + \phi g
       +
       +In the Eulerian formulation, an advection term is added, and the Cauchy stress
       +tensor is represented as isotropic and deviatoric components individually:
       +
       +.. math::
       +    \frac{\partial (\phi v_x)}{\partial t}
       +    + \boldsymbol{v} \cdot \nabla (\phi v_x)
       +    = \frac{1}{\rho} \left[ \nabla \cdot (-\phi p \boldsymbol{I})
       +    + \phi \boldsymbol{\tau}) \right]_x
       +    - \frac{1}{\rho} f^i_x
       +    + \phi g_x
       +
       +Using vector identities to rewrite the advection term, and expanding the fluid
       +stress tensor term:
       +
       +.. math::
       +    \frac{\partial (\phi v_x)}{\partial t}
       +    + \nabla \cdot (\phi v_x \boldsymbol{v})
       +    - \phi v_x (\nabla \cdot \boldsymbol{v})
       +    = \frac{1}{\rho} \left[ -\nabla \phi p \right]_x
       +    + \frac{1}{\rho} \left[ \nabla \cdot (\phi \boldsymbol{\tau}) \right]_x
       +    - \frac{1}{\rho} f^i_x
       +    + \phi g_x
       +
       +Spatial variations in the porosity are neglected,
       +
       +.. math::
       +    \nabla \phi := 0
       +
       +and the pressure is attributed to the fluid phase alone (model B in Zhu et al.
       +2007 and Zhou et al. 2010). The divergence of fluid velocities is defined to be
       +zero:
       +
       +.. math::
       +    \nabla \cdot \boldsymbol{v} := 0
       +
       +With these assumptions, the momentum equation simplifies to:
       +
       +.. math::
       +    \frac{\partial (\phi v_x)}{\partial t}
       +    + \nabla \cdot (\phi v_x \boldsymbol{v})
       +    = -\frac{1}{\rho} \frac{\partial p}{\partial x}
       +    + \frac{1}{\rho} \left[ \nabla \cdot (\phi \boldsymbol{\tau}) \right]_x
       +    - \frac{1}{\rho} f^i_x
       +    + \phi g_x
       +
       +The remaining part of the advection term is for the :math:`x` component
       +found as:
       +
       +.. math::
       +    \nabla \cdot (\phi v_x \boldsymbol{v}) =
       +    \left[
       +        \frac{\partial}{\partial x},
       +        \frac{\partial}{\partial y},
       +        \frac{\partial}{\partial z}
       +    \right]
       +    \left[
       +        \begin{array}{c}
       +            \phi v_x v_x\\
       +            \phi v_x v_y\\
       +            \phi v_x v_z\\
       +        \end{array}
       +    \right]
       +    =
       +    \frac{\partial (\phi v_x v_x)}{\partial x} +
       +    \frac{\partial (\phi v_x v_y)}{\partial y} +
       +    \frac{\partial (\phi v_x v_z)}{\partial z}
       +
       +The deviatoric stress tensor is in this case symmetrical, i.e. :math:`\tau_{ij}
       += \tau_{ji}`, and is found by:
       +
       +.. math::
       +    \frac{1}{\rho} \left[ \nabla \cdot (\phi \boldsymbol{\tau}) \right]_x
       +    = \frac{1}{\rho}
       +    \left[
       +        \left[
       +            \frac{\partial}{\partial x},
       +            \frac{\partial}{\partial y},
       +            \frac{\partial}{\partial z}
       +        \right]
       +        \phi
       +        \left[
       +            \begin{matrix}
       +                \tau_{xx} & \tau_{xy} & \tau_{xz}\\
       +                \tau_{yx} & \tau_{yy} & \tau_{yz}\\
       +                \tau_{zx} & \tau_{zy} & \tau_{zz}\\
       +            \end{matrix}
       +        \right]
       +    \right]_x
       +
       +    = \frac{1}{\rho}
       +    \left[
       +        \begin{array}{c}
       +            \frac{\partial (\phi \tau_{xx})}{\partial x}
       +            + \frac{\partial (\phi \tau_{xy})}{\partial y}
       +            + \frac{\partial (\phi \tau_{xz})}{\partial z}\\
       +            \frac{\partial (\phi \tau_{yx})}{\partial x}
       +            + \frac{\partial (\phi \tau_{yy})}{\partial y}
       +            + \frac{\partial (\phi \tau_{yz})}{\partial z}\\
       +            \frac{\partial (\phi \tau_{zx})}{\partial x}
       +            + \frac{\partial (\phi \tau_{zy})}{\partial y}
       +            + \frac{\partial (\phi \tau_{zz})}{\partial z}\\
       +        \end{array}
       +    \right]_x
       +    = \frac{1}{\rho}
       +    \left(
       +        \frac{\partial (\phi \tau_{xx})}{\partial x}
       +        + \frac{\partial (\phi \tau_{xy})}{\partial y}
       +        + \frac{\partial (\phi \tau_{xz})}{\partial z}
       +    \right)
       +
       +In a linear viscous fluid, the stress and strain rate
       +(:math:`\dot{\boldsymbol{\epsilon}}`) is linearly dependent, scaled by the
       +viscosity parameter :math:`\mu_f`:
       +
       +.. math::
       +    \tau_{ij} = 2 \mu_f \dot{\epsilon}_{ij}
       +    = \mu_f \left(
       +    \frac{\partial v_i}{\partial x_j} + \frac{\partial v_j}{\partial x_i}
       +    \right)
       +
       +With this relationship, the deviatoric stress tensor components can be
       +calculated as:
       +
       +.. math::
       +    \tau_{xx} = 2 \mu_f \frac{\partial v_x}{\partial x} \qquad
       +    \tau_{yy} = 2 \mu_f \frac{\partial v_y}{\partial y} \qquad
       +    \tau_{zz} = 2 \mu_f \frac{\partial v_z}{\partial z}
       +
       +    \tau_{xy} = \mu_f \left(
       +    \frac{\partial v_x}{\partial y} + \frac{\partial v_y}{\partial x} \right)
       +
       +    \tau_{xz} = \mu_f \left(
       +    \frac{\partial v_x}{\partial z} + \frac{\partial v_z}{\partial x} \right)
       +
       +    \tau_{yz} = \mu_f \left(
       +    \frac{\partial v_y}{\partial z} + \frac{\partial v_z}{\partial y} \right)
       +
       +where :math:`\mu_f` is the dynamic viscosity. The above formulation of the
       +fluid rheology assumes identical bulk and shear viscosities. The derivation of
       +the equations for the other spatial components is trivial.
       +
       +Porosity estimation
       +-------------------
       +The solid volume in each fluid cell is determined by the ratio of the
       +a cell-centered spherical cell volume (:math:`V_c`) and the sum of intersecting
       +particle volumes (:math:`V_s`). The spherical cell volume has a center at
       +:math:`\boldsymbol{x}_i`, and a radius of :math:`R_i`, which is equal to half
       +the fluid cell width. The nearby particles are characterized by position
       +:math:`\boldsymbol{x}_j` and radius :math:`r_j`. The center distance is defined
       +as:
       +
       +.. math::
       +    d_{ij} = ||\boldsymbol{x}_i - \boldsymbol{x}_j||
       +
       +The common volume of the two intersecting spheres is zero if the volumes aren't
       +intersecting, lens shaped if they are intersecting, and spherical if the
       +particle is fully contained by the spherical cell volume:
       +
       +.. math::
       +    V^s_{i} = \sum_j
       +    \begin{cases}
       +        0 & \textit{if } R_i + r_j \leq d_{ij} \\
       +        \frac{1}{12d_{ij}} \left[ \pi (R_i + r_j - d_{ij})^2
       +        (d_{ij}^2 + 2d_{ij}r_j - 3r_j^2 + 2d_{ij} R_i + 6r_j R_i - 3R_i^2)
       +        \right] & \textit{if } R_i - r_j < d_{ij} < R_i + r_j \\
       +        \frac{4}{3} \pi r^3_j & \textit{if } d_{ij} \leq R_i - r_j
       +    \end{cases}
       +
       +Using this method, the cell porosity values are continuous through time as
       +particles enter and exit the cell volume. The rate of porosity change
       +(:math:`d\phi/dt`) is estimated by the backwards Euler method
       +by considering the previous and current porosity.
       +
       +Particle-fluid interaction
       +--------------------------
       +The momentum exchange of the granular and fluid phases follows the procedure
       +outlined by Gidaspow 1992 and Shamy and Zhegal 2005. The fluid and particle
       +interaction is based on the concept of drag, where the magnitude is based on
       +semi-empirical relationships. The drag force scales linearly with the relative
       +difference in velocity between the fluid and particle phase. On the base of
       +Newton's third law, the resulting drag force is applied with opposite signs to
       +the particle and fluid.
       +
       +For fluid cells with porosities (:math:`\phi`) less or equal to 0.8, the drag
       +force is based on the Ergun (1952) equation:
       +
       +.. math::
       +    \bar{\boldsymbol{f}}_d = \left(
       +    150 \frac{\mu_f (1-\phi)^2}{\phi\bar{d}^2}
       +    + 1.75 \frac{(1-\phi)\rho_f
       +      ||\boldsymbol{v}_f - \bar{\boldsymbol{v}}_p||}{\bar{d}}
       +    \right)
       +    (\boldsymbol{v}_f - \bar{\boldsymbol{v}}_p)
       +
       +here, :math:`\bar{d}` denotes the average particle diameter in the cell,
       +:math:`\boldsymbol{v}_f` is the fluid flow velocity, and
       +:math:`\bar{\boldsymbol{v}}_p` is the average particle velocity in the cell. All
       +particles in contact with the previously mentioned cell-centered sphere for
       +porosity estimation contribute to the average particle velocity and diameter in
       +the fluid cell.
       +
       +If the porosity is greater than 0.8, the cell-averaged drag force
       +(:math:`\bar{\boldsymbol{f}}_d` is found from the Wen and Yu (1966) equation,
       +which considers the fluid flow situation:
       +
       +.. math::
       +    \bar{\boldsymbol{f}}_d = \left(
       +    \frac{3}{4}
       +    \frac{C_d (1-\phi) \phi^{-2.65} \mu_f \rho_f
       +    ||\boldsymbol{v}_f - \bar{\boldsymbol{v}}_p||}{\bar{d}}
       +    \right)
       +    (\boldsymbol{v}_f - \bar{\boldsymbol{v}}_p)
       +
       +The drag coefficient :math:`C_d` is evaluated depending on the magnitude of the
       +Reynolds number :math:`Re`:
       +
       +.. math::
       +    C_d =
       +    \begin{cases}
       +    \frac{24}{Re} (1+0.15 (Re)^{0.687} & \textit{if } Re < 1,000 \\
       +    0.44 & \textit{if } Re \geq 1,000
       +    \end{cases}
       +
       +where the Reynold's number is found by:
       +
       +.. math::
       +    Re = \frac{\phi\rho_f\bar{d}}{\mu_f}
       +    ||\boldsymbol{v}_f - \bar{\boldsymbol{v}}_p||
       +
       +The interaction force is applied to the fluid with negative sign as a
       +contribution to the body force :math:`\boldsymbol{f}`. The fluid interaction
       +force applied particles in the fluid cell is:
       +
       +.. math::
       +    \boldsymbol{f}_i = \frac{\bar{\boldsymbol{f}}_d V_p}{1-\phi}
       +
       +where :math:`V_p` denotes the particle volume. Optionally, the above
       +interaction force could be expanded to include the force induced by the fluid
       +pressure gradient:
       +
       +.. math::
       +    \boldsymbol{f}_i = \left(
       +    -\nabla p +
       +    \frac{\bar{\boldsymbol{f}}_d}{1-\phi}
       +    \right) V_p
       +
       +
       +Fluid dynamics solution procedure by operator splitting
       +-------------------------------------------------------
       +The partial differential terms in the previously described equations are found
       +using finite central differences. Modifying the operator splitting methodology
       +presented by Langtangen et al.  (2002), the predicted velocity
       +:math:`\boldsymbol{v}^*` after a finite time step
       +:math:`\Delta t` is found by explicit integration of the momentum equation.
       +
       +.. math::
       +    \frac{\Delta (\phi v_x)}{\Delta t}
       +    + \nabla \cdot (\phi v_x \boldsymbol{v})
       +    = - \frac{1}{\rho} \frac{\Delta p}{\Delta x}
       +    + \frac{1}{\rho} \left[ \nabla \cdot (\phi \boldsymbol{\tau}) \right]_x
       +    - \frac{1}{\rho} f^i_x
       +    + \phi g_x
       +
       +    \Downarrow
       +
       +    \phi \frac{\Delta v_x}{\Delta t}
       +    + v_x \frac{\Delta \phi}{\Delta t}
       +    + \nabla \cdot (\phi v_x \boldsymbol{v})
       +    = - \frac{1}{\rho} \frac{\Delta p}{\Delta x}
       +    + \frac{1}{\rho} \left[ \nabla \cdot (\phi \boldsymbol{\tau}) \right]_x
       +    - \frac{1}{\rho} f^i_x
       +    + \phi g_x
       +
       +We want to isolate :math:`\Delta v_x` in the above equation in order to project
       +the new velocity.
       +
       +.. math::
       +    \phi \frac{\Delta v_x}{\Delta t}
       +    = - \frac{1}{\rho} \frac{\Delta p}{\Delta x}
       +    + \frac{1}{\rho} \left[ \nabla \cdot (\phi \boldsymbol{\tau}) \right]_x
       +    - \frac{1}{\rho} f^i_x
       +    + \phi g_x
       +    - v_x \frac{\Delta \phi}{\Delta t}
       +    - \nabla \cdot (\phi v_x \boldsymbol{v})
       +
       +    \Delta v_x
       +    = - \frac{1}{\rho} \frac{\Delta p}{\Delta x} \frac{\Delta t}{\phi}
       +    + \frac{1}{\rho} \left[ \nabla \cdot (\phi \boldsymbol{\tau}) \right]_x
       +      \frac{\Delta t}{\phi}
       +    - \frac{\Delta t}{\rho\phi} f^i_x
       +    + \Delta t g_x
       +    - v_x \frac{\Delta \phi}{\phi}
       +    - \nabla \cdot (\phi v_x \boldsymbol{v}) \frac{\Delta t}{\phi}
       +
       +The term :math:`\beta` is introduced as an adjustable, dimensionless parameter
       +in the range :math:`[0;1]`, and determines the importance of the old pressure
       +values in the solution procedure (Langtangen et al. 2002).  A value of 0
       +corresponds to `Chorin's projection method`_ originally described
       +in `Chorin (1968)`_.
       +
       +.. math::
       +    v_x^* = v_x^t + \Delta v_x
       +
       +    v_x^* = v_x^t
       +    - \frac{\beta}{\rho} \frac{\Delta p^t}{\Delta x} \frac{\Delta t}{\phi^t}
       +    + \frac{1}{\rho} \left[ \nabla \cdot (\phi^t \boldsymbol{\tau}^t) \right]_x
       +      \frac{\Delta t}{\phi}
       +    - \frac{\Delta t}{\rho\phi} f^i_x
       +    + \Delta t g_x
       +    - v^t_x \frac{\Delta \phi}{\phi^t}
       +    - \nabla \cdot (\phi^t v_x^t \boldsymbol{v}^t) \frac{\Delta t}{\phi^t}
       +
       +Here, :math:`\Delta x` denotes the cell spacing. The velocity found
       +(:math:`v_x^*`) is only a prediction of the fluid velocity at time
       +:math:`t+\Delta t`, since the estimate isn't constrained by the continuity
       +equation:
       +
       +.. math::
       +    \frac{\Delta \phi^t}{\Delta t} + \nabla \cdot (\phi^t
       +    \boldsymbol{v}^{t+\Delta t}) = 0
       +
       +The divergence of a scalar and vector can be `split`_:
       +
       +.. math::
       +    \phi^t \nabla \cdot \boldsymbol{v}^{t+\Delta t} +
       +    \boldsymbol{v}^{t+\Delta t} \cdot \nabla \phi^t
       +    + \frac{\Delta \phi^t}{\Delta t} = 0
       +
       +The predicted velocity is corrected using the new pressure (Langtangen et al.
       +2002):
       +
       +.. math::
       +    \boldsymbol{v}^{t+\Delta t} = \boldsymbol{v}^*
       +    %- \frac{\Delta t}{\rho} \nabla \epsilon
       +    - \frac{\Delta t}{\rho \phi^t} \nabla \epsilon
       +    \quad \text{where} \quad
       +    \epsilon = p^{t+\Delta t} - \beta p^t
       +
       +The above formulation of the future velocity is put into the continuity
       +equation:
       +
       +.. math::
       +    \Rightarrow
       +    \phi^t \nabla \cdot
       +    \left( \boldsymbol{v}^* - \frac{\Delta t}{\rho \phi^t} \nabla \epsilon \right)
       +    +
       +    \left( \boldsymbol{v}^* - \frac{\Delta t}{\rho \phi^t} \nabla \epsilon \right)
       +    \cdot \nabla \phi^t + \frac{\Delta \phi^t}{\Delta t} = 0
       +
       +.. math::
       +    \Rightarrow
       +    \phi^t \nabla \cdot
       +    \boldsymbol{v}^* - \frac{\Delta t}{\rho \phi^t} \phi^t \nabla^2 \epsilon
       +    + \nabla \phi^t \cdot \boldsymbol{v}^*
       +    - \nabla \phi^t \cdot \nabla \epsilon \frac{\Delta t}{\rho \phi^t}
       +    + \frac{\Delta \phi^t}{\Delta t} = 0
       +
       +.. math::
       +    \Rightarrow
       +    \frac{\Delta t}{\rho} \nabla^2 \epsilon
       +    = \phi^t \nabla \cdot \boldsymbol{v}^*
       +    + \nabla \phi^t \cdot \boldsymbol{v}^*
       +    - \nabla \phi^t \cdot \nabla \epsilon \frac{\Delta t}{\rho \phi^t}
       +    + \frac{\Delta \phi^t}{\Delta t}
       +
       +The pressure difference in time becomes a `Poisson equation`_ with added terms:
       +
       +.. math::
       +    \Rightarrow
       +    \nabla^2 \epsilon
       +    = \frac{\nabla \cdot \boldsymbol{v}^* \phi^t \rho}{\Delta t}
       +    + \frac{\nabla \phi^t \cdot \boldsymbol{v}^* \rho}{\Delta t}
       +    - \frac{\nabla \phi^t \cdot \nabla \epsilon}{\phi^t}
       +    + \frac{\Delta \phi^t \rho}{\Delta t^2}
       +
       +The right hand side of the above equation is termed the *forcing function*
       +:math:`f`, which is decomposed into two terms, :math:`f_1` and :math:`f_2`:
       +
       +.. math::
       +    f_1 
       +    = \frac{\nabla \cdot \boldsymbol{v}^* \phi^t \rho}{\Delta t}
       +    + \frac{\nabla \phi^t \cdot \boldsymbol{v}^* \rho}{\Delta t}
       +    + \frac{\Delta \phi^t \rho}{\Delta t^2}
       +
       +    f_2 =
       +    \frac{\nabla \phi^t \cdot \nabla \epsilon}{\phi^t}
       +
       +
       +During the `Jacobi iterative solution procedure`_ :math:`f_1` remains constant,
       +while :math:`f_2` changes value. For this reason, :math:`f_1` is found only
       +during the first iteration, while :math:`f_2` is updated every time. The value
       +of the forcing function is found as:
       +
       +.. math::
       +    f = f_1 - f_2
       +
       +Using second-order finite difference approximations of the Laplace operator
       +second-order partial derivatives, the differential equations become a system of
       +equations that is solved using `iteratively`_ using Jacobi updates. The total
       +number of unknowns is :math:`(n_x - 1)(n_y - 1)(n_z - 1)`.
       +
       +The discrete Laplacian (approximation of the Laplace operator) can be obtained
       +by a finite-difference seven-point stencil in a three-dimensional, cubic
       +grid with cell spacing :math:`\Delta x, \Delta y, \Delta z`, considering the six
       +face neighbors:
       +
       +.. math::
       +    \nabla^2 \epsilon_{i_x,i_y,i_z}  \approx 
       +    \frac{\epsilon_{i_x-1,i_y,i_z} - 2 \epsilon_{i_x,i_y,i_z}
       +    + \epsilon_{i_x+1,i_y,i_z}}{\Delta x^2}
       +    + \frac{\epsilon_{i_x,i_y-1,i_z} - 2 \epsilon_{i_x,i_y,i_z}
       +    + \epsilon_{i_x,i_y+1,i_z}}{\Delta y^2}
       +
       +    + \frac{\epsilon_{i_x,i_y,i_z-1} - 2 \epsilon_{i_x,i_y,i_z}
       +    + \epsilon_{i_x,i_y,i_z+1}}{\Delta z^2}
       +    \approx f_{i_x,i_y,i_z}
       +
       +Within a Jacobi iteration, the value of the unknowns (:math:`\epsilon^n`) is
       +used to find an updated solution estimate (:math:`\epsilon^{n+1}`).
       +The solution for the updated value takes the form:
       +
       +.. math::
       +    \epsilon^{n+1}_{i_x,i_y,i_z}
       +    = \frac{-\Delta x^2 \Delta y^2 \Delta z^2 f_{i_x,i_y,i_z}
       +    + \Delta y^2 \Delta z^2 (\epsilon^n_{i_x-1,i_y,i_z} +
       +      \epsilon^n_{i_x+1,i_y,i_z})
       +    + \Delta x^2 \Delta z^2 (\epsilon^n_{i_x,i_y-1,i_z} +
       +      \epsilon^n_{i_x,i_y+1,i_z})
       +    + \Delta x^2 \Delta y^2 (\epsilon^n_{i_x,i_y,i_z-1} +
       +      \epsilon^n_{i_x,i_y,i_z+1})}
       +      {2 (\Delta x^2 \Delta y^2
       +      + \Delta x^2 \Delta z^2
       +      + \Delta y^2 \Delta z^2) }
       +
       +The difference between the current and updated value is termed the *normalized
       +residual*:
       +
       +.. math::
       +    r_{i_x,i_y,i_z} = \frac{(\epsilon^{n+1}_{i_x,i_y,i_z}
       +    - \epsilon^n_{i_x,i_y,i_z})^2}{(\epsilon^{n+1}_{i_x,i_y,i_z})^2}
       +
       +Note that the :math:`\epsilon` values cannot be 0 due to the above normalization
       +of the residual.
       +
       +The updated values are at the end of the iteration stored as the current values,
       +and the maximal value of the normalized residual is found. If this value is
       +larger than a tolerance criteria, the procedure is repeated. The iterative
       +procedure is ended if the number of iterations exceeds a defined limit. 
       +
       +After the values of :math:`\epsilon` are found, they are used to find the new
       +pressures and velocities:
       +
       +.. math::
       +    \bar{p}^{t+\Delta t} = \beta \bar{p}^t + \epsilon
       +
       +.. math::
       +    \bar{\boldsymbol{v}}^{t+\Delta t} =
       +    \bar{\boldsymbol{v}}^* - \frac{\Delta t}{\rho\phi} \nabla \epsilon
       +
       +
       +Boundary conditions
       +-------------------
       +The lateral boundaries are periodic. This cannot be changed in the current
       +version of ``sphere``. This means that the fluid properties at the paired,
       +parallel lateral (:math:`x` and :math:`y`) boundaries are identical. A flow
       +leaving through one side reappears on the opposite side.
       +
       +The top and bottom boundary conditions of the fluid grid can be either:
       +prescribed pressure (Dirichlet), or prescribed velocity (Neumann). The
       +(horizontal) velocities parallel to the boundaries are free to attain other
       +values (free slip). The Dirichlet boundary condition is enforced by keeping the
       +value of :math:`\epsilon` constant at the boundaries, e.g.:
       +
       +.. math::
       +   \epsilon^{n+1}_{i_x,i_y,i_z = 1 \vee n_z}
       +   =
       +   \epsilon^{n}_{i_x,i_y,i_z = 1 \vee n_z}
       +
       +The Neumann boundary condition of no flow across the boundary is enforced by
       +setting the gradient of :math:`\epsilon` perpendicular to the boundary to zero,
       +e.g.:
       +
       +.. math::
       +   \nabla_z \epsilon^{n+1}_{i_x,i_y,i_z = 1 \vee n_z} = 0
       +
       +
       +Numerical implementation
       +------------------------
       +Ghost nodes
       +
       +---
       +
       +
       +
       +
       +.. _Limache and Idelsohn (2006): http://www.cimec.org.ar/ojs/index.php/mc/article/view/486/464
       +.. _Cauchy stress tensor: https://en.wikipedia.org/wiki/Cauchy_stress_tensor
       +.. _`Chorin's projection method`: https://en.wikipedia.org/wiki/Projection_method_(fluid_dynamics)#Chorin.27s_projection_method
       +.. _`Chorin (1968)`: http://www.ams.org/journals/mcom/1968-22-104/S0025-5718-1968-0242392-2/S0025-5718-1968-0242392-2.pdf
       +.. _split: http://www.wolframalpha.com/input/?i=div(p+v)
       +.. _Poisson equation: https://en.wikipedia.org/wiki/Poisson's_equation
       +.. _`Jacobi iterative solution procedure`: http://www.rsmas.miami.edu/personal/miskandarani/Courses/MSC321/Projects/prjpoisson.pdf
       +.. _iteratively: https://en.wikipedia.org/wiki/Relaxation_(iterative_method)
       +
   DIR diff --git a/doc/html/_sources/dem.rst.txt b/doc/html/_sources/dem.rst.txt
       @@ -0,0 +1,217 @@
       +Discrete element method
       +=======================
       +Granular material is a very common form of matter, both in nature and industry.
       +It can be defined as material consisting of interacting, discrete particles.
       +Common granular materials include gravels, sands and soils, ice bergs,
       +asteroids, powders, seeds, and other foods. Over 75% of the raw materials that
       +pass through industry are granular. This wide occurrence has driven the desire
       +to understand the fundamental mechanics of the material.
       +
       +Contrary to other common materials such as gases, liquids and solids, a general
       +mathematical formulation of it's behavior hasn't yet been found. Granular
       +material can, however, display states that somewhat resemble gases, fluids and
       +solids.
       +
       +..  The discrete element method (or distinct element method) was initially
       +    formulated by Cundall and Strack (1979). It simulates the physical behavior and
       +    interaction of discrete, unbreakable particles, with their own mass and inertia,
       +    under the influence of e.g. gravity and boundary conditions such as moving
       +    walls. By discretizing time into small time steps, explicit integration of
       +    Newton's second law of motion is used to predict the new position and kinematic
       +    values for each particle from the previous sums of forces. This Lagrangian
       +    approach is ideal for simulating discontinuous materials, such as granular
       +    matter.
       +    The complexity of the computations is kept low by representing the particles as
       +    spheres, which keeps contact-searching algorithms simple.
       +
       +The `Discrete Element Method
       +<https://en.wikipedia.org/wiki/Discrete_element_method>`_ (DEM) is a numerical
       +method that can be used to
       +simulate the interaction of particles. Originally derived from
       +`Molecular Dynamics <https://en.wikipedia.org/wiki/Molecular_dynamics>`_,
       +it simulates particles as separate entities, and calculates their positions,
       +velocities, and accelerations through time. See Cundall and Strack (1979) and
       +`this blog post
       +<http://anders-dc.github.io/2013/10/16/the-discrete-element-method/>`_ for
       +general introduction to the DEM. The following sections will highlight the
       +DEM implementation in ``sphere``. Some of the details are also described in
       +Damsgaard et al. 2013. In the used notation, a bold symbol denotes a
       +three-dimensional vector, and a dot denotes that the entity is a temporal
       +derivative.
       +
       +Contact search
       +--------------
       +Homogeneous cubic grid.
       +
       +.. math::
       +   \delta_n^{ij} = ||\boldsymbol{x}^i - \boldsymbol{x}^j|| - (r^i + r^j)
       +
       +where :math:`r` is the particle radius, and :math:`\boldsymbol{x}` denotes the
       +positional vector of a particle, and :math:`i` and :math:`j` denote the indexes
       +of two particles. Negative values of :math:`\delta_n` denote that the particles
       +are overlapping.
       +
       +
       +Contact interaction
       +-------------------
       +Now that the inter-particle contacts have been identified and characterized by
       +their overlap, the resulting forces from the interaction can be resolved. The
       +interaction is decomposed into normal and tangential components, relative to the
       +contact interface orientation. The normal vector to the contact interface is
       +found by:
       +
       +.. math::
       +   \boldsymbol{n}^{ij} = 
       +   \frac{\boldsymbol{x}^i - \boldsymbol{x}^j}
       +   {||\boldsymbol{x}^i - \boldsymbol{x}^j||}
       +
       +The contact velocity :math:`\dot{\boldsymbol{\delta}}` is found by:
       +
       +.. math::
       +   \dot{\boldsymbol{\delta}}^{ij} =
       +   (\boldsymbol{x}^i - \boldsymbol{x}^j)
       +   + (r^i + \frac{\delta_n^{ij}}{2})
       +     (\boldsymbol{n}^{ij} \times \boldsymbol{\omega}^{i})
       +   + (r^j + \frac{\delta_n^{ij}}{2})
       +     (\boldsymbol{n}^{ij} \times \boldsymbol{\omega}^{j})
       +
       +The contact velocity is decomposed into normal and tangential components,
       +relative to the contact interface. The normal component is:
       +
       +.. math::
       +   \dot{\delta}^{ij}_n =
       +   -(\dot{\boldsymbol{\delta}}^{ij} \cdot \boldsymbol{n}^{ij})
       +
       +and the tangential velocity component is found as:
       +
       +.. math::
       +   \dot{\boldsymbol{\delta}}^{ij}_t =
       +   \dot{\boldsymbol{\delta}}^{ij}
       +   - \boldsymbol{n}^{ij}
       +     (\boldsymbol{n}^{ij} \cdot \dot{\boldsymbol{\delta}}^{ij})
       +
       +where :math:`\boldsymbol{\omega}` is the rotational velocity vector of a
       +particle. The total tangential displacement on the contact plane is found
       +incrementally:
       +
       +.. math::
       +   \boldsymbol{\delta}_{t,\text{uncorrected}}^{ij} =
       +   \int_0^{t_c} 
       +   \dot{\boldsymbol{\delta}}^{ij}_t \Delta t
       +
       +where :math:`t_c` is the duration of the contact and :math:`\Delta t` is the
       +computational time step length. The tangential contact interface displacement is
       +set to zero when a contact pair no longer overlaps. At each time step, the value
       +of :math:`\boldsymbol{\delta}_t` is corrected for rotation of the contact
       +interface:
       +
       +.. math::
       +   \boldsymbol{\delta}_t^{ij} = \boldsymbol{\delta}_{t,\text{uncorrected}}^{ij}
       +   - (\boldsymbol{n}
       +     (\boldsymbol{n} \cdot \boldsymbol{\delta}_{t,\text{uncorrected}}^{ij})
       +
       +With all the geometrical and kinetic components determined, the resulting forces
       +of the particle interaction can be determined using a contact model. ``sphere``
       +features only one contact model in the normal direction to the contact; the
       +linear-elastic-viscous (*Hookean* with viscous damping, or *Kelvin-Voigt*)
       +contact model. The resulting force in the normal direction of the contact
       +interface on particle :math:`i` is:
       +
       +.. math::
       +   \boldsymbol{f}_n^{ij} = \left(
       +   -k_n \delta_n^{ij} -\gamma_n \dot{\delta_n}^{ij}
       +   \right) \boldsymbol{n}^{ij}
       +
       +The parameter :math:`k_n` is the defined `spring coefficient
       +<https://en.wikipedia.org/wiki/Hooke's_law>`_ in the normal direction of the
       +contact interface, and :math:`\gamma_n` is the defined contact interface
       +viscosity, also in the normal direction. The loss of energy in this interaction
       +due to the viscous component is for particle :math:`i` calculated as:
       +
       +.. math::
       +    \dot{e}^i_v = \gamma_n (\dot{\delta}^{ij}_n)^2
       +
       +The tangential force is determined by either a viscous-frictional contact model,
       +or a elastic-viscous-frictional contact model. The former contact model is very
       +computationally efficient, but somewhat inaccurate relative to the mechanics of
       +real materials.  The latter contact model is therefore the default, even though
       +it results in longer computational times. The tangential force in the
       +visco-frictional contact model:
       +
       +.. math::
       +   \boldsymbol{f}_t^{ij} = -\gamma_t \dot{\boldsymbol{\delta}_t}^{ij}
       +
       +:math:`\gamma_n` is the defined contact interface viscosity in the tangential
       +direction. The tangential displacement along the contact interface
       +(:math:`\boldsymbol{\delta}_t`) is not calculated and stored for this contact
       +model. The tangential force in the more realistic elastic-viscous-frictional
       +contact model:
       +
       +.. math::
       +   \boldsymbol{f}_t^{ij} =
       +   -k_t \boldsymbol{\delta}_t^{ij} -\gamma_t \dot{\boldsymbol{\delta}_t}^{ij}
       +
       +The parameter :math:`k_n` is the defined spring coefficient in the tangential
       +direction of the contact interface. Note that the tangential force is only
       +found if the tangential displacement (:math:`\delta_t`) or the tangential
       +velocity (:math:`\dot{\delta}_t`) is non-zero, in order to avoid division by
       +zero. Otherwise it is defined as being :math:`[0,0,0]`.
       +
       +For both types of contact model, the tangential force is limited by the Coulomb
       +criterion of static and dynamic friction:
       +
       +.. math::
       +   ||\boldsymbol{f}^{ij}_t|| \leq
       +   \begin{cases}
       +   \mu_s ||\boldsymbol{f}^{ij}_n|| &
       +       \text{if} \quad ||\boldsymbol{f}_t^{ij}|| = 0 \\
       +   \mu_d ||\boldsymbol{f}^{ij}_n|| &
       +       \text{if} \quad ||\boldsymbol{f}_t^{ij}|| > 0
       +   \end{cases}
       +
       +If the elastic-viscous-frictional contact model is used and the Coulomb limit is
       +reached, the tangential displacement along the contact interface is limited to
       +this value:
       +
       +.. math::
       +   \boldsymbol{\delta}_t^{ij} =
       +   \frac{1}{k_t} \left(
       +   \mu_d ||\boldsymbol{f}_n^{ij}||
       +   \frac{\boldsymbol{f}^{ij}_t}{||\boldsymbol{f}^{ij}_t||}
       +   + \gamma_t \dot{\boldsymbol{\delta}}_t^{ij} \right)
       +
       +If the tangential force reaches the Coulomb limit, the energy lost due to
       +frictional dissipation is calculated as:
       +
       +.. math::
       +   \dot{e}^i_s = \frac{||\boldsymbol{f}^{ij}_t
       +   \dot{\boldsymbol{\delta}}_t^{ij} \Delta t||}{\Delta t}
       +
       +The loss of energy by viscous dissipation in the tangential direction is not
       +found.
       +
       +
       +Temporal integration
       +--------------------
       +In the DEM, the time is discretized into small steps (:math:`\Delta t`). For each time
       +step, the entire network of contacts is resolved, and the resulting forces and
       +torques for each particle are found. With these values at hand, the new
       +linear and rotational accelerations can be found using
       +`Newton's second law <https://en.wikipedia.org/wiki/Newton%27s_laws_of_motion>`_
       +of the motion of solid bodies. If a particle with mass :math:`m` at a point in time
       +experiences a sum of forces denoted :math:`\boldsymbol{F}`, the resultant acceleration
       +(:math:`\boldsymbol{a}`) can be found by rearranging Newton's second law:
       +
       +.. math::
       +   \boldsymbol{F} = m \boldsymbol{a} \Rightarrow \boldsymbol{a} = \frac{\boldsymbol{F}}{m}
       +
       +The new velocity and position is found by integrating the above equation
       +with regards to time. The simplest integration scheme in this regard is the 
       +`Euler method <https://en.wikipedia.org/wiki/Euler_method>`_:
       +
       +.. math::
       +   \boldsymbol{v} = \boldsymbol{v}_{old} + \boldsymbol{a} \Delta t
       +
       +.. math::
       +   \boldsymbol{p} = \boldsymbol{p}_{old} + \boldsymbol{v} \Delta t
       +
   DIR diff --git a/doc/html/_sources/index.rst.txt b/doc/html/_sources/index.rst.txt
       @@ -0,0 +1,37 @@
       +The sphere documentation
       +========================
       +
       +``sphere`` is a 3D DEM particle simulator with optional coupled fluid solvers.
       +The same source tree can be built for CUDA GPUs or OpenMP CPUs.
       +
       +Start with :doc:`quickstart` for installation and build commands, then use the
       +model and API chapters for setup, execution, and analysis details.
       +
       +``sphere`` is developed by Anders Damsgaard as part as his Ph.D. project, under
       +supervision of David Lundbek Egholm and Jan A. Piotrowski, all of the Department
       +of Geoscience, Aarhus University, Denmark. The author welcomes interested third
       +party developers. This document is a work in progress.
       +
       +Contact: Anders Damsgaard, `<https://adamsgaard.dk>`_,
       +`<anders.damsgaard@geo.au.dk>`_
       +
       +Contents
       +--------
       +
       +.. toctree::
       +   :maxdepth: 2
       +
       +   quickstart
       +   introduction
       +   dem
       +   cfd
       +   python_api
       +   sphere_internals
       +
       +Indices and tables
       +------------------
       +
       +.. * :ref:`modindex`
       +
       +* :ref:`genindex`
       +* :ref:`search`
   DIR diff --git a/doc/html/_sources/introduction.rst.txt b/doc/html/_sources/introduction.rst.txt
       @@ -0,0 +1,49 @@
       +Introduction
       +============
       +
       +``sphere`` is used for three-dimensional DEM particle simulations with optional
       +two-way coupled fluid flow. Simulation setup and post-processing are usually
       +performed through the bundled Python API, while the compiled ``sphere`` binary
       +executes the time integration.
       +
       +The source code can be built for either a CUDA GPU or an OpenMP CPU. See
       +:doc:`quickstart` for current build requirements and commands.
       +
       +The ultimate aim of the ``sphere`` software is to simulate soft-bedded subglacial
       +conditions, while retaining the flexibility to perform simulations of granular
       +material in other environments.
       +
       +The purpose of this documentation is to provide the user with a walk-through of
       +the workflow, data-analysis and visualization methods of ``sphere``. In
       +addition, the ``sphere`` internals are exposed to provide a way to understand the
       +numerical routines used by the discrete element method implementation.
       +
       +.. note:: Command examples in this document starting with the symbol ``$`` are
       +   meant to be executed in the shell of the operating system, and ``>>>`` means
       +   execution in Python. `IPython <http://ipython.org>`_ is an excellent,
       +   interactive Python shell.
       +
       +All numerical values in this document, the source code, and the configuration
       +files are typeset with strict respect to the SI unit system.
       +
       +Workflow
       +--------
       +
       +A typical simulation workflow is:
       +
       +#. Create a ``sphere.sim`` object in Python and write an input binary.
       +#. Run ``./sphere`` on the input file, optionally with ``--fluid``.
       +#. Read output binaries in Python for analysis, visualization, or export.
       +
       +Updating sphere
       +---------------
       +
       +To update your local version, type the following commands in the ``sphere`` root
       +directory::
       +
       +   git pull
       +   cmake -DSPHERE_GPU=OFF .
       +   make
       +
       +Use the CUDA default build by omitting ``-DSPHERE_GPU=OFF`` when CUDA is the
       +intended backend.
   DIR diff --git a/doc/html/_sources/python_api.rst.txt b/doc/html/_sources/python_api.rst.txt
       @@ -0,0 +1,41 @@
       +Python API
       +==========
       +The Python module ``sphere`` is intended as the main interface to the ``sphere``
       +application. It is recommended to use this module for simulation setup,
       +simulation execution, and analysis of the simulation output data.
       +
       +Using the module
       +----------------
       +
       +The package lives in ``python/sphere/``. Use it from the repository root with
       +``PYTHONPATH=python``.
       +
       +.. code-block:: sh
       +
       +   PYTHONPATH=python python3 experiments/collision.py
       +
       +Most workflows follow this pattern: create :class:`sphere.sim`, initialize
       +particles and parameters, call :meth:`sphere.sim.writebin`, run the compiled
       +binary with :meth:`sphere.sim.run` or ``./sphere``, then read outputs with
       +:meth:`sphere.sim.readlast` or related helpers.
       +
       +Sample usage
       +------------
       +Below is a simple, annotated example of how to setup, execute, and post-process
       +a ``sphere`` simulation.  The example is also found in the ``experiments/``
       +folder as ``collision.py``.
       +
       +.. literalinclude:: ../../experiments/collision.py
       +   :language: python
       +   :linenos:
       +
       +The full documentation of the ``sphere`` Python API can be found below.
       +
       +
       +The ``sphere`` module
       +---------------------
       +.. automodule:: sphere
       +   :members:
       +   :imported-members:
       +   :inherited-members:
       +
   DIR diff --git a/doc/html/_sources/quickstart.rst.txt b/doc/html/_sources/quickstart.rst.txt
       @@ -0,0 +1,77 @@
       +Quick start
       +===========
       +
       +Requirements
       +------------
       +
       +Common requirements:
       +
       +* CMake 3.12 or newer
       +* A C++ compiler supported by the selected backend
       +* Python 3 with NumPy for simulation setup and analysis
       +
       +CUDA backend requirements:
       +
       +* Nvidia CUDA toolkit
       +* Nvidia GPU with suitable double-precision support
       +
       +OpenMP CPU backend requirements:
       +
       +* OpenMP-capable compiler/runtime
       +* On macOS with Apple clang: ``brew install libomp``
       +
       +Build
       +-----
       +
       +CUDA backend (default)::
       +
       +   cmake .
       +   make
       +
       +OpenMP CPU backend::
       +
       +   cmake -DSPHERE_GPU=OFF .
       +   make
       +
       +Run checks
       +----------
       +
       +::
       +
       +   ./sphere --version
       +   ./sphere --help
       +   ctest --output-on-failure
       +
       +Some CFD tests may expose known solver stability limitations on specific
       +systems; review failing test output before relying on a solver configuration.
       +
       +Python workflow
       +---------------
       +
       +Use the bundled Python package by setting ``PYTHONPATH`` from the repository
       +root::
       +
       +   PYTHONPATH=python python3 experiments/collision.py
       +
       +Most workflows follow this pattern:
       +
       +#. Create a ``sphere.sim`` object in Python.
       +#. Initialize particles, material properties, boundaries, and time settings.
       +#. Write an input binary with ``writebin``.
       +#. Run the compiled ``sphere`` binary, directly or through the Python API.
       +#. Read output binaries for analysis, visualization, or export.
       +
       +Build documentation
       +-------------------
       +
       +Install Python documentation dependencies::
       +
       +   python3 -m pip install -r doc/requirements.txt
       +
       +Full HTML documentation also requires Doxygen for the C++ reference::
       +
       +   make -C doc/sphinx html
       +
       +PDF documentation additionally requires a working TeX installation::
       +
       +   make -C doc/sphinx latexpdf
   DIR diff --git a/doc/html/_sources/sphere_internals.rst.txt b/doc/html/_sources/sphere_internals.rst.txt
       @@ -0,0 +1,81 @@
       +Sphere internals
       +================
       +
       +Command-line interface
       +----------------------
       +
       +.. command-output:: ../../sphere --help
       +
       +The most common way to invoke ``sphere`` is through the Python API, for example
       +with :py:meth:`sphere.sim.run` or :py:meth:`sphere.sim.render`.
       +
       +Execution pipeline
       +------------------
       +
       +The usual execution path is:
       +
       +#. Parse command-line options in ``main.cpp``.
       +#. Read the binary input file into a ``DEM`` object.
       +#. Validate parameters unless ``--dont-check`` or render mode is selected.
       +#. Initialize the selected backend and transfer or copy simulation state into
       +   the backend arrays.
       +#. Repeatedly sort particles into grid cells, resolve contacts, optionally run
       +   the fluid solver, integrate particle and wall motion, and write output files
       +   at the configured interval.
       +#. Free backend and host memory before returning to the caller.
       +
       +Numerical algorithm
       +-------------------
       +
       +Each simulation step uses the current particle positions to rebuild the grid
       +cell index, sorts particles by cell, identifies neighbor contacts, evaluates
       +contact and body forces, and integrates translational and rotational degrees of
       +freedom. If a coupled fluid solver is enabled, the fluid state and
       +particle-fluid interaction forces are updated as part of the loop.
       +
       +The length of the computational time step, ``time.dt``, is calculated from the
       +particle mass and elastic stiffnesses:
       +
       +.. math::
       +
       +   \Delta t = 0.075 \min \left( m / \max(k_n, k_t) \right)
       +
       +where :math:`m` is the particle mass, and :math:`k_n` and :math:`k_t` are the
       +normal and tangential elastic stiffnesses. The relationship resolves elastic
       +waves several times while they travel through the smallest particle.
       +
       +Memory layout
       +-------------
       +
       +Host arrays store input/output state and backend arrays use the ``dev_`` prefix
       +for the working state used by kernels or emulated kernels. Constant simulation
       +parameters use the ``devC_`` prefix.
       +
       +The internal floating-point precision is defined in ``datatypes.h``. Depending
       +on the build it can be either single precision or double precision. Input and
       +output data files are written in double precision and converted when necessary.
       +
       +Three-dimensional variables such as spatial vectors are stored as ``Float4``
       +arrays in backend memory to preserve the memory layout expected by both backend
       +implementations.
       +
       +Performance
       +-----------
       +
       +Runtime depends on the number of particles, contact density, selected backend,
       +thread or GPU configuration, output interval, and whether the fluid solver or
       +raytracer is enabled. Use release builds and tune ``OMP_NUM_THREADS`` when using
       +the OpenMP CPU backend.
       +
       +Compilation
       +-----------
       +
       +The project is built from the repository root with CMake. The default build uses
       +the CUDA backend. Pass ``-DSPHERE_GPU=OFF`` to build the OpenMP CPU backend. See
       +:doc:`quickstart` for current requirements and build commands.
       +
       +C++ reference
       +-------------
       +
       +.. doxygenclass:: DEM
       +   :members:
   DIR diff --git a/doc/html/_static/base-stemmer.js b/doc/html/_static/base-stemmer.js
       @@ -0,0 +1,476 @@
       +// @ts-check
       +
       +/**@constructor*/
       +BaseStemmer = function() {
       +    /** @protected */
       +    this.current = '';
       +    this.cursor = 0;
       +    this.limit = 0;
       +    this.limit_backward = 0;
       +    this.bra = 0;
       +    this.ket = 0;
       +
       +    /**
       +     * @param {string} value
       +     */
       +    this.setCurrent = function(value) {
       +        this.current = value;
       +        this.cursor = 0;
       +        this.limit = this.current.length;
       +        this.limit_backward = 0;
       +        this.bra = this.cursor;
       +        this.ket = this.limit;
       +    };
       +
       +    /**
       +     * @return {string}
       +     */
       +    this.getCurrent = function() {
       +        return this.current;
       +    };
       +
       +    /**
       +     * @param {BaseStemmer} other
       +     */
       +    this.copy_from = function(other) {
       +        /** @protected */
       +        this.current          = other.current;
       +        this.cursor           = other.cursor;
       +        this.limit            = other.limit;
       +        this.limit_backward   = other.limit_backward;
       +        this.bra              = other.bra;
       +        this.ket              = other.ket;
       +    };
       +
       +    /**
       +     * @param {number[]} s
       +     * @param {number} min
       +     * @param {number} max
       +     * @return {boolean}
       +     */
       +    this.in_grouping = function(s, min, max) {
       +        /** @protected */
       +        if (this.cursor >= this.limit) return false;
       +        var ch = this.current.charCodeAt(this.cursor);
       +        if (ch > max || ch < min) return false;
       +        ch -= min;
       +        if ((s[ch >>> 3] & (0x1 << (ch & 0x7))) == 0) return false;
       +        this.cursor++;
       +        return true;
       +    };
       +
       +    /**
       +     * @param {number[]} s
       +     * @param {number} min
       +     * @param {number} max
       +     * @return {boolean}
       +     */
       +    this.go_in_grouping = function(s, min, max) {
       +        /** @protected */
       +        while (this.cursor < this.limit) {
       +            var ch = this.current.charCodeAt(this.cursor);
       +            if (ch > max || ch < min)
       +                return true;
       +            ch -= min;
       +            if ((s[ch >>> 3] & (0x1 << (ch & 0x7))) == 0)
       +                return true;
       +            this.cursor++;
       +        }
       +        return false;
       +    };
       +
       +    /**
       +     * @param {number[]} s
       +     * @param {number} min
       +     * @param {number} max
       +     * @return {boolean}
       +     */
       +    this.in_grouping_b = function(s, min, max) {
       +        /** @protected */
       +        if (this.cursor <= this.limit_backward) return false;
       +        var ch = this.current.charCodeAt(this.cursor - 1);
       +        if (ch > max || ch < min) return false;
       +        ch -= min;
       +        if ((s[ch >>> 3] & (0x1 << (ch & 0x7))) == 0) return false;
       +        this.cursor--;
       +        return true;
       +    };
       +
       +    /**
       +     * @param {number[]} s
       +     * @param {number} min
       +     * @param {number} max
       +     * @return {boolean}
       +     */
       +    this.go_in_grouping_b = function(s, min, max) {
       +        /** @protected */
       +        while (this.cursor > this.limit_backward) {
       +            var ch = this.current.charCodeAt(this.cursor - 1);
       +            if (ch > max || ch < min) return true;
       +            ch -= min;
       +            if ((s[ch >>> 3] & (0x1 << (ch & 0x7))) == 0) return true;
       +            this.cursor--;
       +        }
       +        return false;
       +    };
       +
       +    /**
       +     * @param {number[]} s
       +     * @param {number} min
       +     * @param {number} max
       +     * @return {boolean}
       +     */
       +    this.out_grouping = function(s, min, max) {
       +        /** @protected */
       +        if (this.cursor >= this.limit) return false;
       +        var ch = this.current.charCodeAt(this.cursor);
       +        if (ch > max || ch < min) {
       +            this.cursor++;
       +            return true;
       +        }
       +        ch -= min;
       +        if ((s[ch >>> 3] & (0X1 << (ch & 0x7))) == 0) {
       +            this.cursor++;
       +            return true;
       +        }
       +        return false;
       +    };
       +
       +    /**
       +     * @param {number[]} s
       +     * @param {number} min
       +     * @param {number} max
       +     * @return {boolean}
       +     */
       +    this.go_out_grouping = function(s, min, max) {
       +        /** @protected */
       +        while (this.cursor < this.limit) {
       +            var ch = this.current.charCodeAt(this.cursor);
       +            if (ch <= max && ch >= min) {
       +                ch -= min;
       +                if ((s[ch >>> 3] & (0X1 << (ch & 0x7))) != 0) {
       +                    return true;
       +                }
       +            }
       +            this.cursor++;
       +        }
       +        return false;
       +    };
       +
       +    /**
       +     * @param {number[]} s
       +     * @param {number} min
       +     * @param {number} max
       +     * @return {boolean}
       +     */
       +    this.out_grouping_b = function(s, min, max) {
       +        /** @protected */
       +        if (this.cursor <= this.limit_backward) return false;
       +        var ch = this.current.charCodeAt(this.cursor - 1);
       +        if (ch > max || ch < min) {
       +            this.cursor--;
       +            return true;
       +        }
       +        ch -= min;
       +        if ((s[ch >>> 3] & (0x1 << (ch & 0x7))) == 0) {
       +            this.cursor--;
       +            return true;
       +        }
       +        return false;
       +    };
       +
       +    /**
       +     * @param {number[]} s
       +     * @param {number} min
       +     * @param {number} max
       +     * @return {boolean}
       +     */
       +    this.go_out_grouping_b = function(s, min, max) {
       +        /** @protected */
       +        while (this.cursor > this.limit_backward) {
       +            var ch = this.current.charCodeAt(this.cursor - 1);
       +            if (ch <= max && ch >= min) {
       +                ch -= min;
       +                if ((s[ch >>> 3] & (0x1 << (ch & 0x7))) != 0) {
       +                    return true;
       +                }
       +            }
       +            this.cursor--;
       +        }
       +        return false;
       +    };
       +
       +    /**
       +     * @param {string} s
       +     * @return {boolean}
       +     */
       +    this.eq_s = function(s)
       +    {
       +        /** @protected */
       +        if (this.limit - this.cursor < s.length) return false;
       +        if (this.current.slice(this.cursor, this.cursor + s.length) != s)
       +        {
       +            return false;
       +        }
       +        this.cursor += s.length;
       +        return true;
       +    };
       +
       +    /**
       +     * @param {string} s
       +     * @return {boolean}
       +     */
       +    this.eq_s_b = function(s)
       +    {
       +        /** @protected */
       +        if (this.cursor - this.limit_backward < s.length) return false;
       +        if (this.current.slice(this.cursor - s.length, this.cursor) != s)
       +        {
       +            return false;
       +        }
       +        this.cursor -= s.length;
       +        return true;
       +    };
       +
       +    /**
       +     * @param {Among[]} v
       +     * @return {number}
       +     */
       +    this.find_among = function(v)
       +    {
       +        /** @protected */
       +        var i = 0;
       +        var j = v.length;
       +
       +        var c = this.cursor;
       +        var l = this.limit;
       +
       +        var common_i = 0;
       +        var common_j = 0;
       +
       +        var first_key_inspected = false;
       +
       +        while (true)
       +        {
       +            var k = i + ((j - i) >>> 1);
       +            var diff = 0;
       +            var common = common_i < common_j ? common_i : common_j; // smaller
       +            // w[0]: string, w[1]: substring_i, w[2]: result, w[3]: function (optional)
       +            var w = v[k];
       +            var i2;
       +            for (i2 = common; i2 < w[0].length; i2++)
       +            {
       +                if (c + common == l)
       +                {
       +                    diff = -1;
       +                    break;
       +                }
       +                diff = this.current.charCodeAt(c + common) - w[0].charCodeAt(i2);
       +                if (diff != 0) break;
       +                common++;
       +            }
       +            if (diff < 0)
       +            {
       +                j = k;
       +                common_j = common;
       +            }
       +            else
       +            {
       +                i = k;
       +                common_i = common;
       +            }
       +            if (j - i <= 1)
       +            {
       +                if (i > 0) break; // v->s has been inspected
       +                if (j == i) break; // only one item in v
       +
       +                // - but now we need to go round once more to get
       +                // v->s inspected. This looks messy, but is actually
       +                // the optimal approach.
       +
       +                if (first_key_inspected) break;
       +                first_key_inspected = true;
       +            }
       +        }
       +        do {
       +            var w = v[i];
       +            if (common_i >= w[0].length)
       +            {
       +                this.cursor = c + w[0].length;
       +                if (w.length < 4) return w[2];
       +                var res = w[3](this);
       +                this.cursor = c + w[0].length;
       +                if (res) return w[2];
       +            }
       +            i = w[1];
       +        } while (i >= 0);
       +        return 0;
       +    };
       +
       +    // find_among_b is for backwards processing. Same comments apply
       +    /**
       +     * @param {Among[]} v
       +     * @return {number}
       +     */
       +    this.find_among_b = function(v)
       +    {
       +        /** @protected */
       +        var i = 0;
       +        var j = v.length
       +
       +        var c = this.cursor;
       +        var lb = this.limit_backward;
       +
       +        var common_i = 0;
       +        var common_j = 0;
       +
       +        var first_key_inspected = false;
       +
       +        while (true)
       +        {
       +            var k = i + ((j - i) >> 1);
       +            var diff = 0;
       +            var common = common_i < common_j ? common_i : common_j;
       +            var w = v[k];
       +            var i2;
       +            for (i2 = w[0].length - 1 - common; i2 >= 0; i2--)
       +            {
       +                if (c - common == lb)
       +                {
       +                    diff = -1;
       +                    break;
       +                }
       +                diff = this.current.charCodeAt(c - 1 - common) - w[0].charCodeAt(i2);
       +                if (diff != 0) break;
       +                common++;
       +            }
       +            if (diff < 0)
       +            {
       +                j = k;
       +                common_j = common;
       +            }
       +            else
       +            {
       +                i = k;
       +                common_i = common;
       +            }
       +            if (j - i <= 1)
       +            {
       +                if (i > 0) break;
       +                if (j == i) break;
       +                if (first_key_inspected) break;
       +                first_key_inspected = true;
       +            }
       +        }
       +        do {
       +            var w = v[i];
       +            if (common_i >= w[0].length)
       +            {
       +                this.cursor = c - w[0].length;
       +                if (w.length < 4) return w[2];
       +                var res = w[3](this);
       +                this.cursor = c - w[0].length;
       +                if (res) return w[2];
       +            }
       +            i = w[1];
       +        } while (i >= 0);
       +        return 0;
       +    };
       +
       +    /* to replace chars between c_bra and c_ket in this.current by the
       +     * chars in s.
       +     */
       +    /**
       +     * @param {number} c_bra
       +     * @param {number} c_ket
       +     * @param {string} s
       +     * @return {number}
       +     */
       +    this.replace_s = function(c_bra, c_ket, s)
       +    {
       +        /** @protected */
       +        var adjustment = s.length - (c_ket - c_bra);
       +        this.current = this.current.slice(0, c_bra) + s + this.current.slice(c_ket);
       +        this.limit += adjustment;
       +        if (this.cursor >= c_ket) this.cursor += adjustment;
       +        else if (this.cursor > c_bra) this.cursor = c_bra;
       +        return adjustment;
       +    };
       +
       +    /**
       +     * @return {boolean}
       +     */
       +    this.slice_check = function()
       +    {
       +        /** @protected */
       +        if (this.bra < 0 ||
       +            this.bra > this.ket ||
       +            this.ket > this.limit ||
       +            this.limit > this.current.length)
       +        {
       +            return false;
       +        }
       +        return true;
       +    };
       +
       +    /**
       +     * @param {number} c_bra
       +     * @return {boolean}
       +     */
       +    this.slice_from = function(s)
       +    {
       +        /** @protected */
       +        var result = false;
       +        if (this.slice_check())
       +        {
       +            this.replace_s(this.bra, this.ket, s);
       +            result = true;
       +        }
       +        return result;
       +    };
       +
       +    /**
       +     * @return {boolean}
       +     */
       +    this.slice_del = function()
       +    {
       +        /** @protected */
       +        return this.slice_from("");
       +    };
       +
       +    /**
       +     * @param {number} c_bra
       +     * @param {number} c_ket
       +     * @param {string} s
       +     */
       +    this.insert = function(c_bra, c_ket, s)
       +    {
       +        /** @protected */
       +        var adjustment = this.replace_s(c_bra, c_ket, s);
       +        if (c_bra <= this.bra) this.bra += adjustment;
       +        if (c_bra <= this.ket) this.ket += adjustment;
       +    };
       +
       +    /**
       +     * @return {string}
       +     */
       +    this.slice_to = function()
       +    {
       +        /** @protected */
       +        var result = '';
       +        if (this.slice_check())
       +        {
       +            result = this.current.slice(this.bra, this.ket);
       +        }
       +        return result;
       +    };
       +
       +    /**
       +     * @return {string}
       +     */
       +    this.assign_to = function()
       +    {
       +        /** @protected */
       +        return this.current.slice(0, this.limit);
       +    };
       +};
   DIR diff --git a/doc/html/_static/basic.css b/doc/html/_static/basic.css
       @@ -1,12 +1,5 @@
        /*
       - * basic.css
       - * ~~~~~~~~~
       - *
         * Sphinx stylesheet -- basic theme.
       - *
       - * :copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS.
       - * :license: BSD, see LICENSE for details.
       - *
         */
        
        /* -- main layout ----------------------------------------------------------- */
       @@ -15,6 +8,12 @@ div.clearer {
            clear: both;
        }
        
       +div.section::after {
       +    display: block;
       +    content: '';
       +    clear: left;
       +}
       +
        /* -- relbar ---------------------------------------------------------------- */
        
        div.related {
       @@ -109,22 +108,18 @@ img {
        /* -- search page ----------------------------------------------------------- */
        
        ul.search {
       -    margin: 10px 0 0 20px;
       -    padding: 0;
       +    margin-top: 10px;
        }
        
        ul.search li {
       -    padding: 5px 0 5px 20px;
       -    background-image: url(file.png);
       -    background-repeat: no-repeat;
       -    background-position: 0 7px;
       +    padding: 5px 0;
        }
        
        ul.search li a {
            font-weight: bold;
        }
        
       -ul.search li div.context {
       +ul.search li p.context {
            color: #888;
            margin: 2px 0 0 30px;
            text-align: left;
       @@ -216,7 +211,7 @@ table.modindextable td {
        /* -- general body styles --------------------------------------------------- */
        
        div.body {
       -    min-width: 450px;
       +    min-width: 360px;
            max-width: 800px;
        }
        
       @@ -231,14 +226,8 @@ a.headerlink {
            visibility: hidden;
        }
        
       -a.brackets:before,
       -span.brackets > a:before{
       -    content: "[";
       -}
       -
       -a.brackets:after,
       -span.brackets > a:after {
       -    content: "]";
       +a:visited {
       +    color: #551A8B;
        }
        
        h1:hover > a.headerlink,
       @@ -271,25 +260,25 @@ p.rubric {
            font-weight: bold;
        }
        
       -img.align-left, .figure.align-left, object.align-left {
       +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, object.align-right {
       +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, object.align-center {
       +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 {
       +img.align-default, figure.align-default, .figure.align-default {
          display: block;
          margin-left: auto;
          margin-right: auto;
       @@ -313,24 +302,35 @@ img.align-default, .figure.align-default {
        
        /* -- sidebars -------------------------------------------------------------- */
        
       -div.sidebar {
       +div.sidebar,
       +aside.sidebar {
            margin: 0 0 0.5em 1em;
            border: 1px solid #ddb;
       -    padding: 7px 7px 0 7px;
       +    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 7px 0 7px;
       +    padding: 7px;
            margin: 10px 0 10px 0;
        }
        
       @@ -352,10 +352,6 @@ div.admonition dt {
            font-weight: bold;
        }
        
       -div.admonition dl {
       -    margin-bottom: 0;
       -}
       -
        p.admonition-title {
            margin: 0px 10px 5px 0px;
            font-weight: bold;
       @@ -366,9 +362,34 @@ div.body p.centered {
            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;
        }
       @@ -398,10 +419,6 @@ table.docutils td, table.docutils th {
            border-bottom: 1px solid #aaa;
        }
        
       -table.footnote td, table.footnote th {
       -    border: 0 !important;
       -}
       -
        th {
            text-align: left;
            padding-right: 5px;
       @@ -416,32 +433,34 @@ table.citation td {
            border-bottom: none;
        }
        
       -th > p:first-child,
       -td > p:first-child {
       +th > :first-child,
       +td > :first-child {
            margin-top: 0px;
        }
        
       -th > p:last-child,
       -td > p:last-child {
       +th > :last-child,
       +td > :last-child {
            margin-bottom: 0px;
        }
        
        /* -- figures --------------------------------------------------------------- */
        
       -div.figure {
       +div.figure, figure {
            margin: 0.5em;
            padding: 0.5em;
        }
        
       -div.figure p.caption {
       +div.figure p.caption, figcaption {
            padding: 0.3em;
        }
        
       -div.figure p.caption span.caption-number {
       +div.figure p.caption span.caption-number,
       +figcaption span.caption-number {
            font-style: italic;
        }
        
       -div.figure p.caption span.caption-text {
       +div.figure p.caption span.caption-text,
       +figcaption span.caption-text {
        }
        
        /* -- field list styles ----------------------------------------------------- */
       @@ -468,10 +487,71 @@ table.field-list td, table.field-list th {
        
        /* -- 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 ----------------------------------------------------- */
        
       @@ -495,26 +575,53 @@ ol.upperroman {
            list-style: upper-roman;
        }
        
       -li > p:first-child {
       +:not(li) > ol > li:first-child > :first-child,
       +:not(li) > ul > li:first-child > :first-child {
            margin-top: 0px;
        }
        
       -li > p:last-child {
       +:not(li) > ol > li:last-child > :last-child,
       +:not(li) > ul > li:last-child > :last-child {
            margin-bottom: 0px;
        }
        
       -dl.footnote > dt,
       -dl.citation > dt {
       -    float: left;
       +ol.simple ol p,
       +ol.simple ul p,
       +ul.simple ol p,
       +ul.simple ul p {
       +    margin-top: 0;
        }
        
       -dl.footnote > dd,
       -dl.citation > dd {
       -    margin-bottom: 0em;
       +ol.simple > li:not(:first-child) > p,
       +ul.simple > li:not(:first-child) > p {
       +    margin-top: 0;
        }
        
       -dl.footnote > dd:after,
       -dl.citation > dd:after {
       +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;
        }
       @@ -531,10 +638,6 @@ dl.field-list > dt {
            padding-right: 5px;
        }
        
       -dl.field-list > dt:after {
       -    content: ":";
       -}
       -
        dl.field-list > dd {
            padding-left: 0.5em;
            margin-top: 0em;
       @@ -546,7 +649,7 @@ dl {
            margin-bottom: 15px;
        }
        
       -dd > p:first-child {
       +dd > :first-child {
            margin-top: 0px;
        }
        
       @@ -560,6 +663,21 @@ dd {
            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;
        }
       @@ -573,14 +691,6 @@ dl.glossary dt {
            font-size: 1.1em;
        }
        
       -.optional {
       -    font-size: 1.3em;
       -}
       -
       -.sig-paren {
       -    font-size: larger;
       -}
       -
        .versionmodified {
            font-style: italic;
        }
       @@ -621,8 +731,9 @@ dl.glossary dt {
        
        .classifier:before {
            font-style: normal;
       -    margin: 0.5em;
       +    margin: 0 0.5em;
            content: ":";
       +    display: inline-block;
        }
        
        abbr, acronym {
       @@ -637,29 +748,69 @@ pre {
            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 {
       -    padding: 5px 0px;
            border: 0;
            background-color: transparent;
            color: #aaa;
        }
        
        table.highlighttable {
       -    margin-left: 0.5em;
       +    display: block;
       +}
       +
       +table.highlighttable tbody {
       +    display: block;
       +}
       +
       +table.highlighttable tr {
       +    display: flex;
        }
        
        table.highlighttable td {
       -    padding: 0 0.5em 0 0.5em;
       +    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;
        }
       @@ -668,8 +819,14 @@ div.code-block-caption code {
            background-color: transparent;
        }
        
       -div.code-block-caption + div > div.highlight > pre {
       -    margin-top: 0;
       +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 {
       @@ -681,21 +838,7 @@ div.code-block-caption span.caption-text {
        }
        
        div.literal-block-wrapper {
       -    padding: 1em 1em 0;
       -}
       -
       -div.literal-block-wrapper div.highlight {
       -    margin: 0;
       -}
       -
       -code.descname {
       -    background-color: transparent;
       -    font-weight: bold;
       -    font-size: 1.2em;
       -}
       -
       -code.descclassname {
       -    background-color: transparent;
       +    margin: 1em 0;
        }
        
        code.xref, a code {
       @@ -736,8 +879,7 @@ span.eqno {
        }
        
        span.eqno a.headerlink {
       -    position: relative;
       -    left: 0px;
       +    position: absolute;
            z-index: 1;
        }
        
   DIR diff --git a/doc/html/_static/classic.css b/doc/html/_static/classic.css
       @@ -0,0 +1,262 @@
       +/*
       + * Sphinx stylesheet -- classic theme.
       + */
       +
       +@import url("basic.css");
       +
       +/* -- page layout ----------------------------------------------------------- */
       +
       +html {
       +    /* CSS hack for macOS's scrollbar (see #1125) */
       +    background-color: #FFFFFF;
       +}
       +
       +body {
       +    font-family: sans-serif;
       +    font-size: 100%;
       +    background-color: #11303d;
       +    color: #000;
       +    margin: 0;
       +    padding: 0;
       +}
       +
       +div.document {
       +    display: flex;
       +    background-color: #1c4e63;
       +}
       +
       +div.documentwrapper {
       +    float: left;
       +    width: 100%;
       +}
       +
       +div.bodywrapper {
       +    margin: 0 0 0 230px;
       +}
       +
       +div.body {
       +    background-color: #ffffff;
       +    color: #000000;
       +    padding: 0 20px 30px 20px;
       +}
       +
       +div.footer {
       +    color: #ffffff;
       +    width: 100%;
       +    padding: 9px 0 9px 0;
       +    text-align: center;
       +    font-size: 75%;
       +}
       +
       +div.footer a {
       +    color: #ffffff;
       +    text-decoration: underline;
       +}
       +
       +div.related {
       +    background-color: #133f52;
       +    line-height: 30px;
       +    color: #ffffff;
       +}
       +
       +div.related a {
       +    color: #ffffff;
       +}
       +
       +div.sphinxsidebar {
       +}
       +
       +div.sphinxsidebar h3 {
       +    font-family: 'Trebuchet MS', sans-serif;
       +    color: #ffffff;
       +    font-size: 1.4em;
       +    font-weight: normal;
       +    margin: 0;
       +    padding: 0;
       +}
       +
       +div.sphinxsidebar h3 a {
       +    color: #ffffff;
       +}
       +
       +div.sphinxsidebar h4 {
       +    font-family: 'Trebuchet MS', sans-serif;
       +    color: #ffffff;
       +    font-size: 1.3em;
       +    font-weight: normal;
       +    margin: 5px 0 0 0;
       +    padding: 0;
       +}
       +
       +div.sphinxsidebar p {
       +    color: #ffffff;
       +}
       +
       +div.sphinxsidebar p.topless {
       +    margin: 5px 10px 10px 10px;
       +}
       +
       +div.sphinxsidebar ul {
       +    margin: 10px;
       +    padding: 0;
       +    color: #ffffff;
       +}
       +
       +div.sphinxsidebar a {
       +    color: #98dbcc;
       +}
       +
       +div.sphinxsidebar input {
       +    border: 1px solid #98dbcc;
       +    font-family: sans-serif;
       +    font-size: 1em;
       +}
       +
       +
       +
       +/* -- hyperlink styles ------------------------------------------------------ */
       +
       +a {
       +    color: #355f7c;
       +    text-decoration: none;
       +}
       +
       +a:visited {
       +    color: #551a8b;
       +    text-decoration: none;
       +}
       +
       +a:hover {
       +    text-decoration: underline;
       +}
       +
       +
       +
       +/* -- body styles ----------------------------------------------------------- */
       +
       +div.body h1,
       +div.body h2,
       +div.body h3,
       +div.body h4,
       +div.body h5,
       +div.body h6 {
       +    font-family: 'Trebuchet MS', sans-serif;
       +    background-color: #f2f2f2;
       +    font-weight: normal;
       +    color: #20435c;
       +    border-bottom: 1px solid #ccc;
       +    margin: 20px -20px 10px -20px;
       +    padding: 3px 0 3px 10px;
       +}
       +
       +div.body h1 { margin-top: 0; font-size: 200%; }
       +div.body h2 { font-size: 160%; }
       +div.body h3 { font-size: 140%; }
       +div.body h4 { font-size: 120%; }
       +div.body h5 { font-size: 110%; }
       +div.body h6 { font-size: 100%; }
       +
       +a.headerlink {
       +    color: #c60f0f;
       +    font-size: 0.8em;
       +    padding: 0 4px 0 4px;
       +    text-decoration: none;
       +}
       +
       +a.headerlink:hover {
       +    background-color: #c60f0f;
       +    color: white;
       +}
       +
       +div.body p, div.body dd, div.body li, div.body blockquote {
       +    text-align: justify;
       +    line-height: 130%;
       +}
       +
       +div.admonition p.admonition-title + p {
       +    display: inline;
       +}
       +
       +div.admonition p {
       +    margin-bottom: 5px;
       +}
       +
       +div.admonition pre {
       +    margin-bottom: 5px;
       +}
       +
       +div.admonition ul, div.admonition ol {
       +    margin-bottom: 5px;
       +}
       +
       +div.note {
       +    background-color: #eee;
       +    border: 1px solid #ccc;
       +}
       +
       +div.seealso {
       +    background-color: #ffc;
       +    border: 1px solid #ff6;
       +}
       +
       +nav.contents,
       +aside.topic,
       +div.topic {
       +    background-color: #eee;
       +}
       +
       +div.warning {
       +    background-color: #ffe4e4;
       +    border: 1px solid #f66;
       +}
       +
       +p.admonition-title {
       +    display: inline;
       +}
       +
       +p.admonition-title:after {
       +    content: ":";
       +}
       +
       +pre {
       +    padding: 5px;
       +    background-color: unset;
       +    color: unset;
       +    line-height: 120%;
       +    border: 1px solid #ac9;
       +    border-left: none;
       +    border-right: none;
       +}
       +
       +code {
       +    background-color: #ecf0f3;
       +    padding: 0 1px 0 1px;
       +    font-size: 0.95em;
       +}
       +
       +th, dl.field-list > dt {
       +    background-color: #ede;
       +}
       +
       +.warning code {
       +    background: #efc2c2;
       +}
       +
       +.note code {
       +    background: #d6d6d6;
       +}
       +
       +.viewcode-back {
       +    font-family: sans-serif;
       +}
       +
       +div.viewcode-block:target {
       +    background-color: #f4debf;
       +    border-top: 1px solid #ac9;
       +    border-bottom: 1px solid #ac9;
       +}
       +
       +div.code-block-caption {
       +    color: #efefef;
       +    background-color: #1c4e63;
       +}
       +\ No newline at end of file
   DIR diff --git a/doc/html/_static/doctools.js b/doc/html/_static/doctools.js
       @@ -1,314 +1,150 @@
        /*
       - * doctools.js
       - * ~~~~~~~~~~~
       - *
       - * Sphinx JavaScript utilities for all documentation.
       - *
       - * :copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS.
       - * :license: BSD, see LICENSE for details.
       - *
       + * Base JavaScript utilities for all Sphinx HTML documentation.
         */
       -
       -/**
       - * select a different prefix for underscore
       - */
       -$u = _.noConflict();
       -
       -/**
       - * make the code below compatible with browsers without
       - * an installed firebug like debugger
       -if (!window.console || !console.firebug) {
       -  var names = ["log", "debug", "info", "warn", "error", "assert", "dir",
       -    "dirxml", "group", "groupEnd", "time", "timeEnd", "count", "trace",
       -    "profile", "profileEnd"];
       -  window.console = {};
       -  for (var i = 0; i < names.length; ++i)
       -    window.console[names[i]] = function() {};
       -}
       - */
       -
       -/**
       - * small helper function to urldecode strings
       - */
       -jQuery.urldecode = function(x) {
       -  return decodeURIComponent(x).replace(/\+/g, ' ');
       -};
       -
       -/**
       - * small helper function to urlencode strings
       - */
       -jQuery.urlencode = encodeURIComponent;
       -
       -/**
       - * This function returns the parsed url parameters of the
       - * current request. Multiple values per key are supported,
       - * it will always return arrays of strings for the value parts.
       - */
       -jQuery.getQueryParameters = function(s) {
       -  if (typeof s === 'undefined')
       -    s = document.location.search;
       -  var parts = s.substr(s.indexOf('?') + 1).split('&');
       -  var result = {};
       -  for (var i = 0; i < parts.length; i++) {
       -    var tmp = parts[i].split('=', 2);
       -    var key = jQuery.urldecode(tmp[0]);
       -    var value = jQuery.urldecode(tmp[1]);
       -    if (key in result)
       -      result[key].push(value);
       -    else
       -      result[key] = [value];
       +"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);
          }
       -  return result;
        };
        
        /**
       - * highlight a given string on a jquery object by wrapping it in
       - * span elements with the given class name.
       - */
       -jQuery.fn.highlightText = function(text, className) {
       -  function highlight(node, addItems) {
       -    if (node.nodeType === 3) {
       -      var val = node.nodeValue;
       -      var pos = val.toLowerCase().indexOf(text);
       -      if (pos >= 0 &&
       -          !jQuery(node.parentNode).hasClass(className) &&
       -          !jQuery(node.parentNode).hasClass("nohighlight")) {
       -        var span;
       -        var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg");
       -        if (isInSVG) {
       -          span = document.createElementNS("http://www.w3.org/2000/svg", "tspan");
       -        } else {
       -          span = document.createElement("span");
       -          span.className = className;
       -        }
       -        span.appendChild(document.createTextNode(val.substr(pos, text.length)));
       -        node.parentNode.insertBefore(span, node.parentNode.insertBefore(
       -          document.createTextNode(val.substr(pos + text.length)),
       -          node.nextSibling));
       -        node.nodeValue = val.substr(0, pos);
       -        if (isInSVG) {
       -          var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
       -          var bbox = node.parentElement.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": node.parentNode,
       -              "target": rect});
       -        }
       -      }
       -    }
       -    else if (!jQuery(node).is("button, select, textarea")) {
       -      jQuery.each(node.childNodes, function() {
       -        highlight(this, addItems);
       -      });
       -    }
       -  }
       -  var addItems = [];
       -  var result = this.each(function() {
       -    highlight(this, addItems);
       -  });
       -  for (var i = 0; i < addItems.length; ++i) {
       -    jQuery(addItems[i].parent).before(addItems[i].target);
       -  }
       -  return result;
       -};
       -
       -/*
       - * backward compatibility for jQuery.browser
       - * This will be supported until firefox bug is fixed.
       - */
       -if (!jQuery.browser) {
       -  jQuery.uaMatch = function(ua) {
       -    ua = ua.toLowerCase();
       -
       -    var match = /(chrome)[ \/]([\w.]+)/.exec(ua) ||
       -      /(webkit)[ \/]([\w.]+)/.exec(ua) ||
       -      /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) ||
       -      /(msie) ([\w.]+)/.exec(ua) ||
       -      ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) ||
       -      [];
       -
       -    return {
       -      browser: match[ 1 ] || "",
       -      version: match[ 2 ] || "0"
       -    };
       -  };
       -  jQuery.browser = {};
       -  jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true;
       -}
       -
       -/**
         * Small JavaScript module for the documentation.
         */
       -var Documentation = {
       -
       -  init : function() {
       -    this.fixFirefoxAnchorBug();
       -    this.highlightSearchWords();
       -    this.initIndexTable();
       -    if (DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) {
       -      this.initOnKeyListeners();
       -    }
       +const Documentation = {
       +  init: () => {
       +    Documentation.initDomainIndexTable();
       +    Documentation.initOnKeyListeners();
          },
        
          /**
           * i18n support
           */
       -  TRANSLATIONS : {},
       -  PLURAL_EXPR : function(n) { return n === 1 ? 0 : 1; },
       -  LOCALE : 'unknown',
       +  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 : function(string) {
       -    var translated = Documentation.TRANSLATIONS[string];
       -    if (typeof translated === 'undefined')
       -      return string;
       -    return (typeof translated === 'string') ? translated : translated[0];
       -  },
       -
       -  ngettext : function(singular, plural, n) {
       -    var translated = Documentation.TRANSLATIONS[singular];
       -    if (typeof translated === 'undefined')
       -      return (n == 1) ? singular : plural;
       -    return translated[Documentation.PLURALEXPR(n)];
       +  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
       +    }
          },
        
       -  addTranslations : function(catalog) {
       -    for (var key in catalog.messages)
       -      this.TRANSLATIONS[key] = catalog.messages[key];
       -    this.PLURAL_EXPR = new Function('n', 'return +(' + catalog.plural_expr + ')');
       -    this.LOCALE = catalog.locale;
       +  ngettext: (singular, plural, n) => {
       +    const translated = Documentation.TRANSLATIONS[singular];
       +    if (typeof translated !== "undefined")
       +      return translated[Documentation.PLURAL_EXPR(n)];
       +    return n === 1 ? singular : plural;
          },
        
       -  /**
       -   * add context elements like header anchor links
       -   */
       -  addContextElements : function() {
       -    $('div[id] > :header:first').each(function() {
       -      $('<a class="headerlink">\u00B6</a>').
       -      attr('href', '#' + this.id).
       -      attr('title', _('Permalink to this headline')).
       -      appendTo(this);
       -    });
       -    $('dt[id]').each(function() {
       -      $('<a class="headerlink">\u00B6</a>').
       -      attr('href', '#' + this.id).
       -      attr('title', _('Permalink to this definition')).
       -      appendTo(this);
       -    });
       +  addTranslations: (catalog) => {
       +    Object.assign(Documentation.TRANSLATIONS, catalog.messages);
       +    Documentation.PLURAL_EXPR = new Function(
       +      "n",
       +      `return (${catalog.plural_expr})`,
       +    );
       +    Documentation.LOCALE = catalog.locale;
          },
        
          /**
       -   * workaround a firefox stupidity
       -   * see: https://bugzilla.mozilla.org/show_bug.cgi?id=645075
       +   * helper function to focus on search bar
           */
       -  fixFirefoxAnchorBug : function() {
       -    if (document.location.hash && $.browser.mozilla)
       -      window.setTimeout(function() {
       -        document.location.href += '';
       -      }, 10);
       +  focusSearchBar: () => {
       +    document.querySelectorAll("input[name=q]")[0]?.focus();
          },
        
          /**
       -   * highlight the search words provided in the url in the text
       +   * Initialise the domain index toggle buttons
           */
       -  highlightSearchWords : function() {
       -    var params = $.getQueryParameters();
       -    var terms = (params.highlight) ? params.highlight[0].split(/\s+/) : [];
       -    if (terms.length) {
       -      var body = $('div.body');
       -      if (!body.length) {
       -        body = $('body');
       +  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 = ""));
              }
       -      window.setTimeout(function() {
       -        $.each(terms, function() {
       -          body.highlightText(this.toLowerCase(), 'highlighted');
       -        });
       -      }, 10);
       -      $('<p class="highlight-link"><a href="javascript:Documentation.' +
       -        'hideSearchWords()">' + _('Hide Search Matches') + '</a></p>')
       -          .appendTo($('#searchbox'));
       -    }
       -  },
       -
       -  /**
       -   * init the domain index toggle buttons
       -   */
       -  initIndexTable : function() {
       -    var togglers = $('img.toggler').click(function() {
       -      var src = $(this).attr('src');
       -      var idnum = $(this).attr('id').substr(7);
       -      $('tr.cg-' + idnum).toggle();
       -      if (src.substr(-9) === 'minus.png')
       -        $(this).attr('src', src.substr(0, src.length-9) + 'plus.png');
       -      else
       -        $(this).attr('src', src.substr(0, src.length-8) + 'minus.png');
       -    }).css('display', '');
       -    if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) {
       -        togglers.click();
       -    }
       -  },
       -
       -  /**
       -   * helper function to hide the search marks again
       -   */
       -  hideSearchWords : function() {
       -    $('#searchbox .highlight-link').fadeOut(300);
       -    $('span.highlighted').removeClass('highlighted');
       -  },
       -
       -  /**
       -   * make the url absolute
       -   */
       -  makeURL : function(relativeURL) {
       -    return DOCUMENTATION_OPTIONS.URL_ROOT + '/' + relativeURL;
       -  },
       +    };
        
       -  /**
       -   * get the current relative url
       -   */
       -  getCurrentURL : function() {
       -    var path = document.location.pathname;
       -    var parts = path.split(/\//);
       -    $.each(DOCUMENTATION_OPTIONS.URL_ROOT.split(/\//), function() {
       -      if (this === '..')
       -        parts.pop();
       -    });
       -    var url = parts.join('/');
       -    return path.substring(url.lastIndexOf('/') + 1, path.length - 1);
       +    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: function() {
       -    $(document).keyup(function(event) {
       -      var activeElementType = document.activeElement.tagName;
       -      // don't navigate when in search box or textarea
       -      if (activeElementType !== 'TEXTAREA' && activeElementType !== 'INPUT' && activeElementType !== 'SELECT') {
       -        switch (event.keyCode) {
       -          case 37: // left
       -            var prevHref = $('link[rel="prev"]').prop('href');
       -            if (prevHref) {
       -              window.location.href = prevHref;
       -              return false;
       +  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();
                    }
       -          case 39: // right
       -            var nextHref = $('link[rel="next"]').prop('href');
       -            if (nextHref) {
       -              window.location.href = nextHref;
       -              return false;
       +            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
       -_ = Documentation.gettext;
       +const _ = Documentation.gettext;
        
       -$(document).ready(function() {
       -  Documentation.init();
       -});
       +_ready(Documentation.init);
   DIR diff --git a/doc/html/_static/documentation_options.js b/doc/html/_static/documentation_options.js
       @@ -0,0 +1,13 @@
       +const DOCUMENTATION_OPTIONS = {
       +    VERSION: '2.15',
       +    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
   DIR diff --git a/doc/html/_static/english-stemmer.js b/doc/html/_static/english-stemmer.js
       @@ -0,0 +1,1066 @@
       +// Generated from english.sbl by Snowball 3.0.1 - https://snowballstem.org/
       +
       +/**@constructor*/
       +var EnglishStemmer = function() {
       +    var base = new BaseStemmer();
       +
       +    /** @const */ var a_0 = [
       +        ["arsen", -1, -1],
       +        ["commun", -1, -1],
       +        ["emerg", -1, -1],
       +        ["gener", -1, -1],
       +        ["later", -1, -1],
       +        ["organ", -1, -1],
       +        ["past", -1, -1],
       +        ["univers", -1, -1]
       +    ];
       +
       +    /** @const */ var a_1 = [
       +        ["'", -1, 1],
       +        ["'s'", 0, 1],
       +        ["'s", -1, 1]
       +    ];
       +
       +    /** @const */ var a_2 = [
       +        ["ied", -1, 2],
       +        ["s", -1, 3],
       +        ["ies", 1, 2],
       +        ["sses", 1, 1],
       +        ["ss", 1, -1],
       +        ["us", 1, -1]
       +    ];
       +
       +    /** @const */ var a_3 = [
       +        ["succ", -1, 1],
       +        ["proc", -1, 1],
       +        ["exc", -1, 1]
       +    ];
       +
       +    /** @const */ var a_4 = [
       +        ["even", -1, 2],
       +        ["cann", -1, 2],
       +        ["inn", -1, 2],
       +        ["earr", -1, 2],
       +        ["herr", -1, 2],
       +        ["out", -1, 2],
       +        ["y", -1, 1]
       +    ];
       +
       +    /** @const */ var a_5 = [
       +        ["", -1, -1],
       +        ["ed", 0, 2],
       +        ["eed", 1, 1],
       +        ["ing", 0, 3],
       +        ["edly", 0, 2],
       +        ["eedly", 4, 1],
       +        ["ingly", 0, 2]
       +    ];
       +
       +    /** @const */ var a_6 = [
       +        ["", -1, 3],
       +        ["bb", 0, 2],
       +        ["dd", 0, 2],
       +        ["ff", 0, 2],
       +        ["gg", 0, 2],
       +        ["bl", 0, 1],
       +        ["mm", 0, 2],
       +        ["nn", 0, 2],
       +        ["pp", 0, 2],
       +        ["rr", 0, 2],
       +        ["at", 0, 1],
       +        ["tt", 0, 2],
       +        ["iz", 0, 1]
       +    ];
       +
       +    /** @const */ var a_7 = [
       +        ["anci", -1, 3],
       +        ["enci", -1, 2],
       +        ["ogi", -1, 14],
       +        ["li", -1, 16],
       +        ["bli", 3, 12],
       +        ["abli", 4, 4],
       +        ["alli", 3, 8],
       +        ["fulli", 3, 9],
       +        ["lessli", 3, 15],
       +        ["ousli", 3, 10],
       +        ["entli", 3, 5],
       +        ["aliti", -1, 8],
       +        ["biliti", -1, 12],
       +        ["iviti", -1, 11],
       +        ["tional", -1, 1],
       +        ["ational", 14, 7],
       +        ["alism", -1, 8],
       +        ["ation", -1, 7],
       +        ["ization", 17, 6],
       +        ["izer", -1, 6],
       +        ["ator", -1, 7],
       +        ["iveness", -1, 11],
       +        ["fulness", -1, 9],
       +        ["ousness", -1, 10],
       +        ["ogist", -1, 13]
       +    ];
       +
       +    /** @const */ var a_8 = [
       +        ["icate", -1, 4],
       +        ["ative", -1, 6],
       +        ["alize", -1, 3],
       +        ["iciti", -1, 4],
       +        ["ical", -1, 4],
       +        ["tional", -1, 1],
       +        ["ational", 5, 2],
       +        ["ful", -1, 5],
       +        ["ness", -1, 5]
       +    ];
       +
       +    /** @const */ var a_9 = [
       +        ["ic", -1, 1],
       +        ["ance", -1, 1],
       +        ["ence", -1, 1],
       +        ["able", -1, 1],
       +        ["ible", -1, 1],
       +        ["ate", -1, 1],
       +        ["ive", -1, 1],
       +        ["ize", -1, 1],
       +        ["iti", -1, 1],
       +        ["al", -1, 1],
       +        ["ism", -1, 1],
       +        ["ion", -1, 2],
       +        ["er", -1, 1],
       +        ["ous", -1, 1],
       +        ["ant", -1, 1],
       +        ["ent", -1, 1],
       +        ["ment", 15, 1],
       +        ["ement", 16, 1]
       +    ];
       +
       +    /** @const */ var a_10 = [
       +        ["e", -1, 1],
       +        ["l", -1, 2]
       +    ];
       +
       +    /** @const */ var a_11 = [
       +        ["andes", -1, -1],
       +        ["atlas", -1, -1],
       +        ["bias", -1, -1],
       +        ["cosmos", -1, -1],
       +        ["early", -1, 5],
       +        ["gently", -1, 3],
       +        ["howe", -1, -1],
       +        ["idly", -1, 2],
       +        ["news", -1, -1],
       +        ["only", -1, 6],
       +        ["singly", -1, 7],
       +        ["skies", -1, 1],
       +        ["sky", -1, -1],
       +        ["ugly", -1, 4]
       +    ];
       +
       +    /** @const */ var /** Array<int> */ g_aeo = [17, 64];
       +
       +    /** @const */ var /** Array<int> */ g_v = [17, 65, 16, 1];
       +
       +    /** @const */ var /** Array<int> */ g_v_WXY = [1, 17, 65, 208, 1];
       +
       +    /** @const */ var /** Array<int> */ g_valid_LI = [55, 141, 2];
       +
       +    var /** boolean */ B_Y_found = false;
       +    var /** number */ I_p2 = 0;
       +    var /** number */ I_p1 = 0;
       +
       +
       +    /** @return {boolean} */
       +    function r_prelude() {
       +        B_Y_found = false;
       +        /** @const */ var /** number */ v_1 = base.cursor;
       +        lab0: {
       +            base.bra = base.cursor;
       +            if (!(base.eq_s("'")))
       +            {
       +                break lab0;
       +            }
       +            base.ket = base.cursor;
       +            if (!base.slice_del())
       +            {
       +                return false;
       +            }
       +        }
       +        base.cursor = v_1;
       +        /** @const */ var /** number */ v_2 = base.cursor;
       +        lab1: {
       +            base.bra = base.cursor;
       +            if (!(base.eq_s("y")))
       +            {
       +                break lab1;
       +            }
       +            base.ket = base.cursor;
       +            if (!base.slice_from("Y"))
       +            {
       +                return false;
       +            }
       +            B_Y_found = true;
       +        }
       +        base.cursor = v_2;
       +        /** @const */ var /** number */ v_3 = base.cursor;
       +        lab2: {
       +            while(true)
       +            {
       +                /** @const */ var /** number */ v_4 = base.cursor;
       +                lab3: {
       +                    golab4: while(true)
       +                    {
       +                        /** @const */ var /** number */ v_5 = base.cursor;
       +                        lab5: {
       +                            if (!(base.in_grouping(g_v, 97, 121)))
       +                            {
       +                                break lab5;
       +                            }
       +                            base.bra = base.cursor;
       +                            if (!(base.eq_s("y")))
       +                            {
       +                                break lab5;
       +                            }
       +                            base.ket = base.cursor;
       +                            base.cursor = v_5;
       +                            break golab4;
       +                        }
       +                        base.cursor = v_5;
       +                        if (base.cursor >= base.limit)
       +                        {
       +                            break lab3;
       +                        }
       +                        base.cursor++;
       +                    }
       +                    if (!base.slice_from("Y"))
       +                    {
       +                        return false;
       +                    }
       +                    B_Y_found = true;
       +                    continue;
       +                }
       +                base.cursor = v_4;
       +                break;
       +            }
       +        }
       +        base.cursor = v_3;
       +        return true;
       +    };
       +
       +    /** @return {boolean} */
       +    function r_mark_regions() {
       +        I_p1 = base.limit;
       +        I_p2 = base.limit;
       +        /** @const */ var /** number */ v_1 = base.cursor;
       +        lab0: {
       +            lab1: {
       +                /** @const */ var /** number */ v_2 = base.cursor;
       +                lab2: {
       +                    if (base.find_among(a_0) == 0)
       +                    {
       +                        break lab2;
       +                    }
       +                    break lab1;
       +                }
       +                base.cursor = v_2;
       +                if (!base.go_out_grouping(g_v, 97, 121))
       +                {
       +                    break lab0;
       +                }
       +                base.cursor++;
       +                if (!base.go_in_grouping(g_v, 97, 121))
       +                {
       +                    break lab0;
       +                }
       +                base.cursor++;
       +            }
       +            I_p1 = base.cursor;
       +            if (!base.go_out_grouping(g_v, 97, 121))
       +            {
       +                break lab0;
       +            }
       +            base.cursor++;
       +            if (!base.go_in_grouping(g_v, 97, 121))
       +            {
       +                break lab0;
       +            }
       +            base.cursor++;
       +            I_p2 = base.cursor;
       +        }
       +        base.cursor = v_1;
       +        return true;
       +    };
       +
       +    /** @return {boolean} */
       +    function r_shortv() {
       +        lab0: {
       +            /** @const */ var /** number */ v_1 = base.limit - base.cursor;
       +            lab1: {
       +                if (!(base.out_grouping_b(g_v_WXY, 89, 121)))
       +                {
       +                    break lab1;
       +                }
       +                if (!(base.in_grouping_b(g_v, 97, 121)))
       +                {
       +                    break lab1;
       +                }
       +                if (!(base.out_grouping_b(g_v, 97, 121)))
       +                {
       +                    break lab1;
       +                }
       +                break lab0;
       +            }
       +            base.cursor = base.limit - v_1;
       +            lab2: {
       +                if (!(base.out_grouping_b(g_v, 97, 121)))
       +                {
       +                    break lab2;
       +                }
       +                if (!(base.in_grouping_b(g_v, 97, 121)))
       +                {
       +                    break lab2;
       +                }
       +                if (base.cursor > base.limit_backward)
       +                {
       +                    break lab2;
       +                }
       +                break lab0;
       +            }
       +            base.cursor = base.limit - v_1;
       +            if (!(base.eq_s_b("past")))
       +            {
       +                return false;
       +            }
       +        }
       +        return true;
       +    };
       +
       +    /** @return {boolean} */
       +    function r_R1() {
       +        return I_p1 <= base.cursor;
       +    };
       +
       +    /** @return {boolean} */
       +    function r_R2() {
       +        return I_p2 <= base.cursor;
       +    };
       +
       +    /** @return {boolean} */
       +    function r_Step_1a() {
       +        var /** number */ among_var;
       +        /** @const */ var /** number */ v_1 = base.limit - base.cursor;
       +        lab0: {
       +            base.ket = base.cursor;
       +            if (base.find_among_b(a_1) == 0)
       +            {
       +                base.cursor = base.limit - v_1;
       +                break lab0;
       +            }
       +            base.bra = base.cursor;
       +            if (!base.slice_del())
       +            {
       +                return false;
       +            }
       +        }
       +        base.ket = base.cursor;
       +        among_var = base.find_among_b(a_2);
       +        if (among_var == 0)
       +        {
       +            return false;
       +        }
       +        base.bra = base.cursor;
       +        switch (among_var) {
       +            case 1:
       +                if (!base.slice_from("ss"))
       +                {
       +                    return false;
       +                }
       +                break;
       +            case 2:
       +                lab1: {
       +                    /** @const */ var /** number */ v_2 = base.limit - base.cursor;
       +                    lab2: {
       +                        {
       +                            /** @const */ var /** number */ c1 = base.cursor - 2;
       +                            if (c1 < base.limit_backward)
       +                            {
       +                                break lab2;
       +                            }
       +                            base.cursor = c1;
       +                        }
       +                        if (!base.slice_from("i"))
       +                        {
       +                            return false;
       +                        }
       +                        break lab1;
       +                    }
       +                    base.cursor = base.limit - v_2;
       +                    if (!base.slice_from("ie"))
       +                    {
       +                        return false;
       +                    }
       +                }
       +                break;
       +            case 3:
       +                if (base.cursor <= base.limit_backward)
       +                {
       +                    return false;
       +                }
       +                base.cursor--;
       +                if (!base.go_out_grouping_b(g_v, 97, 121))
       +                {
       +                    return false;
       +                }
       +                base.cursor--;
       +                if (!base.slice_del())
       +                {
       +                    return false;
       +                }
       +                break;
       +        }
       +        return true;
       +    };
       +
       +    /** @return {boolean} */
       +    function r_Step_1b() {
       +        var /** number */ among_var;
       +        base.ket = base.cursor;
       +        among_var = base.find_among_b(a_5);
       +        base.bra = base.cursor;
       +        lab0: {
       +            /** @const */ var /** number */ v_1 = base.limit - base.cursor;
       +            lab1: {
       +                switch (among_var) {
       +                    case 1:
       +                        /** @const */ var /** number */ v_2 = base.limit - base.cursor;
       +                        lab2: {
       +                            lab3: {
       +                                /** @const */ var /** number */ v_3 = base.limit - base.cursor;
       +                                lab4: {
       +                                    if (base.find_among_b(a_3) == 0)
       +                                    {
       +                                        break lab4;
       +                                    }
       +                                    if (base.cursor > base.limit_backward)
       +                                    {
       +                                        break lab4;
       +                                    }
       +                                    break lab3;
       +                                }
       +                                base.cursor = base.limit - v_3;
       +                                if (!r_R1())
       +                                {
       +                                    break lab2;
       +                                }
       +                                if (!base.slice_from("ee"))
       +                                {
       +                                    return false;
       +                                }
       +                            }
       +                        }
       +                        base.cursor = base.limit - v_2;
       +                        break;
       +                    case 2:
       +                        break lab1;
       +                    case 3:
       +                        among_var = base.find_among_b(a_4);
       +                        if (among_var == 0)
       +                        {
       +                            break lab1;
       +                        }
       +                        switch (among_var) {
       +                            case 1:
       +                                /** @const */ var /** number */ v_4 = base.limit - base.cursor;
       +                                if (!(base.out_grouping_b(g_v, 97, 121)))
       +                                {
       +                                    break lab1;
       +                                }
       +                                if (base.cursor > base.limit_backward)
       +                                {
       +                                    break lab1;
       +                                }
       +                                base.cursor = base.limit - v_4;
       +                                base.bra = base.cursor;
       +                                if (!base.slice_from("ie"))
       +                                {
       +                                    return false;
       +                                }
       +                                break;
       +                            case 2:
       +                                if (base.cursor > base.limit_backward)
       +                                {
       +                                    break lab1;
       +                                }
       +                                break;
       +                        }
       +                        break;
       +                }
       +                break lab0;
       +            }
       +            base.cursor = base.limit - v_1;
       +            /** @const */ var /** number */ v_5 = base.limit - base.cursor;
       +            if (!base.go_out_grouping_b(g_v, 97, 121))
       +            {
       +                return false;
       +            }
       +            base.cursor--;
       +            base.cursor = base.limit - v_5;
       +            if (!base.slice_del())
       +            {
       +                return false;
       +            }
       +            base.ket = base.cursor;
       +            base.bra = base.cursor;
       +            /** @const */ var /** number */ v_6 = base.limit - base.cursor;
       +            among_var = base.find_among_b(a_6);
       +            switch (among_var) {
       +                case 1:
       +                    if (!base.slice_from("e"))
       +                    {
       +                        return false;
       +                    }
       +                    return false;
       +                case 2:
       +                    {
       +                        /** @const */ var /** number */ v_7 = base.limit - base.cursor;
       +                        lab5: {
       +                            if (!(base.in_grouping_b(g_aeo, 97, 111)))
       +                            {
       +                                break lab5;
       +                            }
       +                            if (base.cursor > base.limit_backward)
       +                            {
       +                                break lab5;
       +                            }
       +                            return false;
       +                        }
       +                        base.cursor = base.limit - v_7;
       +                    }
       +                    break;
       +                case 3:
       +                    if (base.cursor != I_p1)
       +                    {
       +                        return false;
       +                    }
       +                    /** @const */ var /** number */ v_8 = base.limit - base.cursor;
       +                    if (!r_shortv())
       +                    {
       +                        return false;
       +                    }
       +                    base.cursor = base.limit - v_8;
       +                    if (!base.slice_from("e"))
       +                    {
       +                        return false;
       +                    }
       +                    return false;
       +            }
       +            base.cursor = base.limit - v_6;
       +            base.ket = base.cursor;
       +            if (base.cursor <= base.limit_backward)
       +            {
       +                return false;
       +            }
       +            base.cursor--;
       +            base.bra = base.cursor;
       +            if (!base.slice_del())
       +            {
       +                return false;
       +            }
       +        }
       +        return true;
       +    };
       +
       +    /** @return {boolean} */
       +    function r_Step_1c() {
       +        base.ket = base.cursor;
       +        lab0: {
       +            /** @const */ var /** number */ v_1 = base.limit - base.cursor;
       +            lab1: {
       +                if (!(base.eq_s_b("y")))
       +                {
       +                    break lab1;
       +                }
       +                break lab0;
       +            }
       +            base.cursor = base.limit - v_1;
       +            if (!(base.eq_s_b("Y")))
       +            {
       +                return false;
       +            }
       +        }
       +        base.bra = base.cursor;
       +        if (!(base.out_grouping_b(g_v, 97, 121)))
       +        {
       +            return false;
       +        }
       +        lab2: {
       +            if (base.cursor > base.limit_backward)
       +            {
       +                break lab2;
       +            }
       +            return false;
       +        }
       +        if (!base.slice_from("i"))
       +        {
       +            return false;
       +        }
       +        return true;
       +    };
       +
       +    /** @return {boolean} */
       +    function r_Step_2() {
       +        var /** number */ among_var;
       +        base.ket = base.cursor;
       +        among_var = base.find_among_b(a_7);
       +        if (among_var == 0)
       +        {
       +            return false;
       +        }
       +        base.bra = base.cursor;
       +        if (!r_R1())
       +        {
       +            return false;
       +        }
       +        switch (among_var) {
       +            case 1:
       +                if (!base.slice_from("tion"))
       +                {
       +                    return false;
       +                }
       +                break;
       +            case 2:
       +                if (!base.slice_from("ence"))
       +                {
       +                    return false;
       +                }
       +                break;
       +            case 3:
       +                if (!base.slice_from("ance"))
       +                {
       +                    return false;
       +                }
       +                break;
       +            case 4:
       +                if (!base.slice_from("able"))
       +                {
       +                    return false;
       +                }
       +                break;
       +            case 5:
       +                if (!base.slice_from("ent"))
       +                {
       +                    return false;
       +                }
       +                break;
       +            case 6:
       +                if (!base.slice_from("ize"))
       +                {
       +                    return false;
       +                }
       +                break;
       +            case 7:
       +                if (!base.slice_from("ate"))
       +                {
       +                    return false;
       +                }
       +                break;
       +            case 8:
       +                if (!base.slice_from("al"))
       +                {
       +                    return false;
       +                }
       +                break;
       +            case 9:
       +                if (!base.slice_from("ful"))
       +                {
       +                    return false;
       +                }
       +                break;
       +            case 10:
       +                if (!base.slice_from("ous"))
       +                {
       +                    return false;
       +                }
       +                break;
       +            case 11:
       +                if (!base.slice_from("ive"))
       +                {
       +                    return false;
       +                }
       +                break;
       +            case 12:
       +                if (!base.slice_from("ble"))
       +                {
       +                    return false;
       +                }
       +                break;
       +            case 13:
       +                if (!base.slice_from("og"))
       +                {
       +                    return false;
       +                }
       +                break;
       +            case 14:
       +                if (!(base.eq_s_b("l")))
       +                {
       +                    return false;
       +                }
       +                if (!base.slice_from("og"))
       +                {
       +                    return false;
       +                }
       +                break;
       +            case 15:
       +                if (!base.slice_from("less"))
       +                {
       +                    return false;
       +                }
       +                break;
       +            case 16:
       +                if (!(base.in_grouping_b(g_valid_LI, 99, 116)))
       +                {
       +                    return false;
       +                }
       +                if (!base.slice_del())
       +                {
       +                    return false;
       +                }
       +                break;
       +        }
       +        return true;
       +    };
       +
       +    /** @return {boolean} */
       +    function r_Step_3() {
       +        var /** number */ among_var;
       +        base.ket = base.cursor;
       +        among_var = base.find_among_b(a_8);
       +        if (among_var == 0)
       +        {
       +            return false;
       +        }
       +        base.bra = base.cursor;
       +        if (!r_R1())
       +        {
       +            return false;
       +        }
       +        switch (among_var) {
       +            case 1:
       +                if (!base.slice_from("tion"))
       +                {
       +                    return false;
       +                }
       +                break;
       +            case 2:
       +                if (!base.slice_from("ate"))
       +                {
       +                    return false;
       +                }
       +                break;
       +            case 3:
       +                if (!base.slice_from("al"))
       +                {
       +                    return false;
       +                }
       +                break;
       +            case 4:
       +                if (!base.slice_from("ic"))
       +                {
       +                    return false;
       +                }
       +                break;
       +            case 5:
       +                if (!base.slice_del())
       +                {
       +                    return false;
       +                }
       +                break;
       +            case 6:
       +                if (!r_R2())
       +                {
       +                    return false;
       +                }
       +                if (!base.slice_del())
       +                {
       +                    return false;
       +                }
       +                break;
       +        }
       +        return true;
       +    };
       +
       +    /** @return {boolean} */
       +    function r_Step_4() {
       +        var /** number */ among_var;
       +        base.ket = base.cursor;
       +        among_var = base.find_among_b(a_9);
       +        if (among_var == 0)
       +        {
       +            return false;
       +        }
       +        base.bra = base.cursor;
       +        if (!r_R2())
       +        {
       +            return false;
       +        }
       +        switch (among_var) {
       +            case 1:
       +                if (!base.slice_del())
       +                {
       +                    return false;
       +                }
       +                break;
       +            case 2:
       +                lab0: {
       +                    /** @const */ var /** number */ v_1 = base.limit - base.cursor;
       +                    lab1: {
       +                        if (!(base.eq_s_b("s")))
       +                        {
       +                            break lab1;
       +                        }
       +                        break lab0;
       +                    }
       +                    base.cursor = base.limit - v_1;
       +                    if (!(base.eq_s_b("t")))
       +                    {
       +                        return false;
       +                    }
       +                }
       +                if (!base.slice_del())
       +                {
       +                    return false;
       +                }
       +                break;
       +        }
       +        return true;
       +    };
       +
       +    /** @return {boolean} */
       +    function r_Step_5() {
       +        var /** number */ among_var;
       +        base.ket = base.cursor;
       +        among_var = base.find_among_b(a_10);
       +        if (among_var == 0)
       +        {
       +            return false;
       +        }
       +        base.bra = base.cursor;
       +        switch (among_var) {
       +            case 1:
       +                lab0: {
       +                    lab1: {
       +                        if (!r_R2())
       +                        {
       +                            break lab1;
       +                        }
       +                        break lab0;
       +                    }
       +                    if (!r_R1())
       +                    {
       +                        return false;
       +                    }
       +                    {
       +                        /** @const */ var /** number */ v_1 = base.limit - base.cursor;
       +                        lab2: {
       +                            if (!r_shortv())
       +                            {
       +                                break lab2;
       +                            }
       +                            return false;
       +                        }
       +                        base.cursor = base.limit - v_1;
       +                    }
       +                }
       +                if (!base.slice_del())
       +                {
       +                    return false;
       +                }
       +                break;
       +            case 2:
       +                if (!r_R2())
       +                {
       +                    return false;
       +                }
       +                if (!(base.eq_s_b("l")))
       +                {
       +                    return false;
       +                }
       +                if (!base.slice_del())
       +                {
       +                    return false;
       +                }
       +                break;
       +        }
       +        return true;
       +    };
       +
       +    /** @return {boolean} */
       +    function r_exception1() {
       +        var /** number */ among_var;
       +        base.bra = base.cursor;
       +        among_var = base.find_among(a_11);
       +        if (among_var == 0)
       +        {
       +            return false;
       +        }
       +        base.ket = base.cursor;
       +        if (base.cursor < base.limit)
       +        {
       +            return false;
       +        }
       +        switch (among_var) {
       +            case 1:
       +                if (!base.slice_from("sky"))
       +                {
       +                    return false;
       +                }
       +                break;
       +            case 2:
       +                if (!base.slice_from("idl"))
       +                {
       +                    return false;
       +                }
       +                break;
       +            case 3:
       +                if (!base.slice_from("gentl"))
       +                {
       +                    return false;
       +                }
       +                break;
       +            case 4:
       +                if (!base.slice_from("ugli"))
       +                {
       +                    return false;
       +                }
       +                break;
       +            case 5:
       +                if (!base.slice_from("earli"))
       +                {
       +                    return false;
       +                }
       +                break;
       +            case 6:
       +                if (!base.slice_from("onli"))
       +                {
       +                    return false;
       +                }
       +                break;
       +            case 7:
       +                if (!base.slice_from("singl"))
       +                {
       +                    return false;
       +                }
       +                break;
       +        }
       +        return true;
       +    };
       +
       +    /** @return {boolean} */
       +    function r_postlude() {
       +        if (!B_Y_found)
       +        {
       +            return false;
       +        }
       +        while(true)
       +        {
       +            /** @const */ var /** number */ v_1 = base.cursor;
       +            lab0: {
       +                golab1: while(true)
       +                {
       +                    /** @const */ var /** number */ v_2 = base.cursor;
       +                    lab2: {
       +                        base.bra = base.cursor;
       +                        if (!(base.eq_s("Y")))
       +                        {
       +                            break lab2;
       +                        }
       +                        base.ket = base.cursor;
       +                        base.cursor = v_2;
       +                        break golab1;
       +                    }
       +                    base.cursor = v_2;
       +                    if (base.cursor >= base.limit)
       +                    {
       +                        break lab0;
       +                    }
       +                    base.cursor++;
       +                }
       +                if (!base.slice_from("y"))
       +                {
       +                    return false;
       +                }
       +                continue;
       +            }
       +            base.cursor = v_1;
       +            break;
       +        }
       +        return true;
       +    };
       +
       +    this.stem = /** @return {boolean} */ function() {
       +        lab0: {
       +            /** @const */ var /** number */ v_1 = base.cursor;
       +            lab1: {
       +                if (!r_exception1())
       +                {
       +                    break lab1;
       +                }
       +                break lab0;
       +            }
       +            base.cursor = v_1;
       +            lab2: {
       +                {
       +                    /** @const */ var /** number */ v_2 = base.cursor;
       +                    lab3: {
       +                        {
       +                            /** @const */ var /** number */ c1 = base.cursor + 3;
       +                            if (c1 > base.limit)
       +                            {
       +                                break lab3;
       +                            }
       +                            base.cursor = c1;
       +                        }
       +                        break lab2;
       +                    }
       +                    base.cursor = v_2;
       +                }
       +                break lab0;
       +            }
       +            base.cursor = v_1;
       +            r_prelude();
       +            r_mark_regions();
       +            base.limit_backward = base.cursor; base.cursor = base.limit;
       +            /** @const */ var /** number */ v_3 = base.limit - base.cursor;
       +            r_Step_1a();
       +            base.cursor = base.limit - v_3;
       +            /** @const */ var /** number */ v_4 = base.limit - base.cursor;
       +            r_Step_1b();
       +            base.cursor = base.limit - v_4;
       +            /** @const */ var /** number */ v_5 = base.limit - base.cursor;
       +            r_Step_1c();
       +            base.cursor = base.limit - v_5;
       +            /** @const */ var /** number */ v_6 = base.limit - base.cursor;
       +            r_Step_2();
       +            base.cursor = base.limit - v_6;
       +            /** @const */ var /** number */ v_7 = base.limit - base.cursor;
       +            r_Step_3();
       +            base.cursor = base.limit - v_7;
       +            /** @const */ var /** number */ v_8 = base.limit - base.cursor;
       +            r_Step_4();
       +            base.cursor = base.limit - v_8;
       +            /** @const */ var /** number */ v_9 = base.limit - base.cursor;
       +            r_Step_5();
       +            base.cursor = base.limit - v_9;
       +            base.cursor = base.limit_backward;
       +            /** @const */ var /** number */ v_10 = base.cursor;
       +            r_postlude();
       +            base.cursor = v_10;
       +        }
       +        return true;
       +    };
       +
       +    /**@return{string}*/
       +    this['stemWord'] = function(/**string*/word) {
       +        base.setCurrent(word);
       +        this.stem();
       +        return base.getCurrent();
       +    };
       +};
   DIR diff --git a/doc/html/_static/language_data.js b/doc/html/_static/language_data.js
       @@ -0,0 +1,13 @@
       +/*
       + * This script contains the language-specific data used by searchtools.js,
       + * namely the set of stopwords, stemmer, scorer and splitter.
       + */
       +
mx1.adamsgaard.dk:70 /src/sphere/commit/8db43a4f44d5a40467db9fd613706de973506ece.gph:3995: line too long