//------------------------------------------------------------------------------
// © 2021. Triad National Security, LLC. All rights reserved.  This
// program was produced under U.S. Government contract 89233218CNA000001
// for Los Alamos National Laboratory (LANL), which is operated by Triad
// National Security, LLC for the U.S.  Department of Energy/National
// Nuclear Security Administration. All rights in the program are
// reserved by Triad National Security, LLC, and the U.S. Department of
// Energy/National Nuclear Security Administration. The Government is
// granted for itself and others acting on its behalf a nonexclusive,
// paid-up, irrevocable worldwide license in this material to reproduce,
// prepare derivative works, distribute copies to the public, perform
// publicly and display publicly, and to permit others to do so.
//------------------------------------------------------------------------------

#ifndef _SINGULARITY_EOS_CLOSURE_MIXED_CELL_MODELS_
#define _SINGULARITY_EOS_CLOSURE_MIXED_CELL_MODELS_

#include <ports-of-call/portability.hpp>
#include <singularity-eos/eos/eos.hpp>

#include <cmath>

#ifdef SINGULARITY_USE_KOKKOSKERNELS
#include <KokkosBatched_ApplyQ_Decl.hpp>
#include <KokkosBatched_QR_Decl.hpp>
#include <KokkosBatched_Trsv_Decl.hpp>
#else
#include <Eigen/Dense>
#endif // SINGULARITY_USE_KOKKOSKERNELS

namespace singularity {

/*
  This is the PTE code based on FLAG's matrix form in their mixgas treatment
  I have modified it to include a line search which improves robustness and
  quality of non-converged solutions, at the likely cost of speed (due to
  additional EOS calls).
  It is templated on nmat and takes an array of Indexers to the EOSs,
  Volume (total volume of materials to be equilibrated), total SIE,
  and an array of masses of each component as inputs, and returns component
  volumes and energies (SIEs) as output.

  EOSIndexer must have an operator[](int) that returns an EOS. e.g., EOS*
  RealIndexer must have an operator[](int) that returns a Real. e.g., Real*
  ConstRealIndexer is as RealIndexer, but assumed const type.
  LambdaIndexer must have an operator[](int) that returns a Real*. e.g., Real**
*/
// Version templated on nmat
// niter version produces histogram
template <int nmat, typename EOSIndexer, typename RealIndexer, typename ConstRealIndexer,
          typename LambdaIndexer>
PORTABLE_INLINE_FUNCTION bool pte_closure_flag_with_line(
    EOSIndexer &&eoss, const Real Volume, const Real TotalSIE,
    ConstRealIndexer &&ComponentMasses, RealIndexer &&ComponentVolumes,
    RealIndexer &&ComponentEnergies, LambdaIndexer &&lambdas, int &niter);
template <int nmat, typename EOSIndexer, typename RealIndexer, typename ConstRealIndexer,
          typename LambdaIndexer>
PORTABLE_INLINE_FUNCTION bool
pte_closure_flag_with_line(EOSIndexer &&eoss, const Real Volume, const Real TotalSIE,
                           ConstRealIndexer &&ComponentMasses,
                           RealIndexer &&ComponentVolumes,
                           RealIndexer &&ComponentEnergies, LambdaIndexer &&lambdas) {
  int niter;
  return pte_closure_flag_with_line(eoss, Volume, TotalSIE, ComponentMasses,
                                    ComponentVolumes, ComponentEnergies, lambdas, niter);
}
// Version with nmat available at runtime
template <typename EOSIndexer, typename RealIndexer, typename ConstRealIndexer,
          typename LambdaIndexer>
PORTABLE_INLINE_FUNCTION bool
pte_closure_flag(int nmat, EOSIndexer &&eoss, const Real Volume, const Real TotalSIE,
                 ConstRealIndexer &&ComponentMasses, RealIndexer &&ComponentVolumes,
                 RealIndexer &&ComponentEnergies, LambdaIndexer &&lambdas, int &niter);
template <typename EOSIndexer, typename RealIndexer, typename ConstRealIndexer,
          typename LambdaIndexer>
PORTABLE_INLINE_FUNCTION bool
pte_closure_flag(int nmat, EOSIndexer &&eoss, const Real Volume, const Real TotalSIE,
                 ConstRealIndexer &&ComponentMasses, RealIndexer &&ComponentVolumes,
                 RealIndexer &&ComponentEnergies, LambdaIndexer &&lambdas) {
  int niter;
  return pte_closure_flag(nmat, eoss, Volume, TotalSIE, ComponentMasses, ComponentVolumes,
                          ComponentEnergies, lambdas, niter);
}
// Pointer-only version with offset for EOS indexing
PORTABLE_INLINE_FUNCTION bool
pte_closure_flag_offset(int nmat, EOS *eoss, const Real Volume, const Real TotalSIE,
                        int const *const ComponentMats, const Real *ComponentMasses,
                        Real *ComponentVolumes, Real *ComponentEnergies, Real **lambdas,
                        int &niter);
PORTABLE_INLINE_FUNCTION bool
pte_closure_flag_offset(int nmat, EOS *eoss, const Real Volume, const Real TotalSIE,
                        int const *const ComponentMats, const Real *ComponentMasses,
                        Real *ComponentVolumes, Real *ComponentEnergies, Real **lambdas) {
  int niter;
  return pte_closure_flag_offset(nmat, eoss, Volume, TotalSIE, ComponentMats,
                                 ComponentMasses, ComponentVolumes, ComponentEnergies,
                                 lambdas, niter);
}
/*
  This solver was developed by Josh Dolence, and is guaranteed to
  return a state where energies and volume fractions add up to the
  total energy and 1 respectively, even if the solve fails.

  EOSIndexer must have an operator[](int) that returns an EOS. e.g., EOS*
  RealIndexer must have an operator[](int) that returns a Real. e.g., Real*
  LambdaIndexer must have an operator[](int) that returns a Real*. e.g., Real**
*/
// Version templated on nmat.
template <int nmat, typename EOSIndexer, typename RealIndexer, typename LambdaIndexer>
PORTABLE_INLINE_FUNCTION bool pte_closure_josh(EOSIndexer &&eos, const Real vfrac_tot,
                                               const Real sie_tot, RealIndexer &&rho,
                                               RealIndexer &&vfrac, RealIndexer &&sie,
                                               RealIndexer &&temp, RealIndexer &&press,
                                               LambdaIndexer &&lambda, int &niter);
template <int nmat, typename EOSIndexer, typename RealIndexer, typename LambdaIndexer>
PORTABLE_INLINE_FUNCTION bool
pte_closure_josh(EOSIndexer &&eos, const Real vfrac_tot, const Real sie_tot,
                 RealIndexer &&rho, RealIndexer &&vfrac, RealIndexer &&sie,
                 RealIndexer &&temp, RealIndexer &&press, LambdaIndexer &&lambda) {
  int niter;
  return pte_closure_josh(eos, vfrac_tot, sie_tot, rho, vfrac, sie, temp, press, lambda,
                          niter);
}
// Version with nmat available at runtime.
template <typename EOSIndexer, typename RealIndexer, typename LambdaIndexer>
PORTABLE_INLINE_FUNCTION bool
pte_closure_josh(int nmat, EOSIndexer &&eoss, const Real vfrac_tot, const Real sie_tot,
                 RealIndexer &&rho, RealIndexer &&vfrac, RealIndexer &&sie,
                 RealIndexer &&temp, RealIndexer &&press, LambdaIndexer &&lambdas,
                 int &niter);
template <typename EOSIndexer, typename RealIndexer, typename LambdaIndexer>
PORTABLE_INLINE_FUNCTION bool
pte_closure_josh(int nmat, EOSIndexer &&eoss, const Real vfrac_tot, const Real sie_tot,
                 RealIndexer &&rho, RealIndexer &&vfrac, RealIndexer &&sie,
                 RealIndexer &&temp, RealIndexer &&press, LambdaIndexer &&lambdas) {
  int niter;
  return pte_closure_josh(nmat, eoss, vfrac_tot, sie_tot, rho, vfrac, sie, temp, press,
                          lambdas, niter);
}
// Pointer-only version with offset for EOS indexing
PORTABLE_INLINE_FUNCTION bool
pte_closure_josh_offset(int nmat, EOS *eoss, const Real vfrac_tot, const Real sie_tot,
                        int const *const Mats, Real *rho, Real *vfrac, Real *sie,
                        Real *temp, Real *press, Real **lambdas, int &niter);
PORTABLE_INLINE_FUNCTION bool
pte_closure_josh_offset(int nmat, EOS *eoss, const Real vfrac_tot, const Real sie_tot,
                        int const *const Mats, Real *rho, Real *vfrac, Real *sie,
                        Real *temp, Real *press, Real **lambdas) {
  int niter;
  return pte_closure_josh_offset(nmat, eoss, vfrac_tot, sie_tot, Mats, rho, vfrac, sie,
                                 temp, press, lambdas, niter);
}

/*
    This is a formulation of the PTE closure equations which tries to minimize
    differences between T and P/T.  Since T (should be) positive definite, this
    should be well-founded.  In mixtures of ideal gasses, this should be linear
    (so for hot plasmas, this should be linear-ish).
    Equations 1-nmat will be:
    T1-T0 = 0, T2-T1=0, [...], Tnmat-Tnmatm1=0, sum(Ei) = Etot
    and Equations nmat+1-2*nmat will be:
    P1/T1-P0/T0 = 0, P2/T2-P1/T1 = 0 [...], Sum(volumes)=Volume

    EOSIndexer must have an operator[](int) that returns an EOS. e.g., EOS*
    RealIndexer must have an operator[](int) that returns a Real. e.g., Real*
    ConstRealIndexer is as RealIndexer, but assumed const type.
    LambdaIndexer must have an operator[](int) that returns a Real*. e.g., Real**
*/
template <typename EOSIndexer, typename RealIndexer, typename ConstRealIndexer,
          typename LambdaIndexer>
PORTABLE_INLINE_FUNCTION bool
pte_closure_ideal(int nmat, EOSIndexer &&eoss, const Real Volume, const Real TotalSIE,
                  ConstRealIndexer &&ComponentMasses, RealIndexer &&ComponentVolumes,
                  RealIndexer &&ComponentEnergies, LambdaIndexer &&lambdas, int &niter);
template <typename EOSIndexer, typename RealIndexer, typename ConstRealIndexer,
          typename LambdaIndexer>
PORTABLE_INLINE_FUNCTION bool
pte_closure_ideal(int nmat, EOSIndexer &&eoss, const Real Volume, const Real TotalSIE,
                  ConstRealIndexer &&ComponentMasses, RealIndexer &&ComponentVolumes,
                  RealIndexer &&ComponentEnergies, LambdaIndexer &&lambdas) {
  int niter;
  return pte_closure_ideal(nmat, eoss, Volume, TotalSIE, ComponentMasses,
                           ComponentVolumes, ComponentEnergies, lambdas, niter);
}

// ======================================================================
// Implementation details below
// ======================================================================

namespace mix_params {
constexpr Real derivative_eps = 3.0e-6;
constexpr Real pte_rel_tolerance_p = 1.e-4;
constexpr Real pte_rel_tolerance_t = 1.e-4;
constexpr Real pte_abs_tolerance_p = 0.0;
constexpr Real pte_abs_tolerance_t = 0.0;
constexpr int pte_max_iter_per_mat = 16;
constexpr Real line_search_alpha = 1.e-4;
constexpr int line_search_max_iter = 3;
constexpr Real line_search_fac = 0.05;
} // namespace mix_params

namespace mix_impl {
template <typename T,
          typename = typename std::enable_if<std::is_floating_point<T>::value>::type>
constexpr bool isfinite(const T &a) {
  return (a == a) && ((a == 0) || (a != 2 * a));
}

constexpr Real square(const Real x) { return x * x; }

PORTABLE_INLINE_FUNCTION
bool check_nans(Real const *const a, const int n, const bool verbose = false) {
  bool retval = true;
  for (int i = 0; i < n; ++i)
    if (!isfinite(a[i])) {
      retval = false;
#ifndef KOKKOS_ENABLE_CUDA
      if (verbose) {
        printf("bad val in element %i/%i\n", i, n);
      }
#endif // KOKKOS_ENABLE_CUDA
    }
  return retval;
}

template <int n>
PORTABLE_INLINE_FUNCTION bool solve_Ax_b(Real *a, Real *b) {
#ifdef SINGULARITY_USE_KOKKOSKERNELS
#ifndef PORTABILITY_STRATEGY_KOKKOS
#error "Kokkos Kernels requires Kokkos."
#endif
  Real t_[n], w_[n];
  // aliases for kokkos views
  using Unmgd = Kokkos::MemoryTraits<Kokkos::Unmanaged>;
  using Lrgt = Kokkos::LayoutRight;
  using vec_t = Kokkos::View<Real *, Unmgd>;
  // aliases for QR solve template params
  using QR_alg = KokkosBatched::Algo::QR::Unblocked;
  using Lft = KokkosBatched::Side::Left;
  using Trs = KokkosBatched::Trans::Transpose;
  using nTrs = KokkosBatched::Trans::NoTranspose;
  using ApQ_alg = KokkosBatched::Algo::ApplyQ::Unblocked;
  using UP = KokkosBatched::Uplo::Upper;
  using NonU = KokkosBatched::Diag::NonUnit;
  using Tr_alg = KokkosBatched::Algo::Trsv::Unblocked;
  // aliases for solver structs ('invoke' member of the struct is the
  // actual function call)
  using QR_factor = KokkosBatched::SerialQR<QR_alg>;
  using ApplyQ_transpose = KokkosBatched::SerialApplyQ<Lft, Trs, ApQ_alg>;
  using InvertR = KokkosBatched::SerialTrsv<UP, nTrs, NonU, Tr_alg>;
  // view of matrix
  Kokkos::View<Real **, Lrgt, Unmgd> A(a, n, n);
  // view of RHS
  vec_t B(b, n);
  // view of reflectors
  vec_t t(t_, n);
  // view of workspace
  vec_t w(w_, n);
  // QR factor A, A x = B -> Q R x = B
  // store result in A and t
  QR_factor::invoke(A, t, w);
  // Apply Q^T from the left to both sides
  // Q^T Q R x = Q^T B -> R x = Q^T B
  // store result of Q^T B in B
  ApplyQ_transpose::invoke(A, t, B, w);
  // Apply R^-1 from the left to both sides
  // R^-1 R x = R^-1 Q^T B -> x = R^-1 Q^T B
  // store solution vector x in B
  InvertR::invoke(1.0, A, B);
#else
#ifdef PORTABILITY_STRATEGY_KOKKOS
#error "Eigen should not be used with Kokkos."
#endif
  // Eigen VERSION
  Eigen::Map<Eigen::Matrix<Real, n, n, Eigen::RowMajor>> A(a);
  Eigen::Map<Eigen::Matrix<Real, n, 1>> B(b);
  Eigen::Matrix<Real, n, 1> X;
  X = A.lu().solve(B);
  B = X;
#endif // SINGULARITY_USE_KOKKOSKERNELS
  bool retval = check_nans(b, n);
  return retval;
}

template <int nmat, typename EOSIndexer, typename RealIndexer, typename ConstRealIndexer,
          typename LambdaIndexer, typename OFFSETTER>
PORTABLE_INLINE_FUNCTION bool
pte_closure_flag_with_line_impl(EOSIndexer &&eoss, const Real Volume, const Real TotalSIE,
                                ConstRealIndexer &&ComponentMasses,
                                RealIndexer &&ComponentVolumes,
                                RealIndexer &&ComponentEnergies, LambdaIndexer &&lambdas,
                                const OFFSETTER ofst, int &iter) {
  using namespace mix_params;

  Real TotalMass = 0.0, Vsum = 0.0, Esum = 0.0;
  Real Densities[nmat], Pressures[nmat], Temperatures[nmat], dpdr[nmat];
  Real dpde[nmat], dtdr[nmat], dtde[nmat], b[2 * nmat], A[4 * nmat * nmat];
  Real rtemp[nmat], sietemp[nmat];
  // First, normalize the energies and volumes.
  // This may or may not be close to reality since SIE isn't strictly positive.
  for (int i = 0; i < nmat; ++i)
    TotalMass += ComponentMasses[i];
  bool iszero = true;
  for (int i = 0; i < nmat; ++i)
    iszero &= ComponentVolumes[i] <= 1.0e-16;
  if (iszero)
    for (int i = 0; i < nmat; ++i)
      ComponentVolumes[i] = Volume * ComponentMasses[i] / TotalMass;
  for (int i = 0; i < nmat; ++i)
    Vsum += ComponentVolumes[i];
  for (int i = 0; i < nmat; ++i)
    ComponentVolumes[i] *=
        Volume / Vsum; // Are compilers smart enough to calculate this RHS once?
  for (int i = 0; i < nmat; ++i)
    Esum += ComponentEnergies[i] * ComponentMasses[i];
  if (std::abs(Esum) <
      0.0001) { // Then we punt and just distribute based on mass fractions
    for (int i = 0; i < nmat; ++i)
      ComponentEnergies[i] = TotalSIE * TotalMass / ComponentMasses[i];
  } else { // Otherwise, we scale
    for (int i = 0; i < nmat; ++i)
      ComponentEnergies[i] *= (TotalSIE * TotalMass / Esum);
  }

  // This is the main loop
  constexpr const int pte_max_iter = nmat * pte_max_iter_per_mat;
  for (iter = 0; iter < pte_max_iter; ++iter) {
    // Call the EOSs
    for (int mat = 0; mat < nmat; ++mat) {
      Densities[mat] = ComponentMasses[mat] / ComponentVolumes[mat];
      eoss[ofst(mat)].PTofRE(Densities[mat], ComponentEnergies[mat], lambdas[mat],
                             Pressures[mat], Temperatures[mat], dpdr[mat], dpde[mat],
                             dtdr[mat], dtde[mat]);
    }
    // Now, zero the matrix and vector
    for (int i = 0; i < 2 * nmat * 2 * nmat; ++i)
      A[i] = 0.0;
    for (int i = 0; i < 2 * nmat; ++i)
      b[i] = 0.0;
    // print_matrix(2*nmat,2*nmat,A);
    // print_vector(2*nmat,b);
    Real vsum = 0.0;
    Real esum = 0.0;
    Real err = 0.0;
    // build matrix and vector
    for (int n = 0; n < nmat - 1; ++n) {
      const int i = n + nmat;
      // First, do the energy side
      A[n * 2 * nmat + n] = dtde[n];
      A[n * 2 * nmat + n + 1] = -dtde[n + 1];
      A[n * 2 * nmat + i] = dtdr[n];
      A[n * 2 * nmat + i + 1] = -dtdr[n + 1];
      A[(nmat - 1) * 2 * nmat + n] = ComponentMasses[n];
      b[n] = Temperatures[n + 1] - Temperatures[n];
      esum += ComponentMasses[n] * ComponentEnergies[n];
      // Now the density half
      A[i * 2 * nmat + n] = dpde[n];
      A[i * 2 * nmat + n + 1] = -dpde[n + 1];
      A[i * 2 * nmat + i] = dpdr[n];
      A[i * 2 * nmat + i + 1] = -dpdr[n + 1];
      A[(2 * nmat - 1) * 2 * nmat + i] = ComponentMasses[n] / square(Densities[n]);
      b[i] = Pressures[n + 1] - Pressures[n];
      vsum += ComponentMasses[n] / Densities[n];
    }
    A[(nmat - 1) * 2 * nmat + nmat - 1] = ComponentMasses[nmat - 1];
    esum += ComponentMasses[nmat - 1] * ComponentEnergies[nmat - 1];
    b[nmat - 1] = TotalSIE * TotalMass - esum; // esum - TotalSIE*TotalMass;
    A[4 * nmat * nmat - 1] = ComponentMasses[nmat - 1] / square(Densities[nmat - 1]);
    vsum += ComponentMasses[nmat - 1] / Densities[nmat - 1];
    b[2 * nmat - 1] = vsum - Volume;
    for (int i = 0; i < 2 * nmat; ++i)
      err += square(b[i]);
    err *= 0.5;
    if (!solve_Ax_b<2 * nmat>(A, b)) {
      // do something to crash out?  Tell folks what happened?
#ifndef KOKKOS_ENABLE_CUDA
      printf("Crashing out on iteration %i\n", iter);
#endif // KOKKOS_ENABLE_CUDA
      break;
    }
    // LINE SEARCH

    // Now, get overall scaling limit
    Real scale = 1.0;
    for (int m = 0; m < nmat; ++m) {
      Real rt = Densities[m] + scale * b[m + nmat];
      if (rt < 0.0) {
        scale = -0.9 * Densities[m] / b[m + nmat];
      }
      const Real dt = (dtdr[m] * b[m + nmat] + dtde[m] * b[m]);
      const Real tt = Temperatures[m] + scale * dt;
      if (tt < 0.0) scale = -0.9 * Temperatures[m] / dt;
    }
    // Now apply the overall pre-scaling
    for (int i = 0; i < 2 * nmat; ++i)
      b[i] *= scale;
    // Line search
    Real gradfdx = -2.0 * scale * err;
    scale = 1.0;
    Real err_old = err;
    int line_iter = 0;
    Real err_p, err_t;
    do {
      for (int m = 0; m < nmat; ++m) {
        rtemp[m] = Densities[m] + scale * b[m + nmat];
        sietemp[m] = ComponentEnergies[m] + scale * b[m];
        Temperatures[m] = eoss[ofst(m)].TemperatureFromDensityInternalEnergy(
            rtemp[m], sietemp[m], lambdas[m]);
        if (eoss[ofst(m)].PreferredInput() ==
            (thermalqs::density | thermalqs::specific_internal_energy)) {
          Pressures[m] = eoss[ofst(m)].PressureFromDensityInternalEnergy(
              rtemp[m], sietemp[m], lambdas[m]);
        } else if (eoss[ofst(m)].PreferredInput() ==
                   (thermalqs::density | thermalqs::temperature)) {
          Pressures[m] = eoss[ofst(m)].PressureFromDensityTemperature(
              rtemp[m], Temperatures[m], lambdas[m]);
        }
      }
      err_p = 0.0;
      err_t = 0.0;
      for (int n = 0; n < nmat - 1; ++n) {
        err_p += square(Pressures[n + 1] - Pressures[n]);
        err_t += square(Temperatures[n + 1] - Temperatures[n]);
      }
      err = 0.5 * (err_p + err_t);
      line_iter++;
      if (line_iter > line_search_max_iter ||
          err < err_old + line_search_alpha * scale * gradfdx)
        break;
      scale *= line_search_fac;
    } while (true);

    // END LINE SEARCH

    Real mean_p = 0, mean_t = 0;

    for (int i = 0; i < 2 * nmat; ++i)
      b[i] *= scale;
    for (int n = 0; n < nmat; ++n) {
      const int i = nmat + n;
      ComponentEnergies[n] += b[n];
      Densities[n] += b[i];
      ComponentVolumes[n] = ComponentMasses[n] / Densities[n];
      mean_p += ComponentVolumes[n] * Pressures[n];
      mean_t += ComponentMasses[n] * Temperatures[n];
    }
    mean_p /= Volume;
    mean_t /= TotalMass;
    err_p = std::sqrt(err_p);
    err_t = std::sqrt(err_t);
    bool converged_p =
        (err_p < pte_rel_tolerance_p * std::abs(mean_p) || err_p < pte_abs_tolerance_p);
    bool converged_t =
        (err_t < pte_rel_tolerance_t * std::abs(mean_t) || err_t < pte_abs_tolerance_t);
    if (converged_p && converged_t) {
      return true; // FIXME
    }
  }
  return false; // FIXME
}

// RealIndexer types may be different because some might be arrays and
// some might be pointers.
template <int nmat, typename T1, typename T2, typename T3>
PORTABLE_INLINE_FUNCTION static void pte_residual(const Real utot, T1 &&vfrac, Real *u,
                                                  T2 &&temp, T3 &&press, Real *residual) {
  Real vsum = 0.0;
  Real esum = 0.0;
  for (int m = 0; m < nmat; ++m) {
    vsum += vfrac[m];
    esum += u[m];
  }
  residual[0] = 1.0 - vsum;
  residual[1] = utot - esum;
  for (int m = 0; m < nmat - 1; ++m) {
    residual[2 + m] = press[m + 1] - press[m];
    residual[1 + nmat + m] = temp[m + 1] - temp[m];
  }
}

template <int nmat, typename EOSIndexer, typename RealIndexer, typename LambdaIndexer,
          typename OFFSETTER>
PORTABLE_INLINE_FUNCTION bool
pte_closure_josh_impl(EOSIndexer &&eos, const Real vfrac_tot, const Real sie_tot,
                      RealIndexer &&rho, RealIndexer &&vfrac, RealIndexer &&sie,
                      RealIndexer &&temp, RealIndexer &&press, LambdaIndexer &&lambda,
                      const OFFSETTER ofst, int &niter) {
  using namespace mix_params;
  const Real ilog2 = 1.0 / std::log(2.0);
  Real vsum = 0.0;
  // Normalize vfrac
  for (int m = 0; m < nmat; ++m)
    vsum += vfrac[m];
  for (int m = 0; m < nmat; ++m)
    vfrac[m] /= vsum;
  Real rhobar[nmat]; // This is a fixed quantity: the average density of
                     // material m averaged over the full PTE volume
  for (int m = 0; m < nmat; ++m)
    rhobar[m] = rho[m] * vfrac[m];
  Real rho_total = 0.0;
  for (int m = 0; m < nmat; ++m)
    rho_total += rhobar[m];
  // Renormalize energies as well
  Real utot = rho_total * sie_tot;
  Real u[nmat];
  Real esum = 0.0;
  for (int m = 0; m < nmat; ++m)
    u[m] = sie[m] * rhobar[m];
  for (int m = 0; m < nmat; ++m)
    esum += u[m];
  for (int m = 0; m < nmat; ++m)
    u[m] *= utot / esum;
  Real jacobian[4 * nmat * nmat];
  Real dx[2 * nmat];
  Real residual[2 * nmat];
  Real Cache[nmat][MAX_NUM_LAMBDAS];
  // Real* Cache[nmat];
  // for (int m {0}; m < nmat; ++m) Cache[m] = nullptr;
  Real vtemp[nmat], rtemp[nmat], utemp[nmat];

  Real dpde[nmat], dtde[nmat], dpdv[nmat], dtdv[nmat];

  // set some options and make the initial EOS calls
  enum class EosPreference { RhoT, Rhoe };
  EosPreference eos_choice[nmat];
  for (int m = 0; m < nmat; m++) {
    temp[m] = eos[ofst(m)].TemperatureFromDensityInternalEnergy(rho[m], sie[m], Cache[m]);
    if (eos[ofst(m)].PreferredInput() ==
        (thermalqs::density | thermalqs::specific_internal_energy)) {
      eos_choice[m] = EosPreference::Rhoe;
      press[m] = eos[ofst(m)].PressureFromDensityInternalEnergy(rho[m], sie[m], Cache[m]);
    } else if (eos[ofst(m)].PreferredInput() ==
               (thermalqs::density | thermalqs::temperature)) {
      eos_choice[m] = EosPreference::RhoT;
      press[m] = eos[ofst(m)].PressureFromDensityTemperature(rho[m], temp[m], Cache[m]);
    }
  }

  bool converged_p = false;
  bool converged_t = false;
  bool converged = true;
  niter = 0;
  constexpr const int pte_max_iter = nmat * pte_max_iter_per_mat;
  for (niter = 0; niter < pte_max_iter; ++niter) {
    for (int m = 0; m < nmat; m++) {
      //////////////////////////////
      // perturb volume fractions
      //////////////////////////////
      const Real deriv_mult = vfrac[m] > 0.01 ? vfrac[m] : 0.01;
      const Real ldv = std::log(derivative_eps * deriv_mult) * ilog2;
      Real dv = std::pow(2.0, std::round(ldv));
      dv *= (vfrac[m] < 0.5 ? 1.0 : -1.0);
      const Real vf_pert = vfrac[m] + dv;
      const Real rho_pert = rhobar[m] / vf_pert;

      Real p_pert;
      Real t_pert =
          eos[ofst(m)].TemperatureFromDensityInternalEnergy(rho_pert, sie[m], Cache[m]);
      switch (eos_choice[m]) {
      case EosPreference::Rhoe:
        p_pert =
            eos[ofst(m)].PressureFromDensityInternalEnergy(rho_pert, sie[m], Cache[m]);
        break;
      case EosPreference::RhoT:
        p_pert = eos[ofst(m)].PressureFromDensityTemperature(rho_pert, t_pert, Cache[m]);
        break;
      }
      dpdv[m] = (p_pert - press[m]) / dv;
      dtdv[m] = (t_pert - temp[m]) / dv;
      //////////////////////////////
      // perturb energies
      //////////////////////////////
      Real lde = std::log(derivative_eps * std::abs(u[m])) * ilog2;
      const Real de = std::pow(2.0, std::round(lde));
      Real e_pert = (u[m] + de) / rhobar[m];

      t_pert =
          eos[ofst(m)].TemperatureFromDensityInternalEnergy(rho[m], e_pert, Cache[m]);
      switch (eos_choice[m]) {
      case EosPreference::Rhoe:
        p_pert = eos[ofst(m)].PressureFromDensityInternalEnergy(rho[m], e_pert, Cache[m]);
        break;
      case EosPreference::RhoT:
        p_pert = eos[ofst(m)].PressureFromDensityTemperature(rho[m], t_pert, Cache[m]);
        break;
      }
      dpde[m] = (p_pert - press[m]) / de;
      dtde[m] = (t_pert - temp[m]) / de;
      if (std::abs(dtde[m]) < 1.e-16) { // must be on the cold curve
        dtde[m] = derivative_eps;
        ;
      }
    }
    // Fill in the residual
    pte_residual<nmat>(utot, vfrac, u, temp, press, residual);
    Real err = 0;
    for (int i = 0; i < 2 * nmat; ++i)
      err += residual[i] * residual[i];
    err *= 0.5;
    // Fill in the Jacobian
    for (int i = 0; i < 4 * nmat * nmat; ++i)
      jacobian[i] = 0.0;
    for (int m = 0; m < nmat; ++m) {
      jacobian[m] = 1.0;
      jacobian[2 * nmat + nmat + m] = 1.0;
    }
    jacobian[2 * 2 * nmat] = dpdv[0];
    jacobian[nmat * 2 * nmat + nmat - 1] = -dpdv[nmat - 1];
    jacobian[(nmat + 1) * 2 * nmat] = dtdv[0];
    jacobian[(2 * nmat - 1) * 2 * nmat + nmat - 1] = -dtdv[nmat - 1];
    jacobian[2 * 2 * nmat + nmat] = dpde[0];
    jacobian[nmat * 2 * nmat + 2 * nmat - 1] = -dpde[nmat - 1];
    jacobian[(nmat + 1) * 2 * nmat + nmat] = dtde[0];
    jacobian[(2 * nmat - 1) * 2 * nmat + 2 * nmat - 1] = -dtde[nmat - 1];
    for (int m = 1; m < nmat - 1; ++m) {
      jacobian[(1 + m) * 2 * nmat + m] = -dpdv[m];
      jacobian[(2 + m) * 2 * nmat + m] = dpdv[m];
      jacobian[(nmat + m) * 2 * nmat + m] = -dtdv[m];
      jacobian[(nmat + m + 1) * 2 * nmat + m] = dtdv[m];
      jacobian[(1 + m) * 2 * nmat + nmat + m] = -dpde[m];
      jacobian[(2 + m) * 2 * nmat + nmat + m] = dpde[m];
      jacobian[(nmat + m) * 2 * nmat + nmat + m] = -dtde[m];
      jacobian[(nmat + m + 1) * 2 * nmat + nmat + m] = dtde[m];
    }
    for (int i = 0; i < 2 * nmat; ++i)
      dx[i] = residual[i];
    if (!solve_Ax_b<2 * nmat>(jacobian, dx)) {
      // do something to crash out?  Tell folks what happened?
#ifndef KOKKOS_ENABLE_CUDA
      printf("crashing out at iteration: %i\n", niter);
#endif // KOKKOS_ENABLE_CUDA
      converged = false;
      // std::cout << "Crashing out on iteration "<< niter << std::endl;
      break;
    }
    // Now, get overall scaling limit
    Real scale = 1.0;
    for (int m = 0; m < nmat; ++m) {
      Real vt = vfrac[m] + scale * dx[m];
      if (vt < 0.0) {
        scale = -0.1 * vfrac[m] / dx[m];
      } else if (vt > 1.0) {
        scale = 0.1 * (1.0 - vfrac[m]) / dx[m];
      }
      // maybe the below is dangerous??
      // const Real dt = (dtdv[m] * dx[m] + dtde[m] * dx[m + nmat]);
      // const Real tt = temp[m] + scale * dt;
      // if (tt < 0.0)
      //  scale = -0.1 * temp[m] / dt;
    }
    // Now apply the overall scaling
    for (int i = 0; i < 2 * nmat; ++i)
      dx[i] *= scale;
    // Line search
    Real gradfdx = -2.0 * scale * err;
    scale = 1.0;
    Real err_old = err;
    int line_iter = 0;
    do {
      for (int m = 0; m < nmat; ++m) {
        vtemp[m] = vfrac[m] + scale * dx[m];
        rtemp[m] = rhobar[m] / vtemp[m];
        utemp[m] = u[m] + scale * dx[nmat + m];
        sie[m] = utemp[m] / rhobar[m];
        temp[m] =
            eos[ofst(m)].TemperatureFromDensityInternalEnergy(rtemp[m], sie[m], Cache[m]);
        switch (eos_choice[m]) {
        case EosPreference::Rhoe:
          press[m] =
              eos[ofst(m)].PressureFromDensityInternalEnergy(rtemp[m], sie[m], Cache[m]);
          break;
        case EosPreference::RhoT:
          press[m] =
              eos[ofst(m)].PressureFromDensityTemperature(rtemp[m], temp[m], Cache[m]);
          break;
        }
      }
      pte_residual<nmat>(utot, vtemp, utemp, temp, press, residual);
      Real err = 0;
      for (int i = 0; i < 2 * nmat; ++i)
        err += residual[i] * residual[i];
      err *= 0.5;
      line_iter++;
      if (line_iter > line_search_max_iter ||
          err < err_old + line_search_alpha * scale * gradfdx)
        break;
      scale *= 0.5;
    } while (true);

    // Update values
    for (int m = 0; m < nmat; ++m) {
      vfrac[m] = vtemp[m];
      rho[m] = rhobar[m] / vfrac[m];
      u[m] = utemp[m];
      sie[m] = u[m] / rhobar[m];
    }
    // Calculate errors
    Real mean_p = vfrac[0] * press[0];
    Real mean_t = rhobar[0] * temp[0];
    Real error_p = 0.0;
    Real error_t = 0.0;
    for (int m = 1; m < nmat; ++m) {
      mean_p += vfrac[m] * press[m];
      mean_t += rhobar[m] * temp[m];
      error_p += residual[m + 1] * residual[m + 1];
      error_t += residual[m + nmat] * residual[m + nmat];
    }
    mean_t /= rho_total;
    error_p = std::sqrt(error_p);
    error_t = std::sqrt(error_t);
    // Check for convergence
    converged_p = (error_p < pte_rel_tolerance_p * std::abs(mean_p) ||
                   error_p < pte_abs_tolerance_p);
    converged_t =
        (error_t < pte_rel_tolerance_t * mean_t || error_t < pte_abs_tolerance_t);
    converged = (converged_p && converged_t);
    if (converged) break;
    // niter++;
  } // while (niter < pte_max_iter && !converged);
  for (int m = 0; m < nmat; ++m)
    vfrac[m] *= vfrac_tot;
  return converged;
}

struct NullPtrIndexer {
  PORTABLE_INLINE_FUNCTION Real *operator[](const int i) { return nullptr; }
};

} // namespace mix_impl

template <int nmat, typename EOSIndexer, typename RealIndexer, typename ConstRealIndexer,
          typename LambdaIndexer>
PORTABLE_INLINE_FUNCTION bool pte_closure_flag_with_line(
    EOSIndexer &&eoss, const Real Volume, const Real TotalSIE,
    ConstRealIndexer &&ComponentMasses, RealIndexer &&ComponentVolumes,
    RealIndexer &&ComponentEnergies, LambdaIndexer &&lambdas, int &niter) {
  using namespace mix_impl;
  return pte_closure_flag_with_line_impl<nmat>(
      eoss, Volume, TotalSIE, ComponentMasses, ComponentVolumes, ComponentEnergies,
      lambdas, [](const int m) { return m; }, niter);
}

template <int nmat>
PORTABLE_INLINE_FUNCTION bool
pte_closure_flag_with_line_offset(EOS *eoss, const Real Volume, const Real TotalSIE,
                                  int const *const ComponentMats,
                                  const Real *ComponentMasses, Real *ComponentVolumes,
                                  Real *ComponentEnergies, Real **lambdas, int &niter) {
  using namespace mix_impl;
  if (lambdas == nullptr) {
    NullPtrIndexer lambda_indexer;
    return pte_closure_flag_with_line_impl<nmat>(
        eoss, Volume, TotalSIE, ComponentMasses, ComponentVolumes, ComponentEnergies,
        lambda_indexer, [&ComponentMats](const int m) { return ComponentMats[m]; },
        niter);
  }
  return pte_closure_flag_with_line_impl<nmat>(
      eoss, Volume, TotalSIE, ComponentMasses, ComponentVolumes, ComponentEnergies,
      lambdas, [&ComponentMats](const int m) { return ComponentMats[m]; }, niter);
}

template <int nmat, typename EOSIndexer, typename RealIndexer, typename LambdaIndexer>
PORTABLE_INLINE_FUNCTION bool pte_closure_josh(EOSIndexer &&eos, const Real vfrac_tot,
                                               const Real sie_tot, RealIndexer &&rho,
                                               RealIndexer &&vfrac, RealIndexer &&sie,
                                               RealIndexer &&temp, RealIndexer &&press,
                                               LambdaIndexer &&lambda, int &niter) {
  using namespace mix_impl;
  return pte_closure_josh_impl<nmat>(
      eos, vfrac_tot, sie_tot, rho, vfrac, sie, temp, press, lambda,
      [&](const int m) { return m; }, niter);
}

template <int nmat>
PORTABLE_INLINE_FUNCTION bool
pte_closure_josh_offset(EOS *eos, const Real vfrac_tot, const Real sie_tot,
                        int const *const Mats, Real *rho, Real *vfrac, Real *sie,
                        Real *temp, Real *press, Real **lambda, int &niter) {
  using namespace mix_impl;
  if (lambda == nullptr) {
    NullPtrIndexer lambda_indexer;
    return pte_closure_josh_impl<nmat>(
        eos, vfrac_tot, sie_tot, rho, vfrac, sie, temp, press, lambda_indexer,
        [&](const int m) { return m; }, niter);
  }
  return pte_closure_josh_impl<nmat>(
      eos, vfrac_tot, sie_tot, rho, vfrac, sie, temp, press, lambda,
      [&Mats](const int m) { return Mats[m]; }, niter);
}

template <int nmat, typename EOSIndexer, typename RealIndexer, typename ConstRealIndexer,
          typename LambdaIndexer>
PORTABLE_INLINE_FUNCTION bool
pte_closure_ideal(EOSIndexer &&eoss, const Real Volume, const Real TotalSIE,
                  ConstRealIndexer &&ComponentMasses, RealIndexer &&ComponentVolumes,
                  RealIndexer &&ComponentEnergies, LambdaIndexer &&lambdas, int &iter) {
  using namespace mix_params;
  using namespace mix_impl;
  Real TotalMass = 0.0, Vsum = 0.0, Esum = 0.0;
  Real Densities[nmat], Pressures[nmat], Temperatures[nmat], dpdr[nmat], dpde[nmat],
      dtdr[nmat], dtde[nmat], dtpdv[nmat], dtpde[nmat], dtdv[nmat], b[2 * nmat],
      A[4 * nmat * nmat], rtemp[nmat], sietemp[nmat];
  // First, normalize the energies and volumes.
  // This may or may not be close to reality since SIE isn't strictly positive.
  for (int i = 0; i < nmat; ++i)
    TotalMass += ComponentMasses[i];
  bool iszero = false;
  for (int i = 0; i < nmat; ++i)
    iszero &= ComponentVolumes[i] <= 1.0e-16;
  if (iszero)
    for (int i = 0; i < nmat; ++i)
      ComponentVolumes[i] = Volume * ComponentMasses[i] / TotalMass;
  for (int i = 0; i < nmat; ++i)
    Vsum += ComponentVolumes[i];
  for (int i = 0; i < nmat; ++i)
    ComponentVolumes[i] *=
        Volume / Vsum; // Are compilers smart enough to calculate this RHS once?
  for (int i = 0; i < nmat; ++i)
    Esum += ComponentEnergies[i] * ComponentMasses[i];
  if (std::abs(Esum) <
      0.0001) { // Then we punt and just distribute based on mass fractions
    for (int i = 0; i < nmat; ++i)
      ComponentEnergies[i] = TotalSIE * TotalMass / ComponentMasses[i];
  } else { // Otherwise, we scale
    for (int i = 0; i < nmat; ++i)
      ComponentEnergies[i] *= (TotalSIE * TotalMass / Esum);
  }

  // This is the main loop
  constexpr const int pte_max_iter = nmat * pte_max_iter_per_mat;
  for (iter = 0; iter < pte_max_iter; ++iter) {
    // Call the EOSs
    for (int mat = 0; mat < nmat; ++mat) {
      Densities[mat] = ComponentMasses[mat] / ComponentVolumes[mat];
      eoss[mat].PTofRE(Densities[mat], ComponentEnergies[mat], lambdas[mat],
                       Pressures[mat], Temperatures[mat], dpdr[mat], dpde[mat], dtdr[mat],
                       dtde[mat]);
      // NOTE: here we are re-using this space for d(P/T)/dr and /de using the
      // chain rule If it is deemed useful, we can output this quantity from the
      // EOS call instead
      dtpdv[mat] = -(dtdr[mat] - Temperatures[mat] / Pressures[mat] * dpdr[mat]) /
                   Pressures[mat] * square(Densities[mat]);
      dtpde[mat] =
          (dtde[mat] - Temperatures[mat] / Pressures[mat] * dpde[mat]) / Pressures[mat];
      dtdv[mat] = -dtdr[mat] * square(Densities[mat]);
    }
    // Now, zero the matrix and vector
    for (int i = 0; i < 2 * nmat * 2 * nmat; ++i)
      A[i] = 0.0;
    for (int i = 0; i < 2 * nmat; ++i)
      b[i] = 0.0;
    Real vsum = 0.0;
    Real esum = 0.0;
    Real err = 0.0;
    // build matrix and vector
    for (int n = 0; n < nmat - 1; ++n) {
      const int i = n + nmat;
      // First, do the energy side
      A[n * 2 * nmat + n] = dtde[n];
      A[n * 2 * nmat + n + 1] = -dtde[n + 1];
      A[n * 2 * nmat + i] = dtdv[n];
      A[n * 2 * nmat + i + 1] = -dtdv[n + 1];
      A[(nmat - 1) * 2 * nmat + n] = ComponentMasses[n];
      b[n] = Temperatures[n + 1] - Temperatures[n];
      // Now the density half
      A[i * 2 * nmat + n] = dtpde[n];
      A[i * 2 * nmat + n + 1] = -dtpde[n + 1];
      A[i * 2 * nmat + i] = dtpdv[n];
      A[i * 2 * nmat + i + 1] = -dtpdv[n + 1];
      A[(2 * nmat - 1) * 2 * nmat + i] = 1;
      b[i] = Pressures[n + 1] / Temperatures[n + 1] - Pressures[n] / Temperatures[n];
      vsum += ComponentMasses[n] / Densities[n];
      esum += ComponentMasses[n] * ComponentEnergies[n];
    }
    A[(nmat - 1) * 2 * nmat + nmat - 1] = ComponentMasses[nmat - 1];
    b[nmat - 1] = esum - TotalMass * TotalSIE;
    A[4 * nmat * nmat - 1] = 1.0;
    vsum += ComponentMasses[nmat - 1] / Densities[nmat - 1];
    b[2 * nmat - 1] = vsum - Volume;
    for (int i = 0; i < 2 * nmat; ++i)
      err += square(b[i]);
    err *= 0.5;
    // Solve the thing
    if (!solve_Ax_b<2 * nmat>(A, b)) {
      // do something to crash out?  Tell folks what happened?
      break;
    }

    // LINE SEARCH

    // Now, get overall scaling limit
    Real scale = 1.0;
    for (int m = 0; m < nmat; ++m) {
      Real rt = ComponentMasses[m] / (ComponentVolumes[m] + scale * b[m + nmat]);
      if (rt < 0.0) {
        scale = -0.9 * ComponentVolumes[m] / b[m + nmat];
      }
      const Real dt = (dtdv[m] * b[m + nmat] + dtde[m] * b[m]);
      const Real tt = Temperatures[m] + scale * dt;
      if (tt < 0.0) scale = -0.9 * Temperatures[m] / dt;
    }
    // Now apply the overall pre-scaling
    for (int i = 0; i < 2 * nmat; ++i)
      b[i] *= scale;
    // Line search
    Real gradfdx = -2.0 * scale * err;
    scale = 1.0;
    Real err_old = err;
    int line_iter = 0;
    Real err_p, err_t;
    do {
      for (int m = 0; m < nmat; ++m) {
        rtemp[m] = ComponentMasses[m] / (ComponentVolumes[m] + scale * b[m + nmat]);
        sietemp[m] = ComponentEnergies[m] + scale * b[m];
        Temperatures[m] = eoss[m].TemperatureFromDensityInternalEnergy(
            rtemp[m], sietemp[m], lambdas[m]);
        if (eoss[m].PreferredInput() ==
            (thermalqs::density | thermalqs::specific_internal_energy)) {
          Pressures[m] =
              eoss[m].PressureFromDensityInternalEnergy(rtemp[m], sietemp[m], lambdas[m]);
        } else if (eoss[m].PreferredInput() ==
                   (thermalqs::density | thermalqs::temperature)) {
          Pressures[m] = eoss[m].PressureFromDensityTemperature(rtemp[m], Temperatures[m],
                                                                lambdas[m]);
        }
      }
      err_p = 0.0;
      err_t = 0.0;
      for (int n = 0; n < nmat - 1; ++n) {
        err_p += square(Pressures[n + 1] - Pressures[n]);
        err_t += square(Temperatures[n + 1] - Temperatures[n]);
      }
      err = 0.5 * (err_p + err_t);
      line_iter++;
      if (line_iter > line_search_max_iter ||
          err < err_old + line_search_alpha * scale * gradfdx)
        break;
      scale *= line_search_fac;
    } while (true);

    // END LINE SEARCH

    Real mean_p = 0, mean_t = 0;

    for (int i = 0; i < 2 * nmat; ++i)
      b[i] *= scale;
    for (int n = 0; n < nmat; ++n) {
      const int i = nmat + n;
      ComponentEnergies[n] += b[n];
      ComponentVolumes[n] += b[i];
      Densities[n] = ComponentMasses[n] / ComponentVolumes[n];
      mean_p += ComponentVolumes[n] * Pressures[n];
      mean_t += ComponentMasses[n] * Temperatures[n];
    }
    mean_p /= Volume;
    mean_t /= TotalMass;
    err_p = std::sqrt(err_p);
    err_t = std::sqrt(err_t);
    bool converged_p =
        (err_p < pte_rel_tolerance_p * std::abs(mean_p) || err_p < pte_abs_tolerance_p);
    bool converged_t =
        (err_t < pte_rel_tolerance_t * std::abs(mean_t) || err_t < pte_abs_tolerance_t);
    if (converged_p && converged_t) {
      return true; // FIXME
    }
  }
  return false; // FIXME
}

// TODO(JMM): The case statement below to switch between templated
// versions of these functions is still extremely gross and I hate it.
// Can we replace with a malloc+free? And if so, what's the
// consequences of that?
template <typename EOSIndexer, typename RealIndexer, typename ConstRealIndexer,
          typename LambdaIndexer>
PORTABLE_INLINE_FUNCTION bool
pte_closure_flag(int nmat, EOSIndexer &&eoss, const Real Volume, const Real TotalSIE,
                 ConstRealIndexer &&ComponentMasses, RealIndexer &&ComponentVolumes,
                 RealIndexer &&ComponentEnergies, LambdaIndexer &&lambdas, int &niter) {
  switch (nmat) {
  case 1:
    return true; // (or should we call the EOS actually?)
  case 2:
    return pte_closure_flag_with_line<2>(eoss, Volume, TotalSIE, ComponentMasses,
                                         ComponentVolumes, ComponentEnergies, lambdas,
                                         niter);
    break;
  case 3:
    return pte_closure_flag_with_line<3>(eoss, Volume, TotalSIE, ComponentMasses,
                                         ComponentVolumes, ComponentEnergies, lambdas,
                                         niter);
    break;
  case 4:
    return pte_closure_flag_with_line<4>(eoss, Volume, TotalSIE, ComponentMasses,
                                         ComponentVolumes, ComponentEnergies, lambdas,
                                         niter);
    break;
  case 5:
    return pte_closure_flag_with_line<5>(eoss, Volume, TotalSIE, ComponentMasses,
                                         ComponentVolumes, ComponentEnergies, lambdas,
                                         niter);
    break;
  case 6:
    return pte_closure_flag_with_line<6>(eoss, Volume, TotalSIE, ComponentMasses,
                                         ComponentVolumes, ComponentEnergies, lambdas,
                                         niter);
    break;
  case 7:
    return pte_closure_flag_with_line<7>(eoss, Volume, TotalSIE, ComponentMasses,
                                         ComponentVolumes, ComponentEnergies, lambdas,
                                         niter);
    break;
  case 8:
    return pte_closure_flag_with_line<8>(eoss, Volume, TotalSIE, ComponentMasses,
                                         ComponentVolumes, ComponentEnergies, lambdas,
                                         niter);
    break;
  }
  return false;
}
PORTABLE_INLINE_FUNCTION
bool pte_closure_flag_offset(int nmat, EOS *eoss, const Real Volume, const Real TotalSIE,
                             int const *const ComponentMats, const Real *ComponentMasses,
                             Real *ComponentVolumes, Real *ComponentEnergies,
                             Real **lambdas, int &niter) {
  switch (nmat) {
  case 1:
    return true; // (or should we call the EOS actually?)
  case 2:
    return pte_closure_flag_with_line_offset<2>(eoss, Volume, TotalSIE, ComponentMats,
                                                ComponentMasses, ComponentVolumes,
                                                ComponentEnergies, lambdas, niter);
    break;
  case 3:
    return pte_closure_flag_with_line_offset<3>(eoss, Volume, TotalSIE, ComponentMats,
                                                ComponentMasses, ComponentVolumes,
                                                ComponentEnergies, lambdas, niter);
    break;
  case 4:
    return pte_closure_flag_with_line_offset<4>(eoss, Volume, TotalSIE, ComponentMats,
                                                ComponentMasses, ComponentVolumes,
                                                ComponentEnergies, lambdas, niter);
    break;
  case 5:
    return pte_closure_flag_with_line_offset<5>(eoss, Volume, TotalSIE, ComponentMats,
                                                ComponentMasses, ComponentVolumes,
                                                ComponentEnergies, lambdas, niter);
    break;
  case 6:
    return pte_closure_flag_with_line_offset<6>(eoss, Volume, TotalSIE, ComponentMats,
                                                ComponentMasses, ComponentVolumes,
                                                ComponentEnergies, lambdas, niter);
    break;
  case 7:
    return pte_closure_flag_with_line_offset<7>(eoss, Volume, TotalSIE, ComponentMats,
                                                ComponentMasses, ComponentVolumes,
                                                ComponentEnergies, lambdas, niter);
    break;
  case 8:
    return pte_closure_flag_with_line_offset<8>(eoss, Volume, TotalSIE, ComponentMats,
                                                ComponentMasses, ComponentVolumes,
                                                ComponentEnergies, lambdas, niter);
    break;
  }
  return false;
}
template <typename EOSIndexer, typename RealIndexer, typename LambdaIndexer>
PORTABLE_INLINE_FUNCTION bool
pte_closure_josh(int nmat, EOSIndexer &&eoss, const Real vfrac_tot, const Real sie_tot,
                 RealIndexer &&rho, RealIndexer &&vfrac, RealIndexer &&sie,
                 RealIndexer &&temp, RealIndexer &&press, LambdaIndexer &&lambdas,
                 int &niter) {
  switch (nmat) {
  case 1:
    return true; // (or should we call the EOS actually?)
  case 2:
    return pte_closure_josh<2>(eoss, vfrac_tot, sie_tot, rho, vfrac, sie, temp, press,
                               lambdas, niter);
    break;
  case 3:
    return pte_closure_josh<3>(eoss, vfrac_tot, sie_tot, rho, vfrac, sie, temp, press,
                               lambdas, niter);
    break;
  case 4:
    return pte_closure_josh<4>(eoss, vfrac_tot, sie_tot, rho, vfrac, sie, temp, press,
                               lambdas, niter);
    break;
  case 5:
    return pte_closure_josh<5>(eoss, vfrac_tot, sie_tot, rho, vfrac, sie, temp, press,
                               lambdas, niter);
    break;
  case 6:
    return pte_closure_josh<6>(eoss, vfrac_tot, sie_tot, rho, vfrac, sie, temp, press,
                               lambdas, niter);
    break;
  case 7:
    return pte_closure_josh<7>(eoss, vfrac_tot, sie_tot, rho, vfrac, sie, temp, press,
                               lambdas, niter);
    break;
  case 8:
    return pte_closure_josh<8>(eoss, vfrac_tot, sie_tot, rho, vfrac, sie, temp, press,
                               lambdas, niter);
    break;
  }
  return false;
}
PORTABLE_INLINE_FUNCTION bool
pte_closure_josh_offset(int nmat, EOS *eoss, const Real vfrac_tot, const Real sie_tot,
                        int const *const Mats, Real *rho, Real *vfrac, Real *sie,
                        Real *temp, Real *press, Real **lambdas, int &niter) {
  switch (nmat) {
  case 1:
    return true; // (or should we call the EOS actually?)
  case 2:
    return pte_closure_josh_offset<2>(eoss, vfrac_tot, sie_tot, Mats, rho, vfrac, sie,
                                      temp, press, lambdas, niter);
    break;
  case 3:
    return pte_closure_josh_offset<3>(eoss, vfrac_tot, sie_tot, Mats, rho, vfrac, sie,
                                      temp, press, lambdas, niter);
    break;
  case 4:
    return pte_closure_josh_offset<4>(eoss, vfrac_tot, sie_tot, Mats, rho, vfrac, sie,
                                      temp, press, lambdas, niter);
    break;
  case 5:
    return pte_closure_josh_offset<5>(eoss, vfrac_tot, sie_tot, Mats, rho, vfrac, sie,
                                      temp, press, lambdas, niter);
    break;
  case 6:
    return pte_closure_josh_offset<6>(eoss, vfrac_tot, sie_tot, Mats, rho, vfrac, sie,
                                      temp, press, lambdas, niter);
    break;
  case 7:
    return pte_closure_josh_offset<7>(eoss, vfrac_tot, sie_tot, Mats, rho, vfrac, sie,
                                      temp, press, lambdas, niter);
    break;
  case 8:
    return pte_closure_josh_offset<8>(eoss, vfrac_tot, sie_tot, Mats, rho, vfrac, sie,
                                      temp, press, lambdas, niter);
    break;
  }
  return false;
}
template <typename EOSIndexer, typename RealIndexer, typename ConstRealIndexer,
          typename LambdaIndexer>
PORTABLE_INLINE_FUNCTION bool
pte_closure_ideal(int nmat, EOSIndexer &&eoss, const Real Volume, const Real TotalSIE,
                  ConstRealIndexer &&ComponentMasses, RealIndexer &&ComponentVolumes,
                  RealIndexer &&ComponentEnergies, LambdaIndexer &&lambdas, int &niter) {
  switch (nmat) {
  case 1:
    return true; // (or should we call the EOS actually?)
  case 2:
    return pte_closure_ideal<2>(eoss, Volume, TotalSIE, ComponentMasses, ComponentVolumes,
                                ComponentEnergies, lambdas, niter);
    break;
  case 3:
    return pte_closure_ideal<3>(eoss, Volume, TotalSIE, ComponentMasses, ComponentVolumes,
                                ComponentEnergies, lambdas, niter);
    break;
  case 4:
    return pte_closure_ideal<4>(eoss, Volume, TotalSIE, ComponentMasses, ComponentVolumes,
                                ComponentEnergies, lambdas, niter);
    break;
  case 5:
    return pte_closure_ideal<5>(eoss, Volume, TotalSIE, ComponentMasses, ComponentVolumes,
                                ComponentEnergies, lambdas, niter);
    break;
  case 6:
    return pte_closure_ideal<6>(eoss, Volume, TotalSIE, ComponentMasses, ComponentVolumes,
                                ComponentEnergies, lambdas, niter);
    break;
  case 7:
    return pte_closure_ideal<7>(eoss, Volume, TotalSIE, ComponentMasses, ComponentVolumes,
                                ComponentEnergies, lambdas, niter);
    break;
  case 8:
    return pte_closure_ideal<8>(eoss, Volume, TotalSIE, ComponentMasses, ComponentVolumes,
                                ComponentEnergies, lambdas, niter);
    break;
  }
  return false;
}
} // namespace singularity

#endif // _SINGULARITY_EOS_CLOSURE_MIXED_CELL_MODELS_


#ifndef FSTCLASSIFIERKNN_H
#define FSTCLASSIFIERKNN_H

/*!======================================================================
   Feature Selection Toolbox 3 source code
   ---------------------------------------

   \file    classifier_knn.hpp
   \brief   Implements k-Nearest Neighbor classifier
   \author  Petr Somol (somol@utia.cas.cz) with collaborators, see Contacts at http://fst.utia.cz
   \date    March 2011
   \version 3.1.0.beta
   \note    FST3 was developed using gcc 4.3 and requires
   \note    \li Boost library (http://www.boost.org/, tested with versions 1.33.1 and 1.44),
   \note    \li (\e optionally) LibSVM (http://www.csie.ntu.edu.tw/~cjlin/libsvm/,
                tested with version 3.00)
   \note    Note that LibSVM is required for SVM related tools only,
            as demonstrated in demo12t.cpp, demo23.cpp, demo25t.cpp, demo32t.cpp, etc.

*/ /*
=========================================================================
Copyright:
  * FST3 software (with exception of any externally linked libraries)
    is copyrighted by Institute of Information Theory and Automation (UTIA),
    Academy of Sciences of the Czech Republic.
  * FST3 source codes as presented here do not contain code of third parties.
    FST3 may need linkage to external libraries to exploit its functionality
    in full. For details on obtaining and possible usage restrictions
    of external libraries follow their original sources (referenced from
    FST3 documentation wherever applicable).
  * FST3 software is available free of charge for non-commercial use.
    Please address all inquires concerning possible commercial use
    of FST3, or if in doubt, to FST3 maintainer (see http://fst.utia.cz)
  * Derivative works based on FST3 are permitted as long as they remain
    non-commercial only.
  * Re-distribution of FST3 software is not allowed without explicit
    consent of the copyright holder.
Disclaimer of Warranty:
  * FST3 software is presented "as is", without warranty of any kind,
    either expressed or implied, including, but not limited to, the implied
    warranties of merchantability and fitness for a particular purpose.
    The entire risk as to the quality and performance of the program
    is with you. Should the program prove defective, you assume the cost
    of all necessary servicing, repair or correction.
Limitation of Liability:
  * The copyright holder will in no event be liable to you for damages,
    including any general, special, incidental or consequential damages
    arising out of the use or inability to use the code (including but not
    limited to loss of data or data being rendered inaccurate or losses
    sustained by you or third parties or a failure of the program to operate
    with any other programs).
========================================================================== */

#include <boost/smart_ptr.hpp>
#include <iostream>
#include <cmath>
#include <list>
#include "classifier.hpp"
#include "error.hpp"
#include "global.hpp"

/*============== Template parameter type naming conventions ==============
--------- Numeric types: -------------------------------------------------
DATATYPE - data sample values - usually real numbers (but may be integers
          in text processing etc.)
REALTYPE - must be real numbers - for representing intermediate results of
          calculations like mean, covariance etc.
IDXTYPE - index values for enumeration of data samples - (nonnegative) integers,
          extent depends on numbers of samples in data
DIMTYPE - index values for enumeration of features (dimensions), or classes (not
          class sizes) - (nonnegative) integers, usually lower extent than IDXTYPE,
          but be aware of expressions like _classes*_features*_features !
          in linearized representations of feature matrices for all classes
BINTYPE - feature selection marker type - represents ca. <10 different feature
          states (selected, deselected, sel./desel. temporarily 1st nested loop, 2nd...)
RETURNTYPE - criterion value: real value, but may be extended in future to support
          multiple values
--------- Class types: ---------------------------------------------------
SUBSET       - class of class type Subset
CLASSIFIER   - class implementing interface defined in abstract class Classifier
EVALUATOR    - class implementing interface defined in abstract class Sequential_Step
DISTANCE     - class implementing interface defined in abstract class Distance
DATAACCESSOR - class implementing interface defined in abstract class Data_Accessor
INTERVALCONTAINER - class of class type TIntervaller
CONTAINER    - STL container of class type TInterval
========================================================================== */

namespace FST {

//! Implements k-Nearest Neighbor classifier
template<class RETURNTYPE, typename DATATYPE, typename IDXTYPE, typename DIMTYPE, class SUBSET, class DATAACCESSOR, class DISTANCE>
class Classifier_kNN : public Classifier<RETURNTYPE,DIMTYPE,SUBSET,DATAACCESSOR> {
public:
	typedef Classifier<RETURNTYPE,DIMTYPE,SUBSET,DATAACCESSOR> parent;
	typedef boost::shared_ptr<DATAACCESSOR> PDataAccessor;
	typedef boost::shared_ptr<SUBSET> PSubset;
	typedef typename DATAACCESSOR::PPattern PPattern;
	Classifier_kNN() {set_k(1); _nns_max_size=1; notify("Classifier_kNN constructor.");}
	virtual ~Classifier_kNN() {notify("Classifier_kNN destructor.");}

	void set_k(const DIMTYPE k) {assert(k>0); _k=k; _nns_max_size=_k;} // NOTE: _nns_max_size may be set larger to help avoiding ties
	DIMTYPE get_k() const {return _k;}

	virtual bool classify(DIMTYPE &cls, const PPattern &pattern);  // classifies pattern, returns the respective class index
	virtual bool train(const PDataAccessor da, const PSubset sub); // with kNN there is actually no training. This just stores pointer to training data to be later accessed from test()
	virtual bool test(RETURNTYPE &result, const PDataAccessor da); // estimates accuracy using designated test data

	Classifier_kNN* clone() const;
	Classifier_kNN* sharing_clone() const {throw fst_error("Classifier_kNN::sharing_clone() not supported, use Classifier_kNN::clone() instead.");}
	Classifier_kNN* stateless_clone() const {throw fst_error("Classifier_kNN::stateless_clone() not supported, use Classifier_kNN::clone() instead.");}

	virtual std::ostream& print(std::ostream& os) const;
private:
	Classifier_kNN(const Classifier_kNN& cknn); // copy-constructor
protected:
	// NOTE: for given k it is better to keep ((k-1)*NoOfClasses+1) nearest neighbours to prevent ties (provided enough neighbours exist)
	// NOTE: equal neighbour distances to different classes are not handled extra

	//! Holds information on distance and data-class membership of neighbors processed in Classifier_kNN
	class Neighbour{
	public:
		Neighbour() {}
		Neighbour(const RETURNTYPE value, const DIMTYPE cls) {_value=value; _cls=cls;}
		Neighbour(const Neighbour& nei) {_value=nei._value; _cls=nei._cls;}	//!< copy constructor
		RETURNTYPE _value;
		DIMTYPE _cls;
	};
	DIMTYPE _k;
	DIMTYPE _k_enough;
	DIMTYPE _nns_max_size;
	void sort_in(const RETURNTYPE value, const DIMTYPE cls);
	DIMTYPE get_most_freq_cls();
	typename std::list<Neighbour>::iterator iter;
	std::list<Neighbour> _nns; // implemented descending ... the closest neighbour is the last
	std::vector<DIMTYPE> cls_freqs;

	boost::scoped_ptr<DISTANCE> _distance;
	PDataAccessor _da_train; // with kNN there is actually no training. This is meant to store pointer to training data to be later accessed from test()
};

template<class RETURNTYPE, typename DATATYPE, typename IDXTYPE, typename DIMTYPE, class SUBSET, class DATAACCESSOR, class DISTANCE>
Classifier_kNN<RETURNTYPE,DATATYPE,IDXTYPE,DIMTYPE,SUBSET,DATAACCESSOR,DISTANCE>::Classifier_kNN(const Classifier_kNN& cknn) :
	_k(cknn._k),
	_k_enough(cknn._k_enough),
	_nns_max_size(cknn._nns_max_size),
	_nns(cknn._nns),
	cls_freqs(cknn.cls_freqs)
{
	notify("Classifier_kNN constructor.");
	if(cknn._distance) _distance.reset(cknn._distance->clone());
}

template<class RETURNTYPE, typename DATATYPE, typename IDXTYPE, typename DIMTYPE, class SUBSET, class DATAACCESSOR, class DISTANCE>
Classifier_kNN<RETURNTYPE,DATATYPE,IDXTYPE,DIMTYPE,SUBSET,DATAACCESSOR,DISTANCE>* Classifier_kNN<RETURNTYPE,DATATYPE,IDXTYPE,DIMTYPE,SUBSET,DATAACCESSOR,DISTANCE>::clone() const
{
	Classifier_kNN<RETURNTYPE,DATATYPE,IDXTYPE,DIMTYPE,SUBSET,DATAACCESSOR,DISTANCE> *clone=new Classifier_kNN<RETURNTYPE,DATATYPE,IDXTYPE,DIMTYPE,SUBSET,DATAACCESSOR,DISTANCE>(*this);
	clone->set_cloned();
	return clone;
}

template<class RETURNTYPE, typename DATATYPE, typename IDXTYPE, typename DIMTYPE, class SUBSET, class DATAACCESSOR, class DISTANCE>
bool Classifier_kNN<RETURNTYPE,DATATYPE,IDXTYPE,DIMTYPE,SUBSET,DATAACCESSOR,DISTANCE>::train(const PDataAccessor da, const PSubset sub)
{
	assert(da);
	assert(sub);
	if(!_distance || _distance->get_n()<sub->get_n_raw()) _distance.reset(new DISTANCE(sub->get_n_raw()));
	_distance->narrow_to(sub);
	_da_train=da;
	return true;
}

template<class RETURNTYPE, typename DATATYPE, typename IDXTYPE, typename DIMTYPE, class SUBSET, class DATAACCESSOR, class DISTANCE>
bool Classifier_kNN<RETURNTYPE,DATATYPE,IDXTYPE,DIMTYPE,SUBSET,DATAACCESSOR,DISTANCE>::classify(DIMTYPE &cls, const PPattern &pattern)
{
	// NOTE: uses da_train_loop=0
	// NOTE: da and _da_train may point to the same instance -> be careful with setClass() etc.
	assert(_da_train);
	assert(_distance);
	assert(_distance->get_n()==_da_train->getNoOfFeatures());
	assert(_distance->get_d()>0);
	typename DATAACCESSOR::PPattern p2;
	IDXTYPE s2,i2;
	bool b;
	DIMTYPE _feats=_da_train->getNoOfFeatures();
	RETURNTYPE val;

	const DIMTYPE da_train_loop=0; // to avoid mixup of get*Block() loops of different types be careful when calling classify() from a different loop

	cls_freqs.resize(_da_train->getNoOfClasses());
	_nns_max_size=(_k-1)*_da_train->getNoOfClasses()+1; // to help avoiding ties
	_k_enough=DIMTYPE(floor(0.5*(RETURNTYPE)_k)+1);

	_nns.clear();
	for(DIMTYPE c_train=0;c_train<_da_train->getNoOfClasses();c_train++)
	{
		_da_train->setClass(c_train);
		for(b=_da_train->getFirstBlock(TRAIN,p2,s2,da_train_loop);b==true;b=_da_train->getNextBlock(TRAIN,p2,s2,da_train_loop)) for(i2=0;i2<s2;i2++)
		{
			// p2[i2*_feats] is the beginning of the current pattern
			val=_distance->distance(pattern,&p2[i2*_feats]);
			sort_in(val,c_train);
		}
	}
	cls=get_most_freq_cls();

	return true;
}

template<class RETURNTYPE, typename DATATYPE, typename IDXTYPE, typename DIMTYPE, class SUBSET, class DATAACCESSOR, class DISTANCE>
bool Classifier_kNN<RETURNTYPE,DATATYPE,IDXTYPE,DIMTYPE,SUBSET,DATAACCESSOR,DISTANCE>::test(RETURNTYPE &result, const PDataAccessor da)
{
	// NOTE: da and _da_train may point to the same instance -> be careful with setClass() etc.
	assert(da);
	assert(_da_train);
	assert(_distance);
	assert(da->getNoOfFeatures()==_da_train->getNoOfFeatures());
	assert(_distance->get_n()==_da_train->getNoOfFeatures());
	typename DATAACCESSOR::PPattern p1;
	IDXTYPE s1,i1;
	bool b;
	IDXTYPE count=0, correct=0;
	DIMTYPE _feats=da->getNoOfFeatures();
	DIMTYPE clstmp;

	const DIMTYPE da_test_loop=1; // to avoid mixup of get*Block() loops of different types (must not! be 0, see classify())

	cls_freqs.resize(_da_train->getNoOfClasses());
	_nns_max_size=(_k-1)*_da_train->getNoOfClasses()+1; // to help avoiding ties
	_k_enough=DIMTYPE(floor(0.5*(RETURNTYPE)_k)+1);

	for(DIMTYPE c_test=0;c_test<da->getNoOfClasses();c_test++)
	{
		da->setClass(c_test);
		for(b=da->getFirstBlock(TEST,p1,s1,da_test_loop);b==true;b=da->getNextBlock(TEST,p1,s1,da_test_loop)) {
			for(i1=0;i1<s1;i1++)
			{
				if(!classify(clstmp,&p1[i1*_feats])) return false; // \note classify() internally uses block loop index 0
				if(clstmp==c_test) ++correct;
				++count;
			}
			da->setClass(c_test); // necessary for the case of da pointing to the same object as _da_train
		}
	}
	if(count==0) return false;
	result = (RETURNTYPE)correct/(RETURNTYPE)count;
	return true;
}

template<class RETURNTYPE, typename DATATYPE, typename IDXTYPE, typename DIMTYPE, class SUBSET, class DATAACCESSOR, class DISTANCE>
void Classifier_kNN<RETURNTYPE,DATATYPE,IDXTYPE,DIMTYPE,SUBSET,DATAACCESSOR,DISTANCE>::sort_in(const RETURNTYPE value, const DIMTYPE cls)
{
	iter=_nns.begin();
	while(iter!=_nns.end() && value < (*iter)._value) iter++;
	if(_nns.size()<_nns_max_size || iter!=_nns.begin()) {
		Neighbour tmp(value,cls);
		_nns.insert(iter,tmp);
		// NOTE: ? consider: if(as below && first value!= second value) _nns.pop_front() .. to prevent loosing tie info
		if(_nns.size()>_nns_max_size) _nns.pop_front();
	}
}

template<class RETURNTYPE, typename DATATYPE, typename IDXTYPE, typename DIMTYPE, class SUBSET, class DATAACCESSOR, class DISTANCE>
DIMTYPE Classifier_kNN<RETURNTYPE,DATATYPE,IDXTYPE,DIMTYPE,SUBSET,DATAACCESSOR,DISTANCE>::get_most_freq_cls()
{ // NOTE: destructs contents of _nns !
	assert(_da_train);
	DIMTYPE i,c,c_max,max=0;
	for(i=0;i<_da_train->getNoOfClasses();i++) cls_freqs[i]=0;
	while(_nns.size()>0) {
		c=(_nns.back()._cls);
		if(++cls_freqs[c]==_k_enough) return c;
		if(cls_freqs[c]>max) {max=cls_freqs[c]; c_max=c;}
		_nns.pop_back();
	}
	if(max>0) return c_max;
	else return _da_train->getNoOfClasses(); // wrong class
}

template<class RETURNTYPE, typename DATATYPE, typename IDXTYPE, typename DIMTYPE, class SUBSET, class DATAACCESSOR, class DISTANCE>
std::ostream& Classifier_kNN<RETURNTYPE,DATATYPE,IDXTYPE,DIMTYPE,SUBSET,DATAACCESSOR,DISTANCE>::print(std::ostream& os) const
{
	os << "Clasifier_kNN(k=" << _k << ")";
	if(_distance) os << *_distance;
	return os;
}

} // namespace
#endif // FSTCLASSIFIERKNN_H ///:~


/*    Copyright (c) 2010-2018, Delft University of Technology
 *    All rigths reserved
 *
 *    This file is part of the Tudat. Redistribution and use in source and
 *    binary forms, with or without modification, are permitted exclusively
 *    under the terms of the Modified BSD license. You should have received
 *    a copy of the license with this file. If not, please or visit:
 *    http://tudat.tudelft.nl/LICENSE.
 *
 *    References
 *      Gentry, A., Smyth, D., and Oliver, W. . The Mark IV Supersonic-Hypersonic Arbitrary Body
 *        Program, Volume II - Program Formulation, Douglas Aircraft Aircraft Company, 1973.
 *
 */

#include <string>

#include <boost/bind.hpp>
#include <functional>
#include <boost/lambda/lambda.hpp>
#include <boost/make_shared.hpp>
#include <boost/pointer_cast.hpp>
#include <memory>

#include <Eigen/Geometry>

#include "Tudat/Mathematics/BasicMathematics/mathematicalConstants.h"

#include "Tudat/Astrodynamics/Aerodynamics/aerodynamics.h"
#include "Tudat/Astrodynamics/Aerodynamics/hypersonicLocalInclinationAnalysis.h"
#include "Tudat/Mathematics/GeometricShapes/compositeSurfaceGeometry.h"
#include "Tudat/Mathematics/GeometricShapes/surfaceGeometry.h"
#include "Tudat/InputOutput/basicInputOutput.h"

namespace tudat
{
namespace aerodynamics
{

using Eigen::Vector6d;
using mathematical_constants::PI;

using namespace geometric_shapes;

//! Returns default values of mach number for use in HypersonicLocalInclinationAnalysis.
std::vector< double > getDefaultHypersonicLocalInclinationMachPoints(
        const std::string& machRegime )
{
    std::vector< double > machPoints;

    // Set default points for full hypersonic analysis.
    if ( machRegime == "Full" )
    {
        machPoints.resize( 6 );

        machPoints[ 0 ] = 3.0;
        machPoints[ 1 ] = 4.0;
        machPoints[ 2 ] = 5.0;
        machPoints[ 3 ] = 8.0;
        machPoints[ 4 ] = 10.0;
        machPoints[ 5 ] = 20.0;
    }

    // Set default points for low hypersonic analysis.
    else if ( machRegime == "Low" )
    {
        machPoints.resize( 5 );
        machPoints[ 0 ] = 3.0;
        machPoints[ 1 ] = 4.0;
        machPoints[ 2 ] = 5.0;
        machPoints[ 3 ] = 8.0;
        machPoints[ 4 ] = 10.0;
    }

    // Set default points for high hypersonic analysis.
    else if ( machRegime == "High" )
    {
        machPoints.resize( 4 );
        machPoints[ 0 ] = 5.0;
        machPoints[ 1 ] = 8.0;
        machPoints[ 2 ] = 10.0;
        machPoints[ 3 ] = 20.0;
    }
    return machPoints;
}

//! Returns default values of angle of attack for use in HypersonicLocalInclinationAnalysis.
std::vector< double > getDefaultHypersonicLocalInclinationAngleOfAttackPoints( )
{
    std::vector< double > angleOfAttackPoints;

    // Set number of data points and allocate memory.
    angleOfAttackPoints.resize( 11 );

    // Set default values, 0 to 40 degrees, with steps of 5 degrees.
    for ( int i = 0; i < 11; i++ )
    {
        angleOfAttackPoints[ i ] =
                ( static_cast< double >( i ) * 5.0 * PI / 180.0 );
    }
    return angleOfAttackPoints;
}

//! Returns default values of angle of sideslip for use in HypersonicLocalInclinationAnalysis.
std::vector< double > getDefaultHypersonicLocalInclinationAngleOfSideslipPoints( )
{
    std::vector< double > angleOfSideslipPoints;

    // Set number of data points and allocate memory.
    angleOfSideslipPoints.resize( 2 );

    // Set default values, 0 and 1 degrees.
    angleOfSideslipPoints[ 0 ] = 0.0;
    angleOfSideslipPoints[ 1 ] = 1.0 * PI / 180.0;

    return angleOfSideslipPoints;
}

//! Function that saves the vehicle mesh data used for a HypersonicLocalInclinationAnalysis to a file
void saveVehicleMeshToFile(
        const std::shared_ptr< HypersonicLocalInclinationAnalysis > localInclinationAnalysis,
        const std::string directory,
        const std::string filePrefix )
{
    std::vector< boost::multi_array< Eigen::Vector3d, 2 > > meshPoints =
            localInclinationAnalysis->getMeshPoints( );
    std::vector< boost::multi_array< Eigen::Vector3d, 2 > > meshSurfaceNormals =
            localInclinationAnalysis->getPanelSurfaceNormals( );

//    boost::array< int, 3 > independentVariables;
//    independentVariables[ 0 ] = 0;
//    independentVariables[ 1 ] = 6;
//    independentVariables[ 2 ] = 0;

//    std::vector< std::vector< std::vector< double > > > pressureCoefficients =
//            localInclinationAnalysis->getPressureCoefficientList( independentVariables );

    int counter = 0;
    std::map< int, Eigen::Vector3d > meshPointsList;
    std::map< int, Eigen::Vector3d > surfaceNormalsList;
//    std::map< int, Eigen::Vector1d > pressureCoefficientsList;

    for( unsigned int i = 0; i < meshPoints.size( ); i++ )
    {
        for( unsigned int j = 0; j < meshPoints.at( i ).shape( )[ 0 ] - 1; j++ )
        {
            for( unsigned int k = 0; k < meshPoints.at( i ).shape( )[ 1 ] - 1; k++ )
            {
                meshPointsList[ counter ] = meshPoints[ i ][ j ][ k ];
                surfaceNormalsList[ counter ] = meshSurfaceNormals[ i ][ j ][ k ];
//                pressureCoefficientsList[ counter ] = ( Eigen::Vector1d( ) << pressureCoefficients[ i ][ j ][ k ] ).finished( );
                counter++;
            }
        }
    }

    input_output::writeDataMapToTextFile(
                meshPointsList, filePrefix + "ShapeFile.dat", directory );
    input_output::writeDataMapToTextFile(
                surfaceNormalsList, filePrefix + "SurfaceNormalFile.dat", directory );

//    input_output::writeDataMapToTextFile(
//                pressureCoefficientsList, filePrefix + "pressureCoefficientFile.dat", directory );
}

//! Default constructor.
HypersonicLocalInclinationAnalysis::HypersonicLocalInclinationAnalysis(
        const std::vector< std::vector< double > >& dataPointsOfIndependentVariables,
        const std::shared_ptr< SurfaceGeometry > inputVehicleSurface,
        const std::vector< int >& numberOfLines,
        const std::vector< int >& numberOfPoints,
        const std::vector< bool >& invertOrders,
        const std::vector< std::vector< int > >& selectedMethods,
        const double referenceArea,
        const double referenceLength,
        const Eigen::Vector3d& momentReferencePoint,
        const bool savePressureCoefficients )
    : AerodynamicCoefficientGenerator< 3, 6 >(
          dataPointsOfIndependentVariables, referenceLength, referenceArea, referenceLength,
          momentReferencePoint, { mach_number_dependent, angle_of_attack_dependent, angle_of_sideslip_dependent },true, false ),
      stagnationPressureCoefficient( 2.0 ),
      ratioOfSpecificHeats( 1.4 ),
      selectedMethods_( selectedMethods ),
      savePressureCoefficients_( savePressureCoefficients )
{
    // Set geometry if it is a single surface.
    if ( std::dynamic_pointer_cast< SingleSurfaceGeometry > ( inputVehicleSurface ) !=
         std::shared_ptr< SingleSurfaceGeometry >( ) )
    {
        // Set number of geometries and allocate memory.
        vehicleParts_.resize( 1 );

        vehicleParts_[ 0 ] = std::make_shared< LawgsPartGeometry >( );
        vehicleParts_[ 0 ]->setReversalOperator( invertOrders[ 0 ] );

        // If part is not already a LaWGS part, convert it.
        if ( std::dynamic_pointer_cast< LawgsPartGeometry >
             ( inputVehicleSurface ) ==
             std::shared_ptr< LawgsPartGeometry >( ) )
        {
            // Convert geometry to LaWGS surface mesh and set in vehicleParts_ list.
            vehicleParts_[ 0 ]->setMesh(
                        std::dynamic_pointer_cast< SingleSurfaceGeometry > ( inputVehicleSurface ),
                        numberOfLines[ 0 ], numberOfPoints[ 0 ] );
        }

        // Else, set geometry directly.
        else
        {
            vehicleParts_[ 0 ] = std::dynamic_pointer_cast< LawgsPartGeometry >
                    ( inputVehicleSurface );
        }
    }

    // Set geometry if it is a composite surface.
    else if ( std::dynamic_pointer_cast< CompositeSurfaceGeometry >( inputVehicleSurface ) !=
              std::shared_ptr< CompositeSurfaceGeometry >( ) )
    {
        // Dynamic cast to composite surface geometry for further processing.
        std::shared_ptr< CompositeSurfaceGeometry > compositeSurfaceGeometry_ =
                std::dynamic_pointer_cast< CompositeSurfaceGeometry >( inputVehicleSurface );

        // Set number of geometries and allocate memory.
        int numberOfVehicleParts =
                compositeSurfaceGeometry_->getNumberOfSingleSurfaceGeometries( );
        vehicleParts_.resize( numberOfVehicleParts );

        // Iterate through all parts and set them in vehicleParts_ list.
        for ( int i = 0; i < numberOfVehicleParts; i++ )
        {
            // If part is not already a LaWGS part, convert it.
            if ( std::dynamic_pointer_cast< LawgsPartGeometry >
                 ( compositeSurfaceGeometry_->getSingleSurfaceGeometry( i ) ) ==
                 std::shared_ptr< LawgsPartGeometry >( ) )
            {
                vehicleParts_[ i ] = std::make_shared< LawgsPartGeometry >( );
                vehicleParts_[ i ]->setReversalOperator( invertOrders[ i ] );

                // Convert geometry to LaWGS and set in list.
                vehicleParts_[ i ]->setMesh(
                            compositeSurfaceGeometry_->getSingleSurfaceGeometry( i ),
                            numberOfLines[ i ], numberOfPoints[ i ] );
            }

            // Else, set geometry directly.
            else
            {
                vehicleParts_[ i ] = std::dynamic_pointer_cast< LawgsPartGeometry >(
                            compositeSurfaceGeometry_->getSingleSurfaceGeometry( i ) );
            }
        }
    }

    // Allocate memory for panel inclinations and pressureCoefficient_.
    inclination_.resize( vehicleParts_.size( ) );
    pressureCoefficient_.resize( vehicleParts_.size( ) );
    for ( unsigned int i = 0 ; i < vehicleParts_.size( ); i++ )
    {
        inclination_[ i ].resize( vehicleParts_[ i ]->getNumberOfLines( ) );
        pressureCoefficient_[ i ].resize( vehicleParts_[ i ]->getNumberOfLines( ) );
        for ( int j = 0 ; j < vehicleParts_[ i ]->getNumberOfLines( ) ; j++ )
        {
            inclination_[ i ][ j ].resize( vehicleParts_[ i ]->getNumberOfPoints( ) );
            pressureCoefficient_[ i ][ j ].resize( vehicleParts_[ i ]->getNumberOfPoints( ) );
        }
    }

    boost::array< int, 3 > numberOfPointsPerIndependentVariables;
    for( int i = 0; i < 3; i++ )
    {
        numberOfPointsPerIndependentVariables[ i ] =
                dataPointsOfIndependentVariables_[ i ].size( );
    }

    isCoefficientGenerated_.resize( numberOfPointsPerIndependentVariables );

    std::fill( isCoefficientGenerated_.origin( ),
               isCoefficientGenerated_.origin( ) + isCoefficientGenerated_.num_elements( ), 0 );

    generateCoefficients( );
    createInterpolator( );
}

//! Get aerodynamic coefficients.
Vector6d HypersonicLocalInclinationAnalysis::getAerodynamicCoefficientsDataPoint(
        const boost::array< int, 3 > independentVariables )
{
    if( isCoefficientGenerated_( independentVariables ) == 0 )
    {
        determineVehicleCoefficients( independentVariables );
    }

    // Return requested coefficients.
    return aerodynamicCoefficients_( independentVariables );
}

//! Generate aerodynamic database.
void HypersonicLocalInclinationAnalysis::generateCoefficients( )
{
    // Allocate variable to pass to coefficient determination for independent
    // variable indices.
    boost::array< int, 3 > independentVariableIndices;

    // Iterate over all combinations of independent variables.
    for ( unsigned int i = 0 ; i < dataPointsOfIndependentVariables_[ 0 ].size( ) ; i++ )
    {
        independentVariableIndices[ 0 ] = i;
        for ( unsigned  int j = 0 ; j < dataPointsOfIndependentVariables_[
              1 ].size( ) ; j++ )
        {
            independentVariableIndices[ 1 ] = j;
            for ( unsigned  int k = 0 ; k < dataPointsOfIndependentVariables_[
                  2 ].size( ) ; k++ )
            {
                independentVariableIndices[ 2 ] = k;

                determineVehicleCoefficients( independentVariableIndices );
            }
        }
    }
}

//! Generate aerodynamic coefficients at a single set of independent variables.
void HypersonicLocalInclinationAnalysis::determineVehicleCoefficients(
        const boost::array< int, 3 > independentVariableIndices )
{
    // Declare coefficients vector and initialize to zeros.
    Vector6d coefficients = Vector6d::Zero( );

    // Loop over all vehicle parts, calculate aerodynamic coefficients and add
    // to aerodynamicCoefficients_.
    for ( unsigned int i = 0 ; i < vehicleParts_.size( ) ; i++ )
    {
        coefficients += determinePartCoefficients( i, independentVariableIndices );
    }

    if( savePressureCoefficients_ )
    {
        pressureCoefficientList_[ independentVariableIndices ] = pressureCoefficient_;
    }

    aerodynamicCoefficients_( independentVariableIndices ) = coefficients;
    isCoefficientGenerated_( independentVariableIndices ) = 1;
}

//! Determine aerodynamic coefficients of a single vehicle part.
Vector6d HypersonicLocalInclinationAnalysis::determinePartCoefficients(
        const int partNumber, const boost::array< int, 3 > independentVariableIndices )
{
    // Declare and determine angles of attack and sideslip for analysis.
    double angleOfAttack =  dataPointsOfIndependentVariables_[ 1 ]
            [ independentVariableIndices[ 1 ] ];

    double angleOfSideslip =  dataPointsOfIndependentVariables_[ 2 ]
            [ independentVariableIndices[ 2 ] ];

    // Declare partCoefficient vector.
    Vector6d partCoefficients = Vector6d::Zero( );

    // Check whether the inclinations of the vehicle part have already been computed.
    if ( previouslyComputedInclinations_.count( std::pair< double, double >(
                                                    angleOfAttack, angleOfSideslip ) ) == 0 )
    {
        // Determine panel inclinations for part.
        determineInclinations( angleOfAttack, angleOfSideslip );

        // Add panel inclinations to container
        previouslyComputedInclinations_[ std::pair< double, double >(
                    angleOfAttack, angleOfSideslip ) ] = inclination_;
    }

    else
    {
        // Fetch inclinations from container
        inclination_ = previouslyComputedInclinations_[ std::pair< double, double >(
                    angleOfAttack, angleOfSideslip ) ];
    }

    // Set pressureCoefficient_ array for given independent variables.
    determinePressureCoefficients( partNumber, independentVariableIndices );

    // Calculate force coefficients from pressure coefficients.
    partCoefficients.segment( 0, 3 ) = calculateForceCoefficients( partNumber );

    // Calculate moment coefficients from pressure coefficients.
    partCoefficients.segment( 3, 3 ) = calculateMomentCoefficients( partNumber );

    return partCoefficients;
}

//! Determine the pressure coefficients on a single vehicle part.
void HypersonicLocalInclinationAnalysis::determinePressureCoefficients(
        const int partNumber, const boost::array< int, 3 > independentVariableIndices )
{
    // Retrieve Mach number.
    double machNumber = dataPointsOfIndependentVariables_[ 0 ]
            [ independentVariableIndices[ 0 ] ];

    // Determine stagnation point pressure coefficients. Value is computed once
    // here to prevent its calculation in inner loop.
    stagnationPressureCoefficient = computeStagnationPressure(
                machNumber, ratioOfSpecificHeats );

    updateCompressionPressures( machNumber, partNumber );
    updateExpansionPressures( machNumber, partNumber );
}

//! Determine force coefficients from pressure coefficients.
Eigen::Vector3d HypersonicLocalInclinationAnalysis::calculateForceCoefficients(
        const int partNumber )
{
    // Declare force coefficient vector and intialize to zeros.
    Eigen::Vector3d forceCoefficients = Eigen::Vector3d::Zero( );

    // Loop over all panels and add pressures, scaled by panel area, to force
    // coefficients.
    for ( int i = 0 ; i < vehicleParts_[ partNumber ]->getNumberOfLines( ) - 1 ; i++ )
    {
        for ( int j = 0 ; j < vehicleParts_[ partNumber ]->getNumberOfPoints( ) - 1 ; j++)
        {
            forceCoefficients -=
                    pressureCoefficient_[ partNumber ][ i ][ j ] *
                    vehicleParts_[ partNumber ]->getPanelArea( i, j ) *
                    vehicleParts_[ partNumber ]->getPanelSurfaceNormal( i, j );
        }
    }

    // Normalize result by reference area.
    forceCoefficients /= referenceArea_;

    return forceCoefficients;
}

//! Determine moment coefficients from pressure coefficients.
Eigen::Vector3d HypersonicLocalInclinationAnalysis::calculateMomentCoefficients(
        const int partNumber )
{
    // Declare moment coefficient vector and intialize to zeros.
    Eigen::Vector3d momentCoefficients = Eigen::Vector3d::Zero( );

    // Declare moment arm for panel moment determination.
    Eigen::Vector3d referenceDistance;

    // Loop over all panels and add moments due pressures.
    for ( int i = 0 ; i < vehicleParts_[ partNumber ]->getNumberOfLines( ) - 1 ; i++ )
    {
        for ( int j = 0 ; j < vehicleParts_[ partNumber ]->getNumberOfPoints( ) - 1 ; j++ )
        {
            // Determine moment arm for given panel centroid.
            referenceDistance = ( vehicleParts_[ partNumber ]->getPanelCentroid( i, j ) -
                                  momentReferencePoint_ );

            momentCoefficients -=
                    pressureCoefficient_[ partNumber ][ i ][ j ] *
                    vehicleParts_[ partNumber ]->getPanelArea( i, j ) *
                    ( referenceDistance.cross( vehicleParts_[ partNumber ]->
                                               getPanelSurfaceNormal( i, j ) ) );
        }
    }

    // Scale result by reference length and area.
    momentCoefficients /= ( referenceLength_ * referenceArea_ );

    return momentCoefficients;
}

//! Determines the inclination angle of panels on a single part.
void HypersonicLocalInclinationAnalysis::determineInclinations( const double angleOfAttack,
                                                                const double angleOfSideslip )
{
    // Declare free-stream velocity vector.
    Eigen::Vector3d freestreamVelocityDirection;

    // Set freestream velocity vector in body frame.
    double freestreamVelocityDirectionX = cos( angleOfAttack )* cos( angleOfSideslip );
    double freestreamVelocityDirectionY = sin( angleOfSideslip );
    double freestreamVelocityDirectionZ = sin( angleOfAttack ) * cos( angleOfSideslip );
    freestreamVelocityDirection( 0 ) = freestreamVelocityDirectionX;
    freestreamVelocityDirection( 1 ) = freestreamVelocityDirectionY;
    freestreamVelocityDirection( 2 ) = freestreamVelocityDirectionZ;

    // Declare cosine of inclination angle.
    double cosineOfInclination;

    // Loop over all panels of given vehicle part and set inclination angles.
    for( unsigned int k = 0; k < vehicleParts_.size( ); k++ )
    {
        for ( int i = 0 ; i < vehicleParts_[ k ]->getNumberOfLines( ) - 1 ; i++ )
        {
            for ( int j = 0 ; j < vehicleParts_[ k ]->getNumberOfPoints( ) - 1 ; j++ )
            {

                // Determine cosine of inclination angle from inner product between
                // surface normal and free-stream direction.
                cosineOfInclination = vehicleParts_[ k ]->
                        getPanelSurfaceNormal( i, j ).
                        dot( freestreamVelocityDirection );

                // Set inclination angle.
                inclination_[ k ][ i ][ j ] = PI / 2.0 - acos( cosineOfInclination );
            }
        }
    }
}

//! Determine compression pressure coefficients on all parts.
void HypersonicLocalInclinationAnalysis::updateCompressionPressures( const double machNumber,
                                                                     const int partNumber )
{
    int method = selectedMethods_[ 0 ][ partNumber ];

    std::function< double( double ) > pressureFunction;

    // Switch to analyze part using correct method.
    switch( method )
    {
    case 0:
        pressureFunction =
                std::bind( aerodynamics::computeNewtonianPressureCoefficient, std::placeholders::_1 );
        break;

    case 1:
        pressureFunction =
                std::bind( aerodynamics::computeModifiedNewtonianPressureCoefficient, std::placeholders::_1,
                           stagnationPressureCoefficient );
        break;

    case 2:
        // Method currently disabled.
        break;

    case 3:
        // Method currently disabled.
        break;

    case 4:
        pressureFunction =
                std::bind( aerodynamics::computeEmpiricalTangentWedgePressureCoefficient, std::placeholders::_1,
                           machNumber );
        break;

    case 5:
        pressureFunction =
                std::bind( aerodynamics::computeEmpiricalTangentConePressureCoefficient, std::placeholders::_1,
                           machNumber );
        break;

    case 6:
        pressureFunction =
                std::bind( aerodynamics::computeModifiedDahlemBuckPressureCoefficient, std::placeholders::_1,
                           machNumber );
        break;

    case 7:
        pressureFunction =
                std::bind( aerodynamics::computeVanDykeUnifiedPressureCoefficient, std::placeholders::_1,
                           machNumber, ratioOfSpecificHeats, 1 );
        break;

    case 8:
        pressureFunction =
                std::bind( aerodynamics::computeSmythDeltaWingPressureCoefficient, std::placeholders::_1,
                           machNumber );
        break;

    case 9:
        pressureFunction =
                std::bind( aerodynamics::computeHankeyFlatSurfacePressureCoefficient, std::placeholders::_1,
                           machNumber );
        break;

    default:
        break;
    }

    for ( int i = 0 ; i < vehicleParts_[ partNumber ]->getNumberOfLines( ) - 1; i++ )
    {
        for ( int j = 0 ; j < vehicleParts_[ partNumber ]->getNumberOfPoints( ) - 1 ; j++ )
        {
            if ( inclination_[ partNumber ][ i ][ j ] > 0 )
            {
                // If panel inclination is positive, calculate pressure coefficient.
                pressureCoefficient_[ partNumber ][ i ][ j ] =
                        pressureFunction( inclination_[ partNumber ][ i ][ j ] );
            }
        }
    }
}

//! Determines expansion pressure coefficients on all parts.
void HypersonicLocalInclinationAnalysis::updateExpansionPressures( const double machNumber,
                                                                   const int partNumber )
{
    // Get analysis method of part to analyze.
    int method = selectedMethods_[ 1 ][ partNumber ];

    if ( method == 0 || method == 1 || method == 4 )
    {
        std::function< double( ) > pressureFunction;
        switch( method )
        {
        case 0:
            pressureFunction = std::bind( &aerodynamics::computeVacuumPressureCoefficient,
                                          machNumber, ratioOfSpecificHeats );
            break;

        case 1:
            pressureFunction = [ ]( ){ return 0.0; };
            break;

        case 4:
            pressureFunction = std::bind( &aerodynamics::computeHighMachBasePressure,
                                          machNumber );
            break;

        }

        // Iterate over all panels on part.
        for ( int i = 0 ; i < vehicleParts_[ partNumber ]->getNumberOfLines( ) - 1 ; i++ )
        {
            for ( int j = 0 ; j < vehicleParts_[ partNumber ]->getNumberOfPoints( ) - 1 ; j++ )
            {
                if ( inclination_[ partNumber ][ i ][ j ] <= 0 )
                {
                    // If panel inclination is negative, calculate pressure using
                    // Van Dyke unified method.
                    pressureCoefficient_[ partNumber ][ i ][ j ] =
                            pressureFunction( );
                }
            }
        }
    }

    else if( method == 3 || method == 5 || method == 6 )
    {

        std::function< double( double ) > pressureFunction;

        // Declare local variable.
        double freestreamPrandtlMeyerFunction;

        // Switch to analyze part using correct method.
        switch( method )
        {
        case 3:
            // Calculate freestream Prandtl-Meyer function.
            freestreamPrandtlMeyerFunction = aerodynamics::computePrandtlMeyerFunction(
                        machNumber, ratioOfSpecificHeats );
            pressureFunction =
                    std::bind( &aerodynamics::computePrandtlMeyerFreestreamPressureCoefficient,
                               std::placeholders::_1, machNumber, ratioOfSpecificHeats,
                               freestreamPrandtlMeyerFunction );
            break;

        case 5:
            pressureFunction =
                    std::bind( &aerodynamics::computePrandtlMeyerFreestreamPressureCoefficient,
                               std::placeholders::_1, machNumber, ratioOfSpecificHeats, -1 );
            break;

        case 6:
            pressureFunction = std::bind( &aerodynamics::computeAcmEmpiricalPressureCoefficient,
                                          std::placeholders::_1, machNumber );
            break;
        }

        // Iterate over all panels on part.
        for ( int i = 0 ; i < vehicleParts_[ partNumber ]->getNumberOfLines( ) - 1 ; i++ )
        {
            for ( int j = 0 ; j < vehicleParts_[ partNumber ]->getNumberOfPoints( ) - 1 ; j++ )
            {
                if ( inclination_[ partNumber ][ i ][ j ] <= 0 )
                {
                    // If panel inclination is negative, calculate pressure using
                    // Van Dyke unified method.
                    pressureCoefficient_[ partNumber ][ i ][ j ] =
                            pressureFunction( inclination_[ partNumber ][ i ][ j ] );
                }
            }
        }
    }

    else
    {
        std::string errorMessage = "Error, expansion local inclination method number "
                + std::to_string( method ) + " not recognized";
        throw std::runtime_error( errorMessage );
    }
}

} // namespace aerodynamics
} // namespace tudat