//$Id: expansion_Jacobian.C,v 1.1 2012/04/03 10:49:51 zjcao Exp $

#include "macrodef.h"
#ifdef With_AHF

#include <stdio.h>
#include <assert.h>
#include <math.h>

#include "util_Table.h"
#include "cctk.h"

#include "config.h"
#include "stdc.h"
#include "util.h"
#include "array.h"
#include "cpm_map.h"
#include "linear_map.h"

#include "coords.h"
#include "tgrid.h"
#include "fd_grid.h"
#include "patch.h"
#include "patch_edge.h"
#include "patch_interp.h"
#include "ghost_zone.h"
#include "patch_system.h"

#include "Jacobian.h"

#include "gfns.h"
#include "gr.h"

namespace AHFinderDirect
{
	using jtutil::error_exit;

	namespace
	{

		void expansion_Jacobian_partial_SD(patch_system &ps, Jacobian &Jac,
										   bool print_msg_flag);

		void add_ghost_zone_Jacobian(const patch_system &ps,
									 Jacobian &Jac,
									 fp mol,
									 const patch &xp, const ghost_zone &xmgz,
									 int x_II,
									 int xm_irho, int xm_isigma);

		enum expansion_status
		expansion_Jacobian_dr_FD(patch_system *ps_ptr, Jacobian *Jac_ptr, fp add_to_expansion,
								 bool initial_flag,
								 bool print_msg_flag);
	}

	//******************************************************************************

	//
	// If ps_ptr != NULL and Jac_ptr != NULL, this function computes the
	// Jacobian matrix J[Theta(h)] of the expansion Theta(h).  We assume
	// that Theta(h) has already been computed.
	//
	// If ps_ptr == NULL and Jac_ptr == NULL, this function does a dummy
	// computation, in which only any expansion() (and hence geometry
	// interpolator) calls are done, these with the number of interpolation
	// points set to 0 and all the output array pointers set to NULL.
	//
	// It's illegal for one but not both of ps_ptr and Jac_ptr to be NULL.
	//
	// Arguments:
	// ps_ptr --> The patch system, or == NULL to do (only) a dummy computation.
	// Jac_ptr --> The Jacobian, or == NULL to do (only) a dummy computation.
	// add_to_expansion = A real number to add to the expansion.
	//
	// Results:
	// This function returns a status code indicating whether the computation
	// succeeded or failed, and if the latter, what caused the failure.
	//
	enum expansion_status
	expansion_Jacobian(patch_system *ps_ptr, Jacobian *Jac_ptr,
					   fp add_to_expansion,
					   bool initial_flag,
					   bool print_msg_flag /* = false */)
	{
		const bool active_flag = (ps_ptr != NULL) && (Jac_ptr != NULL);
		enum expansion_status status;

		if (active_flag)
			then expansion_Jacobian_partial_SD(*ps_ptr, *Jac_ptr,
											   print_msg_flag);
		// this function looks at ps_ptr and Jac_ptr (non-NULL vs NULL)
		// to choose a normal vs dummy computation
		{
			status = expansion_Jacobian_dr_FD(ps_ptr, Jac_ptr, add_to_expansion,
											  initial_flag,
											  print_msg_flag);
			if (status != expansion_success)
				then return status; // *** ERROR RETURN ***
		}

		return expansion_success; // *** NORMAL RETURN ***
	}
	//
	// This function computes the partial derivative terms in the Jacobian
	// matrix of the expansion Theta(h), by symbolic differentiation from
	// the Jacobian coefficient (angular) gridfns.  The Jacobian is traversed
	// by rows, using equation (25) of my 1996 apparent horizon finding paper.
	//
	// Inputs (angular gridfns, on ghosted grid):
	//	h			# shape of trial surface
	//	Theta			# Theta(h) assumed to already be computed
	//	partial_Theta_wrt_partial_d_h	# Jacobian coefficients
	//	partial_Theta_wrt_partial_dd_h	# (also assumed to already be computed)
	//
	// Outputs:
	//	The Jacobian matrix is stored in the Jacobian object Jac.
	//
	namespace
	{
		void expansion_Jacobian_partial_SD(patch_system &ps, Jacobian &Jac,
										   bool print_msg_flag)
		{
			Jac.zero_matrix();
			ps.compute_synchronize_Jacobian();

			for (int xpn = 0; xpn < ps.N_patches(); ++xpn)
			{
				patch &xp = ps.ith_patch(xpn);

				for (int x_irho = xp.min_irho(); x_irho <= xp.max_irho(); ++x_irho)
				{
					for (int x_isigma = xp.min_isigma(); x_isigma <= xp.max_isigma(); ++x_isigma)
					{
						//
						// compute the main Jacobian terms for this grid point, i.e.
						//	partial Theta(this point x, Jacobian row II)
						//	---------------------------------------------
						//	partial h(other points y, Jacobian column JJ)
						//

						// Jacobian row index
						const int II = ps.gpn_of_patch_irho_isigma(xp, x_irho, x_isigma);

						// Jacobian coefficients for this point
						const fp Jacobian_coeff_rho = xp.gridfn(gfns::gfn__partial_Theta_wrt_partial_d_h_1,
																x_irho, x_isigma);
						const fp Jacobian_coeff_sigma = xp.gridfn(gfns::gfn__partial_Theta_wrt_partial_d_h_2,
																  x_irho, x_isigma);
						const fp Jacobian_coeff_rho_rho = xp.gridfn(gfns::gfn__partial_Theta_wrt_partial_dd_h_11,
																	x_irho, x_isigma);
						const fp Jacobian_coeff_rho_sigma = xp.gridfn(gfns::gfn__partial_Theta_wrt_partial_dd_h_12,
																	  x_irho, x_isigma);
						const fp Jacobian_coeff_sigma_sigma = xp.gridfn(gfns::gfn__partial_Theta_wrt_partial_dd_h_22,
																		x_irho, x_isigma);

						// partial_rho, partial_rho_rho
						{
							for (int m_irho = xp.molecule_min_m();
								 m_irho <= xp.molecule_max_m();
								 ++m_irho)
							{
								const int xm_irho = x_irho + m_irho;
								const fp Jac_rho = Jacobian_coeff_rho * xp.partial_rho_coeff(m_irho);
								const fp Jac_rho_rho = Jacobian_coeff_rho_rho * xp.partial_rho_rho_coeff(m_irho);
								const fp Jac_sum = Jac_rho + Jac_rho_rho;
								if (xp.is_in_nominal_grid(xm_irho, x_isigma))
									then
									{
										const int xm_JJ = Jac.II_of_patch_irho_isigma(xp, xm_irho, x_isigma);
										Jac.sum_into_element(II, xm_JJ, Jac_sum);
									}
								else
									add_ghost_zone_Jacobian(ps, Jac,
															Jac_sum,
															xp, xp.minmax_rho_ghost_zone(m_irho < 0),
															II, xm_irho, x_isigma);
							}
						}

						// partial_sigma, partial_sigma_sigma
						{
							for (int m_isigma = xp.molecule_min_m();
								 m_isigma <= xp.molecule_max_m();
								 ++m_isigma)
							{
								const int xm_isigma = x_isigma + m_isigma;
								const fp Jac_sigma = Jacobian_coeff_sigma * xp.partial_sigma_coeff(m_isigma);
								const fp Jac_sigma_sigma = Jacobian_coeff_sigma_sigma * xp.partial_sigma_sigma_coeff(m_isigma);
								const fp Jac_sum = Jac_sigma + Jac_sigma_sigma;
								if (xp.is_in_nominal_grid(x_irho, xm_isigma))
									then
									{
										const int xm_JJ = Jac.II_of_patch_irho_isigma(xp, x_irho, xm_isigma);
										Jac.sum_into_element(II, xm_JJ, Jac_sum);
									}
								else
									add_ghost_zone_Jacobian(ps, Jac,
															Jac_sum,
															xp, xp.minmax_sigma_ghost_zone(m_isigma < 0),
															II, x_irho, xm_isigma);
							}
						}

						// partial_rho_sigma
						{
							for (int m_irho = xp.molecule_min_m();
								 m_irho <= xp.molecule_max_m();
								 ++m_irho)
							{
								for (int m_isigma = xp.molecule_min_m();
									 m_isigma <= xp.molecule_max_m();
									 ++m_isigma)
								{
									const int xm_irho = x_irho + m_irho;
									const int xm_isigma = x_isigma + m_isigma;
									const fp Jac_rho_sigma = Jacobian_coeff_rho_sigma * xp.partial_rho_sigma_coeff(m_irho, m_isigma);
									if (xp.is_in_nominal_grid(xm_irho, xm_isigma))
										then
										{
											const int xm_JJ = Jac.II_of_patch_irho_isigma(xp, xm_irho, xm_isigma);
											Jac.sum_into_element(II, xm_JJ, Jac_rho_sigma);
										}
									else
									{
										const ghost_zone &xmgz = xp.corner_ghost_zone_containing_point(m_irho < 0, m_isigma < 0,
																									   xm_irho, xm_isigma);
										add_ghost_zone_Jacobian(ps, Jac,
																Jac_rho_sigma,
																xp, xmgz,
																II, xm_irho, xm_isigma);
									}
								}
							}
						}
					}
				}
			}
		}
	}

	//******************************************************************************

	//
	// This function adds the ghost-zone Jacobian dependency contributions
	// for a single ghost-zone point, to a Jacobian matrix.
	//
	// Arguments:
	// ps = The patch system.
	// Jac = (out) The Jacobian matrix.
	// mol = The molecule coefficient.
	// xp = The patch containing the center point of the molecule.
	// xmgz = If the x+m point is in a ghost zone, this must be that ghost zone.
	//	  If the x+m point is not in a ghost zone, this argument is ignored.
	// x_II = The Jacobian row of the x point.
	// xm_(irho,isigma) = The coordinates (in xp) of the x+m point of the molecule.

	namespace
	{
		void add_ghost_zone_Jacobian(const patch_system &ps,
									 Jacobian &Jac,
									 fp mol,
									 const patch &xp, const ghost_zone &xmgz,
									 int x_II,
									 int xm_irho, int xm_isigma)
		{
			const patch_edge &xme = xmgz.my_edge();
			const int xm_iperp = xme.iperp_of_irho_isigma(xm_irho, xm_isigma);
			const int xm_ipar = xme.ipar_of_irho_isigma(xm_irho, xm_isigma);

			// FIXME: this won't change from one call to another
			//        ==> it would be more efficient to reuse the same buffer
			//            across multiple calls on this function
			int global_min_ym, global_max_ym;
			ps.synchronize_Jacobian_global_minmax_ym(global_min_ym, global_max_ym);
			jtutil::array1d<fp> Jacobian_buffer(global_min_ym, global_max_ym);

			// on what other points y does this molecule point xm depend
			// via the patch_system::synchronize() operation?
			int y_iperp;
			int y_posn, min_ym, max_ym;
			const patch_edge &ye = ps.synchronize_Jacobian(xmgz,
														   xm_iperp, xm_ipar,
														   y_iperp,
														   y_posn, min_ym, max_ym,
														   Jacobian_buffer);
			patch &yp = ye.my_patch();

			// add the Jacobian contributions from the ym points
			for (int ym = min_ym; ym <= max_ym; ++ym)
			{
				const int y_ipar = y_posn + ym;
				const int y_irho = ye.irho_of_iperp_ipar(y_iperp, y_ipar);
				const int y_isigma = ye.isigma_of_iperp_ipar(y_iperp, y_ipar);
				const int y_JJ = Jac.II_of_patch_irho_isigma(yp, y_irho, y_isigma);
				Jac.sum_into_element(x_II, y_JJ, mol * Jacobian_buffer(ym));
			}
		}
	}

	//******************************************************************************

	//
	// If ps_ptr != NULL and Jac_ptr != NULL, this function sums the d/dr
	// terms into the Jacobian matrix of the expansion Theta(h), computing
	// those terms by finite differencing.
	//
	// If ps_ptr == NULL and Jac_ptr == NULL, this function does a dummy
	// computation, in which only any expansion() (and hence geometry
	// interpolator) calls are done, these with the number of interpolation
	// points set to 0 and all the output array pointers set to NULL.
	//
	// It's illegal for one but not both of ps_ptr and Jac_ptr to be NULL.
	//
	// The basic algorithm is that
	//	Jac += diag[ (Theta(h+epsilon) - Theta(h)) / epsilon ]
	//
	// Inputs (angular gridfns, on ghosted grid):
	//	h			# shape of trial surface
	//	Theta			# Theta(h) assumed to already be computed
	//
	// Outputs:
	//	Jac += d/dr terms
	//
	// Results:
	// This function returns a status code indicating whether the computation
	// succeeded or failed, and if the latter, what caused the failure.
	//
	namespace
	{
		enum expansion_status
		expansion_Jacobian_dr_FD(patch_system *ps_ptr, Jacobian *Jac_ptr, fp add_to_expansion,
								 bool initial_flag,
								 bool print_msg_flag)
		{
			const bool active_flag = (ps_ptr != NULL) && (Jac_ptr != NULL);

			const double epsilon = 1e-6;
			// compute Theta(h+epsilon)
			if (active_flag)
				then
				{
					ps_ptr->gridfn_copy(gfns::gfn__Theta, gfns::gfn__save_Theta);
					ps_ptr->add_to_ghosted_gridfn(epsilon, gfns::gfn__h);
				}
			const enum expansion_status status = expansion(ps_ptr, add_to_expansion,
														   initial_flag);
			if (status != expansion_success)
				then return status; // *** ERROR RETURN ***

			if (active_flag)
				then
				{
					for (int pn = 0; pn < ps_ptr->N_patches(); ++pn)
					{
						patch &p = ps_ptr->ith_patch(pn);
						for (int irho = p.min_irho(); irho <= p.max_irho(); ++irho)
						{
							for (int isigma = p.min_isigma();
								 isigma <= p.max_isigma();
								 ++isigma)
							{
								const int II = ps_ptr->gpn_of_patch_irho_isigma(p, irho, isigma);
								const fp old_Theta = p.gridfn(gfns::gfn__save_Theta,
															  irho, isigma);
								const fp new_Theta = p.gridfn(gfns::gfn__Theta,
															  irho, isigma);
								const fp d_dr_term = (new_Theta - old_Theta) / epsilon;
								Jac_ptr->sum_into_element(II, II, d_dr_term);
							}
						}
					}

					// restore h and Theta
					ps_ptr->add_to_ghosted_gridfn(-epsilon, gfns::gfn__h);
					ps_ptr->gridfn_copy(gfns::gfn__save_Theta, gfns::gfn__Theta);
				}

			return expansion_success; // *** NORMAL RETURN ***
		}
	}

	//******************************************************************************

} // namespace AHFinderDirect
#endif
