#include <catch.hpp>

#include "constitutive/internal_variables.hpp"
#include "constitutive/mechanical/solid/affine_microsphere.hpp"
#include "constitutive/mechanical/solid/compressible_neohooke.hpp"
#include "constitutive/mechanical/solid/small_strain_J2_plasticity.hpp"
#include "constitutive/mechanical/detail/J2_plasticity.hpp"
#include "constitutive/mechanical/plane/isotropic_linear_elasticity.hpp"
#include "constitutive/thermal/isotropic_diffusion.hpp"
#include "constitutive/constitutive_model_factory.hpp"
#include "material/isotropic_elastic_plastic.hpp"
#include "exceptions.hpp"
#include "io/json.hpp"

#include <Eigen/Dense>
#include <Eigen/Eigenvalues>

std::string json_input_file()
{
    return "{\"Name\": \"rubber\", \"ElasticModulus\": 2.0, \"PoissonsRatio\": 0.45}";
}

std::string json_thermal_diffusion()
{
    return "{\"Name\": \"steel\", \"Conductivity\": 386.0, \"Density\": 7800.0, \"SpecificHeat\": "
           "390.0}";
}

constexpr auto internal_variable_size = 2;
constexpr auto ZERO_MARGIN = 1.0e-5;

using neon::json;
using namespace neon;

TEST_CASE("No constitutive model error test")
{
    using namespace neon::mechanical::solid;

    auto variables = std::make_shared<internal_variables_t>(internal_variable_size);

    REQUIRE_THROWS_AS(make_constitutive_model(variables, json::parse("{}"), json::parse("{}")),
                      std::domain_error);
}
TEST_CASE("Constitutive model no name error test")
{
    using namespace neon::mechanical::solid;

    auto variables = std::make_shared<internal_variables_t>(internal_variable_size);

    REQUIRE_THROWS_AS(make_constitutive_model(variables,
                                              json::parse("{}"),
                                              json::parse("{\"ConstitutiveModel\" : {}}")),
                      std::domain_error);
}
TEST_CASE("Constitutive model invalid name error test")
{
    using namespace neon::mechanical::solid;

    auto variables = std::make_shared<internal_variables_t>(internal_variable_size);

    REQUIRE_THROWS_AS(make_constitutive_model(variables,
                                              json::parse("{}"),
                                              json::parse("{\"ConstitutiveModel\" : "
                                                          "{\"Name\":\"PurpleMonkey\"}}")),
                      std::domain_error);
}
TEST_CASE("Neo-Hookean model")
{
    using namespace neon::mechanical::solid;

    auto variables = std::make_shared<internal_variables_t>(internal_variable_size);

    // Add the required variables for an updated Lagrangian formulation
    variables->add(variable::second::deformation_gradient, variable::second::cauchy_stress);
    variables->add(variable::scalar::DetF);

    auto neo_hooke = make_constitutive_model(variables,
                                             json::parse(json_input_file()),
                                             json::parse("{\"ConstitutiveModel\" : {\"Name\": "
                                                         "\"NeoHooke\"} }"));

    // Get the tensor variables
    auto [F_list, cauchy_stresses] = variables->get(variable::second::deformation_gradient,
                                                    variable::second::cauchy_stress);

    auto& J_list = variables->get(variable::scalar::DetF);

    // Fill with identity matrix
    for (auto& F : F_list) F = neon::matrix3::Identity();
    for (auto& J : J_list) J = 1.0;

    neo_hooke->update_internal_variables(1.0);

    Eigen::EigenSolver<Eigen::MatrixXd> eigen_solver;

    SECTION("Sanity check")
    {
        // Ensure the internal variables are allocated correctly
        REQUIRE(F_list.size() == internal_variable_size);
        REQUIRE(cauchy_stresses.size() == internal_variable_size);
        REQUIRE(J_list.size() == internal_variable_size);

        REQUIRE(neo_hooke->is_finite_deformation());
        REQUIRE(neo_hooke->intrinsic_material().name() == "rubber");
    }
    SECTION("Update of internal variables")
    {
        for (auto& cauchy_stress : cauchy_stresses)
        {
            REQUIRE(cauchy_stress.norm() == Approx(0.0).margin(ZERO_MARGIN));
        }
    }
    SECTION("Check of material tangent")
    {
        // Get the matrix variable
        auto& material_tangents = variables->get(variable::fourth::tangent_operator);

        for (auto const& material_tangent : material_tangents)
        {
            // Check a few of the numerical values
            REQUIRE(material_tangent(0, 0) == Approx(material_tangent(1, 1)));
            REQUIRE(material_tangent(0, 1) == Approx(material_tangent(0, 2)));
            REQUIRE(material_tangent(1, 1) == Approx(material_tangent(2, 2)));
            REQUIRE(material_tangent(3, 3) == Approx(material_tangent(4, 4)));
            REQUIRE(material_tangent(5, 5) == Approx(material_tangent(4, 4)));

            // Ensure symmetry is correct
            REQUIRE((material_tangent - material_tangent.transpose()).norm()
                    == Approx(0.0).margin(ZERO_MARGIN));

            eigen_solver.compute(material_tangent);

            REQUIRE((eigen_solver.eigenvalues().real().array() > 0.0).all());
        }
    }
}
TEST_CASE("Microsphere model error test")
{
    using namespace neon::mechanical::solid;

    auto variables = std::make_shared<internal_variables_t>(internal_variable_size);

    SECTION("Type not specified")
    {
        REQUIRE_THROWS_AS(make_constitutive_model(variables,
                                                  json::parse("{}"),
                                                  json::parse("{\"ConstitutiveModel\" : {\"Name\": "
                                                              "\"Microsphere\", \"Tyasdpe\" "
                                                              ": \"Affine\"}}")),
                          std::domain_error);
    }

    SECTION("Type not specified correctly")
    {
        // clang-format off
        auto const input{"{\"ConstitutiveModel\":{\"Name\":\"Microsphere\",\"Type\": \"Afwsfine\"}}"};
        // clang-format on

        REQUIRE_THROWS_AS(make_constitutive_model(variables, json::parse("{}"), json::parse(input)),
                          std::domain_error);
    }

    SECTION("Exception for quadrature scheme")
    {
        // clang-format off
        auto const input{"{\"ConstitutiveModel\":{\"Name\":\"Microsphere\",\"Type\": \"Affine\",\"Statistics\":\"Gaussian\"}}"};
        // clang-format on

        REQUIRE_THROWS_AS(make_constitutive_model(variables, json::parse("{}"), json::parse(input)),
                          std::domain_error);
    }
}
TEST_CASE("Gaussian affine microsphere model")
{
    using namespace neon::mechanical::solid;

    auto variables = std::make_shared<internal_variables_t>(internal_variable_size);

    // Add the required variables for an updated Lagrangian formulation
    variables->add(variable::second::deformation_gradient, variable::second::cauchy_stress);

    variables->add(variable::scalar::DetF);

    auto affine = make_constitutive_model(variables,
                                          json::parse("{\"Name\" : \"rubber\", "
                                                      "\"ElasticModulus\" : 10.0e6, "
                                                      "\"PoissonsRatio\" : 0.45, "
                                                      "\"SegmentsPerChain\" : 50}"),
                                          json::parse("{\"ConstitutiveModel\" : {\"Name\": "
                                                      "\"Microsphere\", \"Type\" "
                                                      ": \"Affine\", \"Statistics\":\"Gaussian\",  "
                                                      "\"Quadrature\" : "
                                                      "\"BO21\"}}"));

    auto [F_list, cauchy_stresses] = variables->get(variable::second::deformation_gradient,
                                                    variable::second::cauchy_stress);

    auto& J_list = variables->get(variable::scalar::DetF);

    for (auto& J : J_list) J = 1.0;

    auto& material_tangents = variables->get(variable::fourth::tangent_operator);

    Eigen::EigenSolver<Eigen::MatrixXd> eigen_solver;

    SECTION("Sanity checks")
    {
        REQUIRE(affine->is_symmetric());
        REQUIRE(affine->is_finite_deformation());
        REQUIRE(affine->intrinsic_material().name() == "rubber");
    }
    SECTION("Affine model under no load")
    {
        // Fill with identity matrix
        for (auto& F : F_list) F = neon::matrix3::Identity();

        affine->update_internal_variables(1.0);

        for (auto const& material_tangent : material_tangents)
        {
            REQUIRE((material_tangent - material_tangent.transpose()).norm()
                    == Approx(0.0).margin(ZERO_MARGIN));

            eigen_solver.compute(material_tangent);
            REQUIRE((eigen_solver.eigenvalues().real().array() > 0.0).all());
        }

        for (auto& cauchy_stress : cauchy_stresses)
        {
            REQUIRE(cauchy_stress.norm() == Approx(0.0).margin(ZERO_MARGIN));
        }
    }
    SECTION("Affine model under uniaxial load")
    {
        for (auto& F : F_list)
        {
            F(0, 0) = 1.1;
            F(1, 1) = 1.0 / std::sqrt(1.1);
            F(2, 2) = 1.0 / std::sqrt(1.1);
        }

        affine->update_internal_variables(1.0);

        for (auto const& material_tangent : material_tangents)
        {
            REQUIRE((material_tangent - material_tangent.transpose()).norm()
                    == Approx(0.0).margin(ZERO_MARGIN));

            eigen_solver.compute(material_tangent);
            REQUIRE((eigen_solver.eigenvalues().real().array() > 0.0).all());
        }

        for (auto& cauchy_stress : cauchy_stresses)
        {
            REQUIRE(cauchy_stress.norm() > 0.0);
        }
    }
}
TEST_CASE("Gaussian affine microsphere model with ageing")
{
    using namespace neon::mechanical::solid;

    auto variables = std::make_shared<internal_variables_t>(1);

    // Add the required variables for an updated Lagrangian formulation
    variables->add(variable::second::deformation_gradient, variable::second::cauchy_stress);

    variables->add(variable::scalar::DetF);

    auto const material_data{"{\"Name\" : \"rubber\","
                             "\"ShearModulus\" : 2.0e6,"
                             "\"BulkModulus\" : 100e6,"
                             "\"SegmentsPerChain\" : 50,"
                             "\"ScissionProbability\" : 1.0e-5,"
                             "\"RecombinationProbability\" : 1.0e-5}"};

    auto const constitutive_data{"{\"ConstitutiveModel\" : {\"Name\": \"Microsphere\","
                                 "\"Type\":\"Affine\","
                                 "\"Statistics\":\"Gaussian\","
                                 "\"Quadrature\":\"BO21\","
                                 "\"Ageing\":\"BAND\"}}"};

    auto affine = make_constitutive_model(variables,
                                          json::parse(material_data),
                                          json::parse(constitutive_data));

    auto [F_list, cauchy_stresses] = variables->get(variable::second::deformation_gradient,
                                                    variable::second::cauchy_stress);

    auto& J_list = variables->get(variable::scalar::DetF);
    std::fill(begin(J_list), end(J_list), 1.0);

    auto& material_tangents = variables->get(variable::fourth::tangent_operator);

    Eigen::EigenSolver<Eigen::MatrixXd> eigen_solver;

    SECTION("Setup checks")
    {
        REQUIRE(affine->is_symmetric());
        REQUIRE(affine->is_finite_deformation());
        REQUIRE(affine->intrinsic_material().name() == "rubber");

        REQUIRE(variables->has(variable::scalar::active_shear_modulus));
        REQUIRE(variables->has(variable::scalar::inactive_shear_modulus));
        REQUIRE(variables->has(variable::scalar::active_segments));
        REQUIRE(variables->has(variable::scalar::inactive_segments));
        REQUIRE(variables->has(variable::scalar::reduction_factor));

        REQUIRE(variables->has(variable::vector::first_ageing_moduli));
        REQUIRE(variables->has(variable::vector::second_ageing_moduli));
        REQUIRE(variables->has(variable::vector::third_ageing_moduli));

        REQUIRE(variables->has(variable::vector::first_previous));
        REQUIRE(variables->has(variable::vector::second_previous));
        REQUIRE(variables->has(variable::vector::third_previous));

        for (auto segment : variables->get(variable::scalar::active_segments))
        {
            REQUIRE(segment == Approx(50.0));
        }
        for (auto shear_modulus : variables->get(variable::scalar::active_shear_modulus))
        {
            REQUIRE(shear_modulus == Approx(2.0e6));
        }
        for (auto& value : variables->get(variable::vector::first_ageing_moduli))
        {
            REQUIRE(value.size() == 21);
        }
        for (auto& value : variables->get(variable::vector::first_previous))
        {
            REQUIRE(value.size() == 21);
        }
    }
    SECTION("no load")
    {
        std::fill(begin(F_list), end(F_list), neon::matrix3::Identity());

        affine->update_internal_variables(1.0);

        // Check the network parameters
        auto [active_segments,
              inactive_segments,
              active_shear_moduli,
              inactive_shear_moduli,
              reductions] = variables->get(variable::scalar::active_segments,
                                           variable::scalar::inactive_segments,
                                           variable::scalar::active_shear_modulus,
                                           variable::scalar::inactive_shear_modulus,
                                           variable::scalar::reduction_factor);

        for (auto active_segment : active_segments)
        {
            REQUIRE(active_segment > 49.0);
            REQUIRE(active_segment < 50.0);
        }
        for (auto inactive_segment : inactive_segments)
        {
            REQUIRE(inactive_segment > 20.0);
            REQUIRE(inactive_segment < 25.0);
        }
        for (auto shear_modulus : active_shear_moduli)
        {
            REQUIRE(shear_modulus < 2.01e6);
            REQUIRE(shear_modulus > 2.0e6);
        }
        for (auto shear_modulus : inactive_shear_moduli)
        {
            REQUIRE(shear_modulus < 2000.0);
            REQUIRE(shear_modulus > 1000.0);
        }
        for (auto reduction : reductions)
        {
            REQUIRE(reduction > 0.99);
            REQUIRE(reduction < 1.0);
        }

        for (auto const& material_tangent : material_tangents)
        {
            REQUIRE((material_tangent - material_tangent.transpose()).norm()
                    == Approx(0.0).margin(ZERO_MARGIN));

            eigen_solver.compute(material_tangent);
            REQUIRE((eigen_solver.eigenvalues().real().array() > 0.0).all());
        }
    }
    SECTION("uniaxial load")
    {
        for (auto& F : F_list)
        {
            F(0, 0) = 1.0;
            F(1, 1) = 1.0;
            F(2, 2) = 1.0;
        }

        affine->update_internal_variables(1.0);

        for (auto& cauchy_stress : cauchy_stresses)
        {
            std::cout << cauchy_stress << "\n\n";
            REQUIRE(cauchy_stress.norm() > 0.0);
        }

        std::cout << "setting to stretch 1.1\n";

        for (auto& F : F_list)
        {
            F(0, 0) = 1.1;
            F(1, 1) = 1.0 / std::sqrt(1.1);
            F(2, 2) = 1.0 / std::sqrt(1.1);
        }

        affine->update_internal_variables(1.0);

        for (auto& cauchy_stress : cauchy_stresses)
        {
            std::cout << cauchy_stress << "\n\n";
            REQUIRE(cauchy_stress.norm() > 0.0);
        }

        affine->update_internal_variables(1.0);

        for (auto& cauchy_stress : cauchy_stresses)
        {
            std::cout << cauchy_stress << "\n\n";
            REQUIRE(cauchy_stress.norm() > 0.0);
        }

        std::cout << "setting to unity\n";

        for (auto& F : F_list)
        {
            F(0, 0) = 1.0;
            F(1, 1) = 1.0;
            F(2, 2) = 1.0;
        }

        affine->update_internal_variables(1.0);

        for (auto& cauchy_stress : cauchy_stresses)
        {
            std::cout << cauchy_stress << "\n\n";
            REQUIRE(cauchy_stress.norm() >= 0.0);
        }

        std::cout << "finished the analysis\n";

        for (auto const& material_tangent : material_tangents)
        {
            REQUIRE((material_tangent - material_tangent.transpose()).norm()
                    == Approx(0.0).margin(ZERO_MARGIN));

            eigen_solver.compute(material_tangent);
            REQUIRE((eigen_solver.eigenvalues().real().array() > 0.0).all());
        }
    }
}
TEST_CASE("Gaussian affine microsphere model with crosslinking only")
{
    using namespace neon::mechanical::solid;

    std::cout << "Constant cross-linking stress check\n";

    auto variables = std::make_shared<internal_variables_t>(1);

    // Add the required variables for an updated Lagrangian formulation
    variables->add(variable::second::deformation_gradient, variable::second::cauchy_stress);

    variables->add(variable::scalar::DetF);

    auto const material_data{"{\"Name\" : \"rubber\","
                             "\"ShearModulus\" : 2.0e6,"
                             "\"BulkModulus\" : 100e6,"
                             "\"SegmentsPerChain\" : 50,"
                             "\"ScissionProbability\" : 0.0,"
                             "\"RecombinationProbability\" : 1.0e-5}"};

    auto const constitutive_data{"{\"ConstitutiveModel\" : {\"Name\": \"Microsphere\","
                                 "\"Type\":\"Affine\","
                                 "\"Statistics\":\"Gaussian\","
                                 "\"Quadrature\":\"BO21\","
                                 "\"Ageing\":\"BAND\"}}"};

    auto affine = make_constitutive_model(variables,
                                          json::parse(material_data),
                                          json::parse(constitutive_data));

    auto [F_list, cauchy_stresses] = variables->get(variable::second::deformation_gradient,
                                                    variable::second::cauchy_stress);

    auto& J_list = variables->get(variable::scalar::DetF);
    std::fill(begin(J_list), end(J_list), 1.0);

    auto& material_tangents = variables->get(variable::fourth::tangent_operator);

    Eigen::EigenSolver<Eigen::MatrixXd> eigen_solver;

    SECTION("no load")
    {
        std::fill(begin(F_list), end(F_list), neon::matrix3::Identity());

        affine->update_internal_variables(1.0);

        // Check the network parameters
        auto [active_segments,
              inactive_segments,
              active_shear_moduli,
              inactive_shear_moduli,
              reductions] = variables->get(variable::scalar::active_segments,
                                           variable::scalar::inactive_segments,
                                           variable::scalar::active_shear_modulus,
                                           variable::scalar::inactive_shear_modulus,
                                           variable::scalar::reduction_factor);

        for (auto active_segment : active_segments)
        {
            REQUIRE(active_segment > 49.0);
            REQUIRE(active_segment < 50.0);
        }
        for (auto inactive_segment : inactive_segments)
        {
            REQUIRE(inactive_segment == Approx(0.0).margin(0.0));
        }
        for (auto shear_modulus : active_shear_moduli)
        {
            REQUIRE(shear_modulus < 2.01e6);
            REQUIRE(shear_modulus > 2.0e6);
        }
        for (auto shear_modulus : inactive_shear_moduli)
        {
            REQUIRE(shear_modulus == Approx(0.0).margin(0.0));
        }
        for (auto reduction : reductions)
        {
            REQUIRE(reduction <= 1.0);
        }

        for (auto const& material_tangent : material_tangents)
        {
            REQUIRE((material_tangent - material_tangent.transpose()).norm()
                    == Approx(0.0).margin(ZERO_MARGIN));

            eigen_solver.compute(material_tangent);
            REQUIRE((eigen_solver.eigenvalues().real().array() > 0.0).all());
        }
    }
    SECTION("constant load")
    {
        // Check the network parameters
        // auto [active_segments,
        //       inactive_segments,
        //       active_shear_moduli,
        //       inactive_shear_moduli,
        //       reductions] = variables->get(variable::scalar::active_segments,
        //                                    variable::scalar::inactive_segments,
        //                                    variable::scalar::active_shear_modulus,
        //                                    variable::scalar::inactive_shear_modulus,
        //                                    variable::scalar::reduction_factor);

        std::fill(begin(F_list), end(F_list), neon::matrix3::Identity());

        affine->update_internal_variables(1.0);

        for (auto& F : F_list)
        {
            F(0, 0) = 1.1;
            F(1, 1) = 1.0 / std::sqrt(1.1);
            F(2, 2) = 1.0 / std::sqrt(1.1);
        }

        affine->update_internal_variables(1.0);

        for (auto const& cauchy_stress : cauchy_stresses)
        {
            std::cout << "Step 1: cauchy_stress\n" << cauchy_stress << "\n\n";
        }

        affine->update_internal_variables(1.0);

        for (auto const& cauchy_stress : cauchy_stresses)
        {
            std::cout << "Step 2: cauchy_stress\n" << cauchy_stress << "\n\n";
        }

        affine->update_internal_variables(1.0);

        for (auto const& cauchy_stress : cauchy_stresses)
        {
            std::cout << "Step 3: cauchy_stress\n" << cauchy_stress << "\n\n";
        }

        affine->update_internal_variables(1.0);

        for (auto const& cauchy_stress : cauchy_stresses)
        {
            std::cout << "Step 3: cauchy_stress\n" << cauchy_stress << "\n\n";
        }

        affine->update_internal_variables(1.0);

        for (auto const& cauchy_stress : cauchy_stresses)
        {
            std::cout << "Step 3: cauchy_stress\n" << cauchy_stress << "\n\n";
        }

        for (auto const& material_tangent : material_tangents)
        {
            std::cout << "tangent_matrix\n" << material_tangent << "\n";

            REQUIRE((material_tangent - material_tangent.transpose()).norm()
                    == Approx(0.0).margin(ZERO_MARGIN));

            eigen_solver.compute(material_tangent);
            REQUIRE((eigen_solver.eigenvalues().real().array() > 0.0).all());
        }
    }
}
TEST_CASE("Affine microsphere model")
{
    using namespace neon::mechanical::solid;

    auto variables = std::make_shared<internal_variables_t>(internal_variable_size);

    // Add the required variables for an updated Lagrangian formulation
    variables->add(variable::second::deformation_gradient, variable::second::cauchy_stress);

    variables->add(variable::scalar::DetF);

    auto affine = make_constitutive_model(variables,
                                          json::parse("{\"Name\" : \"rubber\", "
                                                      "\"ElasticModulus\" : 10.0e6, "
                                                      "\"PoissonsRatio\" : 0.45, "
                                                      "\"SegmentsPerChain\" : 50}"),
                                          json::parse("{\"ConstitutiveModel\" : {\"Name\": "
                                                      "\"Microsphere\", \"Type\" "
                                                      ": \"Affine\", \"Statistics\":\"Langevin\", "
                                                      "\"Quadrature\" : \"BO21\"}}"));

    auto [F_list, cauchy_stresses] = variables->get(variable::second::deformation_gradient,
                                                    variable::second::cauchy_stress);

    auto& J_list = variables->get(variable::scalar::DetF);

    for (auto& J : J_list) J = 1.0;

    auto& material_tangents = variables->get(variable::fourth::tangent_operator);

    Eigen::EigenSolver<Eigen::MatrixXd> eigen_solver;

    SECTION("Sanity checks")
    {
        REQUIRE(affine->is_symmetric());
        REQUIRE(affine->is_finite_deformation());
        REQUIRE(affine->intrinsic_material().name() == "rubber");
    }
    SECTION("Affine model under no load")
    {
        // Fill with identity matrix
        for (auto& F : F_list) F = neon::matrix3::Identity();

        affine->update_internal_variables(1.0);

        for (auto const& material_tangent : material_tangents)
        {
            REQUIRE((material_tangent - material_tangent.transpose()).norm()
                    == Approx(0.0).margin(ZERO_MARGIN));

            eigen_solver.compute(material_tangent);
            REQUIRE((eigen_solver.eigenvalues().real().array() > 0.0).all());
        }

        for (auto& cauchy_stress : cauchy_stresses)
        {
            REQUIRE(cauchy_stress.norm() == Approx(0.0).margin(ZERO_MARGIN));
        }
    }
    SECTION("Affine model under uniaxial load")
    {
        for (auto& F : F_list)
        {
            F(0, 0) = 1.1;
            F(1, 1) = 1.0 / std::sqrt(1.1);
            F(2, 2) = 1.0 / std::sqrt(1.1);
        }

        affine->update_internal_variables(1.0);

        for (auto const& material_tangent : material_tangents)
        {
            REQUIRE((material_tangent - material_tangent.transpose()).norm()
                    == Approx(0.0).margin(ZERO_MARGIN));

            eigen_solver.compute(material_tangent);
            REQUIRE((eigen_solver.eigenvalues().real().array() > 0.0).all());
        }

        for (auto& cauchy_stress : cauchy_stresses)
        {
            REQUIRE(cauchy_stress.norm() > 0.0);
        }
    }
}
TEST_CASE("NonAffine microsphere model")
{
    using namespace neon::mechanical::solid;

    auto variables = std::make_shared<internal_variables_t>(internal_variable_size);

    // Add the required variables for an updated Lagrangian formulation
    variables->add(variable::second::deformation_gradient, variable::second::cauchy_stress);
    variables->add(variable::scalar::DetF);

    auto affine = make_constitutive_model(variables,
                                          json::parse("{\"Name\" : \"rubber\", "
                                                      "\"ElasticModulus\" : 10.0e6, "
                                                      "\"PoissonsRatio\" : 0.45, "
                                                      "\"NonAffineStretchParameter\":1.0, "
                                                      "\"SegmentsPerChain\" : 50}"),
                                          json::parse("{\"ConstitutiveModel\" : {\"Name\": "
                                                      "\"Microsphere\", \"Type\" "
                                                      ": \"NonAffine\", \"Quadrature\" : "
                                                      "\"BO21\"}}"));

    // Get the tensor variables
    auto [F_list, cauchy_stresses] = variables->get(variable::second::deformation_gradient,
                                                    variable::second::cauchy_stress);

    auto& J_list = variables->get(variable::scalar::DetF);

    for (auto& J : J_list) J = 1.0;

    auto& material_tangents = variables->get(variable::fourth::tangent_operator);

    Eigen::EigenSolver<Eigen::MatrixXd> eigen_solver;

    SECTION("exception")
    {
        REQUIRE_THROWS_AS(make_constitutive_model(variables,
                                                  json::parse("{\"Name\" : \"rubber\", "
                                                              "\"ElasticModulus\" : 10.0e6, "
                                                              "\"PoissonsRatio\" : 0.45, "
                                                              "\"NonAffineStrearameter\":1."
                                                              "0, "
                                                              "\"SegmentsPerChain\" : 50}"),
                                                  json::parse("{\"ConstitutiveModel\" : "
                                                              "{\"Name\": "
                                                              "\"Microsphere\", \"Type\" "
                                                              ": \"NonAffine\", \"Quadrature\" "
                                                              ": "
                                                              "\"BO21\"}}")),
                          std::domain_error);
    }
    SECTION("Sanity test")
    {
        REQUIRE(affine->is_symmetric());
        REQUIRE(affine->is_finite_deformation());
        REQUIRE(affine->intrinsic_material().name() == "rubber");
    }
    SECTION("NonAffine model under no load")
    {
        // Fill with identity matrix
        for (auto& F : F_list) F = neon::matrix3::Identity();

        affine->update_internal_variables(1.0);

        for (auto const& material_tangent : material_tangents)
        {
            // Ensure symmetry is correct
            REQUIRE((material_tangent - material_tangent.transpose()).norm()
                    == Approx(0.0).margin(ZERO_MARGIN));

            eigen_solver.compute(material_tangent);
            REQUIRE((eigen_solver.eigenvalues().real().array() > 0.0).all());
        }

        for (auto& cauchy_stress : cauchy_stresses)
        {
            REQUIRE(cauchy_stress.norm() == Approx(0.0).margin(ZERO_MARGIN));
        }
    }
    SECTION("NonAffine model under uniaxial load")
    {
        for (auto& F : F_list)
        {
            F(0, 0) = 1.1;
            F(1, 1) = 1.0 / std::sqrt(1.1);
            F(2, 2) = 1.0 / std::sqrt(1.1);
        }

        affine->update_internal_variables(1.0);

        for (auto const& material_tangent : material_tangents)
        {
            // Ensure symmetry is correct
            REQUIRE((material_tangent - material_tangent.transpose()).norm()
                    == Approx(0.0).margin(ZERO_MARGIN));

            eigen_solver.compute(material_tangent);
            REQUIRE((eigen_solver.eigenvalues().real().array() > 0.0).all());
        }

        for (auto& cauchy_stress : cauchy_stresses)
        {
            REQUIRE(cauchy_stress.norm() > 0.0);
        }
    }
}
TEST_CASE("Plane stress linear elasticity factory error")
{
    using namespace neon::mechanical::plane;

    auto variables = std::make_shared<internal_variables_t>(internal_variable_size);

    REQUIRE_THROWS_AS(make_constitutive_model(variables,
                                              json::parse("{\"Badkey\" : \"donkey\"}"),
                                              json::parse("{\"ConstitutiveModel\" : {\"Name\" : "
                                                          "\"PlaneStrain\"}}")),
                      std::domain_error);
}
TEST_CASE("Plane stress elasticity model")
{
    using namespace neon::mechanical::plane;

    auto variables = std::make_shared<internal_variables_t>(internal_variable_size);

    // Add the required variables for an updated Lagrangian formulation
    variables->add(variable::second::displacement_gradient,
                   variable::second::cauchy_stress,
                   variable::scalar::DetF);

    auto elastic_model = make_constitutive_model(variables,
                                                 json::parse("{\"Name\": \"steel\", "
                                                             "\"ElasticModulus\": 200.0e3, "
                                                             "\"PoissonsRatio\": 0.3}"),
                                                 json::parse("{\"ConstitutiveModel\" : {\"Name\" : "
                                                             "\"PlaneStress\"}}"));

    // Get the tensor variables
    auto [displacement_gradients,
          cauchy_stresses] = variables->get(variable::second::displacement_gradient,
                                            variable::second::cauchy_stress);

    auto& J_list = variables->get(variable::scalar::DetF);

    auto& material_tangents = variables->get(variable::fourth::tangent_operator);

    for (auto& H : displacement_gradients) H = internal_variables_t::second_tensor_type::Zero();
    for (auto& J : J_list) J = 1.0;

    Eigen::EigenSolver<Eigen::MatrixXd> eigen_solver;

    SECTION("Sanity checks")
    {
        REQUIRE(elastic_model->is_symmetric());
        REQUIRE(elastic_model->is_finite_deformation() == false);
        REQUIRE(elastic_model->intrinsic_material().name() == "steel");

        REQUIRE(variables->has(variable::scalar::von_mises_stress));
        REQUIRE(variables->has(variable::second::cauchy_stress));
        REQUIRE(variables->has(variable::second::linearised_strain));
        REQUIRE(variables->has(variable::fourth::tangent_operator));
    }
    SECTION("No load")
    {
        elastic_model->update_internal_variables(1.0);

        // Ensure symmetry is correct
        for (auto const& material_tangent : material_tangents)
        {
            REQUIRE(material_tangent(0, 0) == Approx(219780.21978022));
            REQUIRE(material_tangent(1, 1) == Approx(219780.21978022));
            REQUIRE(material_tangent(2, 2) == Approx(76923.0769230769));
            REQUIRE(material_tangent(0, 1) == Approx(65934.065934066));
            REQUIRE(material_tangent(1, 0) == Approx(65934.065934066));

            REQUIRE(material_tangent(0, 2) == Approx(0.0).margin(ZERO_MARGIN));
            REQUIRE(material_tangent(2, 0) == Approx(0.0).margin(ZERO_MARGIN));
            REQUIRE(material_tangent(1, 2) == Approx(0.0).margin(ZERO_MARGIN));
            REQUIRE(material_tangent(2, 1) == Approx(0.0).margin(ZERO_MARGIN));

            REQUIRE((material_tangent - material_tangent.transpose()).norm()
                    == Approx(0.0).margin(ZERO_MARGIN));

            eigen_solver.compute(material_tangent);
            REQUIRE((eigen_solver.eigenvalues().real().array() > 0.0).all());
        }

        for (auto& cauchy_stress : cauchy_stresses)
        {
            REQUIRE(cauchy_stress.norm() == Approx(0.0).margin(ZERO_MARGIN));
        }
    }
    SECTION("Uniaxial elastic load")
    {
        for (auto& H : displacement_gradients) H(1, 1) = 0.001;

        elastic_model->update_internal_variables(1.0);

        auto [von_mises_stresses,
              accumulated_plastic_strains] = variables->get(variable::scalar::von_mises_stress,
                                                            variable::scalar::effective_plastic_strain);

        for (auto const& material_tangent : material_tangents)
        {
            REQUIRE(material_tangent(0, 0) == Approx(219780.21978022));
            REQUIRE(material_tangent(1, 1) == Approx(219780.21978022));
            REQUIRE(material_tangent(2, 2) == Approx(76923.0769230769));
            REQUIRE(material_tangent(0, 1) == Approx(65934.065934066));
            REQUIRE(material_tangent(1, 0) == Approx(65934.065934066));

            REQUIRE(material_tangent(0, 2) == Approx(0.0).margin(ZERO_MARGIN));
            REQUIRE(material_tangent(2, 0) == Approx(0.0).margin(ZERO_MARGIN));
            REQUIRE(material_tangent(1, 2) == Approx(0.0).margin(ZERO_MARGIN));
            REQUIRE(material_tangent(2, 1) == Approx(0.0).margin(ZERO_MARGIN));

            // Ensure symmetry is correct
            REQUIRE((material_tangent - material_tangent.transpose()).norm()
                    == Approx(0.0).margin(ZERO_MARGIN));

            eigen_solver.compute(material_tangent);
            REQUIRE((eigen_solver.eigenvalues().real().array() > 0.0).all());
        }

        for (auto const& cauchy_stress : cauchy_stresses)
        {
            REQUIRE(cauchy_stress.norm() != Approx(0.0).margin(ZERO_MARGIN));

            // Shear components should be close to zero
            REQUIRE(cauchy_stress(0, 1) == Approx(0.0).margin(ZERO_MARGIN));
            REQUIRE(cauchy_stress(0, 0) > 0.0);
            REQUIRE(cauchy_stress(1, 1) > 0.0);
        }

        for (auto& von_mises_stress : von_mises_stresses)
        {
            REQUIRE(von_mises_stress < 200.0e6);
        }
    }
}
TEST_CASE("Plane strain elasticity model")
{
    using namespace neon::mechanical::plane;

    auto variables = std::make_shared<internal_variables_t>(internal_variable_size);

    // Add the required variables for an updated Lagrangian formulation
    variables->add(variable::second::displacement_gradient,
                   variable::second::cauchy_stress,
                   variable::scalar::DetF);

    auto elastic_model = make_constitutive_model(variables,
                                                 json::parse("{\"Name\": \"steel\", "
                                                             "\"ElasticModulus\": 200.0e3, "
                                                             "\"PoissonsRatio\": 0.3}"),
                                                 json::parse("{\"ConstitutiveModel\" : {\"Name\" : "
                                                             "\"PlaneStrain\"}}"));

    // Get the tensor variables
    auto [displacement_gradients,
          cauchy_stresses] = variables->get(variable::second::displacement_gradient,
                                            variable::second::cauchy_stress);

    auto& J_list = variables->get(variable::scalar::DetF);

    auto& material_tangents = variables->get(variable::fourth::tangent_operator);

    for (auto& H : displacement_gradients) H = internal_variables_t::second_tensor_type::Zero();
    for (auto& J : J_list) J = 1.0;

    Eigen::EigenSolver<Eigen::MatrixXd> eigen_solver;

    SECTION("Sanity checks")
    {
        REQUIRE(elastic_model->is_symmetric());
        REQUIRE(elastic_model->is_finite_deformation() == false);
        REQUIRE(elastic_model->intrinsic_material().name() == "steel");

        REQUIRE(variables->has(variable::scalar::von_mises_stress));
        REQUIRE(variables->has(variable::second::cauchy_stress));
        REQUIRE(variables->has(variable::second::linearised_strain));
        REQUIRE(variables->has(variable::fourth::tangent_operator));
    }
    SECTION("No load")
    {
        elastic_model->update_internal_variables(1.0);

        // Ensure symmetry is correct
        for (auto const& material_tangent : material_tangents)
        {
            REQUIRE(material_tangent(0, 0) == Approx(269230.769230769));
            REQUIRE(material_tangent(1, 1) == Approx(269230.769230769));
            REQUIRE(material_tangent(2, 2) == Approx(76923.0769230769));
            REQUIRE(material_tangent(0, 1) == Approx(115384.615384615));

            REQUIRE(material_tangent(0, 2) == Approx(0.0).margin(ZERO_MARGIN));
            REQUIRE(material_tangent(2, 0) == Approx(0.0).margin(ZERO_MARGIN));
            REQUIRE(material_tangent(1, 2) == Approx(0.0).margin(ZERO_MARGIN));
            REQUIRE(material_tangent(2, 1) == Approx(0.0).margin(ZERO_MARGIN));

            REQUIRE((material_tangent - material_tangent.transpose()).norm()
                    == Approx(0.0).margin(ZERO_MARGIN));

            eigen_solver.compute(material_tangent);
            REQUIRE((eigen_solver.eigenvalues().real().array() > 0.0).all());
        }

        for (auto& cauchy_stress : cauchy_stresses)
        {
            REQUIRE(cauchy_stress.norm() == Approx(0.0).margin(ZERO_MARGIN));
        }
    }
    SECTION("Uniaxial elastic load")
    {
        for (auto& H : displacement_gradients) H(1, 1) = 0.001;

        elastic_model->update_internal_variables(1.0);

        auto [von_mises_stresses,
              accumulated_plastic_strains] = variables->get(variable::scalar::von_mises_stress,
                                                            variable::scalar::effective_plastic_strain);

        for (auto const& material_tangent : material_tangents)
        {
            REQUIRE(material_tangent(0, 0) == Approx(269230.769230769));
            REQUIRE(material_tangent(1, 1) == Approx(269230.769230769));
            REQUIRE(material_tangent(2, 2) == Approx(76923.0769230769));
            REQUIRE(material_tangent(0, 1) == Approx(115384.615384615));

            REQUIRE(material_tangent(0, 2) == Approx(0.0).margin(ZERO_MARGIN));
            REQUIRE(material_tangent(2, 0) == Approx(0.0).margin(ZERO_MARGIN));
            REQUIRE(material_tangent(1, 2) == Approx(0.0).margin(ZERO_MARGIN));
            REQUIRE(material_tangent(2, 1) == Approx(0.0).margin(ZERO_MARGIN));

            // Ensure symmetry is correct
            REQUIRE((material_tangent - material_tangent.transpose()).norm()
                    == Approx(0.0).margin(ZERO_MARGIN));

            eigen_solver.compute(material_tangent);
            REQUIRE((eigen_solver.eigenvalues().real().array() > 0.0).all());
        }

        for (auto const& cauchy_stress : cauchy_stresses)
        {
            REQUIRE(cauchy_stress.norm() != Approx(0.0).margin(ZERO_MARGIN));

            // Shear components should be close to zero
            REQUIRE(cauchy_stress(0, 1) == Approx(0.0).margin(ZERO_MARGIN));
            REQUIRE(cauchy_stress(0, 0) > 0.0);
            REQUIRE(cauchy_stress(1, 1) > 0.0);
        }

        for (auto& von_mises_stress : von_mises_stresses)
        {
            REQUIRE(von_mises_stress < 200.0e6);
        }
    }
}
TEST_CASE("Solid mechanics elasticity model")
{
    using namespace neon::mechanical::solid;

    auto variables = std::make_shared<internal_variables_t>(internal_variable_size);

    // Add the required variables for an updated Lagrangian formulation
    variables->add(variable::second::displacement_gradient,
                   variable::second::cauchy_stress,
                   variable::scalar::DetF);

    auto elastic_model = make_constitutive_model(variables,
                                                 json::parse("{\"Name\": \"steel\", "
                                                             "\"ElasticModulus\": 200.0e3, "
                                                             "\"PoissonsRatio\": 0.3}"),
                                                 json::parse("{\"ConstitutiveModel\" : {\"Name\" : "
                                                             "\"IsotropicLinearElasticity\"}}"));

    // Get the tensor variables
    auto [displacement_gradients,
          cauchy_stresses] = variables->get(variable::second::displacement_gradient,
                                            variable::second::cauchy_stress);

    auto& J_list = variables->get(variable::scalar::DetF);

    auto& material_tangents = variables->get(variable::fourth::tangent_operator);

    for (auto& H : displacement_gradients) H = internal_variables_t::second_tensor_type::Zero();
    for (auto& J : J_list) J = 1.0;

    Eigen::EigenSolver<Eigen::MatrixXd> eigen_solver;

    SECTION("Sanity checks")
    {
        REQUIRE(elastic_model->is_symmetric());
        REQUIRE(elastic_model->is_finite_deformation() == false);
        REQUIRE(elastic_model->intrinsic_material().name() == "steel");

        REQUIRE(variables->has(variable::scalar::von_mises_stress));
        REQUIRE(variables->has(variable::second::cauchy_stress));
        REQUIRE(variables->has(variable::second::linearised_strain));
        REQUIRE(variables->has(variable::fourth::tangent_operator));
    }
    SECTION("No load")
    {
        elastic_model->update_internal_variables(1.0);

        // Ensure symmetry is correct
        for (auto const& material_tangent : material_tangents)
        {
            REQUIRE(material_tangent(0, 0) == Approx(269230.769230769));
            REQUIRE(material_tangent(1, 1) == Approx(269230.769230769));
            REQUIRE(material_tangent(2, 2) == Approx(269230.769230769));

            REQUIRE(material_tangent(3, 3) == Approx(76923.0769230769));
            REQUIRE(material_tangent(4, 4) == Approx(76923.0769230769));
            REQUIRE(material_tangent(5, 5) == Approx(76923.0769230769));

            REQUIRE(material_tangent(0, 1) == Approx(115384.615384615));
            REQUIRE(material_tangent(0, 2) == Approx(115384.615384615));
            REQUIRE(material_tangent(1, 2) == Approx(115384.615384615));

            REQUIRE(material_tangent(0, 3) == Approx(0.0).margin(ZERO_MARGIN));
            REQUIRE(material_tangent(0, 4) == Approx(0.0).margin(ZERO_MARGIN));
            REQUIRE(material_tangent(0, 5) == Approx(0.0).margin(ZERO_MARGIN));

            REQUIRE(material_tangent(1, 3) == Approx(0.0).margin(ZERO_MARGIN));
            REQUIRE(material_tangent(1, 4) == Approx(0.0).margin(ZERO_MARGIN));
            REQUIRE(material_tangent(1, 5) == Approx(0.0).margin(ZERO_MARGIN));

            REQUIRE(material_tangent(2, 3) == Approx(0.0).margin(ZERO_MARGIN));
            REQUIRE(material_tangent(2, 4) == Approx(0.0).margin(ZERO_MARGIN));
            REQUIRE(material_tangent(2, 5) == Approx(0.0).margin(ZERO_MARGIN));

            REQUIRE(material_tangent(3, 4) == Approx(0.0).margin(ZERO_MARGIN));
            REQUIRE(material_tangent(3, 5) == Approx(0.0).margin(ZERO_MARGIN));

            REQUIRE(material_tangent(4, 5) == Approx(0.0).margin(ZERO_MARGIN));

            REQUIRE((material_tangent - material_tangent.transpose()).norm()
                    == Approx(0.0).margin(ZERO_MARGIN));

            eigen_solver.compute(material_tangent);
            REQUIRE((eigen_solver.eigenvalues().real().array() > 0.0).all());
        }

        for (auto& cauchy_stress : cauchy_stresses)
        {
            REQUIRE(cauchy_stress.norm() == Approx(0.0).margin(ZERO_MARGIN));
        }
    }
    SECTION("Uniaxial elastic load")
    {
        for (auto& H : displacement_gradients) H(1, 1) = 0.001;

        elastic_model->update_internal_variables(1.0);

        auto [von_mises_stresses,
              accumulated_plastic_strains] = variables->get(variable::scalar::von_mises_stress,
                                                            variable::scalar::effective_plastic_strain);

        for (auto const& material_tangent : material_tangents)
        {
            REQUIRE(material_tangent(0, 0) == Approx(269230.769230769));
            REQUIRE(material_tangent(1, 1) == Approx(269230.769230769));
            REQUIRE(material_tangent(2, 2) == Approx(269230.769230769));

            REQUIRE(material_tangent(3, 3) == Approx(76923.0769230769));
            REQUIRE(material_tangent(4, 4) == Approx(76923.0769230769));
            REQUIRE(material_tangent(5, 5) == Approx(76923.0769230769));

            REQUIRE(material_tangent(0, 1) == Approx(115384.615384615));
            REQUIRE(material_tangent(0, 2) == Approx(115384.615384615));
            REQUIRE(material_tangent(1, 2) == Approx(115384.615384615));

            REQUIRE(material_tangent(0, 3) == Approx(0.0).margin(ZERO_MARGIN));
            REQUIRE(material_tangent(0, 4) == Approx(0.0).margin(ZERO_MARGIN));
            REQUIRE(material_tangent(0, 5) == Approx(0.0).margin(ZERO_MARGIN));

            REQUIRE(material_tangent(1, 3) == Approx(0.0).margin(ZERO_MARGIN));
            REQUIRE(material_tangent(1, 4) == Approx(0.0).margin(ZERO_MARGIN));
            REQUIRE(material_tangent(1, 5) == Approx(0.0).margin(ZERO_MARGIN));

            REQUIRE(material_tangent(2, 3) == Approx(0.0).margin(ZERO_MARGIN));
            REQUIRE(material_tangent(2, 4) == Approx(0.0).margin(ZERO_MARGIN));
            REQUIRE(material_tangent(2, 5) == Approx(0.0).margin(ZERO_MARGIN));

            REQUIRE(material_tangent(3, 4) == Approx(0.0).margin(ZERO_MARGIN));
            REQUIRE(material_tangent(3, 5) == Approx(0.0).margin(ZERO_MARGIN));

            REQUIRE(material_tangent(4, 5) == Approx(0.0).margin(ZERO_MARGIN));

            // Ensure symmetry is correct
            REQUIRE((material_tangent - material_tangent.transpose()).norm()
                    == Approx(0.0).margin(ZERO_MARGIN));

            eigen_solver.compute(material_tangent);
            REQUIRE((eigen_solver.eigenvalues().real().array() > 0.0).all());
        }

        for (auto const& cauchy_stress : cauchy_stresses)
        {
            REQUIRE(cauchy_stress.norm() != Approx(0.0).margin(ZERO_MARGIN));

            // Shear components should be close to zero
            REQUIRE(cauchy_stress(0, 1) == Approx(0.0).margin(ZERO_MARGIN));
            REQUIRE(cauchy_stress(0, 0) > 0.0);
            REQUIRE(cauchy_stress(1, 1) > 0.0);
        }

        for (auto& von_mises_stress : von_mises_stresses)
        {
            REQUIRE(von_mises_stress < 200.0e6);
        }
    }
}
TEST_CASE("Solid mechanics J2 plasticity model factory errors")
{
    using namespace neon::mechanical::solid;

    auto variables = std::make_shared<internal_variables_t>(internal_variable_size);

    SECTION("FiniteStrain not specified")
    {
        REQUIRE_THROWS_AS(make_constitutive_model(variables,
                                                  json::parse("{}"),
                                                  json::parse("{\"ConstitutiveModel\" : {\"Name\" "
                                                              ": "
                                                              "\"J2Plasticity\"}}")),
                          std::domain_error);
    }
    SECTION("Damage model not specified correctly")
    {
        REQUIRE_THROWS_AS(make_constitutive_model(variables,
                                                  json::parse("{}"),
                                                  json::parse("{\"ConstitutiveModel\" : {\"Name\" "
                                                              ": "
                                                              "\"J2Plasticity\", "
                                                              "\"Damage\" : \"IsotropicChaboche\", "
                                                              "\"FiniteStrain\" : true}}")),
                          std::domain_error);
    }
}
TEST_CASE("Solid mechanics J2 plasticity model")
{
    using namespace neon::mechanical::solid;

    auto variables = std::make_shared<internal_variables_t>(internal_variable_size);

    // Add the required variables for an updated Lagrangian formulation
    variables->add(variable::second::displacement_gradient, variable::second::cauchy_stress);
    variables->add(variable::scalar::DetF);

    auto const material_data = json::parse("{\"Name\": \"steel\", "
                                           "\"ElasticModulus\": 200.0e9, "
                                           "\"PoissonsRatio\": 0.3, "
                                           "\"YieldStress\": 200.0e6, "
                                           "\"IsotropicHardeningModulus\": 400.0e6}");

    auto const constitutive_data = json::parse("{\"ConstitutiveModel\" : "
                                               "{\"Name\" : \"J2Plasticity\", "
                                               "\"FiniteStrain\" : false}}");

    auto small_strain_J2_plasticity = make_constitutive_model(variables,
                                                              material_data,
                                                              constitutive_data);

    // Get the tensor variables
    auto [displacement_gradients,
          cauchy_stresses] = variables->get(variable::second::displacement_gradient,
                                            variable::second::cauchy_stress);

    auto& J_list = variables->get(variable::scalar::DetF);

    auto& material_tangents = variables->get(variable::fourth::tangent_operator);

    for (auto& H : displacement_gradients) H = neon::matrix3::Zero();
    for (auto& J : J_list) J = 1.0;

    Eigen::EigenSolver<Eigen::MatrixXd> eigen_solver;

    SECTION("Sanity checks")
    {
        REQUIRE(small_strain_J2_plasticity->is_symmetric());
        REQUIRE(small_strain_J2_plasticity->is_finite_deformation() == false);
        REQUIRE(small_strain_J2_plasticity->intrinsic_material().name() == "steel");

        REQUIRE(variables->has(variable::scalar::von_mises_stress));
        REQUIRE(variables->has(variable::scalar::effective_plastic_strain));
        REQUIRE(variables->has(variable::second::linearised_strain));
        REQUIRE(variables->has(variable::second::linearised_plastic_strain));
        REQUIRE(variables->has(variable::fourth::tangent_operator));
    }
    SECTION("Helper function")
    {
        neon::isotropic_elastic_plastic material{material_data};

        REQUIRE(neon::mechanical::evaluate_J2_yield_function(material, 100.0e6, 0.0) <= 0.0);
        REQUIRE(neon::mechanical::evaluate_J2_yield_function(material, 300.0e6, 0.0) > 0.0);
    }
    SECTION("No load")
    {
        small_strain_J2_plasticity->update_internal_variables(1.0);

        // Ensure symmetry is correct
        for (auto const& material_tangent : material_tangents)
        {
            REQUIRE((material_tangent - material_tangent.transpose()).norm()
                    == Approx(0.0).margin(ZERO_MARGIN));

            eigen_solver.compute(material_tangent);
            REQUIRE((eigen_solver.eigenvalues().real().array() > 0.0).all());
        }

        for (auto& cauchy_stress : cauchy_stresses)
        {
            REQUIRE(cauchy_stress.norm() == Approx(0.0).margin(ZERO_MARGIN));
        }
    }
    SECTION("Uniaxial elastic load")
    {
        for (auto& H : displacement_gradients) H(2, 2) = 0.001;

        small_strain_J2_plasticity->update_internal_variables(1.0);

        auto [von_mises_stresses,
              accumulated_plastic_strains] = variables->get(variable::scalar::von_mises_stress,
                                                            variable::scalar::effective_plastic_strain);

        for (auto const& material_tangent : material_tangents)
        {
            // Ensure symmetry is correct
            REQUIRE((material_tangent - material_tangent.transpose()).norm()
                    == Approx(0.0).margin(ZERO_MARGIN));

            eigen_solver.compute(material_tangent);
            REQUIRE((eigen_solver.eigenvalues().real().array() > 0.0).all());
        }

        for (auto const& cauchy_stress : cauchy_stresses)
        {
            REQUIRE(cauchy_stress.norm() != Approx(0.0).margin(ZERO_MARGIN));

            // Shear components should be close to zero
            REQUIRE(cauchy_stress(0, 1) == Approx(0.0).margin(ZERO_MARGIN));
            REQUIRE(cauchy_stress(0, 2) == Approx(0.0).margin(ZERO_MARGIN));
            REQUIRE(cauchy_stress(1, 2) == Approx(0.0).margin(ZERO_MARGIN));
            REQUIRE(cauchy_stress(0, 0) > 0.0);
            REQUIRE(cauchy_stress(1, 1) > 0.0);
            REQUIRE(cauchy_stress(2, 2) > 0.0);
        }

        for (auto& accumulated_plastic_strain : accumulated_plastic_strains)
        {
            REQUIRE(accumulated_plastic_strain == Approx(0.0).margin(ZERO_MARGIN));
        }

        for (auto& von_mises_stress : von_mises_stresses)
        {
            REQUIRE(von_mises_stress < 200.0e6);
        }
    }
    SECTION("Plastic uniaxial elastic load")
    {
        for (auto& H : displacement_gradients) H(2, 2) = 0.003;

        small_strain_J2_plasticity->update_internal_variables(1.0);

        auto [von_mises_stresses,
              accumulated_plastic_strains] = variables->get(variable::scalar::von_mises_stress,
                                                            variable::scalar::effective_plastic_strain);

        for (auto const& material_tangent : material_tangents)
        {
            // Ensure symmetry is correct
            REQUIRE((material_tangent - material_tangent.transpose()).norm()
                    == Approx(0.0).margin(ZERO_MARGIN));

            eigen_solver.compute(material_tangent);
            REQUIRE((eigen_solver.eigenvalues().real().array() > 0.0).all());
        }

        for (auto& cauchy_stress : cauchy_stresses)
        {
            REQUIRE(cauchy_stress.norm() != Approx(0.0).margin(ZERO_MARGIN));

            // Shear components should be close to zero
            REQUIRE(cauchy_stress(0, 1) == Approx(0.0).margin(ZERO_MARGIN));
            REQUIRE(cauchy_stress(0, 2) == Approx(0.0).margin(ZERO_MARGIN));
            REQUIRE(cauchy_stress(1, 2) == Approx(0.0).margin(ZERO_MARGIN));
            REQUIRE(cauchy_stress(0, 0) > 0.0);
            REQUIRE(cauchy_stress(1, 1) > 0.0);
            REQUIRE(cauchy_stress(2, 2) > 0.0);
        }

        for (auto& accumulated_plastic_strain : accumulated_plastic_strains)
        {
            REQUIRE(accumulated_plastic_strain > 0.0);
        }

        for (auto& von_mises_stress : von_mises_stresses)
        {
            // Should experience hardening
            REQUIRE(von_mises_stress <= 201.0e6);
            REQUIRE(von_mises_stress > 200.0e6);
        }
    }
}
TEST_CASE("Solid mechanics J2 plasticity damage model")
{
    using namespace neon::mechanical::solid;

    auto variables = std::make_shared<internal_variables_t>(internal_variable_size);

    variables->add(variable::second::displacement_gradient, variable::second::cauchy_stress);
    variables->add(variable::scalar::DetF);

    auto const material_input{"{\"Name\":\"steel\","
                              "\"ElasticModulus\":134.0e3,"
                              "\"PoissonsRatio\":0.3,"
                              "\"YieldStress\":85,"
                              "\"KinematicHardeningModulus\": 5500,"
                              "\"SofteningMultiplier\" : 250,"
                              "\"PlasticityViscousExponent\" : 2.5,"
                              "\"PlasticityViscousDenominator\" : 1220,"
                              "\"DamageViscousExponent\" : 2,"
                              "\"DamageViscousDenominator\" : 0.6"
                              "}"};

    auto const constitutive_input{"{\"ConstitutiveModel\" : "
                                  "{\"Name\" : \"J2Plasticity\","
                                  "\"Damage\" : \"IsotropicChaboche\", "
                                  "\"FiniteStrain\" : false}}"};

    auto small_strain_J2_plasticity_damage = make_constitutive_model(variables,
                                                                     json::parse(material_input),
                                                                     json::parse(constitutive_input));

    // Get the tensor variables
    auto [displacement_gradients,
          cauchy_stresses] = variables->get(variable::second::displacement_gradient,
                                            variable::second::cauchy_stress);

    auto [J_list, damage_list] = variables->get(variable::scalar::DetF, variable::scalar::damage);

    auto& material_tangents = variables->get(variable::fourth::tangent_operator);

    for (auto& H : displacement_gradients) H = neon::matrix3::Zero();
    for (auto& J : J_list) J = 1.0;

    Eigen::EigenSolver<Eigen::MatrixXd> eigen_solver;

    SECTION("Sanity checks")
    {
        REQUIRE(small_strain_J2_plasticity_damage->is_finite_deformation() == false);
        REQUIRE(small_strain_J2_plasticity_damage->is_symmetric() == false);

        REQUIRE(small_strain_J2_plasticity_damage->intrinsic_material().name() == "steel");

        REQUIRE(variables->has(variable::scalar::von_mises_stress));
        REQUIRE(variables->has(variable::scalar::effective_plastic_strain));
        REQUIRE(variables->has(variable::second::linearised_strain));
        REQUIRE(variables->has(variable::second::linearised_plastic_strain));
        REQUIRE(variables->has(variable::fourth::tangent_operator));
        REQUIRE(variables->has(variable::scalar::damage));
        REQUIRE(variables->has(variable::scalar::energy_release_rate));
        REQUIRE(variables->has(variable::second::kinematic_hardening));
        REQUIRE(variables->has(variable::second::back_stress));
    }
    SECTION("Uniaxial elastic load")
    {
        for (auto& H : displacement_gradients) H(2, 2) = 0.0008;

        small_strain_J2_plasticity_damage->update_internal_variables(1.0);

        auto [von_mises_stresses,
              accumulated_plastic_strains] = variables->get(variable::scalar::von_mises_stress,
                                                            variable::scalar::effective_plastic_strain);

        for (auto const& material_tangent : material_tangents)
        {
            // Ensure symmetry is correct when the loading is elastic
            REQUIRE((material_tangent - material_tangent.transpose()).norm()
                    == Approx(0.0).margin(ZERO_MARGIN));

            eigen_solver.compute(material_tangent);
            REQUIRE((eigen_solver.eigenvalues().real().array() > 0.0).all());
        }

        for (auto& cauchy_stress : cauchy_stresses)
        {
            REQUIRE(cauchy_stress.norm() != Approx(0.0).margin(ZERO_MARGIN));

            // Shear components should be close to zero
            REQUIRE(cauchy_stress(0, 1) == Approx(0.0).margin(ZERO_MARGIN));
            REQUIRE(cauchy_stress(0, 2) == Approx(0.0).margin(ZERO_MARGIN));
            REQUIRE(cauchy_stress(1, 2) == Approx(0.0).margin(ZERO_MARGIN));
            REQUIRE(cauchy_stress(0, 0) == Approx(cauchy_stress(1, 1)));
            REQUIRE(cauchy_stress(1, 1) > 0.0);
            REQUIRE(cauchy_stress(2, 2) > 0.0);
        }
        for (auto& accumulated_plastic_strain : accumulated_plastic_strains)
        {
            REQUIRE(accumulated_plastic_strain == Approx(0.0).margin(ZERO_MARGIN));
        }
        for (auto& von_mises_stress : von_mises_stresses)
        {
            REQUIRE(von_mises_stress > 80);
            REQUIRE(von_mises_stress < 85);
        }
    }
    SECTION("Plastic uniaxial load")
    {
        for (auto& H : displacement_gradients) H(2, 2) = 0.0009; // 0.000825 for comparison with 1D

        small_strain_J2_plasticity_damage->update_internal_variables(
            0.01); // time here is real (not pseudo time)

        auto [von_mises_stresses,
              accumulated_plastic_strains] = variables->get(variable::scalar::von_mises_stress,
                                                            variable::scalar::effective_plastic_strain);

        for (auto const& cauchy_stress : cauchy_stresses)
        {
            REQUIRE(cauchy_stress.norm() != Approx(0.0).margin(ZERO_MARGIN));

            // Shear components should be close to zero
            REQUIRE(cauchy_stress(0, 1) == Approx(0.0).margin(ZERO_MARGIN));
            REQUIRE(cauchy_stress(0, 2) == Approx(0.0).margin(ZERO_MARGIN));
            REQUIRE(cauchy_stress(1, 2) == Approx(0.0).margin(ZERO_MARGIN));
            REQUIRE(cauchy_stress(0, 0) > 0.0);
            REQUIRE(cauchy_stress(1, 1) > 0.0);
            REQUIRE(cauchy_stress(2, 2) > 0.0);
        }
        for (auto& accumulated_plastic_strain : accumulated_plastic_strains)
        {
            REQUIRE(accumulated_plastic_strain > 0.0);
        }
        for (auto& damage_var : damage_list)
        {
            REQUIRE(damage_var > 0.0);
        }
        for (auto& von_mises_stress : von_mises_stresses)
        {
            // Should experience hardening
            REQUIRE(von_mises_stress <= 100);
            REQUIRE(von_mises_stress > 85);
        }
        for (auto const& material_tangent : material_tangents)
        {
            eigen_solver.compute(material_tangent);
            REQUIRE((eigen_solver.eigenvalues().real().array() > 0.0).all());
        }
    }
}
TEST_CASE("Thermal isotropic model")
{
    //
    using namespace neon::diffusion;

    auto variables = std::make_shared<internal_variables_t>(internal_variable_size);

    auto thermal = make_constitutive_model(variables,
                                           json::parse(json_thermal_diffusion()),
                                           json::parse("{\"ConstitutiveModel\" : {\"Name\": "
                                                       "\"IsotropicDiffusion\"} }"));

    thermal->update_internal_variables(1.0);

    SECTION("Sanity checks")
    {
        REQUIRE(thermal->is_symmetric());
        REQUIRE(thermal->is_finite_deformation() == false);
        REQUIRE(thermal->intrinsic_material().name() == "steel");

        REQUIRE(variables->has(variable::second::conductivity));
    }
    Eigen::EigenSolver<Eigen::MatrixXd> eigen_solver;
}

// TEST_CASE("Finite J2 plasticity model", "[finite_strain_J2_plasticity]")
// {
//     using namespace neon::mechanical::solid;
//
//     // Create a json reader object from a string
//     std::string
//         input_data = "{\"Name\": \"steel\", \"ElasticModulus\": 200.0e9, \"PoissonsRatio\": 0.3,
//         "
//                      "\"YieldStress\": 200.0e6, \"IsotropicHardeningModulus\": 400.0e6}";
//
//     std::string simulation_input = "{\"ConstitutiveModel\" : {\"Name\" : \"small_strain_J2_plasticity\", "
//                                    "\"FiniteStrain\":true}}";
//
//     Json::Value material_data, simulation_data;
//
//     Json::Reader reader;
//
//     REQUIRE(reader.parse(input_data.c_str(), material_data));
//     REQUIRE(reader.parse(simulation_input.c_str(), simulation_data));
//
//     internal_variables variables(1);
//
//     // Add the required variables for an updated Lagrangian formulation
//     variables->add(variable::second::deformation_gradient,
//     variable::second::cauchy_stress); variables->add(variable::scalar::DetF);
//
//     auto small_strain_J2_plasticity = make_constitutive_model(variables, material_data, simulation_data);
//
//     // Get the tensor variables
//     auto[F_list, cauchy_stresses] =
//     variables->get(variable::second::deformation_gradient,
//                                               variable::second::cauchy_stress);
//
//     auto& J_list = variables->get(variable::scalar::DetF);
//
//     auto& material_tangents = variables->get(variable::fourth::tangent_operator);
//
//     for (auto& F : F_list) F = neon::matrix3::Identity();
//     for (auto& J : J_list) J = 1.0;
//
//     variables.commit();
//
//     SECTION("Sanity checks")
//     {
//         REQUIRE(small_strain_J2_plasticity->is_finite_deformation() == true);
//         REQUIRE(small_strain_J2_plasticity->intrinsic_material().name() == "steel");
//
//         REQUIRE(variables->has(variable::scalar::von_mises_stress));
//         REQUIRE(variables->has(variable::scalar::effective_plastic_strain));
//         REQUIRE(variables->has(variable::second::hencky_strain_elastic));
//         REQUIRE(variables->has(variable::fourth::tangent_operator));
//     }
//     SECTION("Initial material tangent symmetry")
//     {
//         for (auto const& material_tangent : material_tangents)
//         {
//             REQUIRE((material_tangent - material_tangent.transpose()).norm()
//                     == Approx(0.0).margin(ZERO_MARGIN));
//         }
//     }
//     SECTION("No load")
//     {
//         small_strain_J2_plasticity->update_internal_variables(1.0);
//
//         for (auto const& material_tangent : material_tangents)
//         {
//             REQUIRE((material_tangent - material_tangent.transpose()).norm()
//                     == Approx(0.0).margin(ZERO_MARGIN));
//         }
//         for (auto& cauchy_stress : cauchy_stresses)
//         {
//             REQUIRE(cauchy_stress.norm() == Approx(0.0).margin(ZERO_MARGIN));
//         }
//     }
//     // SECTION("Uniaxial elastic load")
//     // {
//     //     for (auto& F : F_list) F(2, 2) = 1.001;
//     //
//     //     small_strain_J2_plasticity->update_internal_variables(1.0);
//     //
//     //     for (auto const& material_tangent : material_tangents)
//     //     {
//     //         REQUIRE((material_tangent - material_tangent.transpose()).norm() ==
//     //         Approx(0.0).margin(ZERO_MARGIN));
//     //     }
//     //     for (auto& cauchy_stress : cauchy_stresses)
//     //     {
//     //         REQUIRE((cauchy_stress - cauchy_stress.transpose()).norm() ==
//     Approx(0.0).margin(ZERO_MARGIN));
//     //
//     //         // Shear components should be close to zero
//     //         REQUIRE(cauchy_stress(0, 1) == Approx(0.0).margin(ZERO_MARGIN));
//     //         REQUIRE(cauchy_stress(0, 2) == Approx(0.0).margin(ZERO_MARGIN));
//     //         REQUIRE(cauchy_stress(1, 2) == Approx(0.0).margin(ZERO_MARGIN));
//     //         REQUIRE(cauchy_stress(0, 0) > 0.0);
//     //         REQUIRE(cauchy_stress(1, 1) > 0.0);
//     //         REQUIRE(cauchy_stress(2, 2) > 0.0);
//     //     }
//     //     for (auto& von_mises_stress :
//     variables->get(variable::scalar::von_mises_stress))
//     //     {
//     //         REQUIRE(von_mises_stress < 200.0e6);
//     //     }
//     // }
//     // SECTION("Plastic uniaxial elastic load")
//     // {
//     //     for (auto& F : F_list) F(2, 2) = 0.003;
//     //
//     //     small_strain_J2_plasticity->update_internal_variables(1.0);
//     //
//     //     auto[von_mises_stresses, accumulated_plastic_strains] =
//     //     variables->get(variable::scalar::von_mises_stress,
//     // variable::scalar::effective_plastic_strain);
//     //
//     //     // Ensure symmetry is correct
//     //     for (auto const& material_tangent : material_tangents)
//     //     {
//     //         REQUIRE((material_tangent - material_tangent.transpose()).norm() ==
//     //         Approx(0.0).margin(ZERO_MARGIN));
//     //     }
//     //     for (auto& cauchy_stress : cauchy_stresses)
//     //     {
//     //         REQUIRE(cauchy_stress.norm() != Approx(0.0).margin(ZERO_MARGIN));
//     //
//     //         // Shear components should be close to zero
//     //         REQUIRE(cauchy_stress(0, 1) == Approx(0.0).margin(ZERO_MARGIN));
//     //         REQUIRE(cauchy_stress(0, 2) == Approx(0.0).margin(ZERO_MARGIN));
//     //         REQUIRE(cauchy_stress(1, 2) == Approx(0.0).margin(ZERO_MARGIN));
//     //         REQUIRE(cauchy_stress(0, 0) > 0.0);
//     //         REQUIRE(cauchy_stress(1, 1) > 0.0);
//     //         REQUIRE(cauchy_stress(2, 2) > 0.0);
//     //     }
//     //     for (auto& accumulated_plastic_strain : accumulated_plastic_strains)
//     //     {
//     //         REQUIRE(accumulated_plastic_strain > 0.0);
//     //     }
//     //     for (auto& von_mises_stress : von_mises_stresses)
//     //     {
//     //         // Should experience hardening
//     //         REQUIRE(von_mises_stress <= 201.0e6);
//     //         REQUIRE(von_mises_stress > 200.0e6);
//     //     }
//     // }
// }


#include <iostream>
#include <fstream>

#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>

#define USE_PERIODIC_FFT
#include <imgproc/derivative_gradient.hpp>
#include <imgproc/susan.hpp>
#include <imgproc/rcmg.hpp>
#include <imgproc/quadratureG2.hpp>
#include <imgproc/quadratureS.hpp>
#include <imgproc/quadratureSF.hpp>
#include <imgproc/quadratureLGF.hpp>
#include <imgproc/pc_sqf.hpp>
#include <imgproc/pc_lgf.hpp>
#include <imgproc/pc_matlab.hpp>
#include <imgproc/laplace.hpp>
#include <edge/zc.hpp>
#include <edge/nms.hpp>

#include <boost/filesystem.hpp>
#include <boost/algorithm/string.hpp>
#include <boost/format.hpp>

using namespace lsfm;
using namespace std;
namespace fs = boost::filesystem;

constexpr int runs = 1;

constexpr int ENTRY_SQR = 1;
constexpr int ENTRY_RGB = 2;
constexpr int ENTRY_NO_3 = 4;
constexpr int ENTRY_NO_5 = 8;

typedef double FT;

struct Entry {
    Entry() {}

    Entry(const cv::Ptr<FilterI<uchar>>& a, const std::string& b, double l, double h, int f = 0)
        : filter(a), name(b), low(l), high(h), flags(f) {}

    Entry(const cv::Ptr<FilterI<uchar>>& a, const std::string& b, int f = 0)
        : filter(a), name(b), low(0.01), high(0.03), flags(f) {}

    cv::Ptr<FilterI<uchar>> filter;
    std::string name;
    int flags;
    double low, high;

    Value value(const std::string &name) {
        return dynamic_cast<ValueManager*>(&(*filter))->value(name);
    }

    void value(const std::string &name, const Value &v) {
        dynamic_cast<ValueManager*>(&(*filter))->value(name, v);
    }

    inline bool rgb() const {
        return flags & ENTRY_RGB;
    }

    inline bool sqr() const {
        return flags & ENTRY_SQR;
    }

    inline int border() const {
        if (flags & ENTRY_NO_3)
            return 2;
        if (flags & ENTRY_NO_5)
            return 3;
        return 0;
    }

    inline lsfm::FilterResults process(const cv::Mat src) {
        filter->process(src);
        return filter->results();
    }
};

cv::Mat createNMS(const cv::Mat &emap) {

    cv::Mat emapImg;
    emapImg.create(emap.rows, emap.cols, CV_8UC3);

    emapImg.setTo(cv::Vec3b(0, 0, 0));
    emapImg.setTo(cv::Vec3b(220, 150, 255), emap == 7); // magenta2
    emapImg.setTo(cv::Vec3b(255, 0, 150), emap == 6); // lila
    emapImg.setTo(cv::Vec3b(255, 0, 0), emap == 5); // blue
    emapImg.setTo(cv::Vec3b(255, 255, 0), emap == 4); // cyan
    emapImg.setTo(cv::Vec3b(0, 255, 0), emap == 3); // green
    emapImg.setTo(cv::Vec3b(0, 255, 255), emap == 2); // yellow
    emapImg.setTo(cv::Vec3b(0, 150, 255), emap == 1); // orange
    emapImg.setTo(cv::Vec3b(0, 0, 255), emap == 0); // red
    return emapImg;
}

void saveEdge(const cv::Mat &data, const std::string &name) {
    cv::imwrite("./nms/" + name + ".png", createNMS(data));
}

void saveResults(const lsfm::FilterResults &results, const std::string &name, double th_low, double th_high, bool sqr = false, int border = 0) {
    ZeroCrossing<FT, FT, FT, FastZC<FT, FT, FT>> zc;
    NonMaximaSuppression<FT, FT, FT, FastNMS8<FT, FT, FT>> nms;
    lsfm::FilterResults::const_iterator f;
    cv::Mat tmp, tmpx, tmpy;
    double low, high;
    std::cout << name << std::endl;
    f = results.find("laplace");
    if (f != results.end() && results.find("even") == results.end()) {
        std::cout << "laplace range upper: " << f->second.range.size() << std::endl;
        if (f->second.data.type() != cv::DataType<FT>::type)
            f->second.data.convertTo(tmp, cv::DataType<FT>::type);
        else
            f->second.data.copyTo(tmp);
        zc.process(tmp, f->second.range.size() * th_low, f->second.range.size() * th_high);
        saveEdge(zc.hysteresis(), name + "_laplace");
    }

    f = results.find("mag");
    if (f != results.end()) {
        std::cout << "mag range upper: " << f->second.range.upper << std::endl;
        if (f->second.data.type() != cv::DataType<FT>::type)
            f->second.data.convertTo(tmp, cv::DataType<FT>::type);
        else
            f->second.data.copyTo(tmp);
        low = f->second.range.upper * th_low;
        high = f->second.range.upper * th_high;
        f = results.find("gx");
        if (f->second.data.type() != cv::DataType<FT>::type)
            f->second.data.convertTo(tmpx, cv::DataType<FT>::type);
        else
            f->second.data.copyTo(tmpx);
        f = results.find("gy");
        if (f->second.data.type() != cv::DataType<FT>::type)
            f->second.data.convertTo(tmpy, cv::DataType<FT>::type);
        else
            f->second.data.copyTo(tmpy);
        nms.process(tmpx,tmpy,tmp,low,high);
        saveEdge(nms.hysteresis(), name + "_mag");
    }

    f = results.find("even");
    if (f != results.end()) {
        std::cout << "even range upper: " << f->second.range.upper << std::endl;
        if (f->second.data.type() != cv::DataType<FT>::type)
            f->second.data.convertTo(tmp, cv::DataType<FT>::type);
        else
            f->second.data.copyTo(tmp);
        low = f->second.range.size() * th_low;
        high = f->second.range.size() * th_high;
        f = results.find("oddx");
        if (f->second.data.type() != cv::DataType<FT>::type)
            f->second.data.convertTo(tmpx, cv::DataType<FT>::type);
        else
            f->second.data.copyTo(tmpx);
        f = results.find("oddy");
        if (f->second.data.type() != cv::DataType<FT>::type)
            f->second.data.convertTo(tmpy, cv::DataType<FT>::type);
        else
            f->second.data.copyTo(tmpy);
        zc.process(tmpx, tmpy, tmp, low, high);
        saveEdge(zc.hysteresis(), name + "_even");
    }

    f = results.find("odd");
    if (f != results.end()) {
        std::cout << "odd range upper: " << f->second.range.upper << std::endl;
        if (f->second.data.type() != cv::DataType<FT>::type)
            f->second.data.convertTo(tmp, cv::DataType<FT>::type);
        else
            f->second.data.copyTo(tmp);
        low = f->second.range.upper * th_low;
        high = f->second.range.upper * th_high;
        f = results.find("oddx");
        if (f->second.data.type() != cv::DataType<FT>::type)
            f->second.data.convertTo(tmpx, cv::DataType<FT>::type);
        else
            f->second.data.copyTo(tmpx);
        f = results.find("oddy");
        if (f->second.data.type() != cv::DataType<FT>::type)
            f->second.data.convertTo(tmpy, cv::DataType<FT>::type);
        else
            f->second.data.copyTo(tmpy);
        nms.process(tmpx, tmpy, tmp, low, high);
        saveEdge(nms.hysteresis(), name + "_odd");
    }

    f = results.find("energy");
    if (f != results.end()) {
        std::cout << "energy range upper: " << f->second.range.upper << std::endl;
        if (f->second.data.type() != cv::DataType<FT>::type)
            f->second.data.convertTo(tmp, cv::DataType<FT>::type);
        else
            f->second.data.copyTo(tmp);
        low = f->second.range.upper * th_low;
        high = f->second.range.upper * th_high;
        f = results.find("oddx");
        if (f->second.data.type() != cv::DataType<FT>::type)
            f->second.data.convertTo(tmpx, cv::DataType<FT>::type);
        else
            f->second.data.copyTo(tmpx);
        f = results.find("oddy");
        if (f->second.data.type() != cv::DataType<FT>::type)
            f->second.data.convertTo(tmpy, cv::DataType<FT>::type);
        else
            f->second.data.copyTo(tmpy);
        nms.process(tmpx, tmpy, tmp, low, high);
        saveEdge(nms.hysteresis(), name + "_energy");
    }

    f = results.find("pc");
    if (f != results.end()) {
        std::cout << "pc range upper: " << f->second.range.upper << std::endl;
        if (f->second.data.type() != cv::DataType<FT>::type)
            f->second.data.convertTo(tmp, cv::DataType<FT>::type);
        else
            f->second.data.copyTo(tmp);
        low = f->second.range.upper * th_low;
        high = f->second.range.upper * th_high;
        f = results.find("oddx");
        if (f->second.data.type() != cv::DataType<FT>::type)
            f->second.data.convertTo(tmpx, cv::DataType<FT>::type);
        else
            f->second.data.copyTo(tmpx);
        f = results.find("oddy");
        if (f->second.data.type() != cv::DataType<FT>::type)
            f->second.data.convertTo(tmpy, cv::DataType<FT>::type);
        else
            f->second.data.copyTo(tmpy);
        nms.process(tmpx, tmpy, tmp, low, high);
        saveEdge(nms.hysteresis(), name + "_pc");
    }

    f = results.find("pclx");
    if (f != results.end()) {
        std::cout << "pcl range upper: " << f->second.range.upper << std::endl;
        if (f->second.data.type() != cv::DataType<FT>::type)
            f->second.data.convertTo(tmp, cv::DataType<FT>::type);
        else
            f->second.data.copyTo(tmp);
        zc.process(tmp, f->second.range.size() * th_low * th_low, f->second.range.size() * th_high * th_high);
        saveEdge(zc.hysteresis(), name + "_pclx");
        f = results.find("pcly");
        if (f->second.data.type() != cv::DataType<FT>::type)
            f->second.data.convertTo(tmp, cv::DataType<FT>::type);
        else
            f->second.data.copyTo(tmp);
        zc.process(tmp, f->second.range.size() * th_low * th_low, f->second.range.size() * th_high * th_high);
        saveEdge(zc.hysteresis(), name + "_pcly");
    }

}

void parseFolder(const fs::path &folder, std::vector<fs::path> &files) {
    fs::directory_iterator end_iter;
    for_each(fs::directory_iterator(folder), fs::directory_iterator(), [&files](const fs::path& file) {
        if (fs::is_regular_file(file))
        {
            std::string ext = file.extension().generic_string();
            boost::algorithm::to_lower(ext);
            if (ext == ".jpg" || ext == ".png") {
                files.push_back(file);
            }
        }
        if (fs::is_directory(file))
            parseFolder(file, files);
    });
}

void processPath(std::vector<Entry> &entries, const std::pair<fs::path, std::string> &path) {
    std::cout << "processing " << path.first << std::endl;
    std::vector<fs::path> files;
    parseFolder(path.first, files);

    std::for_each(files.begin(), files.end(), [&](const fs::path& file) {
        cv::Mat rgb = cv::imread(file.generic_string());
        if (rgb.empty())
        {
            cout << "Can not open " << file.generic_string() << endl;
            return;
        }
        cv::GaussianBlur(rgb, rgb, cv::Size(3, 3), 0.6);
        std::cout << file << std::endl;
        cv::Mat src;
        cv::cvtColor(rgb, src, CV_BGR2GRAY);

        for_each(entries.begin(), entries.end(), [&](Entry &e) {
            saveResults(e.process(e.rgb() ? rgb : src), path.second + "/" + file.stem().generic_string() + "_" + e.name,e.low, e.high, e.sqr(),e.border());
        });
    });
}

int main(int argc, char** argv)
{
    std::vector<std::pair<fs::path, std::string>> sets;
    //sets.push_back(std::pair<fs::path, std::string>("../../images/noise", "noise"));
    sets.push_back(std::pair<fs::path, std::string>("../../images/Selection", "Selection"));
    //sets.push_back(std::pair<fs::path, std::string>("../../images/BSDS500", "BSDS500"));
    //sets.push_back(std::pair<fs::path, std::string>("../../images/MDB/MiddEval3-Q", "MDB-Q"));
    //sets.push_back(std::pair<fs::path, std::string>("../../images/MDB/MiddEval3-H", "MDB-H"));
    //sets.push_back(std::pair<fs::path, std::string>("../../images/MDB/MiddEval3-F", "MDB-F"));

    std::vector<Entry> filter;
    filter.push_back(Entry(new DerivativeGradient<uchar,short, FT, FT, RobertsDerivative>,"Roberts_(2x2)"));
    filter.push_back(Entry(new DerivativeGradient<uchar, short,FT, FT, PrewittDerivative>, "Prewitt_(3x3)"));
    filter.push_back(Entry(new DerivativeGradient<uchar, short, FT, FT, ScharrDerivative>, "Scharr_(3x3)"));

    filter.push_back(Entry(new DerivativeGradient<uchar, short,FT, FT, SobelDerivative>, "Sobel_(3x3)"));
    filter.push_back(Entry(new DerivativeGradient<uchar, short,FT, FT, SobelDerivative>({ NV("grad_kernel_size",5) }), "Sobel_(5x5)"));
    filter.push_back(Entry(new DerivativeGradient<uchar, FT,FT, FT, SobelDerivative>({ NV("grad_kernel_size",7) }), "Sobel_(7x7)"));
    filter.push_back(Entry(new DerivativeGradient<uchar, FT,FT, FT, SobelDerivative>({ NV("grad_kernel_size",9) }), "Sobel_(9x9)"));

    filter.push_back(Entry(new DerivativeGradient<uchar, FT, FT, FT, GaussianDerivative, Magnitude>({ NV("grad_kernel_size",3), NV("grad_range",1.5) }), "Gauss_(3x3)"));
    filter.push_back(Entry(new DerivativeGradient<uchar, FT, FT, FT, GaussianDerivative, Magnitude>({ NV("grad_kernel_size",5), NV("grad_range",2.3) }), "Gauss_(5x5)"));
    filter.push_back(Entry(new DerivativeGradient<uchar, FT, FT, FT, GaussianDerivative, Magnitude>({ NV("grad_kernel_size",7), NV("grad_range",3.0) }), "Gauss_(7x7)"));
    filter.push_back(Entry(new DerivativeGradient<uchar, FT, FT, FT, GaussianDerivative, Magnitude>({ NV("grad_kernel_size",9), NV("grad_range",3.5) }), "Gauss_(9x9)"));

    filter.push_back(Entry(new SusanGradient<short, FT,FT>(20), "Susan_(37)"));
    filter.push_back(Entry(new SusanGradient<short, FT,FT>(20,true), "Susan_(3x3)"));
    filter.push_back(Entry(new RCMGradient<uchar, 1, short, FT,FT>(3, 1, cv::NORM_L2), "RMG_(3x3)", ENTRY_NO_3));
    filter.push_back(Entry(new RCMGradient<uchar, 1, short, FT,FT>(5, 3, cv::NORM_L2), "RMG_(5x5)", ENTRY_NO_5));
    filter.push_back(Entry(new RCMGradient<uchar, 3, short, FT,FT>(3, 1, cv::NORM_L2), "RCMG_(3x3)", ENTRY_NO_3 | ENTRY_RGB));

    filter.push_back(Entry(new LaplaceSimple<uchar, short>, "Laplace_(3x3)"));
    filter.push_back(Entry(new LaplaceCV<uchar, short>(5), "Laplace_Sobel_(5x5)"));
    filter.push_back(Entry(new LaplaceCV<uchar, short>(7), "Laplace_Sobel_(7x7)"));
    filter.push_back(Entry(new LaplaceCV<uchar, FT>(9), "Laplace_Sobel_(9x9)"));

    filter.push_back(Entry(new LoG<uchar, FT>(3, 1.240080), "LoG_(3x3)"));
    filter.push_back(Entry(new LoG<uchar, FT>(5, 1.008030), "LoG_(5x5)"));
    filter.push_back(Entry(new LoG<uchar, FT>(7, 0.873228), "LoG_(7x7)"));
    filter.push_back(Entry(new LoG<uchar, FT>(9, 0.781859), "LoG_(9x9)"));

    filter.push_back(Entry(new QuadratureG2<uchar, FT>(3, 1.24008), "QF_StG_(3x3)"));
    filter.push_back(Entry(new QuadratureG2<uchar, FT>(5, 1.008),   "QF_StG_(5x5)"));
    filter.push_back(Entry(new QuadratureG2<uchar, FT>(7, 0.873226), "QF_StG_(7x7)"));
    filter.push_back(Entry(new QuadratureG2<uchar, FT>(9, 0.781854), "QF_StG_(9x9)"));

    /*filter.push_back(Entry(new QuadratureS<uchar, FT, FT>(1, 2, 3, 1.2), "SQF_Po_(3x3)"));
    filter.push_back(Entry(new QuadratureS<uchar, FT, FT>(1, 2, 5, 1.2), "SQF_Po_(5x5)"));
    filter.push_back(Entry(new QuadratureS<uchar, FT, FT>(1, 2, 7, 1.2), "SQF_Po_(7x7)"));
    filter.push_back(Entry(new QuadratureS<uchar, FT, FT>(1, 2, 9, 1.2), "SQF_Po_(9x9)"));*/

    filter.push_back(Entry(dynamic_cast<Quadrature<uchar, FT, FT, FT, FT>*>(new PCLSq<uchar, FT, FT>(1, 2, 3, 1.2)), "SQF_Po_(3x3)"));
    filter.push_back(Entry(dynamic_cast<Quadrature<uchar, FT, FT, FT, FT>*>(new PCLSq<uchar, FT, FT>(1, 2, 5, 1.2)), "SQF_Po_(5x5)"));
    filter.push_back(Entry(dynamic_cast<Quadrature<uchar, FT, FT, FT, FT>*>(new PCLSq<uchar, FT, FT>(1, 2, 7, 1.2)), "SQF_Po_(7x7)"));
    filter.push_back(Entry(dynamic_cast<Quadrature<uchar, FT, FT, FT, FT>*>(new PCLSq<uchar, FT, FT>(1, 2, 9, 1.2)), "SQF_Po_(9x9)"));

    //filter.push_back(Entry(new QuadratureSF<uchar, FT>(1, 2, 1.2), "SQFF_Po"));
    filter.push_back(Entry(dynamic_cast<Quadrature<uchar,FT,FT,FT, FT>*>(new PCLSqf<uchar, FT>(1, 2, 1.2)), "SQFF_Po_12"));
    filter.push_back(Entry(dynamic_cast<Quadrature<uchar, FT, FT, FT, FT>*>(new PCLSqf<uchar, FT>(1, 3, 2.5)), "SQFF_Po"));
    filter.push_back(Entry(new QuadratureLGF<uchar, FT>(5, 0.55), "SQFF_Lg"));

    filter.push_back(Entry(new PCLgf<uchar, FT>(4, 3,2.1,0.55), "PCF_Lg"));
    filter.push_back(Entry(new PCMatlab<uchar>(4, 3, 2.1, 0.55), "PCF_Ml"));
    filter.push_back(Entry(new PCSqf<uchar, FT>(1,3,2.5), "PCF_Po"));

    fs::create_directory("./nms");
    for_each(sets.begin(), sets.end(), [&](const std::pair<fs::path, std::string> &data) {
        fs::create_directory("./nms/" + data.second);
        processPath(filter, data);
    });

    return 0;
}


///////////////////////////////////////////////////////////////////////////////
// distribution::survival::failure_time::meta::distribution.hpp       		 //
//                                                                           //
//  Copyright 2009 Erwann Rogard. Distributed under the Boost                //
//  Software License, Version 1.0. (See accompanying file                    //
//  LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)         //
///////////////////////////////////////////////////////////////////////////////
#ifndef BOOST_STATISTICS_DETAIL_DISTRIBUTION_SURVIVAL_FAILURE_TIME_META_DISTRIBUTION_HPP_ER_2009
#define BOOST_STATISTICS_DETAIL_DISTRIBUTION_SURVIVAL_FAILURE_TIME_META_DISTRIBUTION_HPP_ER_2009
#include <boost/mpl/bool.hpp>
#include <boost/utility/enable_if.hpp>
#include <boost/statistics/detail/distribution/model/tag/covariate.hpp>
#include <boost/statistics/detail/distribution/model/tag/model.hpp>
#include <boost/statistics/detail/distribution/survival/failure_time/meta/detail/traits.hpp>

namespace boost {
namespace statistics{
namespace detail{
namespace distribution{
namespace survival {
namespace failure_time{

    template<typename Model>
    struct distribution{
        // Specialize for each model. See below.
    };

    template<typename X,typename Model>
	typename distribution<Model>::type
    make_distribution(const X& x,const Model& m)
    {
        typedef failure_time::distribution<Model> meta_;
        return meta_::call(x,m);
    }

	template<typename Map>
	typename distribution<
		typename failure_time::detail::traits<Map>::model_type
    >::type
    make_distribution(const Map& map)
    {
    	typedef model::tag::covariate tag_x_;
    	typedef model::tag::model tag_m_;
        return make_distribution(
        	boost::fusion::at_key<tag_x_>(map),
        	boost::fusion::at_key<tag_m_>(map)
        );
    }

}// failure_time
}// survival
}// distribution
}// detail
}// statistics
}// boost

#endif


//          Copyright Gavin Band 2008 - 2012.
// Distributed under the Boost Software License, Version 1.0.
//    (See accompanying file LICENSE_1_0.txt or copy at
//          http://www.boost.org/LICENSE_1_0.txt)

#include <vector>
#include <memory>
#include <iostream>
#include <iomanip>
#include <boost/noncopyable.hpp>
#include <boost/math/special_functions/gamma.hpp>
#include "Eigen/Core"
#include "Eigen/LU"
#include "metro/regression/Design.hpp"
#include "metro/regression/LogLikelihood.hpp"
#include "metro/regression/IndependentLogFWeightedLogLikelihood.hpp"

// #define DEBUG_NORMALWEIGHTEDLOGLIKELIHOOD 1

namespace metro {
	namespace regression {
		/*
		logF density
		this can be computed in R as follows:

		logistic <- function(x) { exp(x) / (1 + exp(x) )}
		dlogf <- function( x, nu1 = 1, nu2 = 1, normalisation = "zeroatmean" ) {
			alpha = nu1/2
			beta = nu2/2
			l = logistic((x))
			constant = 0
			if( normalisation == "zeroatmean" ) {
				constant = -(alpha+beta) * log(0.5)
			} else if( normalisation == "density" ) {
				constant = -lbeta( alpha, beta )
			} else {
				constant = 0
			}
			constant + l^alpha * ( 1-l)^ beta
		}
		dlogf.v2 <- function( x, nu1 = 1, nu2 = 1 ) {
			dbeta( logistic(x), shape1 = nu1/2, shape2 = nu2/2 )
		}
		*/
		IndependentLogFWeightedLogLikelihood::UniquePtr IndependentLogFWeightedLogLikelihood::create(
			LogLikelihood::UniquePtr ll,
			std::vector< int > parameter_indices,
			std::vector< double > nu1,
			std::vector< double > nu2
		) {
			return UniquePtr( new IndependentLogFWeightedLogLikelihood( ll, parameter_indices, nu1, nu2 ) ) ;
		}

		IndependentLogFWeightedLogLikelihood::IndependentLogFWeightedLogLikelihood(
			LogLikelihood::UniquePtr ll,
			std::vector< int > parameter_indices,
			std::vector< double > nu1,
			std::vector< double > nu2
		):
			m_ll( ll ),
			m_parameter_indices( parameter_indices ),
			m_alpha( nu1 ),
			m_beta( nu2 ),
			m_normalisation( eZeroAtMean )
		{
			assert( m_alpha.size() == m_parameter_indices.size() ) ;
			assert( m_beta.size() == m_parameter_indices.size() ) ;
			for( std::size_t i = 0; i < m_alpha.size(); ++i ) {
				m_alpha[i] = m_alpha[i]/2.0 ;
				m_beta[i] = m_beta[i]/2.0 ;
			}
			m_constant = 0.0 ;
			if( m_normalisation == eZeroAtMean ) {
				// Normalise so the log-likelihood value at zero is zero.
				// ll computes as log(0.5)*(alpha+beta) for each parameter.
				m_constant = 0.0  ;
				for( std::size_t i = 0; i < m_alpha.size(); ++i ) {
					m_constant -= (m_alpha[i] + m_beta[i]) * std::log(0.5) ;
				}
			} else if( m_normalisation == ePDF ) {
				assert(0) ; // this might not be correct yet
				using boost::math::lgamma ;
				for( std::size_t i = 0; i < m_alpha.size(); ++i ) {
					m_constant -= lgamma(m_alpha[i]) + lgamma(m_beta[i]) - lgamma(m_alpha[i] + m_beta[i] ) ;
				}
			} else {
				assert(0) ;
			}
		}

		std::string IndependentLogFWeightedLogLikelihood::get_parameter_name( std::size_t i ) const {
			return m_ll->get_parameter_name(i) ;
		}
		IndependentLogFWeightedLogLikelihood::IntegerMatrix IndependentLogFWeightedLogLikelihood::identify_parameters() const {
			return m_ll->identify_parameters() ;
		}

		int IndependentLogFWeightedLogLikelihood::number_of_parameters() const {
			return m_ll->number_of_parameters() ;
		}

		int IndependentLogFWeightedLogLikelihood::number_of_outcomes() const {
			return m_ll->number_of_outcomes() ;
		}

		void IndependentLogFWeightedLogLikelihood::evaluate_at( Point const& parameters, int const numberOfDerivatives ) {
			m_ll->evaluate_at( parameters, numberOfDerivatives ) ;
			evaluate_impl( numberOfDerivatives ) ;
		}

		void IndependentLogFWeightedLogLikelihood::evaluate( int const numberOfDerivatives ) {
			m_ll->evaluate( numberOfDerivatives ) ;
			evaluate_impl( numberOfDerivatives ) ;
		}

		void IndependentLogFWeightedLogLikelihood::evaluate_impl( int const numberOfDerivatives ) {
			Vector const& parameters = m_ll->parameters() ;
			m_value_of_function = m_constant ;
			m_value_of_first_derivative.setZero( parameters.size() ) ;
			m_value_of_second_derivative.setZero( parameters.size(), parameters.size() ) ;

			for( std::size_t i = 0; i < m_parameter_indices.size(); ++i ) {
				int const& parameter_index = m_parameter_indices[i] ;
				assert( parameter_index >= 0 && parameter_index < parameters.size() ) ;
				double const x = parameters( parameter_index ) ;
				// logistic function evaluated at this value.
				double const l = 1.0 / (1.0 + std::exp(-x)) ;
				m_value_of_function += ( m_alpha[i] * std::log(l) + m_beta[i] * std::log( 1-l ) );
				m_value_of_first_derivative( parameter_index ) = m_alpha[i] - (m_alpha[i] + m_beta[i]) * l ;
				m_value_of_second_derivative( parameter_index, parameter_index ) = -(m_alpha[i] + m_beta[i]) * l * (1-l) ;
			}
		}

		IndependentLogFWeightedLogLikelihood::Vector IndependentLogFWeightedLogLikelihood::get_prior_mode() const {
			Vector result = Vector::Zero( m_value_of_first_derivative.size() ) ;
			for( std::size_t i = 0; i < m_parameter_indices.size(); ++i ) {
				// mode is logit() of mode of beta distribution with shape1=1+alpha, shape2=1+beta
				// this is given by:
				double mode = m_alpha[i] / ( m_alpha[i] + m_beta[i] ) ;
				result[i] = std::log( mode / (1.0 - mode)) ;
			}
			return result ;
		}

		std::string IndependentLogFWeightedLogLikelihood::get_summary() const {
			std::ostringstream ostr ;
			ostr << m_ll->get_summary() << "\n" ;
			ostr << "  with priors:\n" ;
			std::size_t max_label_length = 0 ;
			int const numberOfParameters = m_ll->identify_parameters().rows() ;
			for( int i = 0; i < numberOfParameters; ++i ) {
				max_label_length = std::max( max_label_length, m_ll->get_parameter_name(i).size() ) ;
			}

			for( std::size_t i = 0; i < m_parameter_indices.size(); ++i ) {
				int const parameter_index = m_parameter_indices[i] ;
				ostr
					<< "  "
					<< std::setw( max_label_length)
					<< m_ll->get_parameter_name(parameter_index)
					<< " ~ logF( "
					<< (m_alpha[i]*2) << ", " << (m_beta[i]*2)
					<< " ).\n" ;
			}
#if 0

			ostr
				<< std::setw(3) << "" << "  "
				<< std::setw( max_label_length + 2 ) << ""
				<< ":"
				<< " " << std::setw(5) << "alpha"
				<< " " << std::setw(5) << "beta"
				<< "\n" ;
			for( std::size_t i = 0; i < m_parameter_indices.size(); ++i ) {
				int const parameter_index = m_parameter_indices[i] ;
				ostr << std::setw(3) << parameter_index
					<< ": " << std::setw( max_label_length + 2 ) << m_ll->get_parameter_name(parameter_index) << ":" ;
					ostr
						<< " " << std::setw(5) << m_alpha[i]
						<< " " << std::setw(5) << m_beta[i]
						<< "\n" ;
			}
#endif
			return ostr.str() ;
		}
	}
}