static void
aran_multipole_translate_vertical (const AranSphericalSeriesd * src,
                                   AranSphericalSeriesd * dst,
                                   gdouble r,
                                   gdouble cost,
                                   gdouble cosp, gdouble sinp)
{
  gint l, m;
  gint n;
  gdouble rpow[dst->negdeg];
  gdouble pow;
  gcomplex128 *srcterm, *dstterm;
  gint d = MAX (src->negdeg, dst->negdeg) - 1;

  if (src->negdeg > dst->negdeg)
    g_warning ("could loose precision in \"%s\"\n", __PRETTY_FUNCTION__);

  aran_spherical_seriesd_alpha_require (d);
  aran_spherical_seriesd_beta_require (d);


  pow = 1.;
  for (l = 0; l < dst->negdeg; l++)
    {
      rpow[l] = pow;
      pow *= r;
      for (m = 0; m <= l; m++)
        {
          dstterm = _spherical_seriesd_get_neg_term (dst, l, m);

          for (n = m; n <= MIN (l, src->negdeg - 1); n++)
            {
              gdouble normaliz = aran_spherical_seriesd_beta (l) /
                aran_spherical_seriesd_beta (n);
              gdouble factor;
	      gdouble h;
              srcterm = _spherical_seriesd_get_neg_term (src, n, 0);

              /* m-o = 0 */
              factor =
                aran_spherical_seriesd_alpha (l - n, 0) *
                aran_spherical_seriesd_alpha (n, m) /
                aran_spherical_seriesd_alpha (l, m);

	      /* h= Y_(l-n)^(m-o) */

              /*
               * in this case, h=Y_(l-n)^0, which simplifies with "normaliz"
               * removing beta(l-n)
               * and then becomes h = P_(l-n)^0 = (cost)^(l-n)
               */
              h = ((l-n)%2 == 0)? 1. : cost;

              *dstterm += h * srcterm[m] * factor * normaliz *
                rpow[l - n];
            }
        }
    }
}
/**
 * aran_spherical_seriesd_rotate_inverse:
 * @src: source #AranSphericalSeriesd.
 * @alpha: 1st Euler angle.
 * @beta: 2nd Euler angle. Condition 0 <= /theta < pi must hold.
 * @gamma: 3rd Euler angle.
 * @dst: destination #AranSphericalSeriesd.
 *
 * Computes the inverse rotation of @src and accumulates the result into
 * @dst. Angles are in the Euler ZYZ convention. The term inverse means that
 * performing aran_spherical_seriesd_rotate_inverse to the result of a call to
 * aran_spherical_seriesd_rotate() is equivalent to identity.
 */
void aran_spherical_seriesd_rotate_inverse (const AranSphericalSeriesd * src,
                                            gdouble alpha, gdouble beta,
                                            gdouble gamma,
                                            AranSphericalSeriesd * dst)
{
  gint l;
  guint8 nd = MIN (dst->negdeg, src->negdeg);
  guint8 pd = MIN (dst->posdeg, src->posdeg);
  guint8 lmax = MAX (pd+1, nd);

  gcomplex128 expma[lmax];
  gcomplex128 expmg[lmax];
  gcomplex128 expa = cos (alpha) - G_I * sin (alpha);
  gcomplex128 expg = cos (gamma) - G_I * sin (gamma);

  AranWigner *aw = aran_wigner_repo_lookup (beta);

  aran_wigner_require (aw, lmax);

  expma[0] = 1.;
  expmg[0] = 1.;
  for (l = 1; l < lmax; l++)
    {
      expma[l] = expma[l - 1] * expa;
      expmg[l] = expmg[l - 1] * expg;
    }

  _buffer_rotate_inverse (aw, pd, expma, expmg,
                  _spherical_seriesd_get_pos_term (src, 0, 0),
                  _spherical_seriesd_get_pos_term (dst, 0, 0));

  if (nd > 0)
    {
      _buffer_rotate_inverse (aw, nd - 1, expma, expmg,
                      _spherical_seriesd_get_neg_term (src, 0, 0),
                      _spherical_seriesd_get_neg_term (dst, 0, 0));
    }
}
/**
 * aran_spherical_seriesd_translate_rotate:
 * @src: source expansion series.
 * @xsrc: @src center.
 * @dst: destination expansion series.
 * @xdst: @dst center.
 *
 * Performs the same operation as aran_spherical_seriesd_translate() but
 * with the "Point and Shoot" algorithm which achieves O(p^3) complexity. 
 */
void aran_spherical_seriesd_translate_rotate (const AranSphericalSeriesd *src,
                                              const VsgVector3d *xsrc,
                                              AranSphericalSeriesd *dst,
                                              const VsgVector3d *xdst)
{
  gint l;
  guint8 nd = MAX (dst->negdeg, src->negdeg);
  guint8 pd = MAX (dst->posdeg, src->posdeg);
  guint8 lmax = MAX (pd+1, nd);

  gcomplex128 expma[lmax];
  gcomplex128 expa;

  AranWigner *aw;
  VsgVector3d dir;
  gdouble r, theta, phi;
  gdouble cost = 1.;
  AranSphericalSeriesd *rot = (AranSphericalSeriesd *)
    g_alloca (ARAN_SPHERICAL_SERIESD_SIZE (src->posdeg, src->negdeg));

  AranSphericalSeriesd *trans = (AranSphericalSeriesd *)
    g_alloca (ARAN_SPHERICAL_SERIESD_SIZE (dst->posdeg, dst->negdeg));

  /* create work AranSphericalSeriesd */
  memcpy (rot, src, sizeof (AranSphericalSeriesd));
  memcpy (trans, dst, sizeof (AranSphericalSeriesd));

  aran_spherical_seriesd_set_zero (rot);
  aran_spherical_seriesd_set_zero (trans);

  /* get translation vector */
  vsg_vector3d_sub (xdst, xsrc, &dir);

  if (dir.z < 0.)
    {
      cost = -1.;
      vsg_vector3d_scalp (&dir, -1., &dir);
    }

  /* get rotation angles */
  vsg_vector3d_to_spherical (&dir, &r, &theta, &phi);

  /* prepare rotation tools: AranWigner + complex exponentials arrays */
  aw = aran_wigner_repo_lookup (theta);

  aran_wigner_require (aw, lmax);

  expa = cos (-phi) - G_I * sin (-phi);

  expma[0] = 1.;
  for (l = 1; l < lmax; l++)
    {
      expma[l] = expma[l - 1] * expa;
    }

  /* rotate src */
  _buffer_rotate_ab (aw, src->posdeg, expma,
                     _spherical_seriesd_get_pos_term (src, 0, 0),
                     _spherical_seriesd_get_pos_term (rot, 0, 0));

  if (src->negdeg > 0)
    {
      _buffer_rotate_ab (aw, src->negdeg - 1, expma,
                      _spherical_seriesd_get_neg_term (src, 0, 0),
                      _spherical_seriesd_get_neg_term (rot, 0, 0));
    }
/*   aran_spherical_seriesd_rotate (src, -phi, theta, 0., rot); */

  /* translate src to dst center */
  aran_spherical_seriesd_translate_vertical (rot, trans,
                                             r, cost,
                                             1., 0.);

  /* rotate back and accumulate into dst */
  _buffer_rotate_ab_inverse (aw, dst->posdeg, expma,
                  _spherical_seriesd_get_pos_term (trans, 0, 0),
                  _spherical_seriesd_get_pos_term (dst, 0, 0));

  if (dst->negdeg > 0)
    {
      _buffer_rotate_ab_inverse (aw, dst->negdeg - 1, expma,
                      _spherical_seriesd_get_neg_term (trans, 0, 0),
                      _spherical_seriesd_get_neg_term (dst, 0, 0));
    }
/*   aran_spherical_seriesd_rotate_inverse (trans, -phi, theta, 0., dst); */
}
static void aran_multipole_to_local (const AranSphericalSeriesd * src,
                                     AranSphericalSeriesd * dst,
                                     gdouble r,
                                     gdouble cost, gdouble sint,
                                     gdouble cosp, gdouble sinp)
{
  gint l, m;
  gint n, o;
  gint d = dst->posdeg + src->negdeg;
  gdouble rpow[d + 1];
  gdouble pow, inv_r;
  gcomplex128 *srcterm, *dstterm, *hterm;
  gcomplex128 harmonics[((d + 1) * (d + 2)) / 2];
  gcomplex128 expp = cosp + G_I * sinp;
  gdouble sign;

/*   if ((src->negdeg-1) > dst->posdeg) */
/*     g_warning ("could loose precision in \"%s\"\n", __PRETTY_FUNCTION__); */

  /* vertical translation */
  if (ABS (sint) < 1.e-5) /* sint in m2l translation is zero or high values */
    {
      aran_spherical_seriesd_multipole_to_local_vertical (src, dst, r,
                                                          cost, cosp, sinp);
      return;
    }

  aran_spherical_seriesd_alpha_require (d);
  aran_spherical_seriesd_beta_require (d);

  aran_spherical_harmonic_evaluate_multiple_internal (d, cost, sint, expp,
                                                      harmonics);

  inv_r = 1. / r;
  pow = 1.;
  for (l = 0; l <= d; l++)
    {
      rpow[l] = pow;
      pow *= inv_r;
    }

  sign = 1.;
  for (l = 0; l <= dst->posdeg; l++)
    {
      for (m = 0; m <= l; m++)
        {
          dstterm = _spherical_seriesd_get_pos_term (dst, l, m);

          for (n = 0; n < src->negdeg; n++)
            {
              gdouble normaliz = aran_spherical_seriesd_beta (l + n) *
                aran_spherical_seriesd_beta (l) /
                aran_spherical_seriesd_beta (n);
              gcomplex128 sum = 0.;

              srcterm = _spherical_seriesd_get_neg_term (src, n, 0);
              hterm = aran_spherical_harmonic_multiple_get_term (l + n, 0,
                                                                 harmonics);

              sum = aran_spherical_seriesd_alpha (l, m) *
                aran_spherical_seriesd_alpha (n, 0) /
                aran_spherical_seriesd_alpha (l + n, m) * hterm[m] *
                srcterm[0];

              for (o = 1; o <= n; o++)
                {
                  guint m_p_o = m + o;

                  gdouble factor = aran_spherical_seriesd_alpha (l, m) *
                    aran_spherical_seriesd_alpha (n, o);

                  /* h= Y_(l+n)^(m+o) */
                  gcomplex128 h = hterm[m_p_o];

                  sum += h * srcterm[o] * factor /
                    aran_spherical_seriesd_alpha (l + n, m_p_o);

                  m_p_o = ABS (m - o);

                  /* h= Y_(l+n)^(m-o) */
                  h = hterm[m_p_o];
                  if ((m - o) < 0)
                    h = _sph_sym (h, m_p_o);

                  sum += h * _sph_sym (srcterm[o], o) * factor /
                    aran_spherical_seriesd_alpha (l + n, m_p_o);
                }

              *dstterm += conj (sum) * sign * normaliz * rpow[l + n + 1];
            }
        }
      sign = -sign;
    }
}
void
aran_spherical_seriesd_multipole_to_local_vertical
(const AranSphericalSeriesd * src,
 AranSphericalSeriesd * dst,
 gdouble r,
 gdouble cost,
 gdouble cosp, gdouble sinp)
{
  gint l, m;
  gint n;
  gint d = dst->posdeg + src->negdeg;
  gdouble rpow[d + 1];
  gdouble pow, inv_r;
  gcomplex128 *srcterm, *dstterm, *hterm;
  gcomplex128 harmonics[((d + 1) * (d + 2)) / 2];
  gcomplex128 expp = cosp + G_I * sinp;
  gdouble sign;

  aran_spherical_seriesd_alpha_require (d);
  aran_spherical_seriesd_beta_require (d);

  aran_spherical_harmonic_evaluate_multiple_internal (d, cost, 0, expp,
                                                      harmonics);

  inv_r = 1. / r;
  pow = 1.;
  for (l = 0; l <= d; l++)
    {
      rpow[l] = pow;
      pow *= inv_r;
    }

  sign = 1.;
  for (l = 0; l <= dst->posdeg; l++)
    {
      for (m = 0; m <= l; m++)
        {
          dstterm = _spherical_seriesd_get_pos_term (dst, l, m);

          for (n = m; n < src->negdeg; n++)
            {
              gdouble normaliz = aran_spherical_seriesd_beta (l + n) *
                aran_spherical_seriesd_beta (l) /
                aran_spherical_seriesd_beta (n);
              gcomplex128 sum;
              gdouble factor;
              gcomplex128 h;

              srcterm = _spherical_seriesd_get_neg_term (src, n, 0);
              hterm = aran_spherical_harmonic_multiple_get_term (l + n, 0,
                                                                 harmonics);

              /* o == -m */
              factor = aran_spherical_seriesd_alpha (l, m) *
                aran_spherical_seriesd_alpha (n, m);

              /* h= Y_(l+n)^(m+o) */
              h = hterm[0];

              sum = h * _sph_sym (srcterm[m], m) * factor /
                aran_spherical_seriesd_alpha (l + n, 0);

              *dstterm += conj (sum) * sign * normaliz * rpow[l + n + 1];
            }
        }
      sign = -sign;
    }
}
static void aran_multipole_translate (const AranSphericalSeriesd * src,
                                      AranSphericalSeriesd * dst,
                                      gdouble r,
                                      gdouble cost, gdouble sint,
                                      gdouble cosp, gdouble sinp)
{
  gint l, m;
  gint n, o;
  gdouble rpow[dst->negdeg];
  gdouble pow;
  gcomplex128 *srcterm, *dstterm, *hterm;
  gint d = MAX (src->negdeg, dst->negdeg) - 1;
  gcomplex128 harmonics[((d + 1) * (d + 2)) / 2];
  gcomplex128 expp = cosp + G_I * sinp;

  if (src->negdeg > dst->negdeg)
    g_warning ("could loose precision in \"%s\"\n", __PRETTY_FUNCTION__);

  aran_spherical_seriesd_alpha_require (d);
  aran_spherical_seriesd_beta_require (d);

  aran_spherical_harmonic_evaluate_multiple_internal (dst->negdeg - 1, cost,
                                                      sint, expp, harmonics);

  pow = 1.;
  for (l = 0; l < dst->negdeg; l++)
    {
      rpow[l] = pow;
      pow *= r;
      for (m = 0; m <= l; m++)
        {
          dstterm = _spherical_seriesd_get_neg_term (dst, l, m);

          for (n = 0; n <= MIN (l, src->negdeg - 1); n++)
            {
              gdouble normaliz = aran_spherical_seriesd_beta (l - n) *
                aran_spherical_seriesd_beta (l) /
                aran_spherical_seriesd_beta (n);
              gcomplex128 sum = 0.;

              srcterm = _spherical_seriesd_get_neg_term (src, n, 0);
              hterm = aran_spherical_harmonic_multiple_get_term (l - n, 0,
                                                                 harmonics);
              for (o = MAX (-n, m + n - l); o <= MIN (n, l + m - n); o++)
                {
                  guint abs_m_m_o = ABS (m - o);
                  gdouble factor =
                    aran_spherical_seriesd_alpha (l - n, abs_m_m_o) *
                    aran_spherical_seriesd_alpha (n, ABS (o)) /
                    aran_spherical_seriesd_alpha (l, ABS (m));

                  gcomplex128 h = conj (hterm[abs_m_m_o]);

                  /* h=conj ( Y_(l-n)^(m-o) ) */
                  if ((m - o) < 0)
                    h = _sph_sym (h, abs_m_m_o);

                  if (o >= 0)
                    h *= srcterm[o];
                  else
                    h *= _sph_sym (srcterm[-o], -o);

                  sum += h * factor;
                }

              *dstterm += sum * normaliz * rpow[l - n];
            }
        }
    }
}
void
aran_spherical_seriesd_multipole_to_local_vertical
(const AranSphericalSeriesd * src,
 AranSphericalSeriesd * dst,
 gdouble r,
 gdouble cost,
 gdouble cosp, gdouble sinp)
{
  gint l, m;
  gint n;
  gint d = dst->posdeg + src->negdeg;
  gdouble rpow[d + 1];
  gdouble pow, inv_r;
  gcomplex128 *srcterm, *dstterm;
  gdouble sign;

  aran_spherical_seriesd_alpha_require (d);
  aran_spherical_seriesd_beta_require (d);

  inv_r = 1. / r;
  pow = 1.;
  for (l = 0; l <= d; l++)
    {
      rpow[l] = pow;
      pow *= inv_r;
    }

  sign = 1.;
  for (l = 0; l <= dst->posdeg; l++)
    {
      for (m = 0; m <= l; m++)
        {
          dstterm = _spherical_seriesd_get_pos_term (dst, l, m);

          for (n = m; n < src->negdeg; n++)
            {
              gdouble normaliz = aran_spherical_seriesd_beta (l) /
                aran_spherical_seriesd_beta (n);
              gcomplex128 sum;
              gdouble factor;
              gcomplex128 h;

              srcterm = _spherical_seriesd_get_neg_term (src, n, 0);

              /* o == -m */
              factor = aran_spherical_seriesd_alpha (l, m) *
                aran_spherical_seriesd_alpha (n, m);

              /* h= Y_(l+n)^(m+o) */

              /*
               * in this case, h=Y_(l+n)^0, which simplifies with "normaliz"
               * removing beta(l+n)
               * and then becomes h = P_(l+n)^0 = (cost)^(l+n)
               */
              h = ((l+n)%2 == 0)? 1. : cost;

              sum = h * _sph_sym (srcterm[m], m) * factor /
                aran_spherical_seriesd_alpha (l + n, 0);

              *dstterm += conj (sum) * sign * normaliz * rpow[l + n + 1];
            }
        }
      sign = -sign;
    }
}
/*
 * Compute M2L translation in a vertical direction, positive or negative,
 * depending on the value of cost (+1. or -1.)
 *
 * Formula:
 * \check{Y}_l^m (\vec{r_0'}) =
 * (-1)^l \sum_{n=m}^{\infty}
 * \frac{1}{|\vec{r_0'}-\vec{r_0}|^{l+n+1}}
 * \frac{\beta_l \beta_{l+n}}{\beta_n}
 * \frac{\alpha_l^m \alpha_n^{-m}}{\alpha_{l+n}^{0}}
 * \overline{Y_{l+n}^{0}(\theta, \phi_{\vec{r_0'}-\vec{r_0}})}
 * \overline{\hat{Y}_{n}^{-m}(\vec{r_0})}
 */
void
aran_spherical_seriesd_multipole_to_local_vertical
(const AranSphericalSeriesd * src,
 AranSphericalSeriesd * dst,
 gdouble r,
 gdouble cost,
 gdouble cosp, gdouble sinp)
{
  gint l, m;
  gint n;
  gint d = dst->posdeg + src->negdeg;
  gdouble rpow[d + 1];
  gdouble pow, inv_r;
  gcomplex128 *dstterm;

  aran_spherical_seriesd_alpha_require (d);
  aran_spherical_seriesd_beta_require (d);
  _betal_over_betan_require (MAX(dst->posdeg, src->negdeg));
  _precomputed_translate_vertical_require(MAX(dst->posdeg, src->negdeg));

  inv_r = 1. / r;
  pow = 1.;
  for (l = 0; l <= d; l++)
    {
      rpow[l] = pow;

      /* o == -m */
      /* h= Y_(l+n)^(m+o) */
      /*
       * in this case, h=Y_(l+n)^0, which simplifies with beta(l+n)
       * and then becomes h = P_(l+n)^0 = (cost)^(l+n)
       * we integrate (cost)^(l+n) within rpow[l+n+1]
       */
      if (l%2 == 0) rpow[l] *= cost;

      pow *= inv_r;
    }

  for (l = 0; l <= dst->posdeg; l++)
    {
      for (m = 0; m <= l; m++)
        {
          gcomplex128 sum = 0.;

          dstterm = _spherical_seriesd_get_pos_term (dst, l, m);

          for (n = m; n < src->negdeg; n++)
            {
              gcomplex128 srcterm;
              gdouble translate_factor;

              srcterm = *_spherical_seriesd_get_neg_term (src, n, m);

              /* translate_factor = beta(l)/beta(n) * alpha(l,m) * alpha(n,m) /
               * alpha (l + n, 0);
               */
              translate_factor = _precomputed_translate_vertical (l, m, n);

              sum += srcterm * (rpow[l + n + 1] * translate_factor);
            }

          /* combination of (-1)^l and Y_n^(-m)*/
          sum = ((l+m)%2 == 0)? sum : -sum;

          *dstterm += sum;
        }
    }
}