"""
generic
=======
Flexible and general plotting functions
"""
import numpy as np
import pylab as plt
import pynbody
from ..analysis import profile, angmom, halo
from .. import config
from ..array import SimArray
from ..units import NoUnit
def qprof(sim, qty='metals', weights=None, q=(0.16, 0.5, 0.84),
ax=False, ylabel=None, xlog=False, ylog=False, xlabel=None,
facecolor='#BBBBBB', color='#BBBBBB', medcolor='black', filename=False):
if ax:
p = ax
f = plt.gca()
else:
f, p = plt.subplots(1)
qp = pynbody.analysis.profile.QuantileProfile(sim, q=q, weights=weights)
p.fill_between(qp['rbins'].in_units('kpc'), qp[qty][:, 0], y2=qp[qty][:, 2],
facecolor=facecolor, color=color,
where=np.isfinite(qp[qty][:, 0]))
p.plot(qp['rbins'].in_units('kpc'), qp[qty][:, 1], color=medcolor)
if xlabel is None:
p.set_xlabel('r [kpc]')
else:
p.set_xlabel(xlabel)
if ylabel is not None:
p.set_ylabel(ylabel)
if xlog:
p.semilogx()
if ylog:
p.semilogy()
if filename:
f.savefig(filename)
[docs]def hist2d(xo, yo, weights=None, mass=None, gridsize=(100, 100), nbins = None, make_plot = True, **kwargs):
"""
Plot 2D histogram for arbitrary arrays that get passed in.
**Input:**
*x*: array
*y*: array
**Optional keywords:**
*x_range*: list, array, or tuple
size(x_range) must be 2. Specifies the X range.
*y_range*: tuple
size(y_range) must be 2. Specifies the Y range.
*gridsize*: (int, int) (default (100,100))
number of bins to use for the 2D histogram
*nbins*: int
number of bins for the histogram - if specified, gridsize is set to (nbins,nbins)
*nlevels*: int
number of levels to use for the contours
*logscale*: boolean
whether to use log or linear spaced contours
*weights*: numpy array of same length as x and y
if weights is passed, color corresponds to
the mean value of weights in each cell
*mass*: numpy array of masses same length as x andy
must also have weights passed in. If you just
want to weight by mass, pass the masses to weights
*colorbar*: boolean
draw a colorbar
*scalemin*: float
minimum value to use for the color scale
*scalemax*: float
maximum value to use for the color scale
"""
global config
# process keywords
x_range = kwargs.get('x_range', None)
y_range = kwargs.get('y_range', None)
xlogrange = kwargs.get('xlogrange', False)
ylogrange = kwargs.get('ylogrange', False)
ret_im = kwargs.get('ret_im', False)
if y_range is not None:
if len(y_range) != 2:
raise RuntimeError("Range must be a length 2 list or array")
else:
if ylogrange:
y_range = [np.log10(np.min(yo)), np.log10(np.max(yo))]
else:
y_range = [np.min(yo), np.max(yo)]
kwargs['y_range'] = y_range
if x_range is not None:
if len(x_range) != 2:
raise RuntimeError("Range must be a length 2 list or array")
else:
if xlogrange:
x_range = [np.log10(np.min(xo)), np.log10(np.max(xo))]
else:
x_range = [np.min(xo), np.max(xo)]
kwargs['x_range'] = x_range
if (xlogrange):
x = np.log10(xo)
else:
x = xo
if (ylogrange):
y = np.log10(yo)
else:
y = yo
if nbins is not None:
gridsize = (nbins, nbins)
ind = np.where((x > x_range[0]) & (x < x_range[1]) &
(y > y_range[0]) & (y < y_range[1]))
x = x[ind[0]]
y = y[ind[0]]
draw_contours = False
if weights is not None and mass is not None:
draw_contours = True
weights = weights[ind[0]]
mass = mass[ind[0]]
# produce a mass-weighted histogram of average weight values at each
# bin
hist, ys, xs = np.histogram2d(
y, x, weights=weights * mass, bins=gridsize, range=[y_range, x_range])
hist_mass, ys, xs = np.histogram2d(
y, x, weights=mass, bins=gridsize, range=[y_range, x_range])
good = np.where(hist_mass > 0)
hist[good] = hist[good] / hist_mass[good]
else:
if weights is not None:
# produce a weighted histogram
weights = weights[ind[0]]
elif mass is not None:
# produce a mass histogram
weights = mass[ind[0]]
hist, ys, xs = np.histogram2d(
y, x, weights=weights, bins=gridsize, range=[y_range, x_range])
try:
hist = SimArray(hist, weights.units)
except AttributeError:
hist = SimArray(hist)
try:
xs = SimArray(.5 * (xs[:-1] + xs[1:]), x.units)
ys = SimArray(.5 * (ys[:-1] + ys[1:]), y.units)
except AttributeError:
xs = .5 * (xs[:-1] + xs[1:])
ys = .5 * (ys[:-1] + ys[1:])
if ret_im:
return make_contour_plot(hist, xs, ys, **kwargs)
if make_plot:
make_contour_plot(hist, xs, ys, **kwargs)
if draw_contours:
make_contour_plot(SimArray(density_mass, mass.units), xs, ys, filled=False,
clear=False, colorbar=False, colors='black', scalemin=nmin, nlevels=10)
return hist, xs, ys
[docs]def gauss_kde(xo, yo, weights=None, mass=None, gridsize=(100, 100), nbins = None,
make_plot = True, nmin = None, nmax = None, **kwargs):
"""
Plot 2D gaussian kernel density estimate (KDE) given values at points (*x*, *y*).
Behavior changes depending on which keywords are passed:
If a *weights* array is supplied, produce a weighted KDE.
If a *mass* array is supplied, a mass density is computed.
If both *weights* and *mass* are supplied, a mass-averaged KDE of the weights is
computed.
By default, norm=False is passed to :func:`~pynbody.plot.util.fast_kde` meaning
that the result returned *is not* normalized such that the integral over the area
equals one.
Since this function produces a density estimate, the units of the
output grid are different than for the output of
:func:`~pynbody.plot.generic.hist2d`. To get to the same units,
you must multiply by the size of the cells.
**Input:**
*xo*: array
*yo*: array
**Optional keywords:**
*mass*: numpy array of same length as x and y
particle masses to be used for weighting
*weights*: numpy array of same length as x and y
if weights is passed, color corresponds to
the mean value of weights in each cell
*nmin*: float (default None)
if *weights* and *mass* are both specified, the mass-weighted
contours are only drawn where the mass exceeds *nmin*.
*gridsize*: (int, int) (default: 100,100)
size of grid for computing the density estimate
*nbins*: int
number of bins for the histogram - if specified, gridsize is set to (nbins,nbins)
*make_plot*: boolean (default: True)
whether or not to produce a plot
**Keywords passed to** :func:`~pynbody.plot.util.fast_kde`:
*norm*: boolean (default: False)
If False, the output is only corrected for the kernel. If True,
the result is normalized such that the integral over the area
yields 1.
*nocorrelation*: (default: False) If True, the correlation
between the x and y coords will be ignored when preforming
the KDE.
**Keywords passed to** :func:`~pynbody.plot.generic.make_contour_plot`:
*x_range*: list, array, or tuple
size(x_range) must be 2. Specifies the X range.
*y_range*: tuple
size(y_range) must be 2. Specifies the Y range.
*nlevels*: int
number of levels to use for the contours
*logscale*: boolean
whether to use log or linear spaced contours
*colorbar*: boolean
draw a colorbar
*scalemin*: float
minimum value to use for the color scale
*scalemax*: float
maximum value to use for the color scale
"""
from .util import fast_kde
from scipy.stats.kde import gaussian_kde
global config
# process keywords
x_range = kwargs.get('x_range', None)
y_range = kwargs.get('y_range', None)
xlogrange = kwargs.get('xlogrange', False)
ylogrange = kwargs.get('ylogrange', False)
ret_im = kwargs.get('ret_im', False)
if y_range is not None:
if len(y_range) != 2:
raise RuntimeError("Range must be a length 2 list or array")
else:
if ylogrange:
y_range = [np.log10(np.min(yo)), np.log10(np.max(yo))]
else:
y_range = [np.min(yo), np.max(yo)]
if x_range is not None:
if len(x_range) != 2:
raise RuntimeError("Range must be a length 2 list or array")
else:
if xlogrange:
x_range = [np.log10(np.min(xo)), np.log10(np.max(xo))]
else:
x_range = [np.min(xo), np.max(xo)]
if (xlogrange):
x = np.log10(xo)
else:
x = xo
if (ylogrange):
y = np.log10(yo)
else:
y = yo
if nbins is not None:
gridsize = (nbins, nbins)
ind = np.where((x > x_range[0]) & (x < x_range[1]) &
(y > y_range[0]) & (y < y_range[1]))
x = x[ind[0]]
y = y[ind[0]]
try:
xs = SimArray(
np.linspace(x_range[0], x_range[1], gridsize[0] + 1), x.units)
except AttributeError:
xs = np.linspace(x_range[0], x_range[1], gridsize[0] + 1)
xs = .5 * (xs[:-1] + xs[1:])
try:
ys = SimArray(
np.linspace(y_range[0], y_range[1], gridsize[1] + 1), y.units)
except AttributeError:
ys = np.linspace(y_range[0], y_range[1], gridsize[1] + 1)
ys = .5 * (ys[:-1] + ys[1:])
extents = [x_range[0], x_range[1], y_range[0], y_range[1]]
draw_contours = False
if weights is not None and mass is not None:
draw_contours = True
weights = weights[ind[0]]
mass = mass[ind[0]]
# produce a mass-weighted gaussian KDE of average weight values at each
# bin
density = fast_kde(
x, y, weights=weights * mass, gridsize=gridsize, extents=extents, **kwargs)
density_mass = fast_kde(
x, y, weights=mass, gridsize=gridsize, extents=extents, **kwargs)
good = np.where(density_mass > 0)
density[good] = density[good] / density_mass[good]
if nmin is not None:
density *= density_mass > nmin
else:
# produce a weighted gaussian KDE
if weights is not None:
weights = weights[ind[0]]
elif mass is not None:
weights = mass[ind[0]]
density = fast_kde(x, y, weights=weights, gridsize=gridsize,
extents=extents, **kwargs)
try:
density = SimArray(density, weights.units)
except AttributeError:
density = SimArray(density)
if ret_im:
return make_contour_plot(density, xs, ys, **kwargs)
if make_plot:
make_contour_plot(density, xs, ys, **kwargs)
if draw_contours:
make_contour_plot(SimArray(density_mass, mass.units), xs, ys, filled=False,
clear=False, colorbar=False, colors='black', scalemin=nmin, nlevels=10)
return density, xs, ys
[docs]def make_contour_plot(arr, xs, ys, x_range=None, y_range=None, nlevels=20,
logscale=True, xlogrange=False, ylogrange=False,
subplot=False, colorbar=False, ret_im=False, cmap=None,
clear=True, legend=False, scalemin=None,levels=None,
scalemax=None, filename=None, **kwargs):
"""
Plot a contour plot of grid *arr* corresponding to bin centers
specified by *xs* and *ys*. Labels the axes and colobar with
proper units taken from x
Called by :func:`~pynbody.plot.generic.hist2d` and
:func:`~pynbody.plot.generic.gauss_density`.
**Input**:
*arr*: 2D array to plot
*xs*: x-coordinates of bins
*ys*: y-coordinates of bins
**Optional Keywords**:
*x_range*: list, array, or tuple (default = None)
size(x_range) must be 2. Specifies the X range.
*y_range*: tuple (default = None)
size(y_range) must be 2. Specifies the Y range.
*xlogrange*: boolean (default = False)
whether the x-axis should have a log scale
*ylogrange*: boolean (default = False)
whether the y-axis should have a log scale
*nlevels*: int (default = 20)
number of levels to use for the contours
*logscale*: boolean (default = True)
whether to use log or linear spaced contours
*colorbar*: boolean (default = False)
draw a colorbar
*scalemin*: float (default = arr.min())
minimum value to use for the color scale
*scalemax*: float (default = arr.max())
maximum value to use for the color scale
"""
from matplotlib import ticker, colors
if not subplot:
import matplotlib.pyplot as plt
else:
plt = subplot
if scalemin is None:
scalemin = np.min(arr[arr > 0])
if scalemax is None:
scalemax = np.max(arr[arr > 0])
arr[arr < scalemin] = scalemin
arr[arr > scalemax] = scalemax
if 'norm' in kwargs:
del(kwargs['norm'])
if logscale:
try:
levels = np.logspace(np.log10(scalemin),
np.log10(scalemax), nlevels)
cont_color = colors.LogNorm()
except ValueError:
raise ValueError(
'crazy contour levels -- try specifying the *levels* keyword or use a linear scale')
return
if arr.units != NoUnit() and arr.units != 1:
cb_label = '$log_{10}(' + arr.units.latex() + ')$'
else:
cb_label = '$log_{10}(N)$'
else:
levels = np.linspace(scalemin,
scalemax, nlevels)
cont_color = None
if arr.units != NoUnit() and arr.units != 1:
cb_label = '$' + arr.units.latex() + '$'
else:
cb_label = '$N$'
if not subplot and clear:
plt.clf()
if ret_im:
if logscale:
arr = np.log10(arr)
scalemin, scalemax = np.log10((scalemin, scalemax))
return plt.imshow(arr, origin='down', vmin=scalemin, vmax=scalemax,
aspect='auto', cmap=cmap, axes=subplot,
# aspect =
# np.diff(x_range)/np.diff(y_range),cmap=cmap,
extent=[x_range[0], x_range[1], y_range[0], y_range[1]])
cs = plt.contourf(
xs, ys, arr, levels, norm=cont_color, cmap=cmap, **kwargs)
if 'xlabel' in kwargs:
xlabel = kwargs['xlabel']
else:
try:
if xlogrange:
xlabel = r'' + '$log_{10}(' + xs.units.latex() + ')$'
else:
xlabel = r'' + '$x/' + xs.units.latex() + '$'
except AttributeError:
xlabel = None
if xlabel:
try:
if subplot:
plt.set_xlabel(xlabel)
else:
plt.xlabel(xlabel)
except:
pass
if 'ylabel' in kwargs:
ylabel = kwargs['ylabel']
else:
try:
if ylogrange:
ylabel = '$log_{10}(' + ys.units.latex() + ')$'
else:
ylabel = r'' + '$y/' + ys.units.latex() + '$'
except AttributeError:
ylabel = None
if ylabel:
try:
if subplot:
plt.set_ylabel(ylabel)
else:
plt.ylabel(ylabel)
except:
pass
# if not subplot:
# plt.xlim((x_range[0],x_range[1]))
# plt.ylim((y_range[0],y_range[1]))
if colorbar:
cb = plt.colorbar(cs, format="%.2e").set_label(r'' + cb_label)
if legend:
plt.legend(loc=2)
if (filename):
if config['verbose']:
print("Saving " + filename)
plt.savefig(filename)
[docs]def fourier_map(sim, nbins=100, nmin=1000, nphi=100, mmin=1, mmax=7, rmax=10,
levels=[.01, .05, .1, .2], subplot=None, ret=False, **kwargs):
"""
Plot an overdensity map generated from a Fourier expansion of the
particle distribution. A :func:`~pynbody.analysis.profile.Profile`
is made and passed to :func:`~pynbody.plot.util.inv_fourier` to
obtain an overdensity map. The map is plotted using the usual
matplotlib.contour.
**Input**:
*sim* : a :func:`~pynbody.snapshot.SimSnap` object
**Optional Keywords**:
*nbins* (100) : number of radial bins to use for the profile
*nmin* (1000) : minimum number of particles required per bin
*nphi* (100) : number of azimuthal bins to use for the map
*mmin* (1) : lowest multiplicity Fourier component
*mmax* (7) : highest multiplicity Fourier component
*rmax* (10) : maximum radius to use when generating the profile
*levels* [0.01,0.05,0.1,0.2] : tuple of levels for plotting contours
*subplot* (None) : Axes object on which to plot the contours
"""
from . import util
if subplot is None:
import matplotlib.pylab as plt
else:
plt = subplot
p = pynbody.analysis.profile.Profile(sim, max=rmax, nbins=nbins)
phi, phi_inv = util.inv_fourier(p, nmin, mmin, mmax, nphi)
rr, pp = np.meshgrid(p['rbins'], phi)
xx = (rr * np.cos(pp)).T
yy = (rr * np.sin(pp)).T
plt.contour(xx, yy, phi_inv, levels, **kwargs)
if ret:
return xx, yy, phi_inv
[docs]def prob_plot(x, y, weight, nbins=(100, 100), extent=None, axes=None, **kwargs):
"""
Make a plot of the probability of y given x, i.e. p(y|x). The
values are normalized such that the integral along each column is
one.
**Input**:
*x*: primary binning axis
*y*: secondary binning axis
*weight*: weights array
*nbins*: tuple of length 2 specifying the number of bins in each direction
*extent*: tuple of length 4 speciphysical extent of the axes
(xmin,xmax,ymin,ymax)
**Optional Keywords**:
all optional keywords are passed on to the imshow() command
"""
import matplotlib.pylab as plt
assert(len(nbins) == 2)
grid = np.zeros(nbins)
if extent is None:
extent = (min(x), max(x), min(y), max(y))
xbinedges = np.linspace(extent[0], extent[1], nbins[0] + 1)
ybinedges = np.linspace(extent[2], extent[3], nbins[1] + 1)
for i in range(nbins[0]):
ind = np.where((x > xbinedges[i]) & (x < xbinedges[i + 1]))[0]
h, bins = np.histogram(
y[ind], weights=weight[ind], bins=ybinedges, density=True)
grid[:, i] = h
if axes is None:
im = plt.imshow(grid, extent=extent, origin='lower', **kwargs)
else:
im = axes.imshow(grid, extent=extent, origin='lower', **kwargs)
cb = plt.colorbar(im, format='%.2f')
cb.set_label(r'$P(y|x)$')
return grid, xbinedges, ybinedges