Source code for gravity_toolkit.sea_level_equation

#!/usr/bin/env python
u"""
sea_level_equation.py (06/2025)
Solves the sea level equation with the option of including polar motion feedback
Uses a Clenshaw summation to calculate the spherical harmonic summation

CALLING SEQUENCE:
    sea_level = sea_level_equation(loadClm, loadSlm, glon, glat, landmask,
        LMAX=LMAX, LOVE=(hl,kl,ll), BODY_TIDE_LOVE=0, FLUID_LOVE=0, POLAR=True,
        ITERATIONS=6, FILL_VALUE=0)

INPUTS:
    loadClm: input set of cosine spherical harmonics
    loadSlm: input set of sine spherical harmonics
    glon: longitude of the land-sea mask used in the sea level solver
    glat: latitude of the land-sea mask used in the sea level solver
    land_function: land-sea mask for use with the sea level solver (land=1)

OUTPUTS:
    sea_level: spatial field calculated using sea level solver

OPTIONS:
    LMAX: Maximum spherical harmonic degree
    LOVE: input load Love numbers up to degree LMAX (hl,kl,ll)
    BODY_TIDE_LOVE: Treatment of the body tide Love number
        0: Wahr (1981) and Wahr (1985) values from PREM
        1: Farrell (1972) values from Gutenberg-Bullen oceanic mantle model
        list or tuple: custom values (k2b,h2b)
    FLUID_LOVE: Treatment of the fluid Love number
        0: Han and Wahr (1989) fluid love number
        1: Munk and MacDonald (1960) secular love number
        2: Munk and MacDonald (1960) fluid love number
        3: Lambeck (1980) fluid love number
        list or tuple: custom value (klf)
    DENSITY: Density of water [g/cm^3]
    POLAR: Include polar feedback
    ITERATIONS: maximum number of iterations for the solver
    PLM: input Legendre polynomials
    FILL_VALUE: value used over land points
    ASTYPE: floating point precision for calculating Clenshaw summation
    SCALE: scaling factor to prevent underflow in Clenshaw summation

PYTHON DEPENDENCIES:
    numpy: Scientific Computing Tools For Python (https://numpy.org)

PROGRAM DEPENDENCIES:
    associated_legendre.py: Computes fully normalized associated
        Legendre polynomials
    gen_harmonics.py: Computes spherical harmonic coefficients from a grid
    units.py: class for converting spherical harmonic data to specific units
    harmonics.py: spherical harmonic data class for processing GRACE/GRACE-FO
    destripe_harmonics.py: calculates the decorrelation (destriping) filter
        and filters the GRACE/GRACE-FO coefficients for striping errors

REFERENCES:
    W. E. Farrell, "Deformation of the Earth by surface loads",
        Reviews of Geophysics, 10(3), 761--797, (1972).
        https://doi.org/10.1029/RG010i003p00761
    W. E. Farrell and J. A. Clark, "On Postglacial Sea Level", Geophysical
        Journal of the Royal Astronomical Society, 46(3), 647--667, (1976).
        https://doi.org/10.1111/j.1365-246X.1976.tb01252.x
    D. Han and J. Wahr, "Post-Glacial Rebound Analysis for a Rotating Earth",
        Slow Deformation and Transmission of Stress in the Earth, 49, (1989).
        https://doi.org/10.1029/GM049p0001
    S. A. Holmes and W. E. Featherstone, "A unified approach to the Clenshaw
        summation and the recursive computation of very high degree and
        order normalised associated Legendre functions",
        Journal of Geodesy, 76, 279--299, (2002).
        https://doi.org/10.1007/s00190-002-0216-2
    R. A. Kendall, J. X. Mitrovica, and G. A. Milne,
        "On post-glacial sea level -- II. Numerical formulation and
        comparative results on spherically symmetric models",
        Geophysical Journal International, 161(3), 679--706, (2005).
        https://doi.org/10.1111/j.1365-246X.2005.02553.x
    K. Lambeck, The Earth's Variable Rotation:
        Geophysical Causes and Consequences, First Edition, (1980).
    J. X. Mitrovica and G. A. Milne,
        "On post-glacial sea level: I. General theory",
        Geophysical Journal International, 154(2), 253--267, (2003).
        https://doi.org/10.1046/j.1365-246X.2003.01942.x
    W. H. Munk and G. J. F. MacDonald, The Rotation of the Earth:
        A Geophysical Discussion, First Edition, (1960).
    C. C. Tscherning and K. Poder, "Some Geodetic Applications of Clenshaw
        Summation", Bollettino di Geodesia e Scienze, 4, 349--375, (1982).
    J. M. Wahr, "Body tides on an elliptical, rotating, elastic and
        oceanless Earth", Geophysical Journal of the Royal Astronomical
        Society, 64(3), 677--703, (1981).
        https://doi.org/10.1111/j.1365-246X.1981.tb02690.x
    J. M. Wahr, "Deformation induced by polar motion", Journal of
        Geophysical Research: Solid Earth, 90(B11), 9363--9368, (1985).
        https://doi.org/10.1029/JB090iB11p09363

UPDATE HISTORY:
    Updated 06/2025: added option to set the density of sea water (g/cm^3)
    Updated 03/2023: improve typing for variables in docstrings
    Updated 01/2023: refactored associated legendre polynomials
    Updated 11/2022: use f-strings for formatting verbose or ascii output
    Updated 04/2022: updated docstrings to numpy documentation format
    Updated 10/2021: using python logging for handling verbose output
        can set custom values for BODY_TIDE_LOVE and FLUID_LOVE
    Updated 05/2021: define int/float precision to prevent deprecation warning
    Updated 01/2021: use harmonics class for spherical harmonic operations
    Updated 08/2020: parameterize float precision to improve computational time
    Updated 07/2020: added function docstrings
    Updated 06/2020: added verbose output of processing run
    Updated 04/2020: reading load love numbers outside of this function
        added option to precompute Legendre Polynomials
    Updated 09/2019: parameters for Munk and MacDonald (1960) fluid love number
    Forked 10/2018 from run_sea_level_equation.py
    Updated 06/2018: using python3 compatible octal and input
    Updated 04/2018: added option to set the gravitational load Love number
        added extrapolation of load love numbers for LMAX > 696
    Updated 03/2018: updated header text.  --format option sets data format
    Updated 02/2018: spherical harmonic order same as spherical harmonic degree
    Updated 09/2017: ocean mask with GEUS Greenland coastlines.  added comments
        more calculations shifted from spatial domain to spherical harmonics
    Updated 08/2017: different landsea mask (small fixes for West Antarctica)
        Clenshaw summation of spherical harmonics to run at 0.5x0.5 degrees
        iterate until convergence of spherical harmonic modulus residuals
    Updated 07/2017: outputs of legendre_polynomials.py include derivatives now
    Updated 04/2017: finished writing algorithms. should be in working condition
        set the permissions mode of the output files with --mode
    Written 09/2016
"""
import logging
import numpy as np
from gravity_toolkit.gen_harmonics import gen_harmonics
from gravity_toolkit.associated_legendre import plm_holmes
from gravity_toolkit.units import units

# PURPOSE: Computes Sea Level Fingerprints including polar motion feedback
[docs] def sea_level_equation(loadClm, loadSlm, glon, glat, land_function, LMAX=0, LOVE=None, BODY_TIDE_LOVE=0, FLUID_LOVE=0, DENSITY=1.0, POLAR=True, ITERATIONS=6, PLM=None, FILL_VALUE=0, ASTYPE=np.longdouble, SCALE=1e-280, **kwargs): r""" Solves the sea level equation with the option of including polar motion feedback :cite:p:`Farrell:1976hm,Kendall:2005ds,Mitrovica:2003cq` Uses a Clenshaw summation to calculate the spherical harmonic summation :cite:p:`Holmes:2002ff,Tscherning:1982tu` Parameters ---------- loadClm: np.ndarray Cosine spherical harmonic coefficients loadSlm: np.ndarray Sine spherical harmonic coefficients glon: np.ndarray longitude of the land-sea mask glat: np.ndarray latitude of the land-sea mask land_function: np.ndarray land-sea mask with land=1 LMAX: int, default 0 Maximum spherical harmonic degree LOVE: tuple or NoneType, default None Load Love numbers up to degree LMAX (``hl``, ``kl``, ``ll``) BODY_TIDE_LOVE: int, default 0 Treatment of the body tide Love number - ``0``: :cite:p:`Wahr:1981ea` and :cite:p:`Wahr:1985gr` values from PREM - ``1``: :cite:p:`Farrell:1972cm` values from Gutenberg-Bullen oceanic mantle model - list or tuple: custom values ``(k2b,h2b)`` FLUID_LOVE: int, default 0 Treatment of the fluid Love number - ``0``: :cite:p:`Han:1989kj` fluid love number - ``1``: :cite:p:`Munk:1960uk` secular love number - ``2``: :cite:p:`Munk:1960uk` fluid love number - ``3``: :cite:p:`Lambeck:1980um` fluid love number - list or tuple: custom value ``(klf)`` DENSITY: float, default 1.0 Density of water [g/cm\ :sup:`3`] POLAR: bool, default True Include polar feedback ITERATIONS: int, default 6 Maximum number of iterations for the solver PLM: np.ndarray or NoneType, default None Legendre polynomials FILL_VALUE: float, default 0 Invalid value used over land points ASTYPE: np.dtype, default np.longdouble Floating point precision for calculating Clenshaw summation SCALE: float, default 1e-280 Scaling factor to prevent underflow in Clenshaw summation Returns ------- sea_level: np.ndarray spatial field calculated using sea level solver """ # dimensions of land function nphi,nth = np.shape(land_function) # calculate colatitude and longitude in radians th = (90.0 - glat)*np.pi/180.0 phi = np.squeeze(glon*np.pi/180.0) # calculate ocean function from land function ocean_function = 1.0 - land_function # indices of the ocean function ii,jj = np.nonzero(ocean_function) # extract arrays of kl, hl, and ll Love Numbers hl,kl,ll = LOVE # density of water [g/cm^3] rho_water = np.float64(DENSITY) # Earth Parameters factors = units(lmax=LMAX) # Average Density of the Earth [g/cm^3] rho_e = factors.rho_e # Average Radius of the Earth [cm] rad_e = factors.rad_e # different treatments of the body tide Love numbers of degree 2 if isinstance(BODY_TIDE_LOVE,(list,tuple)): # use custom defined values k2b,h2b = BODY_TIDE_LOVE elif (BODY_TIDE_LOVE == 0): # Wahr (1981) and Wahr (1985) values from PREM k2b = 0.298 h2b = 0.604 elif (BODY_TIDE_LOVE == 1): # Farrell (1972) values from Gutenberg-Bullen oceanic mantle model k2b = 0.3055 h2b = 0.6149 # different treatments of the fluid Love number of gravitational potential if isinstance(FLUID_LOVE,(list,tuple)): # use custom defined value klf, = FLUID_LOVE elif (FLUID_LOVE == 0): # Han and Wahr (1989) fluid love number # klf = 3.0*G*(C-A)/(rad_e**5*omega**2) # klf = 3.0*G*H0*A/(rad_e**5*omega**2) G = 6.6740e-11# gravitational constant [m^3/(kg*s^2)] Re = 6.371e6# mean radius of the Earth [m] A_moi = 8.0077e+37# mean equatorial moment of inertia [kg m^2] omega = 7.292115e-5# mean rotation rate of the Earth [radians/s] H0 = 0.00328475# dynamical ellipticity (C_moi-A_moi)/A_moi klf = 3.0*G*H0*A_moi*(Re**-5)*(omega**-2) klf = 0.00328475/0.00348118 if (FLUID_LOVE == 1): # Munk and MacDonald (1960) secular love number with IERS and PREM values GM = 3.98004418e14# geocentric gravitational constant [m^3/s^2] Re = 6.371e6# mean radius of the Earth [m] omega = 7.292115e-5# mean rotation rate of the Earth [radians/s] C_moi = 0.33068# reduced polar moment of inertia (C/Ma^2) H = 1.0/305.51# precessional constant (C_moi-A_moi)/C_moi klf = 3.0*GM*H*C_moi/(Re**3*omega**2) elif (FLUID_LOVE == 2): # Munk and MacDonald (1960) fluid love number with IERS and WGS84 values flat = 1.0/298.257223563# flattening of the WGS84 ellipsoid Re = 6.371e6# mean radius of the Earth [m] omega = 7.292115e-5# mean rotation rate of the Earth [radians/s] ge = 9.80665# standard gravity (mean gravitational acceleration) [m/s^2] klf = 2.0*flat*ge/(omega**2*Re) - 1.0 elif (FLUID_LOVE == 3): # Fluid love number from Lambeck (1980) # klf = 3.0*(C-A)*G/(omega**2*rad_e**5) = 3.0*GM*C20/(omega**2*rad_e**3) G = 6.672e-11# gravitational constant [m^3/(kg*s^2)] M = 5.974e+24# mass of the Earth [kg] R = 6.378140e6# equatorial radius of the Earth [m] Re = 6.3710121e6# mean radius of the Earth [m] omega = 7.292115e-5# mean rotation rate of the Earth [radians/s] A_moi = 0.3295*M*R**2# mean equatorial moment of inertia [kg m^2] H = 0.003275# precessional constant (C_moi-A_moi)/C_moi C_moi = -A_moi/(H-1.0)# mean polar moment of inertia [kg m^2] klf = 3.0*(C_moi-A_moi)*G*(omega**-2)*(Re**-5) klf = 0.942 # calculate coefh and coefp for each degree and order # see equation 11 from Tamisiea et al (2010) coefh = np.zeros((LMAX+1,LMAX+1)) coefp = np.zeros((LMAX+1,LMAX+1)) for l in range(LMAX+1): # coefh and coefp will be the same for all orders except for degree 2 # and order 1 (if POLAR motion feedback is included) m = np.arange(0,l+1) coefh[l,m] = 3.0*rho_water*(1.0 + kl[l] - hl[l])/rho_e/np.float64(2*l+1) coefp[l,m] = (1.0 + kl[l] - hl[l])/(kl[l] + 1.0) # if degree 2 and POLAR parameter is set if (l == 2) and POLAR: # calculate coefficient for polar motion feedback and add to coefs # For small perturbations in rotation vector: driving potential # will be dominated by degree two and order one polar wander # effects (quadrantal geometry effects) (Kendall et al., 2005) coefpmf = (1.0 + k2b - h2b)*(1.0 + kl[l])/(klf - k2b) # add effects of polar motion feedback to order 1 coefficients coefh[l,1] += 3.0*rho_water*coefpmf/rho_e/np.float64(2*l+1) coefp[l,1] += coefpmf/(kl[l] + 1.0) # added option to precompute plms to improve computational speed if PLM is None: # calculate Legendre polynomials using Holmes and Featherstone relation PLM, dPLM = plm_holmes(LMAX, np.cos(th)) # calculate sin of colatitudes gth,gphi = np.meshgrid(th, phi) u = np.sin(gth[ii,jj]) # indices of spherical harmonics for calculating eps l1,m1 = np.tril_indices(LMAX+1) # total mass of the surface mass load [g] from harmonics tmass = 4.0*np.pi*(rad_e**3.0)*rho_e*loadClm[0,0]/3.0 # convert ocean function into a series of spherical harmonics ocean_Ylms = gen_harmonics(ocean_function,glon,glat,LMAX=LMAX,PLM=PLM) # total area of ocean calculated by integrating the ocean function ocean_area = 4.0*np.pi*ocean_Ylms.clm[0,0] # uniform distribution as initial guess of the ocean change following # Mitrovica and Peltier (1991) doi:10.1029/91JB01284 # sea level height change sea_height = -tmass/rho_water/rad_e**2/ocean_area # if verbose output: print ocean area and uniform sea level height logging.info(f'Total Ocean Area: {ocean_area:0.10g}') logging.info(f'Uniform Ocean Height: {sea_height:0.10g}') # distribute sea height over ocean harmonics height_Ylms = ocean_Ylms.scale(sea_height) # iterate solutions until convergence or reaching total iterations n_iter = 1 # use maximum eps values from Mitrovica and Peltier (1991) # Milne and Mitrovica (1998) doi:10.1046/j.1365-246X.1998.1331455.x eps = np.inf eps_max = 1e-4 while (eps > eps_max) and (n_iter <= ITERATIONS): # allocate for sea level field of iteration sea_level = np.zeros((nphi,nth)) # calculate combined spherical harmonics for Clenshaw summation clm1 = coefh*height_Ylms.clm + rad_e*coefp*loadClm slm1 = coefh*height_Ylms.slm + rad_e*coefp*loadSlm # calculate clenshaw summations over colatitudes s_m_c = np.zeros((nth,LMAX*2+2)) for m in range(LMAX, -1, -1): s_m_c[:,2*m:2*m+2] = clenshaw_s_m(np.cos(th), m, clm1, slm1, LMAX, ASTYPE=ASTYPE, SCALE=SCALE) # calculate cos(phi) cos_phi_2 = 2.0*np.cos(phi) # matrix of cos/sin m*phi summation cos_m_phi = np.zeros((nphi,LMAX+2),dtype=ASTYPE) sin_m_phi = np.zeros((nphi,LMAX+2),dtype=ASTYPE) # initialize matrix with values at lmax+1 and lmax cos_m_phi[:,LMAX+1] = np.cos(ASTYPE(LMAX + 1)*phi) sin_m_phi[:,LMAX+1] = np.sin(ASTYPE(LMAX + 1)*phi) cos_m_phi[:,LMAX] = np.cos(ASTYPE(LMAX)*phi) sin_m_phi[:,LMAX] = np.sin(ASTYPE(LMAX)*phi) # calculate summation gc=np.multiply(s_m_c[np.newaxis,:,2*LMAX],cos_m_phi[:,np.newaxis,LMAX]) gs=np.multiply(s_m_c[np.newaxis,:,2*LMAX+1],sin_m_phi[:,np.newaxis,LMAX]) s_m = gc[ii,jj] + gs[ii,jj] # iterate to calculate complete summation for m in range(LMAX-1, 0, -1): cos_m_phi[:,m] = cos_phi_2*cos_m_phi[:,m+1] - cos_m_phi[:,m+2] sin_m_phi[:,m] = cos_phi_2*sin_m_phi[:,m+1] - sin_m_phi[:,m+2] a_m = np.sqrt((2.0*m+3.0)/(2.0*m+2.0)) gc=np.multiply(s_m_c[np.newaxis,:,2*m],cos_m_phi[:,np.newaxis,m]) gs=np.multiply(s_m_c[np.newaxis,:,2*m+1],sin_m_phi[:,np.newaxis,m]) s_m = a_m*u*s_m + gc[ii,jj] + gs[ii,jj] # calculate new sea level for iteration gsmc,gcmp = np.meshgrid(s_m_c[:,0],cos_m_phi[:,0]) sea_level[ii,jj] = np.sqrt(3.0)*u*s_m + gsmc[ii,jj] # calculate spherical harmonic field for iteration Ylms = gen_harmonics(sea_level, glon, glat, LMAX=LMAX, PLM=PLM) # total sea level height for iteration # integrated total rmass will differ as sea_level is only over ocean # whereas the crustal and gravitational effects are global rmass = 4.0*np.pi*Ylms.clm[0,0] # mass anomaly converted to ocean height to ensure mass conservation # (this is the gravitational perturbation (Delta Phi)/g) sea_height = (-tmass/rho_water/rad_e**2 - rmass)/ocean_area # if verbose output: print iteration, mass and anomaly for convergence logging.info(f'Iteration: {n_iter:d}') logging.info(f'Integrated Ocean Height: {rmass:0.10g}') logging.info(f'Difference from Initial Height: {sea_height:0.10g}') # geoid component is split into two parts (Kendall 2005) # this part is the spatially uniform shift in the geoid that is # constrained by invoking conservation of mass of the surface load # Equation 48 of Mitrovica and Peltier (1991) # add difference to total sea level field to force mass conservation sea_level += sea_height*ocean_function[:,:] uniform_Ylms = ocean_Ylms.scale(sea_height) Ylms.add(uniform_Ylms) # calculate eps to determine if solution is appropriately converged mod1 = np.sqrt(height_Ylms.clm**2 + height_Ylms.slm**2) mod2 = np.sqrt(Ylms.clm**2 + Ylms.slm**2) eps = np.abs(np.sum(mod2[l1,m1] - mod1[l1,m1])/np.sum(mod1[l1,m1])) # save height harmonics for use in the next iteration height_Ylms = Ylms.copy() # add 1 to n_iter n_iter += 1 # calculate final total mass for sanity check omass = 4.0*np.pi*(rad_e**2.0)*rho_water*height_Ylms.clm[0,0] # if verbose output: sanity check of masses logging.info('Original Total Ocean Mass: {0:0.10g}'.format(-tmass/1e15)) logging.info('Final Iterated Ocean Mass: {0:0.10g}'.format(omass/1e15)) # set final invalid points to fill value if applicable if (FILL_VALUE != 0): ii,jj = np.nonzero(land_function) sea_level[ii,jj] = FILL_VALUE # return the sea level spatial field return sea_level
# PURPOSE: compute Clenshaw summation of the fully normalized associated # Legendre's function for constant order m def clenshaw_s_m(t, m, clm1, slm1, lmax, ASTYPE=np.longdouble, SCALE=1e-280 ): """ Compute conditioned arrays for Clenshaw summation from the fully-normalized associated Legendre's function for an order m Parameters ---------- t: np.ndarray elements ranging from -1 to 1, typically cos(th) m: int spherical harmonic order clm1: np.ndarray cosine spherical harmonics slm1: np.ndarray sine spherical harmonics lmax: int maximum spherical harmonic degree ASTYPE: np.dtype, default np.longdouble floating point precision for calculating Clenshaw summation SCALE: float, default 1e-280 scaling factor to prevent underflow in Clenshaw summation Returns ------- s_m_c: np.ndarray conditioned array for clenshaw summation """ # allocate for output matrix N = len(t) s_m = np.zeros((N,2),dtype=ASTYPE) # scaling to prevent overflow clm = SCALE*clm1.astype(ASTYPE) slm = SCALE*slm1.astype(ASTYPE) # convert lmax and m to float lm = ASTYPE(lmax) mm = ASTYPE(m) if (m == lmax): s_m[:,0] = np.copy(clm[lmax,lmax]) s_m[:,1] = np.copy(slm[lmax,lmax]) elif (m == (lmax-1)): a_lm = np.sqrt(((2.0*lm-1.0)*(2.0*lm+1.0))/((lm-mm)*(lm+mm)))*t s_m[:,0] = a_lm*clm[lmax,lmax-1] + clm[lmax-1,lmax-1] s_m[:,1] = a_lm*slm[lmax,lmax-1] + slm[lmax-1,lmax-1] elif ((m <= (lmax-2)) and (m >= 1)): s_mm_c_pre_2 = np.copy(clm[lmax,m]) s_mm_s_pre_2 = np.copy(slm[lmax,m]) a_lm = np.sqrt(((2.0*lm-1.0)*(2.0*lm+1.0))/((lm-mm)*(lm+mm)))*t s_mm_c_pre_1 = a_lm*s_mm_c_pre_2 + clm[lmax-1,m] s_mm_s_pre_1 = a_lm*s_mm_s_pre_2 + slm[lmax-1,m] for l in range(lmax-2, m-1, -1): ll = ASTYPE(l) a_lm=np.sqrt(((2.0*ll+1.0)*(2.0*ll+3.0))/((ll+1.0-mm)*(ll+1.0+mm)))*t b_lm=np.sqrt(((2.*ll+5.)*(ll+mm+1.)*(ll-mm+1.))/((ll+2.-mm)*(ll+2.+mm)*(2.*ll+1.))) s_mm_c = a_lm * s_mm_c_pre_1 - b_lm * s_mm_c_pre_2 + clm[l,m] s_mm_s = a_lm * s_mm_s_pre_1 - b_lm * s_mm_s_pre_2 + slm[l,m] s_mm_c_pre_2 = np.copy(s_mm_c_pre_1) s_mm_s_pre_2 = np.copy(s_mm_s_pre_1) s_mm_c_pre_1 = np.copy(s_mm_c) s_mm_s_pre_1 = np.copy(s_mm_s) s_m[:,0] = np.copy(s_mm_c) s_m[:,1] = np.copy(s_mm_s) elif (m == 0): s_mm_c_pre_2 = np.copy(clm[lmax,0]) a_lm = np.sqrt(((2.0*lm-1.0)*(2.0*lm+1.0))/(lm*lm))*t s_mm_c_pre_1 = a_lm * s_mm_c_pre_2 + clm[lmax-1,0] for l in range(lmax-2, m-1, -1): ll = ASTYPE(l) a_lm=np.sqrt(((2.0*ll+1.0)*(2.0*ll+3.0))/((ll+1.0)*(ll+1.0)))*t b_lm=np.sqrt(((2.0*ll+5.0)*(ll+1.0)*(ll+1.0))/((ll+2.0)*(ll+2.0)*(2.0*ll+1.0))) s_mm_c = a_lm * s_mm_c_pre_1 - b_lm * s_mm_c_pre_2 + clm[l,0] s_mm_c_pre_2 = np.copy(s_mm_c_pre_1) s_mm_c_pre_1 = np.copy(s_mm_c) s_m[:,0] = np.copy(s_mm_c) # return rescaled s_m return s_m/SCALE