######################################################################
# FIlamEntary STructure Analysis (FIESTA) -- Pre-release
# Authors: Jiten Dhandha, Zoe Faes and Rowan Smith
# Last edited: September 2022
######################################################################
"""
This module contains useful plotting routines for statistical analysis.
"""
######################################################################
# LIBRARIES #
######################################################################
#Standard libs
#Numpy
import numpy as np
#Scipy
from scipy.interpolate import PchipInterpolator
#Astropy
from astropy import units as u
#Matplotlib
import matplotlib as mpl
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
#Fiesta
from fiesta import units as ufi
from fiesta import arepo
from fiesta import _utils as utils
######################################################################
# plot_ndensity_PDFs #
######################################################################
[docs]def plot_ndensity_PDFs(avgs,
ndensity_unit=u.cm**-3,
bins=None,
colors=None,
linestyles=None,
linewidths=None,
labels=None,
cumulative=False,
fit_spline=False,
only_spline=False,
save=None,
**kwargs):
"""
Plot the mass-weighted logarithmic probability distribution function (PDF)
or cumulative distribution function (CDF) histograms of the number density
of AREPO Voronoi grids.
Parameters
----------
avgs : `list` of `~fiesta.arepo.ArepoVoronoiGrid`'s
The list of AREPO Voronoi grid instances to plot the PDFs/CDFs of.
ndensity_unit : `~astropy.units.Unit`, optional
The unit of number density to use. Default value is ``u.cm**-3``.
bins: `list`, optional
The number of bins corresponding to each PDF/CDF. If ``None``
(default), they are set to ``25``.
colors: `list`, optional
A list of *matplotlib*-compatible color strings corresponding
to each line plotted. If ``None`` (default), colors are picked
by cycling through *matplotlib* tab10 colour palette.
linestyles : `list`, optional
A list of *matplotlib*-compatible linestyle strings corresponding
to each line plotted. If ``None`` (default), they are set to ``-``.
linewidths : `list`, optional
A list of linewidths corresponding to each line plotted.
If ``None`` (default), they are set to ``1.5``.
labels : `list`, optional
A list of labels corresponding to each line plotted.
If ``None`` (default), they are numbered ``1, ..., n``.
cumulative : `bool`, optional
If ``True``, plots the CDF, else plots the PDF.
Default value is ``False``.
fit_spline : `bool`, optional
If ``True``, fits a spline interpolator through each PDF/CDF
histogram. Default value is ``False``.
only_spline : `bool`, optional
If ``True``, plots only the spline interpolator and not the
PDF/CDF histograms. Default value is ``False``.
save : `str`, optional
File path to save the plot.
If ``None`` (default), plot is not saved.
**kwargs : `dict`, optional
Additional *matplotlib*-based keyword arguments to control
finer details of the plot.
Returns
-------
fig : `~matplotlib.figure.Figure`
Main `~matplotlib.figure.Figure` instance.
"""
#Check unit
utils.check_unit(ndensity_unit, u.cm**-3)
#Figure properties
nsols = len(avgs)
if colors is None:
cmap = plt.cm.tab10
colors = cmap(np.arange(nsols)%cmap.N)
if linestyles is None:
linestyles = ["-"]*nsols
if linewidths is None:
linewidths = [1.5]*nsols
if labels is None:
labels = [fr"${i}$" for i in np.arange(1,nsols+1)]
if bins is None:
bins = [25]*nsols
#Main figure
fig = plt.figure(figsize=(8,8))
if "figure" in kwargs:
plt.setp(fig,**kwargs["figure"])
ax = fig.add_subplot(111)
#Grid
if "grid" in kwargs:
ax.grid(**kwargs["grid"])
#Axes scales
ax.set_yscale('log')
if "xscale" in kwargs:
ax.set_xscale(**kwargs["xscale"])
if "yscale" in kwargs:
ax.set_yscale(**kwargs["yscale"])
#Axes ticks
ax.xaxis.set_tick_params(which='major', width=1, length=5, labelsize=15)
ax.xaxis.set_tick_params(which='minor', width=1, length=2.5, labelsize=10)
ax.yaxis.set_tick_params(which='major', width=1, length=5, labelsize=15)
ax.yaxis.set_tick_params(which='minor', width=1, length=2.5, labelsize=10)
if "xtick_params" in kwargs:
ax.xaxis.set_tick_params(**kwargs["xtick_params"])
if "ytick_params" in kwargs:
ax.yaxis.set_tick_params(**kwargs["ytick_params"])
if "tick_params" in kwargs:
ax.tick_params(**kwargs["tick_params"])
#Axes labels
ax.set_xlabel(r"Number density log($n$) [{}]".format(ndensity_unit.to_string()),fontsize=15)
if(cumulative):
ax.set_ylabel(r"Cumulative probability density",fontsize=15)
else:
ax.set_ylabel(r"Probability density",fontsize=15)
if "xlabel" in kwargs:
ax.set_xlabel(**kwargs["xlabel"])
if "ylabel" in kwargs:
ax.set_ylabel(**kwargs["ylabel"])
#Figure title
ax.set_title("",fontsize=15)
if "title" in kwargs:
ax.set_title(**kwargs["title"])
############### Plotting start ################
bars = []
lines = []
for avg, bin, c, ls, lw, la in zip(avgs, bins, colors, linestyles, linewidths, labels):
ndens = avg.get_ndensity().to_value(ndensity_unit)
mass = avg.mass[avg.gas_ids].to_value(u.g)
log_ndens = np.log10(ndens, out=np.zeros_like(ndens), where=(ndens>0))
hist, bin_edges = np.histogram(log_ndens, bins=bin, weights=mass, density=True)
#mass-weighted PDF of the density distribution (see Burkhart, 2018)
#rho_avg = np.average(AVG.rho,weights=AVG.mass[AVG.gas_ids])
#s = np.log10(AVG.rho/rho_avg, out=np.zeros_like(AVG.rho), where=(AVG.rho>0))
#hist, bin_edges = np.histogram(s, bins=25, weights=AVG.mass[AVG.gas_ids], density=True)
x = bin_edges[:-1]
y = hist
if(cumulative):
y = np.cumsum(y*np.diff(bin_edges))
#Plotting histogram of PDF
if(not only_spline):
width = (max(log_ndens)-min(log_ndens))/len(x)
bar = ax.bar(x, y, width=width, color=c, linestyle=ls, linewidth=lw, label=la, alpha=0.1)
bars.append(bar)
#Fitting cubic spline to the whole PDF
if(fit_spline):
spline = PchipInterpolator(x,y)
xval = np.linspace(min(x),max(x),1000)
yval = spline(xval)
line, = ax.plot(xval, yval, color=c, linestyle=ls, linewidth=lw, label=la)
lines.append(line)
ax.axvline(x=2.0,color='k',linestyle='--')
############### Plotting end ################
#Axes limits
ax.set_xlim(-2,6)
ax.set_ylim(1e-3,1)
if "xlim" in kwargs:
ax.set_xlim(**kwargs["xlim"])
if "ylim" in kwargs:
ax.set_ylim(**kwargs["ylim"])
#Text
if "text" in kwargs:
ax.text(**kwargs["text"],transform=ax.transAxes)
#Figure legend
ax.legend(fontsize=15)
if "legend" in kwargs:
ax.legend(**kwargs["legend"])
if save is not None:
fig.savefig(save, bbox_inches='tight', dpi=100)
return fig
######################################################################
# plot_sink_mass_evolutions #
######################################################################
[docs]def plot_sink_mass_evolutions(base_file_paths,
min_nums,
max_nums,
time_unit=u.s,
mass_unit=u.g,
colors=None,
linestyles=None,
linewidths=None,
labels=None,
save=None,
**kwargs):
"""
Plot the total sink mass of AREPO Voronoi grids as a function of time.
Parameters
----------
base_file_paths : `list` of `str`'s
The list of base file path strings to read :class:`~fiesta.arepo.ArepoVoronoiGrid`
from. The format of the AREPO snapshots is usually ``<base_file_path><num>`` where
``<num>`` is an positive integer corresponding to the snapshot number.
min_nums: `list`
The list of starting value of ``<num>`` for each AREPO simulation.
max_nums : `list`
The list of ending value of ``<num>`` for each AREPO simulation.
time_unit : `~astropy.units.Unit`, optional
The unit of time to use. Default value is ``u.s``.
mass_unit : `~astropy.units.Unit`, optional
The unit of mass to use. Default value is ``u.g``.
colors: `list`, optional
A list of *matplotlib*-compatible color strings corresponding
to each line plotted. If ``None`` (default), colors are picked
by cycling through *matplotlib* tab10 colour palette.
linestyles : `list`, optional
A list of *matplotlib*-compatible linestyle strings corresponding
to each line plotted. If ``None`` (default), they are set to ``-``.
linewidths : `list`, optional
A list of linewidths corresponding to each line plotted.
If ``None`` (default), they are set to ``1.5``.
labels : `list`, optional
A list of labels corresponding to each line plotted.
If ``None`` (default), they are numbered ``1, ..., n``.
save : `str`, optional
File path to save the plot.
If ``None`` (default), plot is not saved.
**kwargs : `dict`, optional
Additional *matplotlib*-based keyword arguments to control
finer details of the plot.
Returns
-------
fig : `~matplotlib.figure.Figure`
Main `~matplotlib.figure.Figure` instance.
"""
#Check unit
utils.check_unit(time_unit, u.s)
utils.check_unit(mass_unit, u.g)
#Figure properties
nsols = len(base_file_paths)
if colors is None:
cmap = plt.cm.tab10
colors = cmap(np.arange(nsols)%cmap.N)
if linestyles is None:
linestyles = ["-"]*nsols
if linewidths is None:
linewidths = [1.5]*nsols
if labels is None:
labels = [fr"${i}$" for i in np.arange(1,nsols+1)]
#Main figure
fig = plt.figure(figsize=(8,8))
if "figure" in kwargs:
plt.setp(fig,**kwargs["figure"])
ax = fig.add_subplot(111)
#Grid
if "grid" in kwargs:
ax.grid(**kwargs["grid"])
#Axes scales
if "xscale" in kwargs:
ax.set_xscale(**kwargs["xscale"])
if "yscale" in kwargs:
ax.set_yscale(**kwargs["yscale"])
#Axes ticks
ax.xaxis.set_tick_params(which='major', width=1, length=5, labelsize=15)
ax.xaxis.set_tick_params(which='minor', width=1, length=2.5, labelsize=10)
ax.yaxis.set_tick_params(which='major', width=1, length=5, labelsize=15)
ax.yaxis.set_tick_params(which='minor', width=1, length=2.5, labelsize=10)
if "xtick_params" in kwargs:
ax.xaxis.set_tick_params(**kwargs["xtick_params"])
if "ytick_params" in kwargs:
ax.yaxis.set_tick_params(**kwargs["ytick_params"])
if "tick_params" in kwargs:
ax.tick_params(**kwargs["tick_params"])
#Axes labels
ax.set_xlabel(r"Time [{}] ".format(time_unit.to_string()),fontsize=15)
ax.set_ylabel(r"Total sink mass [{}] ".format(mass_unit.to_string()),fontsize=15)
if "xlabel" in kwargs:
ax.set_xlabel(**kwargs["xlabel"])
if "ylabel" in kwargs:
ax.set_ylabel(**kwargs["ylabel"])
#Figure title
ax.set_title("",fontsize=15)
if "title" in kwargs:
ax.set_title(**kwargs["title"])
############### Plotting start ################
lines = []
for bfp, minn, maxn, c, ls, lw, la in zip(base_file_paths, min_nums, max_nums, colors, linestyles, linewidths, labels):
nums = ["{0:03}".format(i) for i in range(minn,maxn)]
counter = 0
time_arr = []
totsinkmass_arr = []
for num in nums:
file_path = bfp + num
avg = arepo.ArepoVoronoiGrid(file_path)
time = avg.time.to_value(time_unit)
time_arr.append(time)
totsinkmass = np.sum(avg.mass[avg.sink_ids].to_value(mass_unit))
totsinkmass_arr.append(totsinkmass)
#ax.scatter(time_array,totsinkmass_array,color=color,s=3,zorder=1)
line, = ax.plot(time_arr, totsinkmass_arr, color=c, linestyle=ls, linewidth=lw, label=la)
lines.append(line)
############### Plotting end ################
#Axes limits
if "xlim" in kwargs:
ax.set_xlim(**kwargs["xlim"])
if "ylim" in kwargs:
ax.set_ylim(**kwargs["ylim"])
#Text
if "text" in kwargs:
ax.text(**kwargs["text"],transform=ax.transAxes)
#Figure legend
if "legend" in kwargs:
ax.legend(**kwargs["legend"])
else:
box = ax.get_position()
ax.set_position([box.x0, box.y0, box.width, box.height * 0.9])
ax.legend(fontsize=15, loc='upper center', bbox_to_anchor=(0.5, 1.18))
if save is not None:
fig.savefig(save, bbox_inches='tight', dpi=100)
return fig
######################################################################
# plot_log_histogram #
######################################################################
[docs]def plot_log_histogram(arrays,
bins='auto',
colors=None,
linestyles=None,
linewidths=None,
labels=None,
save=None,
**kwargs):
"""
A general function to plot histograms of data on a log scale.
Parameters
----------
arrays : `list` of `list`'s
The list of data arrays to generate histograms of.
bins: `int` or `sequence` or `str`
See `~matplotlib.axes.Axes.hist` documentation for detail.
Default value is ``auto``.
colors: `list`, optional
A list of *matplotlib*-compatible color strings corresponding
to each line plotted. If ``None`` (default), colors are picked
by cycling through *matplotlib* tab10 colour palette.
linestyles : `list`, optional
A list of *matplotlib*-compatible linestyle strings corresponding
to each line plotted. If ``None`` (default), they are set to ``-``.
linewidths : `list`, optional
A list of linewidths corresponding to each line plotted.
If ``None`` (default), they are set to ``1.5``.
labels : `list`, optional
A list of labels corresponding to each histogram plotted.
If ``None`` (default), they are numbered ``1, ..., n``.
save : `str`, optional
File path to save the plot.
If ``None`` (default), plot is not saved.
**kwargs : `dict`, optional
Additional *matplotlib*-based keyword arguments to control
finer details of the plot.
Returns
-------
fig : `~matplotlib.figure.Figure`
Main `~matplotlib.figure.Figure` instance.
"""
#Figure properties
nsols = len(arrays)
if colors is None:
cmap = plt.cm.tab10
colors = cmap(np.arange(nsols)%cmap.N)
if linestyles is None:
linestyles = ["-"]*nsols
if linewidths is None:
linewidths = [1.5]*nsols
if labels is None:
labels = [fr"${i}$" for i in np.arange(1,nsols+1)]
#Figure properties
nsols = len(arrays)
if colors is None:
cmap = plt.cm.tab10
colors = cmap(np.arange(nsols)%cmap.N)
if linestyles is None:
linestyles = ["-"]*nsols
if linewidths is None:
linewidths = [1.5]*nsols
if labels is None:
labels = [fr"${i}$" for i in np.arange(1,nsols+1)]
#Main figure
fig = plt.figure(figsize=(8,8))
if "figure" in kwargs:
plt.setp(fig,**kwargs["figure"])
ax = fig.add_subplot(111)
#Grid
if "grid" in kwargs:
ax.grid(**kwargs["grid"])
#Axes scales
ax.set_xscale('log')
ax.set_yscale('log')
if "xscale" in kwargs:
ax.set_xscale(**kwargs["xscale"])
if "yscale" in kwargs:
ax.set_yscale(**kwargs["yscale"])
#Axes ticks
ax.xaxis.set_tick_params(which='major', width=1, length=5, labelsize=15)
ax.xaxis.set_tick_params(which='minor', width=1, length=2.5, labelsize=10)
ax.yaxis.set_tick_params(which='major', width=1, length=5, labelsize=15)
ax.yaxis.set_tick_params(which='minor', width=1, length=2.5, labelsize=10)
if "xtick_params" in kwargs:
ax.xaxis.set_tick_params(**kwargs["xtick_params"])
if "ytick_params" in kwargs:
ax.yaxis.set_tick_params(**kwargs["ytick_params"])
if "tick_params" in kwargs:
ax.tick_params(**kwargs["tick_params"])
#Axes labels
ax.set_xlabel(r"$x$",fontsize=15)
ax.set_ylabel(r"$y$",fontsize=15)
if "xlabel" in kwargs:
ax.set_xlabel(**kwargs["xlabel"])
if "ylabel" in kwargs:
ax.set_ylabel(**kwargs["ylabel"])
#Figure title
ax.set_title("",fontsize=15)
if "title" in kwargs:
ax.set_title(**kwargs["title"])
############### Plotting start ################
hists = []
arrays_flattened = np.array([item for sublist in arrays for item in sublist])
bins=np.logspace(np.log10(arrays_flattened.min()), np.log10(arrays_flattened.max()), bins)
for arr, c, ls, lw, la in zip(arrays, colors, linestyles, linewidths, labels):
hist = ax.hist(arr, bins=bins, edgecolor=c, label=la, linestyle = ls, linewidth = lw,
histtype='step', stacked=True, density=True)
hists.append(hist)
############### Plotting end ################
#Axes limits
ax.set_xlim(1e-2,1e+2)
ax.set_ylim(1e-3,1)
if "xlim" in kwargs:
ax.set_xlim(**kwargs["xlim"])
if "ylim" in kwargs:
ax.set_ylim(**kwargs["ylim"])
#Text
if "text" in kwargs:
ax.text(**kwargs["text"],transform=ax.transAxes)
#Figure legend
ax.legend(fontsize=15)
if "legend" in kwargs:
ax.legend(**kwargs["legend"])
if save is not None:
fig.savefig(save, bbox_inches='tight', dpi=100)
return fig
######################################################################
# plot_boxplot #
######################################################################
[docs]def plot_boxplot(arrays,
write_mean=True,
write_median=True,
write_minmax=True,
write_total=True,
colors=None,
linestyles=None,
linewidths=None,
labels=None,
save=None,
**kwargs):
"""
A general function to generate boxplot (or box and whiskers plot) of data.
Parameters
----------
arrays : `list` of `list`'s
The list of data arrays to generate boxplot of.
write_mean: bool
If ``True`` (default), the mean value is mentioned below each mean line.
write_median: `bool`
If ``True`` (default), the median value is mentioned below each median line.
write_minmax : `bool`
If ``True`` (default), the minimum and maximum values are mentioned below
the end whiskers.
write_total : `bool`
If ``True`` (default), the length of data arrays is mentioned at the right side
of the plot, outside the axes.
colors: `list`, optional
A list of *matplotlib*-compatible color strings corresponding
to each line plotted. If ``None`` (default), colors are picked
by cycling through *matplotlib* tab10 colour palette.
linestyles : `list`, optional
A list of *matplotlib*-compatible linestyle strings corresponding
to each line plotted. If ``None`` (default), they are set to ``-``.
linewidths : `list`, optional
A list of linewidths corresponding to each line plotted.
If ``None`` (default), they are set to ``1.5``.
labels : list, optional
A list of labels corresponding to each box plotted.
If ``None`` (default), they are numbered ``1, ..., n``.
save : str, optional
The name of the file to save the plot as. If ``None`` (default), plot is
not saved.
**kwargs : dict, optional
Additional *matplotlib*-based keyword arguments to control
finer details of the plot.
Returns
-------
fig : `~matplotlib.figure.Figure`
Main `~matplotlib.figure.Figure` instance.
"""
#Figure properties
nsols = len(arrays)
if colors is None:
cmap = plt.cm.tab10
colors = cmap(np.arange(nsols)%cmap.N)
if linestyles is None:
linestyles = ["-"]*nsols
if linewidths is None:
linewidths = [1.5]*nsols
if labels is None:
labels = [fr"${i}$" for i in np.arange(1,nsols+1)]
#Main figure
fig = plt.figure(figsize=(8,8))
if "figure" in kwargs:
plt.setp(fig,**kwargs["figure"])
ax = fig.add_subplot(111)
#Grid
if "grid" in kwargs:
ax.grid(**kwargs["grid"])
#Axes scales
ax.set_xscale('log')
if "xscale" in kwargs:
ax.set_xscale(**kwargs["xscale"])
if "yscale" in kwargs:
ax.set_yscale(**kwargs["yscale"])
#Axes ticks
ax.xaxis.set_tick_params(which='major', width=1, length=5, labelsize=15)
ax.xaxis.set_tick_params(which='minor', width=1, length=2.5, labelsize=10)
ax.yaxis.set_tick_params(which='major', width=1, length=5, labelsize=15)
ax.yaxis.set_tick_params(which='minor', width=1, length=2.5, labelsize=10)
if "xtick_params" in kwargs:
ax.xaxis.set_tick_params(**kwargs["xtick_params"])
if "ytick_params" in kwargs:
ax.yaxis.set_tick_params(**kwargs["ytick_params"])
if "tick_params" in kwargs:
ax.tick_params(**kwargs["tick_params"])
#Axes labels
ax.set_xlabel(r"$x$",fontsize=15)
ax.set_ylabel(r"$y$",fontsize=15)
if "xlabel" in kwargs:
ax.set_xlabel(**kwargs["xlabel"])
if "ylabel" in kwargs:
ax.set_ylabel(**kwargs["ylabel"])
#Figure title
ax.set_title("",fontsize=15)
if "title" in kwargs:
ax.set_title(**kwargs["title"])
############### Plotting start ################
#Reversing arrays so they appear in order top to bottom
arrays=arrays[::-1]
colors=colors[::-1]
linestyles=linestyles[::-1]
linewidths=linewidths[::-1]
labels=labels[::-1]
#Note: setting whiskers to very high value to force min/max to be within whiskers range
boxplot = ax.boxplot(arrays,
patch_artist = True,
showfliers=True,
showmeans = True, meanline = True,
whis=1e18,
labels = labels,
vert = False)
for patch, c, ls, lw in zip(boxplot['boxes'], colors, linestyles, linewidths):
patch.set_facecolor(c)
patch.set_alpha(0.7)
patch.set_linestyle(ls)
patch.set_linewidth(lw)
for whisker in boxplot['whiskers']:
whisker.set(color ='black', linewidth = 1, linestyle='-')
n = np.arange(len(arrays))
lens = np.array([len(array) for array in arrays])
if(write_mean):
for (i,mean) in zip(n,boxplot['means']):
mean.set(color ='black', linewidth = 1, linestyle='--')
x = mean.get_xdata()[0]
xround = np.round(x,2)
ax.annotate(r'${}$'.format(xround), (x, i+1-0.45), fontsize=8, ha='center', va='center')
if(write_median):
for (i,median) in zip(n, boxplot['medians']):
median.set(color ='black', linewidth = 1, linestyle='-')
x = median.get_xdata()[0]
xround = np.round(x,2)
ax.annotate(r'${}$'.format(xround), (x, i+1-0.45), fontsize=8, ha='center', va='center')
if(write_minmax):
for (i,num) in zip(n,lens):
#min
x = boxplot['caps'][(2*i)].get_xdata()[0]
xround = np.round(x,3)
ax.annotate(r'${}$'.format(xround), (x, i+1-0.45), fontsize=8, ha='center', va='center')
#max
x = boxplot['caps'][(2*i)+1].get_xdata()[0]
xround = np.round(x,3)
ax.annotate(r'${}$'.format(xround), (x, i+1-0.45), fontsize=8, ha='center', va='center')
if(write_total):
for (i,num) in zip(n,lens):
ax.annotate(r'${}$'.format(num), (ax.get_xlim()[1], i+1), fontsize=8, ha='left', va='center')
############### Plotting end ################
#Axes limits
ax.set_ylim(0.2,len(arrays)+0.5)
if "xlim" in kwargs:
ax.set_xlim(**kwargs["xlim"])
if "ylim" in kwargs:
ax.set_ylim(**kwargs["ylim"])
#Text
if "text" in kwargs:
ax.text(**kwargs["text"],transform=ax.transAxes)
if save is not None:
fig.savefig(save, bbox_inches='tight', dpi=100)
return fig