Tropical Waves Identification and Analysis#

本單元主要示範如何利用Python工具判識熱帶波動訊號,而非提供熱帶波動理論完整的回顧與介紹。完整的理論介紹請參照下方所列的參考文獻。

熱帶波動理論簡介#

熱帶波動是主導熱帶地區季內尺度變異的系統,它的理論最早在 Matsuno (1966) 文章中提出,這篇文章從熱帶 \(\beta \) 面上的淺水方程 (shallow water equations) 出發,導出了熱帶波動頻散關係的理論解:

\[ \omega ^{2} - k^2 - \dfrac{k}{\omega } = 2n+1, \qquad n = 1, 2, ... \]

若將頻散關係方程式繪圖,可以得到如下圖的結果。

https://agupubs.onlinelibrary.wiley.com/cms/asset/15874aea-bc1f-4385-b84c-26698dd2748d/rog1687-fig-0002.png

Fig. 1 Dispersion curves for equatorial waves (up to \(n = 4\)) as a function of the nondimensional frequency, \(\omega ^{*}\), and nondimensional zonal wave number, \(k^{*}\), where \(\omega ^{*} \equiv \left( \omega /\beta \sqrt{gh_e} \right) ^{1/2}\), and \(k^{*} \equiv k\left( \sqrt{gh_e} / \beta \right) ^{1/2}\). For all but the Kelvin wave, these dispersion curves are solutions of equation (1). Westward propagating waves (relative to the zero basic state) appear on the left, and eastward propagating waves appear on the right. For consistency with equation (1), the Kelvin wave solution is labeled as \(n = −1\). (Source: Kiladis et al. (2009))#

在這張圖中,\(k^{*} > 0\) 的部分代表往東傳遞的波動,\(k^{*} < 0\) 則代表往西傳遞的波動。隨後,許多觀測實驗證實了熱帶確實有這些往東和往西的波動存在,以及當衛星觀測和再分析資料越來越盛行後,從波譜分析的結果也可以發現波譜的峰值區域和理論解非常吻合,甚至可以根據波譜分析的結果進行適當的濾波來得到熱帶波動的資料。而從淺水方程式導出的波動的動力結構如下圖所示:

https://agupubs.onlinelibrary.wiley.com/cms/asset/e5386eb7-680f-46b0-9887-1f9389a70417/rog1687-fig-0003.png

Fig. 2 Horizontal structures of a subset of the zonally propagating wave solutions to the shallow water equations on an equatorial \(\beta \) plane (equations (1)–(3)). Each is shown for a nondimensional zonal wave number, \(k^{*} = \pm 1\). All scales and fields have been nondimensionalized by taking the units of time and length as given in the caption of Figure 1. The equator runs through the center of each diagram. Hatching is for divergence, and shading is for convergence, with a 0.6 unit interval between successive levels. Unshaded contours are geopotential, with a contour interval of 0.5 units. Negative contours are dashed, and the zero contour is omitted. The maximum wind vectors in each panel are specified in the bottom right corner. (Source: Kiladis et al. (2009))#

以下會介紹兩種常用的分析方法,其一是 Wheeler and Kiladis (1999) 提出的時空濾波法 (space-time filtering technique),其二是 Yang et al. (2003) 提出的雙曲柱狀函數 (PCF) 投影法 (2D spatial projection on the parabolic cylinder functions, 2DS-PCF) 方法。其他還有許多熱帶波動辨識的方法,詳細的介紹與回顧請見 Knippertz et al. (2022)。

時空濾波 (Space-time filtering)#

時空濾波即在資料的經度和時間兩個維度軸上進行快速傅立葉變換 (fast Fourier transform, FFT),然後將對應要保留的波段留下來,其他設為0,再進行反變換 (reversed FFT),就可以得到濾波的資料。這個方法可以用在任何變數上,非常彈性。圖2為外逸長波輻射 (OLR) 時空頻譜的範例,可以看到幾個訊號特別強的地方,就是理論上熱帶波動的頻譜範圍。

images/fig.A6-1_tw_ps.png

Fig. 3 The wavenumber (\(k\))-frequency (\(\omega \)) spectra of OLR data from 1981 to 2018 between 15°S and 15°N, without symmetric partition. The thick polygons indicate domains for the filtered MJO, Kelvin waves, ER waves, and MRG waves/TD-type disturbances. Source: Tsai et al. (2020).#

時空濾波的程式由 Carl Schreck III 團隊提供 (https://ncics.org/portfolio/monitor/mjo/)。程序簡單介紹如下:

  1. 計算資料的距平,其中氣候場只保留日氣候平均的前三個球諧函數 (請見第六單元)。

  2. 將資料的距平帶入時空濾波函數。

Note

輸入時空濾波的資料,經度範圍必須包含全球的範圍!

請先在程式建立以下時空濾波的函式套件 kf_filter

#################################################
# MODULE: kf_filter
#################################################
def kf_filter(inData,obsPerDay,tMin,tMax,kMin,kMax,hMin,hMax,waveName):
    
    import numpy as np
    from scipy import signal
    import math
    
    mis = -999.
    
    timeDim = inData.shape[0]
    lonDim = inData.shape[1]
	
    # Reshape data from [time,lon] to [lon,time]
    originalData=np.zeros([lonDim,timeDim],dtype='f')
    for counterX in range(timeDim):
        test=0
        for counterY in range(lonDim-1,-1,-1):
            originalData[test,counterX]=inData[counterX,counterY]
            test+=1
	
    # Detrend the Data
    detrendData=np.zeros([lonDim,timeDim],dtype='f')
    for counterX in range(lonDim):
        detrendData[counterX,:]=signal.detrend(originalData[counterX,:])
	
    # Taper 
    taper=signal.tukey(timeDim,0.05,True)
    taperData=np.zeros([lonDim,timeDim],dtype='f')
    for counterX in range(lonDim):
        taperData[counterX,:]=detrendData[counterX,:]*taper
	
    # Perform 2-D Fourier Transform
    fftData=np.fft.rfft2(taperData)
    kDim=lonDim 
    freqDim=round(fftData.shape[1])
	
    # Find the indeces for the period cut-offs
    jMin = int(round( ( timeDim * 1. / ( tMax * obsPerDay ) ), 0 ))
    jMax = int(round( ( timeDim * 1. / ( tMin * obsPerDay ) ), 0 ))
    jMax = min( ( jMax, freqDim ) )

    # Find the indices for the wavenumber cut-offs
    # This is more complicated because east and west are separate
    if( kMin < 0 ):
        iMin = round( ( kDim + kMin ), 3 )
        iMin = max( ( iMin, ( kDim / 2 ) ) )
    else:
        iMin = round( kMin, 3 )
        iMin = min( ( iMin, ( kDim / 2 ) ) )

    if( kMax < 0 ):
        iMax = round( ( kDim + kMax ), 3 )
        iMax = max( ( iMax, ( kDim / 2 ) ) )
    else:
        iMax = round( kMax, 3 )
        iMax = min( ( iMax, ( kDim / 2 ) ) )
	  
    # set the appropriate coefficients to zero
    iMin=int(iMin)
    iMax=int(iMax)
    jMin=int(jMin)
    jMax=int(jMax)
    if( jMin > 0 ):
        fftData[:, :jMin-1 ] = 0
    if( jMax < ( freqDim - 1 ) ):
        fftData[:, jMax+1: ] = 0

    if( iMin < iMax ):
        # Set things outside the range to zero, this is more normal
        if( iMin > 0 ):
            fftData[:iMin-1, : ] = 0
        if( iMax < ( kDim - 1 ) ):
            fftData[iMax+1:, : ] = 0
    else:
        # Set things inside the range to zero, this should be somewhat unusual
        fftData[iMax+1:iMin-1, : ] = 0
	
    # Find constants
    PI = math.pi
    beta = 2.28e-11
    if hMin != -999:
        cMin = float( 9.8 * float(hMin) )**0.5
    else:
        cMin=hMin
    if hMax != -999:
        cMax = float( 9.8 * float(hMax) )**0.5
    else:
        cMax=hMax
    c = np.array([cMin,cMax])
    spc = 24 * 3600. / ( 2 * PI * obsPerDay ) # seconds per cycle
	
    # Now set things to zero that are outside the Kelvin dispersion
    for i in range(0,kDim):
        # Find Non-Dimensional WaveNumber (k)
        if( i > ( kDim / 2 ) ):
            # k is negative
            k = ( i - kDim  ) * 1. / (6.37e6) # adjusting for circumfrence of earth
        else:
            # k is positive
            k = i * 1. / (6.37e6) # adjusting for circumfrence of earth

        # Find Frequency
        freq = np.array([ 0, freqDim * (1. / spc) ]) #waveName='None'
        jMinWave = 0
        jMaxWave = freqDim	
        if waveName.lower() == "kelvin":
            freq = k * c
        if waveName.lower() == "er":
            freq = -beta * k / ( k**2 + 3. * beta / c )
        if waveName.lower() == "ig1":
            freq = ( 3 * beta * c + k**2 * c**2 )**0.5
        if waveName.lower() == "ig2":
            freq = ( 5 * beta * c + k**2 * c**2 )**0.5
        if waveName.lower() == "mrg" or waveName.lower()=="ig0":   	
           if( k == 0 ):
               freq = ( beta * c )**0.5
           else:
               if( k > 0):
                   freq = k * c * ( 0.5 + 0.5 * ( 1 + 4 * beta / ( k**2 * c ) )**0.5 )
               else:
                freq = k * c * ( 0.5 - 0.5 * ( 1 + 4 * beta / ( k**2 * c ) )**0.5 )	
		
        # Get Min/Max Wave 
        if(hMin==mis):
            jMinWave = 0
        else:
            jMinWave = int( math.floor( freq[0] * spc * timeDim ) )

        if(hMax==mis):
            jMaxWave = freqDim
        else:
            jMaxWave = int( math.ceil( freq[1] * spc * timeDim ) )

        jMaxWave = max(jMaxWave, 0)
        jMinWave = min(jMinWave, freqDim)
		
        # set the appropriate coefficients to zero
        i=int(i)
        jMinWave=int(jMinWave)
        jMaxWave=int(jMaxWave)
        if( jMinWave > 0 ):
            fftData[i, :jMinWave-1] = 0
        if( jMaxWave < ( freqDim - 1 ) ):
            fftData[i, jMaxWave+1:] = 0
	
    # perform the inverse transform to reconstruct the data
    returnedData=np.fft.irfft2(fftData) 
	
    # Reshape data from [lon,time] to [time,lon]
    outData=np.zeros([timeDim,lonDim],dtype='f')
    for counterX in range(returnedData.shape[1]):
        test=0
        for counterY in range(lonDim-1,-1,-1):
            outData[counterX,counterY]=returnedData[test,counterX] 
            test+=1
    
    # Return Result
    return (outData)

以及計算氣候場前三個球諧函數 (這部分和第六單元是完全一樣的)。

def smthClmDay(clmDay, nHarm):
    
    from scipy.fft import rfft, irfft
    
    nt, ny, nx = clmDay.shape
    cf = rfft(clmDay.values, axis=0)     # xarray.DataArray.values 可將DataArray 轉換成numpy.ndarray。
    cf[nHarm,:,:] = 0.5*cf[nHarm,:,:]    # mini-taper.
    cf[nHarm+1:,:,:] = 0.0               # set all higher coef to 0.0
    icf = irfft(cf, n=nt, axis=0)       # reconstructed series
    clmDaySmth = clmDay.copy(data=icf, deep=False)
    return(clmDaySmth)

接著我們讀取OLR資料,計算其距平。

# Import modules
import numpy as np
import xarray as xr

lats, latn = -20, 20 # 只選擇熱帶範圍
name = 'olra'

olr = xr.open_dataset('./data/olr.nc').sel(lat=slice(lats,latn)).olr
olrDayClm = olr.groupby('time.dayofyear').mean('time')
olrDayClm_sm = smthClmDay(olrDayClm, 3)
# 只選2017, 2018年做範例,可以選任意時間長度 (建議至少1年)
olra = olr.sel(time=slice('2017-01-01','2018-12-31')).groupby('time.dayofyear') - olrDayClm_sm 

olra
---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
Cell In[3], line 9
      6 name = 'olra'
      8 olr = xr.open_dataset('./data/olr.nc').sel(lat=slice(lats,latn)).olr
----> 9 olrDayClm = olr.groupby('time.dayofyear').mean('time')
     10 olrDayClm_sm = smthClmDay(olrDayClm, 3)
     11 # 只選2017, 2018年做範例,可以選任意時間長度 (建議至少1年)

File /data/wtsai/micromamba/p3t/lib/python3.10/site-packages/xarray/core/_aggregations.py:7229, in DataArrayGroupByAggregations.mean(self, dim, skipna, keep_attrs, **kwargs)
   7147 """
   7148 Reduce this DataArray's data by applying ``mean`` along some dimension(s).
   7149 
   (...)
   7222   * labels   (labels) object 24B 'a' 'b' 'c'
   7223 """
   7224 if (
   7225     flox_available
   7226     and OPTIONS["use_flox"]
   7227     and contains_only_chunked_or_numpy(self._obj)
   7228 ):
-> 7229     return self._flox_reduce(
   7230         func="mean",
   7231         dim=dim,
   7232         skipna=skipna,
   7233         # fill_value=fill_value,
   7234         keep_attrs=keep_attrs,
   7235         **kwargs,
   7236     )
   7237 else:
   7238     return self.reduce(
   7239         duck_array_ops.mean,
   7240         dim=dim,
   (...)
   7243         **kwargs,
   7244     )

File /data/wtsai/micromamba/p3t/lib/python3.10/site-packages/xarray/core/groupby.py:1093, in GroupBy._flox_reduce(self, dim, keep_attrs, **kwargs)
   1088 expected_groups = tuple(
   1089     pd.RangeIndex(len(grouper)) for grouper in self.groupers
   1090 )
   1092 codes = tuple(g.codes for g in self.groupers)
-> 1093 result = xarray_reduce(
   1094     obj.drop_vars(non_numeric.keys()),
   1095     *codes,
   1096     dim=parsed_dim,
   1097     expected_groups=expected_groups,
   1098     isbin=False,
   1099     keep_attrs=keep_attrs,
   1100     **kwargs,
   1101 )
   1103 # we did end up reducing over dimension(s) that are
   1104 # in the grouped variable
   1105 group_dims = set(grouper.group.dims)

File /data/wtsai/micromamba/p3t/lib/python3.10/site-packages/flox/xarray.py:410, in xarray_reduce(obj, func, expected_groups, isbin, sort, dim, fill_value, dtype, method, engine, keep_attrs, skipna, min_count, reindex, *by, **finalize_kwargs)
    407 output_sizes = group_sizes
    408 output_sizes.update({dim.name: dim.size for dim in newdims if dim.size != 0})
--> 410 actual = xr.apply_ufunc(
    411     wrapper,
    412     ds_broad.drop_vars(tuple(missing_dim)).transpose(..., *grouper_dims),
    413     *by_da,
    414     input_core_dims=input_core_dims,
    415     # for xarray's test_groupby_duplicate_coordinate_labels
    416     exclude_dims=set(dim_tuple),
    417     output_core_dims=[output_core_dims],
    418     dask="allowed",
    419     dask_gufunc_kwargs=dict(
    420         output_sizes=output_sizes,
    421         output_dtypes=[dtype] if dtype is not None else None,
    422     ),
    423     keep_attrs=keep_attrs,
    424     kwargs={
    425         "func": func,
    426         "axis": axis,
    427         "sort": sort,
    428         "fill_value": fill_value,
    429         "method": method,
    430         "min_count": min_count,
    431         "skipna": skipna,
    432         "engine": engine,
    433         "reindex": reindex,
    434         "expected_groups": tuple(expected_groups_valid_list),
    435         "isbin": isbins,
    436         "finalize_kwargs": finalize_kwargs,
    437         "dtype": dtype,
    438         "core_dims": input_core_dims,
    439     },
    440 )
    442 # restore non-dim coord variables without the core dimension
    443 # TODO: shouldn't apply_ufunc handle this?
    444 for var in set(ds_broad._coord_names) - set(ds_broad._indexes) - set(ds_broad.dims):

File /data/wtsai/micromamba/p3t/lib/python3.10/site-packages/xarray/core/computation.py:1258, in apply_ufunc(func, input_core_dims, output_core_dims, exclude_dims, vectorize, join, dataset_join, dataset_fill_value, keep_attrs, kwargs, dask, output_dtypes, output_sizes, meta, dask_gufunc_kwargs, on_missing_core_dim, *args)
   1256 # feed datasets apply_variable_ufunc through apply_dataset_vfunc
   1257 elif any(is_dict_like(a) for a in args):
-> 1258     return apply_dataset_vfunc(
   1259         variables_vfunc,
   1260         *args,
   1261         signature=signature,
   1262         join=join,
   1263         exclude_dims=exclude_dims,
   1264         dataset_join=dataset_join,
   1265         fill_value=dataset_fill_value,
   1266         keep_attrs=keep_attrs,
   1267         on_missing_core_dim=on_missing_core_dim,
   1268     )
   1269 # feed DataArray apply_variable_ufunc through apply_dataarray_vfunc
   1270 elif any(isinstance(a, DataArray) for a in args):

File /data/wtsai/micromamba/p3t/lib/python3.10/site-packages/xarray/core/computation.py:529, in apply_dataset_vfunc(func, signature, join, dataset_join, fill_value, exclude_dims, keep_attrs, on_missing_core_dim, *args)
    524 list_of_coords, list_of_indexes = build_output_coords_and_indexes(
    525     args, signature, exclude_dims, combine_attrs=keep_attrs
    526 )
    527 args = tuple(getattr(arg, "data_vars", arg) for arg in args)
--> 529 result_vars = apply_dict_of_variables_vfunc(
    530     func,
    531     *args,
    532     signature=signature,
    533     join=dataset_join,
    534     fill_value=fill_value,
    535     on_missing_core_dim=on_missing_core_dim,
    536 )
    538 out: Dataset | tuple[Dataset, ...]
    539 if signature.num_outputs > 1:

File /data/wtsai/micromamba/p3t/lib/python3.10/site-packages/xarray/core/computation.py:453, in apply_dict_of_variables_vfunc(func, signature, join, fill_value, on_missing_core_dim, *args)
    451 core_dim_present = _check_core_dims(signature, variable_args, name)
    452 if core_dim_present is True:
--> 453     result_vars[name] = func(*variable_args)
    454 else:
    455     if on_missing_core_dim == "raise":

File /data/wtsai/micromamba/p3t/lib/python3.10/site-packages/xarray/core/computation.py:733, in apply_variable_ufunc(func, signature, exclude_dims, dask, output_dtypes, vectorize, keep_attrs, dask_gufunc_kwargs, *args)
    728 broadcast_dims = tuple(
    729     dim for dim in dim_sizes if dim not in signature.all_core_dims
    730 )
    731 output_dims = [broadcast_dims + out for out in signature.output_core_dims]
--> 733 input_data = [
    734     (
    735         broadcast_compat_data(arg, broadcast_dims, core_dims)
    736         if isinstance(arg, Variable)
    737         else arg
    738     )
    739     for arg, core_dims in zip(args, signature.input_core_dims, strict=True)
    740 ]
    742 if any(is_chunked_array(array) for array in input_data):
    743     if dask == "forbidden":

File /data/wtsai/micromamba/p3t/lib/python3.10/site-packages/xarray/core/computation.py:735, in <listcomp>(.0)
    728 broadcast_dims = tuple(
    729     dim for dim in dim_sizes if dim not in signature.all_core_dims
    730 )
    731 output_dims = [broadcast_dims + out for out in signature.output_core_dims]
    733 input_data = [
    734     (
--> 735         broadcast_compat_data(arg, broadcast_dims, core_dims)
    736         if isinstance(arg, Variable)
    737         else arg
    738     )
    739     for arg, core_dims in zip(args, signature.input_core_dims, strict=True)
    740 ]
    742 if any(is_chunked_array(array) for array in input_data):
    743     if dask == "forbidden":

File /data/wtsai/micromamba/p3t/lib/python3.10/site-packages/xarray/core/computation.py:656, in broadcast_compat_data(variable, broadcast_dims, core_dims)
    651 def broadcast_compat_data(
    652     variable: Variable,
    653     broadcast_dims: tuple[Hashable, ...],
    654     core_dims: tuple[Hashable, ...],
    655 ) -> Any:
--> 656     data = variable.data
    658     old_dims = variable.dims
    659     new_dims = broadcast_dims + core_dims

File /data/wtsai/micromamba/p3t/lib/python3.10/site-packages/xarray/core/variable.py:415, in Variable.data(self)
    413     return self._data
    414 elif isinstance(self._data, indexing.ExplicitlyIndexed):
--> 415     return self._data.get_duck_array()
    416 else:
    417     return self.values

File /data/wtsai/micromamba/p3t/lib/python3.10/site-packages/xarray/core/indexing.py:734, in LazilyVectorizedIndexedArray.get_duck_array(self)
    729 # self.array[self.key] is now a numpy array when
    730 # self.array is a BackendArray subclass
    731 # and self.key is BasicIndexer((slice(None, None, None),))
    732 # so we need the explicit check for ExplicitlyIndexed
    733 if isinstance(array, ExplicitlyIndexed):
--> 734     array = array.get_duck_array()
    735 return _wrap_numpy_scalars(array)

File /data/wtsai/micromamba/p3t/lib/python3.10/site-packages/xarray/coding/variables.py:81, in _ElementwiseFunctionArray.get_duck_array(self)
     80 def get_duck_array(self):
---> 81     return self.func(self.array.get_duck_array())

File /data/wtsai/micromamba/p3t/lib/python3.10/site-packages/xarray/core/indexing.py:728, in LazilyVectorizedIndexedArray.get_duck_array(self)
    724     array = apply_indexer(self.array, self.key)
    725 else:
    726     # If the array is not an ExplicitlyIndexedNDArrayMixin,
    727     # it may wrap a BackendArray so use its __getitem__
--> 728     array = self.array[self.key]
    729 # self.array[self.key] is now a numpy array when
    730 # self.array is a BackendArray subclass
    731 # and self.key is BasicIndexer((slice(None, None, None),))
    732 # so we need the explicit check for ExplicitlyIndexed
    733 if isinstance(array, ExplicitlyIndexed):

File /data/wtsai/micromamba/p3t/lib/python3.10/site-packages/xarray/backends/netCDF4_.py:103, in NetCDF4ArrayWrapper.__getitem__(self, key)
    102 def __getitem__(self, key):
--> 103     return indexing.explicit_indexing_adapter(
    104         key, self.shape, indexing.IndexingSupport.OUTER, self._getitem
    105     )

File /data/wtsai/micromamba/p3t/lib/python3.10/site-packages/xarray/core/indexing.py:1012, in explicit_indexing_adapter(key, shape, indexing_support, raw_indexing_method)
    985 def explicit_indexing_adapter(
    986     key: ExplicitIndexer,
    987     shape: _Shape,
    988     indexing_support: IndexingSupport,
    989     raw_indexing_method: Callable[..., Any],
    990 ) -> Any:
    991     """Support explicit indexing by delegating to a raw indexing method.
    992 
    993     Outer and/or vectorized indexers are supported by indexing a second time
   (...)
   1010     Indexing result, in the form of a duck numpy-array.
   1011     """
-> 1012     raw_key, numpy_indices = decompose_indexer(key, shape, indexing_support)
   1013     result = raw_indexing_method(raw_key.tuple)
   1014     if numpy_indices.tuple:
   1015         # index the loaded np.ndarray

File /data/wtsai/micromamba/p3t/lib/python3.10/site-packages/xarray/core/indexing.py:1045, in decompose_indexer(indexer, shape, indexing_support)
   1041 def decompose_indexer(
   1042     indexer: ExplicitIndexer, shape: _Shape, indexing_support: IndexingSupport
   1043 ) -> tuple[ExplicitIndexer, ExplicitIndexer]:
   1044     if isinstance(indexer, VectorizedIndexer):
-> 1045         return _decompose_vectorized_indexer(indexer, shape, indexing_support)
   1046     if isinstance(indexer, BasicIndexer | OuterIndexer):
   1047         return _decompose_outer_indexer(indexer, shape, indexing_support)

File /data/wtsai/micromamba/p3t/lib/python3.10/site-packages/xarray/core/indexing.py:1147, in _decompose_vectorized_indexer(indexer, shape, indexing_support)
   1144         np_indexer_elems.append(vind.reshape(*k.shape))
   1146 backend_indexer = OuterIndexer(tuple(backend_indexer_elems))
-> 1147 np_indexer = VectorizedIndexer(tuple(np_indexer_elems))
   1149 if indexing_support is IndexingSupport.OUTER:
   1150     return backend_indexer, np_indexer

File /data/wtsai/micromamba/p3t/lib/python3.10/site-packages/xarray/core/indexing.py:489, in VectorizedIndexer.__init__(self, key)
    484         ndims = [k.ndim for k in key if isinstance(k, np.ndarray)]
    485         raise ValueError(
    486             "invalid indexer key: ndarray arguments "
    487             f"have different numbers of dimensions: {ndims}"
    488         )
--> 489     k = k.astype(np.int64)  # type: ignore[union-attr]
    490 else:
    491     raise TypeError(
    492         f"unexpected indexer type for {type(self).__name__}: {k!r}"
    493     )

KeyboardInterrupt: 

接著,建立各個波段的空DataArray,然後設定各個波段的波數、週期、equivalent depth參數。其中,equivalent depth只適用有頻散關係理論解的波動 (Kelvin, ER, MRG),其他波動的請設定為mis

# wave filter
mis = -999.
obsPerDay = 1.
lat = olra.lat.values

lf     = xr.DataArray(dims=['time','lat','lon'],coords=dict(time=olra.time,lat=olra.lat,lon=olra.lon))
mjo    = xr.DataArray(dims=['time','lat','lon'],coords=dict(time=olra.time,lat=olra.lat,lon=olra.lon))
er     = xr.DataArray(dims=['time','lat','lon'],coords=dict(time=olra.time,lat=olra.lat,lon=olra.lon))
kelvin = xr.DataArray(dims=['time','lat','lon'],coords=dict(time=olra.time,lat=olra.lat,lon=olra.lon))
mt     = xr.DataArray(dims=['time','lat','lon'],coords=dict(time=olra.time,lat=olra.lat,lon=olra.lon))
mrg    = xr.DataArray(dims=['time','lat','lon'],coords=dict(time=olra.time,lat=olra.lat,lon=olra.lon))

# lf parameters 
lf_filter="above 120 days"
lf_wavenumber=np.array([mis,mis],dtype='f')
lf_period=np.array([120,mis],dtype='f')
lf_depth=np.array([mis*-1.,mis],dtype='f')

# mjo parameters 
mjo_filter="Kiladis et al. (2005 JAS) for 20-100"
mjo_wavenumber=np.array([1,5],dtype='f')
mjo_period=np.array([30,96],dtype='f')
mjo_depth=np.array([mis,mis],dtype='f')
    
# er parameters 
er_filter="Kiladis et al. (2009 Rev. Geophys.)"
er_wavenumber=np.array([-10,-1],dtype='f')
er_period=np.array([9.7,48],dtype='f')
er_depth=np.array([8,90],dtype='f')

# kw parameters
kelvin_filter="Straub & Kiladis (2002) to 20 days"
kelvin_wavenumber=np.array([1,14],dtype='f')
kelvin_period=np.array([2.5,30],dtype='f')
kelvin_depth=np.array([8,90],dtype='f')

# mt parameters
mt_filter="MRG-TD type wave filter (Frank & Roundy 2006)"
mt_wavenumber=np.array([-14,0],dtype='f')
mt_period=np.array([2.5,10],dtype='f')
mt_depth=np.array([mis,mis],dtype='f')

# mrg parameters
mrg_filter="MRG-TD type wave filter"
mrg_wavenumber=np.array([-10,-1],dtype='f')
mrg_period=np.array([3,9.6],dtype='f')
mrg_depth=np.array([8,90],dtype='f')

接著一個緯度一個緯度進行2D-FFT的濾波。

其中,kf_filter (inData,obsPerDay,tMin,tMax,kMin,kMax,hMin,hMax,waveName)

  • inData代表輸入的資料,

  • obsPerDay 代表一天有幾筆觀測資料 (即資料的取樣頻率),

  • tMintMax代表濾波頻段週期的最小最大值,

  • kMinkMax代表波數範圍最小最大值,

  • hMinhMax代表equivalent depth的最小最大值,

  • waveName是波動的名稱,如果Kelvin, ER, MRG以外的系統,由於程式中沒有設定好的頻散關係,請直接寫none即可。

for y in lat: 
    #print('latitude: ' + str(y))
    #################################################
    # Filter
    #################################################
    #lf
    lf.loc[:,y,:] = kf_filter(olra.loc[:,y,:].values,
                        obsPerDay,
                        lf_period[0],lf_period[1],
                        lf_wavenumber[0],lf_wavenumber[1],
                        lf_depth[0],lf_depth[1],"none")
    
    #mjo
    mjo.loc[:,y,:] = kf_filter(olra.loc[:,y,:].values,
                        obsPerDay,
                        mjo_period[0],mjo_period[1],
                        mjo_wavenumber[0],mjo_wavenumber[1],
                        mjo_depth[0],mjo_depth[1],"none")
    
    #er 
    er.loc[:,y,:] = kf_filter(olra.loc[:,y,:].values,
                       obsPerDay,
                       er_period[0],er_period[1],
                       er_wavenumber[0],er_wavenumber[1],
                       er_depth[0],er_depth[1],"er")
    
    ## kw
    kelvin.loc[:,y,:] = kf_filter(olra.loc[:,y,:].values,
                           obsPerDay,
                           kelvin_period[0],kelvin_period[1],
                           kelvin_wavenumber[0],kelvin_wavenumber[1],
                           kelvin_depth[0],kelvin_depth[1],"kelvin")
    
    # mt 
    mt.loc[:,y,:] = kf_filter(olra.loc[:,y,:].values,
                       obsPerDay,
                       mt_period[0],mt_period[1],
                       mt_wavenumber[0],mt_wavenumber[1],
                       mt_depth[0],mt_depth[1],"none")
        
    # mrg 
    mrg.loc[:,y,:] = kf_filter(olra.loc[:,y,:].values,
                        obsPerDay,
                        mrg_period[0],mrg_period[1],
                        mrg_wavenumber[0],mrg_wavenumber[1],
                        mrg_depth[0],mrg_depth[1],"mrg")
    
er
<xarray.DataArray (time: 730, lat: 40, lon: 360)>
array([[[ 0.41869953,  0.74632704,  1.07617247, ..., -0.48732913,
         -0.20450903,  0.09961336],
        [ 0.16254583,  0.38559777,  0.6125918 , ..., -0.44736761,
         -0.25748172, -0.05300589],
        [ 0.1102673 ,  0.26533216,  0.4232206 , ..., -0.31864047,
         -0.18367998, -0.0400544 ],
        ...,
        [-5.61130285, -5.43971968, -5.16630745, ..., -5.5121479 ,
         -5.64538622, -5.67955065],
        [-6.04585648, -5.95039845, -5.75541639, ..., -5.74995708,
         -5.9418354 , -6.04211521],
        [-6.54671526, -6.63207436, -6.63014841, ..., -5.81441212,
         -6.13140631, -6.37794161]],

       [[ 0.76235485,  1.1360811 ,  1.49986124, ..., -0.35526246,
          0.00991862,  0.38476983],
        [ 0.05015931,  0.32514107,  0.60003805, ..., -0.73913151,
         -0.48576149, -0.22138192],
        [-0.28530368, -0.08856484,  0.11314388, ..., -0.82769424,
         -0.6566962 , -0.47528636],
...
        [-4.84334517, -5.02544785, -5.14676905, ..., -4.01491976,
         -4.32888174, -4.60822058],
        [-5.2019372 , -5.39951229, -5.54685974, ..., -4.38925982,
         -4.68813229, -4.96198225],
        [-5.03971434, -5.30969191, -5.55134392, ..., -4.13197279,
         -4.44375563, -4.74866533]],

       [[ 0.36513716,  0.63959444,  0.92730278, ..., -0.32453212,
         -0.12197281,  0.10952018],
        [ 0.52740246,  0.72218305,  0.9244988 , ...,  0.02059937,
          0.17357652,  0.34348166],
        [ 0.65549105,  0.81336552,  0.97305393, ...,  0.21653658,
          0.35472208,  0.50181794],
        ...,
        [-5.3761735 , -5.42974091, -5.39585304, ..., -4.74491978,
         -5.02649164, -5.23962402],
        [-5.70784616, -5.81145811, -5.83556461, ..., -4.9854064 ,
         -5.28673029, -5.53047705],
        [-5.8078804 , -6.04300451, -6.21886206, ..., -4.82505178,
         -5.19027758, -5.52087641]]])
Coordinates:
  * time     (time) datetime64[ns] 2017-01-01 2017-01-02 ... 2018-12-31
  * lat      (lat) float32 -19.5 -18.5 -17.5 -16.5 -15.5 ... 16.5 17.5 18.5 19.5
  * lon      (lon) float32 0.5 1.5 2.5 3.5 4.5 ... 355.5 356.5 357.5 358.5 359.5

最後可以輸出資料:

# Output files

outdir = './data/olr_ccew_sample/'

#print(lf) 
lf.to_netcdf(outdir+'olr_2017-2018_LF.nc',unlimited_dims='time')

#print(mjo)
mjo.to_netcdf(outdir+'olr_2017-2018_MJO.nc',unlimited_dims='time')

#print(er)
er.to_netcdf(outdir+'olr_2017-2018_ER.nc',unlimited_dims='time')

#print(kelvin)
kelvin.to_netcdf(outdir+'olr_2017-2018_KW.nc',unlimited_dims='time')

#print(mt)
mt.to_netcdf(outdir+'olr_2017-2018_MT.nc',unlimited_dims='time')

#print(mrg)
mrg.to_netcdf(outdir+'olr_2017-2018_MRG.nc',unlimited_dims='time')

我們選定2017/12-2018/02期間,繪製5˚-15˚N平均的 OLR、濾波的MJO、ER波、MRG/TD波哈莫圖。

import cmaps
cmap=cmaps.sunshine_diff_12lev
import pandas as pd
from matplotlib import pyplot as plt
from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER
import cmaps

time1 = '2017-12-01'
time2 = '2018-02-28'
lon1 = 39 
lon2 = 181
lats = 5
latn = 15

olrh     =   olra.sel(time=slice(time1,time2),lat=slice(lats,latn),lon=slice(lon1,lon2)).mean(axis=1)
er_hovm  =     er.sel(time=slice(time1,time2),lat=slice(lats,latn),lon=slice(lon1,lon2)).mean(axis=1)
mjo_hovm =    mjo.sel(time=slice(time1,time2),lat=slice(lats,latn),lon=slice(lon1,lon2)).mean(axis=1)
mt_hovm  =     mt.sel(time=slice(time1,time2),lat=slice(lats,latn),lon=slice(lon1,lon2)).mean(axis=1)
kw_hovm  = kelvin.sel(time=slice(time1,time2),lat=slice(lats,latn),lon=slice(lon1,lon2)).mean(axis=1)

# Plot settings
plt.figure(figsize=(6, 8))     
ax = plt.axes() 
ax.set_xticks(np.arange(40,200,20))
lon_formatter = LONGITUDE_FORMATTER
ax.xaxis.set_major_formatter(lon_formatter)

clevs = [-72,-60,-48,-36,-24,-12,12,24,36,48,60,72]
plt.title("OLR anomaly", loc='left')
hovm_plot = olrh.plot.contourf(x="lon", y="time",
                               ax=ax, 
                               levels=clevs,                    
                               cmap=cmap,  
                               yincrease=False,                 # y axis be increasing from top to bottom
                               add_colorbar=True, 
                               extend='both',                   # color bar 兩端向外延伸
                               cbar_kwargs={'orientation': 'horizontal', 'aspect': 30, 'label': ' ', 'ticks':clevs})
#kw_plot = kw_hovm.plot.contour(x='lon',y='time', ax=ax,
#                               levels=[-30,-15,15,30], 
#                               colors='blue',linewidths=1,
#                               yincrease=False) 
mt_plot = mt_hovm.plot.contour(x='lon',y='time', ax=ax,
                               levels=[-30,-15,15,30], 
                               colors='navy',linewidths=1,
                               yincrease=False) 
mjo_plot = mjo_hovm.plot.contour(x='lon',y='time', ax=ax,
                                 levels=[-30,-20,-10,10,20,30], 
                                 colors='black',linewidths=1,
                                 yincrease=False)
er_plot  = er_hovm.plot.contour(x='lon',y='time', ax=ax,
                                levels=[-30,-20,-10,10,20,30], 
                                colors='red',linewidths=1,
                                yincrease=False)

ax.set_xlabel(' ')
ax.set_ylabel(' ')
ax.set_title(' ')
ax.set_title('5˚-15˚N averaged', loc='left')

plt.subplots_adjust(left=0.2)

plt.show()
_images/71033370a1181fee992ada772fcf8addaca5f4f1514d05de7ae6e3ad4e0232ce.png

上圖中,底色是濾波前OLR距平,黑色往東的等值線代表MJO,紅色往西的等值線代表赤道羅士比波 (ER waves),深藍色往西的等值線是混合羅士比-重力波和熱帶擾動 (MRG/TD wave),且實線 (正值) 代表對流抑制相位,虛線 (負值) 代表對流活躍相位。

雙曲柱狀函數投影法 (2DS-PCF)#

在Yang et al. (2003) 中,他們指出由於熱帶波動會受到背景場調節,產生都卜勒效應,波動的頻散關係會被改變,因此像Wheeler and Kiladis (1999) 的方法去限制一個窄的時空頻段來進行濾波,可能會造成有部分的波動訊號被忽略。因此,他們只對動力場 (u, v, z) 做一個寬帶濾波,波數的範圍是 \(1\leq \left| k\right| \leq 15\),週期的範圍是 \(2\leq \left| T\right| \leq 30\) 天, 再將寬帶濾波後的動力場分別投影至各個波動模態的理論解上,此時得到的就是熱帶波動,其中理論解釋是根據 Gill (1980) ,這篇文章假定表面有加熱的作用,加入淺水方程中,導出的解就是雙曲柱狀函數的形式。

https://rmets.onlinelibrary.wiley.com/cms/asset/0cf13e1c-5d09-4004-9f66-8dc3b634d610/qj4338-fig-0002-m.jpg

Fig. 4 Latitudinal profiles of different equatorial wave solutions. Spherical solutions for (a) Kelvin wave zonal wind \(u^{\prime }\) and (b) mixed Rossby–gravity wave (MRG) meridional wind \(v^{\prime }\), both for \(D\) = 8 m and 90 m and for \(k\) = 1 and \(k\) = 20, as well as (c) the \(n\) = 1 equatorial Rossby wave (ER) \(u^{\prime }, v^{\prime }\), and geopotential \(\phi ^{\prime }\) for \(k\) = 1 and \(D\) = 90 m. These curves were produced using the software developed by Swarztrauber and Kasahara (1985). (d) Parabolic cylinder functions (PCFs) 0, 1, and 2 for \(y_0 = 6\). The 15° S–15° N belt used for averaging throughout the paper is shaded in grey. Source: Knippertz et al. 2023.#

Note

  1. 這個方法必須同時使用u, v, z三個變數!

  2. 這個方法不能濾MJO的訊號!

2DS-PCF方法的程式,是由Professor Steven Woolnough 和 Dr. Gui-Ying Yang (University of Reading) 提供,並在 Dr. Sandro Lubis的GitHub網頁下載 (sandrolubis/CCEWs-PCF-Filter)。以下示範對2016-2020年的u, v, z資料進行熱帶波動辨識 (此處示範使用Dr. Lubis提供的範例資料檔)。

'''
; Authors: Prof. Steven Woolnough and Dr. Gui-Ying Yang
;          Modified by Dr. Sandro W. Lubis (Nov 2021)
;          CCEW Filter via PCFs following Yang et al., (2003)
; Contact: slubis.geomar@gmail.com
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
'''
import numpy as np
import pandas as pd
import xarray as xr
import pandas as pd
from scipy import signal
from metpy.units import units

### Read data and calculate anomaly

lev = 850

# Open files 
ua = xr.open_dataarray('./data/u.850.20N-20S.0-360.20160101-20201231.nc')
va = xr.open_dataarray('./data/v.850.20N-20S.0-360.20160101-20201231.nc')
za = xr.open_dataarray('./data/phi.850.20N-20S.0-360.20160101-20201231.nc')

### 2DS-PCF starts (以下都不需要更動)
u = signal.detrend(ua.values, axis=0)
v = signal.detrend(va.values, axis=0)
z = signal.detrend(za.values, axis=0)

wgt_taper = signal.tukey(u.shape[0], alpha=0.1)

uT = np.transpose(u, (1, 2, 0)) * np.reshape(wgt_taper, (-1, u.shape[0]))
vT = np.transpose(v, (1, 2, 0)) * np.reshape(wgt_taper, (-1, v.shape[0]))
zT = np.transpose(z, (1, 2, 0)) * np.reshape(wgt_taper, (-1, z.shape[0]))

u = np.transpose(uT, (2, 0, 1))
v = np.transpose(vT, (2, 0, 1))
z = np.transpose(zT, (2, 0, 1))

g     = 9.8
beta  = 2.3e-11
radea = 6.371e+06
spd   = 86400.0
ww    = 2.0 * np.pi / spd

latmax = 20.0

kmin = 2
kmax = 20

pmin = 3.0
pmax = 30.0

# convert trapping scale to meters
y0 = 6.0
y0real = 2.0 * np.pi * radea * y0 / 360.0

ce = 2.0 * y0real**2 * beta

g_on_c = g / ce
c_on_g = ce / g

waves = np.array(['Kelvin', 'WMRG', 'R1', 'R2'])

# transform u,z to q, r using q=z*(g/c) + u; r=z*(g/c) - u 

q = z * g_on_c + u
r = z * g_on_c - u

qf = np.fft.fft2(q, axes=(0, 2))
vf = np.fft.fft2(v, axes=(0, 2))
rf = np.fft.fft2(r, axes=(0, 2))

nf = qf.shape[0]
nlat = qf.shape[1]
nk = qf.shape[2]

# Find frequencies and wavenumbers corresponding to pmin,pmax and kmin,kmax in coeff matrices

f = np.fft.fftfreq(nf)
k = np.fft.fftfreq(nk) * nk

fmin = np.where(f >= 1.0 / pmax)[0][0]
fmax = (np.where(f > 1.0 / pmin)[0][0]) - 1

f1p = fmin
f2p = fmax + 1
f1n = nf - fmax
f2n = nf - fmin + 1

k1p = kmin
k2p = kmax + 1
k1n = nk - kmax
k2n = nk - kmin + 1

# Define the parobolic cylinder functions

spi2 = np.sqrt(2.0 * np.pi)
dsq = np.array([spi2, spi2, 2.0 * spi2, 6.0 * spi2])
d = np.zeros((dsq.size, nlat))
y = ua.Y.values / y0
ysq = y**2

d[0, :] = np.exp(-ysq / 4.0)
d[1, :] = y * d[0, :]
d[2, :] = (ysq - 1.0) * d[0, :]
d[3, :] = y * (ysq - 3.0) * d[0, :]

dlat = np.abs(ua.Y.values[1] - ua.Y.values[0]) * np.pi / 180.0

qf_Kel = np.zeros((nf, nk), dtype='complex')

qf_mode = np.zeros((dsq.size, nf, nk), dtype='complex')
vf_mode = np.zeros((dsq.size, nf, nk), dtype='complex')
rf_mode = np.zeros((dsq.size, nf, nk), dtype='complex')

# reorder the spectral coefficents to make the latitudes the last dimension

qf = np.transpose(qf, (0, 2, 1))
vf = np.transpose(vf, (0, 2, 1))
rf = np.transpose(rf, (0, 2, 1))

for m in np.arange(dsq.size):
    if m == 0:
        qf_Kel[f1n:f2n, k1p:k2p] = np.sum(qf[f1n:f2n, k1p:k2p, :] * d[m, :] * dlat, axis=-1) / (dsq[m] / y0)
        qf_Kel[f1p:f2p, k1n:k2n] = np.sum(qf[f1p:f2p, k1n:k2n, :] * d[m, :] * dlat, axis=-1) / (dsq[m] / y0)
    
    qf_mode[m, f1n:f2n, k1n:k2n] = np.sum(qf[f1n:f2n, k1n:k2n, :] * d[m, :] * dlat, axis=-1) / (dsq[m] / y0)
    qf_mode[m, f1p:f2p, k1p:k2p] = np.sum(qf[f1p:f2p, k1p:k2p, :] * d[m, :] * dlat, axis=-1) / (dsq[m] / y0)
    
    vf_mode[m, f1n:f2n, k1n:k2n] = np.sum(vf[f1n:f2n, k1n:k2n, :] * d[m, :] * dlat, axis=-1) / (dsq[m] / y0)
    vf_mode[m, f1p:f2p, k1p:k2p] = np.sum(vf[f1p:f2p, k1p:k2p, :] * d[m, :] * dlat, axis=-1) / (dsq[m] / y0)
    
    rf_mode[m, f1n:f2n, k1n:k2n] = np.sum(rf[f1n:f2n, k1n:k2n, :] * d[m, :] * dlat, axis=-1) / (dsq[m] / y0)
    rf_mode[m, f1p:f2p, k1p:k2p] = np.sum(rf[f1p:f2p, k1p:k2p, :] * d[m, :] * dlat, axis=-1) / (dsq[m] / y0)

uf_wave = np.zeros((waves.size, nf, nlat, nk), dtype='complex')
vf_wave = np.zeros((waves.size, nf, nlat, nk), dtype='complex')
zf_wave = np.zeros((waves.size, nf, nlat, nk), dtype='complex')

for w in np.arange(waves.size):
    if waves[w] == 'Kelvin':
        for j in np.arange(nlat):
            uf_wave[w, :, j, :] = 0.5 * qf_Kel * d[0, j]
            zf_wave[w, :, j, :] = 0.5 * qf_Kel * d[0, j] * c_on_g
    
    if waves[w] == 'WMRG':
        for j in np.arange(nlat):
            uf_wave[w, :, j, :] = 0.5 * qf_mode[1, :, :] * d[1, j]
            vf_wave[w, :, j, :] = 0.5 * vf_mode[0, :, :] * d[0, j]
            zf_wave[w, :, j, :] = 0.5 * qf_mode[1, :, :] * d[1, j] * c_on_g
    
    if waves[w] == 'R1':
        for j in np.arange(nlat):
            uf_wave[w, :, j, :] = 0.5 * (qf_mode[2, :, :] * d[2, j] - rf_mode[0, :, :] * d[0, j])
            vf_wave[w, :, j, :] = 0.5 * vf_mode[1, :, :] * d[1, j]
            zf_wave[w, :, j, :] = 0.5 * (qf_mode[2, :, :] * d[2, j] + rf_mode[0, :, :] * d[0, j]) * c_on_g
    
    if waves[w] == 'R2':
        for j in np.arange(nlat):
            uf_wave[w, :, j, :] = 0.5 * (qf_mode[3, :, :] * d[3, j] - rf_mode[1, :, :] * d[1, j])
            vf_wave[w, :, j, :] = 0.5 * vf_mode[2, :, :] * d[2, j]
            zf_wave[w, :, j, :] = 0.5 * (qf_mode[3, :, :] * d[3, j] + rf_mode[1, :, :] * d[1, j]) * c_on_g

u_Kelvin = xr.DataArray(np.real(np.fft.ifft2(uf_wave[0, :, :, :], axes=(0, 2))), coords=[ua['T'], ua['Y'], ua['X']], dims=['T', 'Y', 'X'], name='u')
v_Kelvin = xr.DataArray(np.real(np.fft.ifft2(vf_wave[0, :, :, :], axes=(0, 2))), coords=[va['T'], va['Y'], va['X']], dims=['T', 'Y', 'X'], name='v')
z_Kelvin = xr.DataArray(np.real(np.fft.ifft2(zf_wave[0, :, :, :], axes=(0, 2))), coords=[za['T'], za['Y'], za['X']], dims=['T', 'Y', 'X'], name='z')

u_Kelvin.attrs['long_name'] = 'Kelvin Waves in '+ str(lev) + ' hPa Zonal Wind'
v_Kelvin.attrs['long_name'] = 'Kelvin Waves in '+ str(lev) + ' hPa Meridional Wind'
z_Kelvin.attrs['long_name'] = 'Kelvin Waves in '+ str(lev) + ' hPa Geopotential Height'

u_Kelvin.attrs['units'] = 'm/s'
v_Kelvin.attrs['units'] = 'm/s'
z_Kelvin.attrs['units'] = 'm'

u_WMRG = xr.DataArray(np.real(np.fft.ifft2(uf_wave[1, :, :, :], axes=(0, 2))), coords=[ua['T'], ua['Y'], ua['X']], dims=['T', 'Y', 'X'], name='u')
v_WMRG = xr.DataArray(np.real(np.fft.ifft2(vf_wave[1, :, :, :], axes=(0, 2))), coords=[va['T'], va['Y'], va['X']], dims=['T', 'Y', 'X'], name='v')
z_WMRG = xr.DataArray(np.real(np.fft.ifft2(zf_wave[1, :, :, :], axes=(0, 2))), coords=[za['T'], za['Y'], za['X']], dims=['T', 'Y', 'X'], name='z')

u_WMRG.attrs['long_name'] = 'Westward Mixed Rossby-Gravity Waves in '+ str(lev) + ' hPa Zonal Wind'
v_WMRG.attrs['long_name'] = 'Westward Mixed Rossby-Gravity Waves in '+ str(lev) + ' hPa Meridional Wind'
z_WMRG.attrs['long_name'] = 'Westward Mixed Rossby-Gravity Waves in '+ str(lev) + ' hPa Geopotential Height'

u_WMRG.attrs['units'] = 'm/s'
v_WMRG.attrs['units'] = 'm/s'
z_WMRG.attrs['units'] = 'm'

u_R1 = xr.DataArray(np.real(np.fft.ifft2(uf_wave[2, :, :, :], axes=(0, 2))), coords=[ua['T'], ua['Y'], ua['X']], dims=['T', 'Y', 'X'], name='u')
v_R1 = xr.DataArray(np.real(np.fft.ifft2(vf_wave[2, :, :, :], axes=(0, 2))), coords=[va['T'], va['Y'], va['X']], dims=['T', 'Y', 'X'], name='v')
z_R1 = xr.DataArray(np.real(np.fft.ifft2(zf_wave[2, :, :, :], axes=(0, 2))), coords=[za['T'], za['Y'], za['X']], dims=['T', 'Y', 'X'], name='z')

u_R1.attrs['long_name'] = 'n = 1 Equatorial Rossby Waves in '+ str(lev) + ' hPa Zonal Wind'
v_R1.attrs['long_name'] = 'n = 1 Equatorial Rossby Waves in '+ str(lev) + ' hPa Meridional Wind'
z_R1.attrs['long_name'] = 'n = 1 Equatorial Rossby Waves in '+ str(lev) + ' hPa Geopotential Height'

u_R1.attrs['units'] = 'm/s'
v_R1.attrs['units'] = 'm/s'
z_R1.attrs['units'] = 'm'

u_R2 = xr.DataArray(np.real(np.fft.ifft2(uf_wave[3, :, :, :], axes=(0, 2))), coords=[ua['T'], ua['Y'], ua['X']], dims=['T', 'Y', 'X'], name='u')
v_R2 = xr.DataArray(np.real(np.fft.ifft2(vf_wave[3, :, :, :], axes=(0, 2))), coords=[va['T'], va['Y'], va['X']], dims=['T', 'Y', 'X'], name='v')
z_R2 = xr.DataArray(np.real(np.fft.ifft2(zf_wave[3, :, :, :], axes=(0, 2))), coords=[za['T'], za['Y'], za['X']], dims=['T', 'Y', 'X'], name='z')

u_R2.attrs['long_name'] = 'n = 2 Equatorial Rossby Waves in '+ str(lev) + ' hPa Zonal Wind'
v_R2.attrs['long_name'] = 'n = 2 Equatorial Rossby Waves in '+ str(lev) + ' hPa Meridional Wind'
z_R2.attrs['long_name'] = 'n = 2 Equatorial Rossby Waves in '+ str(lev) + ' hPa Geopotential Height'

u_R2.attrs['units'] = 'm/s'
v_R2.attrs['units'] = 'm/s'
z_R2.attrs['units'] = 'm' 

到此熱帶波動投影的程序已經結束。以下也可以輸出資料:

u_Kelvin.to_netcdf('./data/yhs03_ccews/u.850.kelvin.2016-2020.nc',unlimited_dims='T')
v_Kelvin.to_netcdf('./data/yhs03_ccews/v.850.kelvin.2016-2020.nc',unlimited_dims='T')
z_Kelvin.to_netcdf('./data/yhs03_ccews/z.850.kelvin.2016-2020.nc',unlimited_dims='T')

u_WMRG.to_netcdf('./data/yhs03_ccews/u.850.wmrg.2016-2020.nc',unlimited_dims='T')
v_WMRG.to_netcdf('./data/yhs03_ccews/v.850.wmrg.2016-2020.nc',unlimited_dims='T')
z_WMRG.to_netcdf('./data/yhs03_ccews/z.850.wmrg.2016-2020.nc',unlimited_dims='T')

u_R1.to_netcdf('./data/yhs03_ccews/u.850.r1.2016-2020.nc',unlimited_dims='T')
v_R1.to_netcdf('./data/yhs03_ccews/v.850.r1.2016-2020.nc',unlimited_dims='T')
z_R1.to_netcdf('./data/yhs03_ccews/z.850.r1.2016-2020.nc',unlimited_dims='T')

u_R2.to_netcdf('./data/yhs03_ccews/u.850.r2.2016-2020.nc',unlimited_dims='T')
v_R2.to_netcdf('./data/yhs03_ccews/v.850.r2.2016-2020.nc',unlimited_dims='T')
z_R2.to_netcdf('./data/yhs03_ccews/z.850.r2.2016-2020.nc',unlimited_dims='T')

我們繪製2019年12月16日這4種波動的平面圖。

import xarray as xr 
import numpy as np 
import cmaps
from matplotlib import pyplot as plt
from cartopy import crs as ccrs   
from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER

lon_formatter = LONGITUDE_FORMATTER
lat_formatter = LATITUDE_FORMATTER

time = '2019-12-15 12:00:00'
lon1, lon2 =  30, 210
lats, latn = -20,  20
lb = ['a','b','d','c','e']

# Define the figure and each axis for the 3 rows and 3 columns
proj = ccrs.PlateCarree(central_longitude=180) 
fig, axes = plt.subplots(nrows=3,ncols=2,
                         subplot_kw={'projection': proj}, 
                         figsize=(12,8))

# axs is a 2 dimensional array of `GeoAxes`.  We will flatten it into a 1-D array
ax = axes.flatten()

#### Plot Total fields 
cf1 = (za.sel(T=time,X=slice(lon1,lon2))
         .plot.contourf(x="X", y="Y", 
                        levels=[-25,-20,-15,-10,-5,0,5,10,15,20,25],
                        cmap='RdYlBu',
                        add_colorbar=False, extend='both',
                        transform=ccrs.PlateCarree(),
                        ax=ax[0]))
tot_wnd = (xr.merge([ua,va]).sel(T=time,X=slice(lon1,lon2))
                            .interp(X=np.arange(31,211,5),Y=np.arange(-19,24,5)))
qv = (tot_wnd.plot.quiver(ax=ax[0],
                          transform=ccrs.PlateCarree(),
                          x='X', y='Y',
                          u='u', v='v',
                          width=0.003 ,headaxislength=3,headlength=6,headwidth=7,
                          scale=200, colors="black",add_guide=False
                          ))
ax[0].set_title('')
ax[0].set_title("(a) Total Fields",loc='left')
qk = 5
qv_key = ax[0].quiverkey(qv,0.95,1.05,qk,(str(qk)+' m s$^{-1}$'),labelpos='N', labelsep =0.05, color='black')
    
# KW
cf = (z_Kelvin.sel(T=time,X=slice(lon1,lon2))
              .plot.contourf(x="X", y="Y", 
                             levels=[-5,-4,-3,-2,-1,-0.5,0.5,1,2,3,4,5],
                             cmap='RdYlBu',
                             add_colorbar=False, extend='both',
                             transform=ccrs.PlateCarree(),
                             ax=ax[2]))
kw_wnd = (xr.merge([u_Kelvin,v_Kelvin]).sel(T=time,X=slice(lon1,lon2))
                                       .interp(X=np.arange(31,211,7),Y=np.arange(-19,24,5)))
qv = (kw_wnd.plot.quiver(ax=ax[2],
                         transform=ccrs.PlateCarree(),
                         x='X', y='Y',
                         u='u', v='v',
                         width=0.005 ,headaxislength=3,headlength=6,headwidth=7,
                         scale=30, colors="black",add_guide=False
                         ))
ax[2].set_title('')
ax[2].set_title('(b) Kelvin Waves',loc='left')

qk = 2
qv_key = ax[2].quiverkey(qv,0.95,1.05,qk,(str(qk)+' m s$^{-1}$'),labelpos='N', labelsep =0.05, color='black')
    
# R1
cf = (z_R1.sel(T=time,X=slice(lon1,lon2))
           .plot.contourf(x="X", y="Y", 
                          levels=[-5,-4,-3,-2,-1,-0.5,0.5,1,2,3,4,5],
                          cmap='RdYlBu',
                          add_colorbar=False, extend='both',
                          transform=ccrs.PlateCarree(),
                          ax=ax[3]))
r1_wnd = (xr.merge([u_R1,v_R1]).sel(T=time,X=slice(lon1,lon2))
                               .interp(X=np.arange(31,211,7),Y=np.arange(-19,24,5)))
qv = (r1_wnd.plot.quiver(ax=ax[3],
                         transform=ccrs.PlateCarree(),
                         x='X', y='Y',
                         u='u', v='v',
                         width=0.005 ,headaxislength=3,headlength=6,headwidth=7,
                         scale=30, colors="black",add_guide=False
                         ))
ax[3].set_title('')
ax[3].set_title('(d) R1 waves',loc='left')

qk = 2
qv_key = ax[3].quiverkey(qv,0.95,1.05,qk,(str(qk)+' m s$^{-1}$'),labelpos='N', labelsep =0.05, color='black')

# R2
cf = (z_R2.sel(T=time,X=slice(lon1,lon2))
           .plot.contourf(x="X", y="Y", 
                          levels=[-5,-4,-3,-2,-1,-0.5,0.5,1,2,3,4,5],
                          cmap='RdYlBu',
                          add_colorbar=False, extend='both',
                          transform=ccrs.PlateCarree(),
                          ax=ax[4]))
r2_wnd = (xr.merge([u_R2,v_R2]).sel(T=time,X=slice(lon1,lon2))
                               .interp(X=np.arange(31,211,7),Y=np.arange(-19,24,5)))
qv = (r2_wnd.plot.quiver(ax=ax[4],
                         transform=ccrs.PlateCarree(),
                         x='X', y='Y',
                         u='u', v='v',
                         width=0.005 ,headaxislength=3,headlength=6,headwidth=7,
                         scale=30, colors="black",add_guide=False
                         ))
ax[4].set_title('')
ax[4].set_title('(d) R2 waves',loc='left')

# WMRG
cf = (z_WMRG.sel(T=time,X=slice(lon1,lon2))
           .plot.contourf(x="X", y="Y", 
                          levels=[-5,-4,-3,-2,-1,-0.5,0.5,1,2,3,4,5],
                          cmap='RdYlBu',
                          add_colorbar=False, extend='both',
                          transform=ccrs.PlateCarree(),
                          ax=ax[5]))
r1_wnd = (xr.merge([u_WMRG,v_WMRG]).sel(T=time,X=slice(lon1,lon2))
                               .interp(X=np.arange(31,211,7),Y=np.arange(-19,24,5)))
qv = (r1_wnd.plot.quiver(ax=ax[5],
                         transform=ccrs.PlateCarree(),
                         x='X', y='Y',
                         u='u', v='v',
                         width=0.005 ,headaxislength=3,headlength=6,headwidth=7,
                         scale=30, colors="black",add_guide=False
                         ))
ax[5].set_title('')
ax[5].set_title('(e) Westward MRG waves',loc='left')
    
qk = 2
qv_key = ax[4].quiverkey(qv,0.95,1.05,qk,(str(qk)+' m s$^{-1}$'),labelpos='N', labelsep =0.05, color='black')

for i in range(0,6):
    # Draw the coastines for each subplot
    ax[i].coastlines(color="dimgray")
    ax[i].set_extent([lon1,lon2,lats,latn], ccrs.PlateCarree())   
    gl = ax[i].gridlines(crs=ccrs.PlateCarree(), draw_labels=True, 
                         xlocs=np.arange(-180,240,30), ylocs=np.arange(-20, 30,10), 
                         x_inline=False, y_inline=False, linewidth=0)
    gl.right_labels = False
    gl.top_labels = False 
    ax[i].set_ylabel(' ')             # 設定坐標軸名稱。
    ax[i].set_xlabel(' ')
    ax[i].set_title(' ')

ax[1].remove()

plt.suptitle('GPH (Shade.), 850-hPa Wind Anml. (vectors)',y=0.95,size='large',weight='bold')

#plt.show()
Text(0.5, 0.95, 'GPH (Shade.), 850-hPa Wind Anml. (vectors)')
Error in callback <function _draw_all_if_interactive at 0x189a96680> (for post_execute):
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
File ~/opt/anaconda3/envs/p3/lib/python3.10/site-packages/matplotlib/pyplot.py:120, in _draw_all_if_interactive()
    118 def _draw_all_if_interactive():
    119     if matplotlib.is_interactive():
--> 120         draw_all()

File ~/opt/anaconda3/envs/p3/lib/python3.10/site-packages/matplotlib/_pylab_helpers.py:132, in Gcf.draw_all(cls, force)
    130 for manager in cls.get_all_fig_managers():
    131     if force or manager.canvas.figure.stale:
--> 132         manager.canvas.draw_idle()

File ~/opt/anaconda3/envs/p3/lib/python3.10/site-packages/matplotlib/backend_bases.py:2082, in FigureCanvasBase.draw_idle(self, *args, **kwargs)
   2080 if not self._is_idle_drawing:
   2081     with self._idle_draw_cntx():
-> 2082         self.draw(*args, **kwargs)

File ~/opt/anaconda3/envs/p3/lib/python3.10/site-packages/matplotlib/backends/backend_agg.py:400, in FigureCanvasAgg.draw(self)
    396 # Acquire a lock on the shared font cache.
    397 with RendererAgg.lock, \
    398      (self.toolbar._wait_cursor_for_draw_cm() if self.toolbar
    399       else nullcontext()):
--> 400     self.figure.draw(self.renderer)
    401     # A GUI class may be need to update a window using this draw, so
    402     # don't forget to call the superclass.
    403     super().draw()

File ~/opt/anaconda3/envs/p3/lib/python3.10/site-packages/matplotlib/artist.py:95, in _finalize_rasterization.<locals>.draw_wrapper(artist, renderer, *args, **kwargs)
     93 @wraps(draw)
     94 def draw_wrapper(artist, renderer, *args, **kwargs):
---> 95     result = draw(artist, renderer, *args, **kwargs)
     96     if renderer._rasterizing:
     97         renderer.stop_rasterizing()

File ~/opt/anaconda3/envs/p3/lib/python3.10/site-packages/matplotlib/artist.py:72, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
     69     if artist.get_agg_filter() is not None:
     70         renderer.start_filter()
---> 72     return draw(artist, renderer)
     73 finally:
     74     if artist.get_agg_filter() is not None:

File ~/opt/anaconda3/envs/p3/lib/python3.10/site-packages/matplotlib/figure.py:3150, in Figure.draw(self, renderer)
   3147 finally:
   3148     self.stale = False
-> 3150 DrawEvent("draw_event", self.canvas, renderer)._process()

File ~/opt/anaconda3/envs/p3/lib/python3.10/site-packages/matplotlib/backend_bases.py:1263, in Event._process(self)
   1261 def _process(self):
   1262     """Generate an event with name ``self.name`` on ``self.canvas``."""
-> 1263     self.canvas.callbacks.process(self.name, self)

File ~/opt/anaconda3/envs/p3/lib/python3.10/site-packages/matplotlib/cbook/__init__.py:309, in CallbackRegistry.process(self, s, *args, **kwargs)
    307 except Exception as exc:
    308     if self.exception_handler is not None:
--> 309         self.exception_handler(exc)
    310     else:
    311         raise

File ~/opt/anaconda3/envs/p3/lib/python3.10/site-packages/matplotlib/cbook/__init__.py:96, in _exception_printer(exc)
     94 def _exception_printer(exc):
     95     if _get_running_interactive_framework() in ["headless", None]:
---> 96         raise exc
     97     else:
     98         traceback.print_exc()

File ~/opt/anaconda3/envs/p3/lib/python3.10/site-packages/matplotlib/cbook/__init__.py:304, in CallbackRegistry.process(self, s, *args, **kwargs)
    302 if func is not None:
    303     try:
--> 304         func(*args, **kwargs)
    305     # this does not capture KeyboardInterrupt, SystemExit,
    306     # and GeneratorExit
    307     except Exception as exc:

File ~/opt/anaconda3/envs/p3/lib/python3.10/site-packages/cartopy/mpl/gridliner.py:492, in Gridliner._draw_event(self, event)
    491 def _draw_event(self, event):
--> 492     self._draw_gridliner(renderer=event.renderer)

File ~/opt/anaconda3/envs/p3/lib/python3.10/site-packages/cartopy/mpl/gridliner.py:917, in Gridliner._draw_gridliner(self, nx, ny, renderer)
    915         visible = False
    916     kw = self._get_text_specs(segment_angle, loc, xylabel)
--> 917     kw['transform'] = self._get_padding_transform(
    918         segment_angle, loc, xylabel)
    919 kw.update(label_style)
    921 # Get x and y in data coords

File ~/opt/anaconda3/envs/p3/lib/python3.10/site-packages/cartopy/mpl/gridliner.py:1160, in Gridliner._get_padding_transform(self, padding_angle, loc, xylabel, padding_factor)
   1157 dy = padding_factor * padding * np.sin(padding_angle * np.pi / 180)
   1159 # Final transform
-> 1160 return mtrans.offset_copy(
   1161     self.axes.transData, fig=self.axes.figure,
   1162     x=dx, y=dy, units='points')

File ~/opt/anaconda3/envs/p3/lib/python3.10/site-packages/matplotlib/transforms.py:2974, in offset_copy(trans, fig, x, y, units)
   2972     return trans + Affine2D().translate(x, y)
   2973 if fig is None:
-> 2974     raise ValueError('For units of inches or points a fig kwarg is needed')
   2975 if units == 'points':
   2976     x /= 72.0

ValueError: For units of inches or points a fig kwarg is needed
_images/fac80019118904e1296da20b67a0b8db29f6cca3c30a2dcec1e46dff1c2d0d02.png

Caution

ERA5下載的資料是geopotential,但是計算熱帶波動時要使用geopotential height,請使用metpy套件進行轉換,如:

from metpy import units 
import metpy.calc

zha = (metpy.calc.geopotential_to_height(za * ((units.m)**2 / (units.s)**2))
            .metpy.dequantify())

參考文獻#

  • Gill, A. E., 1980: Some simple solutions for heat‐induced tropical circulation. Q. J. R. Meteorol. Soc., 106, 447–462, https://doi.org/10.1002/qj.49710644905.

  • Kiladis, G. N., M. C. Wheeler, P. T. Haertel, K. H. Straub, and P. E. Roundy, 2009: Convectively coupled equatorial waves. Rev. Geophys., 47, 1–42, https://doi.org/10.1029/2008RG000266.

  • Knippertz, P., and Coauthors, 2022: The intricacies of identifying equatorial waves. Q. J. R. Meteorol. Soc., 148, 2814–2852, https://doi.org/10.1002/qj.4338.

  • Matsuno, T., 1966: Quasi-Geostrophic Motions in the Equatorial Area. J. Meteorol. Soc. Japan. Ser. II, 44, 25–43, https://doi.org/10.2151/jmsj1965.44.1_25.

  • Tsai, W. Y., M.-M. Lu, C.-H. Sui, and P.-H. Lin, 2020: MJO and CCEW modulation on South China Sea and Maritime Continent boreal winter subseasonal peak precipitation. Terr. Atmos. Ocean. Sci., 31, 177–195, https://doi.org/10.3319/TAO.2019.10.28.01.

  • Wheeler, M. C., and G. N. Kiladis, 1999: Convectively Coupled Equatorial Waves: Analysis of Clouds and Temperature in the Wavenumber–Frequency Domain. J. Atmos. Sci., 56, 374–399, https://doi.org/10.1175/1520-0469(1999)056<0374:CCEWAO>2.0.CO;2.

  • Yang, G.-Y., B. Hoskins, and J. Slingo, 2003: Convectively Coupled Equatorial Waves: A New Methodology for Identifying Wave Structures in Observational Data. J. Atmos. Sci., 60, 1637–1654, https://doi.org/10.1175/1520-0469(2003)060<1637:CCEWAN>2.0.CO;2.