/* * Copyright (c) 2010-2015 Centre National de la Recherche Scientifique. * written by Nathanael Schaeffer (CNRS, ISTerre, Grenoble, France). * * nathanael.schaeffer@ujf-grenoble.fr * * This software is governed by the CeCILL license under French law and * abiding by the rules of distribution of free software. You can use, * modify and/or redistribute the software under the terms of the CeCILL * license as circulated by CEA, CNRS and INRIA at the following URL * "http://www.cecill.info". * * The fact that you are presently reading this means that you have had * knowledge of the CeCILL license and that you accept its terms. * */ /** \internal \file sht_legendre.c \brief Compute legendre polynomials and associated functions. - Normalization of the associated functions for various spherical harmonics conventions are possible, with or without Condon-Shortley phase. - When computing the derivatives (with respect to colatitude theta), there are no singularities. - written by Nathanael Schaeffer / CNRS, with some ideas and code from the GSL 1.13 and Numerical Recipies. The associated legendre functions are computed using the following recurrence formula : \f[ Y_l^m(x) = a_l^m \, x \ Y_{l-1}^m(x) + b_l^m \ Y_{l-2}^m(x) \f] with starting values : \f[ Y_m^m(x) = a_m^m \ (1-x^2)^{m/2} \f] and \f[ Y_{m+1}^m(x) = a_{m+1}^m \ Y_m^m(x) \f] where \f$ a_l^m \f$ and \f$ b_l^m \f$ are coefficients that depend on the normalization, and which are precomputed once for all by \ref legendre_precomp. */ // index in alm array for given im. #define alm_im(shtns, im) (shtns->alm + (im)*(2*shtns->lmax - ((im)-1)*shtns->mres)) #if SHT_VERBOSE > 1 #define LEG_RANGE_CHECK #endif #ifndef HAVE_LONG_DOUBLE_WIDER #define long_double_caps 0 typedef double real; #define SQRT sqrt #define COS cos #define SIN sin #define FABS fabs #define legendre_sphPlm_hp legendre_sphPlm #define legendre_sphPlm_array_hp legendre_sphPlm_array #define legendre_sphPlm_deriv_array_hp legendre_sphPlm_deriv_array #else int long_double_caps = 0; typedef long double real; #define SQRT sqrtl #define COS cosl #define SIN sinl #define FABS fabsl // scale factor applied for LMAX larger than SHT_L_RESCALE. Allows accurate transforms up to l=2700 with 64 bit double precision. #define SHT_LEG_SCALEF 1.1018032079253110206e-280 #define SHT_L_RESCALE 1536 // returns 1 for large exponent only => useless. // returns 2 for extended precision only => ok to improve gauss points. // returns 3 for extended precision and large exponent => ok to improve legendre recurrence. static int test_long_double() { volatile real tt; // volatile avoids optimizations. int p = 0; tt = 1.0e-1000L; tt *= tt; if (tt > 0) p |= 1; // bit 0 set for large exponent tt = 1.0; tt += 1.e-18; if (tt > 1.0) p |= 2; // bit 1 set for extended precision long_double_caps = p; return p; } /// \internal high precision version of \ref a_sint_pow_n static real a_sint_pow_n_hp(real val, real cost, long int n) { real s2 = (1.-cost)*(1.+cost); // sin(t)^2 = 1 - cos(t)^2 long int k = n >> 7; if (sizeof(s2) > 8) k = 0; // enough accuracy, we do not bother. #ifdef LEG_RANGE_CHECK if (s2 < 0) shtns_runerr("sin(t)^2 < 0 !!!"); #endif if (n&1) val *= SQRT(s2); // = sin(t) do { if (n&2) val *= s2; n >>= 1; s2 *= s2; } while(n > k); n >>= 1; while(n > 0) { // take care of very large power n n--; val *= s2; } return val; // = sint(t)^n } #endif /// \internal computes val.sin(t)^n from cos(t). ie returns val.(1-x^2)^(n/2), with x = cos(t) /// assumes: -1 <= cost <= 1, n>=0, and nval<=0 is the extended exponent associated to val. /// updates nval, and returns val such as the result is val.SHT_SCALE_FACTOR^(nval) static double a_sint_pow_n_ext(double val, double cost, int n, int *nval) { double s2 = (1.-cost)*(1.+cost); // sin(t)^2 = 1 - cos(t)^2 >= 0 double val0 = val; // store sign int ns2 = 0; int nv = *nval; #ifdef LEG_RANGE_CHECK if (s2 < 0) shtns_runerr("sin(t)^2 < 0 !!!"); #endif val = fabs(val); // val >= 0 if (n&1) val *= sqrt(s2); // = sin(t) while (n >>= 1) { if (n&1) { if (val < 1.0/SHT_SCALE_FACTOR) { // val >= 0 nv--; val *= SHT_SCALE_FACTOR; } val *= s2; nv += ns2; // 1/S^2 < val < 1 } s2 *= s2; ns2 += ns2; if (s2 < 1.0/SHT_SCALE_FACTOR) { // s2 >= 0 ns2--; s2 *= SHT_SCALE_FACTOR; // 1/S < s2 < 1 } } while ((nv < 0) && (val > 1.0/SHT_SCALE_FACTOR)) { // try to minimize |nv| ++nv; val *= 1.0/SHT_SCALE_FACTOR; } if (val0 < 0) val *= -1.0; // restore sign. *nval = nv; return val; // 1/S^2 < val < 1 } /// \internal Returns the value of a legendre polynomial of degree l and order im*MRES, noramalized for spherical harmonics, using recurrence. /// Requires a previous call to \ref legendre_precomp(). /// Output compatible with the GSL function gsl_sf_legendre_sphPlm(l, m, x) static double legendre_sphPlm(shtns_cfg shtns, const int l, const int im, const double x) { double *al; int m, ny; double ymm, ymmp1; m = im*MRES; #ifdef LEG_RANGE_CHECK if ( (l>LMAX) || (lMMAX) ) shtns_runerr("argument out of range in legendre_sphPlm"); #endif ny = 0; al = alm_im(shtns, im); ymm = al[0]; if (m>0) ymm = a_sint_pow_n_ext(ymm, x, m, &ny); // ny <= 0 ymmp1 = ymm; // l=m if (l == m) goto done; ymmp1 = al[1] * (x*ymmp1); // l=m+1 if (l == m+1) goto done; m+=2; al+=2; while ((ny < 0) && (m < l)) { // take care of scaled values ymm = al[1]*(x*ymmp1) + al[0]*ymm; ymmp1 = al[3]*(x*ymm) + al[2]*ymmp1; m+=2; al+=4; if (fabs(ymm) > 1.0/SHT_SCALE_FACTOR) { // rescale when value is significant ++ny; ymm *= 1.0/SHT_SCALE_FACTOR; ymmp1 *= 1.0/SHT_SCALE_FACTOR; } } while (m < l) { // values are unscaled. ymm = al[1]*(x*ymmp1) + al[0]*ymm; ymmp1 = al[3]*(x*ymm) + al[2]*ymmp1; m+=2; al+=4; } if (m == l) { ymmp1 = al[1]*(x*ymmp1) + al[0]*ymm; } done: if (ny < 0) { if (ny+3 < 0) return 0.0; do { ymmp1 *= 1.0/SHT_SCALE_FACTOR; } while (++ny < 0); // return unscaled values. } return ymmp1; } #if HAVE_LONG_DOUBLE_WIDER static double legendre_sphPlm_hp(shtns_cfg shtns, const int l, const int im, double x) { double *al; int i,m; real ymm, ymmp1; if (long_double_caps < 3) legendre_sphPlm(shtns, l, im, x); // not worth it. m = im*MRES; #ifdef LEG_RANGE_CHECK if ( (l>LMAX) || (lMMAX) ) shtns_runerr("argument out of range in legendre_sphPlm"); #endif al = alm_im(shtns, im); ymm = al[0]; // l=m if (m>0) ymm *= SHT_LEG_SCALEF; ymmp1 = ymm; if (l==m) goto done; ymmp1 = al[1] * (x*ymmp1); // l=m+1 al+=2; if (l == m+1) goto done; for (i=m+2; i0) ymmp1 *= a_sint_pow_n_hp(1.0/SHT_LEG_SCALEF, x, m); return ((double) ymmp1); } #endif /// \internal Compute values of legendre polynomials noramalized for spherical harmonics, /// for a range of l=m..lmax, at given m and x, using recurrence. /// Requires a previous call to \ref legendre_precomp(). /// Output compatible with the GSL function gsl_sf_legendre_sphPlm_array(lmax, m, x, yl) /// \param lmax maximum degree computed, \param im = m/MRES with m the SH order, \param x argument, x=cos(theta). /// \param[out] yl is a double array of size (lmax-m+1) filled with the values. static void legendre_sphPlm_array(shtns_cfg shtns, const int lmax, const int im, const double x, double *yl) { double *al; int l, m, ny; double ymm, ymmp1; m = im*MRES; #ifdef LEG_RANGE_CHECK if ( (lmax>LMAX) || (lmaxMMAX) ) shtns_runerr("argument out of range in legendre_sphPlm"); #endif al = alm_im(shtns, im); yl -= m; // shift pointer for (l=m; l<=lmax; ++l) yl[l] = 0.0; // zero out array. ny = 0; ymm = al[0]; if (m>0) ymm = a_sint_pow_n_ext(ymm, x, m, &ny); // l=m, ny <= 0 if (ny==0) yl[m] = ymm; if (lmax==m) return; ymmp1 = ymm * al[1] * x; // l=m+1 if (ny==0) yl[m+1] = ymmp1; if (lmax==m+1) return; l=m+2; al+=2; while ((ny < 0) && (l < lmax)) { // values are negligible => discard. ymm = al[1]*(x*ymmp1) + al[0]*ymm; ymmp1 = al[3]*(x*ymm) + al[2]*ymmp1; l+=2; al+=4; if (fabs(ymm) > 1.0/SHT_SCALE_FACTOR) { // rescale when value is significant ++ny; ymm *= 1.0/SHT_SCALE_FACTOR; ymmp1 *= 1.0/SHT_SCALE_FACTOR; } } while (l < lmax) { // values are unscaled => store ymm = al[1]*(x*ymmp1) + al[0]*ymm; ymmp1 = al[3]*(x*ymm) + al[2]*ymmp1; yl[l] = ymm; yl[l+1] = ymmp1; l+=2; al+=4; } if ((l == lmax) && (ny == 0)) { yl[l] = al[1]*(x*ymmp1) + al[0]*ymm; } } #if HAVE_LONG_DOUBLE_WIDER /// \internal high precision version of \ref legendre_sphPlm_array static void legendre_sphPlm_array_hp(shtns_cfg shtns, const int lmax, const int im, const double cost, double *yl) { double *al; long int l,m; int rescale = 0; // flag for rescale. real ymm, ymmp1, x; if (long_double_caps < 3) { // not worth it. legendre_sphPlm_array(shtns, lmax, im, cost, yl); return; } m = im*MRES; #ifdef LEG_RANGE_CHECK if ((lmax > LMAX)||(lmax < m)||(im>MMAX)) shtns_runerr("argument out of range in legendre_sphPlm_array"); #endif x = cost; al = alm_im(shtns, im); yl -= m; // shift pointer ymm = al[0]; // l=m if (m>0) { if ((lmax <= SHT_L_RESCALE) || (sizeof(ymm) > 8)) { ymm = a_sint_pow_n_hp(ymm, x, m); } else { rescale = 1; ymm *= SHT_LEG_SCALEF; } } yl[m] = ymm; if (lmax==m) goto done; // done. ymmp1 = ymm * al[1] * x; // l=m+1 yl[m+1] = ymmp1; al+=2; if (lmax==m+1) goto done; // done. for (l=m+2; l0 : returns ylm(x)/sin(theta) and d(ylm)/d(theta). /// This way, there are no singularities, everything is well defined for x=[-1,1], for any m. /// \param lmax maximum degree computed, \param im = m/MRES with m the SH order, \param x argument, x=cos(theta). /// \param sint = sqrt(1-x^2) to avoid recomputation of sqrt. /// \param[out] yl is a double array of size (lmax-m+1) filled with the values (divided by sin(theta) if m>0) /// \param[out] dyl is a double array of size (lmax-m+1) filled with the theta-derivatives. static void legendre_sphPlm_deriv_array(shtns_cfg shtns, const int lmax, const int im, const double x, const double sint, double *yl, double *dyl) { double *al; int l,m, ny; double st, y0, y1, dy0, dy1; m = im*MRES; #ifdef LEG_RANGE_CHECK if ((lmax > LMAX)||(lmax < m)||(im>MMAX)) shtns_runerr("argument out of range in legendre_sphPlm_deriv_array"); #endif al = alm_im(shtns, im); yl -= m; dyl -= m; // shift pointers for (l=m; l<=lmax; ++l) { yl[l] = 0.0; dyl[l] = 0.0; // zero out arrays. } ny = 0; st = sint; y0 = al[0]; dy0 = 0.0; if (m>0) { y0 = a_sint_pow_n_ext(y0, x, m-1, &ny); dy0 = x*m*y0; st *= st; // st = sin(theta)^2 is used in the recurrence for m>0 } if (ny==0) { yl[m] = y0; dyl[m] = dy0; // l=m } if (lmax==m) return; // done. y1 = al[1] * (x * y0); dy1 = al[1]*( x*dy0 - st*y0 ); if (ny == 0) { yl[m+1] = y1; dyl[m+1] = dy1; // l=m+1 } if (lmax==m+1) return; // done. l=m+2; al+=2; while ((ny < 0) && (l < lmax)) { // values are negligible => discard. y0 = al[1]*(x*y1) + al[0]*y0; dy0 = al[1]*(x*dy1 - y1*st) + al[0]*dy0; y1 = al[3]*(x*y0) + al[2]*y1; dy1 = al[3]*(x*dy0 - y0*st) + al[2]*dy1; l+=2; al+=4; if (fabs(y0) > 1.0/SHT_SCALE_FACTOR) { // rescale when value is significant ++ny; y0 *= 1.0/SHT_SCALE_FACTOR; dy0 *= 1.0/SHT_SCALE_FACTOR; y1 *= 1.0/SHT_SCALE_FACTOR; dy1 *= 1.0/SHT_SCALE_FACTOR; } } while (l < lmax) { // values are unscaled => store. y0 = al[1]*(x*y1) + al[0]*y0; dy0 = al[1]*(x*dy1 - y1*st) + al[0]*dy0; y1 = al[3]*(x*y0) + al[2]*y1; dy1 = al[3]*(x*dy0 - y0*st) + al[2]*dy1; yl[l] = y0; dyl[l] = dy0; yl[l+1] = y1; dyl[l+1] = dy1; l+=2; al+=4; } if ((l==lmax) && (ny == 0)) { yl[l] = al[1]*(x*y1) + al[0]*y0; dyl[l] = al[1]*(x*dy1 - y1*st) + al[0]*dy0; } } #if HAVE_LONG_DOUBLE_WIDER /// \internal high precision version of \ref legendre_sphPlm_deriv_array static void legendre_sphPlm_deriv_array_hp(shtns_cfg shtns, const int lmax, const int im, const double cost, const double sint, double *yl, double *dyl) { double *al; long int l,m; int rescale = 0; // flag for rescale. real x, st, y0, y1, dy0, dy1; if (long_double_caps < 3) { // not worth it. legendre_sphPlm_deriv_array(shtns, lmax, im, cost, sint, yl, dyl); return; } x = cost; m = im*MRES; #ifdef LEG_RANGE_CHECK if ((lmax > LMAX)||(lmax < m)||(im>MMAX)) shtns_runerr("argument out of range in legendre_sphPlm_deriv_array"); #endif al = alm_im(shtns, im); yl -= m; dyl -= m; // shift pointers st = sint; y0 = al[0]; dy0 = 0.0; if (m>0) { // m > 0 l = m-1; if ((lmax <= SHT_L_RESCALE) || (sizeof(y0) > 8)) { if (l&1) { y0 = a_sint_pow_n_hp(y0, x, l-1) * sint; // avoid computation of sqrt } else y0 = a_sint_pow_n_hp(y0, x, l); } else { rescale = 1; y0 *= SHT_LEG_SCALEF; } dy0 = x*m*y0; st *= st; // st = sin(theta)^2 is used in the recurrence for m>0 } yl[m] = y0; // l=m dyl[m] = dy0; if (lmax==m) goto done; // done. y1 = al[1] * (x * y0); dy1 = al[1]*( x*dy0 - st*y0 ); yl[m+1] = y1; // l=m+1 dyl[m+1] = dy1; if (lmax==m+1) goto done; // done. al+=2; for (l=m+2; l LMAX)||(lmax < m)||(im>MMAX)) shtns_runerr("argument out of range in legendre_sphPlm_deriv_array"); #endif al = alm_im(shtns, im); yl -= m; dyl -= m; // shift pointers y0 = al[0]; yl[m] = y0; dyl[m] = 0.0; // l=m if (lmax==m) return; // done. dy1 = -al[1]*y0; yl[m+1] = 0.0; dyl[m+1] = dy1; // l=m+1 if (lmax==m+1) return; // done. l=m+2; al+=2; while (l < lmax) { y0 = al[0]*y0; dy1 = al[2]*dy1 - al[3]*y0; yl[l] = y0; dyl[l] = 0.0; yl[l+1] = 0.0; dyl[l+1] = dy1; l+=2; al+=4; } if (l==lmax) { yl[l] = al[0]*y0; dyl[l] = 0.0; } } /// \internal Precompute constants for the recursive generation of Legendre associated functions, with given normalization. /// this function is called by \ref shtns_set_size, and assumes up-to-date values in \ref shtns. /// For the same conventions as GSL, use \c legendre_precomp(sht_orthonormal,1); /// \param[in] norm : normalization of the associated legendre functions (\ref shtns_norm). /// \param[in] with_cs_phase : Condon-Shortley phase (-1)^m is included (1) or not (0) /// \param[in] mpos_renorm : Optional renormalization for m>0. /// 1.0 (no renormalization) is the "complex" convention, while 0.5 leads to the "real" convention (with FFTW). static void legendre_precomp(shtns_cfg shtns, enum shtns_norm norm, int with_cs_phase, double mpos_renorm) { double *alm, *blm; long int im, m, l, lm; real t1, t2; #if HAVE_LONG_DOUBLE_WIDER test_long_double(); #endif #if SHT_VERBOSE > 1 if (verbose) { printf(" > Condon-Shortley phase = %d, normalization = %d\n", with_cs_phase, norm); if (long_double_caps == 3) printf(" > long double has extended precision and large exponent\n"); } #endif if (with_cs_phase != 0) with_cs_phase = 1; // force to 1 if !=0 alm = (double *) malloc( (2*NLM)*sizeof(double) ); blm = alm; if ((norm == sht_schmidt) || (mpos_renorm != 1.0)) { blm = (double *) malloc( (2*NLM)*sizeof(double) ); } if ((alm==0) || (blm==0)) shtns_runerr("not enough memory."); shtns->alm = alm; shtns->blm = blm; /// - Compute and store the prefactor (independant of x) of the starting value for the recurrence : /// \f[ Y_m^m(x) = Y_0^0 \ \sqrt{ \prod_{k=1}^{m} \frac{2k+1}{2k} } \ \ (-1)^m \ (1-x^2)^{m/2} \f] if ((norm == sht_fourpi)||(norm == sht_schmidt)) { t1 = 1.0; alm[0] = t1; /// \f$ Y_0^0 = 1 \f$ for Schmidt or 4pi-normalized } else { t1 = 0.25L/M_PIl; alm[0] = SQRT(t1); /// \f$ Y_0^0 = 1/\sqrt{4\pi} \f$ for orthonormal } t1 *= mpos_renorm; // renormalization for m>0 for (im=1, m=0; im<=MMAX; ++im) { while(m For Schmidt semi-normalized t2 = SQRT(2*m+1); alm[lm] /= t2; /// starting value divided by \f$ \sqrt{2m+1} \f$ alm[lm+1] = t2; // l=m+1 lm+=2; for (l=m+2; l<=LMAX; ++l) { t1 = SQRT((l+m)*(l-m)); alm[lm+1] = (2*l-1)/t1; /// \f[ a_l^m = \frac{2l-1}{\sqrt{(l+m)(l-m)}} \f] alm[lm] = - t2/t1; /// \f[ b_l^m = -\sqrt{\frac{(l-1+m)(l-1-m)}{(l+m)(l-m)}} \f] t2 = t1; lm+=2; } } else { /// For orthonormal or 4pi-normalized t2 = 2*m+1; // starting value unchanged. alm[lm+1] = SQRT(2*m+3); // l=m+1 lm+=2; for (l=m+2; l<=LMAX; ++l) { t1 = (l+m)*(l-m); alm[lm+1] = SQRT(((2*l+1)*(2*l-1))/t1); /// \f[ a_l^m = \sqrt{\frac{(2l+1)(2l-1)}{(l+m)(l-m)}} \f] alm[lm] = - SQRT(((2*l+1)*t2)/((2*l-3)*t1)); /// \f[ b_l^m = -\sqrt{\frac{2l+1}{2l-3}\,\frac{(l-1+m)(l-1-m)}{(l+m)(l-m)}} \f] t2 = t1; lm+=2; } } /// - Compute analysis recurrence coefficients if necessary if ((norm == sht_schmidt) || (mpos_renorm != 1.0)) { lm = im*(2*LMAX - (im-1)*MRES); t1 = 1.0; t2 = alm[lm+1]; if (norm == sht_schmidt) { t1 = 2*m+1; t2 *= (2*m+3)/t1; } if (m>0) t1 /= mpos_renorm; blm[lm] = alm[lm]*t1; blm[lm+1] = t2; lm+=2; for (l=m+2; l<=LMAX; ++l) { t1 = alm[lm]; t2 = alm[lm+1]; if (norm == sht_schmidt) { double t3 = 2*l+1; t1 *= t3/(2*l-3); t2 *= t3/(2*l-1); } blm[lm] = t1; blm[lm+1] = t2; lm+=2; } } } } /// \internal returns the value of the Legendre Polynomial of degree l. /// l is arbitrary, a direct recurrence relation is used, and a previous call to legendre_precomp() is not required. static double legendre_Pl(const int l, double x) { long int i; real p, p0, p1; if ((l==0)||(x==1.0)) return ( 1. ); if (x==-1.0) return ( (l&1) ? -1. : 1. ); p0 = 1.0; /// \f$ P_0 = 1 \f$ p1 = x; /// \f$ P_1 = x \f$ for (i=2; i<=l; ++i) { /// recurrence : \f[ l P_l = (2l-1) x P_{l-1} - (l-1) P_{l-2} \f] (works ok up to l=100000) p = ((x*p1)*(2*i-1) - (i-1)*p0)/i; p0 = p1; p1 = p; } return ((double) p1); } /// \internal Generates the abscissa and weights for a Gauss-Legendre quadrature. /// Newton method from initial Guess to find the zeros of the Legendre Polynome /// \param x = abscissa, \param w = weights, \param n points. /// \note Reference: Numerical Recipes, Cornell press. static void gauss_nodes(real *x, real *w, int n) { long int i,l,m, k; real z, z1, p1, p2, p3, pp; real eps; eps = 2.3e-16; // desired precision, minimum = 2.2204e-16 (double) if ((sizeof(eps) > 8) && (long_double_caps > 1)) eps = 1.1e-19; // desired precision, minimum = 1.0842e-19 (long double i387) m = (n+1)/2; for (i=1;i<=m;++i) { k=10; // maximum Newton iteration count to prevent infinite loop. p1 = M_PIl; p2 = 2*n; z = (1.0 - (n-1)/(p2*p2*p2)) * COS((p1*(4*i-1))/(4*n+2)); // initial guess do { p1 = z; // P_1 p2 = 1.0; // P_0 for(l=2;l<=n;++l) { // recurrence : l P_l = (2l-1) z P_{l-1} - (l-1) P_{l-2} (works ok up to l=100000) p3 = p2; p2 = p1; p1 = ((2*l-1)*z*p2 - (l-1)*p3)/l; // The Legendre polynomial... } pp = ((1.-z)*(1.+z))/(n*(p2-z*p1)); // ... and its inverse derivative. z1 = z; z -= p1*pp; // Newton's method } while (( FABS(z-z1) > (z1+z)*0.5*eps ) && (--k > 0)); x[i-1] = z; // Build up the abscissas. w[i-1] = 2.0*pp*pp/((1.-z)*(1.+z)); // Build up the weights. x[n-i] = -z; w[n-i] = w[i-1]; } if (n&1) x[n/2] = 0.0; // exactly zero. #if SHT_VERBOSE > 1 // test integral to compute : if (verbose) { z = 0; for (i=0;i0; i--) { if (((double) x[i]) == ((double) x[i-1])) shtns_runerr("bad gauss points"); } }