EXPRES solar: an RV time series
Here we query the EXPRES solar Level-2 products over a date range and plot the radial-velocity series with its error bars — a dense, high-cadence look at the Sun observed as a star.
Note
This notebook runs against the live NEID/EXPRES server. The published version here uses synthetic data so it builds without an archive account — the numbers shown are illustrative, not real observations.
What is EXPRES solar data?
EXPRES, the EXtreme PREcision Spectrograph, sits on the Lowell Discovery Telescope. Like NEID it has a small solar feed — a telescope that feeds sunlight into the spectrograph during the day, observing the Sun as if it were a distant star. This client exposes only that solar data, and only at the solar L1 and L2 levels (it is hosted on the same IPAC archive as NEID). The result is a long, closely-spaced radial velocity (RV) time series, used to study solar activity and to validate the instrument’s stability. See Archives at NExScI for more on both spectrographs.
EXPRES solar data is fully public — no login required.
Build the archive client
from nexsci_rv import EXPRESArchive
expres = EXPRESArchive()
expres.data_levels
('solarl1', 'solarl2')
expres.data_levels lists the two levels EXPRES offers:
solarl1— the calibrated 1D extracted spectrum (flux vs. wavelength), for your own line or activity analysis.solarl2— the RV-ready products, where the radial velocity already lives.
We use solarl2 below because we want the velocities, not the raw spectra. See
Data levels for the full level ladder.
Query the solar L2 RV table
sun = expres.query_datetime(
"solarl2",
("2024-11-01", "2024-11-06"),
columns=["l2filename", "obsdate", "mjd", "v", "e_v", "dvrms", "extsnr"],
maxrec=200,
)
sun[:5]
| l2filename | obsdate | mjd | v | e_v | dvrms | extsnr |
|---|---|---|---|---|---|---|
| str30 | str19 | float64 | float64 | float64 | float64 | float64 |
| expressolar_20241100_0000.fits | 2024-11-01T14:15:00 | 60615.5 | 0.3656604957053176 | 0.7979048623631269 | 0.6005208885371415 | 201.32369437984357 |
| expressolar_20241100_0001.fits | 2024-11-01T14:20:00 | 60615.51 | -1.2479809274885947 | 0.8870038929736841 | 0.5413288618429397 | 277.48485251088306 |
| expressolar_20241100_0002.fits | 2024-11-01T14:25:00 | 60615.52 | 0.9005414349677486 | 0.6303301432552608 | 0.5695708319443566 | 242.4618785803643 |
| expressolar_20241100_0003.fits | 2024-11-01T14:30:00 | 60615.53 | 1.1286776596694565 | 0.6481838824139475 | 0.6294996572248076 | 233.34297165350756 |
| expressolar_20241100_0004.fits | 2024-11-01T15:35:00 | 60615.54 | -2.3412422263846038 | 0.6878223245103232 | 0.5904154960001773 | 222.2116924807027 |
The columns we kept, for EXPRES solar L2:
obsdate — the observation time stamp; query_datetime filters on it automatically.
mjd— the observation time as a Julian Date (JD / MJD); the natural numeric x-axis for a time series.v / e_v (EXPRES) — the answer:
vis the radial velocity in m/s ande_vits uncertainty. These play the role ccfrvmod and ccfnoise play on NEID.dvrms — the photon-noise precision floor for the exposure.
extsnr — extracted signal-to-noise; a natural quality cut (e.g.
where='extsnr > 200').
Full column tour: Understanding your results. As with NEID, maxrec
caps the returned rows (200 here); raise it once a query looks right.
Tip
mjd is a plain number. To convert it to a calendar date or another time
scale, wrap it in astropy.time.Time(sun['mjd'], format='mjd').
Plot the RV time series
v is the modelled radial velocity, e_v its formal uncertainty, and
dvrms the photon-noise floor.
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(9, 4))
ax.errorbar(
sun["mjd"],
sun["v"],
yerr=sun["e_v"],
fmt="o",
ms=3,
capsize=2,
alpha=0.7,
)
ax.set_xlabel("MJD")
ax.set_ylabel("RV (m/s)")
ax.set_title("EXPRES solar RV — 2024-11-01 to 2024-11-06")
ax.grid(alpha=0.3)
fig.tight_layout()
plt.show()
Why some searches don’t apply
On a normal target you can search by sky position, by name, or by catalog
ID. The Sun has no fixed celestial coordinate, so EXPRES solar rows carry
sentinel value coordinates (-99.9) instead of a real RA/Dec. The
package guards against treating those as a real position: position-, object-,
and qobject-based searches raise NotApplicable.
Reach for query_datetime (optionally narrowed with a where= cut)
instead.
Here is what that guard looks like in practice:
from astropy import units as u
from astropy.coordinates import SkyCoord
from nexsci_rv import NotApplicable
try:
expres.query_position("solarl2", SkyCoord("0d", "0d"), 1 * u.deg)
except NotApplicable as exc:
print(f"Got expected NotApplicable: {exc}")
Got expected NotApplicable: EXPRES solar rows carry sentinel qrad/qdecd values (-99.9); position search would not return meaningful results.
What next
Downloading FITS files and plotting a spectrum — download FITS files and open a spectrum.
EXPRES — the full catalog of EXPRES solar quirks.
Choosing a query — picking a query method per archive.