3939 PYSHTOOLS_AVAILABLE = False
4040 print (
4141 f"{ Yellow } __________________\n "
42- f"Zonal decomposition relies on the pyshtools library, "
42+ f"Tidal decomposition relies on the pyshtools library, "
4343 f"referenced at:\n \n "
4444 f"Mark A. Wieczorek and Matthias Meschede (2018). "
4545 f"SHTools - Tools for working with spherical harmonics,"
@@ -207,21 +207,22 @@ def reconstruct_diurn(amp, phas, tod, lon, sumList=[]):
207207 # Return harmonics separately
208208 return varOUT
209209
210- def space_time ( lon , timex , varIN , kmx , tmx ):
210+ def extract_diurnal_harmonics ( kmx , tmx , varIN , tod , lon ):
211211 """
212212 Obtain west and east propagating waves. This is a Python
213213 implementation of John Wilson's ``space_time`` routine.
214- Alex Kling 2019 .
214+ Richard Urata 2025 .
215215
216216 :param lon: longitude [°] (0-360)
217217 :type lon: 1D array
218- :param timex: time [sol] (e.g., 1.5 days sampled every hour is
219- ``[0/24, 1/24, 2/24,.. 1,.. 1.5]``)
220- :type timex: 1D array
221- :param varIN: variable for the Fourier analysis. First axis must be
222- ``lon`` and last axis must be ``time`` (e.g.,
223- ``varIN[lon, time]``, ``varIN[lon, lat, time]``, or
224- ``varIN[lon, lev, lat, time]``)
218+ :param tod: time_of_day [hour] (e.g., a 24 hour sol sampled every hour could be
219+ ``[0.5, 1.5, 2.5, ..., 23.5]``)
220+ :type tod: 1D array
221+ :param varIN: variable for the Fourier analysis, expected to be scaled by zonal mean.
222+ First axis must be
223+ ``time`` and last axis must be ``lon`` (e.g.,
224+ ``varIN[time, tod, lon]``, ``varIN[time, tod, lat, lon]``, or
225+ ``varIN[time, tod, lat, lon, lev]``)
225226 :type varIN: array
226227 :param kmx: the number of longitudinal wavenumbers to extract
227228 (max = ``nlon/2``)
@@ -236,7 +237,7 @@ def space_time(lon, timex, varIN, kmx, tmx):
236237
237238 .. note:: 1. ``ampe``, ``ampw``, ``phasee``, and ``phasew`` have
238239 dimensions ``[kmx, tmx]`` or ``[kmx, tmx, lat]`` or
239- ``[kmx, tmx, lev, lat ]`` etc.\n
240+ ``[kmx, tmx, lat, lev ]`` etc.\n
240241 2. The x and y axes may be constructed as follows, which will
241242 display the eastern and western modes::
242243
@@ -247,125 +248,71 @@ def space_time(lon, timex, varIN, kmx, tmx):
247248 phase = np.concatenate((phasew[:, ::-1], phasee), axis = 1)
248249 """
249250
250- # Get input variable dimensions
251- dims = varIN .shape
252- lon_id = dims [0 ]
253- time_id = dims [- 1 ]
254-
255- # Additional dimensions stacked in the middle
256- dim_sup_id = dims [1 :- 1 ]
257-
258- # jd = total number of dimensions in the middle (``varIN > 3D``)
259- jd = int (np .prod (dim_sup_id ))
260-
261- # Flatten the middle dimensions, if any
262- varIN = np .reshape (varIN , (lon_id , jd , time_id ))
263-
264- # Initialize 4 empty arrays
265- ampw , ampe , phasew , phasee = (
266- [np .zeros ((kmx , tmx , jd )) for _x in range (0 , 4 )]
267- )
268-
269- #TODO not implemented yet:
270- # zamp, zphas = [np.zeros((jd, tmx)) for _x in range(0, 2)]
271-
272- tpi = 2 * np .pi
273- # Normalize longitude array
274- argx = lon * 2 * np .pi / 360
275- rnorm = 2. / len (argx )
276- # If timex = [0/24, 1/24, 2/24,.. 1] arg cycles for m [0, 2Pi]
277- arg = timex * 2 * np .pi
278- # Nyquist cut off
279- rnormt = 2. / len (arg )
280-
281- for kk in range (0 , kmx ):
282- progress (kk , kmx )
283- cosx = np .cos (kk * argx ) * rnorm
284- sinx = np .sin (kk * argx ) * rnorm
285-
286- # Inner product calculates the Fourier coefficients of the
287- # cosine and sine contributions of the spatial variation
288- acoef = np .dot (varIN .T , cosx )
289- bcoef = np .dot (varIN .T , sinx )
290-
291- for nn in range (0 , tmx ):
292- # Get the cosine and sine series expansions of the temporal
293- # variations of the acoef and bcoef spatial terms
294- cosray = rnormt * np .cos (nn * arg )
295- sinray = rnormt * np .sin (nn * arg )
296-
297- cosA = np .dot (acoef .T , cosray )
298- sinA = np .dot (acoef .T , sinray )
299- cosB = np .dot (bcoef .T , cosray )
300- sinB = np .dot (bcoef .T , sinray )
301-
302- wr = 0.5 * (cosA - sinB )
303- wi = 0.5 * (- sinA - cosB )
304- er = 0.5 * (cosA + sinB )
305- ei = 0.5 * (sinA - cosB )
306-
307- aw = np .sqrt (wr ** 2 + wi ** 2 )
308- ae = np .sqrt (er ** 2 + ei ** 2 )
309- pe = np .arctan2 (ei , er ) * 180 / np .pi
310- pw = np .arctan2 (wi , wr ) * 180 / np .pi
311-
312- pe = np .mod (- np .arctan2 (ei , er ) + tpi , tpi ) * 180 / np .pi
313- pw = np .mod (- np .arctan2 (wi , wr ) + tpi , tpi ) * 180 / np .pi
314-
315- ampw [kk , nn , :] = aw .T
316- ampe [kk , nn , :] = ae .T
317- phasew [kk , nn , :] = pw .T
318- phasee [kk , nn , :] = pe .T
319-
320- ampw = np .reshape (ampw , (kmx , tmx ) + dim_sup_id )
321- ampe = np .reshape (ampe , (kmx , tmx ) + dim_sup_id )
322- phasew = np .reshape (phasew , (kmx , tmx ) + dim_sup_id )
323- phasee = np .reshape (phasee , (kmx , tmx ) + dim_sup_id )
324-
325- # TODO implement zonal mean: zamp, zphas (standing wave k = 0,
326- # zonally averaged) stamp, stphs (stationary component ktime = 0)
327-
328- # # varIN = reshape(varIN, dims)
329- # # if nargout < 5:
330- # # # only ampe, ampw, phasee, phasew are requested
331- # # return
332-
333- # # Now calculate the axisymmetric tides zamp,zphas
334-
335- # zvarIN = np.mean(varIN, axis=0)
336- # zvarIN = np.reshape(zvarIN, (jd, time_id))
337-
338- # arg = timex * 2*np.pi
339- # arg = np.reshape(arg, (len(arg), 1))
340- # rnorm = 2/time_id
341-
342- # for nn in range(0, tmx):
343- # cosray = rnorm * np.cos(nn*arg)
344- # sinray = rnorm * np.sin(nn*arg)
345- # cosser = np.dot(zvarIN, cosray)
346- # sinser = np.dot(zvarIN, sinray)
347-
348- # zamp[:, nn] = np.sqrt(cosser[:]**2 + sinser[:]**2).T
349- # zphas[:, nn] = np.mod(-np.arctan2(sinser, cosser)
350- # + tpi, tpi).T * 180/np.pi
351-
352- # zamp = zamp.T
353- # zphas = zphas.T
354-
355- # if len(dims) > 2:
356- # zamp = np.reshape(zamp, (tmx,) + dim_sup_id)
357- # zamp = np.reshape(zphas, (tmx,) + dim_sup_id)
358-
359- # # if nargout < 7:
360- # # return
361-
362- # sxx = np.mean(varIN, ndims(varIN))
363- # [stamp, stphs] = amp_phase(sxx, lon, kmx)
364-
365- # if len(dims) > 2:
366- # stamp = reshape(stamp, [kmx dims(2:end-1)])
367- # stphs = reshape(stphs, [kmx dims(2:end-1)])
368- return ampe , ampw , phasee , phasew
251+ # Ensure time is in days
252+ if np .max (tod ) > 1 :
253+ tod = tod / 24
254+
255+ # Reshape varIN to (...,tod)
256+ if varIN .ndim == 3 :
257+ varIN = np .transpose (varIN , (0 , 2 , 1 ))
258+ ntime , nlon , ntod = varIN .shape
259+ output_shape = (ntime , kmx , tmx )
260+ if varIN .ndim == 4 :
261+ varIN = np .transpose (varIN , (0 , 3 , 2 , 1 ))
262+ ntime , nlon , nlat , ntod = varIN .shape
263+ output_shape = (ntime , kmx , tmx , nlat )
264+ if varIN .ndim == 5 :
265+ varIN = np .transpose (varIN , (0 , 4 , 3 , 2 , 1 ))
266+ ntime , nlon , nlat , nlev , ntod = varIN .shape
267+ output_shape = (ntime , kmx , tmx , nlat , nlev )
268+
269+ # Convert longitude to radians
270+ if np .max (lon ) > 100 :
271+ lon_rad = np .deg2rad (lon )
272+ else :
273+ lon_rad = lon
274+
275+ # Prepare arrays for results
276+ ampe = np .zeros (output_shape )
277+ ampw = np .zeros (output_shape )
278+ phasee = np .zeros (output_shape )
279+ phasew = np .zeros (output_shape )
280+
281+ # Constants
282+ tpi = 2 * np .pi
283+ rnorm = 2 / nlon
284+ rnormt = 2 / ntod
285+
286+ # Main calculation loop
287+ for t in range (ntime ):
288+ for k in range (kmx ): # Changed to start from 0
289+ cosx = np .cos ((k + 1 ) * lon_rad ) * rnorm # Add 1 to k for the calculation
290+ sinx = np .sin ((k + 1 ) * lon_rad ) * rnorm
291+
292+ acoef = np .tensordot (varIN [t ,...], cosx , axes = ([0 ], [0 ]))
293+ bcoef = np .tensordot (varIN [t ,...], sinx , axes = ([0 ], [0 ]))
294+
295+ for n in range (tmx ): # Changed to start from 0
296+ arg = (n + 1 ) * tpi * tod # Add 1 to n for the calculation
297+ cosray = rnormt * np .cos (arg )
298+ sinray = rnormt * np .sin (arg )
299+
300+ cosA = np .dot (acoef , cosray )
301+ sinA = np .dot (acoef , sinray )
302+ cosB = np .dot (bcoef , cosray )
303+ sinB = np .dot (bcoef , sinray )
304+
305+ wr = 0.5 * (cosA - sinB )
306+ wi = 0.5 * (- sinA - cosB )
307+ er = 0.5 * (cosA + sinB )
308+ ei = 0.5 * (sinA - cosB )
309+
310+ ampw [t , k , n , ...] = np .sqrt (wr ** 2 + wi ** 2 )
311+ ampe [t , k , n , ...] = np .sqrt (er ** 2 + ei ** 2 )
312+ phasew [t , k , n , ...] = np .mod (- np .arctan2 (wi , wr ) + tpi , tpi ) * 180 / np .pi
313+ phasee [t , k , n , ...] = np .mod (- np .arctan2 (ei , er ) + tpi , tpi ) * 180 / np .pi
314+
315+ return np .squeeze (ampe ), np .squeeze (ampw ), np .squeeze (phasee ), np .squeeze (phasew )
369316
370317
371318def zeroPhi_filter (VAR , btype , low_highcut , fs , axis = 0 , order = 4 ,
0 commit comments