Basic Usage

The most basic use case of GRASS is generating time series of synthetic spectra and measuring apparent velocities. These can both be done in only a few lines of code.

Generating Synthetic Spectra

The simplest use case for GRASS is the generation of time series of synthetic spectra. This can be done in only a few lines of code. The following example generates 25 spectra consisting of a single line at 5434.5 Angstroms. By default, the temporal spacing of the spectra is 15 seconds.

using GRASS
using Statistics

# PythonPlot comes from the default environment, not from GRASS's dependencies
import PythonPlot
const plt = PythonPlot.pyplot
const mpl = PythonPlot.matplotlib

# parameters for lines in the spectra
λrest = 5434.5
lines = [λrest]     # array of line centers in angstroms
depths = [0.75]      # continuum-normalized depth of lines
resolution = 7e5     # spectral resolution of the output spectra
spec = SpecParams(lines=lines, depths=depths, resolution=resolution)

# specify number of epochs (default 15-second spacing)
disk = DiskParams(Nt=50)

# synthesize the spectra
wavelengths, flux = synthesize_spectra(spec, disk)

# get the residuals
resid_flux = flux .- mean(flux, dims=2)

# get a colormap for time
cmap = plt.get_cmap("viridis")
colors = cmap(range(0, 1, length=disk.Nt))
norm = mpl.colors.Normalize(vmin=15, vmax=15*disk.Nt)
smap = mpl.cm.ScalarMappable(norm=norm, cmap=cmap)

# plot the result; the axis array comes back as a Python object, so it is indexed from 0
fig, axs = plt.subplots(nrows=2, ncols=1, height_ratios=[3,1], sharex=true)
ax1 = axs[0]
ax2 = axs[1]
for i in 1:disk.Nt
    xs = wavelengths
    ys1 = view(flux, :, i)
    ys2 = view(resid_flux, :, i)
    ax1.plot(xs, ys1, c = colors[i, :])
    ax2.plot(xs, ys2, c = colors[i, :])
end
ax2.set_xlabel("Air Wavelength [Å]")
ax1.set_ylabel("Normalized Flux")
ax2.set_ylabel("Residual Flux")
ax1.grid(ls = ":", alpha=0.5)
ax2.grid(ls = ":", alpha=0.5)
ax1.set_xlim(λrest - 0.5, λrest + 0.5)
fname = joinpath(GRASS.moddir, "docs", "src", "assets", "spectrum.png")
plt.savefig(fname, bbox_inches="tight")
plt.show()

formation_temps

Measuring Velocities

GRASS wraps the EchelleCCFs package to measure apparent Doppler velocities from spectra.


# generate a velocity grid and compute the cross-correlation function
vel_grid, ccf = calc_ccf(wavelengths, flux, spec, normalize=true)

# measure radial velocities and uncertainties from the ccfs
rvs, sigs = calc_rvs_from_ccf(vel_grid, ccf)

# measure an rms
rms = calc_rms(rvs)
rms_string = string(round(rms, digits=3))

# plot the RVs
plt.scatter(15 .* collect(1:disk.Nt), rvs, c="k", s=5)
plt.xlabel("Time [s]")
plt.ylabel("RV [m/s]")
# plt.xlim(0, 15 * disk.Nt + 1)
# plt.ylim(minimum(rvs) - rms, maximum(rvs) + rms)
plt.title("RMS ≈ $rms_string [m/s]")
plt.grid(ls = ":", alpha=0.5)
fname = joinpath(GRASS.moddir, "docs", "src", "assets", "rvs.png")
plt.savefig(fname, bbox_inches="tight")
plt.show()

formation_temps

Measuring Bisectors

GRASS wraps the EchelleCCFs package to measure apparent Doppler velocities from spectra.


# measure bisectors
bis_wavs, bis_flux = calc_bisector(wavelengths, flux, nflux=200)

# cut off the bottom-most noisy measurement
bis_wavs = bis_wavs[2:end, :]
bis_flux = bis_flux[2:end, :]

# convert to from a wavelength to a velocity scale
bis_vels = GRASS.c_ms .* (bis_wavs .- λrest) ./ λrest 

# plot it 
for i in 1:disk.Nt
    xs = view(bis_vels, :, i)
    ys = view(bis_flux, :, i)
    plt.plot(xs, ys, c=colors[i,:])
end
plt.xlabel("Velocity [m/s]")
plt.ylabel("Normalized Flux")
plt.colorbar(smap, label = "Time [s]", ax=plt.gca())
plt.grid(ls = ":", alpha=0.5)
fname = joinpath(GRASS.moddir, "docs", "src", "assets", "bisectors.png")
plt.savefig(fname, bbox_inches="tight")
plt.show()

formation_temps

We can also examine the mean-subtracted bisectors to better see the temporal variability in line shape created by granulation.


# now measure the means
mean_bis_wavs = mean(bis_wavs, dims=2)
mean_bis_vels = mean(bis_vels, dims=2)

# get residuals
resid_bis_wavs = bis_wavs .- mean_bis_wavs
resid_bis_vels = bis_vels .- mean_bis_vels

# smooth them
n = 10
resid_bis_wavs_smooth = moving_average(resid_bis_wavs, n)
resid_bis_vels_smooth = moving_average(resid_bis_vels, n)
bis_flux_smooth = moving_average(bis_flux, n)

# plot it
for i in 1:disk.Nt
    xs = view(resid_bis_vels_smooth, :, i)
    ys = view(bis_flux_smooth, :, i)
    plt.plot(xs, ys, c=colors[i,:])
end
plt.xlabel("Residual Velocity [m/s]")
plt.ylabel("Normalized Flux")
plt.colorbar(smap, label = "Time [s]", ax=plt.gca())
plt.grid(ls = ":", alpha=0.5)
fname = joinpath(GRASS.moddir, "docs", "src", "assets", "bisector_resids.png")
plt.savefig(fname, bbox_inches="tight")
plt.show()

formation_temps