{ "cells": [ { "cell_type": "markdown", "id": "intro", "metadata": {}, "source": [ "# EXPRES solar: an RV time series\n", "\n", "Here we query the EXPRES solar Level-2 products over a date range and plot the\n", "radial-velocity series with its error bars — a dense, high-cadence look at the\n", "Sun observed as a star.\n", "\n", "```{note}\n", "This notebook runs against the live NEID/EXPRES server. The published version\n", "here uses synthetic data so it builds without an archive account — the numbers\n", "shown are illustrative, not real observations.\n", "```\n", "\n", "## What is EXPRES solar data?\n", "\n", "{term}`EXPRES`, the EXtreme PREcision Spectrograph, sits on the Lowell Discovery\n", "Telescope. Like NEID it has a small {term}`solar feed` — a telescope that feeds\n", "sunlight into the spectrograph during the day, observing the Sun as if it were a\n", "distant star. This client exposes **only** that solar data, and only at the\n", "solar L1 and L2 levels (it is hosted on the same IPAC archive as NEID). The\n", "result is a long, closely-spaced {term}`radial velocity (RV)` time series, used\n", "to study solar activity and to validate the instrument's stability. See\n", "{doc}`../concepts/instruments` for more on both spectrographs.\n", "\n", "EXPRES solar data is fully **public** — no login required." ] }, { "cell_type": "code", "execution_count": null, "id": "setup", "metadata": { "tags": [ "remove-cell" ] }, "outputs": [], "source": [ "import os\n", "\n", "import numpy as np\n", "from astropy.table import Table\n", "\n", "OFFLINE = bool(os.getenv(\"NEXSCI_RV_OFFLINE_FIXTURES\"))\n", "\n", "if OFFLINE:\n", " from unittest.mock import patch\n", "\n", " rng = np.random.default_rng(42)\n", " n = 30\n", " base_mjd = 60615.5\n", " _fake = Table(\n", " {\n", " \"l2filename\": [f\"expressolar_2024110{i // 10}_{i % 10:04d}.fits\" for i in range(n)],\n", " \"obsdate\": [\n", " f\"2024-11-{1 + i // 10:02d}T{14 + (i % 10) // 4:02d}:{15 + (i % 10) * 5:02d}:00\"\n", " for i in range(n)\n", " ],\n", " \"mjd\": base_mjd + np.arange(n) * 0.01,\n", " \"v\": rng.normal(0.0, 1.2, n),\n", " \"e_v\": rng.uniform(0.5, 0.9, n),\n", " \"dvrms\": rng.uniform(0.4, 0.7, n),\n", " \"extsnr\": rng.uniform(180, 320, n),\n", " }\n", " )\n", " _patcher = patch(\"nexsci_rv.tap.TapClient.query\", return_value=_fake)\n", " _patcher.start()\n", " print(\"Offline mode: queries return synthetic data.\")" ] }, { "cell_type": "markdown", "id": "client-md", "metadata": {}, "source": [ "## Build the archive client" ] }, { "cell_type": "code", "execution_count": null, "id": "client", "metadata": {}, "outputs": [], "source": [ "from nexsci_rv import EXPRESArchive\n", "\n", "expres = EXPRESArchive()\n", "expres.data_levels" ] }, { "cell_type": "markdown", "id": "898640eb", "metadata": {}, "source": [ "`expres.data_levels` lists the two levels EXPRES offers:\n", "\n", "- `solarl1` — the calibrated 1D {term}`extracted spectrum` (flux vs.\n", " wavelength), for your own line or activity analysis.\n", "- `solarl2` — the RV-ready products, where the radial velocity already lives.\n", "\n", "We use `solarl2` below because we want the velocities, not the raw spectra. See\n", "{doc}`../concepts/data-levels` for the full level ladder." ] }, { "cell_type": "markdown", "id": "query-md", "metadata": {}, "source": [ "## Query the solar L2 RV table" ] }, { "cell_type": "code", "execution_count": null, "id": "query", "metadata": {}, "outputs": [], "source": [ "sun = expres.query_datetime(\n", " \"solarl2\",\n", " (\"2024-11-01\", \"2024-11-06\"),\n", " columns=[\"l2filename\", \"obsdate\", \"mjd\", \"v\", \"e_v\", \"dvrms\", \"extsnr\"],\n", " maxrec=200,\n", ")\n", "sun[:5]" ] }, { "cell_type": "markdown", "id": "5408a74a", "metadata": {}, "source": [ "The columns we kept, for EXPRES solar L2:\n", "\n", "- `l2filename` — the L2 {term}`FITS` file name (the {term}`download` key).\n", "- {term}`obsdate` — the observation time stamp; {term}`query_datetime`\n", " filters on it automatically.\n", "- `mjd` — the observation time as a {term}`Julian Date (JD / MJD)`; the natural\n", " numeric x-axis for a time series.\n", "- {term}`v / e_v (EXPRES)` — **the answer**: `v` is the radial velocity in m/s\n", " and `e_v` its uncertainty. These play the role {term}`ccfrvmod` and\n", " {term}`ccfnoise` play on NEID.\n", "- {term}`dvrms` — the photon-noise precision floor for the exposure.\n", "- {term}`extsnr` — extracted signal-to-noise; a natural quality cut\n", " (e.g. `where='extsnr > 200'`).\n", "\n", "Full column tour: {doc}`../concepts/results-columns`. As with NEID, `maxrec`\n", "caps the returned rows (200 here); raise it once a query looks right.\n", "\n", "```{tip}\n", "`mjd` is a plain number. To convert it to a calendar date or another time\n", "scale, wrap it in `astropy.time.Time(sun['mjd'], format='mjd')`.\n", "```" ] }, { "cell_type": "markdown", "id": "plot-md", "metadata": {}, "source": [ "## Plot the RV time series\n", "\n", "`v` is the modelled radial velocity, `e_v` its formal uncertainty, and\n", "`dvrms` the photon-noise floor." ] }, { "cell_type": "code", "execution_count": null, "id": "plot", "metadata": {}, "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", "\n", "fig, ax = plt.subplots(figsize=(9, 4))\n", "ax.errorbar(\n", " sun[\"mjd\"],\n", " sun[\"v\"],\n", " yerr=sun[\"e_v\"],\n", " fmt=\"o\",\n", " ms=3,\n", " capsize=2,\n", " alpha=0.7,\n", ")\n", "ax.set_xlabel(\"MJD\")\n", "ax.set_ylabel(\"RV (m/s)\")\n", "ax.set_title(\"EXPRES solar RV — 2024-11-01 to 2024-11-06\")\n", "ax.grid(alpha=0.3)\n", "fig.tight_layout()\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "not-applicable-md", "metadata": {}, "source": [ "## Why some searches don't apply\n", "\n", "On a normal target you can search by sky position, by name, or by catalog\n", "ID. The Sun has no fixed celestial coordinate, so EXPRES solar rows carry\n", "{term}`sentinel value` coordinates (`-99.9`) instead of a real RA/Dec. The\n", "package guards against treating those as a real position: position-, object-,\n", "and qobject-based searches raise {py:exc}`~nexsci_rv.exceptions.NotApplicable`.\n", "Reach for {term}`query_datetime` (optionally narrowed with a `where=` cut)\n", "instead.\n", "\n", "Here is what that guard looks like in practice:" ] }, { "cell_type": "code", "execution_count": null, "id": "not-applicable", "metadata": {}, "outputs": [], "source": [ "from astropy import units as u\n", "from astropy.coordinates import SkyCoord\n", "\n", "from nexsci_rv import NotApplicable\n", "\n", "try:\n", " expres.query_position(\"solarl2\", SkyCoord(\"0d\", \"0d\"), 1 * u.deg)\n", "except NotApplicable as exc:\n", " print(f\"Got expected NotApplicable: {exc}\")" ] }, { "cell_type": "markdown", "id": "next-steps", "metadata": {}, "source": [ "## What next\n", "\n", "- {doc}`03_download_and_plot` — download FITS files and open a spectrum.\n", "- {doc}`../archives/expres` — the full catalog of EXPRES solar quirks.\n", "- {doc}`../concepts/choosing-a-query` — picking a query method per archive." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python", "version": "3.12" } }, "nbformat": 4, "nbformat_minor": 5 }