diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c4f6fd1..7a69da7 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -32,7 +32,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.11' + python-version: '3.12' - name: Install package run: | @@ -42,6 +42,8 @@ jobs: pip install -e .[doc] - name: Build with Sphinx + env: + JSOC_EMAIL: "roytsmart@gmail.com" run: | cd ./docs make html diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e50f63b..8f8b820 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,7 +14,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macOS-latest] - python-version: ["3.11", "3.12"] + python-version: ["3.12", "3.13"] name: ${{ matrix.os }}, Python ${{ matrix.python-version }} tests steps: - name: Set Swap Space @@ -40,6 +40,7 @@ jobs: - name: Test with pytest env: MPLBACKEND: "agg" + JSOC_EMAIL: "roytsmart@gmail.com" run: | pip install pytest pytest-cov pytest --cov=. --cov-report=xml --cov-report=html diff --git a/docs/conf.py b/docs/conf.py index 94fbf47..6be3366 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -113,6 +113,10 @@ nbsphinx_execute = 'always' +suppress_warnings = [ + 'nbsphinx', +] + codeautolink_custom_blocks = {"jupyter-execute": None} intersphinx_mapping = { diff --git a/docs/index.rst b/docs/index.rst index 58b9f48..d558d74 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -98,6 +98,7 @@ Flight 1 (2019) reports/point-spread-function reports/throughput + reports/aia-image-simulation reports/level-0 reports/level-1 diff --git a/docs/refs.bib b/docs/refs.bib index e23b276..9d51869 100644 --- a/docs/refs.bib +++ b/docs/refs.bib @@ -85,4 +85,86 @@ @article{Poletto2004 doi = {10.1364/AO.43.002029}, abstract = {Performances are presented of three classes of imaging slit spectrometers for extended sources with aberration-corrected gratings. A general analytical expression for minimizing off-axis grating aberrations is obtained, and it is demonstrated that these aberrations are minimized when the spectrometer is operated at a magnification higher than unity. Classical designs with toroidal uniform-line-spaced (TULS) or spherical varied-line-space (SVLS) gratings are compared with a new class of designs that utilize toroidal varied-line-space (TVLS) gratings. Although TULS and SVLS designs with two stigmatic points can be designed to operate at near-unity magnification with excellent on-axis spectral and spatial resolutions, they cannot be made to satisfy the general off-axis condition, and so their off-axis performances are not optimum. On the contrary TVLS designs with two stigmatic points can be operated at almost any magnification, thus satisfying the off-axis condition perfectly. Such designs are suitable for imaging spectrometer observations that require an extended field of view.}, } +@ARTICLE{Vernazza1978, + author = {{Vernazza}, J.~E. and {Reeves}, E.~M.}, + title = "{Extreme ultraviolet composite spectra of representative solar features.}", + journal = {\apjs}, + keywords = {Far Ultraviolet Radiation, Line Spectra, Solar Spectra, Spectrum Analysis, Tables (Data), Ultraviolet Spectra, Astronomical Photometry, Solar Corona, Solar Cycles, Solar Physics, Extreme UV:Sun, Solar Spectrum: Line Identifications}, + year = 1978, + month = aug, + volume = {37}, + pages = {485-513}, + doi = {10.1086/190539}, + adsurl = {https://ui.adsabs.harvard.edu/abs/1978ApJS...37..485V}, + adsnote = {Provided by the SAO/NASA Astrophysics Data System} +} +@dataset{Dere1997, + author = {{Dere}, K.~P. and {Landi}, E. and {Mason}, H.~E. and {Monsignori Fossi}, B.~C. and {Young}, P.~R.}, + title = "{VizieR Online Data Catalog: CHIANTI - An Atomic Database For Emission Lines I. (Dere+ 1997)}", + howpublished = {VizieR On-line Data Catalog: J/A+AS/125/149. Originally published in: 1997A\&AS..125..149D}, + year = 1997, + month = apr, + eid = {J/A+AS/125/149}, + adsurl = {https://ui.adsabs.harvard.edu/abs/1997yCat..41250149D}, + adsnote = {Provided by the SAO/NASA Astrophysics Data System} +} +@article{Doschek2004, + doi = {10.1086/379877}, + url = {https://doi.org/10.1086/379877}, + year = {2004}, + month = {jan}, + publisher = {}, + volume = {600}, + number = {2}, + pages = {1061}, + author = {Doschek, G. A. and Feldman, U.}, + title = {Properties of the Lower Transition Region: The Widths of Optically Allowed and Intersystem Spectral Lines}, + journal = {The Astrophysical Journal}, + abstract = {The widths of spectral lines in the ultraviolet (UV) and extreme ultraviolet (EUV) spectral regions that are formed in the solar transition region and corona are usually greater than the optically thin widths due to thermal Doppler broadening calculated under the assumption of ionization equilibrium. Although opacity can explain the widths of some lines, there are a host of optically thin lines for which the excess widths are attributed to nonthermal motions. Interest in these motions for coronal heating theories has led to the measurement and comparison of spectral line profiles/widths throughout the solar UV and EUV spectrum. We find that for the quiet Sun the widths of some optically allowed lower transition region lines, deduced from spectra obtained by the Solar Ultraviolet Measurements of Ultraviolet Radiation (SUMER) spectrometer on the Solar and Heliospheric Observatory (SOHO) spacecraft, are considerably larger than predicted from simply scaling previously measured wavelengths of other lines from the same ion. For example, the O III lines of the multiplet near 834 Å are considerably wider than predicted from the previously measured (from Skylab) width of the optically thin O III 1666.15 Å intersystem line. The excess widths are not due to nonthermal motions, as these are already included in the width of the 1666.15 Å line. In this paper, we analyze the widths of some prominent optically allowed lines and discuss possible causes for discrepancies with previous measurements of intersystem lines.} +} +@ARTICLE{Peter1999, + author = {{Peter}, H.}, + title = "{The Chromosphere in Coronal Holes and the Quiet-Sun Network: an HE I (584 {\r{A}}) Full-Disk Scan by SUMER/SOHO}", + journal = {\apjl}, + keywords = {SUN: SOLAR WIND, SUN: CHROMOSPHERE, SUN: CORONA, Sun: Solar Wind, Sun: Chromosphere, Sun: Corona}, + year = 1999, + month = sep, + volume = {522}, + number = {1}, + pages = {L77-L80}, + doi = {10.1086/312214}, + adsurl = {https://ui.adsabs.harvard.edu/abs/1999ApJ...522L..77P}, + adsnote = {Provided by the SAO/NASA Astrophysics Data System} +} +@ARTICLE{Lemen2012, + author = {{Lemen}, James R. and {Title}, Alan M. and {Akin}, David J. and {Boerner}, Paul F. and {Chou}, Catherine and {Drake}, Jerry F. and {Duncan}, Dexter W. and {Edwards}, Christopher G. and {Friedlaender}, Frank M. and {Heyman}, Gary F. and {Hurlburt}, Neal E. and {Katz}, Noah L. and {Kushner}, Gary D. and {Levay}, Michael and {Lindgren}, Russell W. and {Mathur}, Dnyanesh P. and {McFeaters}, Edward L. and {Mitchell}, Sarah and {Rehse}, Roger A. and {Schrijver}, Carolus J. and {Springer}, Larry A. and {Stern}, Robert A. and {Tarbell}, Theodore D. and {Wuelser}, Jean-Pierre and {Wolfson}, C. Jacob and {Yanari}, Carl and {Bookbinder}, Jay A. and {Cheimets}, Peter N. and {Caldwell}, David and {Deluca}, Edward E. and {Gates}, Richard and {Golub}, Leon and {Park}, Sang and {Podgorski}, William A. and {Bush}, Rock I. and {Scherrer}, Philip H. and {Gummin}, Mark A. and {Smith}, Peter and {Auker}, Gary and {Jerram}, Paul and {Pool}, Peter and {Soufli}, Regina and {Windt}, David L. and {Beardsley}, Sarah and {Clapp}, Matthew and {Lang}, James and {Waltham}, Nicholas}, + title = "{The Atmospheric Imaging Assembly (AIA) on the Solar Dynamics Observatory (SDO)}", + journal = {\solphys}, + keywords = {Solar corona, Solar instrumentation, Solar imaging, Extreme ultraviolet}, + year = 2012, + month = jan, + volume = {275}, + number = {1-2}, + pages = {17-40}, + doi = {10.1007/s11207-011-9776-8}, + adsurl = {https://ui.adsabs.harvard.edu/abs/2012SoPh..275...17L}, + adsnote = {Provided by the SAO/NASA Astrophysics Data System} +} +@ARTICLE{Pesnell2012, + author = {{Pesnell}, W. Dean and {Thompson}, B.~J. and {Chamberlin}, P.~C.}, + title = "{The Solar Dynamics Observatory (SDO)}", + journal = {\solphys}, + keywords = {SDO, Solar cycle, Helioseismology, Coronal, Space weather}, + year = 2012, + month = jan, + volume = {275}, + number = {1-2}, + pages = {3-15}, + doi = {10.1007/s11207-011-9841-3}, + adsurl = {https://ui.adsabs.harvard.edu/abs/2012SoPh..275....3P}, + adsnote = {Provided by the SAO/NASA Astrophysics Data System} +} + + + diff --git a/docs/reports/aia-image-simulation.ipynb b/docs/reports/aia-image-simulation.ipynb new file mode 100644 index 0000000..7634ac4 --- /dev/null +++ b/docs/reports/aia-image-simulation.ipynb @@ -0,0 +1,378 @@ +{ + "cells": [ + { + "cell_type": "raw", + "id": "0", + "metadata": { + "editable": true, + "raw_mimetype": "text/x-rst", + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Image Simulation\n", + "----------------\n", + "The Atmospheric Imaging Assembly (AIA) :cite:p:`Lemen2012` on the Solar Dynamics Observatory :cite:p:`Pesnell2012` is a NASA satellite that continuously observes the Sun in extreme ultraviolet (EUV). Since some of the EUV channels that AIA observes are close in temperature to the lines that ESIS observes, we can use AIA observations to simulate ESIS images.\n", + "In this notebook, we select the AIA images from during the ESIS-I 2019 flight and simulate sythetic images using the ideal ESIS design." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "import warnings\n", + "import matplotlib.pyplot as plt\n", + "import named_arrays as na\n", + "import esis" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "warnings.filterwarnings(\"ignore\")" + ] + }, + { + "cell_type": "raw", + "id": "3", + "metadata": { + "editable": true, + "raw_mimetype": "text/x-rst", + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Define the logical axis corresponding to changes in time." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "axis_time = \"time\"" + ] + }, + { + "cell_type": "raw", + "id": "5", + "metadata": { + "editable": true, + "raw_mimetype": "text/x-rst", + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Define the logical axis corresponding to changes in line-of-sight velocity." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "axis_velocity = \"velocity\"" + ] + }, + { + "cell_type": "raw", + "id": "7", + "metadata": { + "editable": true, + "raw_mimetype": "text/x-rst", + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Define the logical axes corresponding to changes in position on the disk of the Sun." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "axis_x = \"detector_x\"\n", + "axis_y = \"detector_y\"" + ] + }, + { + "cell_type": "raw", + "id": "9", + "metadata": { + "editable": true, + "raw_mimetype": "text/x-rst", + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Load the sythetic scene composed of AIA images.\n", + "Here, we use the :func:`~esis.flights.f1.data.synth.scene_aia` function, which recreates the brightest three lines in the ESIS passband." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "scene = esis.flights.f1.data.synth.scene_aia(\n", + " axis_time=axis_time,\n", + " axis_detector_x=axis_x,\n", + " axis_detector_y=axis_y,\n", + " axis_velocity=axis_velocity,\n", + " limit=1,\n", + ")" + ] + }, + { + "cell_type": "raw", + "id": "11", + "metadata": { + "editable": true, + "raw_mimetype": "text/x-rst", + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Load a single channel of the ideal ESIS optical model." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "instrument = esis.flights.f1.optics.design_single(num_distribution=0)" + ] + }, + { + "cell_type": "raw", + "id": "13", + "metadata": { + "editable": true, + "raw_mimetype": "text/x-rst", + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Reduce the number of rays traced per field angle so that this tutorial has a reasonable runtime.\n", + "This number should be increased to simulate research-quality images." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14", + "metadata": {}, + "outputs": [], + "source": [ + "instrument.pupil.num = 2" + ] + }, + { + "cell_type": "raw", + "id": "15", + "metadata": { + "editable": true, + "raw_mimetype": "text/x-rst", + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Also halve the number of pixels in each axis to increase the signal-to-noise ratio of the final image." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "sensor = instrument.camera.sensor\n", + "sensor.width_pixel = 2 * sensor.width_pixel\n", + "sensor.num_pixel_x = sensor.num_pixel_x // 2\n", + "sensor.num_pixel_y = sensor.num_pixel_y // 2" + ] + }, + { + "cell_type": "raw", + "id": "17", + "metadata": { + "editable": true, + "raw_mimetype": "text/x-rst", + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Image the sythetic scene using our model of the ESIS optical system." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "%%time\n", + "images = instrument.system.image(\n", + " scene=scene[{axis_time: 0}],\n", + " axis_wavelength=axis_velocity,\n", + " axis_field=(axis_x, axis_y),\n", + ")" + ] + }, + { + "cell_type": "raw", + "id": "19", + "metadata": { + "editable": true, + "raw_mimetype": "text/x-rst", + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Display the simulated image." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "fig, ax = plt.subplots(\n", + " figsize=(11, 5),\n", + " constrained_layout=True,\n", + ")\n", + "vmax = images.outputs.percentile(99.9).value\n", + "img = na.plt.pcolormesh(\n", + " images.inputs.position.x,\n", + " images.inputs.position.y,\n", + " C=images.outputs.sum((\"wavelength\", axis_velocity)).value,\n", + " cmap=\"gray\",\n", + " vmin=0,\n", + " vmax=vmax,\n", + ")\n", + "ax.set_xticks([])\n", + "ax.set_yticks([])\n", + "ax.set_aspect(\"equal\")\n", + "plt.colorbar(img.ndarray.item(), label=f\"signal ({images.outputs.unit})\");" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/esis/data/__init__.py b/esis/data/__init__.py index 0472ab5..3c66a0e 100644 --- a/esis/data/__init__.py +++ b/esis/data/__init__.py @@ -28,11 +28,13 @@ """ from . import abc +from . import synth from ._level_0 import Level_0 from ._level_1 import Level_1 __all__ = [ "abc", + "synth", "Level_0", "Level_1", ] diff --git a/esis/data/synth/__init__.py b/esis/data/synth/__init__.py new file mode 100644 index 0000000..276f66b --- /dev/null +++ b/esis/data/synth/__init__.py @@ -0,0 +1,7 @@ +"""Tools for creating synthetic solar scenes for ESIS analysis.""" + +from ._scene_aia import scene_aia + +__all__ = [ + "scene_aia", +] diff --git a/esis/data/synth/_scene_aia/__init__.py b/esis/data/synth/_scene_aia/__init__.py new file mode 100644 index 0000000..a15754d --- /dev/null +++ b/esis/data/synth/_scene_aia/__init__.py @@ -0,0 +1,5 @@ +from ._scene_aia import scene_aia + +__all__ = [ + "scene_aia", +] diff --git a/esis/data/synth/_scene_aia/_scene_aia.py b/esis/data/synth/_scene_aia/_scene_aia.py new file mode 100644 index 0000000..e6703a2 --- /dev/null +++ b/esis/data/synth/_scene_aia/_scene_aia.py @@ -0,0 +1,125 @@ +import numpy as np +import scipy.special as sp +import astropy.units as u +import astropy.time +from astropy import constants as const +import named_arrays as na +import sdo + +__all__ = [ + "scene_aia", +] + + +def scene_aia( + time_start: astropy.time.Time, + time_stop: astropy.time.Time, + wavelength_aia: u.Quantity | na.AbstractScalarArray, + wavelength_new: u.Quantity | na.AbstractScalarArray, + radiance: u.Quantity | na.AbstractScalarArray, + width_doppler: u.Quantity | na.AbstractScalarArray, + axis_time: str = "time", + axis_detector_x: str = "detector_x", + axis_detector_y: str = "detector_y", + axis_velocity: str = "velocity", + num_velocity: int = 1, + num_std: float = 3, + user_email: None | str = None, + limit: None | int = None, +): + r""" + Create a synthetic solar scene composed of AIA images. + + AIA images from channels `wavelength_aia` over a supplied time range are used to + represent estimates of images at `wavelength_new`. + A supplied mean radiance is assigned to each image at `wavelength_new` + and distributed along `axis_velocity` into `num_velocity` bins + using a Gaussian with standard deviation `width_doppler`. + + Parameters + ---------- + time_start + The start time of the AIA observations. + time_stop + The stop time of the AIA observations. + wavelength_aia + The wavelength label of the AIA channel. + wavelength_new + The rest wavelength of each spectral line in the synthetic scene replacing + `wavelength_aia`. + Elements of `wavelength_new` should correspond to the elements of . + radiance + The average radiance of each spectral line in the synthetic scene in + units of :math:`\text{erg}\,\text{cm}^{-2}\,\text{sr}^{-1}\,\text{s}^{-1}.` + width_doppler + The average standard deviation of each spectral line in the synthetic scene. + axis_time + The logical axis corresponding to changes in time. + axis_detector_x + The logical axis corresponding to changes in detector :math:`x`-coordinate. + axis_detector_y + The logical axis corresponding to changes in detector :math:`y`-coordinate. + axis_velocity + The logical axis corresponding to changes in line-of-sight velocity. + num_velocity + The number of velocity bins in the synthetic scene. + num_std + The size of the domain for each spectral line in standard deviation units. + user_email + An email address used to notify the user that their JSOC request + is complete. + This email must be registered with JSOC before using this function. + If :obj:`None`, the value is taken from the ``JSOC_EMAIL`` + environment variable. + limit + The maximum number of files to download per wavelength. + + See Also + -------- + :func:`esis.flights.f1.data.synth.scene_aia`: + A wrapper around this function for ESIS-I. + """ + velocity_max = width_doppler * num_std + + velocity = na.linspace( + start=-velocity_max, + stop=velocity_max, + axis=axis_velocity, + num=num_velocity + 1, + ) + + z_a = velocity[{axis_velocity: slice(0, -1)}] / (width_doppler * np.sqrt(2)) + z_b = velocity[{axis_velocity: slice(1, None)}] / (width_doppler * np.sqrt(2)) + gaussian = 0.5 * (sp.erf(z_b) - sp.erf(z_a)) + + wavelength = (1 + velocity / const.c) * wavelength_new + + obs = sdo.aia.open( + time_start=time_start, + time_stop=time_stop, + wavelength=wavelength_aia, + user_email=user_email, + axis_time=axis_time, + axis_detector_x=axis_detector_x, + axis_detector_y=axis_detector_y, + limit=limit, + ) + axis_detector_xy = axis_detector_x, axis_detector_y + + crop = { + axis_detector_x: slice(1024, 1024 + 2048), + axis_detector_y: slice(1024, 1024 + 2048), + } + + outputs = radiance * obs.outputs / obs.outputs[crop].mean(axis_detector_xy) + delta_lambda = np.diff(wavelength, axis=axis_velocity) + outputs = outputs * gaussian / delta_lambda + + return na.FunctionArray( + inputs=na.TemporalSpectralPositionalVectorArray( + time=obs.inputs.time, + wavelength=wavelength, + position=obs.inputs.position, + ), + outputs=outputs, + ) diff --git a/esis/flights/f1/__init__.py b/esis/flights/f1/__init__.py index 74df5e5..d051720 100644 --- a/esis/flights/f1/__init__.py +++ b/esis/flights/f1/__init__.py @@ -1,10 +1,12 @@ """Models and data associated with the first flight in 2019.""" +from . import spectrum from . import optics from . import nsroc from . import data __all__ = [ + "spectrum", "optics", "nsroc", "data", diff --git a/esis/flights/f1/data/__init__.py b/esis/flights/f1/data/__init__.py index 94eb471..481e255 100644 --- a/esis/flights/f1/data/__init__.py +++ b/esis/flights/f1/data/__init__.py @@ -3,9 +3,11 @@ from ._fits import path_fits from ._level_0 import level_0 from ._level_1 import level_1 +from . import synth __all__ = [ "path_fits", "level_0", "level_1", + "synth", ] diff --git a/esis/flights/f1/data/synth/__init__.py b/esis/flights/f1/data/synth/__init__.py new file mode 100644 index 0000000..61296f2 --- /dev/null +++ b/esis/flights/f1/data/synth/__init__.py @@ -0,0 +1,7 @@ +"""Create synthetic solar scenes that can be observed with an instrument model.""" + +from ._scene_aia import scene_aia + +__all__ = [ + "scene_aia", +] diff --git a/esis/flights/f1/data/synth/_scene_aia/__init__.py b/esis/flights/f1/data/synth/_scene_aia/__init__.py new file mode 100644 index 0000000..a15754d --- /dev/null +++ b/esis/flights/f1/data/synth/_scene_aia/__init__.py @@ -0,0 +1,5 @@ +from ._scene_aia import scene_aia + +__all__ = [ + "scene_aia", +] diff --git a/esis/flights/f1/data/synth/_scene_aia/_scene_aia.py b/esis/flights/f1/data/synth/_scene_aia/_scene_aia.py new file mode 100644 index 0000000..f5134eb --- /dev/null +++ b/esis/flights/f1/data/synth/_scene_aia/_scene_aia.py @@ -0,0 +1,130 @@ +import astropy.units as u +import astropy.time +import named_arrays as na +import esis +from ... import level_1 +from ....spectrum import O_V, Mg_X, He_I + +__all__ = [ + "scene_aia", +] + + +def scene_aia( + time_start: None | astropy.time.Time = None, + time_stop: None | astropy.time.Time = None, + axis_time: str = "time", + axis_detector_x: str = "detector_x", + axis_detector_y: str = "detector_y", + axis_wavelength: str = "wavelength", + axis_velocity: str = "velocity", + num_velocity: int = 1, + num_std: float = 3, + user_email: None | str = None, + limit: None | int = None, +): + r""" + Load a synthetic solar scene composed of AIA images captured during the flight. + + This function plugs the spectral line properties in + :mod:`esis.flights.f1.spectrum` into :func:`esis.data.synth.scene_aia` + to produce the synthic scene. + + Only the brightest three lines in the ESIS passband are recreated: + :math:`\text{O\,V}\;630\,\AA`, :math:`\text{Mg\,X}\;609\,\AA`, and + :math:`\text{He\,I}\;584\,\AA`. + + Parameters + ---------- + time_start + The start time of the AIA observations. + If :obj:`None`, the start time of the ESIS Level-1 observations is used. + time_stop + The stop time of the AIA observations. + If :obj:`None`, the stop time of the ESIS Level-1 observations is used. + axis_time + The logical axis corresponding to changes in time. + axis_detector_x + The logical axis corresponding to changes in detector :math:`x`-coordinate. + axis_detector_y + The logical axis corresponding to changes in detector :math:`y`-coordinate. + axis_wavelength + The logical axis corresponding to changing spectral line. + axis_velocity + The logical axis corresponding to changes in line-of-sight velocity. + num_velocity + The number of velocity bins in the synthetic scene. + num_std + The size of the domain for each spectral line in standard deviation units. + user_email + An email address used to notify the user that their JSOC request + is complete. + This email must be registered with JSOC before using this function. + If :obj:`None`, the value is taken from the ``JSOC_EMAIL`` + environment variable. + limit + The maximum number of files to download per wavelength. + """ + l1 = level_1() + + if time_start is None: + time_start = l1.inputs.time_start[{l1.axis_time: 0}].ndarray.mean() + + if time_stop is None: + time_stop = l1.inputs.time_end[{l1.axis_time: ~0}].ndarray.mean() + + wavelength_aia = [304, 193, 304] * u.AA + + wavelength_esis = [ + O_V.wavelength, + Mg_X.wavelength, + He_I.wavelength, + ] + + radiance_esis = [ + O_V.radiance, + Mg_X.radiance, + He_I.radiance, + ] + + width_esis = [ + O_V.width_doppler, + Mg_X.width_doppler, + He_I.width_doppler, + ] + + wavelength_aia = na.ScalarArray(wavelength_aia, axis_wavelength) + wavelength_esis = na.stack(wavelength_esis, axis_wavelength) + radiance_esis = na.stack(radiance_esis, axis_wavelength) + width_esis = na.stack(width_esis, axis_wavelength) + + result = esis.data.synth.scene_aia( + time_start=time_start, + time_stop=time_stop, + wavelength_aia=wavelength_aia, + wavelength_new=wavelength_esis, + radiance=radiance_esis, + width_doppler=width_esis, + axis_time=axis_time, + axis_detector_x=axis_detector_x, + axis_detector_y=axis_detector_y, + axis_velocity=axis_velocity, + num_velocity=num_velocity, + num_std=num_std, + user_email=user_email, + limit=limit, + ) + + shape = result.outputs.shape + + num_x = shape[axis_detector_x] + num_y = shape[axis_detector_y] + + crop = { + axis_detector_x: slice(num_x // 3, 2 * num_x // 3), + axis_detector_y: slice(num_y // 3, 2 * num_y // 3), + } + + result = result[crop] + + return result diff --git a/esis/flights/f1/data/synth/_scene_aia/_scene_aia_test.py b/esis/flights/f1/data/synth/_scene_aia/_scene_aia_test.py new file mode 100644 index 0000000..2463f64 --- /dev/null +++ b/esis/flights/f1/data/synth/_scene_aia/_scene_aia_test.py @@ -0,0 +1,37 @@ +import pytest +import astropy.units as u +import numpy as np +import esis +from esis.flights.f1.spectrum import O_V, Mg_X, He_I + + +@pytest.mark.parametrize("num_velocity", [3]) +def test_scene_aia( + num_velocity: int, +): + + axis_x = "detector_x" + axis_y = "detector_y" + axis_xy = (axis_x, axis_y) + axis_wavelength = "wavelength" + axis_velocity = "velocity" + + limit = 1 + + result = esis.flights.f1.data.synth.scene_aia( + axis_detector_x=axis_x, + axis_detector_y=axis_y, + axis_wavelength=axis_wavelength, + axis_velocity=axis_velocity, + num_velocity=num_velocity, + limit=limit, + ) + + assert result.shape[axis_velocity] == num_velocity + assert result.outputs.unit.is_equivalent(u.erg / u.cm**2 / u.sr / u.AA / u.s) + + delta_lambda = np.diff(result.inputs.wavelength, axis=axis_velocity) + radiance = (result.outputs * delta_lambda).sum(axis_velocity).mean(axis_xy) + assert np.allclose(radiance[{axis_wavelength: 0}], O_V.radiance, rtol=1e-1) + assert np.allclose(radiance[{axis_wavelength: 1}], Mg_X.radiance, rtol=1e-1) + assert np.allclose(radiance[{axis_wavelength: 2}], He_I.radiance, rtol=1e-1) diff --git a/esis/flights/f1/spectrum/He_I.py b/esis/flights/f1/spectrum/He_I.py new file mode 100644 index 0000000..5783a58 --- /dev/null +++ b/esis/flights/f1/spectrum/He_I.py @@ -0,0 +1,18 @@ +r"""Properties of the :math:`\text{He\,I}\;584\,\AA` spectral line.""" + +import astropy.units as u + +__all__ = [ + "wavelength", + "radiance", + "width_doppler", +] + +#: Rest wavelength calculated by the Chianti Atomic Database :cite:p:`Dere1997`. +wavelength = 584.334 * u.AA + +#: Average quiet-sun radiance measured by :cite:t:`Vernazza1978`. +radiance = 544.98 * u.erg / u.cm**2 / u.sr / u.s + +#: Average quiet-sun Doppler width measured by :cite:t:`Peter1999`. +width_doppler = 20 * u.km / u.s diff --git a/esis/flights/f1/spectrum/Mg_X.py b/esis/flights/f1/spectrum/Mg_X.py new file mode 100644 index 0000000..6aff841 --- /dev/null +++ b/esis/flights/f1/spectrum/Mg_X.py @@ -0,0 +1,25 @@ +r"""Properties of the :math:`\text{Mg\,X}\;609\,\AA` spectral line.""" + +import numpy as np +import astropy.units as u + +__all__ = [ + "wavelength", + "radiance", + "width_doppler", +] + +#: Rest wavelength calculated by the Chianti Atomic Database :cite:p:`Dere1997`. +wavelength = 609.793 * u.AA + +#: Average quiet-sun radiance measured by :cite:t:`Vernazza1978`. +radiance = 125.05 * u.erg / u.cm**2 / u.sr / u.s + +_fwhm = 0.138 * u.AA + +_width = _fwhm / (2 * np.sqrt(2 * np.log(2))) + +_eq = u.doppler_optical(wavelength) + +#: Average quiet-sun Doppler width measured by :cite:t:`Doschek2004`. +width_doppler = (wavelength + _width).to(u.km / u.s, equivalencies=_eq) diff --git a/esis/flights/f1/spectrum/O_V.py b/esis/flights/f1/spectrum/O_V.py new file mode 100644 index 0000000..47d637e --- /dev/null +++ b/esis/flights/f1/spectrum/O_V.py @@ -0,0 +1,25 @@ +r"""Properties of the :math:`\text{O\,V}\;630\,\AA` spectral line.""" + +import numpy as np +import astropy.units as u + +__all__ = [ + "wavelength", + "radiance", + "width_doppler", +] + +#: Rest wavelength calculated by the Chianti Atomic Database :cite:p:`Dere1997`. +wavelength = 629.732 * u.AA + +#: Average quiet-sun radiance measured by :cite:t:`Vernazza1978`. +radiance = 334.97 * u.erg / u.cm**2 / u.sr / u.s + +_fwhm = 0.129 * u.AA + +_width = _fwhm / (2 * np.sqrt(2 * np.log(2))) + +_eq = u.doppler_optical(wavelength) + +#: Average quiet-sun Doppler width measured by :cite:t:`Doschek2004`. +width_doppler = (wavelength + _width).to(u.km / u.s, equivalencies=_eq) diff --git a/esis/flights/f1/spectrum/__init__.py b/esis/flights/f1/spectrum/__init__.py new file mode 100644 index 0000000..8ba9b89 --- /dev/null +++ b/esis/flights/f1/spectrum/__init__.py @@ -0,0 +1,11 @@ +"""Properties of the spectral lines observed during this flight.""" + +from . import O_V +from . import Mg_X +from . import He_I + +__all__ = [ + "O_V", + "Mg_X", + "He_I", +] diff --git a/pyproject.toml b/pyproject.toml index 2b6433b..83f42c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ authors = [ ] description = "A Python library for modeling and interpreting data from the EUV Snapshot Imaging Spectrograph (ESIS)." readme = "README.md" -requires-python = ">=3.11" +requires-python = ">=3.12" classifiers = [ "Programming Language :: Python :: 3", ] @@ -22,6 +22,7 @@ dependencies = [ "named-arrays~=1.0", "optika~=1.1", "msfc-ccd~=1.0", + "solar-dynamics-observatory~=0.2", ] dynamic = ["version"]