Skip to content

Commit 1ef7383

Browse files
Merge pull request #141 from rurata/devel
adding in option for propagating tide analyses to MarsFiles
2 parents 75a8264 + e31e578 commit 1ef7383

File tree

4 files changed

+263
-132
lines changed

4 files changed

+263
-132
lines changed

amescap/Spectral_utils.py

Lines changed: 77 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
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

371318
def zeroPhi_filter(VAR, btype, low_highcut, fs, axis=0, order=4,

0 commit comments

Comments
 (0)