# -*- coding: utf-8 -*-
from magic import npfile, scanDir, MagicSetup, hammer2cart, symmetrize, progressbar
from scipy.interpolate import interp1d
from scipy import signal
from scipy.version import version
import os, re
import numpy as np
import matplotlib.pyplot as plt
from .spectralTransforms import SpectralTransforms
from magic.setup import labTex
import copy
[docs]def deriv(x, y, axis=0):
"""
This function is a simple second order derivative
:param x: input x-axis
:type x: numpy.ndarray
:param y: input array
:type y: numpy.ndarray
:returns: an array that contains the derivatives
:rtype: numpy.ndarray
"""
if len(x) < 3:
exit("Paramaters must have at least three points")
#if len(x) != len(y):
#exit("Vectors must have the same size")
d = (np.roll(y, -1, axis=axis)-np.roll(y, 1, axis=axis))/ \
(np.roll(x, -1)-np.roll(x, 1))
d[..., 0] = (-3.*y[..., 0]+4.*y[..., 1]-y[..., 2])/(x[..., 2]-x[..., 0])
d[..., -1] = (3*y[..., -1]-4*y[..., -2]+y[..., -3])/(x[..., -1]-x[..., -3])
return d
[docs]def getGauss(alm, blm, ell, m, scale_b, ratio_cmb_surface, rcmb):
r"""
Get the Gauss coefficients from the real and imaginary parts
of the poloidal potential
:param alm: real part of the poloidal potential
:type alm: numpy.ndarray
:param blm: imaginary part of the poloidal potential
:type blm: numpy.ndarray
:param ell: spherical harmonic degree :math:`\ell`
:type ell: numpy.ndarray
:param scale_b: magnetic field unit (default is 1)
:type scale_b: float
:param ratio_cmb_surface: ratio of CMB to surface radius (default is 1)
:type ratio_cmb_surface: float
:param rcmb: radius of the outer boundary
:type rcmb: float
"""
fac = (-1)**m*ell*np.sqrt((2*ell+1.)/(4.*np.pi))
fac[m > 0 ] *= np.sqrt(2)
glm = scale_b*ratio_cmb_surface**(ell+2.)/rcmb**2*fac*alm
hlm = -scale_b*ratio_cmb_surface**(ell+2.)/rcmb**2*fac*blm
return glm, hlm
[docs]def rearangeLat(field):
"""
This function is used to unfold the colatitudes
:param field: input array with MagIC ordering of colatitudes (i.e.
successively Northern Hemisphere and Southern
Hemisphere)
:type field: numpy.ndarray
:return: an array with the regular ordering of the colatitudes
:rtype: numpy.ndarray
"""
even = field[:, ::2]
odd = field[:, 1::2]
return np.concatenate((even, odd[:, ::-1]), axis=1)
[docs]class MagicCoeffCmb(MagicSetup):
r"""
This class allows to read the :ref:`B_coeff_cmb.TAG <secCoeffFiles>` files.
It first read the poloidal potential at the CMB and then transform
it to the Gauss coefficients :math:`g_{\ell m}` and :math:`h_{\ell m}`
using the getGauss function.
>>> # Reads the files B_coeff_cmb.testa, B_coeff_cmb.testb
>>> # and B_coeff_cmb.testc and stack them in one single time series
>>> cmb = MagicCoeffCmb(tag='test[a-c]')
>>> print(cmb.ell, cmb.glm) # print \ell and g_{\ell m}
>>> print(cmb.glm[:, cmb.idx[1, 0]]) # time-series of the axisymmetric dipole
>>> plot(cmb.time, cmb.dglmdt[:, cmb.idx[2, 0]]) # Secular variation of the quadrupole
>>> # Display the time-evolution of the CMB field
>>> cmb.movieCmb(levels=12, cm='seismic')
>>> # Save the time-evolution of the CMB field
>>> cmb.movieCmb(levels=12, cm='seismic', png=True)
"""
[docs] def __init__(self, tag=None, datadir='.', ratio_cmb_surface=1, scale_b=1,
iplot=True, lCut=None, precision=np.float64, ave=False, sv=False,
quiet=False):
"""
A class to read the B_coeff_cmb files
:param tag: if you specify a pattern, it tries to read the corresponding files
:type tag: str
:param ratio_cmb_surface: ratio of CMB to surface radius (default is 1)
:type ratio_cmb_surface: float
:param scale_b: magnetic field unit (default is 1)
:type scale_b: float
:param iplot: a logical to toggle the plot (default is True)
:type iplot: int
:param precision: single or double precision
:type precision: char
:param ave: load a time-averaged CMB file when set to True
:type ave: bool
:param sv: load a dt_b CMB file when set to True
:type sv: bool
:param quiet: verbose when toggled to True (default is True)
:type quiet: bool
:param lCut: reduce the spherical harmonic truncation to l <= lCut
:type lCut: int
:param datadir: working directory
:type datadir: str
"""
pattern = os.path.join(datadir, 'log.*')
logFiles = scanDir(pattern)
if ave:
self.name = 'B_coeff_cmb_ave'
elif sv:
self.name = 'B_coeff_dt_cmb'
else:
self.name = 'B_coeff_cmb'
if tag is not None:
pattern = os.path.join(datadir, '{}.{}'.format(self.name, tag))
files = scanDir(pattern)
# Either the log.tag directly exists and the setup is easy to obtain
if os.path.exists(os.path.join(datadir, 'log.{}'.format(tag))):
MagicSetup.__init__(self, datadir=datadir, quiet=True,
nml='log.{}'.format(tag))
# Or the tag is a bit more complicated and we need to find
# the corresponding log file
else:
pattern = os.path.join(datadir, '{}'.format(self.name))
mask = re.compile(r'{}\.(.*)'.format(pattern))
if mask.match(files[-1]):
ending = mask.search(files[-1]).groups(0)[0]
pattern = os.path.join(datadir, 'log.{}'.format(ending))
if logFiles.__contains__(pattern):
MagicSetup.__init__(self, datadir=datadir, quiet=True,
nml='log.{}'.format(ending))
else:
pattern = os.path.join(datadir, '{}.*'.format(self.name))
files = scanDir(pattern)
filename = files[-1]
# Determine the setup
mask = re.compile(r'{}\.(.*)'.format(self.name))
ending = mask.search(files[-1]).groups(0)[0]
if os.path.exists('log.{}'.format(ending)):
try:
MagicSetup.__init__(self, datadir=datadir, quiet=True,
nml='log.{}'.format(ending))
except AttributeError:
pass
self.rcmb = 1./(1.-self.radratio)
ricb = self.radratio/(1.-self.radratio)
# Read the B_coeff files (by stacking the different tags)
data = []
for k, file in enumerate(files):
if not quiet: print('Reading {}'.format(file))
f = npfile(file, endian='B')
self.l_max_cmb, self.minc, n_data = f.fort_read('i')
self.m_max_cmb = int((self.l_max_cmb/self.minc)*self.minc)
while 1:
try:
data.append(f.fort_read(precision))
except TypeError:
break
self.lm_max_cmb = self.m_max_cmb*(self.l_max_cmb+1)//self.minc - \
self.m_max_cmb*(self.m_max_cmb-self.minc)//(2*self.minc) + \
self.l_max_cmb-self.m_max_cmb+1
# Get indices location
self.idx = np.zeros((self.l_max_cmb+1, self.m_max_cmb+1), 'i')
self.ell = np.zeros(self.lm_max_cmb, 'i')
self.ms = np.zeros(self.lm_max_cmb, 'i')
self.idx[0:self.l_max_cmb+2, 0] = np.arange(self.l_max_cmb+1)
self.ell[0:self.l_max_cmb+2] = np.arange(self.l_max_cmb+2)
k = self.l_max_cmb+1
for m in range(self.minc, self.l_max_cmb+1, self.minc):
for l in range(m, self.l_max_cmb+1):
self.idx[l, m] = k
self.ell[self.idx[l,m]] = l
self.ms[self.idx[l,m]] = m
k +=1
# Rearange data
data = np.array(data, dtype=precision)
self.nstep = data.shape[0]
self.blm = np.zeros((self.nstep, self.lm_max_cmb), np.complex128)
self.blm[:, 1:self.l_max_cmb+1] = data[:, 1:self.l_max_cmb+1]
self.blm[:, self.l_max_cmb+1:] = data[:, self.l_max_cmb+1::2]+\
1j*data[:, self.l_max_cmb+2::2]
# Truncate!
if lCut is not None:
if lCut < self.l_max_cmb:
self.truncate(lCut)
# Get time
self.time = np.zeros(self.nstep, precision)
self.time = data[:, 0]
# Get Gauss coefficients
self.glm = np.zeros((self.nstep, self.lm_max_cmb), precision)
self.hlm = np.zeros((self.nstep, self.lm_max_cmb), precision)
self.glm, self.hlm = getGauss(self.blm.real, self.blm.imag,
self.ell, self.ms, scale_b,
ratio_cmb_surface, self.rcmb)
# Time-averaged Gauss coefficient
if not ave:
facT = 1./(self.time[-1]-self.time[0])
self.glmM = facT * np.trapz(self.glm, self.time, axis=0)
self.hlmM = facT * np.trapz(self.hlm, self.time, axis=0)
if len(self.time) > 3:
self.dglmdt = deriv(self.time, self.glm.T, axis=1)
self.dhlmdt = deriv(self.time, self.hlm.T, axis=1)
self.dglmdt = self.dglmdt.T
self.dhlmdt = self.dhlmdt.T
else:
self.dglmdt = np.zeros_like(self.glm)
self.dhlmdt = np.zeros_like(self.hlm)
# Magnetic energy (Lowes)
self.El = np.zeros((self.nstep, self.l_max_cmb+1), precision)
self.Em = np.zeros((self.nstep, self.m_max_cmb+1), precision)
self.ESVl = np.zeros((self.nstep, self.l_max_cmb+1), precision)
E = 0.
for l in range(1, self.l_max_cmb+1):
self.El[:, l] = 0.
self.ESVl[:, l] = 0.
for m in range(0, l+1, self.minc):
lm = self.idx[l, m]
self.El[:, l] += (self.ell[lm]+1)*\
(self.glm[:,lm]**2+self.hlm[:,lm]**2)
self.Em[:, m] += (self.ell[lm]+1)*\
(self.glm[:,lm]**2+self.hlm[:,lm]**2)
if not ave:
self.ESVl[:, l] += (self.ell[lm]+1)*\
(self.dglmdt[:, lm]**2+self.dhlmdt[:, lm]**2)
if not ave:
# Time-averaged energy
self.ElM = facT * np.trapz(self.El, self.time, axis=0)
self.EmM = facT * np.trapz(self.Em, self.time, axis=0)
# Secular variation
self.ESVlM = facT * np.trapz(self.ESVl, self.time, axis=0)
if abs(self.ESVlM[1:]).min() > 0.:
self.taul = np.sqrt(self.ElM[1:]/self.ESVlM[1:])
else:
self.taul = np.zeros_like(self.ElM[1:])
if iplot and not ave:
self.plot()
[docs] def __add__(self, new):
"""
Built-in function to sum two cmb files
.. note:: So far this function only works for two cmb files with the same
grid sizes. At some point, we might introduce grid extrapolation
to allow any summation/
"""
out = copy.deepcopy(new)
out.nstep = new.nstep+self.nstep
out.time = np.concatenate((self.time, new.time), axis=0)
out.blm = np.concatenate((self.blm, new.blm), axis=0)
out.glm = np.concatenate((self.glm, new.glm), axis=0)
out.hlm = np.concatenate((self.hlm, new.hlm), axis=0)
out.El = np.concatenate((self.El, new.El), axis=0)
out.Em = np.concatenate((self.Em, new.Em), axis=0)
out.ESVl = np.concatenate((self.ESVl, new.ESVl), axis=0)
return out
[docs] def truncate(self, lCut):
"""
:param lCut: truncate to spherical harmonic degree lCut
:type lCut: int
"""
self.l_max_cmb = lCut
self.m_max_cmb = int((self.l_max_cmb/self.minc)*self.minc)
self.lm_max_cmb = self.m_max_cmb*(self.l_max_cmb+1)//self.minc - \
self.m_max_cmb*(self.m_max_cmb-self.minc)//(2*self.minc) + \
self.l_max_cmb-self.m_max_cmb+1
# Get indices location
idx_new = np.zeros((self.l_max_cmb+1, self.m_max_cmb+1), 'i')
ell_new = np.zeros(self.lm_max_cmb, 'i')
ms_new = np.zeros(self.lm_max_cmb, 'i')
idx_new[0:self.l_max_cmb+2, 0] = np.arange(self.l_max_cmb+1)
ell_new[0:self.l_max_cmb+2] = np.arange(self.l_max_cmb+2)
k = self.l_max_cmb+1
for m in range(self.minc, self.l_max_cmb+1, self.minc):
for l in range(m, self.l_max_cmb+1):
idx_new[l, m] = k
ell_new[idx_new[l,m]] = l
ms_new[idx_new[l,m]] = m
k +=1
blm_new = np.zeros((self.nstep, self.lm_max_cmb), np.complex128)
for l in range(1, self.l_max_cmb+1):
for m in range(0, l+1, self.minc):
lm = idx_new[l, m]
blm_new[:, lm] = self.blm[:, self.idx[l,m]]
self.idx = idx_new
self.ell = ell_new
self.ms = ms_new
self.blm = blm_new
[docs] def plot(self):
"""
Display some results when iplot is set to True
"""
ell = np.arange(self.l_max_cmb+1)
fig = plt.figure()
ax = fig.add_subplot(211)
ax.semilogy(ell[1:], self.ElM[1:], marker='o')
if labTex:
ax.set_xlabel(r'$\ell$')
else:
ax.set_xlabel('Degree l')
ax.set_ylabel('Magnetic energy')
ax.set_xlim(1., self.l_max_cmb)
ax1 = fig.add_subplot(212)
ax1.semilogy(ell[0:self.m_max_cmb+1:self.minc], self.EmM[::self.minc],
marker='o')
if labTex:
ax1.set_xlabel(r'$m$')
else:
ax1.set_xlabel('Order m')
ax1.set_ylabel('Magnetic energy')
fig1 = plt.figure()
ax = fig1.add_subplot(111)
ax.loglog(ell[1:], self.taul, marker='o')
if labTex:
ax.set_xlabel(r'$\ell$')
ax.set_ylabel(r'$\tau_\ell$')
else:
ax.set_xlabel('Degree l')
ax.set_ylabel('tau l')
ax.set_xlim(1, self.l_max_cmb)
fig2 = plt.figure()
ax = fig2.add_subplot(111)
ax.plot(self.time, self.glm[:, self.idx[1,0]], label='g10')
ax.plot(self.time, self.glm[:, self.idx[2,0]], label='g20')
ax.plot(self.time, self.glm[:, self.idx[3,0]], label='g30')
ax.set_xlabel('Time')
ax.set_ylabel('Gauss coefficients')
[docs] def timeLongitude(self, removeMean=True, lat0=0., levels=12, cm='RdYlBu_r',
deminc=True, shtns_lib='shtns'):
"""
Plot the time-longitude diagram of Br (input latitude can be chosen)
.. warning:: the python bindings of `SHTns <https://bitbucket.org/bputigny/shtns-magic>`_ are mandatory to use this plotting function!
:param lat0: value of the latitude
:type lat0: float
:param levels: number of contour levels
:type levels: int
:param cm: name of the colormap
:type cm: str
:param deminc: a logical to indicate if one wants do get rid of the
possible azimuthal symmetry
:type deminc: bool
:param shtns_lib: version of shtns library used: can be either 'shtns'
or 'shtns-magic'
:type shtns_lib: char
:param removeMean: remove the time-averaged part when set to True
:type removeMean: bool
"""
# The python bindings of shtns are mandatory to use this function !!!
import shtns
if removeMean:
blmCut = self.blm-self.blm.mean(axis=0)
else:
blmCut = self.blm
# Define shtns setup
sh = shtns.sht(int(self.l_max_cmb), int(self.m_max_cmb/self.minc),
mres=int(self.minc),
norm=shtns.sht_orthonormal | shtns.SHT_NO_CS_PHASE)
polar_opt_threshold = 1e-10
nlat = max(int(self.l_max_cmb*(3./2./2.)*2.),192)
nphi = 2*nlat/self.minc
nlat, nphi = sh.set_grid(nlat, nphi, polar_opt=polar_opt_threshold)
th = np.linspace(np.pi/2., -np.pi/2., nlat)
lat0 *= np.pi/180.
mask = np.where(abs(th-lat0) == abs(th-lat0).min(), 1, 0)
idx = np.nonzero(mask)[0][0]
# Transform data on grid space
BrCMB = np.zeros((self.nstep, nphi, nlat), np.float64)
if deminc:
dat = np.zeros((self.nstep, self.minc*nphi+1), np.float64)
else:
dat = np.zeros((self.nstep, nphi), np.float64)
for k in range(self.nstep):
tmp = sh.synth(blmCut[k, :]*sh.l*(sh.l+1)/self.rcmb**2)
tmp = tmp.T # Longitude, Latitude
if shtns_lib == 'shtns-magic':
BrCMB[k, ...] = rearangeLat(tmp)
else:
BrCMB[k, ...] = tmp
if deminc:
dat[k, :] = symmetrize(BrCMB[k, :, idx], self.minc)
else:
dat[k, :] = BrCMB[k, :, idx]
th = np.linspace(np.pi/2., -np.pi/2., nlat)
if deminc:
phi = np.linspace(-np.pi, np.pi, self.minc*nphi+1)
else:
phi = np.linspace(-np.pi/self.minc, np.pi/self.minc, nphi)
fig = plt.figure()
ax = fig.add_subplot(111)
vmin = -max(abs(dat.max()), abs(dat.min()))
vmax = -vmin
cs = np.linspace(vmin, vmax, levels)
ax.contourf(phi, self.time, dat, cs, cmap=plt.get_cmap(cm))
ax.set_xlabel('Longitude')
ax.set_ylabel('Time')
w2 = np.fft.fft2(dat)
w2 = abs(w2[1:self.nstep/2+1, 0:self.m_max_cmb+1])
dw = 2.*np.pi/(self.time[-1]-self.time[0])
omega = dw*np.arange(self.nstep)
omega = omega[1:self.nstep/2+1]
ms = np.arange(self.m_max_cmb+1)
fig1 = plt.figure()
ax1 = fig1.add_subplot(111)
ax1.contourf(ms, omega, w2, 17, cmap=plt.get_cmap('jet'))
ax1.set_yscale('log')
ax1.set_xlim(0,13)
ax1.set_xlabel(r'Azimuthal wavenumber')
ax1.set_ylabel(r'Frequency')
[docs] def movieCmb(self, cut=0.5, levels=12, cm='RdYlBu_r', png=False, step=1,
normed=False, dpi=80, bgcolor=None, deminc=True, removeMean=False,
precision=np.float64, contour=False, mer=False):
"""
Plotting function (it can also write the png files)
:param levels: number of contour levels
:type levels: int
:param cm: name of the colormap
:type cm: str
:param cut: adjust the contour extrema to max(abs(data))*cut
:type cut: float
:param png: save the movie as a series of png files when
set to True
:type png: bool
:param dpi: dot per inch when saving PNGs
:type dpi: int
:param bgcolor: background color of the figure
:type bgcolor: str
:param normed: the colormap is rescaled every timestep when set to True,
otherwise it is calculated from the global extrema
:type normed: bool
:param step: the stepping between two timesteps
:type step: int
:param deminc: a logical to indicate if one wants do get rid of the
possible azimuthal symmetry
:type deminc: bool
:param precision: single or double precision
:type precision: char
:param contour: also display the solid contour levels when set to True
:type contour: bool
:param mer: display meridians and circles when set to True
:type mer: bool
:param removeMean: remove the time-averaged part when set to True
:type removeMean: bool
"""
if removeMean:
blmCut = self.blm-self.blm.mean(axis=0)
else:
blmCut = self.blm
nlat = int(max(int(self.l_max_cmb*(3./2./2.)*2.),192))
if np.mod(nlat, 2) == 1:
nlat += 1
nphi = int(2*nlat/self.minc)
# Define spectral transform setup
sh = SpectralTransforms(l_max=self.l_max_cmb, minc=self.minc,
n_theta_max=nlat)
# Transform data on grid space
BrCMB = np.zeros((self.nstep, nphi, nlat), precision)
print('Spectral -> Spatial transform')
for k in progressbar(range(self.nstep)):
BrCMB[k, ...] = sh.spec_spat(blmCut[k, :]*self.ell*(sh.ell+1)/self.rcmb**2)
print('Done')
if png:
plt.ioff()
if not os.path.exists('movie'):
os.mkdir('movie')
else:
plt.ion()
if not normed:
vmin = - max(abs(BrCMB.max()), abs(BrCMB.min()))
vmin = cut * vmin
vmax = -vmin
cs = np.linspace(vmin, vmax, levels)
th = np.linspace(np.pi/2., -np.pi/2., nlat)
if deminc:
phi = np.linspace(-np.pi, np.pi, self.minc*nphi+1)
xxout, yyout = hammer2cart(th, -np.pi)
xxin, yyin = hammer2cart(th, np.pi)
else:
phi = np.linspace(-np.pi/self.minc, np.pi/self.minc, nphi)
xxout, yyout = hammer2cart(th, -np.pi/self.minc)
xxin, yyin = hammer2cart(th, np.pi/self.minc)
ttheta, pphi = np.meshgrid(th, phi)
xx, yy = hammer2cart(ttheta, pphi)
if deminc:
fig = plt.figure(figsize=(8, 4))
else:
fig = plt.figure(figsize=(8/self.minc, 4))
fig.subplots_adjust(top=0.99, right=0.99, bottom=0.01, left=0.01)
ax = fig.add_subplot(111, frameon=False)
if mer:
theta = np.linspace(np.pi/2, -np.pi/2, nlat)
meridians = np.r_[-120, -60, 0, 60, 120]
circles = np.r_[ 60, 30, 0, -30, -60]
for k in range(self.nstep):
if k == 0:
if normed:
vmin = - max(abs(BrCMB[k, ...].max()), abs(BrCMB[k, ...].min()))
vmin = cut * vmin
vmax = -vmin
cs = np.linspace(vmin, vmax, levels)
if deminc:
dat = symmetrize(BrCMB[k, ...], self.minc)
else:
dat = BrCMB[k, ...]
im = ax.contourf(xx, yy, dat, cs, cmap=plt.get_cmap(cm), extend='both')
if contour:
ax.contour(xx, yy, dat, cs, linestyles=['-', '-'],
colors=['k', 'k'], linewidths=[0.7, 0.7])
ax.plot(xxout, yyout, 'k-', lw=1.5)
ax.plot(xxin, yyin, 'k-', lw=1.5)
#ax.text(0.12, 0.9, 't={:.6f}'.format(self.time[0]), fontsize=16,
#horizontalalignment='right',
#verticalalignment='center', transform = ax.transAxes)
if mer:
for lat0 in circles:
x0, y0 = hammer2cart(lat0*np.pi/180., phi)
ax.plot(x0, y0, 'k:', linewidth=0.7)
for lon0 in meridians:
x0, y0 = hammer2cart(theta, lon0*np.pi/180.)
ax.plot(x0, y0, 'k:', linewidth=0.7)
ax.axis('off')
man = plt.get_current_fig_manager()
man.canvas.draw()
if k != 0 and k % step == 0:
if not png:
print(k)
plt.cla()
if normed:
vmin = - max(abs(BrCMB[k, ...].max()), abs(BrCMB[k, ...].min()))
vmin = cut * vmin
vmax = -vmin
cs = np.linspace(vmin, vmax, levels)
if deminc:
dat = symmetrize(BrCMB[k, ...], self.minc)
else:
dat = BrCMB[k, ...]
im = ax.contourf(xx, yy, dat, cs, cmap=plt.get_cmap(cm), extend='both')
if contour:
ax.contour(xx, yy, dat, cs, colors=['k'],
linestyles=['-', '-'], linewidths=[0.7, 0.7])
ax.plot(xxout, yyout, 'k-', lw=1.5)
ax.plot(xxin, yyin, 'k-', lw=1.5)
#ax.text(0.12, 0.9, 't={:.6f}'.format(self.time[k]), fontsize=16,
#horizontalalignment='right',
#verticalalignment='center', transform = ax.transAxes)
if mer:
for lat0 in circles:
x0, y0 = hammer2cart(lat0*np.pi/180., phi)
ax.plot(x0, y0, 'k:', linewidth=0.7)
for lon0 in meridians:
x0, y0 = hammer2cart(theta, lon0*np.pi/180.)
ax.plot(x0, y0, 'k:', linewidth=0.7)
ax.axis('off')
man.canvas.draw()
if png:
filename = 'movie/img{:05d}.png'.format(k)
print('write {}'.format(filename))
#st = 'echo {}'.format(ivar) + ' > movie/imgmax'
if bgcolor is not None:
fig.savefig(filename, facecolor=bgcolor, dpi=dpi)
else:
fig.savefig(filename, dpi=dpi)
[docs]class MagicCoeffR(MagicSetup):
r"""
This class allows to read the :ref:`B_coeff_r#.TAG <secBcoeffrFile>`
and :ref:`V_coeff_r#.TAG <secVcoeffrFile>` files.
It reads the poloidal and toroidal potentials and reconstruct the time
series (or the energy) contained in any given mode.
>>> # Reads the files V_coeff_r2.test*
>>> cr = MagicCoeffR(tag='test*', field='V', r=2)
>>> print(cr.ell, cr.wlm) # print \ell and w_{\ell m}
>>> # Time-evolution of the poloidal energy in the (\ell=10, m=10) mode
>>> plot(cr.time, cr.epolLM[:, cr.idx[10, 10]])
"""
[docs] def __init__(self, tag, datadir='.', ratio_cmb_surface=1, scale_b=1, iplot=True,
field='V', r=1, precision=np.float64, lCut=None, quiet=False, step=1):
"""
:param tag: if you specify a pattern, it tries to read the corresponding files
:type tag: str
:param ratio_cmb_surface: ratio of surface ratio to CMB radius (default is 1)
:type ratio_cmb_surface: float
:param scale_b: magnetic field unit (default is 1)
:type scale_b: float
:param iplot: a logical to toggle the plot (default is True)
:type iplot: bool
:param field: 'B', 'V', 'T' or 'Xi' (magnetic field, velocity field, temperature or composition)
:type field: str
:param r: an integer to characterise which file we want to plot
:type r: int
:param precision: single or double precision
:type precision: str
:param lCut: reduce the spherical harmonic truncation to l <= lCut
:type lCut: int
:param quiet: verbose when toggled to True (default is True)
:type quiet: bool
:param datadir: working directory
:type datadir: str
:param step: step>1 allows to down sample the data
:type step: int
"""
pattern = os.path.join(datadir, 'log.*')
logFiles = scanDir(pattern)
if len(logFiles) != 0:
MagicSetup.__init__(self, quiet=True, nml=logFiles[-1])
else:
str1 = 'Aspect ratio ?\n'
self.radratio = float(input(str1))
self.rcmb = 1./(1.-self.radratio)
ricb = self.radratio/(1.-self.radratio)
pattern = os.path.join(datadir, '{}_coeff_r{}.{}'.format(field,r,tag))
files = scanDir(pattern)
# Read the B_coeff files (by stacking the different tags)
data = []
for k, file in enumerate(files):
if not quiet: print('Reading {}'.format(file))
f = npfile(file, endian='B')
if precision == np.float32:
out = f.fort_read('3i4,f4')[0]
else:
out = f.fort_read('3i4,f8')[0]
self.l_max_r, self.minc, n_data = out[0]
self.m_max_r = int((self.l_max_r//self.minc)*self.minc)
self.radius = out[1]
while 1:
try:
data.append(f.fort_read(precision))
except TypeError:
break
self.lm_max_r = self.m_max_r*(self.l_max_r+1)//self.minc - \
self.m_max_r*(self.m_max_r-self.minc)//(2*self.minc) + \
self.l_max_r-self.m_max_r+1
# Get indices location
self.idx = np.zeros((self.l_max_r+1, self.m_max_r+1), 'i')
self.ell = np.zeros(self.lm_max_r, 'i')
self.ms = np.zeros(self.lm_max_r, 'i')
self.idx[0:self.l_max_r+2, 0] = np.arange(self.l_max_r+1)
self.ell[0:self.l_max_r+2] = np.arange(self.l_max_r+2)
k = self.l_max_r+1
for m in range(self.minc, self.l_max_r+1, self.minc):
for l in range(m, self.l_max_r+1):
self.idx[l, m] = k
self.ell[self.idx[l,m]] = l
self.ms[self.idx[l,m]] = m
k +=1
# Rearange data
data = np.array(data, dtype=precision)
self.nstep = data.shape[0]
self.wlm = np.zeros((self.nstep, self.lm_max_r), np.complex128)
if field == 'V' or field == 'B':
self.dwlm = np.zeros((self.nstep, self.lm_max_r), np.complex128)
self.zlm = np.zeros((self.nstep, self.lm_max_r), np.complex128)
# Get time
self.time = np.zeros(self.nstep, dtype=precision)
self.time = data[:, 0]
# wlm
if field == 'T' or field == 'Xi': #T or Xi contains l = m = 0
self.wlm[:, 0:self.l_max_r+1] = data[:, 1:self.l_max_r+2]
k = self.l_max_r+2
else:
self.wlm[:, 1:self.l_max_r+1] = data[:, 1:self.l_max_r+1]
k = self.l_max_r+1
for m in range(self.minc, self.l_max_r+1, self.minc):
for l in range(m, self.l_max_r+1):
self.wlm[:, self.idx[l, m]] = data[:, k]+1j*data[:, k+1]
k += 2
if step > 1:
self.wlm = self.wlm[::step, :]
if field == 'V' or field == 'B':
# dwlm
self.dwlm[:, 1:self.l_max_r+1] = data[:, k:k+self.l_max_r]
k += self.l_max_r
for m in range(self.minc, self.l_max_r+1, self.minc):
for l in range(m, self.l_max_r+1):
self.dwlm[:, self.idx[l, m]] = data[:, k]+1j*data[:, k+1]
k += 2
# zlm
self.zlm[:, 1:self.l_max_r+1] = data[:, k:k+self.l_max_r]
k += self.l_max_r
for m in range(self.minc, self.l_max_r+1, self.minc):
for l in range(m, self.l_max_r+1):
self.zlm[:, self.idx[l, m]] = data[:, k]+1j*data[:, k+1]
k += 2
if step > 1:
self.dwlm = self.dwlm[::step, :]
self.zlm = self.dwlm[::step, :]
# ddw in case B is stored
if field == 'B':
self.ddwlm = np.zeros((self.nstep, self.lm_max_r), np.complex128)
self.ddwlm[:, 1:self.l_max_r+1] = data[:, k:k+self.l_max_r]
k += self.l_max_r
for m in range(self.minc, self.l_max_r+1, self.minc):
for l in range(m, self.l_max_r+1):
self.ddwlm[:, self.idx[l, m]] = data[:, k]+1j*data[:, k+1]
k += 2
if step > 1:
self.ddwlm = self.ddwlm[::step, :]
# Truncate!
if lCut is not None:
if lCut < self.l_max_r:
self.truncate(lCut, field=field)
if step > 1:
self.time = self.time[::step]
self.nstep = len(self.time)
if field == 'V' or field == 'B':
self.e_pol_axi_l = np.zeros((self.nstep, self.l_max_r+1), precision)
self.e_tor_axi_l = np.zeros((self.nstep, self.l_max_r+1), precision)
self.e_pol_l = np.zeros((self.nstep, self.l_max_r+1), precision)
self.e_tor_l = np.zeros((self.nstep, self.l_max_r+1), precision)
for l in range(1, self.l_max_r+1):
self.e_pol_l[:, l] = 0.
self.e_tor_l[:, l] = 0.
self.e_pol_axi_l[:, l] = 0.
self.e_tor_axi_l[:, l] = 0.
for m in range(0, l+1, self.minc):
lm = self.idx[l, m]
if m == 0:
epol = 0.5*self.ell[lm]*(self.ell[lm]+1)*( \
self.ell[lm]*(self.ell[lm]+1)/self.radius**2* \
abs(self.wlm[:,lm])**2+ abs(self.dwlm[:,lm])**2 )
etor = 0.5*self.ell[lm]*(self.ell[lm]+1)*abs(self.zlm[:, lm])**2
self.e_pol_axi_l[:, l] += epol
self.e_tor_axi_l[:, l] += etor
else:
epol = self.ell[lm]*(self.ell[lm]+1)*( \
self.ell[lm]*(self.ell[lm]+1)/self.radius**2* \
abs(self.wlm[:,lm])**2+ abs(self.dwlm[:,lm])**2 )
etor = self.ell[lm]*(self.ell[lm]+1)*abs(self.zlm[:, lm])**2
self.e_pol_l[:, l] += epol
self.e_tor_l[:, l] += etor
# Time-averaged energy
facT = 1./(self.time[-1]-self.time[0])
self.e_pol_lM = facT * np.trapz(self.e_pol_l, self.time, axis=0)
self.e_tor_lM = facT * np.trapz(self.e_tor_l, self.time, axis=0)
self.e_pol_axi_lM = facT * np.trapz(self.e_pol_axi_l, self.time, axis=0)
self.e_tor_axi_lM = facT * np.trapz(self.e_tor_axi_l, self.time, axis=0)
[docs] def truncate(self, lCut, field='B'):
"""
:param lCut: truncate to spherical harmonic degree lCut
:type lCut: int
:param field: name of the field ('V', 'B' or 'T')
:type field: char
"""
self.l_max_r = lCut
self.m_max_r = int((self.l_max_r/self.minc)*self.minc)
self.lm_max_r = int(self.m_max_r*(self.l_max_r+1)/self.minc - \
self.m_max_r*(self.m_max_r-self.minc)/(2*self.minc) + \
self.l_max_r-self.m_max_r+1)
# Get indices location
idx_new = np.zeros((self.l_max_r+1, self.m_max_r+1), 'i')
ell_new = np.zeros(self.lm_max_r, 'i')
ms_new = np.zeros(self.lm_max_r, 'i')
idx_new[0:self.l_max_r+2, 0] = np.arange(self.l_max_r+1)
ell_new[0:self.l_max_r+2] = np.arange(self.l_max_r+2)
k = self.l_max_r+1
for m in range(self.minc, self.l_max_r+1, self.minc):
for l in range(m, self.l_max_r+1):
idx_new[l, m] = k
ell_new[idx_new[l,m]] = l
ms_new[idx_new[l,m]] = m
k +=1
wlm_new = np.zeros((self.nstep, self.lm_max_r), np.complex128)
if field == 'V' or field == 'B':
dwlm_new = np.zeros((self.nstep, self.lm_max_r), np.complex128)
zlm_new = np.zeros((self.nstep, self.lm_max_r), np.complex128)
if field == 'B':
ddwlm_new = np.zeros((self.nstep, self.lm_max_r), np.complex128)
for l in range(1, self.l_max_r+1):
for m in range(0, l+1, self.minc):
lm = idx_new[l, m]
wlm_new[:, lm] = self.wlm[:, self.idx[l,m]]
zlm_new[:, lm] = self.zlm[:, self.idx[l,m]]
dwlm_new[:, lm] = self.dwlm[:, self.idx[l,m]]
if field == 'B':
ddwlm_new[:, lm] = self.ddwlm[:, self.idx[l,m]]
else:
for l in range(1, self.l_max_r+1):
for m in range(0, l+1, self.minc):
lm = idx_new[l, m]
wlm_new[:, lm] = self.wlm[:, self.idx[l,m]]
#for m in range(self.minc, self.l_max_r+1, self.minc):
#for l in range(m, self.l_max_r+1):
#wlm_new[:, idx_new[l, m]] = self.wlm[:, self.idx[l,m]]
#dwlm_new[:, idx_new[l, m]] = self.dwlm[:, self.idx[l,m]]
#zlm_new[:, idx_new[l, m]] = self.zlm[:, self.idx[l,m]]
#if field == 'B':
#ddwlm_new[:, idx_new[l, m]] = self.ddwlm[:, self.idx[l,m]]
self.idx = idx_new
self.ell = ell_new
self.ms = ms_new
self.wlm = wlm_new
if field == 'V' or field == 'B':
self.dwlm = dwlm_new
self.zlm = zlm_new
if field == 'B':
self.ddwlm = ddwlm_new
[docs] def movieRad(self, cut=0.5, levels=12, cm='RdYlBu_r', png=False, step=1,
normed=False, dpi=80, bgcolor=None, deminc=True, removeMean=False,
precision=np.float64, contour=False, mer=False):
"""
Plotting function (it can also write the png files)
:param levels: number of contour levels
:type levels: int
:param cm: name of the colormap
:type cm: str
:param cut: adjust the contour extrema to max(abs(data))*cut
:type cut: float
:param png: save the movie as a series of png files when
set to True
:type png: bool
:param dpi: dot per inch when saving PNGs
:type dpi: int
:param bgcolor: background color of the figure
:type bgcolor: str
:param normed: the colormap is rescaled every timestep when set to True,
otherwise it is calculated from the global extrema
:type normed: bool
:param step: the stepping between two timesteps
:type step: int
:param deminc: a logical to indicate if one wants do get rid of the
possible azimuthal symmetry
:type deminc: bool
:param precision: single or double precision
:type precision: char
:param contour: also display the solid contour levels when set to True
:type contour: bool
:param mer: display meridians and circles when set to True
:type mer: bool
:param removeMean: remove the time-averaged part when set to True
:type removeMean: bool
"""
if removeMean:
dataCut = self.wlm-self.wlm.mean(axis=0)
else:
dataCut = self.wlm
nlat = max(int(self.l_max_r*(3./2./2.)*2.),192)
nphi = 2*nlat//self.minc
# Define spectral transform setup
sh = SpectralTransforms(l_max=self.l_max_r, minc=self.minc,
lm_max=self.lm_max_r,
n_theta_max=nlat)
"""
# The python bindings of shtns are mandatory to use this function !!!
import shtns
# Define shtns setup
sh = shtns.sht(int(self.l_max_r), int(self.m_max_r/self.minc),
mres=int(self.minc),
norm=shtns.sht_orthonormal | shtns.SHT_NO_CS_PHASE)
"""
# Transform data on grid space
data = np.zeros((self.nstep, nphi, nlat), precision)
print('Spectral -> Spatial transform')
for k in progressbar(range(self.nstep)):
data[k, ...] = sh.spec_spat(dataCut[k, :]*self.ell*(self.ell+1)/self.radius**2)
print('Done')
if png:
plt.ioff()
if not os.path.exists('movie'):
os.mkdir('movie')
else:
plt.ion()
if not normed:
vmin = - max(abs(data.max()), abs(data.min()))
vmin = cut * vmin
vmax = -vmin
cs = np.linspace(vmin, vmax, levels)
th = np.linspace(np.pi/2., -np.pi/2., nlat)
if deminc:
phi = np.linspace(-np.pi, np.pi, self.minc*nphi+1)
xxout, yyout = hammer2cart(th, -np.pi)
xxin, yyin = hammer2cart(th, np.pi)
else:
phi = np.linspace(-np.pi/self.minc, np.pi/self.minc, nphi)
xxout, yyout = hammer2cart(th, -np.pi/self.minc)
xxin, yyin = hammer2cart(th, np.pi/self.minc)
ttheta, pphi = np.meshgrid(th, phi)
xx, yy = hammer2cart(ttheta, pphi)
if deminc:
fig = plt.figure(figsize=(8, 4))
else:
fig = plt.figure(figsize=(8/self.minc, 4))
fig.subplots_adjust(top=0.99, right=0.99, bottom=0.01, left=0.01)
ax = fig.add_subplot(111, frameon=False)
if mer:
theta = np.linspace(np.pi/2, -np.pi/2, nlat)
meridians = np.r_[-120, -60, 0, 60, 120]
circles = np.r_[ 60, 30, 0, -30, -60]
for k in range(self.nstep):
if k == 0:
if normed:
vmin = - max(abs(data[k, ...].max()), abs(data[k, ...].min()))
vmin = cut * vmin
vmax = -vmin
cs = np.linspace(vmin, vmax, levels)
if deminc:
dat = symmetrize(data[k, ...], self.minc)
else:
dat = data[k, ...]
im = ax.contourf(xx, yy, dat, cs, cmap=plt.get_cmap(cm), extend='both')
if contour:
ax.contour(xx, yy, dat, cs, linestyles=['-', '-'],
colors=['k', 'k'], linewidths=[0.7, 0.7])
ax.plot(xxout, yyout, 'k-', lw=1.5)
ax.plot(xxin, yyin, 'k-', lw=1.5)
if mer:
for lat0 in circles:
x0, y0 = hammer2cart(lat0*np.pi/180., phi)
ax.plot(x0, y0, 'k:', linewidth=0.7)
for lon0 in meridians:
x0, y0 = hammer2cart(theta, lon0*np.pi/180.)
ax.plot(x0, y0, 'k:', linewidth=0.7)
ax.axis('off')
man = plt.get_current_fig_manager()
man.canvas.draw()
if png:
filename = 'movie/img{:05d}.png'.format(k)
print('write {}'.format(filename))
#st = 'echo {}'.format(ivar) + ' > movie/imgmax'
if bgcolor is not None:
fig.savefig(filename, facecolor=bgcolor, dpi=dpi)
else:
fig.savefig(filename, dpi=dpi)
elif k != 0 and k % step == 0:
if not png:
print(k)
plt.cla()
if normed:
vmin = - max(abs(data[k, ...].max()), abs(data[k, ...].min()))
vmin = cut * vmin
vmax = -vmin
cs = np.linspace(vmin, vmax, levels)
if deminc:
dat = symmetrize(data[k, ...], self.minc)
else:
dat = data[k, ...]
im = ax.contourf(xx, yy, dat, cs, cmap=plt.get_cmap(cm), extend='both')
if contour:
ax.contour(xx, yy, dat, cs, colors=['k'],
linestyles=['-', '-'], linewidths=[0.7, 0.7])
ax.plot(xxout, yyout, 'k-', lw=1.5)
ax.plot(xxin, yyin, 'k-', lw=1.5)
if mer:
for lat0 in circles:
x0, y0 = hammer2cart(lat0*np.pi/180., phi)
ax.plot(x0, y0, 'k:', linewidth=0.7)
for lon0 in meridians:
x0, y0 = hammer2cart(theta, lon0*np.pi/180.)
ax.plot(x0, y0, 'k:', linewidth=0.7)
ax.axis('off')
man.canvas.draw()
if png:
filename = 'movie/img{:05d}.png'.format(k)
print('write {}'.format(filename))
#st = 'echo {}'.format(ivar) + ' > movie/imgmax'
if bgcolor is not None:
fig.savefig(filename, facecolor=bgcolor, dpi=dpi)
else:
fig.savefig(filename, dpi=dpi)
[docs] def fft(self, pcolor=False, cm='turbo'):
"""
Fourier transform of the poloidal potential
:param pcolor: this is a switch to use pcolormesh instead of contourf
:type pcolor: bool
:param cm: the name of the colormap (default is 'turbo')
:type cm: char
"""
dt = np.diff(self.time)
# If the data is not regularly sampled, use splines to resample them
if dt.min() != dt.max():
time = np.linspace(self.time[0], self.time[-1], self.nstep)
it = interp1d(self.time, self.wlm, axis=0)
wlm = it(time)
else:
time = self.time
wlm = self.wlm
wlm_hat = np.fft.fft(wlm, axis=0)
ek = np.zeros((self.nstep//2, self.l_max_r+1), np.float64)
for l in range(1, self.l_max_r+1):
ek[:, l] = 0.
for m in range(0, l+1, self.minc):
lm = self.idx[l, m]
if m == 0:
epol = 0.5 * abs(wlm_hat[:, lm])**2
else:
epol = abs(wlm_hat[:, lm])**2
ek[:, l] += epol[1:self.nstep//2+1]
ek = ek[:, 1:] # remove l=0
self.ek_omega = ek
dw = 2.*np.pi/(time[-1]-time[0])
omega = dw*np.arange(self.nstep)
self.omega = omega[1:self.nstep//2+1]
ls = np.arange(self.l_max_r+1)
ls = ls[1:]
dat = np.log10(ek)
vmax = dat.max()-0.5
#vmin = vmax - 7
vmin = max(vmax-10, dat.min()+2)
levs = np.linspace(vmin, vmax, 129)
fig = plt.figure()
ax = fig.add_subplot(111)
if pcolor:
im = ax.pcolormesh(ls, self.omega, dat, cmap=plt.get_cmap(cm),
vmin=vmin, vmax=vmax)
else:
im = ax.contourf(ls, self.omega, dat, levs, cmap=plt.get_cmap(cm),
extend='both')
ax.set_yscale('log')
cbar = fig.colorbar(im)
ax.set_xlabel('Spherical harmonic degree')
ax.set_ylabel('Frequency')
fig.tight_layout()
[docs] def cwt(self, ell, w0=20, nfreq=256, fmin_fac=8, fmax_fac=0.5,
cm='turbo', logscale=False):
r"""
Build a time-frequency spectrum at a given degree :math:`\ell` using
a continuous wavelet transform with morlet wavelets.
:param w0: a parameter to normalize the width of the wavelet
:type w0: float
:param fmin_fac: a factor to adjust the minimum frequency considered
in the time-frequency domain. Minimum frequency is
given by fmin=1/(time[-1]-time[0]), such that
the minimum frequency retained is fmin_fac*fmin
:type fmin_fac: float
:param fmax_fac: a factor to adjust the maximum frequency retained
in the time-frequency domain. Maximum frequency is
given by fmax=fmax_fac*fcut, where fcut is 1/dt.
:type fmax_fac: float
:param ell: spherical harmonic degree at which ones want to build
the time frequency diagram. If one gives a negative
number, then the sum over all mode is computed.
:type ell: int
:param nfreq: number of frequency bins
:type nfreq: int
:param cm: the name of the colormap (default is 'turbo')
:type cm: char
:param logscale: when turned to True, this displays the amplitude in
logarithmic scale (linear by default)
:type logscale: bool
"""
assert version > '1.4.0'
dt = np.diff(self.time)
# If the data is not regularly sampled, use splines to resample them
if dt.min() != dt.max():
time = np.linspace(self.time[0], self.time[-1], self.nstep)
it = interp1d(self.time, self.wlm, axis=0)
wlm = it(time)
else:
time = self.time
wlm = self.wlm
dt = time[1]-time[0]
fcut = 1./dt # Maximum sampling
fmin = 1./(time[-1]-time[0]) # Minimum frequency
#self.omega = 2.*np.pi*np.linspace(fmin*fmin_fac, fcut/2, 100)
self.omega = np.logspace(np.log10(fmin*fmin_fac), np.log10(fmax_fac*fcut), nfreq)
self.omega *= 2.*np.pi
# Define the widths of the wavelets (related to their frequency)
widths = w0*fcut/self.omega
#widths = np.arange(1, len(self.time)//8)
self.ek_time_omega = np.zeros((len(widths), self.nstep), np.float64)
if ell < 0: # Then the sum is computed over all ell's
for ll in range(1, self.l_max_r+1):
print(ll)
for m in range(0, ll+1, self.minc):
lm = self.idx[ll, m]
out = signal.cwt(wlm[:, lm], signal.morlet2, widths, w=w0)
if m == 0:
tmp = 0.5*abs(out)**2
else:
tmp = abs(out)**2
self.ek_time_omega += tmp
else:
for m in range(0, ell+1, self.minc):
print(m)
lm = self.idx[ell, m]
out = signal.cwt(wlm[:, lm], signal.morlet2, widths, w=w0)
if m == 0:
tmp = 0.5*abs(out)**2
else:
tmp = abs(out)**2
self.ek_time_omega += tmp
if logscale:
dat = np.log10(self.ek_time_omega)
vmax = dat.max()#-0.5
vmin = max(vmax-10, dat.min()+2)
levs = np.linspace(vmin, vmax, 64)
else:
dat = self.ek_time_omega
levs = 64
fig = plt.figure()
ax = fig.add_subplot(111)
im = ax.contourf(time, self.omega, dat, levs, cmap=plt.get_cmap(cm),
extend='both')
#im = ax.pcolormesh(time, self.omega, dat, cmap=plt.get_cmap('turbo'),
# shading='gouraud')
ax.set_yscale('log')
cbar = fig.colorbar(im)
ax.set_xlabel('Time')
ax.set_ylabel('Frequency')
fig.tight_layout()