Spectrograph Demo

In addition to supporting simulations with that apply filters to provide bandfluxes, LightCurveLynx also provides the ability to generate spectra at different time steps.

Spectrograph

The core class used for generating spectra is the Spectrograph class, which defines information on the bins and sensitivity of the instrument. A user defines a spectrograph instrument by passing an array of bin centers (wavelength in Angstroms) and an optional array of per-bin scales.

[1]:
import matplotlib.pyplot as plt
import numpy as np

from lightcurvelynx.astro_utils.spectrograph import Spectrograph

bin_centers = np.arange(4000, 8000, 100)
my_spectrograph = Spectrograph(bin_centers)

The spectrograph’s evaluate() function is then used to turn a model’s SED into spectrograph readings.

Let’s look at a concrete function where we evaluate the spectrograph of sncosmo’s SALT2 model.

[2]:
from lightcurvelynx.models.sncosmo_models import SncosmoWrapperModel

model = SncosmoWrapperModel(
    "salt2-h17",  # Model name
    t0=53370.5,
    x0=100.0,
    x1=0.01,
    c=0.001,
    ra=0.0,
    dec=0.0,
    redshift=0.05,
    node_label="source",
)
/home/docs/checkouts/readthedocs.org/user_builds/lightcurvelynx/envs/latest/lib/python3.12/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
  from .autonotebook import tqdm as notebook_tqdm
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
File ~/checkouts/readthedocs.org/user_builds/lightcurvelynx/envs/latest/lib/python3.12/site-packages/lightcurvelynx/models/sncosmo_models.py:78, in SncosmoWrapperModel.__init__(self, source_name, node_label, wave_extrapolation, time_extrapolation, seed, **kwargs)
     77 try:
---> 78     from sncosmo.models import get_source
     79 except ImportError as err:  # pragma: no cover

ModuleNotFoundError: No module named 'sncosmo'

The above exception was the direct cause of the following exception:

ImportError                               Traceback (most recent call last)
Cell In[2], line 3
      1 from lightcurvelynx.models.sncosmo_models import SncosmoWrapperModel
      2
----> 3 model = SncosmoWrapperModel(
      4     "salt2-h17",  # Model name
      5     t0=53370.5,
      6     x0=100.0,

File ~/checkouts/readthedocs.org/user_builds/lightcurvelynx/envs/latest/lib/python3.12/site-packages/citation_compass/citation.py:73, in CiteClass.__init_subclass__.<locals>.init_wrapper(*args, **kwargs)
     69 @wraps(original_init)
     70 def init_wrapper(*args, **kwargs):
     71     # Save the citation as USED when it is first called.
     72     CITATION_COMPASS_REGISTRY.mark_used(cls._citation_compass_name)
---> 73     return original_init(*args, **kwargs)

File ~/checkouts/readthedocs.org/user_builds/lightcurvelynx/envs/latest/lib/python3.12/site-packages/lightcurvelynx/models/sncosmo_models.py:80, in SncosmoWrapperModel.__init__(self, source_name, node_label, wave_extrapolation, time_extrapolation, seed, **kwargs)
     78     from sncosmo.models import get_source
     79 except ImportError as err:  # pragma: no cover
---> 80     raise ImportError(
     81         "sncosmo package is not installed by default. To use the SncosmoWrapperModel, "
     82         "please install sncosmo. For example, you can install it with "
     83         "`pip install sncosmo` or `conda install conda-forge::sncosmo`."
     84     ) from err
     86 # We explicitly ask for and pass along the PhysicalModel parameters such
     87 # as node_label and wave_extrapolation so they do not go into kwargs
     88 # and get added to the sncosmo model below.
     89 super().__init__(
     90     node_label=node_label,
     91     wave_extrapolation=wave_extrapolation,
   (...)     94     **kwargs,
     95 )

ImportError: sncosmo package is not installed by default. To use the SncosmoWrapperModel, please install sncosmo. For example, you can install it with `pip install sncosmo` or `conda install conda-forge::sncosmo`.

We can evaluate the modeled SEDs at a few times (2 days before t0, t0, 2 days after t0, 5 days after t0). We use the bin’s wavelength centers as our evaluation points.

[3]:
times = 53370.5 + np.array([-5.0, 0.0, 5.0, 10.0])
seds = model.evaluate_sed(times, my_spectrograph.waves)
spectra = my_spectrograph.evaluate(seds)

plt.plot(my_spectrograph.waves, spectra[0], marker=".", linestyle=":", label="t=-5.0")
plt.plot(my_spectrograph.waves, spectra[1], marker=".", linestyle=":", label="t=0.0")
plt.plot(my_spectrograph.waves, spectra[2], marker=".", linestyle=":", label="t=5.0")
plt.plot(my_spectrograph.waves, spectra[3], marker=".", linestyle=":", label="t=10.0")
plt.xlabel("Wavelength (Angstrom)")
plt.ylabel("Flux")
plt.title("Spectral Energy Distribution of SN at Different Times")
_ = plt.legend()
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[3], line 2
      1 times = 53370.5 + np.array([-5.0, 0.0, 5.0, 10.0])
----> 2 seds = model.evaluate_sed(times, my_spectrograph.waves)
      3 spectra = my_spectrograph.evaluate(seds)
      4
      5 plt.plot(my_spectrograph.waves, spectra[0], marker=".", linestyle=":", label="t=-5.0")

NameError: name 'model' is not defined

Using in Simulations

We can include spectrograph readings into our normal simulation workflow by adding an ObsTable with the spectrograph’s pointings and a “PassbandGroup” that is just the spectrograph instrument itself. While any ObsTable subclass can be used (all that matters is the pointing information), we have provided a minimal SpectrographObsTable that uses a tighter radius and does not require zero point or filter information.

We start by creating a fake observation table that includes points at our object and away from our object.

[4]:
from lightcurvelynx.obstable.spectrograph_table import SpectrographObsTable

data = {
    "ra": [0.0, 10.0, 0.0, 0.0, 10.0, 0.0],
    "dec": [0.0, -10.0, 0.0, 0.0, -10.0, 0.0],
    "time": 53370.5 + np.array([-5.0, -2.0, 0.0, 5.0, 7.0, 10.0]),
}
osbtable = SpectrographObsTable(data)

As of version 0.4.0, we wrap all the survey related information in a SurveyInfo object.

[5]:
from lightcurvelynx.survey_info import SurveyInfo

survey_info = SurveyInfo(
    obstable=osbtable,
    passbands=my_spectrograph,  # The spectrograph is used as the passband information for the survey.
    survey_name="spectrograph",
)

We can then just pass the model, survey information, and spectrograph to the simulation function.

[6]:
from lightcurvelynx.simulate import simulate_lightcurves

results = simulate_lightcurves(
    model,  # The model we are simulating.
    1,  # The number of simulations to run,
    survey_info,  # The survey information, which includes the observation times and passbands.
)
results.head()
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[6], line 4
      1 from lightcurvelynx.simulate import simulate_lightcurves
      2
      3 results = simulate_lightcurves(
----> 4     model,  # The model we are simulating.
      5     1,  # The number of simulations to run,
      6     survey_info,  # The survey information, which includes the observation times and passbands.
      7 )

NameError: name 'model' is not defined

The results look like the table for a normal simulation, but contain an additional “spectra” nested column. Each row of the nested data contains the time, instrument, list of wavelengths, and list of measured values.

Out of the six observations in our fake table, four of them part pointed at the object. So the results include 4 spectra.

[7]:
row_0_spectra_table = results.iloc[0]["spectra"]
row_0_spectra_table
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[7], line 1
----> 1 row_0_spectra_table = results.iloc[0]["spectra"]
      2 row_0_spectra_table

NameError: name 'results' is not defined

We can plot the resulting spectra. As expected, they are identical to the manually generated spectra.

[8]:
row_0_spectra_table = results.iloc[0]["spectra"]
for row_idx in range(len(row_0_spectra_table)):
    plt.plot(
        row_0_spectra_table.iloc[row_idx]["waves"],
        row_0_spectra_table.iloc[row_idx]["measured_flux"],
        marker=".",
        linestyle=":",
        label=f"t={row_0_spectra_table.iloc[row_idx]['mjd']:.2f}",
    )
plt.xlabel("Wavelength (Angstrom)")
plt.ylabel("Flux (nJy)")
plt.title("Spectral Energy Distribution of SN at Different Times")
_ = plt.legend()
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[8], line 1
----> 1 row_0_spectra_table = results.iloc[0]["spectra"]
      2 for row_idx in range(len(row_0_spectra_table)):
      3     plt.plot(
      4         row_0_spectra_table.iloc[row_idx]["waves"],

NameError: name 'results' is not defined

Caveats

The spectrograph code is still in development and does not simulate noise yet.