/**
 * \file
 * \copyright
 * Copyright (c) 2012-2022, OpenGeoSys Community (http://www.opengeosys.org)
 *            Distributed under a Modified BSD License.
 *              See accompanying file LICENSE.txt or
 *              http://www.opengeosys.org/project/license
 */

#include "Ehlers.h"

#include <boost/math/special_functions/pow.hpp>

#include "LinearElasticIsotropic.h"
#include "MaterialLib/MPL/Utils/GetSymmetricTensor.h"
#include "MathLib/LinAlg/Eigen/EigenMapTools.h"

namespace MPL = MaterialPropertyLib;

/**
 * Common convenitions for naming:
 * x_D              - deviatoric part of tensor x
 * x_V              - volumetric part of tensor x
 * x_p              - a variable related to plastic potential
 * x_prev           - value of x in previous time step
 *
 * Variables used in the code:
 * eps_D            - deviatoric strain
 * eps_p_D_dot      - deviatoric increment of plastic strain
 * eps_p_eff_dot    - increment of effective plastic strain
 * eps_p_V_dot      - volumetric increment of plastic strain
 * sigma_D_inverse_D - deviatoric part of sigma_D_inverse
 *
 * derivation of the flow rule
 * theta            - J3 / J2^(3 / 2) from yield function
 * dtheta_dsigma    - derivative of theta
 * sqrtPhi          - square root of Phi from plastic potential
 * flow_D           - deviatoric part of flow
 * flow_V           - volumetric part of flow
 * lambda_flow_D    - deviatoric increment of plastic strain
 */

namespace MaterialLib
{
namespace Solids
{
namespace Ehlers
{
template <int DisplacementDim>
double StateVariables<DisplacementDim>::getEquivalentPlasticStrain() const
{
    return std::sqrt(2.0 / 3.0 * Invariants::FrobeniusNorm(eps_p.D.eval()));
}

/// Special product of \c v with itself: \f$v \odot v\f$.
/// The tensor \c v is given in Kelvin mapping.
/// \note Implementation only for 2 and 3 dimensions.
/// \attention Pay attention to the sign of the result, which normally would be
/// negative, but the returned value is not negated. This has to do with \f$
/// d(A^{-1})/dA = -A^{-1} \odot A^{-1} \f$.
template <int DisplacementDim>
MathLib::KelvinVector::KelvinMatrixType<DisplacementDim> sOdotS(
    MathLib::KelvinVector::KelvinVectorType<DisplacementDim> const& v);

template <int DisplacementDim>
struct PhysicalStressWithInvariants final
{
    static int const KelvinVectorSize =
        MathLib::KelvinVector::kelvin_vector_dimensions(DisplacementDim);
    using Invariants = MathLib::KelvinVector::Invariants<KelvinVectorSize>;
    using KelvinVector =
        MathLib::KelvinVector::KelvinVectorType<DisplacementDim>;

    explicit PhysicalStressWithInvariants(KelvinVector const& stress)
        : value{stress},
          D{Invariants::deviatoric_projection * stress},
          I_1{Invariants::trace(stress)},
          J_2{Invariants::J2(D)},
          J_3{Invariants::J3(D)}
    {
    }

    KelvinVector value;
    KelvinVector D;
    double I_1;
    double J_2;
    double J_3;

    EIGEN_MAKE_ALIGNED_OPERATOR_NEW;
};

/// Holds powers of 1 + gamma_p*theta to base 0, m_p, and m_p-1.
struct OnePlusGamma_pTheta final
{
    OnePlusGamma_pTheta(double const gamma_p, double const theta,
                        double const m_p)
        : value{1 + gamma_p * theta},
          pow_m_p{std::pow(value, m_p)},
          pow_m_p1{pow_m_p / value}
    {
    }

    double const value;
    double const pow_m_p;
    double const pow_m_p1;
};

template <int DisplacementDim>
double plasticFlowVolumetricPart(
    PhysicalStressWithInvariants<DisplacementDim> const& s,
    double const sqrtPhi, double const alpha_p, double const beta_p,
    double const delta_p, double const epsilon_p)
{
    return 3 *
               (alpha_p * s.I_1 +
                4 * boost::math::pow<2>(delta_p) * boost::math::pow<3>(s.I_1)) /
               (2 * sqrtPhi) +
           3 * beta_p + 6 * epsilon_p * s.I_1;
}

template <int DisplacementDim>
typename SolidEhlers<DisplacementDim>::KelvinVector plasticFlowDeviatoricPart(
    PhysicalStressWithInvariants<DisplacementDim> const& s,
    OnePlusGamma_pTheta const& one_gt, double const sqrtPhi,
    typename SolidEhlers<DisplacementDim>::KelvinVector const& dtheta_dsigma,
    double const gamma_p, double const m_p)
{
    return (one_gt.pow_m_p *
            (s.D + s.J_2 * m_p * gamma_p * dtheta_dsigma / one_gt.value)) /
           (2 * sqrtPhi);
}

template <int DisplacementDim>
double yieldFunction(MaterialProperties const& mp,
                     PhysicalStressWithInvariants<DisplacementDim> const& s,
                     double const k)
{
    double const I_1_squared = boost::math::pow<2>(s.I_1);
    assert(s.J_2 != 0);

    return std::sqrt(s.J_2 * std::pow(1 + mp.gamma * s.J_3 /
                                              (s.J_2 * std::sqrt(s.J_2)),
                                      mp.m) +
                     mp.alpha / 2. * I_1_squared +
                     boost::math::pow<2>(mp.delta) *
                         boost::math::pow<2>(I_1_squared)) +
           mp.beta * s.I_1 + mp.epsilon * I_1_squared - k;
}

template <int DisplacementDim>
typename SolidEhlers<DisplacementDim>::ResidualVectorType
calculatePlasticResidual(
    MathLib::KelvinVector::KelvinVectorType<DisplacementDim> const& eps_D,
    double const eps_V,
    PhysicalStressWithInvariants<DisplacementDim> const& s,
    MathLib::KelvinVector::KelvinVectorType<DisplacementDim> const& eps_p_D,
    MathLib::KelvinVector::KelvinVectorType<DisplacementDim> const& eps_p_D_dot,
    double const eps_p_V,
    double const eps_p_V_dot,
    double const eps_p_eff_dot,
    double const lambda,
    double const k,
    MaterialProperties const& mp)
{
    static int const KelvinVectorSize =
        MathLib::KelvinVector::kelvin_vector_dimensions(DisplacementDim);
    using Invariants = MathLib::KelvinVector::Invariants<KelvinVectorSize>;
    using KelvinVector =
        MathLib::KelvinVector::KelvinVectorType<DisplacementDim>;

    auto const& P_dev = Invariants::deviatoric_projection;
    auto const& identity2 = Invariants::identity2;

    double const theta = s.J_3 / (s.J_2 * std::sqrt(s.J_2));

    typename SolidEhlers<DisplacementDim>::ResidualVectorType residual;
    // calculate stress residual
    residual.template segment<KelvinVectorSize>(0).noalias() =
        s.value / mp.G - 2 * (eps_D - eps_p_D) -
        mp.K / mp.G * (eps_V - eps_p_V) * identity2;

    // deviatoric plastic strain
    KelvinVector const sigma_D_inverse_D =
        P_dev * MathLib::KelvinVector::inverse(s.D);
    KelvinVector const dtheta_dsigma =
        theta * sigma_D_inverse_D - 3. / 2. * theta / s.J_2 * s.D;

    OnePlusGamma_pTheta const one_gt{mp.gamma_p, theta, mp.m_p};
    double const sqrtPhi = std::sqrt(
        s.J_2 * one_gt.pow_m_p + mp.alpha_p / 2. * boost::math::pow<2>(s.I_1) +
        boost::math::pow<2>(mp.delta_p) * boost::math::pow<4>(s.I_1));
    KelvinVector const flow_D = plasticFlowDeviatoricPart(
        s, one_gt, sqrtPhi, dtheta_dsigma, mp.gamma_p, mp.m_p);
    KelvinVector const lambda_flow_D = lambda * flow_D;

    residual.template segment<KelvinVectorSize>(KelvinVectorSize).noalias() =
        eps_p_D_dot - lambda_flow_D;

    // plastic volume strain
    {
        double const flow_V = plasticFlowVolumetricPart<DisplacementDim>(
            s, sqrtPhi, mp.alpha_p, mp.beta_p, mp.delta_p, mp.epsilon_p);
        residual(2 * KelvinVectorSize, 0) = eps_p_V_dot - lambda * flow_V;
    }

    // evolution of plastic equivalent strain
    residual(2 * KelvinVectorSize + 1) =
        eps_p_eff_dot -
        std::sqrt(2. / 3. * lambda_flow_D.transpose() * lambda_flow_D);

    // yield function (for plastic multiplier)
    residual(2 * KelvinVectorSize + 2) = yieldFunction(mp, s, k) / mp.G;
    return residual;
}

template <int DisplacementDim>
typename SolidEhlers<DisplacementDim>::JacobianMatrix calculatePlasticJacobian(
    double const dt,
    PhysicalStressWithInvariants<DisplacementDim> const& s,
    double const lambda,
    MaterialProperties const& mp)
{
    static int const KelvinVectorSize =
        MathLib::KelvinVector::kelvin_vector_dimensions(DisplacementDim);
    using Invariants = MathLib::KelvinVector::Invariants<KelvinVectorSize>;
    using KelvinVector =
        MathLib::KelvinVector::KelvinVectorType<DisplacementDim>;
    using KelvinMatrix =
        MathLib::KelvinVector::KelvinMatrixType<DisplacementDim>;

    auto const& P_dev = Invariants::deviatoric_projection;
    auto const& identity2 = Invariants::identity2;

    double const theta = s.J_3 / (s.J_2 * std::sqrt(s.J_2));
    OnePlusGamma_pTheta const one_gt{mp.gamma_p, theta, mp.m_p};

    // inverse of deviatoric stress tensor
    if (Invariants::determinant(s.D) == 0)
    {
        OGS_FATAL("Determinant is zero. Matrix is non-invertable.");
    }
    // inverse of sigma_D
    KelvinVector const sigma_D_inverse = MathLib::KelvinVector::inverse(s.D);
    KelvinVector const sigma_D_inverse_D = P_dev * sigma_D_inverse;

    KelvinVector const dtheta_dsigma =
        theta * sigma_D_inverse_D - 3. / 2. * theta / s.J_2 * s.D;

    // deviatoric flow
    double const sqrtPhi = std::sqrt(
        s.J_2 * one_gt.pow_m_p + mp.alpha_p / 2. * boost::math::pow<2>(s.I_1) +
        boost::math::pow<2>(mp.delta_p) * boost::math::pow<4>(s.I_1));
    KelvinVector const flow_D = plasticFlowDeviatoricPart(
        s, one_gt, sqrtPhi, dtheta_dsigma, mp.gamma_p, mp.m_p);
    KelvinVector const lambda_flow_D = lambda * flow_D;

    typename SolidEhlers<DisplacementDim>::JacobianMatrix jacobian =
        SolidEhlers<DisplacementDim>::JacobianMatrix::Zero();

    // G_11
    jacobian.template block<KelvinVectorSize, KelvinVectorSize>(0, 0)
        .noalias() = KelvinMatrix::Identity();

    // G_12
    jacobian
        .template block<KelvinVectorSize, KelvinVectorSize>(0, KelvinVectorSize)
        .noalias() = 2 * KelvinMatrix::Identity();

    // G_13
    jacobian.template block<KelvinVectorSize, 1>(0, 2 * KelvinVectorSize)
        .noalias() = mp.K / mp.G * identity2;

    // G_14 and G_15 are zero

    // G_21 -- derivative of deviatoric flow

    double const gm_p = mp.gamma_p * mp.m_p;
    // intermediate variable for derivative of deviatoric flow
    KelvinVector const M0 = s.J_2 / one_gt.value * dtheta_dsigma;
    // derivative of Phi w.r.t. sigma
    KelvinVector const dPhi_dsigma =
        one_gt.pow_m_p * (s.D + gm_p * M0) +
        (mp.alpha_p * s.I_1 +
         4 * boost::math::pow<2>(mp.delta_p) * boost::math::pow<3>(s.I_1)) *
            identity2;

    // intermediate variable for derivative of deviatoric flow
    KelvinMatrix const M1 =
        one_gt.pow_m_p *
        (s.D * dPhi_dsigma.transpose() + gm_p * M0 * dPhi_dsigma.transpose());
    // intermediate variable for derivative of deviatoric flow
    KelvinMatrix const M2 =
        one_gt.pow_m_p * (P_dev + s.D * gm_p * M0.transpose());
    // second derivative of theta
    KelvinMatrix const d2theta_dsigma2 =
        theta * P_dev * sOdotS<DisplacementDim>(sigma_D_inverse) * P_dev +
        sigma_D_inverse_D * dtheta_dsigma.transpose() -
        3. / 2. * theta / s.J_2 * P_dev -
        3. / 2. * dtheta_dsigma / s.J_2 * s.D.transpose() +
        3. / 2. * theta / boost::math::pow<2>(s.J_2) * s.D * s.D.transpose();

    // intermediate variable for derivative of deviatoric flow
    KelvinMatrix const M3 =
        gm_p * one_gt.pow_m_p1 *
        ((s.D + (gm_p - mp.gamma_p) * M0) * dtheta_dsigma.transpose() +
         s.J_2 * d2theta_dsigma2);

    // derivative of flow_D w.r.t. sigma
    KelvinMatrix const dflow_D_dsigma =
        (-M1 / (4 * boost::math::pow<3>(sqrtPhi)) + (M2 + M3) / (2 * sqrtPhi)) *
        mp.G;
    jacobian
        .template block<KelvinVectorSize, KelvinVectorSize>(KelvinVectorSize, 0)
        .noalias() = -lambda * dflow_D_dsigma;

    // G_22
    jacobian
        .template block<KelvinVectorSize, KelvinVectorSize>(KelvinVectorSize,
                                                            KelvinVectorSize)
        .noalias() = KelvinMatrix::Identity() / dt;

    // G_23 and G_24 are zero

    // G_25
    jacobian
        .template block<KelvinVectorSize, 1>(KelvinVectorSize,
                                             2 * KelvinVectorSize + 2)
        .noalias() = -flow_D;

    // G_31
    {
        // derivative of flow_V w.r.t. sigma
        KelvinVector const dflow_V_dsigma =
            3 * mp.G *
            (-(mp.alpha_p * s.I_1 + 4 * boost::math::pow<2>(mp.delta_p) *
                                        boost::math::pow<3>(s.I_1)) /
                 (4 * boost::math::pow<3>(sqrtPhi)) * dPhi_dsigma +
             (mp.alpha_p * identity2 +
              12 * boost::math::pow<2>(mp.delta_p * s.I_1) * identity2) /
                 (2 * sqrtPhi) +
             2 * mp.epsilon_p * identity2);

        jacobian.template block<1, KelvinVectorSize>(2 * KelvinVectorSize, 0)
            .noalias() = -lambda * dflow_V_dsigma.transpose();
    }

    // G_32 is zero

    // G_33
    jacobian(2 * KelvinVectorSize, 2 * KelvinVectorSize) = 1. / dt;

    // G_34 is zero

    // G_35
    {
        double const flow_V = plasticFlowVolumetricPart<DisplacementDim>(
            s, sqrtPhi, mp.alpha_p, mp.beta_p, mp.delta_p, mp.epsilon_p);
        jacobian(2 * KelvinVectorSize, 2 * KelvinVectorSize + 2) = -flow_V;
    }

    // increment of effectiv plastic strain
    double const eff_flow =
        std::sqrt(2. / 3. * lambda_flow_D.transpose() * lambda_flow_D);

    if (eff_flow > 0)
    {
        // intermediate variable for derivative of plastic jacobian
        KelvinVector const eff_flow23_lambda_flow_D =
            -2 / 3. / eff_flow * lambda_flow_D;
        // G_41
        jacobian
            .template block<1, KelvinVectorSize>(2 * KelvinVectorSize + 1, 0)
            .noalias() = lambda * dflow_D_dsigma * eff_flow23_lambda_flow_D;
        // G_45
        jacobian(2 * KelvinVectorSize + 1, 2 * KelvinVectorSize + 2) =
            eff_flow23_lambda_flow_D.transpose() * flow_D;
    }

    // G_42 and G_43 are zero

    // G_44
    jacobian(2 * KelvinVectorSize + 1, 2 * KelvinVectorSize + 1) = 1. / dt;

    // G_51
    {
        double const one_gt_pow_m = std::pow(one_gt.value, mp.m);
        double const gm = mp.gamma * mp.m;
        // derivative of yield function w.r.t. sigma
        KelvinVector const dF_dsigma =
            mp.G *
                (one_gt_pow_m * (s.D + gm * M0) +
                 (mp.alpha * s.I_1 + 4 * boost::math::pow<2>(mp.delta) *
                                         boost::math::pow<3>(s.I_1)) *
                     identity2) /
                (2. * sqrtPhi) +
            mp.G * (mp.beta + 2 * mp.epsilon_p * s.I_1) * identity2;

        jacobian
            .template block<1, KelvinVectorSize>(2 * KelvinVectorSize + 2, 0)
            .noalias() = dF_dsigma.transpose() / mp.G;
    }

    // G_54
    jacobian(2 * KelvinVectorSize + 2, 2 * KelvinVectorSize + 1) =
        -mp.kappa * mp.hardening_coefficient / mp.G;

    // G_52, G_53, G_55 are zero
    return jacobian;
}

/// Calculates the derivative of the residuals with respect to total
/// strain. Implementation fully implicit only.
template <int DisplacementDim>
MathLib::KelvinVector::KelvinMatrixType<DisplacementDim> calculateDResidualDEps(
    double const K, double const G)
{
    static int const KelvinVectorSize =
        MathLib::KelvinVector::kelvin_vector_dimensions(DisplacementDim);
    using Invariants = MathLib::KelvinVector::Invariants<KelvinVectorSize>;

    auto const& P_dev = Invariants::deviatoric_projection;
    auto const& P_sph = Invariants::spherical_projection;
    auto const& I =
        MathLib::KelvinVector::KelvinMatrixType<DisplacementDim>::Identity();

    return -2. * I * P_dev - 3. * K / G * I * P_sph;
}

inline double calculateIsotropicHardening(double const kappa,
                                          double const hardening_coefficient,
                                          double const eps_p_eff)
{
    return kappa * (1. + eps_p_eff * hardening_coefficient);
}

template <int DisplacementDim>
typename SolidEhlers<DisplacementDim>::KelvinVector predict_sigma(
    double const G, double const K,
    typename SolidEhlers<DisplacementDim>::KelvinVector const& sigma_prev,
    typename SolidEhlers<DisplacementDim>::KelvinVector const& eps,
    typename SolidEhlers<DisplacementDim>::KelvinVector const& eps_prev,
    double const eps_V)
{
    static int const KelvinVectorSize =
        MathLib::KelvinVector::kelvin_vector_dimensions(DisplacementDim);
    using Invariants = MathLib::KelvinVector::Invariants<KelvinVectorSize>;
    auto const& P_dev = Invariants::deviatoric_projection;

    // dimensionless initial hydrostatic pressure
    double const pressure_prev = Invariants::trace(sigma_prev) / (-3. * G);
    // initial strain invariant
    double const e_prev = Invariants::trace(eps_prev);
    // dimensioness hydrostatic stress increment
    double const pressure = pressure_prev - K / G * (eps_V - e_prev);
    // dimensionless deviatoric initial stress
    typename SolidEhlers<DisplacementDim>::KelvinVector const sigma_D_prev =
        P_dev * sigma_prev / G;
    // dimensionless deviatoric stress
    typename SolidEhlers<DisplacementDim>::KelvinVector const sigma_D =
        sigma_D_prev + 2 * P_dev * (eps - eps_prev);
    return sigma_D - pressure * Invariants::identity2;
}

/// Split the agglomerated solution vector in separate items. The arrangement
/// must be the same as in the newton() function.
template <typename ResidualVector, typename KelvinVector>
std::tuple<KelvinVector, PlasticStrain<KelvinVector>, double>
splitSolutionVector(ResidualVector const& solution)
{
    static auto const size = KelvinVector::SizeAtCompileTime;
    return std::forward_as_tuple(
        solution.template segment<size>(size * 0),
        PlasticStrain<KelvinVector>{solution.template segment<size>(size * 1),
                                    solution[size * 2], solution[size * 2 + 1]},
        solution[size * 2 + 2]);
}

template <int DisplacementDim>
SolidEhlers<DisplacementDim>::SolidEhlers(
    NumLib::NewtonRaphsonSolverParameters nonlinear_solver_parameters,
    MaterialPropertiesParameters material_properties,
    std::unique_ptr<DamagePropertiesParameters>&& damage_properties,
    TangentType tangent_type)
    : _nonlinear_solver_parameters(std::move(nonlinear_solver_parameters)),
      _mp(std::move(material_properties)),
      _damage_properties(std::move(damage_properties)),
      _tangent_type(tangent_type)
{
}

template <int DisplacementDim>
double SolidEhlers<DisplacementDim>::computeFreeEnergyDensity(
    double const /*t*/,
    ParameterLib::SpatialPosition const& /*x*/,
    double const /*dt*/,
    KelvinVector const& eps,
    KelvinVector const& sigma,
    typename MechanicsBase<DisplacementDim>::MaterialStateVariables const&
        material_state_variables) const
{
    assert(dynamic_cast<StateVariables<DisplacementDim> const*>(
               &material_state_variables) != nullptr);

    auto const& eps_p = static_cast<StateVariables<DisplacementDim> const&>(
                            material_state_variables)
                            .eps_p;
    using Invariants = MathLib::KelvinVector::Invariants<KelvinVectorSize>;
    auto const& identity2 = Invariants::identity2;
    return (eps - eps_p.D - eps_p.V / 3 * identity2).dot(sigma) / 2;
}

template <int DisplacementDim>
std::optional<std::tuple<typename SolidEhlers<DisplacementDim>::KelvinVector,
                         std::unique_ptr<typename MechanicsBase<
                             DisplacementDim>::MaterialStateVariables>,
                         typename SolidEhlers<DisplacementDim>::KelvinMatrix>>
SolidEhlers<DisplacementDim>::integrateStress(
    MaterialPropertyLib::VariableArray const& variable_array_prev,
    MaterialPropertyLib::VariableArray const& variable_array, double const t,
    ParameterLib::SpatialPosition const& x, double const dt,
    typename MechanicsBase<DisplacementDim>::MaterialStateVariables const&
        material_state_variables) const
{
    auto const& eps_m = std::get<MPL::SymmetricTensor<DisplacementDim>>(
        variable_array[static_cast<int>(MPL::Variable::mechanical_strain)]);
    auto const& eps_m_prev = std::get<MPL::SymmetricTensor<DisplacementDim>>(
        variable_array_prev[static_cast<int>(
            MPL::Variable::mechanical_strain)]);
    auto const& sigma_prev = std::get<MPL::SymmetricTensor<DisplacementDim>>(
        variable_array_prev[static_cast<int>(MPL::Variable::stress)]);

    assert(dynamic_cast<StateVariables<DisplacementDim> const*>(
               &material_state_variables) != nullptr);

    StateVariables<DisplacementDim> state =
        static_cast<StateVariables<DisplacementDim> const&>(
            material_state_variables);
    state.setInitialConditions();

    using Invariants = MathLib::KelvinVector::Invariants<KelvinVectorSize>;

    // volumetric strain
    double const eps_V = Invariants::trace(eps_m);

    auto const& P_dev = Invariants::deviatoric_projection;
    // deviatoric strain
    KelvinVector const eps_D = P_dev * eps_m;

    // do the evaluation once per function call.
    MaterialProperties const mp(t, x, _mp);

    KelvinVector sigma = predict_sigma<DisplacementDim>(
        mp.G, mp.K, sigma_prev, eps_m, eps_m_prev, eps_V);

    KelvinMatrix tangentStiffness;

    PhysicalStressWithInvariants<DisplacementDim> s{mp.G * sigma};
    // Quit early if sigma is zero (nothing to do) or if we are still in elastic
    // zone.
    if ((sigma.squaredNorm() == 0 ||
         yieldFunction(
             mp, s,
             calculateIsotropicHardening(mp.kappa, mp.hardening_coefficient,
                                         state.eps_p.eff)) < 0))
    {
        tangentStiffness = elasticTangentStiffness<DisplacementDim>(
            mp.K - 2. / 3 * mp.G, mp.G);
    }
    else
    {
        // Linear solver for the newton loop is required after the loop with the
        // same matrix. This saves one decomposition.
        Eigen::FullPivLU<Eigen::Matrix<double, JacobianResidualSize,
                                       JacobianResidualSize, Eigen::RowMajor>>
            linear_solver;

        {
            static int const KelvinVectorSize =
                MathLib::KelvinVector::kelvin_vector_dimensions(
                    DisplacementDim);
            using KelvinVector =
                MathLib::KelvinVector::KelvinVectorType<DisplacementDim>;
            using ResidualVectorType =
                Eigen::Matrix<double, JacobianResidualSize, 1>;
            using JacobianMatrix =
                Eigen::Matrix<double, JacobianResidualSize,
                              JacobianResidualSize, Eigen::RowMajor>;

            JacobianMatrix jacobian;

            // Agglomerated solution vector construction.  It is later split
            // into individual parts by splitSolutionVector().
            ResidualVectorType solution;
            solution << sigma, state.eps_p.D, state.eps_p.V, state.eps_p.eff, 0;

            auto const update_residual = [&](ResidualVectorType& residual)
            {
                auto const& eps_p_D =
                    solution.template segment<KelvinVectorSize>(
                        KelvinVectorSize);
                KelvinVector const eps_p_D_dot =
                    (eps_p_D - state.eps_p_prev.D) / dt;

                double const& eps_p_V = solution[KelvinVectorSize * 2];
                double const eps_p_V_dot = (eps_p_V - state.eps_p_prev.V) / dt;

                double const& eps_p_eff = solution[KelvinVectorSize * 2 + 1];
                double const eps_p_eff_dot =
                    (eps_p_eff - state.eps_p_prev.eff) / dt;

                double const k_hardening = calculateIsotropicHardening(
                    mp.kappa, mp.hardening_coefficient,
                    solution[KelvinVectorSize * 2 + 1]);
                residual = calculatePlasticResidual<DisplacementDim>(
                    eps_D, eps_V, s,
                    solution.template segment<KelvinVectorSize>(
                        KelvinVectorSize),
                    eps_p_D_dot, solution[KelvinVectorSize * 2], eps_p_V_dot,
                    eps_p_eff_dot, solution[KelvinVectorSize * 2 + 2],
                    k_hardening, mp);
            };

            auto const update_jacobian = [&](JacobianMatrix& jacobian)
            {
                jacobian = calculatePlasticJacobian<DisplacementDim>(
                    dt, s, solution[KelvinVectorSize * 2 + 2], mp);
            };

            auto const update_solution =
                [&](ResidualVectorType const& increment)
            {
                solution += increment;
                s = PhysicalStressWithInvariants<DisplacementDim>{
                    mp.G * solution.template segment<KelvinVectorSize>(0)};
            };

            auto newton_solver = NumLib::NewtonRaphson<
                decltype(linear_solver), JacobianMatrix,
                decltype(update_jacobian), ResidualVectorType,
                decltype(update_residual), decltype(update_solution)>(
                linear_solver, update_jacobian, update_residual,
                update_solution, _nonlinear_solver_parameters);

            auto const success_iterations = newton_solver.solve(jacobian);

            if (!success_iterations)
            {
                return {};
            }

            // If the Newton loop didn't run, the linear solver will not be
            // initialized.
            // This happens usually for the first iteration of the first
            // timestep.
            if (*success_iterations == 0)
            {
                linear_solver.compute(jacobian);
            }

            std::tie(sigma, state.eps_p, std::ignore) =
                splitSolutionVector<ResidualVectorType, KelvinVector>(solution);
        }

        // Calculate residual derivative w.r.t. strain
        Eigen::Matrix<double, JacobianResidualSize, KelvinVectorSize,
                      Eigen::RowMajor>
            dresidual_deps =
                Eigen::Matrix<double, JacobianResidualSize, KelvinVectorSize,
                              Eigen::RowMajor>::Zero();
        dresidual_deps.template block<KelvinVectorSize, KelvinVectorSize>(0, 0)
            .noalias() = calculateDResidualDEps<DisplacementDim>(mp.K, mp.G);

        if (_tangent_type == TangentType::Elastic)
        {
            tangentStiffness =
                elasticTangentStiffness<DisplacementDim>(mp.K, mp.G);
        }
        else if (_tangent_type == TangentType::Plastic ||
                 _tangent_type == TangentType::PlasticDamageSecant)
        {
            tangentStiffness =
                mp.G *
                linear_solver.solve(-dresidual_deps)
                    .template block<KelvinVectorSize, KelvinVectorSize>(0, 0);
            if (_tangent_type == TangentType::PlasticDamageSecant)
            {
                tangentStiffness *= 1 - state.damage.value();
            }
        }
        else
        {
            OGS_FATAL(
                "Unimplemented tangent type behaviour for the tangent type "
                "'{:d}'.",
                _tangent_type);
        }
    }

    KelvinVector sigma_final = mp.G * sigma;

    return {std::make_tuple(
        sigma_final,
        std::unique_ptr<
            typename MechanicsBase<DisplacementDim>::MaterialStateVariables>{
            new StateVariables<DisplacementDim>{
                static_cast<StateVariables<DisplacementDim> const&>(state)}},
        tangentStiffness)};
}

template <int DisplacementDim>
std::vector<typename MechanicsBase<DisplacementDim>::InternalVariable>
SolidEhlers<DisplacementDim>::getInternalVariables() const
{
    return {{"damage.kappa_d", 1,
             [](typename MechanicsBase<
                    DisplacementDim>::MaterialStateVariables const& state,
                std::vector<double>& cache) -> std::vector<double> const&
             {
                 assert(dynamic_cast<StateVariables<DisplacementDim> const*>(
                            &state) != nullptr);
                 auto const& ehlers_state =
                     static_cast<StateVariables<DisplacementDim> const&>(state);

                 cache.resize(1);
                 cache.front() = ehlers_state.damage.kappa_d();
                 return cache;
             },
             [](typename MechanicsBase<DisplacementDim>::MaterialStateVariables&
                    state) -> BaseLib::DynamicSpan<double>
             {
                 assert(dynamic_cast<StateVariables<DisplacementDim> const*>(
                            &state) != nullptr);
                 auto& ehlers_state =
                     static_cast<StateVariables<DisplacementDim>&>(state);

                 return {&ehlers_state.damage.kappa_d(), 1};
             }},
            {"damage.value", 1,
             [](typename MechanicsBase<
                    DisplacementDim>::MaterialStateVariables const& state,
                std::vector<double>& cache) -> std::vector<double> const&
             {
                 assert(dynamic_cast<StateVariables<DisplacementDim> const*>(
                            &state) != nullptr);
                 auto const& ehlers_state =
                     static_cast<StateVariables<DisplacementDim> const&>(state);

                 cache.resize(1);
                 cache.front() = ehlers_state.damage.value();
                 return cache;
             },
             [](typename MechanicsBase<DisplacementDim>::MaterialStateVariables&
                    state) -> BaseLib::DynamicSpan<double>
             {
                 assert(dynamic_cast<StateVariables<DisplacementDim> const*>(
                            &state) != nullptr);
                 auto& ehlers_state =
                     static_cast<StateVariables<DisplacementDim>&>(state);

                 return {&ehlers_state.damage.value(), 1};
             }},
            {"eps_p.D", KelvinVector::RowsAtCompileTime,
             [](typename MechanicsBase<
                    DisplacementDim>::MaterialStateVariables const& state,
                std::vector<double>& cache) -> std::vector<double> const&
             {
                 assert(dynamic_cast<StateVariables<DisplacementDim> const*>(
                            &state) != nullptr);
                 auto const& ehlers_state =
                     static_cast<StateVariables<DisplacementDim> const&>(state);

                 cache.resize(KelvinVector::RowsAtCompileTime);
                 MathLib::toVector<KelvinVector>(
                     cache, KelvinVector::RowsAtCompileTime) =
                     MathLib::KelvinVector::kelvinVectorToSymmetricTensor(
                         ehlers_state.eps_p.D);

                 return cache;
             },
             [](typename MechanicsBase<DisplacementDim>::MaterialStateVariables&
                    state) -> BaseLib::DynamicSpan<double>
             {
                 assert(dynamic_cast<StateVariables<DisplacementDim> const*>(
                            &state) != nullptr);
                 auto& ehlers_state =
                     static_cast<StateVariables<DisplacementDim>&>(state);

                 return {
                     ehlers_state.eps_p.D.data(),
                     static_cast<std::size_t>(KelvinVector::RowsAtCompileTime)};
             }},
            {"eps_p.V", 1,
             [](typename MechanicsBase<
                    DisplacementDim>::MaterialStateVariables const& state,
                std::vector<double>& cache) -> std::vector<double> const&
             {
                 assert(dynamic_cast<StateVariables<DisplacementDim> const*>(
                            &state) != nullptr);
                 auto const& ehlers_state =
                     static_cast<StateVariables<DisplacementDim> const&>(state);

                 cache.resize(1);
                 cache.front() = ehlers_state.eps_p.V;
                 return cache;
             },
             [](typename MechanicsBase<DisplacementDim>::MaterialStateVariables&
                    state) -> BaseLib::DynamicSpan<double>
             {
                 assert(dynamic_cast<StateVariables<DisplacementDim> const*>(
                            &state) != nullptr);
                 auto& ehlers_state =
                     static_cast<StateVariables<DisplacementDim>&>(state);

                 return {&ehlers_state.eps_p.V, 1};
             }},
            {"eps_p.eff", 1,
             [](typename MechanicsBase<
                    DisplacementDim>::MaterialStateVariables const& state,
                std::vector<double>& cache) -> std::vector<double> const&
             {
                 assert(dynamic_cast<StateVariables<DisplacementDim> const*>(
                            &state) != nullptr);
                 auto const& ehlers_state =
                     static_cast<StateVariables<DisplacementDim> const&>(state);

                 cache.resize(1);
                 cache.front() = ehlers_state.eps_p.eff;
                 return cache;
             },
             [](typename MechanicsBase<DisplacementDim>::MaterialStateVariables&
                    state) -> BaseLib::DynamicSpan<double>
             {
                 assert(dynamic_cast<StateVariables<DisplacementDim> const*>(
                            &state) != nullptr);
                 auto& ehlers_state =
                     static_cast<StateVariables<DisplacementDim>&>(state);

                 return {&ehlers_state.eps_p.eff, 1};
             }}};
}

template class SolidEhlers<2>;
template class SolidEhlers<3>;

template <>
MathLib::KelvinVector::KelvinMatrixType<3> sOdotS<3>(
    MathLib::KelvinVector::KelvinVectorType<3> const& v)
{
    MathLib::KelvinVector::KelvinMatrixType<3> result;

    result(0, 0) = v(0) * v(0);
    result(0, 1) = result(1, 0) = v(3) * v(3) / 2.;
    result(0, 2) = result(2, 0) = v(5) * v(5) / 2.;
    result(0, 3) = result(3, 0) = v(0) * v(3);
    result(0, 4) = result(4, 0) = v(3) * v(5) / std::sqrt(2.);
    result(0, 5) = result(5, 0) = v(0) * v(5);

    result(1, 1) = v(1) * v(1);
    result(1, 2) = result(2, 1) = v(4) * v(4) / 2.;
    result(1, 3) = result(3, 1) = v(3) * v(1);
    result(1, 4) = result(4, 1) = v(1) * v(4);
    result(1, 5) = result(5, 1) = v(3) * v(4) / std::sqrt(2.);

    result(2, 2) = v(2) * v(2);
    result(2, 3) = result(3, 2) = v(5) * v(4) / std::sqrt(2.);
    result(2, 4) = result(4, 2) = v(4) * v(2);
    result(2, 5) = result(5, 2) = v(5) * v(2);

    result(3, 3) = v(0) * v(1) + v(3) * v(3) / 2.;
    result(3, 4) = result(4, 3) =
        v(3) * v(4) / 2. + v(5) * v(1) / std::sqrt(2.);
    result(3, 5) = result(5, 3) =
        v(0) * v(4) / std::sqrt(2.) + v(3) * v(5) / 2.;

    result(4, 4) = v(1) * v(2) + v(4) * v(4) / 2.;
    result(4, 5) = result(5, 4) =
        v(3) * v(2) / std::sqrt(2.) + v(5) * v(4) / 2.;

    result(5, 5) = v(0) * v(2) + v(5) * v(5) / 2.;
    return result;
}

template <>
MathLib::KelvinVector::KelvinMatrixType<2> sOdotS<2>(
    MathLib::KelvinVector::KelvinVectorType<2> const& v)
{
    MathLib::KelvinVector::KelvinMatrixType<2> result;

    result(0, 0) = v(0) * v(0);
    result(0, 1) = result(1, 0) = v(3) * v(3) / 2.;
    result(0, 2) = result(2, 0) = 0;
    result(0, 3) = result(3, 0) = v(0) * v(3);

    result(1, 1) = v(1) * v(1);
    result(1, 2) = result(2, 1) = 0;
    result(1, 3) = result(3, 1) = v(3) * v(1);

    result(2, 2) = v(2) * v(2);
    result(2, 3) = result(3, 2) = 0;

    result(3, 3) = v(0) * v(1) + v(3) * v(3) / 2.;

    return result;
}

}  // namespace Ehlers
}  // namespace Solids
}  // namespace MaterialLib


//  (C) Copyright John Maddock 2006.
//  Use, modification and distribution are subject to 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)

#define BOOST_MATH_MAX_SERIES_ITERATION_POLICY 10000000

#define BOOST_MATH_USE_MPFR
#include "mp_t.hpp"
#include <boost/math/special_functions/hypergeometric_1f1.hpp>
#include <boost/math/constants/constants.hpp>
#include <boost/lexical_cast.hpp>
#include <fstream>
#include <map>
#include <boost/math/tools/test_data.hpp>
#include <boost/random.hpp>

#include <boost/multiprecision/mpfi.hpp>

using namespace boost::math::tools;
using namespace boost::math;
using namespace std;
using namespace boost::multiprecision;

typedef mpfi_float_1000 mpfi_type;

mp_t hypergeometric_1f1_generic_series(mp_t a_, mp_t b_, mp_t z_)
{
   using namespace boost::math::tools;
   using namespace boost::math;
   using namespace std;
   using namespace boost::multiprecision;

   mpfi_type a(a_), b(b_), z(z_), sum(0), term(1), diff, term0(0);
   unsigned n = 0;
   bool cont = true;

   unsigned max_n;
   if (b < 0)
      max_n = itrunc(-b) + 10000;
   else
      max_n = 10000000;

   mpfi_type overflow_limit("1.189731495357231765e+4900");  // a bit less than LDBL_MAX for extended long doubles.

   do
   {
      sum += term;
      term *= (((a + n) / ((b + n) * (n + 1))) * z);
      ++n;
      diff = fabs(term / sum);
      if (n > max_n)
      {
         std::cout << "Aborting series evaluation due to too many iterations...\n";
         throw evaluation_error("");
      }
      if (fabs(upper(sum)) > overflow_limit)
      {
         std::cout << "Aborting series evaluation due to over large sum...\n";
         throw evaluation_error("");
      }
      cont = (fabs(upper(diff)) > 1e-40) || (b + n < 0) || (fabs(term0) < fabs(term));
      term0 = term;
      //std::cout << upper(term) << " " << upper(sum) << " " << upper(diff) << " " << cont << std::endl;
   } while (cont);

   mp_t r = mp_t(width(sum) / median(sum));
   if (fabs(r) > 1e-40)
   {
      std::cout << "Aborting to to error in result of " << r << std::endl;
      throw evaluation_error("");
   }
   std::cout << "Found error in sum was " << r << std::endl;

   return mp_t(median(sum));
}

struct hypergeometric_1f1_gen
{
   mp_t operator()(mp_t a1, mp_t a2, mp_t z)
   {
      mp_t result;
      try {
         result = hypergeometric_1f1_generic_series(a1, a2, z);
         std::cout << a1 << " " << a2 << " " << z << " " << result << std::endl;
      }
      catch (...)
      {
         throw std::domain_error("");
      }
      if (fabs(result) > (std::numeric_limits<double>::max)())
      {
         std::cout << "Rejecting over large value\n";
         throw std::domain_error("");
      }
      return result;
   }
};

int main(int, char* [])
{
   parameter_info<mp_t> arg1, arg2, arg3;
   test_data<mp_t> data;

   std::cout << "Welcome.\n"
      "This program will generate spot tests for 1F1 (Yeh!!):\n";

   std::string line;
   //bool cont;

   std::vector<mp_t> v;
   random_ns::mt19937 rnd;
   random_ns::uniform_real_distribution<float> ur_a(0, 1);

   mp_t p = ur_a(rnd);
   p *= 1e6;
   v.push_back(p);
   v.push_back(-p);
   p = ur_a(rnd);
   p *= 1e5;
   v.push_back(p);
   v.push_back(-p);
   p = ur_a(rnd);
   p *= 1e4;
   v.push_back(p);
   v.push_back(-p);
   p = ur_a(rnd);
   p *= 1e3;
   v.push_back(p);
   v.push_back(-p);
   p = ur_a(rnd);
   p *= 1e2;
   v.push_back(p);
   v.push_back(-p);
   p = ur_a(rnd);
   p *= 1e-5;
   v.push_back(p);
   v.push_back(-p);
   p = ur_a(rnd);
   p *= 1e-12;
   v.push_back(p);
   v.push_back(-p);
   p = ur_a(rnd);
   p *= 1e-30;
   v.push_back(p);
   v.push_back(-p);

   for (unsigned i = 0; i < v.size(); ++i)
   {
      for (unsigned j = 0; j < v.size(); ++j)
      {
         for (unsigned k = 0; k < v.size(); ++k)
         {
            std::cout << i << " " << j << " " << k << std::endl;
            std::cout << v[i] << " " << (v[j] * 3) / 2 << " " << (v[j] * 5) / 4 << std::endl;
            arg1 = make_single_param(v[i]);
            arg2 = make_single_param(mp_t((v[j] * 3) / 2));
            arg3 = make_single_param(mp_t((v[k] * 5) / 4));
            data.insert(hypergeometric_1f1_gen(), arg1, arg2, arg3);
         }
      }
   }

   std::cout << "Enter name of test data file [default=hypergeometric_1f1.ipp]";
   std::getline(std::cin, line);
   boost::algorithm::trim(line);
   if(line == "")
      line = "hypergeometric_1f1.ipp";
   std::ofstream ofs(line.c_str());
   ofs << std::scientific << std::setprecision(40);
   write_code(ofs, data, line.c_str());

   return 0;
}


#ifndef TRIUMF_NMR_SPECTRAL_DENSITY_HPP
#define TRIUMF_NMR_SPECTRAL_DENSITY_HPP

// Author: Ryan M. L. McFadden
// NMR spectral density functions for spin-lattice relaxation in solids.
//
// For additional details see:
//
// N. Bloembergen, E. M. Purcell, and R. V. Pound, "Relaxation Effects in
// Nuclear Magnetic Resonance Absorption", Phys. Rev. 73, 679-712 (1948).
// https://dx.doi.org/10.1103/PhysRev.73.679
//
// P. M. Richards, "Effect of low dimensionality on prefactor anomalies in
// superionic conductors", Solid State Commun. 25, 1019-1021 (1978).
// https://dx.doi.org/10.1016/0038-1098(78)90896-7
//
// C. A Sholl, "Nuclear spin relaxation by translational diffusion in liquids
// and solids: high- and low-frequency limits", J. Phys. C.: Solid State Phys.
// 14, 447-464 (1981). https://dx.doi.org/10.1088/0022-3719/14/4/018
//
// P. A. Beckmann, "Spectral densities and nuclear spin relaxation in solids",
// Phys. Rep. 171, 85-128 (1988).
// https://dx.doi.org/10.1016/0370-1573(88)90073-7
//
// W. Küchler, P. Heitjans, A. Payer, and R. Schöllhorn, "7Li NMR relaxation by
// diffusion in hexagonal and cubic LixTiS2". Solid State Ionics 70-71, Part 1,
// 434-438 (1994). https://dx.doi.org/10.1016/0167-2738(94)90350-6
//

#include <cmath>
#include <iostream>

#include <boost/math/constants/constants.hpp>

#include <triumf/constants/codata_2018.hpp>

// TRIUMF: Canada's particle accelerator centre
namespace triumf {

// Nuclear Magnetic Resonance (NMR)
namespace nmr {

// Arrhenius correlation time
template <typename T = double>
T tau_c(T temperature, T prefactor, T activation_energy) {
  constexpr T boltzmann_constant =
      triumf::constants::codata_2018::Boltzmann_constant_in_eV_K<T>::value();
  if (temperature <= 0.0 or prefactor <= 0.0 or activation_energy < 0.0) {
    return 0.0;
  } else {
    return prefactor *
           std::exp(activation_energy / (boltzmann_constant * temperature));
  }
}

// Arrhenius correlation rate
template <typename T = double>
T nu_c(T temperature, T prefactor, T activation_energy) {
  constexpr T boltzmann_constant =
      triumf::constants::codata_2018::Boltzmann_constant_in_eV_K<T>::value();
  if (temperature <= 0.0 or prefactor <= 0.0 or activation_energy < 0.0) {
    return 0.0;
  } else {
    return prefactor *
           std::exp(-activation_energy / (boltzmann_constant * temperature));
  }
}

// Bloembergen-Purcell-Pound (i.e., Debye) - isotropic 3D fluctuations
template <typename T = double>
T j_3d(T correlation_time, T nmr_frequency, T interaction_strength,
       T stretching_exponent) {
  if (correlation_time < 0 or nmr_frequency < 0 or interaction_strength <= 0) {
    return 0.0;
  }
  if (stretching_exponent > 2 or stretching_exponent < 1) {
    std::cout << "WARNING : stretching exponent outside of bounds : [1, 2]"
              << std::endl;
    return 0.0;
  }
  return interaction_strength * 2.0 * correlation_time /
         (1.0 +
          std::pow(nmr_frequency * correlation_time, stretching_exponent));
}

// Richards - empirocal function for 2D fluctuations that gives correct
// asymptotic limits
template <typename T = double>
T j_2d(T correlation_time, T nmr_frequency, T interaction_strength,
       T stretching_exponent) {
  if (correlation_time < 0 or nmr_frequency < 0 or interaction_strength <= 0) {
    return 0.0;
  }
  if (stretching_exponent > 2 or stretching_exponent < 1) {
    std::cout << "WARNING : stretching exponent outside of bounds : [1, 2]"
              << std::endl;
    return 0.0;
  }
  return interaction_strength * correlation_time *
         std::log1p(
             std::pow(nmr_frequency * correlation_time, -stretching_exponent));
}

// Cole-Cole - correlated motion (j_cc -> j_bpp as stretching_exponent -> 1)
template <typename T = double>
T j_cc(T correlation_time, T nmr_frequency, T interaction_strength,
       T stretching_exponent) {
  if (correlation_time < 0 or nmr_frequency < 0 or interaction_strength <= 0) {
    return 0.0;
  }
  if (stretching_exponent > 1 or stretching_exponent <= 0) {
    std::cout << "WARNING : stretching exponent outside of bounds : (0, 1]"
              << std::endl;
    return 0.0;
  }
  return interaction_strength * (2.0 / nmr_frequency) *
         std::sin(stretching_exponent * boost::math::constants::pi<T>() / 2.0) *
         (std::pow(nmr_frequency * correlation_time, stretching_exponent) /
          (1.0 +
           std::pow(nmr_frequency * correlation_time,
                    2.0 * stretching_exponent) +
           2.0 *
               std::cos(stretching_exponent * boost::math::constants::pi<T>() /
                        2.0) *
               std::pow(nmr_frequency * correlation_time,
                        stretching_exponent)));
}

// Davidson-Cole - distribution of barriers (j_dc -> j_bpp as
// stretching_exponent -> 1)
template <typename T = double>
T j_dc(T correlation_time, T nmr_frequency, T interaction_strength,
       T stretching_exponent) {
  if (correlation_time < 0 or nmr_frequency < 0 or interaction_strength <= 0) {
    return 0.0;
  }
  if (stretching_exponent > 1 or stretching_exponent <= 0) {
    std::cout << "WARNING : stretching exponent outside of bounds : (0, 1]"
              << std::endl;
    return 0.0;
  }
  return interaction_strength * (2.0 / nmr_frequency) *
         std::sin(stretching_exponent *
                  std::atan(nmr_frequency * correlation_time)) /
         std::pow(1.0 + nmr_frequency * nmr_frequency * correlation_time *
                            correlation_time,
                  stretching_exponent / 2.0);
}

// Fang - mirror image of Davidson-Cole
template <typename T = double>
T j_fang(T correlation_time, T nmr_frequency, T interaction_strength,
         T stretching_exponent) {
  if (correlation_time < 0 or nmr_frequency < 0 or interaction_strength <= 0) {
    return 0.0;
  }
  if (stretching_exponent > 1 or stretching_exponent <= 0) {
    std::cout << "WARNING : stretching exponent outside of bounds : (0, 1]"
              << std::endl;
    return 0.0;
  }
  return interaction_strength * (2.0 / nmr_frequency) *
         std::pow(nmr_frequency * correlation_time, stretching_exponent) *
         std::sin(stretching_exponent *
                  std::atan(1.0 / (nmr_frequency * correlation_time))) /
         std::pow(1.0 + std::pow(nmr_frequency * correlation_time, 2.0),
                  stretching_exponent / 2.0);
}

// Fuoss-Kirkwood - distribution of correlation times
template <typename T = double>
T j_fk(T correlation_time, T nmr_frequency, T interaction_strength,
       T stretching_exponent) {
  if (correlation_time < 0 or nmr_frequency < 0 or interaction_strength <= 0) {
    return 0.0;
  }
  if (stretching_exponent > 1 or stretching_exponent <= 0) {
    std::cout << "WARNING : stretching exponent outside of bounds : (0, 1]"
              << std::endl;
    return 0.0;
  }
  return interaction_strength * (2.0 * stretching_exponent / nmr_frequency) *
         std::pow(nmr_frequency * correlation_time, stretching_exponent) /
         (1.0 + std::pow(nmr_frequency * correlation_time,
                         2.0 * stretching_exponent));
}

// Havriliak-Negami - correlated motion w/ distribution of barriers
// - delta ~ measure of correlations
// - delta*epsilon ~ measure of a spread in barriers
template <typename T = double>
T j_hn(T correlation_time, T nmr_frequency, T interaction_strength, T delta,
       T epsilon) {
  if (correlation_time < 0 or nmr_frequency < 0 or interaction_strength <= 0) {
    return 0.0;
  }
  if (delta > 1 or delta <= 0 or epsilon > 1.0 / delta or epsilon <= 0) {
    std::cout << "WARNING : stretching exponents outside of bounds : delta -> "
                 "(0, 1]\n";
    std::cout << "                                                   epsilon "
                 "-> (0, 1/delta]"
              << std::endl;
    return 0.0;
  }
  return interaction_strength * (2.0 / nmr_frequency) *
         (std::sin(
             epsilon *
             std::atan(
                 (std::pow(nmr_frequency * correlation_time, delta) *
                  std::sin(delta * boost::math::constants::pi<T>() / 2.0)) /
                 (1.0 + std::pow(nmr_frequency * correlation_time, delta) *
                            std::cos(delta * boost::math::constants::pi<T>() /
                                     2.0))))) *
         std::pow(
             1.0 +
                 2.0 * std::pow(nmr_frequency * correlation_time, delta) *
                     std::cos(delta * boost::math::constants::pi<T>() / 2.0) +
                 std::pow(nmr_frequency * correlation_time, 2.0 * delta),
             -epsilon / 2.0);
}

// Bryn-Mawr
// double j_bm(double correlation_time, double nmr_frequency, double
// interaction_strength = 1, double epsilon = 1, double eta = 1);

// Wagner (i.e., log-Gaussian)
// double j_lg(double correlation_time, double nmr_frequency, double
// interaction_strength = 1, double width = 1);

// log-Lorentzian
// double j_ll(double correlation_time, double nmr_frequency, double
// interaction_strength = 1, double width = 1);

// Frolich (i.e., energy box)
// double j_eb(double correlation_time, double nmr_frequency, double
// interaction_strength = 1, ...);

// Power law : power = 1 (e.g., Korringa); power = 2 (e.g., phonon); power = 3
// (e.g., [Dirac] orbital)
template <typename T = double>
T j_pow(T temperature, T intercept, T constant, T power) {
  if (temperature <= 0.0) {
    return 0.0;
  }
  if (constant == 0.0) {
    return intercept;
  }
  return intercept + constant * std::pow(temperature, power);
}

} // namespace nmr

} // namespace triumf

#endif // TRIUMF_NMR_SPECTRAL_DENSITY_HPP


/**
 * @file EigenMathUtil.hpp
 * @author Adam Li (adamli@umich.edu)
 * @date 2021-11-12
 *
 * @brief Includes a variety of utility functions for Eigen objects of the same type.
 *
 */

#include <Eigen/Core>

namespace sel_map::core::EigenMathUtil {

    template <typename Derived1, typename Derived2>
    inline double crossProduct2D(const Eigen::EigenBase<Derived1>& x, const Eigen::EigenBase<Derived2>& y){
        return x.derived()(0)*y.derived()(1) - x.derived()(1)*y.derived()(0);
    }

    // Convert boolean masks to a sequence usable for slicing vectors
    template <typename Derived>
    inline std::vector<unsigned int>& seqLogical(const Eigen::ArrayBase<Derived>& mask, std::vector<unsigned int>& sequence){
        if (mask.cols() != 1) throw std::invalid_argument("Mask should be single dim column array!");

        // Create the result
        // std::vector<unsigned int> sequence;
        sequence.clear();
        sequence.resize(mask.count());

        unsigned int accumulator = 0;
        auto itr = sequence.begin();

        for (auto val : mask){
            if(val) *(itr++) = (accumulator);
            ++accumulator;
        }

        // Make a sequence
        // Eigen::Array<unsigned int, Eigen::Dynamic, 1>::Map(&sequence[0], sequence.size())
        //      = mask.template cast<bool>().template cast<unsigned int>() * Eigen::Array<unsigned int, Eigen::Dynamic, 1>::LinSpaced(mask.rows(), 1, mask.rows());

        // // remove the 0's
        // auto seq_end = std::remove(sequence.begin(), sequence.end(), 0);

        // // Isolate the actual values
        // sequence.resize(seq_end - sequence.begin());

        // // Subtract 1 to make it valid
        // Eigen::Array<unsigned int, Eigen::Dynamic, 1>::Map(&sequence[0], sequence.size()) -= 1;

        return sequence;
    }
}


#include "RayTracingOpenMP.h"

#include <boost/math/constants/constants.hpp>

#include "Camera.h"
#include "KdTree.h"

namespace math = boost::math::constants;

const Color BACKGROUND_COLOR(0, 0, 0);

RayTracingOpenMP::RayTracingOpenMP() {
    lights = new Light[20];
    Ia = Color(0.2, 0.2, 0.2);
}

RayTracingOpenMP::~RayTracingOpenMP() {
    delete[] data;
}

Image RayTracingOpenMP::render() {
    Light light(Point(0, 0, -1), Color(1, 1, 1), Color(1, 1, 1));
    lights[0] = light;
    numberOfLights = 1;

    Resolution resolution = Resolution(width, height);
    Camera camera(Point(0, 0, -1), Point(0, math::pi<float>(), 0),
                  math::pi<float>() / 2, resolution, 1);
#pragma omp parallel for
    for (int y = 0; y < height; ++y) {
        for (int x = 0; x < width; ++x) {
            Vector vector = camera.getPrimaryVector(x, y);
            Color color = trace(vector, 0);
            camera.update(x, y, color);
        }
    }

    data = new Color[width * height];
#pragma omp parallel for
    for (int y = 0; y < resolution.height; ++y) {
        for (int x = 0; x < resolution.width; ++x) {
            data[width * y + x] = camera.getPixelColor(x, y);
        }
    }
    return Image(resolution.width, resolution.height, data);
}

Color RayTracingOpenMP::trace(Vector vector, int depth, int ignoredTriangle, float weight) {
    if (depth > MAX_DEPTH || weight < MINIMUM_WEIGHT) {
        return BACKGROUND_COLOR;
    }

    int triangleIndex = kdTree->getNearestTriangle(vector, ignoredTriangle);
    if (triangleIndex == -1) {
        return BACKGROUND_COLOR;
    }

    Triangle &triangle = scene->getTriangles()[triangleIndex];
    Vector reflectionVector = triangle.getReflectedVector(vector);
    reflectionVector.normalize();

    Point reflectionPoint = reflectionVector.startPoint;

    Vector normal = scene->getTriangles()[triangleIndex].getNormal();

    Material material = scene->getMaterial((scene->getTriangles()[triangleIndex]).materialCode);

    Vector toViewer = vector.mul(-1);
    Color refractionColor(0, 0, 0);
    Color reflectionColor = Ia * material.ambient;
    float refractivity = 0;

    if (material.dissolve < FULLY_OPAQUE_RATIO) {
        float ior = material.refractiveIndex;
        float reflectivity = fresnel(vector, normal, ior);
        refractivity = (1 - reflectivity) * (1 - material.dissolve);
        Vector refractionVector = refract(vector, triangle.getNormal(), ior);

        refractionVector.startPoint = reflectionPoint;
        refractionColor =
                trace(refractionVector, depth + 1, triangleIndex, weight * refractivity) *
                material.transparent;
    }

    if (material.dissolve > FULLY_TRANSPARENT_RATIO) {
        for (int light = 0; light < numberOfLights; ++light) {
            Vector toLight = Vector(reflectionPoint, lights[light].point);
            toLight.normalize();

            // Check if the light is blocked out
            if (normal.isObtuse(toLight)) {
                continue;
            }

            // Cast shadow ray, take refraction into account (simplified version)
            float intensity = 1;
            float dist = 0;
            float lightDistance = lights[light].point.getDist(reflectionPoint);
            int lightTriangleIndex = triangleIndex;
            for (int lightDepth = depth;
                 lightDepth < MAX_DEPTH && intensity > 0.01f; ++lightDepth) {
                lightTriangleIndex = kdTree->getNearestTriangle(toLight, lightTriangleIndex);

                if (lightTriangleIndex == -1) {
                    break;
                }
                const Intersection &intersection =
                        scene->getTriangles()[lightTriangleIndex].intersect(toLight);
                dist += intersection.distance;
                if (dist >= lightDistance) {
                    break;
                }

                toLight.startPoint = intersection.point;
                intensity *= (1 - scene->getMaterial(triangle.materialCode).dissolve);
            }
            if (intensity <= 0.01f) {
                continue;
            }

            // Calculate reflection color
            Vector fromLight(lights[light].point, reflectionPoint);
            Vector fromLightReflected = scene->getTriangles()[triangleIndex].getReflectedVector(
                    fromLight);
            fromLightReflected.normalize();

            reflectionColor +=
                    lights[light].diffuse * intensity *
                    std::max(0.f, normal.dot(toLight)) * material.diffuse;
            reflectionColor +=
                    lights[light].specular * intensity *
                    powf(std::max(0.f, toViewer.dot(fromLightReflected)),
                         material.specularExponent) * material.specular;
        }

        reflectionColor +=
                trace(reflectionVector, depth + 1, triangleIndex, weight * (1 - refractivity)) *
                powf(std::max(0.f, toViewer.dot(normal)), material.specularExponent) *
                material.specular;
    }

    if (material.dissolve >= FULLY_OPAQUE_RATIO) {
        return reflectionColor;
    } else if (material.dissolve <= FULLY_TRANSPARENT_RATIO) {
        return refractionColor;
    }

    return reflectionColor * (1 - refractivity) + refractionColor * refractivity;
}

Vector RayTracingOpenMP::refract(const Vector &vector, const Vector &normal, float ior) const {
    float dot = vector.dot(normal);
    float eta1 = 1;
    float eta2 = ior;
    Vector localNormal = normal;

    if (dot < 0) {
        // Ray entering the object
        dot *= -1;
    } else {
        // Ray going out of the object
        localNormal = normal.mul(-1);
        std::swap(eta1, eta2);
    }

    float eta = eta1 / eta2;
    float k = 1 - eta * eta * (1 - dot * dot);
    if (k < 0) {
        // Total internal reflection
        return Vector::ZERO;
    }

    Vector returnVector = vector.mul(eta).add(localNormal.mul(eta * dot - sqrtf(k)));
    returnVector.normalize();
    return returnVector;
}

float RayTracingOpenMP::fresnel(const Vector &vector, const Vector &normal, float ior) const {
    float cos1 = vector.dot(normal);
    float eta1 = 1;
    float eta2 = ior;

    if (cos1 > 0) {
        std::swap(eta1, eta2);
    }

    float sin2 = eta1 / eta2 * sqrtf(std::max(0.f, 1 - cos1 * cos1));
    if (sin2 >= 1.f) {
        // Total internal reflection
        return 1;
    }

    float cos2 = sqrtf(1 - sin2 * sin2);
    cos1 = fabsf(cos1);
    float reflectS = (eta1 * cos1 - eta2 * cos2) / (eta1 * cos1 + eta2 * cos2);
    float reflectP = (eta1 * cos2 - eta2 * cos1) / (eta1 * cos2 + eta2 * cos1);
    return (reflectS * reflectS + reflectP * reflectP) / 2;
}

void RayTracingOpenMP::setScene(std::unique_ptr<Scene> scene) {
    Backend::setScene(std::move(scene));
    kdTree = std::make_unique<KdTree>(this->scene.get());
}


#include "rv/Math.h"
#include <algorithm>
#include <boost/lexical_cast.hpp>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <iostream>
#include <list>
#include <sstream>
#include "rv/IOError.h"
#include "rv/Random.h"
#include "rv/string_utils.h"

namespace rv {

namespace Math {

struct SortElement {
  SortElement(unsigned int ind, double val) : index(ind), value(val) {}

  bool operator<(const SortElement& other) { return (value < other.value); }

  uint32_t index;
  double value;
};

Eigen::MatrixXf cov(const Eigen::MatrixXf& X, const int32_t N) {
  int n = std::min(1, std::max(N, 0));
  if (X.rows() == 1) n = 1;

  // calculate the mean

  Eigen::VectorXf mean = X.colwise().sum();
  mean = mean / X.rows();

  Eigen::MatrixXf X_mean = Eigen::MatrixXf::Zero(X.rows(), X.cols());
  for (uint32_t i = 0; i < X.rows(); ++i)
    for (uint32_t j = 0; j < X.cols(); ++j) X_mean(i, j) = X(i, j) - mean[j];

  Eigen::MatrixXf covariance = 1.0 / (double)(X.rows() - 1 + n) * X_mean.transpose() * X_mean;

  return covariance;
}

// double min(const legacy_fkie::Vector& V)
//{
//  return V.min();
//}
//

Eigen::MatrixXf min(const Eigen::MatrixXf& M) { return M.colwise().minCoeff(); }

//
// double max(const legacy_fkie::Vector& V)
//{
//  return V.max();
//}
//
Eigen::MatrixXf max(const Eigen::MatrixXf& M) { return M.colwise().maxCoeff(); }

void sort(const Eigen::MatrixXf& M, Eigen::MatrixXf& result, Eigen::MatrixXf& indices) {
  std::list<SortElement> temp;

  for (uint32_t c = 0; c < M.cols(); ++c) {
    for (uint32_t r = 0; r < M.rows(); ++r) temp.push_back(SortElement(r, M(r, c)));

    temp.sort();

    uint32_t r = 0;
    while (!temp.empty()) {
      indices(r, c) = temp.front().index;
      result(r, c) = temp.front().value;
      temp.pop_front();  // clear the list
      ++r;
    }
  }
}
//
// legacy_fkie::Vector prod(const legacy_fkie::Matrix& M, int dim)
//{
//  legacy_fkie::Vector result;
//  if (dim == 0)
//  {
//    result = legacy_fkie::Vector(M.cols());
//    for (unsigned int c = 0; c < M.cols(); ++c)
//    {
//      double product = 1.0;
//      for (unsigned int r = 0; r < M.rows(); ++r)
//        product *= M(r, c);
//      result[c] = product;
//    }
//  }
//  else if (dim == 1)
//  {
//    result = legacy_fkie::Vector(M.rows());
//
//    for (unsigned int r = 0; r < M.rows(); ++r)
//    {
//      double product = 1.0;
//      for (unsigned int c = 0; c < M.cols(); ++c)
//        product *= M(r, c);
//      result[r] = product;
//    }
//  }
//
//  return result;
//}
//
// legacy_fkie::Matrix importdata(const std::string& filename,
//    const std::string& delimiter, const unsigned int headerline)
//{
//  const unsigned int MAX_LENGTH = 64000;
//  char line[MAX_LENGTH];
//  memset(line, 0, MAX_LENGTH * sizeof(char));
//  char* token = 0;
//
//  std::ifstream file(filename.c_str());
//  if (!file.good()) throw legacy_fkie::IOError(
//      "importdata: failed opening " + filename);
//
//  // determine the rows and column count
//  unsigned int lineno = 0;
//  unsigned int rows = 0;
//  unsigned int cols = 0;
//  while (!file.eof())
//  {
//    file.getline(line, 64000);
//    ++lineno;
//    if (lineno <= headerline) continue;
//
//    // count columns
//    if (rows == 0)
//    {
//      token = strtok(line, delimiter.c_str());
//      while (token != 0)
//      {
//        ++cols;
//        token = strtok(0, delimiter.c_str());
//      }
//    }
//    ++rows;
//  }
//  file.close();
//
//  legacy_fkie::Matrix result(rows, cols);
//  file.open(filename.c_str());
//  unsigned int r = 0;
//  unsigned int c = 0;
//  lineno = 0;
//  while (!file.eof())
//  {
//    c = 0;
//    file.getline(line, 64000);
//    ++lineno;
//    if (lineno <= headerline) continue;
//    // split line
//    token = strtok(line, delimiter.c_str());
//    while (token != 0)
//    {
//      result(r, c) = atof(token);
//      token = strtok(0, delimiter.c_str());
//      ++c;
//    }
//    ++r;
//  }
//
//  file.close();
//
//  return result;
//}
//
// legacy_fkie::Matrix sqrt(const legacy_fkie::Matrix& M)
//{
//  legacy_fkie::Matrix Z = M;
//  std::vector<double> ev;
//
//  int info = legacy_fkie::LA::dsyev('V', 'U', Z, ev);
//  if (info < 0)
//  {
//    throw legacy_fkie::MathError(
//        "failed to determine the eigenvalues and eigenvectors of M");
//  }
//  else if (info > 0)
//  {
//    throw legacy_fkie::MathError("failed to converge!");
//  }
//
////  std::cout << ev.size() << std::endl;
//  /* calculate D^(1/2) */
//  legacy_fkie::Matrix D(ev.size(), ev.size());
//  for (unsigned int i = 0; i < ev.size(); ++i)
//  {
//    // epsilon test for values near zero.
//    if (std::abs(ev[i]) < 0.0000001)
//    {
//      D(i, i) = 0.0;
//      continue;
//    }
//    if (ev[i] < 0)
//    {
//      std::stringstream sstr;
//      sstr
//          << "Matrix M is not positive semi-definite. Eigenvalue less than zero: "
//          << ev[i];
//      throw legacy_fkie::MathError(sstr.str());
//    }
//    D(i, i) = std::sqrt(ev[i]);
//  }
//
//  return Z * D * Z.transpose();
//}

// legacy_fkie::Vector randperm(const unsigned int n)
//{
//  static legacy_fkie::Random rand;
//  legacy_fkie::Vector v(n);
//
//  for (unsigned int i = 1; i <= n; ++i)
//    v[i - 1] = i;
//
//  std::random_shuffle(v.ptr(), v.ptr() + n, rand);
//
//  return v;
//}

void save(const std::string& filename, const std::string& varname, const Eigen::MatrixXf& mat) {
  std::ofstream out(filename.c_str());
  assert(out.is_open());

  /** outputting the header information. **/
  out << "# name: " << varname << std::endl;
  out << "# type: matrix" << std::endl;
  out << "# rows: " << mat.rows() << std::endl;
  out << "# columns: " << mat.cols() << std::endl;

  for (uint32_t r = 0; r < mat.rows(); ++r) {
    for (uint32_t c = 0; c < mat.cols(); ++c) {
      out << " " << mat(r, c);
    }
    out << std::endl;
  }

  out.close();
}

bool load(const std::string& filename, Eigen::MatrixXf& M) {
  std::ifstream in(filename.c_str());
  if (!in.is_open()) {
    std::cerr << "unable to open '" << filename << "'" << std::endl;
    return false;
  }

  std::string line;
  std::vector<std::string> tokens;

  bool rows_read = false, cols_read = false;
  uint32_t rows = 0, cols = 0;

  in.peek();
  while ((!rows_read || !cols_read) && !in.eof()) {
    std::getline(in, line);
    if (line[0] != '#') break;
    tokens = split(line, ":");
    // trim = remove "# " in the begining.
    std::string name = trim(tokens[0], " #");
    std::string value = trim(tokens[1]);

    if (name == "type") {
      if (value != "matrix") {
        std::cerr << "unknown type '" << tokens[1] << "'!" << std::endl;
        return false;
      }
    }

    if (name == "rows") {
      rows_read = true;
      // trim = remove leading spaces
      rows = boost::lexical_cast<uint32_t>(value);
    }

    if (name == "columns") {
      cols_read = true;
      // trim = remove leading spaces
      cols = boost::lexical_cast<uint32_t>(value);
    }
    in.peek();
  }

  if (!rows_read) {
    std::cerr << "missing 'rows' entry!" << std::endl;
    return false;
  }
  if (!cols_read) {
    std::cerr << "missing 'cols' entry!" << std::endl;
    return false;
  }

  M = Eigen::MatrixXf(rows, cols);

  in.peek();
  for (uint32_t r = 0; r < rows; ++r) {
    if (in.eof()) return false;

    std::getline(in, line);
    // trim = remove leading space.
    tokens = split(trim(line));
    if (tokens.size() < cols) return false;

    for (uint32_t c = 0; c < cols; ++c) M(r, c) = boost::lexical_cast<float>(tokens[c]);

    in.peek();
  }

  in.close();

  return true;
}

void save(const std::string& filename, const std::string& varname, const Eigen::MatrixXd& mat) {
  std::ofstream out(filename.c_str());
  assert(out.is_open());

  /** outputting the header information. **/
  out << "# name: " << varname << std::endl;
  out << "# type: matrix" << std::endl;
  out << "# rows: " << mat.rows() << std::endl;
  out << "# columns: " << mat.cols() << std::endl;

  for (uint32_t r = 0; r < mat.rows(); ++r) {
    for (uint32_t c = 0; c < mat.cols(); ++c) {
      out << " " << mat(r, c);
    }
    out << std::endl;
  }

  out.close();
}

bool load(const std::string& filename, Eigen::MatrixXd& M) {
  std::ifstream in(filename.c_str());
  if (!in.is_open()) {
    std::cerr << "unable to open '" << filename << "'" << std::endl;
    return false;
  }

  std::string line;
  std::vector<std::string> tokens;

  bool rows_read = false, cols_read = false;
  uint32_t rows = 0, cols = 0;

  in.peek();
  while ((!rows_read || !cols_read) && !in.eof()) {
    std::getline(in, line);
    if (line[0] != '#') break;
    tokens = split(line, ":");
    // trim = remove "# " in the begining.
    std::string name = trim(tokens[0], " #");
    std::string value = trim(tokens[1]);

    if (name == "type") {
      if (value != "matrix") {
        std::cerr << "unknown type '" << tokens[1] << "'!" << std::endl;
        return false;
      }
    }

    if (name == "rows") {
      rows_read = true;
      // trim = remove leading spaces
      rows = boost::lexical_cast<uint32_t>(value);
    }

    if (name == "columns") {
      cols_read = true;
      // trim = remove leading spaces
      cols = boost::lexical_cast<uint32_t>(value);
    }
    in.peek();
  }

  if (!rows_read) {
    std::cerr << "missing 'rows' entry!" << std::endl;
    return false;
  }
  if (!cols_read) {
    std::cerr << "missing 'cols' entry!" << std::endl;
    return false;
  }

  M = Eigen::MatrixXd(rows, cols);

  in.peek();
  for (uint32_t r = 0; r < rows; ++r) {
    if (in.eof()) return false;

    std::getline(in, line);
    // trim = remove leading space.
    tokens = split(trim(line));
    if (tokens.size() < cols) return false;

    for (uint32_t c = 0; c < cols; ++c) M(r, c) = boost::lexical_cast<double>(tokens[c]);

    in.peek();
  }

  in.close();

  return true;
}

double normal_pdf(double x, double mu, double sigma) {
  static const double sqrt2pi = std::sqrt(2.0 * PI);

  double value = 1. / (sigma * sqrt2pi);
  value *= std::exp(-0.5 * (x - mu) * (x - mu) / (sigma * sigma));

  return value;
}

double exponential_pdf(double x, double lambda) { return lambda * exp(-lambda * x); }

double sigmoid2(double x, double loc, double scale) { return 1.0 / (1.0 + std::exp(-scale * x + scale * loc)); }

Eigen::Matrix2f diag(float val0, float val1) {
  Eigen::Matrix2f m = Eigen::Matrix2f::Zero();

  m(0, 0) = val0;
  m(1, 1) = val1;

  return m;
}

Eigen::Matrix3f diag(float val0, float val1, float val2) {
  Eigen::Matrix3f m = Eigen::Matrix3f::Zero();

  m(0, 0) = val0;
  m(1, 1) = val1;
  m(2, 2) = val2;

  return m;
}
}
}