Source code for spatial_interpolators.legendre

#!/usr/bin/env python
u"""
legendre.py
Written by Tyler Sutterley (05/2022)
Computes associated Legendre functions of degree l evaluated for elements x
l must be a scalar integer and x must contain real values ranging -1 <= x <= 1
Parallels the MATLAB legendre function

Based on Fortran program by Robert L. Parker, Scripps Institution of
Oceanography, Institute for Geophysics and Planetary Physics, UCSD. 1993

INPUTS:
    l: degree of Legrendre polynomials
    x: elements ranging from -1 to 1
        typically cos(theta), where theta is the colatitude in radians

OUTPUT:
    Pl: legendre polynomials of degree l for orders 0 to l

OPTIONS:
    NORMALIZE: output Fully Normalized Associated Legendre Functions

PYTHON DEPENDENCIES:
    numpy: Scientific Computing Tools For Python (https://numpy.org)
    scipy: Scientific Tools for Python (https://docs.scipy.org/doc/)

REFERENCES:
    M. Abramowitz and I.A. Stegun, "Handbook of Mathematical Functions",
        Dover Publications, 1965, Ch. 8.
    J. A. Jacobs, "Geomagnetism", Academic Press, 1987, Ch.4.

UPDATE HISTORY:
    Updated 05/2022: updated docstrings to numpy documentation format
    Updated 11/2021: modify normalization to prevent high degree overflows
    Updated 05/2021: define int/float precision to prevent deprecation warning
    Updated 02/2021: modify case with underflow
    Updated 09/2020: verify dimensions of x variable
    Updated 07/2020: added function docstrings
    Updated 05/2020: added normalization option for output polynomials
    Updated 03/2019: calculate twocot separately to avoid divide warning
    Written 08/2016
"""
import numpy as np

[docs]def legendre(l, x, NORMALIZE=False): """ Computes associated Legendre functions of degree ``l`` following [Abramowitz1965]_ and [Jacobs1987]_ Parameters ---------- l: int degree of Legrendre polynomials x: float elements ranging from -1 to 1 Typically ``cos(theta)``, where ``theta`` is the colatitude in radians NORMALIZE: bool, default False Fully-normalize the Legendre Functions Returns ------- Pl: legendre polynomials of degree ``l`` References ---------- .. [Abramowitz1965] M. Abramowitz and I. A. Stegun, *Handbook of Mathematical Functions*, 1046 pp., (1965). .. [Jacobs1987] J. A. Jacobs, *Geomagnetism*, Volume 1, 1st Edition, 832 pp., (1987). """ #-- verify integer l = np.int64(l) #-- verify dimensions x = np.atleast_1d(x).flatten() #-- size of the x array nx = len(x) #-- for the l = 0 case if (l == 0): Pl = np.ones((1,nx), dtype=np.float64) return Pl #-- for all other degrees greater than 0 rootl = np.sqrt(np.arange(0,2*l+1))#-- +1 to include 2*l #-- s is sine of colatitude (cosine of latitude) so that 0 <= s <= 1 s = np.sqrt(1.0 - x**2)#-- for x=cos(th): s=sin(th) P = np.zeros((l+3,nx), dtype=np.float64) #-- Find values of x,s for which there will be underflow sn = (-s)**l tol = np.sqrt(np.finfo(np.float64).tiny) count = np.count_nonzero((s > 0) & (np.abs(sn) <= tol)) if (count > 0): ind, = np.nonzero((s > 0) & (np.abs(sn) <= tol)) #-- Approximate solution of x*ln(x) = Pl v = 9.2 - np.log(tol)/(l*s[ind]) w = 1.0/np.log(v) m1 = 1+l*s[ind]*v*w*(1.0058+ w*(3.819 - w*12.173)) m1 = np.where(l < np.floor(m1), l, np.floor(m1)).astype(np.int64) #-- Column-by-column recursion for k,mm1 in enumerate(m1): col = ind[k] #-- Calculate twocot for underflow case twocot = -2.0*x[col]/s[col] P[mm1-1:l+1,col] = 0.0 #-- Start recursion with proper sign tstart = np.finfo(np.float64).eps P[mm1-1,col] = np.sign(np.fmod(mm1,2)-0.5)*tstart if (x[col] < 0): P[mm1-1,col] = np.sign(np.fmod(l+1,2)-0.5)*tstart #-- Recur from m1 to m = 0, accumulating normalizing factor. sumsq = tol.copy() for m in range(mm1-2,-1,-1): P[m,col] = ((m+1)*twocot*P[m+1,col] - \ rootl[l+m+2]*rootl[l-m-1]*P[m+2,col]) / \ (rootl[l+m+1]*rootl[l-m]) sumsq += P[m,col]**2 #-- calculate scale scale = 1.0/np.sqrt(2.0*sumsq - P[0,col]**2) P[0:mm1+1,col] = scale*P[0:mm1+1,col] #-- Find the values of x,s for which there is no underflow, and (x != +/-1) count = np.count_nonzero((x != 1) & (np.abs(sn) >= tol)) if (count > 0): nind, = np.nonzero((x != 1) & (np.abs(sn) >= tol)) #-- Calculate twocot for normal case twocot = -2.0*x[nind]/s[nind] #-- Produce normalization constant for the m = l function d = np.arange(2,2*l+2,2) c = np.prod(1.0 - 1.0/d) #-- Use sn = (-s)**l (written above) to write the m = l function P[l,nind] = np.sqrt(c)*sn[nind] P[l-1,nind] = P[l,nind]*twocot*l/rootl[-1] #-- Recur downwards to m = 0 for m in range(l-2,-1,-1): P[m,nind] = (P[m+1,nind]*twocot*(m+1) - \ P[m+2,nind]*rootl[l+m+2]*rootl[l-m-1]) / \ (rootl[l+m+1]*rootl[l-m]) #-- calculate Pl from P Pl = np.copy(P[0:l+1,:]) #-- Polar argument (x == +/-1) count = np.count_nonzero(s == 0) if (count > 0): s0, = np.nonzero(s == 0) Pl[0,s0] = x[s0]**l #-- calculate Fully Normalized Associated Legendre functions if NORMALIZE: norm = np.zeros((l+1)) norm[0] = np.sqrt(2.0*l+1) m = np.arange(1,l+1) norm[1:] = (-1)**m*np.sqrt(2.0*(2.0*l+1.0)) Pl *= np.kron(np.ones((1,nx)), norm[:,np.newaxis]) else: #-- Calculate the unnormalized Legendre functions by multiplying each row #-- by: sqrt((l+m)!/(l-m)!) == sqrt(prod(n-m+1:n+m)) #-- following Abramowitz and Stegun for m in range(1,l): Pl[m,:] *= np.prod(rootl[l-m+1:l+m+1]) #-- sectoral case (l = m) should be done separately to handle 0! Pl[l,:] *= np.prod(rootl[1:]) return Pl