// Distributed under the MIT License.
// See LICENSE.txt for details.

#pragma once

#include <array>
#include <boost/functional/hash.hpp>  // IWYU pragma: keep
#include <cmath>
#include <cstddef>
#include <limits>
#include <ostream>
#include <pup.h>  // IWYU pragma: keep
#include <type_traits>
#include <unordered_map>
#include <utility>  // for pair

#include "DataStructures/DataVector.hpp"  // IWYU pragma: keep
#include "DataStructures/Index.hpp"
#include "DataStructures/ModalVector.hpp"  // IWYU pragma: keep
#include "DataStructures/Tags.hpp"
#include "DataStructures/Tensor/Metafunctions.hpp"  // IWYU pragma: keep
#include "DataStructures/Tensor/Tensor.hpp"         // IWYU pragma: keep
#include "DataStructures/Variables.hpp"             // IWYU pragma: keep
#include "Domain/Structure/Direction.hpp"
#include "Domain/Structure/Element.hpp"         // IWYU pragma: keep
#include "Domain/Structure/ElementId.hpp"       // IWYU pragma: keep
#include "Domain/Structure/OrientationMap.hpp"  // IWYU pragma: keep
#include "Domain/Structure/OrientationMapHelpers.hpp"
#include "Domain/Tags.hpp"  // IWYU pragma: keep
#include "NumericalAlgorithms/LinearOperators/CoefficientTransforms.hpp"
#include "NumericalAlgorithms/Spectral/Mesh.hpp"
#include "NumericalAlgorithms/Spectral/Spectral.hpp"
#include "Options/Options.hpp"
#include "Utilities/Algorithm.hpp"
#include "Utilities/ErrorHandling/Error.hpp"
#include "Utilities/Gsl.hpp"
#include "Utilities/MakeArray.hpp"
#include "Utilities/Math.hpp"
#include "Utilities/TMPL.hpp"

// IWYU pragma: no_include <algorithm>
// IWYU pragma: no_forward_declare Variables

/// \cond
namespace Limiters {
template <size_t VolumeDim, typename TagsToLimit>
class Krivodonova;
}  // namespace Limiters
/// \endcond

namespace Limiters {
/*!
 * \ingroup LimitersGroup
 * \brief An implementation of the Krivodonova limiter.
 *
 * The limiter is described in \cite Krivodonova2007. The Krivodonova limiter
 * works by limiting the highest derivatives/modal coefficients using an
 * aggressive minmod approach, decreasing in derivative/modal coefficient order
 * until no more limiting is necessary. In 3d, the function being limited is
 * expanded as:
 *
 * \f{align}{
 * u^{l,m,n}=\sum_{i,j,k=0,0,0}^{N_i,N_j,N_k}c^{l,m,n}_{i,j,k}
 *  P_{i}(\xi)P_{j}(\eta)P_{k}(\zeta)
 * \f}
 *
 * where \f$\left\{\xi, \eta, \zeta\right\}\f$ are the logical coordinates,
 * \f$P_{i}\f$ are the Legendre polynomials, the superscript \f$\{l,m,n\}\f$
 * represents the element indexed by \f$l,m,n\f$, and \f$N_i,N_j\f$ and
 * \f$N_k\f$ are the number of collocation points minus one in the
 * \f$\xi,\eta,\f$ and \f$\zeta\f$ direction, respectively. The coefficients are
 * limited according to:
 *
 * \f{align}{
 * \tilde{c}^{l,m,n}_{i,j,k}=\mathrm{minmod}
 *   &\left(c_{i,j,k}^{l,m,n},
 *          \alpha_i\left(c^{l+1,m,n}_{i-1,j,k}-c^{l,m,n}_{i-1,j,k}\right),
 *          \alpha_i\left(c^{l,m,n}_{i-1,j,k}-c^{l-1,m,n}_{i-1,j,k}\right),
 *     \right.\notag \\
 *   &\;\;\;\;
 *          \alpha_j\left(c^{l,m+1,n}_{i,j-1,k}-c^{l,m,n}_{i,j-1,k}\right),
 *          \alpha_j\left(c^{l,m,n}_{i,j-1,k}-c^{l,m-1,n}_{i,j-1,k}\right),
 *     \notag \\
 *   &\;\;\;\;\left.
 *          \alpha_k\left(c^{l,m,n+1}_{i,j,k-1}-c^{l,m,n}_{i,j,k-1}\right),
 *          \alpha_k\left(c^{l,m,n}_{i,j,k-1}-c^{l,m,n-1}_{i,j,k-1}\right)
 *     \right),
 * \label{eq:krivodonova 3d minmod}
 * \f}
 *
 * where \f$\mathrm{minmod}\f$ is the minmod function defined as
 *
 * \f{align}{
 *  \mathrm{minmod}(a,b,c,\ldots)=
 *  \left\{
 *  \begin{array}{ll}
 *    \mathrm{sgn}(a)\min(\lvert a\rvert, \lvert b\rvert,
 *    \lvert c\rvert, \ldots) & \mathrm{if} \;
 *    \mathrm{sgn}(a)=\mathrm{sgn}(b)=\mathrm{sgn}(c)=\mathrm{sgn}(\ldots) \\
 *    0 & \mathrm{otherwise}
 *  \end{array}\right.
 * \f}
 *
 * Krivodonova \cite Krivodonova2007 requires \f$\alpha_i\f$ to be in the range
 *
 * \f{align*}{
 * \frac{1}{2(2i-1)}\le \alpha_i \le 1
 * \f}
 *
 * where the lower bound comes from finite differencing the coefficients between
 * neighbor elements when using Legendre polynomials (see \cite Krivodonova2007
 * for details). Note that we normalize our Legendre polynomials by \f$P_i(1) =
 * 1\f$; this is the normalization \cite Krivodonova2007 uses in 1D, (but not in
 * 2D), which is why our bounds on \f$\alpha_i\f$ match Eq. 14 of
 * \cite Krivodonova2007 (but not Eq. 23). We relax the lower bound:
 *
 * \f{align*}{
 * 0 \le \alpha_i \le 1
 * \f}
 *
 * to allow different basis functions (e.g. Chebyshev polynomials) and to allow
 * the limiter to be more dissipative if necessary. The same \f$\alpha_i\f$s are
 * used in all dimensions.
 *
 * \note The only place where the specific choice of 1d basis
 * comes in is the lower bound for the \f$\alpha_i\f$s, and so in general the
 * limiter can be applied to any 1d or tensor product of 1d basis functions.
 *
 * The limiting procedure must be applied from the highest derivatives to the
 * lowest, i.e. the highest coefficients to the lowest. Let us consider a 3d
 * element with \f$N+1\f$ coefficients in each dimension and denote the
 * coefficients as \f$c_{i,j,k}\f$. Then the limiting procedure starts at
 * \f$c_{N,N,N}\f$, followed by \f$c_{N,N,N-1}\f$, \f$c_{N,N-1,N}\f$, and
 * \f$c_{N-1,N,N}\f$. A detailed example is given below. Limiting is stopped if
 * all symmetric pairs of coefficients are left unchanged, i.e.
 * \f$c_{i,j,k}=\tilde{c}_{i,j,k}\f$. By all symmetric coefficients we mean
 * that, for example, \f$c_{N-i,N-j,N-k}\f$, \f$c_{N-j,N-i,N-k}\f$,
 * \f$c_{N-k,N-j,N-i}\f$, \f$c_{N-j,N-k,N-i}\f$, \f$c_{N-i,N-k,N-j}\f$, and
 * \f$c_{N-k,N-i,N-j}\f$ are not limited. As a concrete example, consider a 3d
 * element with 3 collocation points per dimension. Each limited coefficient is
 * defined as (though only computed if needed):
 *
 * \f{align*}{
 * \tilde{c}^{l,m,n}_{2,2,2}=\mathrm{minmod}
 *   &\left(c_{2,2,2}^{l,m,n},
 *          \alpha_2\left(c^{l+1,m,n}_{1,2,2}-c^{l,m,n}_{1,2,2}\right),
 *          \alpha_2\left(c^{l,m,n}_{1,2,2}-c^{l-1,m,n}_{1,2,2}\right),
 *     \right.\\
 *   &\;\;\;\;
 *          \alpha_2\left(c^{l,m+1,n}_{2,1,2}-c^{l,m,n}_{2,1,2}\right),
 *          \alpha_2\left(c^{l,m,n}_{2,1,2}-c^{l,m-1,n}_{2,1,2}\right),\\
 *   &\;\;\;\;\left.
 *          \alpha_2\left(c^{l,m,n+1}_{2,2,1}-c^{l,m,n}_{2,2,1}\right),
 *          \alpha_2\left(c^{l,m,n}_{2,2,1}-c^{l,m,n-1}_{2,2,1}\right)
 *     \right),\\
 * \tilde{c}^{l,m,n}_{2,2,1}=\mathrm{minmod}
 *   &\left(c_{2,2,1}^{l,m,n},
 *          \alpha_2\left(c^{l+1,m,n}_{1,2,1}-c^{l,m,n}_{1,2,1}\right),
 *          \alpha_2\left(c^{l,m,n}_{1,2,1}-c^{l-1,m,n}_{1,2,1}\right),
 *     \right.\\
 *   &\;\;\;\;
 *          \alpha_2\left(c^{l,m+1,n}_{2,1,1}-c^{l,m,n}_{2,1,1}\right),
 *          \alpha_2\left(c^{l,m,n}_{2,1,1}-c^{l,m-1,n}_{2,1,1}\right),\\
 *   &\;\;\;\;\left.
 *          \alpha_1\left(c^{l,m,n+1}_{2,2,0}-c^{l,m,n}_{2,2,0}\right),
 *          \alpha_1\left(c^{l,m,n}_{2,2,0}-c^{l,m,n-1}_{2,2,0}\right)
 *     \right),\\
 * \tilde{c}^{l,m,n}_{2,1,2}=\mathrm{minmod}
 *   &\left(c_{2,1,2}^{l,m,n},
 *          \alpha_2\left(c^{l+1,m,n}_{1,1,2}-c^{l,m,n}_{1,1,2}\right),
 *          \alpha_2\left(c^{l,m,n}_{1,1,2}-c^{l-1,m,n}_{1,1,2}\right),
 *     \right.\\
 *   &\;\;\;\;
 *          \alpha_1\left(c^{l,m+1,n}_{2,0,2}-c^{l,m,n}_{2,0,2}\right),
 *          \alpha_1\left(c^{l,m,n}_{2,0,2}-c^{l,m-1,n}_{2,0,2}\right),\\
 *   &\;\;\;\;\left.
 *          \alpha_2\left(c^{l,m,n+1}_{2,1,1}-c^{l,m,n}_{2,1,1}\right),
 *          \alpha_2\left(c^{l,m,n}_{2,1,1}-c^{l,m,n-1}_{2,1,1}\right)
 *     \right),\\
 * \tilde{c}^{l,m,n}_{1,2,2}=\mathrm{minmod}
 *   &\left(c_{1,2,2}^{l,m,n},
 *          \alpha_1\left(c^{l+1,m,n}_{0,2,2}-c^{l,m,n}_{0,2,2}\right),
 *          \alpha_1\left(c^{l,m,n}_{0,2,2}-c^{l-1,m,n}_{0,2,2}\right),
 *     \right.\\
 *   &\;\;\;\;
 *          \alpha_2\left(c^{l,m+1,n}_{1,1,2}-c^{l,m,n}_{1,1,2}\right),
 *          \alpha_2\left(c^{l,m,n}_{1,1,2}-c^{l,m-1,n}_{1,1,2}\right),\\
 *   &\;\;\;\;\left.
 *          \alpha_2\left(c^{l,m,n+1}_{1,2,1}-c^{l,m,n}_{1,2,1}\right),
 *          \alpha_2\left(c^{l,m,n}_{1,2,1}-c^{l,m,n-1}_{1,2,1}\right)
 *     \right),\\
 * \tilde{c}^{l,m,n}_{2,2,0}=\mathrm{minmod}
 *   &\left(c_{2,2,0}^{l,m,n},
 *          \alpha_2\left(c^{l+1,m,n}_{1,2,0}-c^{l,m,n}_{1,2,0}\right),
 *          \alpha_2\left(c^{l,m,n}_{1,2,0}-c^{l-1,m,n}_{1,2,0}\right),
 *     \right.\\
 *   &\;\;\;\;\left.
 *          \alpha_2\left(c^{l,m+1,n}_{2,1,0}-c^{l,m,n}_{2,1,0}\right),
 *          \alpha_2\left(c^{l,m,n}_{2,1,0}-c^{l,m-1,n}_{2,1,0}\right)
 *     \right),\\
 * \tilde{c}^{l,m,n}_{2,0,2}=\mathrm{minmod}
 *   &\left(c_{2,0,2}^{l,m,n},
 *          \alpha_2\left(c^{l+1,m,n}_{1,0,2}-c^{l,m,n}_{1,0,2}\right),
 *          \alpha_2\left(c^{l,m,n}_{1,0,2}-c^{l-1,m,n}_{1,0,2}\right),
 *     \right.\\
 *   &\;\;\;\;\left.
 *          \alpha_2\left(c^{l,m,n+1}_{2,0,1}-c^{l,m,n}_{2,0,1}\right),
 *          \alpha_2\left(c^{l,m,n}_{2,0,1}-c^{l,m,n-1}_{2,0,1}\right)
 *     \right),\\
 * \tilde{c}^{l,m,n}_{0,2,2}=\mathrm{minmod}
 *   &\left(c_{0,2,2}^{l,m,n},
 *          \alpha_2\left(c^{l,m+1,n}_{0,1,2}-c^{l,m,n}_{0,1,2}\right),
 *          \alpha_2\left(c^{l,m,n}_{0,1,2}-c^{l,m-1,n}_{0,1,2}\right),
 *     \right.\\
 *   &\;\;\;\;\left.
 *          \alpha_2\left(c^{l,m,n+1}_{0,2,1}-c^{l,m,n}_{0,2,1}\right),
 *          \alpha_2\left(c^{l,m,n}_{0,2,1}-c^{l,m,n-1}_{0,2,1}\right)
 *     \right),\\
 * \tilde{c}^{l,m,n}_{2,1,1}=\mathrm{minmod}
 *   &\left(c_{2,1,1}^{l,m,n},
 *          \alpha_2\left(c^{l+1,m,n}_{1,1,1}-c^{l,m,n}_{1,1,1}\right),
 *          \alpha_2\left(c^{l,m,n}_{1,1,1}-c^{l-1,m,n}_{1,1,1}\right),
 *     \right.\\
 *   &\;\;\;\;
 *          \alpha_1\left(c^{l,m+1,n}_{2,0,1}-c^{l,m,n}_{2,0,1}\right),
 *          \alpha_1\left(c^{l,m,n}_{2,0,1}-c^{l,m-1,n}_{2,0,1}\right),\\
 *   &\;\;\;\;\left.
 *          \alpha_1\left(c^{l,m,n+1}_{2,1,0}-c^{l,m,n}_{2,1,0}\right),
 *          \alpha_1\left(c^{l,m,n}_{2,1,0}-c^{l,m,n-1}_{2,1,0}\right)
 *     \right),\\
 * \tilde{c}^{l,m,n}_{1,2,1}=\mathrm{minmod}
 *   &\left(c_{1,2,1}^{l,m,n},
 *          \alpha_1\left(c^{l+1,m,n}_{0,2,1}-c^{l,m,n}_{0,2,1}\right),
 *          \alpha_1\left(c^{l,m,n}_{0,2,1}-c^{l-1,m,n}_{0,2,1}\right),
 *     \right.\\
 *   &\;\;\;\;
 *          \alpha_2\left(c^{l,m+1,n}_{1,1,1}-c^{l,m,n}_{1,1,1}\right),
 *          \alpha_2\left(c^{l,m,n}_{1,1,1}-c^{l,m-1,n}_{1,1,1}\right),\\
 *   &\;\;\;\;\left.
 *          \alpha_1\left(c^{l,m,n+1}_{1,2,0}-c^{l,m,n}_{1,2,0}\right),
 *          \alpha_1\left(c^{l,m,n}_{1,2,0}-c^{l,m,n-1}_{1,2,0}\right)
 *     \right),\\
 * \tilde{c}^{l,m,n}_{1,1,2}=\mathrm{minmod}
 *   &\left(c_{1,1,2}^{l,m,n},
 *          \alpha_1\left(c^{l+1,m,n}_{0,1,2}-c^{l,m,n}_{0,1,2}\right),
 *          \alpha_1\left(c^{l,m,n}_{0,1,2}-c^{l-1,m,n}_{0,1,2}\right),
 *     \right.\\
 *   &\;\;\;\;
 *          \alpha_1\left(c^{l,m+1,n}_{1,0,2}-c^{l,m,n}_{1,0,2}\right),
 *          \alpha_1\left(c^{l,m,n}_{1,0,2}-c^{l,m-1,n}_{1,0,2}\right),\\
 *   &\;\;\;\;\left.
 *          \alpha_2\left(c^{l,m,n+1}_{1,1,1}-c^{l,m,n}_{1,1,1}\right),
 *          \alpha_2\left(c^{l,m,n}_{1,1,1}-c^{l,m,n-1}_{1,1,1}\right)
 *     \right),
 * \f}
 * \f{align*}{
 * \tilde{c}^{l,m,n}_{2,1,0}=\mathrm{minmod}
 *   &\left(c_{2,1,0}^{l,m,n},
 *          \alpha_2\left(c^{l+1,m,n}_{1,1,0}-c^{l,m,n}_{1,1,0}\right),
 *          \alpha_2\left(c^{l,m,n}_{1,1,0}-c^{l-1,m,n}_{1,1,0}\right),
 *     \right.\\
 *   &\;\;\;\;\left.
 *          \alpha_1\left(c^{l,m+1,n}_{2,0,0}-c^{l,m,n}_{2,0,0}\right),
 *          \alpha_1\left(c^{l,m,n}_{2,0,0}-c^{l,m-1,n}_{2,0,0}\right)
 *     \right),\\
 * \tilde{c}^{l,m,n}_{2,0,1}=\mathrm{minmod}
 *   &\left(c_{2,0,1}^{l,m,n},
 *          \alpha_2\left(c^{l+1,m,n}_{1,0,1}-c^{l,m,n}_{1,0,1}\right),
 *          \alpha_2\left(c^{l,m,n}_{1,0,1}-c^{l-1,m,n}_{1,0,1}\right),
 *     \right.\\
 *   &\;\;\;\;\left.
 *          \alpha_1\left(c^{l,m,n+1}_{2,0,0}-c^{l,m,n}_{2,0,0}\right),
 *          \alpha_1\left(c^{l,m,n}_{2,0,0}-c^{l,m,n-1}_{2,0,0}\right)
 *     \right),\\
 * \tilde{c}^{l,m,n}_{1,2,0}=\mathrm{minmod}
 *   &\left(c_{1,2,0}^{l,m,n},
 *          \alpha_1\left(c^{l+1,m,n}_{0,2,0}-c^{l,m,n}_{0,2,0}\right),
 *          \alpha_1\left(c^{l,m,n}_{0,2,0}-c^{l-1,m,n}_{0,2,0}\right),
 *     \right.\\
 *   &\;\;\;\;\left.
 *          \alpha_2\left(c^{l,m+1,n}_{1,1,0}-c^{l,m,n}_{1,1,0}\right),
 *          \alpha_2\left(c^{l,m,n}_{1,1,0}-c^{l,m-1,n}_{1,1,0}\right)
 *     \right),\\
 * \tilde{c}^{l,m,n}_{1,0,2}=\mathrm{minmod}
 *   &\left(c_{1,0,2}^{l,m,n},
 *          \alpha_1\left(c^{l+1,m,n}_{0,0,2}-c^{l,m,n}_{0,0,2}\right),
 *          \alpha_1\left(c^{l,m,n}_{0,0,2}-c^{l-1,m,n}_{0,0,2}\right),
 *     \right.\\
 *   &\;\;\;\;\left.
 *          \alpha_2\left(c^{l,m,n+1}_{1,0,1}-c^{l,m,n}_{1,0,1}\right),
 *          \alpha_2\left(c^{l,m,n}_{1,0,1}-c^{l,m,n-1}_{1,0,1}\right)
 *     \right),\\
 * \tilde{c}^{l,m,n}_{0,1,2}=\mathrm{minmod}
 *   &\left(c_{0,1,2}^{l,m,n},
 *          \alpha_1\left(c^{l,m+1,n}_{0,0,2}-c^{l,m,n}_{0,0,2}\right),
 *          \alpha_1\left(c^{l,m,n}_{0,0,2}-c^{l,m-1,n}_{0,0,2}\right),
 *   \right. \\
 *   &\;\;\;\;\left.
 *          \alpha_2\left(c^{l,m,n+1}_{0,1,1}-c^{l,m,n}_{0,1,1}\right),
 *          \alpha_2\left(c^{l,m,n}_{0,1,1}-c^{l,m,n-1}_{0,1,1}\right)
 *     \right),\\
 * \tilde{c}^{l,m,n}_{0,2,1}=\mathrm{minmod}
 *   &\left(c_{0,2,1}^{l,m,n},
 *          \alpha_2\left(c^{l,m+1,n}_{0,1,1}-c^{l,m,n}_{0,1,1}\right),
 *          \alpha_2\left(c^{l,m,n}_{0,1,1}-c^{l,m-1,n}_{0,1,1}\right),
 *   \right.\\
 *   &\;\;\;\;\left.
 *          \alpha_1\left(c^{l,m,n+1}_{0,2,0}-c^{l,m,n}_{0,2,0}\right),
 *          \alpha_1\left(c^{l,m,n}_{0,2,0}-c^{l,m,n-1}_{0,2,0}\right)
 *     \right),
 * \f}
 * \f{align*}{
 * \tilde{c}^{l,m,n}_{2,0,0}=\mathrm{minmod}
 *   &\left(c_{2,0,0}^{l,m,n},
 *          \alpha_2\left(c^{l+1,m,n}_{1,0,0}-c^{l,m,n}_{1,0,0}\right),
 *          \alpha_2\left(c^{l,m,n}_{1,0,0}-c^{l-1,m,n}_{1,0,0}\right)
 *     \right),\\
 * \tilde{c}^{l,m,n}_{0,2,0}=\mathrm{minmod}
 *   &\left(c_{0,2,0}^{l,m,n},
 *          \alpha_2\left(c^{l,m+1,n}_{0,1,0}-c^{l,m,n}_{0,1,0}\right),
 *          \alpha_2\left(c^{l,m,n}_{0,1,0}-c^{l,m-1,n}_{0,1,0}\right)
 *     \right),\\
 * \tilde{c}^{l,m,n}_{0,0,2}=\mathrm{minmod}
 *   &\left(c_{0,0,2}^{l,m,n},
 *          \alpha_2\left(c^{l,m,n+1}_{0,0,1}-c^{l,m,n}_{0,0,1}\right),
 *          \alpha_2\left(c^{l,m,n}_{0,0,1}-c^{l,m,n-1}_{0,0,1}\right)
 *     \right),\\
 * \tilde{c}^{l,m,n}_{1,1,1}=\mathrm{minmod}
 *   &\left(c_{1,1,1}^{l,m,n},
 *          \alpha_1\left(c^{l+1,m,n}_{0,1,1}-c^{l,m,n}_{0,1,1}\right),
 *          \alpha_1\left(c^{l,m,n}_{0,1,1}-c^{l-1,m,n}_{0,1,1}\right),
 *     \right.\\
 *   &\;\;\;\;
 *          \alpha_1\left(c^{l,m+1,n}_{1,0,1}-c^{l,m,n}_{1,0,1}\right),
 *          \alpha_1\left(c^{l,m,n}_{1,0,1}-c^{l,m-1,n}_{1,0,1}\right),\\
 *   &\;\;\;\;\left.
 *          \alpha_1\left(c^{l,m,n+1}_{1,1,0}-c^{l,m,n}_{1,1,0}\right),
 *          \alpha_1\left(c^{l,m,n}_{1,1,0}-c^{l,m,n-1}_{1,1,0}\right)
 *     \right),\\
 * \tilde{c}^{l,m,n}_{1,1,0}=\mathrm{minmod}
 *   &\left(c_{1,1,0}^{l,m,n},
 *          \alpha_1\left(c^{l+1,m,n}_{0,1,0}-c^{l,m,n}_{0,1,0}\right),
 *          \alpha_1\left(c^{l,m,n}_{0,1,0}-c^{l-1,m,n}_{0,1,0}\right),
 *     \right.\\
 *   &\;\;\;\;\left.
 *          \alpha_1\left(c^{l,m+1,n}_{1,0,0}-c^{l,m,n}_{1,0,0}\right),
 *          \alpha_1\left(c^{l,m,n}_{1,0,0}-c^{l,m-1,n}_{1,0,0}\right),
 *     \right),\\
 * \tilde{c}^{l,m,n}_{1,0,1}=\mathrm{minmod}
 *   &\left(c_{1,0,1}^{l,m,n},
 *          \alpha_1\left(c^{l+1,m,n}_{0,0,1}-c^{l,m,n}_{0,0,1}\right),
 *          \alpha_1\left(c^{l,m,n}_{0,0,1}-c^{l-1,m,n}_{0,0,1}\right),
 *     \right.\\
 *   &\;\;\;\;\left.
 *          \alpha_1\left(c^{l,m,n+1}_{1,0,0}-c^{l,m,n}_{1,0,0}\right),
 *          \alpha_1\left(c^{l,m,n}_{1,0,0}-c^{l,m,n-1}_{1,0,0}\right)
 *     \right),\\
 * \tilde{c}^{l,m,n}_{0,1,1}=\mathrm{minmod}
 *   &\left(c_{0,1,1}^{l,m,n},
 *          \alpha_1\left(c^{l,m+1,n}_{0,0,1}-c^{l,m,n}_{0,0,1}\right),
 *          \alpha_1\left(c^{l,m,n}_{0,0,1}-c^{l,m-1,n}_{0,0,1}\right),
 *   \right.\\
 *   &\;\;\;\;\left.
 *          \alpha_1\left(c^{l,m,n+1}_{0,1,0}-c^{l,m,n}_{0,1,0}\right),
 *          \alpha_1\left(c^{l,m,n}_{0,1,0}-c^{l,m,n-1}_{0,1,0}\right)
 *     \right),\\
 * \tilde{c}^{l,m,n}_{1,0,0}=\mathrm{minmod}
 *   &\left(c_{1,0,0}^{l,m,n},
 *          \alpha_1\left(c^{l+1,m,n}_{0,0,0}-c^{l,m,n}_{0,0,0}\right),
 *          \alpha_1\left(c^{l,m,n}_{0,0,0}-c^{l-1,m,n}_{0,0,0}\right)
 *     \right),\\
 * \tilde{c}^{l,m,n}_{0,1,0}=\mathrm{minmod}
 *   &\left(c_{0,1,0}^{l,m,n},
 *          \alpha_1\left(c^{l,m+1,n}_{0,0,0}-c^{l,m,n}_{0,0,0}\right),
 *          \alpha_1\left(c^{l,m,n}_{0,0,0}-c^{l,m-1,n}_{0,0,0}\right)
 *     \right),\\
 * \tilde{c}^{l,m,n}_{0,0,1}=\mathrm{minmod}
 *   &\left(c_{0,0,1}^{l,m,n},
 *          \alpha_1\left(c^{l,m,n+1}_{0,0,0}-c^{l,m,n}_{0,0,0}\right),
 *          \alpha_1\left(c^{l,m,n}_{0,0,0}-c^{l,m,n-1}_{0,0,0}\right)
 *     \right),
 * \f}
 *
 *
 * The algorithm to perform the limiting is as follows:
 *
 * - limit \f$c_{2,2,2}\f$ (i.e. \f$c_{2,2,2}\leftarrow\tilde{c}_{2,2,2}\f$), if
 *   not changed, stop
 * - limit \f$c_{2,2,1}\f$, \f$c_{2,1,2}\f$, and \f$c_{1,2,2}\f$, if all not
 * changed, stop
 * - limit \f$c_{2,2,0}\f$, \f$c_{2,0,2}\f$, and \f$c_{0,2,2}\f$, if all not
 * changed, stop
 * - limit \f$c_{2,1,1}\f$, \f$c_{1,2,1}\f$, and \f$c_{1,1,2}\f$, if all not
 * changed, stop
 * - limit \f$c_{2,1,0}\f$, \f$c_{2,0,1}\f$, \f$c_{1,2,0}\f$, \f$c_{1,0,2}\f$,
 * \f$c_{0,1,2}\f$, and \f$c_{0,2,1}\f$, if all not changed, stop
 * - limit \f$c_{2,0,0}\f$, \f$c_{0,2,0}\f$, and \f$c_{0,0,2}\f$, if all not
 * changed, stop
 * - limit \f$c_{1,1,1}\f$, if not changed, stop
 * - limit \f$c_{1,1,0}\f$, \f$c_{1,0,1}\f$, and \f$c_{0,1,1}\f$, if all not
 * changed, stop
 * - limit \f$c_{1,0,0}\f$, \f$c_{0,1,0}\f$, and \f$c_{0,0,1}\f$, if all not
 * changed, stop
 *
 * The 1d and 2d implementations are straightforward restrictions of the
 * described algorithm.
 *
 * #### Limitations:
 *
 * - We currently recompute the spectral coefficients for local use, after
 *   having computed these same coefficients for sending to the neighbors. We
 *   should be able to avoid this either by storing the coefficients in the
 *   DataBox or by allowing the limiters' `packaged_data` function to return an
 *   object to be passed as an additional argument to the `operator()` (still
 *   would need to be stored in the DataBox).
 * - We cannot handle the case where neighbors have more/fewer
 *   coefficients. In the case that the neighbor has more coefficients we could
 *   just ignore the higher coefficients. In the case that the neighbor has
 *   fewer coefficients we have a few choices.
 * - Having a different number of collocation points in different directions is
 *   not supported. However, it is straightforward to handle this case. The
 *   outermost loop should be in the direction with the most collocation points,
 *   while the inner most loop should be over the direction with the fewest
 *   collocation points. The highest to lowest coefficients can then be limited
 *   appropriately again.
 * - h-refinement is not supported, but there is one reasonably straightforward
 *   implementation that may work. In this case we would ignore refinement that
 *   is not in the direction of the neighbor, treating the element as
 *   simply having multiple neighbors in that direction. The only change would
 *   be accounting for different refinement in the direction of the neighor,
 *   which should be easy to add since the differences in coefficients in
 *   Eq.\f$\ref{eq:krivodonova 3d minmod}\f$ will just be multiplied by
 *   non-unity factors.
 */
template <size_t VolumeDim, typename... Tags>
class Krivodonova<VolumeDim, tmpl::list<Tags...>> {
 public:
  /*!
   * \brief The \f$\alpha_i\f$ values in the Krivodonova algorithm.
   */
  struct Alphas {
    using type = std::array<
        double, Spectral::maximum_number_of_points<Spectral::Basis::Legendre>>;
    static constexpr Options::String help = {
        "The alpha parameters of the Krivodonova limiter"};
  };
  /*!
   * \brief Turn the limiter off
   *
   * This option exists to temporarily disable the limiter for debugging
   * purposes. For problems where limiting is not needed, the preferred
   * approach is to not compile the limiter into the executable.
   */
  struct DisableForDebugging {
    using type = bool;
    static type suggested_value() noexcept { return false; }
    static constexpr Options::String help = {"Disable the limiter"};
  };

  using options = tmpl::list<Alphas, DisableForDebugging>;
  static constexpr Options::String help = {
      "The hierarchical limiter of Krivodonova.\n\n"
      "This limiter works by limiting the highest modal "
      "coefficients/derivatives using an aggressive minmod approach, "
      "decreasing in modal coefficient order until no more limiting is "
      "necessary."};

  explicit Krivodonova(
      std::array<double,
                 Spectral::maximum_number_of_points<Spectral::Basis::Legendre>>
          alphas,
      bool disable_for_debugging = false, const Options::Context& context = {});

  Krivodonova() = default;
  Krivodonova(const Krivodonova&) = delete;
  Krivodonova& operator=(const Krivodonova&) = delete;
  Krivodonova(Krivodonova&&) = default;
  Krivodonova& operator=(Krivodonova&&) = default;
  ~Krivodonova() = default;

  // NOLINTNEXTLINE(google-runtime-references)
  void pup(PUP::er& p) noexcept;

  bool operator==(const Krivodonova& rhs) const noexcept;

  struct PackagedData {
    Variables<tmpl::list<::Tags::Modal<Tags>...>> modal_volume_data;
    Mesh<VolumeDim> mesh;

    // clang-tidy: google-runtime-references
    void pup(PUP::er& p) noexcept {  // NOLINT
      p | modal_volume_data;
      p | mesh;
    }
  };

  using package_argument_tags =
      tmpl::list<Tags..., domain::Tags::Mesh<VolumeDim>>;

  /// \brief Package data for sending to neighbor elements.
  void package_data(gsl::not_null<PackagedData*> packaged_data,
                    const typename Tags::type&... tensors,
                    const Mesh<VolumeDim>& mesh,
                    const OrientationMap<VolumeDim>& orientation_map) const
      noexcept;

  using limit_tags = tmpl::list<Tags...>;
  using limit_argument_tags = tmpl::list<domain::Tags::Element<VolumeDim>,
                                         domain::Tags::Mesh<VolumeDim>>;

  bool operator()(
      const gsl::not_null<std::add_pointer_t<typename Tags::type>>... tensors,
      const Element<VolumeDim>& element, const Mesh<VolumeDim>& mesh,
      const std::unordered_map<
          std::pair<Direction<VolumeDim>, ElementId<VolumeDim>>, PackagedData,
          boost::hash<std::pair<Direction<VolumeDim>, ElementId<VolumeDim>>>>&
          neighbor_data) const noexcept;

 private:
  template <typename Tag>
  char limit_one_tensor(
      gsl::not_null<Variables<tmpl::list<::Tags::Modal<Tags>...>>*> coeffs_self,
      gsl::not_null<bool*> limited_any_component, const Mesh<1>& mesh,
      const std::unordered_map<
          std::pair<Direction<1>, ElementId<1>>, PackagedData,
          boost::hash<std::pair<Direction<1>, ElementId<1>>>>& neighbor_data)
      const noexcept;
  template <typename Tag>
  char limit_one_tensor(
      gsl::not_null<Variables<tmpl::list<::Tags::Modal<Tags>...>>*> coeffs_self,
      gsl::not_null<bool*> limited_any_component, const Mesh<2>& mesh,
      const std::unordered_map<
          std::pair<Direction<2>, ElementId<2>>, PackagedData,
          boost::hash<std::pair<Direction<2>, ElementId<2>>>>& neighbor_data)
      const noexcept;
  template <typename Tag>
  char limit_one_tensor(
      gsl::not_null<Variables<tmpl::list<::Tags::Modal<Tags>...>>*> coeffs_self,
      gsl::not_null<bool*> limited_any_component, const Mesh<3>& mesh,
      const std::unordered_map<
          std::pair<Direction<3>, ElementId<3>>, PackagedData,
          boost::hash<std::pair<Direction<3>, ElementId<3>>>>& neighbor_data)
      const noexcept;

  template <typename Tag, size_t Dim>
  char fill_variables_tag_with_spectral_coeffs(
      gsl::not_null<Variables<tmpl::list<::Tags::Modal<Tags>...>>*>
          modal_coeffs,
      const typename Tag::type& nodal_tensor, const Mesh<Dim>& mesh) const
      noexcept;

  std::array<double,
             Spectral::maximum_number_of_points<Spectral::Basis::Legendre>>
      alphas_ = make_array<
          Spectral::maximum_number_of_points<Spectral::Basis::Legendre>>(
          std::numeric_limits<double>::signaling_NaN());
  bool disable_for_debugging_{false};
};

template <size_t VolumeDim, typename... Tags>
Krivodonova<VolumeDim, tmpl::list<Tags...>>::Krivodonova(
    std::array<double,
               Spectral::maximum_number_of_points<Spectral::Basis::Legendre>>
        alphas,
    bool disable_for_debugging, const Options::Context& context)
    : alphas_(alphas), disable_for_debugging_(disable_for_debugging) {
  // See the main documentation for an explanation of why these bounds are
  // different from those of Krivodonova 2007
  if (alg::any_of(alphas_, [](const double t) noexcept {
        return t > 1.0 or t <= 0.0;
      })) {
    PARSE_ERROR(context,
                "The alphas in the Krivodonova limiter must be in the range "
                "(0,1].");
  }
}

template <size_t VolumeDim, typename... Tags>
void Krivodonova<VolumeDim, tmpl::list<Tags...>>::package_data(
    const gsl::not_null<PackagedData*> packaged_data,
    const typename Tags::type&... tensors, const Mesh<VolumeDim>& mesh,
    const OrientationMap<VolumeDim>& orientation_map) const noexcept {
  if (UNLIKELY(disable_for_debugging_)) {
    // Do not initialize packaged_data
    return;
  }

  packaged_data->modal_volume_data.initialize(mesh.number_of_grid_points());
  // perform nodal coefficients to modal coefficients transformation on each
  // tensor component
  expand_pack(fill_variables_tag_with_spectral_coeffs<Tags>(
      &(packaged_data->modal_volume_data), tensors, mesh)...);

  packaged_data->modal_volume_data = orient_variables(
      packaged_data->modal_volume_data, mesh.extents(), orientation_map);

  packaged_data->mesh = orientation_map(mesh);
}

template <size_t VolumeDim, typename... Tags>
bool Krivodonova<VolumeDim, tmpl::list<Tags...>>::operator()(
    const gsl::not_null<std::add_pointer_t<typename Tags::type>>... tensors,
    const Element<VolumeDim>& element, const Mesh<VolumeDim>& mesh,
    const std::unordered_map<
        std::pair<Direction<VolumeDim>, ElementId<VolumeDim>>, PackagedData,
        boost::hash<std::pair<Direction<VolumeDim>, ElementId<VolumeDim>>>>&
        neighbor_data) const noexcept {
  if (UNLIKELY(disable_for_debugging_)) {
    // Do not modify input tensors
    return false;
  }
  if (UNLIKELY(mesh != Mesh<VolumeDim>(mesh.extents()[0], mesh.basis()[0],
                                       mesh.quadrature()[0]))) {
    ERROR(
        "The Krivodonova limiter does not yet support non-uniform number of "
        "collocation points, bases, and quadrature in each direction. The "
        "mesh is: "
        << mesh);
  }
  if (UNLIKELY(alg::any_of(element.neighbors(),
                           [](const auto& direction_neighbors) noexcept {
                             return direction_neighbors.second.size() != 1;
                           }))) {
    ERROR("The Krivodonova limiter does not yet support h-refinement");
  }
  alg::for_each(neighbor_data, [&mesh](const auto& id_packaged_data) noexcept {
    if (UNLIKELY(id_packaged_data.second.mesh != mesh)) {
      ERROR(
          "The Krivodonova limiter does not yet support differing meshes "
          "between neighbors. Self mesh is: "
          << mesh << " neighbor mesh is: " << id_packaged_data.second.mesh);
    }
  });

  // Compute local modal coefficients
  Variables<tmpl::list<::Tags::Modal<Tags>...>> coeffs_self(
      mesh.number_of_grid_points(), 0.0);
  expand_pack(fill_variables_tag_with_spectral_coeffs<Tags>(&coeffs_self,
                                                            *tensors, mesh)...);

  // Perform the limiting on the modal coefficients
  bool limited_any_component = false;
  expand_pack(limit_one_tensor<::Tags::Modal<Tags>>(
      make_not_null(&coeffs_self), make_not_null(&limited_any_component), mesh,
      neighbor_data)...);

  // transform back to nodal coefficients
  const auto wrap_copy_nodal_coeffs =
      [&mesh, &coeffs_self](auto tag, const auto tensor) noexcept {
        auto& coeffs_tensor = get<decltype(tag)>(coeffs_self);
        auto tensor_it = tensor->begin();
        for (auto coeffs_it = coeffs_tensor.begin();
             coeffs_it != coeffs_tensor.end();
             (void)++coeffs_it, (void)++tensor_it) {
          to_nodal_coefficients(make_not_null(&*tensor_it), *coeffs_it, mesh);
        }
        return '0';
      };
  expand_pack(wrap_copy_nodal_coeffs(::Tags::Modal<Tags>{}, tensors)...);

  return limited_any_component;
}

template <size_t VolumeDim, typename... Tags>
template <typename Tag>
char Krivodonova<VolumeDim, tmpl::list<Tags...>>::limit_one_tensor(
    const gsl::not_null<Variables<tmpl::list<::Tags::Modal<Tags>...>>*>
        coeffs_self,
    const gsl::not_null<bool*> limited_any_component, const Mesh<1>& mesh,
    const std::unordered_map<
        std::pair<Direction<1>, ElementId<1>>, PackagedData,
        boost::hash<std::pair<Direction<1>, ElementId<1>>>>& neighbor_data)
    const noexcept {
  using tensor_type = typename Tag::type;
  for (size_t storage_index = 0; storage_index < tensor_type::size();
       ++storage_index) {
    // for each coefficient...
    for (size_t i = mesh.extents()[0] - 1; i > 0; --i) {
      auto& self_coeffs = get<Tag>(*coeffs_self)[storage_index];
      double min_abs_coeff = std::abs(self_coeffs[i]);
      const double sgn_of_coeff = sgn(self_coeffs[i]);
      bool sgns_all_equal = true;
      for (const auto& kv : neighbor_data) {
        const auto& neighbor_coeffs =
            get<Tag>(kv.second.modal_volume_data)[storage_index];
        const double tmp = kv.first.first.sign() * gsl::at(alphas_, i) *
                           (neighbor_coeffs[i - 1] - self_coeffs[i - 1]);

        min_abs_coeff = std::min(min_abs_coeff, std::abs(tmp));
        sgns_all_equal &= sgn(tmp) == sgn_of_coeff;
        if (not sgns_all_equal) {
          self_coeffs[i] = 0.0;
          break;
        }
      }
      if (sgns_all_equal) {
        const double tmp = sgn_of_coeff * min_abs_coeff;
        if (tmp == self_coeffs[i]) {
          break;
        }
        *limited_any_component |= true;
        self_coeffs[i] = tmp;
      }
    }
  }
  return '0';
}

template <size_t VolumeDim, typename... Tags>
template <typename Tag>
char Krivodonova<VolumeDim, tmpl::list<Tags...>>::limit_one_tensor(
    const gsl::not_null<Variables<tmpl::list<::Tags::Modal<Tags>...>>*>
        coeffs_self,
    const gsl::not_null<bool*> limited_any_component, const Mesh<2>& mesh,
    const std::unordered_map<
        std::pair<Direction<2>, ElementId<2>>, PackagedData,
        boost::hash<std::pair<Direction<2>, ElementId<2>>>>& neighbor_data)
    const noexcept {
  using tensor_type = typename Tag::type;
  const auto minmod = [&coeffs_self, &mesh, &neighbor_data, this](
                          const size_t local_i, const size_t local_j,
                          const size_t local_tensor_storage_index) noexcept {
    const auto& self_coeffs =
        get<Tag>(*coeffs_self)[local_tensor_storage_index];
    double min_abs_coeff = std::abs(
        self_coeffs[mesh.storage_index(Index<VolumeDim>{local_i, local_j})]);
    const double sgn_of_coeff = sgn(
        self_coeffs[mesh.storage_index(Index<VolumeDim>{local_i, local_j})]);
    for (const auto& kv : neighbor_data) {
      const Direction<2>& dir = kv.first.first;
      const auto& neighbor_coeffs =
          get<Tag>(kv.second.modal_volume_data)[local_tensor_storage_index];
      const size_t index_i =
          dir.axis() == Direction<VolumeDim>::Axis::Xi ? local_i - 1 : local_i;
      const size_t index_j =
          dir.axis() == Direction<VolumeDim>::Axis::Eta ? local_j - 1 : local_j;
      // skip neighbors where we cannot compute a finite difference in that
      // direction because we are already at the lowest coefficient.
      if (index_i == std::numeric_limits<size_t>::max() or
          index_j == std::numeric_limits<size_t>::max()) {
        continue;
      }
      const size_t alpha_index =
          dir.axis() == Direction<VolumeDim>::Axis::Xi ? local_i : local_j;
      const double tmp =
          dir.sign() * gsl::at(alphas_, alpha_index) *
          (neighbor_coeffs[mesh.storage_index(
               Index<VolumeDim>{index_i, index_j})] -
           self_coeffs[mesh.storage_index(Index<VolumeDim>{index_i, index_j})]);

      min_abs_coeff = std::min(min_abs_coeff, std::abs(tmp));
      if (sgn(tmp) != sgn_of_coeff) {
        return 0.0;
      }
    }
    return sgn_of_coeff * min_abs_coeff;
  };

  for (size_t tensor_storage_index = 0;
       tensor_storage_index < tensor_type::size(); ++tensor_storage_index) {
    // for each coefficient...
    for (size_t i = mesh.extents()[0] - 1; i > 0; --i) {
      for (size_t j = i; j < mesh.extents()[1]; --j) {
        // Check if we are done limiting, and if so we move on to the next
        // tensor component.
        auto& self_coeffs = get<Tag>(*coeffs_self)[tensor_storage_index];
        // We treat the different cases separately to reduce the number of
        // times we call minmod, not because it is required for correctness.
        if (UNLIKELY(i == j)) {
          const double tmp = minmod(i, j, tensor_storage_index);
          if (tmp == self_coeffs[mesh.storage_index(Index<VolumeDim>{i, j})]) {
            goto next_tensor_index;
          }
          *limited_any_component |= true;
          self_coeffs[mesh.storage_index(Index<VolumeDim>{i, j})] = tmp;
        } else {
          const double tmp_ij = minmod(i, j, tensor_storage_index);
          const double tmp_ji = minmod(j, i, tensor_storage_index);
          if (tmp_ij ==
                  self_coeffs[mesh.storage_index(Index<VolumeDim>{i, j})] and
              tmp_ji ==
                  self_coeffs[mesh.storage_index(Index<VolumeDim>{j, i})]) {
            goto next_tensor_index;
          }
          *limited_any_component |= true;
          self_coeffs[mesh.storage_index(Index<VolumeDim>{i, j})] = tmp_ij;
          self_coeffs[mesh.storage_index(Index<VolumeDim>{j, i})] = tmp_ji;
        }
      }
    }
  next_tensor_index:
    continue;
  }
  return '0';
}

template <size_t VolumeDim, typename... Tags>
template <typename Tag>
char Krivodonova<VolumeDim, tmpl::list<Tags...>>::limit_one_tensor(
    const gsl::not_null<Variables<tmpl::list<::Tags::Modal<Tags>...>>*>
        coeffs_self,
    const gsl::not_null<bool*> limited_any_component, const Mesh<3>& mesh,
    const std::unordered_map<
        std::pair<Direction<3>, ElementId<3>>, PackagedData,
        boost::hash<std::pair<Direction<3>, ElementId<3>>>>& neighbor_data)
    const noexcept {
  using tensor_type = typename Tag::type;
  const auto minmod = [&coeffs_self, &mesh, &neighbor_data, this](
                          const size_t local_i, const size_t local_j,
                          const size_t local_k,
                          const size_t local_tensor_storage_index) noexcept {
    const auto& self_coeffs =
        get<Tag>(*coeffs_self)[local_tensor_storage_index];
    double min_abs_coeff = std::abs(self_coeffs[mesh.storage_index(
        Index<VolumeDim>{local_i, local_j, local_k})]);
    const double sgn_of_coeff = sgn(self_coeffs[mesh.storage_index(
        Index<VolumeDim>{local_i, local_j, local_k})]);
    for (const auto& kv : neighbor_data) {
      const Direction<3>& dir = kv.first.first;
      const auto& neighbor_coeffs =
          get<Tag>(kv.second.modal_volume_data)[local_tensor_storage_index];
      const size_t index_i =
          dir.axis() == Direction<VolumeDim>::Axis::Xi ? local_i - 1 : local_i;
      const size_t index_j =
          dir.axis() == Direction<VolumeDim>::Axis::Eta ? local_j - 1 : local_j;
      const size_t index_k = dir.axis() == Direction<VolumeDim>::Axis::Zeta
                                 ? local_k - 1
                                 : local_k;
      // skip neighbors where we cannot compute a finite difference in that
      // direction because we are already at the lowest coefficient.
      if (index_i == std::numeric_limits<size_t>::max() or
          index_j == std::numeric_limits<size_t>::max() or
          index_k == std::numeric_limits<size_t>::max()) {
        continue;
      }
      const size_t alpha_index =
          dir.axis() == Direction<VolumeDim>::Axis::Xi
              ? local_i
              : dir.axis() == Direction<VolumeDim>::Axis::Eta ? local_j
                                                              : local_k;
      const double tmp = dir.sign() * gsl::at(alphas_, alpha_index) *
                         (neighbor_coeffs[mesh.storage_index(
                              Index<VolumeDim>{index_i, index_j, index_k})] -
                          self_coeffs[mesh.storage_index(
                              Index<VolumeDim>{index_i, index_j, index_k})]);

      min_abs_coeff = std::min(min_abs_coeff, std::abs(tmp));
      if (sgn(tmp) != sgn_of_coeff) {
        return 0.0;
      }
    }
    return sgn_of_coeff * min_abs_coeff;
  };

  for (size_t tensor_storage_index = 0;
       tensor_storage_index < tensor_type::size(); ++tensor_storage_index) {
    // for each coefficient...
    for (size_t i = mesh.extents()[0] - 1; i > 0; --i) {
      for (size_t j = i; j < mesh.extents()[1]; --j) {
        for (size_t k = j; k < mesh.extents()[2]; --k) {
          // Check if we are done limiting, and if so we move on to the next
          // tensor component.
          auto& self_coeffs = get<Tag>(*coeffs_self)[tensor_storage_index];
          // We treat the different cases separately to reduce the number of
          // times we call minmod, not because it is required for correctness.
          //
          // Note that the case `i == k and i != j` cannot be encountered since
          // the loop bounds are `i >= j >= k`.
          if (UNLIKELY(i == j and i == k)) {
            const double tmp = minmod(i, j, k, tensor_storage_index);
            if (tmp ==
                self_coeffs[mesh.storage_index(Index<VolumeDim>{i, j, k})]) {
              goto next_tensor_index;
            }
            *limited_any_component |= true;
            self_coeffs[mesh.storage_index(Index<VolumeDim>{i, j, k})] = tmp;
          } else if (i == j and i != k) {
            const double tmp_ijk = minmod(i, j, k, tensor_storage_index);
            const double tmp_ikj = minmod(i, k, j, tensor_storage_index);
            const double tmp_kij = minmod(k, i, j, tensor_storage_index);
            if (tmp_ijk == self_coeffs[mesh.storage_index(
                               Index<VolumeDim>{i, j, k})] and
                tmp_ikj == self_coeffs[mesh.storage_index(
                               Index<VolumeDim>{i, k, j})] and
                tmp_kij == self_coeffs[mesh.storage_index(
                               Index<VolumeDim>{k, i, j})]) {
              goto next_tensor_index;
            }
            *limited_any_component |= true;
            self_coeffs[mesh.storage_index(Index<VolumeDim>{i, j, k})] =
                tmp_ijk;
            self_coeffs[mesh.storage_index(Index<VolumeDim>{i, k, j})] =
                tmp_ikj;
            self_coeffs[mesh.storage_index(Index<VolumeDim>{k, i, j})] =
                tmp_kij;
          } else if (i != j and j == k) {
            const double tmp_ijk = minmod(i, j, k, tensor_storage_index);
            const double tmp_kij = minmod(k, i, j, tensor_storage_index);
            const double tmp_kji = minmod(k, j, i, tensor_storage_index);
            if (tmp_ijk == self_coeffs[mesh.storage_index(
                               Index<VolumeDim>{i, j, k})] and
                tmp_kij == self_coeffs[mesh.storage_index(
                               Index<VolumeDim>{k, i, j})] and
                tmp_kji == self_coeffs[mesh.storage_index(
                               Index<VolumeDim>{k, j, i})]) {
              goto next_tensor_index;
            }
            *limited_any_component |= true;
            self_coeffs[mesh.storage_index(Index<VolumeDim>{i, j, k})] =
                tmp_ijk;
            self_coeffs[mesh.storage_index(Index<VolumeDim>{k, i, j})] =
                tmp_kij;
            self_coeffs[mesh.storage_index(Index<VolumeDim>{k, j, i})] =
                tmp_kji;
          } else {
            const double tmp_ijk = minmod(i, j, k, tensor_storage_index);
            const double tmp_jik = minmod(j, i, k, tensor_storage_index);
            const double tmp_ikj = minmod(i, k, j, tensor_storage_index);
            const double tmp_jki = minmod(j, k, i, tensor_storage_index);
            const double tmp_kij = minmod(k, i, j, tensor_storage_index);
            const double tmp_kji = minmod(k, j, i, tensor_storage_index);
            if (tmp_ijk == self_coeffs[mesh.storage_index(
                               Index<VolumeDim>{i, j, k})] and
                tmp_jik == self_coeffs[mesh.storage_index(
                               Index<VolumeDim>{j, i, k})] and
                tmp_ikj == self_coeffs[mesh.storage_index(
                               Index<VolumeDim>{i, k, j})] and
                tmp_jki == self_coeffs[mesh.storage_index(
                               Index<VolumeDim>{j, k, i})] and
                tmp_kij == self_coeffs[mesh.storage_index(
                               Index<VolumeDim>{k, i, j})] and
                tmp_kji == self_coeffs[mesh.storage_index(
                               Index<VolumeDim>{k, j, i})]) {
              goto next_tensor_index;
            }
            *limited_any_component |= true;
            self_coeffs[mesh.storage_index(Index<VolumeDim>{i, j, k})] =
                tmp_ijk;
            self_coeffs[mesh.storage_index(Index<VolumeDim>{j, i, k})] =
                tmp_jik;
            self_coeffs[mesh.storage_index(Index<VolumeDim>{i, k, j})] =
                tmp_ikj;
            self_coeffs[mesh.storage_index(Index<VolumeDim>{j, k, i})] =
                tmp_jki;
            self_coeffs[mesh.storage_index(Index<VolumeDim>{k, i, j})] =
                tmp_kij;
            self_coeffs[mesh.storage_index(Index<VolumeDim>{k, j, i})] =
                tmp_kji;
          }
        }
      }
    }
  next_tensor_index:
    continue;
  }
  return '0';
}

template <size_t VolumeDim, typename... Tags>
template <typename Tag, size_t Dim>
char Krivodonova<VolumeDim, tmpl::list<Tags...>>::
    fill_variables_tag_with_spectral_coeffs(
        const gsl::not_null<Variables<tmpl::list<::Tags::Modal<Tags>...>>*>
            modal_coeffs,
        const typename Tag::type& nodal_tensor, const Mesh<Dim>& mesh) const
    noexcept {
  auto& coeffs_tensor = get<::Tags::Modal<Tag>>(*modal_coeffs);
  auto tensor_it = nodal_tensor.begin();
  for (auto coeffs_it = coeffs_tensor.begin(); coeffs_it != coeffs_tensor.end();
       (void)++coeffs_it, (void)++tensor_it) {
    to_modal_coefficients(make_not_null(&*coeffs_it), *tensor_it, mesh);
  }
  return '0';
}

template <size_t VolumeDim, typename... Tags>
void Krivodonova<VolumeDim, tmpl::list<Tags...>>::pup(PUP::er& p) noexcept {
  p | alphas_;
  p | disable_for_debugging_;
}

template <size_t VolumeDim, typename... Tags>
bool Krivodonova<VolumeDim, tmpl::list<Tags...>>::operator==(
    const Krivodonova<VolumeDim, tmpl::list<Tags...>>& rhs) const noexcept {
  return alphas_ == rhs.alphas_ and
         disable_for_debugging_ == rhs.disable_for_debugging_;
}

template <size_t VolumeDim, typename... Tags>
bool operator!=(
    const Krivodonova<VolumeDim, tmpl::list<Tags...>>& lhs,
    const Krivodonova<VolumeDim, tmpl::list<Tags...>>& rhs) noexcept {
  return not(lhs == rhs);
}
}  // namespace Limiters


#define CATCH_CONFIG_MAIN

#include "KKSFreeEnergyFunctionDiluteBinary.h"

#include "catch.hpp"

#include <boost/optional/optional.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <boost/property_tree/ptree.hpp>

#include <string>

namespace pt = boost::property_tree;

TEST_CASE("Loop dilute binary KKS", "[loop dilute binary kks]")
{
    Thermo4PFM::EnergyInterpolationType energy_interp_func_type
        = Thermo4PFM::EnergyInterpolationType::PBG;
    Thermo4PFM::ConcInterpolationType conc_interp_func_type
        = Thermo4PFM::ConcInterpolationType::LINEAR;

    const double Tmin     = 1600.;
    const double Tmax     = 1720.;
    const int nTintervals = 10;
    const double deltaT   = (Tmax - Tmin) / (double)nTintervals;

    // read dilute alloy informaton
    pt::ptree conc_db;
    try
    {
        pt::read_json("dilute_binary.json", conc_db);
    }
    catch (std::exception& e)
    {
        std::cerr << "exception caught: " << e.what() << std::endl;
    }
    pt::write_json(std::clog, conc_db);

    Thermo4PFM::KKSFreeEnergyFunctionDiluteBinary* cafe
        = new Thermo4PFM::KKSFreeEnergyFunctionDiluteBinary(
            conc_db, energy_interp_func_type, conc_interp_func_type);

    // initial guesses
    double c_init0 = 0.5;
    double c_init1 = 0.5;

    // solve KKS equations
    double nominalc = 0.05;
    double phi      = 0.5;

    std::vector<double> cl(nTintervals + 1);
    std::vector<double> cs(nTintervals + 1);

    {
        double sol[2];

        // serial loop
        for (int i = 0; i < nTintervals + 1; i++)
        {
            double temperature = Tmin + i * deltaT;
            ;

            sol[0] = c_init0;
            sol[1] = c_init1;
            cafe->computePhaseConcentrations(
                temperature, &nominalc, &phi, &sol[0]);

            std::cout << "-------------------------------" << std::endl;
            std::cout << "Temperature = " << temperature << std::endl;
            std::cout << "   cL = " << sol[0] << ",   cS = " << sol[1]
                      << std::endl;

            cl[i] = sol[0];
            cs[i] = sol[1];
        }
    }

    std::vector<short> nitscpu(nTintervals + 1);

    // parallel loop
#pragma omp parallel for
    for (int i = 0; i < nTintervals + 1; i++)
    {
        const double temperature = Tmin + i * deltaT;

        double conc[2] = { c_init0, c_init1 };

        // compute concentrations in each phase
        nitscpu[i] = cafe->computePhaseConcentrations(
            temperature, &nominalc, &phi, conc);

        CHECK(conc[0] == Approx(cl[i]).margin(1.e-6));
        CHECK(conc[1] == Approx(cs[i]).margin(1.e-6));
    }

    short* nits          = new short[nTintervals + 1];
    double* xdev         = new double[2 * (nTintervals + 1)];
    double init_guess[2] = { c_init0, c_init1 };

    std::cout << "=========================\n";
    std::cout << "GPU offload testing:" << std::endl;

// clang-format off
#pragma omp target map(to:cafe[0:1]) \
                   map(to: init_guess) \
                   map(from: xdev[:2*(nTintervals+1)]) \
                   map(from: nits[:nTintervals+1])
// clang-format on
#pragma omp parallel for
    for (int i = 0; i < nTintervals + 1; i++)
    {
        const double temperature = Tmin + i * deltaT;

        double phi      = 0.5;
        double nominalc = 0.05;

        xdev[2 * i]     = init_guess[0];
        xdev[2 * i + 1] = init_guess[1];

        // compute concentrations in each phase
        nits[i] = cafe->computePhaseConcentrations(
            temperature, &nominalc, &phi, &xdev[2 * i]);
    }

    // verify results
    for (int i = 0; i < nTintervals + 1; i++)
    {
        std::cout << "Number of Newton iterations: " << nits[i] << std::endl;
        std::cout << "Device: x=" << xdev[2 * i] << "," << xdev[2 * i + 1]
                  << std::endl;
        CHECK(xdev[2 * i] == Approx(cl[i]).margin(1.e-6));
        CHECK(xdev[2 * i + 1] == Approx(cs[i]).margin(1.e-6));
        CHECK(nits[i] == nitscpu[i]);
    }
    delete[] nits;
    delete[] xdev;

    delete cafe;
}


#include <boost/accumulators/statistics/weighted_median.hpp>


#pragma once

#include <fstream>
#include <sstream>
#include <map>
#include <vector>
#include <algorithm>
#include <assert.h>
#include <iostream>
#include <cmath>
#include <stdexcept>
#include <list>
#include <float.h>
#include <random>
#include <chrono>
#include <unordered_map>
#include <unordered_set>
#include <stdexcept>
#include <time.h>
#include <stdio.h>
#include <boost/math/distributions/binomial.hpp>
#include <boost/math/distributions/poisson.hpp>
#include <boost/random.hpp>
#include <boost/math/special_functions/gamma.hpp>

//#define REUSE
#define STACK
//#define SAMPLE

#define MISS_BAR 1024 * 2

namespace sdd {

/* to calculate C(a, k) / C(b, k) */
inline double combinationRatio(int b, int a, int k)
{
	if (b < a || !a || !b || !k)
		throw std::exception();

	/* This is for the condition that wouldn't occur. */
	if (k > a) return 0.0;

	int loops = b - a;
	double result = 1.0;
	while (loops--)
		result *= (double)(b - k) / (b--);

	if (result <= DBL_MIN)
		throw std::exception();

	return result;
}

/* calculate log2(s) + 1 */
template<class T>
inline T log2p1(T s)
{
	T result = 0;
	while (s) {
		s >>= 1;
		++result;
	}

	return result;
}

/* for recording distribution into a Histogram,
   Accur is the accuracy of transforming calculation */
template <class B = int64_t>
class Histogram
{
	B * bins;
	B samples;
	int size;

public:
	Histogram() : bins(nullptr), samples(0), size(0) {};

	~Histogram()
	{
    	delete [] bins;
	}

	void allocateBins(int _size)
	{
    	size = _size;
    	bins = new B[size];
    	/* init bins to 0 */
    	for (int i = 0; i < size; ++i)
        	bins[i] = 0;

    	std::cout << "size of bins " << size << std::endl;
	}

	void clear()
	{
    	delete [] bins;
    	samples = 0;
    	/* when clear bins, the size keeps unchanged */
    	allocateBins(size);
	}

	void sample(B x);

	void print(std::ofstream & file)
	{
    	//file.write((char *)bins, sizeof(B) * size);
    	for (int i = 0; i < size; ++i) {
    		file << bins[i] << " ";
    	}
    	file << "\n";
	}
};

template <class Accur>
class AvlNode
{
	//int holes;
	friend class AvlTreeStack;
	/* this is the num of holes of this tree,
	including the holes of subtrees and the self interval. */
	int holes;
	/* this is the num of holes of entire right subtree. */
	int rHoles;
	std::pair<Accur, Accur> interval;
	int height;
	AvlNode<Accur> * left;
	AvlNode<Accur> * right;

public:
	AvlNode(Accur & a);

	AvlNode(AvlNode<Accur> & n);

	~AvlNode();

	static int getHeight(AvlNode<Accur> * & node)
	{
		return node ? node->height : -1;
	}

	void updateHeight();

	void updateHoles();
};

/* for calculating stack distance distribution via AVL Tree, with no sampling*/
class AvlTreeStack
{
	std::map <uint64_t, long> addrMap;
	AvlNode<long> * root;
	/* the index of refs in memory trace */
	long index;
	/* holes between current ref and last ref with same address */
	int curHoles;

public:
	AvlTreeStack(long & v);

	AvlTreeStack();

	~AvlTreeStack() { destroy(root); }

	void destroy(AvlNode<long> * & tree);

	void clear();

	void insert(AvlNode<long> * & tree, long & v);

	void insert(long & a);

	/*AvlNode<long> * & find(AvlNode<long> * & tree, int & v)
	{
	if (!tree)
	return nullptr;

	if (v < tree->holes)
	find(tree->left, v);
	else if (v > tree->holes)
	find(tree->right, v);
	else
	return tree;
	}*/

	/* find the minimal interval node */
	AvlNode<long> * & findMin(AvlNode<long> * & tree);

	/* find the maximal interval node */
	AvlNode<long> * & findMax(AvlNode<long> * & tree);

	void remove(AvlNode<long> * & tree, std::pair<long, long> & inter);

	void rotate(AvlNode<long> * & tree);

	void doubleRotate(AvlNode<long> * & tree);

	void balance(AvlNode<long> * & tree);

	void calStackDist(uint64_t addr, Histogram<> & hist);
};

/* do reuse distance statistics */
class ReuseDist
{
	std::map<uint64_t, long> addrMap;
	long index;

public:
	ReuseDist() {};
	~ReuseDist() {};

	void calReuseDist(uint64_t addr, Histogram<> & hist);
};

/* stack distance staticstics with sampling */
class SampleStack
{
	long index;

	typedef std::unordered_set<uint64_t> AddrSet;
	/* each watchpoint has a set to keep the unique mem refs */
	std::unordered_map<uint64_t, AddrSet> addrTable;
	int randNum;
	int sampleCounter;
	int expectSamples;
	int hibernInter;
	int sampleInter;
	long statusCounter;
	bool isSampling;

public:
	SampleStack();

	/* to generate a random number */
	int genRandom();

	void calStackDist(uint64_t addr, Histogram<> & hist);
};

}; // namespace sdd


/***********************************************************************************************************************
 *  OpenStudio(R), Copyright (c) 2008-2018, Alliance for Sustainable Energy, LLC. All rights reserved.
 *
 *  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
 *  following conditions are met:
 *
 *  (1) Redistributions of source code must retain the above copyright notice, this list of conditions and the following
 *  disclaimer.
 *
 *  (2) Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
 *  following disclaimer in the documentation and/or other materials provided with the distribution.
 *
 *  (3) Neither the name of the copyright holder nor the names of any contributors may be used to endorse or promote
 *  products derived from this software without specific prior written permission from the respective party.
 *
 *  (4) Other than as required in clauses (1) and (2), distributions in any form of modifications or other derivative
 *  works may not use the "OpenStudio" trademark, "OS", "os", or any other confusingly similar designation without
 *  specific prior written permission from Alliance for Sustainable Energy, LLC.
 *
 *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 *  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 *  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER, THE UNITED STATES GOVERNMENT, OR ANY CONTRIBUTORS BE LIABLE FOR
 *  ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 *  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
 *  AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 *  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 **********************************************************************************************************************/

#ifndef UTILITIES_GEOMETRY_POINTLATLON_HPP
#define UTILITIES_GEOMETRY_POINTLATLON_HPP

#include "../UtilitiesAPI.hpp"
#include "../data/Vector.hpp"
#include "../core/Logger.hpp"

#include <vector>
#include <boost/optional.hpp>

// do not include anything from GeographicLib in header or expose in the API
namespace GeographicLib{
  class LocalCartesian;
  class UTMUPS;
}

namespace openstudio{

  class Point3d;

  /* PointLatLon represents a point on the Earth's (reference ellipsoid) surface, an optional height parameter is available
  ** for points above or below the geoid surface.  Currently only the WGS84 ellipsoid is supported,
  ** other systems may be added in the future. When one PointLatLon is taken as an origin, it can be used to
  ** translate other PointLatLon's into either a local Cartesian coordinate system (centered on the origin, with z up and y oriented North)
  ** or the UTM (Universal Transverse Mercator) zone which contains the origin. The origin PointLatLon can be used to
  ** translate Point3d's in either local Cartesian or UTM coordinate systems back to lat/lon.
  */
  class UTILITIES_API PointLatLon{
  public:

    /// default constructor creates point at 0, 0, 0
    PointLatLon();

    /// constructor with lat, lon, and height
    PointLatLon(double lat, double lon, double height = 0);

    /// copy constructor
    PointLatLon(const PointLatLon& other);

    /// destructor
    ~PointLatLon();

    /// get lat
    double lat() const;

    /// get lon
    double lon() const;

    /// get height
    double height() const;

    PointLatLon& operator=(const openstudio::PointLatLon& other);

    /// check equality
    bool operator==(const PointLatLon& other) const;

    /// point minus another point is distance along geodesic line
    double operator-(const PointLatLon& other) const;

    /// converts a PointLatLon to a Point3d in a Local Cartesian frame centered on this with y oriented North and z oriented up
    Point3d toLocalCartesian(const PointLatLon& point) const;
    std::vector<Point3d> toLocalCartesian(const std::vector<PointLatLon>& points) const;

    /// converts a Point3d in Local Cartesian coordinate system centered on this to LatLon
    PointLatLon fromLocalCartesian(const Point3d& point) const;
    std::vector<PointLatLon> fromLocalCartesian(const std::vector<Point3d>& points) const;

    /// gets the UTM zone for this point
    int utmZone() const;

    /// converts a PointLatLon to a Point3d in UTM centered on this
    Point3d toUTM(const PointLatLon& point) const;
    std::vector<Point3d> toUTM(const std::vector<PointLatLon>& points) const;

    /// converts a Point3d in UTM centered on this to LatLon
    PointLatLon fromUTM(const Point3d& point) const;
    std::vector<PointLatLon> fromUTM(const std::vector<Point3d>& points) const;

  private:

    REGISTER_LOGGER("utilities.PointLatLon");
    Vector m_storage;

    void initLocalCartesianConverter() const;

    mutable std::unique_ptr<GeographicLib::LocalCartesian> m_localCartesianConverter;

  };

  /// ostream operator
  UTILITIES_API std::ostream& operator<<(std::ostream& os, const PointLatLon& point);

  /// ostream operator
  UTILITIES_API std::ostream& operator<<(std::ostream& os, const std::vector<PointLatLon>& pointVector);

  // optional PointLatLon
  typedef boost::optional<PointLatLon> OptionalPointLatLon;

  // vector of PointLatLon
  typedef std::vector<PointLatLon> PointLatLonVector;

  /// compute distance in meters between two points on the Earth's surface
  /// lat and lon are specified in degrees
  UTILITIES_API double getDistanceLatLon(double lat1, double lon1, double lat2, double lon2);

} // openstudio

#endif //UTILITIES_GEOMETRY_POINTLATLON_HPP


//
// Copyright © 2017 Arm Ltd. All rights reserved.
// SPDX-License-Identifier: MIT
//

#include <armnn/ArmNN.hpp>
#include <Graph.hpp>
#include <Network.hpp>

#include <reference/RefWorkloadFactory.hpp>

#include <boost/test/unit_test.hpp>
#include <test/GraphUtils.hpp>

BOOST_AUTO_TEST_SUITE(RefOptimizedNetwork)

BOOST_AUTO_TEST_CASE(OptimizeValidateCpuRefWorkloads)
{
    const armnn::TensorInfo desc({3, 5}, armnn::DataType::Float32);

    armnn::Network  net;

    armnn::NormalizationDescriptor nmDesc;
    armnn::ActivationDescriptor acDesc;

    //    in
    //     |
    //    nm
    //   /  |
    //  ac  |
    //   \  |
    //    ml
    //     |
    //    sm
    //     |
    //    ot
    armnn::IConnectableLayer* layer = net.AddInputLayer(0, "in");
    layer->GetOutputSlot(0).SetTensorInfo(desc);

    armnn::IConnectableLayer* const normLayer = net.AddNormalizationLayer(nmDesc, "nm");

    layer->GetOutputSlot(0).Connect(normLayer->GetInputSlot(0));
    normLayer->GetOutputSlot(0).SetTensorInfo(desc);

    layer = net.AddActivationLayer(acDesc, "ac");

    normLayer->GetOutputSlot(0).Connect(layer->GetInputSlot(0));
    layer->GetOutputSlot(0).SetTensorInfo(desc);

    armnn::IConnectableLayer* prevLayer = layer;
    layer = net.AddMultiplicationLayer("ml");

    prevLayer->GetOutputSlot(0).Connect(layer->GetInputSlot(0));
    normLayer->GetOutputSlot(0).Connect(layer->GetInputSlot(1));
    layer->GetOutputSlot(0).SetTensorInfo(desc);

    prevLayer = layer;
    armnn::SoftmaxDescriptor softmaxDescriptor;
    layer = net.AddSoftmaxLayer(softmaxDescriptor, "sm");

    prevLayer->GetOutputSlot(0).Connect(layer->GetInputSlot(0));
    layer->GetOutputSlot(0).SetTensorInfo(desc);

    prevLayer = layer;
    layer = net.AddOutputLayer(0, "ot");

    prevLayer->GetOutputSlot(0).Connect(layer->GetInputSlot(0));

    armnn::IRuntime::CreationOptions options;
    armnn::IRuntimePtr runtime(armnn::IRuntime::Create(options));

    std::vector<armnn::BackendId> backends = { armnn::Compute::CpuRef };
    armnn::IOptimizedNetworkPtr optNet = armnn::Optimize(net, backends, runtime->GetDeviceSpec());
    static_cast<armnn::OptimizedNetwork*>(optNet.get())->GetGraph().AllocateDynamicBuffers();
    BOOST_CHECK(optNet);

    // Validates workloads.
    armnn::RefWorkloadFactory fact;
    for (auto&& layer : static_cast<armnn::OptimizedNetwork*>(optNet.get())->GetGraph())
    {
        BOOST_CHECK_NO_THROW(
            layer->CreateWorkload(static_cast<armnn::OptimizedNetwork*>(optNet.get())->GetGraph(), fact));
    }
}

BOOST_AUTO_TEST_CASE(OptimizeValidateWorkloadsCpuRefPermuteLayer)
{
    // Create runtime in which test will run
    armnn::IRuntime::CreationOptions options;
    armnn::IRuntimePtr runtime(armnn::IRuntime::Create(options));

    std::vector<armnn::BackendId> backends = {armnn::Compute::CpuRef};

    // build up the structure of the network
    armnn::INetworkPtr net(armnn::INetwork::Create());

    armnn::IConnectableLayer* input = net->AddInputLayer(0);

    armnn::PermuteDescriptor descriptor({0, 2, 3, 1});
    armnn::IConnectableLayer* permute = net->AddPermuteLayer(descriptor);

    armnn::IConnectableLayer* output = net->AddOutputLayer(0);

    input->GetOutputSlot(0).Connect(permute->GetInputSlot(0));
    permute->GetOutputSlot(0).Connect(output->GetInputSlot(0));

    input->GetOutputSlot(0).SetTensorInfo(armnn::TensorInfo({ 1, 1, 4, 4 }, armnn::DataType::Float32));
    permute->GetOutputSlot(0).SetTensorInfo(armnn::TensorInfo({ 1, 4, 1, 4 }, armnn::DataType::Float32));

    // optimize the network
    armnn::IOptimizedNetworkPtr optNet = armnn::Optimize(*net, backends, runtime->GetDeviceSpec());

    for (auto&& layer : static_cast<armnn::OptimizedNetwork*>(optNet.get())->GetGraph())
    {
        BOOST_CHECK(layer->GetBackendId() == armnn::Compute::CpuRef);
    }
}

BOOST_AUTO_TEST_CASE(OptimizeValidateWorkloadsCpuRefMeanLayer)
{
    // Create runtime in which test will run
    armnn::IRuntime::CreationOptions options;
    armnn::IRuntimePtr runtime(armnn::IRuntime::Create(options));

    std::vector<armnn::BackendId> backends = {armnn::Compute::CpuRef};

    // build up the structure of the network
    armnn::INetworkPtr net(armnn::INetwork::Create());

    armnn::IConnectableLayer* input = net->AddInputLayer(0);

    armnn::MeanDescriptor descriptor({ 0, 1 }, false);
    armnn::IConnectableLayer* meanLayer = net->AddMeanLayer(descriptor);

    armnn::IConnectableLayer* output = net->AddOutputLayer(0);

    input->GetOutputSlot(0).Connect(meanLayer->GetInputSlot(0));
    meanLayer->GetOutputSlot(0).Connect(output->GetInputSlot(0));

    input->GetOutputSlot(0).SetTensorInfo(armnn::TensorInfo({ 4, 3, 2 }, armnn::DataType::Float32));
    meanLayer->GetOutputSlot(0).SetTensorInfo(armnn::TensorInfo({ 2 }, armnn::DataType::Float32));

    // optimize the network
    armnn::IOptimizedNetworkPtr optNet = armnn::Optimize(*net, backends, runtime->GetDeviceSpec());

    for (auto&& layer : static_cast<armnn::OptimizedNetwork*>(optNet.get())->GetGraph())
    {
        BOOST_CHECK(layer->GetBackendId() == armnn::Compute::CpuRef);
    }
}

BOOST_AUTO_TEST_CASE(DebugTestOnCpuRef)
{
    armnn::Network net;

    armnn::ActivationDescriptor activation1Descriptor;
    activation1Descriptor.m_Function = armnn::ActivationFunction::BoundedReLu;
    activation1Descriptor.m_A = 1.f;
    activation1Descriptor.m_B = -1.f;

    // Defines layers.
    auto input = net.AddInputLayer(0, "InputLayer");
    auto activation = net.AddActivationLayer(activation1Descriptor, "ActivationLayer");
    auto output = net.AddOutputLayer(0, "OutputLayer");

    // Connects layers.
    input->GetOutputSlot(0).Connect(activation->GetInputSlot(0));
    activation->GetOutputSlot(0).Connect(output->GetInputSlot(0));

    armnn::TensorShape shape({4});
    armnn::TensorInfo info(shape, armnn::DataType::Float32);
    input->GetOutputSlot(0).SetTensorInfo(info);
    activation->GetOutputSlot(0).SetTensorInfo(info);

    armnn::IRuntime::CreationOptions options;
    armnn::IRuntimePtr runtime(armnn::IRuntime::Create(options));

    std::vector<armnn::BackendId> backends = {armnn::Compute::CpuRef};

    armnn::OptimizerOptions optimizerOptions;
    optimizerOptions.m_Debug = true;

    armnn::IOptimizedNetworkPtr optimizedNet = armnn::Optimize(net, backends, runtime->GetDeviceSpec(),
                                                               optimizerOptions);

    const armnn::Graph& graph = static_cast<armnn::OptimizedNetwork*>(optimizedNet.get())->GetGraph();
    // Tests that all layers are present in the graph.
    BOOST_TEST(graph.GetNumLayers() == 5);

    // Tests that the vertices exist and have correct names.
    BOOST_TEST(GraphHasNamedLayer(graph, "InputLayer"));
    BOOST_TEST(GraphHasNamedLayer(graph, "DebugLayerAfterInputLayer"));
    BOOST_TEST(GraphHasNamedLayer(graph, "ActivationLayer"));
    BOOST_TEST(GraphHasNamedLayer(graph, "DebugLayerAfterActivationLayer"));
    BOOST_TEST(GraphHasNamedLayer(graph, "OutputLayer"));
}

BOOST_AUTO_TEST_SUITE_END()


///////////////////////////////////////////////////////////////////////////////
// distribution::survival::response::right_truncated::event_factory.hpp   		 //
//                                                                           //
//  Copyright 2009 Erwann Rogard. Distributed under the Boost                //
//  Software License, Version 1.0. (See accompanying file                    //
//  LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)         //
///////////////////////////////////////////////////////////////////////////////
#ifndef BOOST_STATISTICS_DETAIL_DISTRIBUTION_SURVIVAL_RESPONSE_RIGHT_TRUNCATED_FACTORY_HPP_ER_2009
#define BOOST_STATISTICS_DETAIL_DISTRIBUTION_SURVIVAL_RESPONSE_RIGHT_TRUNCATED_FACTORY_HPP_ER_2009
#include <stdexcept>
#include <boost/utility/enable_if.hpp>
#include <boost/mpl/not.hpp>
#include <boost/fusion/sequence/intrinsic/has_key.hpp>
#include <boost/fusion/sequence/intrinsic/at_key.hpp>
#include <boost/statistics/detail/math/constant/shortcut.hpp>
#include <boost/statistics/detail/distribution/survival/response/types/right_truncated/event.hpp>
#include <boost/statistics/detail/distribution/survival/record/key/censoring_time.hpp>
#include <boost/statistics/detail/distribution/survival/record/key/entry_time.hpp>
#include <boost/statistics/detail/distribution/survival/record/key/failure_time.hpp>
#include <boost/statistics/detail/distribution/survival/record/function/time_since_entry.hpp>

namespace boost {
namespace statistics{
namespace detail{
namespace distribution{
namespace survival {
namespace response {

    // returns a event at a given time of analysis
    template<typename T,typename B = bool>
    class event_factory{
        typedef statistics::detail::math::constant::shortcut<T> const_;

        typedef std::string str_;

        typedef record::tag::entry_time     tag_et_;
        typedef record::tag::failure_time   tag_ft_;
        typedef record::tag::censoring_time tag_ct_;

        template<typename M>
        struct has_ct
            : boost::fusion::result_of::has_key<M,tag_ct_>{};

        public:
        typedef event<T,B> result_type;

        event_factory():t_(static_cast<T>(0)){}
        template<typename T1>
        event_factory(const T1& t):t_(t){}

        template<typename M>
        typename boost::enable_if< has_ct<M>, result_type >::type
        operator()(const M& record)const
        {
            return this->impl(
                time_since_entry(record,this->time()),
                boost::fusion::at_key<tag_ft_>(record),
                boost::fusion::at_key<tag_ct_>(record)
            );
        }

        template<typename M>
        typename boost::disable_if< has_ct<M>, result_type >::type
        operator()(const M& record)const
        {
            T tmp = record::time_since_entry(record,this->time());
            return this->impl(
                tmp,
                boost::fusion::at_key<tag_ft_>(record)
            );
        }

        const T& time()const{ return this->t_; }

        private:

        // no const for dt intended
        result_type impl(T dt,const T& ft,const T& ct)const
        {
            static const str_ msg = "event_factory::impl(%1%,%2%,%3%)";
            dt = (ct<dt)? ct : dt;

            if(!(dt < static_cast<T>(0)))
            {
                bool b = !( ft > dt );
                T rt = b ? ft : dt;
                return result_type(b, rt);
            }else{
                str_ str = ( boost::format(msg) % dt % ft % ct ).str();
                throw std::runtime_error( str );
            }
        }

        result_type impl(T dt,const T& ft)const
        {
            return this->impl(dt,ft,const_::inf_else_highest);
        }

        T t_;
    };

}// response
}// survival
}// distribution
}// detail
}// statistics
}// boost

#endif


/*
 * Copyright [2015]
 * [Kartik Mohta <kartikmohta@gmail.com>]
 * [Ke Sun <sunke.polyu@gmail.com>]
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <cmath>
#include <Eigen/Dense>
#include <Eigen/Geometry>
#include <tf_conversions/tf_eigen.h>
#include <mocap_qualisys/QualisysDriver.h>

using namespace std;
using namespace Eigen;

namespace mocap{

double QualisysDriver::deg2rad = M_PI / 180.0;

bool QualisysDriver::init() {
  // The base port (as entered in QTM, TCP/IP port number, in the RT output tab
  // of the workspace options
  nh.param("server_address", server_address, string("192.168.254.1"));
  nh.param("server_base_port", base_port, 22222);
  nh.param("model_list", model_list, vector<string>(0));
  nh.param("frame_rate", frame_rate, 100);
  nh.param("max_accel", max_accel, 10.0);
  nh.param("publish_tf", publish_tf, false);
  nh.param("fixed_frame_id", fixed_frame_id, string("mocap"));

  frame_interval = 1.0 / static_cast<double>(frame_rate);
  double& dt = frame_interval;
  process_noise.topLeftCorner<6, 6>() =
    0.5*Matrix<double, 6, 6>::Identity()*dt*dt*max_accel;
  process_noise.bottomRightCorner<6, 6>() =
    Matrix<double, 6, 6>::Identity()*dt*max_accel;
  process_noise *= process_noise; // Make it a covariance
  measurement_noise =
    Matrix<double, 6, 6>::Identity()*1e-3;
  measurement_noise *= measurement_noise; // Make it a covariance
  model_set.insert(model_list.begin(), model_list.end());

  // Connecting to the server
  ROS_INFO_STREAM("Connecting to the Qualisys at: "
      << server_address << ":" << base_port);

  if(!port_protocol.Connect((char *)server_address.data(), base_port, 0, 1, 7)) {
    ROS_FATAL_STREAM("Could not find the Qualisys at: "
        << server_address << ":" << base_port);
    return false;
  }
  ROS_INFO_STREAM("Connected to " << server_address << ":" << base_port);

  // Get 6DOF settings
  port_protocol.Read6DOFSettings();

  return true;
}

void QualisysDriver::disconnect() {
  ROS_INFO_STREAM("Disconnected with the server "
      << server_address << ":" << base_port);
  port_protocol.StreamFramesStop();
  port_protocol.Disconnect();
  return;
}

void QualisysDriver::run() {

  prt_packet = port_protocol.GetRTPacket();
  CRTPacket::EPacketType e_type;
  port_protocol.GetCurrentFrame(CRTProtocol::Component6dEuler);

  if(port_protocol.ReceiveRTPacket(e_type, true)) {

    switch(e_type) {
      // Case 1 - sHeader.nType 0 indicates an error
      case CRTPacket::PacketError:
        ROS_ERROR_STREAM_THROTTLE(
            1, "Error when streaming frames: "
            << port_protocol.GetRTPacket()->GetErrorString());
        break;

      // Case 2 - No more data
      case CRTPacket::PacketNoMoreData:
        ROS_WARN_STREAM_THROTTLE(1, "No more data");
        break;

      // Case 3 - Data received
      case CRTPacket::PacketData:
        handleFrame();
        break;

      default:
        ROS_ERROR_THROTTLE(1, "Unknown CRTPacket case");
        break;
    }
  }

  return;
}

void QualisysDriver::handleFrame() {
  // Number of rigid bodies
  int body_count = prt_packet->Get6DOFEulerBodyCount();
  // Assign each subject with a thread
  vector<boost::thread> subject_threads;
  subject_threads.reserve(body_count);

  for (int i = 0; i< body_count; ++i) {
    string subject_name(
        port_protocol.Get6DOFBodyName(i));

    // Process the subject if required
    if (model_set.empty() || model_set.count(subject_name)) {
      // Create a new subject if it does not exist
      if (subjects.find(subject_name) == subjects.end()) {
        subjects[subject_name] = Subject::SubjectPtr(
            new Subject(&nh, subject_name, fixed_frame_id));
        subjects[subject_name]->setParameters(
            process_noise, measurement_noise, frame_rate);
      }
      // Handle the subject in a different thread
      subject_threads.emplace_back(&QualisysDriver::handleSubject, this, i);
      //handleSubject(i);
    }
  }

  // Wait for all the threads to stop
  for (auto it = subject_threads.begin();
      it != subject_threads.end(); ++it) {
    it->join();
  }

  // Send out warnings
  for (auto it = subjects.begin();
      it != subjects.end(); ++it) {
    Subject::Status status = it->second->getStatus();
    if (status == Subject::LOST)
      ROS_WARN_THROTTLE(1, "Lose track of subject %s", (it->first).c_str());
    else if (status == Subject::INITIALIZING)
      ROS_WARN("Initialize subject %s", (it->first).c_str());
  }

  return;
}

void QualisysDriver::handleSubject(const int& sub_idx) {

  boost::unique_lock<boost::shared_mutex> write_lock(mtx);
  // Name of the subject
  string subject_name(port_protocol.Get6DOFBodyName(sub_idx));
  // Pose of the subject
  float x, y, z, roll, pitch, yaw;
  prt_packet->Get6DOFEulerBody(
      sub_idx, x, y, z, roll, pitch, yaw);
  write_lock.unlock();

  // If the subject is lost
  if(isnan(x) || isnan(y) || isnan(z) ||
     isnan(roll) || isnan(pitch) || isnan(yaw)) {
    subjects[subject_name]->disable();
    return;
  }

  // Qualisys sometimes flips 180 degrees around the x axis
  //if(roll > 90)
  //  roll -= 180;
  //else if(roll < -90)
  //  roll += 180;

  // Convert the msgs to Eigen type
  Eigen::Quaterniond m_att;
  tf::quaternionTFToEigen(
      tf::createQuaternionFromRPY(roll*deg2rad, pitch*deg2rad, yaw*deg2rad), m_att);
  // Convert mm to m
  Eigen::Vector3d m_pos(x/1000.0, y/1000.0, z/1000.0);
  // Re-enable the object if it is lost previously
  if (subjects[subject_name]->getStatus() == Subject::LOST) {
    subjects[subject_name]->enable();
  }

  // Compute the timestamp
  // double time = ros::Time::now().toSec();
  if(start_time_local_ == 0)
  {
    start_time_local_ = ros::Time::now().toSec();
    start_time_packet_ = prt_packet->GetTimeStamp() / 1e6;
  }

  const double packet_time = prt_packet->GetTimeStamp() / 1e6;
  const double time = start_time_local_ + (packet_time - start_time_packet_);

  // Feed the new measurement to the subject
  subjects[subject_name]->processNewMeasurement(time, m_att, m_pos);

  // Publish tf if requred
  if (publish_tf &&
      subjects[subject_name]->getStatus() == Subject::TRACKED) {

    Quaterniond att = subjects[subject_name]->getAttitude();
    Vector3d pos = subjects[subject_name]->getPosition();
    tf::Quaternion att_tf;
    tf::Vector3 pos_tf;
    tf::quaternionEigenToTF(att, att_tf);
    tf::vectorEigenToTF(pos, pos_tf);

    tf::StampedTransform stamped_transform =
      tf::StampedTransform(tf::Transform(att_tf, pos_tf),
        ros::Time::now(), fixed_frame_id, subject_name);
    write_lock.lock();
    tf_publisher.sendTransform(stamped_transform);
    write_lock.unlock();
  }

  return;
}
}


#include <internal/facts/solaris/memory_resolver.hpp>
#include <internal/util/solaris/k_stat.hpp>
#include <leatherman/execution/execution.hpp>
#include <leatherman/logging/logging.hpp>
#include <leatherman/util/regex.hpp>
#include <boost/algorithm/string.hpp>
#include <sys/sysinfo.h>
#include <sys/sysconfig.h>
#include <sys/stat.h>
#include <sys/swap.h>

using namespace std;
using namespace facter::util::solaris;
using namespace leatherman::execution;
using namespace leatherman::util;

namespace facter { namespace facts { namespace solaris {

    memory_resolver::data memory_resolver::collect_data(collection& facts)
    {
        data result;

        const uint64_t page_size = sysconf(_SC_PAGESIZE);
        const auto max_dev_size = PATH_MAX;

        try {
            k_stat ks;
            auto ke = ks[make_pair("unix", "system_pages")][0];
            result.mem_total = ke.value<ulong_t>("physmem") * page_size;
            result.mem_free = ke.value<ulong_t>("pagesfree") * page_size;
        } catch (kstat_exception &ex) {
            LOG_DEBUG("failed to read memory facts from kstat api: {1}.", ex.what());

            uint64_t physmem, pagesfree;

            static boost::regex physmem_rx("^\\s*physmem\\s+(.+)$");
            static boost::regex pagesfree_rx("^\\s*pagesfree\\s+(\\d+)$");

            each_line("/usr/bin/kstat", {"-m", "unix", "-n", "system_pages"}, [&] (string& line) {
                if (re_search(line, physmem_rx, &physmem)) {
                    result.mem_total = physmem * page_size;
                } else if (re_search(line, pagesfree_rx, &pagesfree)) {
                    result.mem_free = pagesfree * page_size;
                }
                return result.mem_total == 0 || result.mem_free == 0;
            });
        }

        // Swap requires a little more effort. See
        // https://community.oracle.com/thread/1951228?start=0&tstart=0
        // http://www.brendangregg.com/K9Toolkit/swapinfo
        int num = 0;
        if ((num = swapctl(SC_GETNSWP,  0)) == -1) {
            LOG_DEBUG("swapctl failed: {1} ({2}): swap information is unavailable", strerror(errno), errno);
            return result;
        }
        if (num == 0) {
            // no swap devices configured
            return result;
        }

        // swap devices can be added online. So add one extra.
        num++;

        vector<char> buffer(num * sizeof(swapent_t) + sizeof(swaptbl_t));
        vector<vector<char>> str_table(num);

        swaptbl_t* swaps = reinterpret_cast<swaptbl_t*>(buffer.data());
        swaps->swt_n = num;

        for (int i = 0; i < num; i++) {
            str_table[i].resize(max_dev_size);
            swaps->swt_ent[i].ste_path = str_table[i].data();
        }

        if (swapctl(SC_LIST, swaps) == -1) {
            LOG_DEBUG("swapctl with SC_LIST failed: {1} ({2}): swap information is unavailable", strerror(errno), errno);
            return result;
        }

        for (int i = 0; i < num; i++) {
            result.swap_free += swaps->swt_ent[i].ste_free;
            result.swap_total += swaps->swt_ent[i].ste_pages;
        }

        result.swap_free *= page_size;
        result.swap_total *= page_size;

        return result;
    }
}}}  // namespace facter::facts::solaris