3131# ======================================================================
3232# DEFINITIONS
3333# ======================================================================
34+ # Try to import pyshtools with proper error handling
35+ try :
36+ import pyshtools
37+ PYSHTOOLS_AVAILABLE = True
38+ except ImportError :
39+ PYSHTOOLS_AVAILABLE = False
40+ print (
41+ f"{ Yellow } __________________\n "
42+ f"Zonal decomposition relies on the pyshtools library, "
43+ f"referenced at:\n \n "
44+ f"Mark A. Wieczorek and Matthias Meschede (2018). "
45+ f"SHTools - Tools for working with spherical harmonics,"
46+ f"Geochemistry, Geophysics, Geosystems, 2574-2592, "
47+ f"doi:10.1029/2018GC007529\n \n Please consult pyshtools "
48+ f"documentation at:\n "
49+ f" { Cyan } https://pypi.org/project/pyshtools\n "
50+ f"{ Yellow } And installation instructions for CAP with pyshtools:\n "
51+ f" { Cyan } https://amescap.readthedocs.io/en/latest/installation."
52+ f"html#_spectral_analysis{ Yellow } \n "
53+ f"__________________{ Nclr } \n \n "
54+ )
55+
3456def diurn_extract (VAR , N , tod , lon ):
3557 """
3658 Extract the diurnal component of a field. Original code by John
@@ -414,3 +436,122 @@ def zeroPhi_filter(VAR, btype, low_highcut, fs, axis=0, order=4,
414436 return VAR_trend + VAR_f
415437 else :
416438 return VAR_f
439+
440+ def zonal_decomposition (VAR ):
441+ """
442+ Decomposition into spherical harmonics. [A. Kling, 2020]
443+
444+ :param VAR: Detrend variable for decomposition. Lat is SECOND to
445+ LAST dimension and lon is LAST (e.g., ``[time,lat,lon]`` or
446+ ``[time,lev,lat,lon]``)
447+
448+ :return: (COEFFS) coefficient for harmonic decomposion, shape is
449+ flattened (e.g., ``[time, 2, lat/2, lat/2]``
450+ ``[time x lev, 2, lat/2, lat/2]``);
451+ (power_per_l) power spectral density, shape is re-organized
452+ (e.g., [time, lat/2] or [time, lev, lat/2])
453+
454+ .. NOTE:: Output size is (``[...,lat/2, lat/2]``) as lat is the
455+ smallest dimension. This matches the Nyquist frequency.
456+
457+ """
458+ if not PYSHTOOLS_AVAILABLE :
459+ raise ImportError (
460+ "This function requires pyshtools. Install with:\n "
461+ "conda install -c conda-forge pyshtools\n "
462+ "or\n "
463+ "pip install amescap[spectral]"
464+ )
465+
466+ var_shape = np .array (VAR .shape )
467+
468+ # Flatten array (e.g., [10, 36, lat, lon] -> [360, lat, lon])
469+ nflatten = int (np .prod (var_shape [:- 2 ]))
470+ reshape_flat = np .append (nflatten , var_shape [- 2 :])
471+ VAR = VAR .reshape (reshape_flat )
472+
473+ coeff_out_shape = np .append (var_shape [0 :- 2 ],
474+ [2 , var_shape [- 2 ]// 2 , var_shape [- 2 ]// 2 ])
475+ psd_out_shape = np .append (var_shape [0 :- 2 ], var_shape [- 2 ]// 2 )
476+ coeff_flat_shape = np .append (nflatten , coeff_out_shape [- 3 :])
477+ COEFFS = np .zeros (coeff_flat_shape )
478+
479+ psd = np .zeros ((nflatten ,var_shape [- 2 ]// 2 ))
480+
481+ for ii in range (0 ,nflatten ):
482+ COEFFS [ii ,...] = pyshtools .expand .SHExpandDH (VAR [ii ,...], sampling = 2 )
483+ psd [ii ,:] = pyshtools .spectralanalysis .spectrum (COEFFS [ii ,...])
484+
485+ return COEFFS , psd .reshape (psd_out_shape )
486+
487+ def zonal_construct (COEFFS_flat , VAR_shape , btype = None , low_highcut = None ):
488+ """
489+ Recomposition into spherical harmonics
490+
491+ :param COEFFS_flat: Spherical harmonic coefficients as a flattened
492+ array, (e.g., ``[time, lat, lon]`` or
493+ ``[time x lev, 2, lat, lon]``)
494+ :type COEFFS_flat: array
495+
496+ :param VAR_shape: shape of the original variable
497+ :type VAR_shape: tuple
498+
499+ :param btype: filter type: "low", "high", or "band". If None,
500+ returns reconstructed array using all zonal wavenumbers
501+ :type btype: str or None
502+
503+ :param low_high_cut: low, high or [low, high] zonal wavenumber
504+ :type low_high_cut: int
505+
506+ :return: detrended variable reconstructed to original size
507+ (e.g., [time, lev, lat, lon])
508+
509+ .. NOTE:: The minimum and maximum wavelenghts in [km] are computed::
510+ dx = 2*np.pi * 3400
511+ L_min = (1./kmax) * dx
512+ L_max = 1./max(kmin, 1.e-20) * dx
513+ if L_max > 1.e20:
514+ L_max = np.inf
515+ print("(kmin,kmax) = ({kmin}, {kmax})
516+ -> dx min = {L_min} km, dx max = {L_max} km")
517+
518+ """
519+ if not PYSHTOOLS_AVAILABLE :
520+ raise ImportError (
521+ "This function requires pyshtools. Install with:\n "
522+ "conda install -c conda-forge pyshtools\n "
523+ "or\n "
524+ "pip install amescap[spectral]"
525+ )
526+
527+ # Initialization
528+ nflatten = COEFFS_flat .shape [0 ]
529+ kmin = 0
530+ kmax = COEFFS_flat .shape [- 1 ]
531+
532+ VAR = np .zeros ((nflatten , VAR_shape [- 2 ], VAR_shape [- 1 ]))
533+
534+ if btype == "low" :
535+ kmax = int (low_highcut )
536+ if btype == "high" :
537+ kmin = int (low_highcut )
538+ if btype == "band" :
539+ kmin , kmax = int (low_highcut [0 ]), int (low_highcut [1 ])
540+
541+ for ii in range (0 , nflatten ):
542+ # Filtering
543+ COEFFS_flat [ii , :, :kmin , :] = 0.
544+ COEFFS_flat [ii , :, kmax :, :] = 0.
545+ VAR [ii , :, :] = pyshtools .expand .MakeGridDH (COEFFS_flat [ii , :, :],
546+ sampling = 2 )
547+ return VAR .reshape (VAR_shape )
548+
549+
550+ def init_shtools ():
551+ """
552+ Check if pyshtools is available and return its availability status
553+
554+ :return: True if pyshtools is available, False otherwise
555+ :rtype: bool
556+ """
557+ return PYSHTOOLS_AVAILABLE
0 commit comments