// Copyright (c) 2010-2023, Lawrence Livermore National Security, LLC. Produced
// at the Lawrence Livermore National Laboratory. All Rights reserved. See files
// LICENSE and NOTICE for details. LLNL-CODE-806117.
//
// This file is part of the MFEM library. For more information and source code
// availability visit https://mfem.org.
//
// MFEM is free software; you can redistribute it and/or modify it under the
// terms of the BSD-3 license. We welcome feedback and contributions, see file
// CONTRIBUTING.md for details.
//
//            --------------------------------------------------------
//            cvsRoberts-ASAi-DNS Miniapp:  Serial MFEM CVODES Example
//            --------------------------------------------------------
//
// Compile with: make cvsRoberts_ASAi_dns
//
// Sample runs:  cvsRoberts_ASAi_dns -dt 0.01
//               cvsRoberts_ASAi_dns -dt 0.005
//
// Description:  This example is a port of cvodes/serial/cvsRoberts_ASAi_dns
//               example that is part of SUNDIALS. The goal is to demonstrate
//               how to use the adjoint SUNDIALS CVODES interface from MFEM.
//               Below is an excerpt description from the aforementioned file.
//
// Adjoint sensitivity example problem:
//
// The following is a simple example problem, with the coding needed for its
// solution by CVODES. The problem is from chemical kinetics, and consists of
// the following three rate equations
//
//     dy1/dt = -p1*y1 + p2*y2*y3
//     dy2/dt =  p1*y1 - p2*y2*y3 - p3*(y2)^2
//     dy3/dt =  p3*(y2)^2
//
// on the interval from t = 0.0 to t = 4e10, with initial conditions: y1 = 1.0,
// y2 = y3 = 0. The reaction rates are: p1=0.04, p2=1e4, and p3=3e7. The problem
// is stiff. This program solves the problem with the BDF method, Newton
// iteration with the DENSE linear solver, and a user-supplied Jacobian routine.
// It uses a scalar relative tolerance and a vector absolute tolerance. Output
// is printed in decades from t = 0.4 to t = 4e10. Run statistics (optional
// outputs) are printed at the end.
//
//  Optionally, CVODES can compute sensitivities with respect to the problem
//  parameters p1, p2, and p3 of the following quantity:
//
//    G = int_t0^t1 g(t,p,y) dt
//
//  where g(t,p,y) = y3.
//
//  The gradient dG/dp is obtained as:
//
//    dG/dp = int_t0^t1 (g_p - lambda^T f_p ) dt - lambda^T(t0)*y0_p
//          = - xi^T(t0) - lambda^T(t0)*y0_p
//
//  where lambda and xi are solutions of:
//
//    d(lambda)/dt = - (f_y)^T * lambda - (g_y)^T
//    lambda(t1) = 0
//
//  and
//
//    d(xi)/dt = - (f_p)^T * lambda + (g_p)^T
//    xi(t1) = 0
//
//  During the backward integration, CVODES also evaluates G as
//
//    G = - phi(t0)
//
//  where d(phi)/dt = g(t,y,p), phi(t1) = 0.

#include "mfem.hpp"
#include <fstream>
#include <iostream>
#include <algorithm>

#ifndef MFEM_USE_SUNDIALS
#error This example requires that MFEM is built with MFEM_USE_SUNDIALS=YES
#endif

using namespace std;
using namespace mfem;


// We create a TimeDependentAdjointOperator implementation of the rate equations
// to recreate the cvsRoberts_ASAi_dns problem
class RobertsTDAOperator : public TimeDependentAdjointOperator
{
public:
   RobertsTDAOperator(int dim, Vector p) :
      TimeDependentAdjointOperator(dim, 3),
      p_(p),
      adjointMatrix(NULL)
   {}

   // Rate equation for forward problem
   virtual void Mult(const Vector &x, Vector &y) const;

   // Quadrature integration for G
   virtual void QuadratureIntegration(const Vector &x, Vector &y) const;

   // Adjoint rate equation corresponding to d(lambda)/dt
   virtual void AdjointRateMult(const Vector &y, Vector &yB,
                                Vector &yBdot) const;

   // Quadrature sensitivity equations corresponding to dG/dp
   virtual void QuadratureSensitivityMult(const Vector &y, const Vector &yB,
                                          Vector &qbdot) const;

   // Setup custom MFEM solvers using GMRES since the Jacobian matrix is not
   // symmetric
   virtual int SUNImplicitSetupB(const double t, const Vector &y,
                                 const Vector &yB, const Vector &fyB, int jokB,
                                 int *jcurB, double gammaB);

   // Setup custom MFEM solve
   virtual int SUNImplicitSolveB(Vector &x, const Vector &b, double tol);

   ~RobertsTDAOperator()
   {
      delete adjointMatrix;
   }

protected:
   Vector p_;

   // Solvers
   GMRESSolver adjointSolver;
   SparseMatrix* adjointMatrix;
};

int main(int argc, char *argv[])
{
   // Parse command-line options.
   double t_final = 4e7;
   double dt = 0.01;

   // Relative tolerance for CVODES.
   const double reltol = 1e-4;

   int precision = 8;
   cout.precision(precision);

   OptionsParser args(argc, argv);
   args.AddOption(&dt, "-dt", "--time-step", "Time step.");

   args.Parse();
   if (!args.Good())
   {
      args.PrintUsage(cout);
      return 1;
   }
   args.PrintOptions(cout);

   // The original cvsRoberts_ASAi_dns problem is a fixed sized problem of size
   // 3. Define the solution vector.
   Vector u(3);
   u = 0.;
   u[0] = 1.;

   // Define the TimeDependentAdjointOperator which implements the various rate
   // equations: rate, quadrature rate, adjoint rate, and quadrature sensitivity
   // rate equation

   // Define material parameters p
   Vector p(3);
   p[0] = 0.04;
   p[1] = 1.0e4;
   p[2] = 3.0e7;

   // 3 is the size of the adjoint solution vector
   RobertsTDAOperator adv(3, p);

   // Set the initial time
   double t = 0.0;
   adv.SetTime(t);

   // Create the CVODES solver and set the various tolerances. Set absolute
   // tolerances for the solution
   Vector abstol_v(3);
   abstol_v[0] = 1.0e-8;
   abstol_v[1] = 1.0e-14;
   abstol_v[2] = 1.0e-6;

   // Initialize the quadrature result
   Vector q(1);
   q = 0.;

   // Create the solver
   CVODESSolver *cvodes = new CVODESSolver(CV_BDF);

   // Initialize the forward problem (this must be done first)
   cvodes->Init(adv);

   // Set error control function
   cvodes->SetWFTolerances([reltol, abstol_v]
                           (Vector y, Vector w, CVODESolver * self)
   {
      for (int i = 0; i < y.Size(); i++)
      {
         double ww = reltol * abs(y[i]) + abstol_v[i];
         if (ww <= 0.) { return -1; }
         w[i] = 1./ww;
      }
      return 0;
   }
                          );

   // Set weighted tolerances
   cvodes->SetSVtolerances(reltol, abstol_v);

   // Use the builtin sundials solver for the forward solver
   cvodes->UseSundialsLinearSolver();

   // Initialize the quadrature integration and set the tolerances
   cvodes->InitQuadIntegration(q, 1.e-6, 1.e-6);

   // Initialize the adjoint solve
   cvodes->InitAdjointSolve(150, CV_HERMITE);

   // Perform time-integration (looping over the time iterations, ti, with a
   // time-step dt).
   bool done = false;
   for (int ti = 0; !done; )
   {
      double dt_real = max(dt, t_final - t);
      cvodes->Step(u, t, dt_real);
      ti++;

      done = (t >= t_final - 1e-8*dt);

      if (done)
      {
         cvodes->PrintInfo();
      }
   }

   cout << "Final Solution: " << t << endl;
   u.Print();

   q = 0.;
   cout << " Final Quadrature " << endl;
   cvodes->EvalQuadIntegration(t, q);
   q.Print();

   // Solve the adjoint problem at different points in time. Create the adjoint
   // solution vector
   Vector w(3);
   w=0.;
   double TBout1 = 40.;
   Vector dG_dp(3);
   dG_dp=0.;
   if (cvodes)
   {
      t = t_final;
      adv.SetTime(t);
      cvodes->InitB(adv);
      cvodes->InitQuadIntegrationB(dG_dp, 1.e-6, 1.e-6);
      cvodes->SetMaxNStepsB(5000);

      // Results at time TBout1
      double dt_real = max(dt, t - TBout1);
      cvodes->StepB(w, t, dt_real);
      cout << "t: " << t << endl;
      cout << "w:" << endl;
      w.Print();

      cvodes->GetForwardSolution(t, u);
      cout << "u:" << endl;
      u.Print();

      // Results at T0
      dt_real = max(dt, t - 0.);
      cvodes->StepB(w, t, dt_real);
      cout << "t: " << t << endl;
      cout << "w:" << endl;

      cvodes->GetForwardSolution(t, u);
      w.Print();
      cout << "u:" << endl;
      u.Print();

      // Evaluate Sensitivity
      cvodes->EvalQuadIntegrationB(t, dG_dp);
      cout << "dG/dp:" << endl;
      dG_dp.Print();

   }

   // Free the used memory.
   delete cvodes;

   return 0;
}

// cvsRoberts_ASAi_dns rate equation
void RobertsTDAOperator::Mult(const Vector &x, Vector &y) const
{
   y[0] = -p_[0]*x[0] + p_[1]*x[1]*x[2];
   y[2] = p_[2]*x[1]*x[1];
   y[1] = -y[0] - y[2];
}

// cvsRoberts_ASAi_dns quadrature rate equation
void RobertsTDAOperator::QuadratureIntegration(const Vector &y,
                                               Vector &qdot) const
{
   qdot[0] = y[2];
}

// cvsRoberts_ASAi_dns adjoint rate equation
void RobertsTDAOperator::AdjointRateMult(const Vector &y, Vector & yB,
                                         Vector &yBdot) const
{
   double l21 = (yB[1]-yB[0]);
   double l32 = (yB[2]-yB[1]);
   double p1 = p_[0];
   double p2 = p_[1];
   double p3 = p_[2];
   yBdot[0] = -p1 * l21;
   yBdot[1] = p2 * y[2] * l21 - 2. * p3 * y[1] * l32;
   yBdot[2] = p2 * y[1] * l21 - 1.0;
}

// cvsRoberts_ASAi_dns quadrature sensitivity rate equation
void RobertsTDAOperator::QuadratureSensitivityMult(const Vector &y,
                                                   const Vector &yB,
                                                   Vector &qBdot) const
{
   double l21 = (yB[1]-yB[0]);
   double l32 = (yB[2]-yB[1]);
   double y23 = y[1] * y[2];

   qBdot[0] = y[0] * l21;
   qBdot[1] = -y23 * l21;
   qBdot[2] = y[1]*y[1]*l32;
}

// cvsRoberts_ASAi_dns implicit solve setup for adjoint
int RobertsTDAOperator::SUNImplicitSetupB(const double t, const Vector &y,
                                          const Vector &yB,
                                          const Vector &fyB, int jokB,
                                          int *jcurB, double gammaB)
{
   // M = I- gamma J
   // J = dfB/dyB
   delete adjointMatrix;
   adjointMatrix = new SparseMatrix(y.Size(), yB.Size());
   for (int j = 0; j < y.Size(); j++)
   {
      Vector JacBj(yB.Size());
      Vector yBone(yB.Size());
      yBone = 0.;
      yBone[j] = 1.;
      AdjointRateMult(y, yBone, JacBj);
      JacBj[2] += 1.;
      for (int i = 0; i < y.Size(); i++)
      {
         adjointMatrix->Set(i,j, (i == j ? 1.0 : 0.) - gammaB * JacBj[i]);
      }
   }

   *jcurB = 1;
   adjointMatrix->Finalize();
   adjointSolver.SetOperator(*adjointMatrix);

   return 0;
}

// cvsRoberts_ASAi_dns implicit solve for adjoint
int RobertsTDAOperator::SUNImplicitSolveB(Vector &x, const Vector &b,
                                          double tol)
{
   // The argument "tol" is ignored in this implementation for simplicity.
   adjointSolver.SetRelTol(1e-14);
   adjointSolver.Mult(b, x);
   return (0);
}
