#!/usr/bin/env python
'''
The PyBats submodule for handling input and output for the Dynamic Global
Core Plasma Model (DGCPM), a plasmasphere module of the SWMF.
'''
import numpy as np
from spacepy.plot import applySmartTimeTicks, set_target
from spacepy.pybats import PbData
def _adjust_dialplot(ax, rad, title='12', labelsize=15):
'''
Ram output is often visualized with equatorial dial plots. This
function quickly adjusts those plots to give a uniform, clean
look and feel.
'''
from numpy import max, pi
from matplotlib.ticker import MultipleLocator
from spacepy.pybats.ram import add_body_polar
# Constrain range of plot:
ax.set_ylim([0, max(rad)])
# Set MLT labels:
lt_labels = ['06', title, '18', '00']
xticks = [0, pi/2, pi, 3*pi/2]
ax.set_xticks(xticks)
ax.set_xticklabels(lt_labels)
ax.tick_params('x', labelsize=14)
# Set L labels and grid. Turn off label at L=0.
ax.yaxis.set_major_locator(MultipleLocator(2))
ax.figure.canvas.draw()
labs = [item.get_text() for item in ax.get_yticklabels()]
if labelsize > 0:
ax.set_yticklabels(labs, color='w', size=labelsize,
backgroundcolor='k')
labels = ax.get_yticklabels()
labels[0].set_visible(False)
else:
ax.set_yticklabels('')
labels = ax.get_yticklabels()
labels[0].set_visible(False)
# Turn on grid.
ax.grid(True, c='w', lw=1.5, ls=':')
# Change background color so labels stand out.
ax.set_facecolor('gray')
add_body_polar(ax)
[docs]
def saturation(L):
'''
Return saturation density of a flux tube as a function of L-shell in units
of :math:`\\#/cm^3`. Formula is from Carpenter and Anderson, JGR, 1992.
'''
return 10**(-0.3145*L + 3.9043)
[docs]
def refill_flux(n, L, tau=1.5, chi='Auto'):
'''
Calculate the refill flux of a flux tube of density *n*, L-shell *L*,
using a refill time constant of *tau*. If kwarg *chi* is set to 'Auto',
then zenith angle dependence is calculated using an assumed dipole field.
To remove this dependence, set *chi* to a solar zenith angle in degrees.
'''
if chi == 'Auto':
chi = np.pi/2. - np.arccos(1./np.sqrt(L))
else:
chi *= np.pi/180.0
# Convenient values:
vmin = 1879379699284659.0 # Taken right from pbo.f!
Lmin = 1.29762044
# Start with fmax.
fmax = 2.*vmin*Lmin*saturation(Lmin)/(tau*24.*3600.0)
# Calculate refill rate.
nsat = saturation(L)
flux = (nsat-n)/nsat * fmax * np.sin(chi) * L**-3
return flux
[docs]
class PlasmaFile(PbData):
'''
DGCPM's plasma files contain a plethora of values from the model's 2D plane.
This class produces objects that load the data and sort it into a PyBats
data object.
'''
[docs]
def __init__(self, filename, *args, **kwargs):
'''
Reads the data; sorts into arrays.
'''
import datetime as dt
import gzip
from spacepy.datamodel import dmarray
super(PlasmaFile, self).__init__(*args, **kwargs) # Init as PbData.
self.attrs['file'] = filename
# Some visualization defaults.
self._default_cmaps = {'n':'Greens_r', 'pot':'bwr'}
self._default_zlims = {'n':[1, 1000], 'pot':[-150, 150]}
# Set units and variable names.
# Some units are not what are actually in the file; see conversions
# below (i.e. density defaults to m^-3, but that's unweildy.)
units = [('n', 'cm^{-3}'), ('x', 'R_E'), ('y', 'R_E'),
('b_open', 'boolean'), ('theta', 'degrees'),
('phi', 'degrees'), ('pot', 'kV',), ('corot', 'kV'),
('vr', 'm/s'), ('vphi', 'm/s'), ('source', 'm^-3'),
('fluxphi', 'm^{-3}s^{-1}'), ('fluxr', 'm^{-3}s^{-1}'),
('totaln', '#'), ('vol', 'km^3')]
units = dict(units)
# Open file; unzip if necessary.
if self.attrs['file'][-3:] == '.gz':
infile = gzip.open(self.attrs['file'], 'rt')
else:
infile = open(self.attrs['file'], 'r')
# Read the header of the file.
parts = infile.readline().split()
nLat, nLon = int(parts[-2]), int(parts[-1])
self.attrs['time'] = dt.datetime.strptime(parts[0],
'T=%Y%m%d_%H%M%S_000')
varlist = infile.readline().lower().split()[2:]
# Create containers for data:
for v in varlist:
unit = '' if v not in units else units[v]
self[v] = dmarray(np.zeros((nLat, nLon)), {'units': unit})
# Read rest of file and sort data into arrays:
for line in infile.readlines():
parts = line.split()
i, j = int(parts.pop(0))-1, int(parts.pop(0))-1
for v, x in zip(varlist, parts):
self[v][i, j] = x
# That's it for our file.
infile.close()
# Create some "helper" variables:
self['lat'] = dmarray(self['theta'][:, 0], {'units':'degrees'})
self['lon'] = dmarray(self['phi'][0, :], {'units':'degrees'})
self['mlt'] = dmarray(self['lon']/15.0, {'units':'hours'})
# Calculate L-Shell for convenience.
self['L'] = dmarray(np.cos(self['lat']*np.pi/180.)**-2,
{'units':'R_E'})
# Sometimes, SI units aren't the best.
if 'n' in self:
self['n'] /= 100.0**3
if 'pot' in self:
self['pot'] /= 1000.0
if 'corot' in self:
self['corot'] /= 1000.0
# Some other useful attributes.
self.attrs['dLat'] = self['lat'][1] - self['lat'][0]
self.attrs['dLon'] = self['lon'][1] - self['lon'][0]
self.attrs['dL'] = self['L'][1] - self['L'][0]
[docs]
def calc_E(self):
'''
Differentiate the equatorial electric potential to arrive at electric field
(stored using keys 'Er', 'Ephi', and 'E').
'''
from spacepy.datamodel import dmarray
from spacepy.pybats.batsmath import d_dx, d_dy
conv = 1.0E6/6371000.0 # Unit conversion: kV -> mV, 1/Re -> 1/m.
x, L = np.meshgrid(np.zeros(self['n'].shape[-1]), np.array(self['L']))
Er = d_dy(self['pot']*conv, self.attrs['dL'])
Ephi = d_dx(self['pot']*conv/L, self.attrs['dLon']*np.pi/180.0)
E = np.sqrt(Er**2 + Ephi**2)
self['E'] = dmarray(E, {'units':'mV/m'})
self['Er'] = dmarray(Er, {'units':'mV/m'})
self['Ephi'] = dmarray(Ephi, {'units':'mV/m'})
[docs]
def add_pcolor(self, var, zlim=None, target=None, loc=111, title=None,
Lmax=None, add_cbar=False, clabel=None, dolog=False,
labelsize=14, **kwargs):
'''
Create a polar pcolor plot of variable *var* and add it to *target*.
Extra keyword arguments are handed to matplotlib's pcolor command.
Parameters
==========
var : string
The variable within the object to plot.
Returns
=======
fig : matplotlib figure object
ax : matplotlib axes object
cont : matplotlib contour object
cbar : matplotlib colorbar object
Other Parameters
================
zlim : two-element list or array
Set the color bar range. Some variables have default ranges.
Others will use max and min of *var*.
Lmax : real
Set the radial extent of the plot.
add_cbar : bool
Set whether to add a color bar or not. Defaults to **False**.
dolog : bool
If **True**, use a log scale to plot *var*.
Defaults to **False**.
labelsize : int
Sets the font size of the labels. Defaults to 14.
title : string
Sets the plot title.
Defaults to 'auto', using the variable label.
clabel : string
Set label for the color bar.
Defaults to *var* and associated units.
target : Figure or Axes
If None (default), a new figure is generated from scratch.
If a matplotlib Figure object, a new axis is created
to fill that figure.
If a matplotlib Axes object, the plot is placed
into that axis.
loc : int
Use to specify the subplot placement of the axis
(e.g. loc=212, etc.) Used if target is a Figure or None.
Default 111 (single plot).
'''
from numpy import linspace, pi
import matplotlib.pyplot as plt
from matplotlib.colors import LogNorm
# Set ax and fig based on given target.
fig, ax = set_target(target, figsize=(10.5, 8), loc=loc, polar=True)
# Get max/min if none given.
if zlim is None:
if var in self._default_zlims:
zlim = self._default_zlims[var]
else:
zlim = [self[var].min(), self[var].max()]
if dolog and zlim[0] <= 0:
zlim[0] = np.min([0.0001, zlim[1]/1000.0])
# Logarithmic scale?
if dolog:
z = np.where(self[var] > zlim[0], self[var], 1.01*zlim[0])
norm = LogNorm()
else:
z = self[var]
norm = None
# Set up proper meshes.
nL, nPhi = len(self['L']), len(self['lon'])
dL, dPhi = self.attrs['dL'], self.attrs['dLon']*pi/180.0
phi = linspace(-1.0*dPhi/2.0, 2.*pi-dPhi/2.0, nPhi+1)-pi/2.
r = linspace(self['L'][0]-dL/2.0, self['L'][-1]+dL/2.0, nL+1)
# Set default color tables based on variable plotted.
if ('cmap' not in kwargs) and var in self._default_cmaps:
kwargs['cmap'] = self._default_cmaps[var]
# -------Plot away------
# Mesh:
pcol = ax.pcolormesh(phi, r, z, vmin=zlim[0], vmax=zlim[1],
norm=norm, **kwargs)
# Add cbar if requested:
if add_cbar:
cbar = plt.colorbar(pcol, pad=0.08, shrink=.8, ax=ax)
if clabel is None:
clabel = "%s ($%s$)" % (var, self[var].attrs['units'])
cbar.set_label(clabel)
else:
cbar = None
# Adjust plot appropriately.
if not Lmax:
# Default to inside ghost cells.
Lmax = self['L'][-3]
if title:
ax.set_title(title+'\n'+self.attrs['time'].isoformat(),
position=(0, 1), ha='left', size=14)
_adjust_dialplot(ax, Lmax, labelsize=labelsize)
return fig, ax, pcol, cbar
[docs]
def add_contour(self, var, zlim=None, target=None, loc=111, title=None,
Lmax=None, add_cbar=False, clabel=None, dolog=False,
filled=True, nLev=31, labelsize=14, **kwargs):
'''
Create a polar contour plot of variable *var* and add it to *target*.
Extra keyword arguments are handed to matplotlib's contourf command.
Parameters
==========
var : string
The variable within the object to plot.
Returns
=======
fig : matplotlib figure object
ax : matplotlib axes object
cont : matplotlib contour object
cbar : matplotlib colorbar object
Other Parameters
================
nLev : int
Sets the number of contour levels. Defaults to 31.
zlim : two-element list or array
Set the color bar range. Some variables have default ranges. Others
will use max and min of *var*.
Lmax : real
Set the radial extent of the plot.
add_cbar : bool
Set whether to add a color bar or not. Defaults to **False**.
dolog : bool
If **True**, use a log scale to plot *var*. Defaults to **False**.
labelsize : int
Sets the font size of the labels. Defaults to 14.
title : string
Sets the plot title. Defaults to 'auto', using the variable label.
clabel : string
Set label for the color bar. Defaults to *var* and associated units.
target : Figure or Axes
If None (default), a new figure is generated from scratch.
If a matplotlib Figure object, a new axis is created
to fill that figure.
If a matplotlib Axes object, the plot is placed
into that axis.
loc : int
Use to specify the subplot placement of the axis
(e.g. loc=212, etc.) Used if target is a Figure or None.
Default 111 (single plot).
'''
import matplotlib.pyplot as plt
from matplotlib.colors import LogNorm
from matplotlib.ticker import LogLocator
# Set ax and fig based on given target.
fig, ax = set_target(target, figsize=(10.5, 8), loc=loc, polar=True)
# Set function based on boolean "filled":
if filled:
contour = ax.contourf
else:
contour = ax.contour
# Get max/min if none given.
if zlim is None:
if var in self._default_zlims:
zlim = self._default_zlims[var]
else:
zlim = [self[var].min(), self[var].max()]
if dolog and zlim[0] <= 0:
zlim[0] = np.min([0.0001, zlim[1]/1000.0])
# Create levels and set norm based on dolog.
if dolog:
levs = np.power(10, np.linspace(np.log10(zlim[0]),
np.log10(zlim[1]), nLev))
z = np.where(self[var] > zlim[0], self[var], 1.01*zlim[0])
z[z > zlim[-1]] = zlim[-1]
norm = LogNorm()
lct = LogLocator()
else:
levs = np.linspace(zlim[0], zlim[1], nLev)
z = self[var]
norm = None
lct = None
# Allow results to cross phi=360.
phi = np.concatenate((self['lon'], [360.0])) * np.pi/180. - np.pi/2.
z = np.concatenate((z, np.array([z[:, 0]]).transpose()), 1)
# Set default color tables based on variable plotted.
if ('cmap' not in kwargs) and ('colors' not in kwargs) and \
(var in self._default_cmaps):
kwargs['cmap'] = self._default_cmaps[var]
# -------Plot away------
# Contour:
cont = contour(phi, self['L'], z, levs, norm=norm, **kwargs)
# Add cbar if requested:
if add_cbar:
cbar = plt.colorbar(cont, pad=0.08, shrink=.8, ticks=lct, ax=ax)
if clabel is None:
clabel = f"{var} (${self[var].attrs['units']}$)"
cbar.set_label(clabel)
else:
cbar = None
# Adjust plot appropriately.
if not Lmax:
# Default to inside ghost cells.
Lmax = self['L'][-3]
if title:
ax.set_title(title+'\n'+self.attrs['time'].isoformat(),
position=(0, 1), ha='left', size=14)
_adjust_dialplot(ax, Lmax, labelsize=labelsize)
return fig, ax, cont, cbar
[docs]
def add_separatrix(self, target=None, loc=111, Lmax=None,
title=None, **kwargs):
'''
Attempts to locate the separatrix (separator between closed and
open drift paths) by finding the minimum velocity in the domain and
tracing the path of constant potential that passes through that point.
The figure, axes, and contour object containing the separatrix line
are returned to the user.
If kwarg **target** is None (default), a new figure is
generated from scratch. If target is a matplotlib Figure
object, a new axis is created to fill that figure at subplot
location **loc**. If **target** is a matplotlib Axes object,
the plot is placed into that axis.
Four values are returned: the matplotlib Figure and Axes objects,
the matplotlib contour object, and the matplotlib colorbar object
(defaults to *False* if not used.)
Kwargs that set line characteristics behave in the typical matplotlib
manner (i.e. "colors" can be set to either a color name or a
hexidecimal specifier.)
=========== ==========================================================
Kwarg Description
----------- ----------------------------------------------------------
target Set plot destination. Defaults to new figure.
loc Set subplot location. Defaults to 111.
linewidths Set width of plotted line. Defaults to 3.0.
colors Set color of line. Defaults to 'orange'.
linestyles Set line style. Defaults to 'dashed'.
----------- ----------------------------------------------------------
=========== ==========================================================
'''
from numpy import pi
# Set default line behavior.
if 'linewidths' not in kwargs:
kwargs['linewidths'] = 3.0
if 'colors' not in kwargs:
kwargs['colors'] = 'orange'
if 'linestyles' not in kwargs:
kwargs['linestyles'] = 'solid'
# Set ax and fig based on given target.
fig, ax = set_target(target, figsize=(10.5, 8), loc=loc, polar=True)
doAdjust = not target == ax
# Find the stagnation point by looking for E = 0.
# Get the potential value at that spot.
# Exclude near-boundary points and limit azimuthal extent.
if 'E' not in self:
self.calc_E()
i = np.arange(self['L'].size)[self['L'] < 9.75]
j = np.arange(self['lon'].size)[(self['lon'] > 250.) &
(self['lon'] < 360.)]
pmin = self['pot'][self['E'] == self['E'][i[0]:i[-1],
j[0]:j[-1]].min()][0]
# Create a contour that only follows that potential curve.
phi = np.concatenate((self['lon'], [360.0])) * pi/180. - pi/2.
z = np.concatenate((self['pot'],
np.array([self['pot'][:, 0]]).transpose()), 1)
cnt = ax.contour(phi, self['L'], z, levels=[pmin], **kwargs)
# Adjust plot appropriately.
if doAdjust:
if not Lmax:
# Default to inside ghost cells.
Lmax = self['L'][-3]
if title:
ax.set_title(title+'\n'+self.attrs['time'].isoformat(),
position=(0, 1), ha='left', size=14)
_adjust_dialplot(ax, Lmax, labelsize=14)
# Return bits to user.
return fig, ax, cnt
[docs]
class MltSlice(PbData):
'''
Open and handle an MLT Slice output file.
'''
[docs]
def __init__(self, filename, *args, **kwargs):
'''
Reads the data; sorts into arrays.
'''
import datetime as dt
from spacepy.datamodel import dmarray
from matplotlib.dates import date2num
super(MltSlice, self).__init__(*args, **kwargs) # Init as PbData.
self.attrs['file'] = filename
f = open(filename, 'r')
# Parse header.
self.attrs['mlt'] = float(f.readline().split()[-1])
line = f.readline().split('=')[-1]
self['L'] = dmarray(np.array(line.split(), dtype=float),
{'units':'$R_E$'})
# Parse remainder of file.
lines = f.readlines()
self['n'] = dmarray(np.zeros([len(lines), len(self['L'])]),
{'units':'cm^{-3}'})
self['time'] = dmarray(np.zeros(len(lines), dtype=object))
for i, l in enumerate(lines):
p = l.split()
self['time'][i] = dt.datetime(int(p[0]), int(p[1]), int(p[2]),
int(p[3]), int(p[4]), int(p[5]),
int(p[6])*1000)
self['n'][i, :] = p[7:]
# Some "hidden" variables for plotting.
self._dtime = date2num(self['time'])
self._dy = self['L'][1] - self['L'][0]
[docs]
def add_lut(self, target=None, loc=111, cmap='Greens_r', zlim=[1, 1000],
add_cbar=True, clabel='Density $cm^{-3}$', xlabel='full',
title=None, grid=True, ntick=5):
'''
Plot log(density) as a contour against L-Shell (y-axis) and
universal time (x-axis) using the PyBats *target* method of other
standard plotting methods. Four items are returned: the Matplotlib
Figure, Axes, Mesh, and ColorBar objects used (if cbar is set to
**False**, the returned ColorBar object is simply set to **False**.)
========== =======================================================
Kwarg Description
---------- -------------------------------------------------------
target Select plot destination. Defaults to new figure/axis.
loc The location of any generated subplots. Default is 111.
add_cbar Toggles the automatic colorbar. Default is**True**.
cmap Selects Matplotlib color table. Defaults to *Greens_r*.
zlim Limits for z-axis. Defaults to [0.1, 1000]
clabel Sets colorbar label. Defaults to units.
xlabel Sets x-axis labels, use 'full', 'ticks', or **None**.
title Sets axis title; defaults to **None**.
grid Show white dotted grid? Defaults to **True**
ntick Number of attempted cbar ticks. Defaults to 5.
========== =======================================================
'''
import matplotlib.pyplot as plt
from matplotlib.colors import LogNorm
# Set ax and fig based on given target.
fig, ax = set_target(target, loc=loc)
# Enforce values to be within limits.
z = np.where(self['n'] > zlim[0], self['n'], 1.01*zlim[0])
z[z > zlim[1]] = zlim[1]
# Create plot:
mesh = ax.pcolormesh(self._dtime, self['L'], z.transpose(),
cmap=plt.get_cmap(cmap), norm=LogNorm(),
vmin=zlim[0], vmax=zlim[-1])
# Use LT ticks and markers on y-axis:
ax.set_ylabel('L-Shell')
# White ticks, slightly thicker:
ax.tick_params(axis='both', which='both', color='w', width=1.2)
# Grid marks:
if grid:
ax.grid(c='w')
if title:
ax.set_title(title)
if xlabel == 'full':
# Both ticks and label.
applySmartTimeTicks(ax, self['time'], dolabel=True)
elif xlabel == 'ticks':
# Ticks, but no date label.
applySmartTimeTicks(ax, self['time'], dolabel=False)
else:
# A blank x-axis is often useful.
applySmartTimeTicks(ax, self['time'], dolabel=False)
ax.set_xticklabels('')
# Add cbar as necessary:
if add_cbar:
cbar = plt.colorbar(mesh, ax=ax, pad=0.01, shrink=0.85)
cbar.set_label(clabel)
else:
cbar = None
return fig, ax, mesh, cbar
[docs]
class Lslice(PbData):
'''
Open an L-Slice output file.
'''
[docs]
def __init__(self, filename, *args, **kwargs):
'''
Reads the data; sorts into arrays.
'''
import datetime as dt
from spacepy.datamodel import dmarray
from matplotlib.dates import date2num
super(Lslice, self).__init__(*args, **kwargs) # Init as PbData.
self.attrs['file'] = filename
f = open(filename, 'r')
# Parse header.
self.attrs['L'] = float(f.readline().split()[-1])
self['mlt'] = dmarray(np.array(f.readline().split()[1:], dtype=float),
{'units':'Hours'})
# Parse remainder of file.
lines = f.readlines()
self['n'] = dmarray(np.zeros([len(lines), len(self['mlt'])]),
{'units':'cm^{-3}'})
self['time'] = dmarray(np.zeros(len(lines), dtype=object))
for i, l in enumerate(lines):
p = l.split()
self['time'][i] = dt.datetime(int(p[0]), int(p[1]), int(p[2]),
int(p[3]), int(p[4]), int(p[5]),
int(p[6])*1000)
self['n'][i, :] = p[7:]
# Some "hidden" variables for plotting.
self._dtime = date2num(self['time'])
self._dy = self['mlt'][1] - self['mlt'][0]
self._y = np.arange(0, self['mlt'][-1]+2*self._dy, self._dy)
[docs]
def add_ltut(self, target=None, loc=111, cmap='Greens_r', zlim=[1, 1000],
add_cbar=True, clabel='Density $cm^{-3}$', xlabel='full',
ylim=[4, 20], title=None, grid=True, ntick=5):
'''
Plot log(density) as a contour against local time (y-axis) and
universal time (x-axis) using the PyBats *target* method of other
standard plotting methods. Four items are returned: the Matplotlib
Figure, Axes, Mesh, and ColorBar objects used (if cbar is set to
**False**, the returned ColorBar object is simply set to **False**.)
========== =======================================================
Kwarg Description
---------- -------------------------------------------------------
target Select plot destination. Defaults to new figure/axis.
loc The location of any generated subplots. Default is 111.
add_cbar Toggles the automatic colorbar. Default is**True**.
cmap Selects Matplotlib color table. Defaults to *Greens_r*.
zlim Limits for z-axis. Defaults to [0.1, 1000]
ylim Sets the MLT range on the y-axis. Defaults to [4,20].
clabel Sets colorbar label. Defaults to units.
xlabel Sets x-axis labels, use 'full', 'ticks', or **None**.
title Sets axis title; defaults to **None**.
grid Show white dotted grid? Defaults to **True**
ntick Number of attempted cbar ticks. Defaults to 5.
========== =======================================================
'''
import matplotlib.pyplot as plt
from matplotlib.colors import LogNorm
# Set ax and fig based on given target.
fig, ax = set_target(target, loc=loc)
# Enforce values to be within limits.
z = np.where(self['n'] > zlim[0], self['n'], 1.01*zlim[0])
z[z > zlim[1]] = zlim[1]
# Create plot:
mesh = ax.pcolormesh(self._dtime, self._y, z.transpose(),
cmap=plt.get_cmap(cmap), norm=LogNorm(),
vmin=zlim[0], vmax=zlim[-1])
# Use LT ticks and markers on y-axis:
ax.set_yticks([6, 12, 18])
ax.set_yticklabels(['Dawn', 'Noon', 'Dusk'])
ax.set_ylim(ylim)
# White ticks, slightly thicker:
ax.tick_params(axis='both', which='both', color='w', width=1.2)
# Grid marks:
if grid:
ax.grid(c='w')
if title:
ax.set_title(title)
if xlabel == 'full':
# Both ticks and label.
applySmartTimeTicks(ax, self['time'], dolabel=True)
elif xlabel == 'ticks':
# Ticks, but no date label.
applySmartTimeTicks(ax, self['time'], dolabel=False)
else:
# A blank x-axis is often useful.
applySmartTimeTicks(ax, self['time'], dolabel=False)
ax.set_xticklabels('')
# Add cbar as necessary:
if add_cbar:
cbar = plt.colorbar(mesh, ax=ax, pad=0.01, shrink=0.85)
cbar.set_label(clabel)
else:
cbar = None
return fig, ax, mesh, cbar