TraceScene Class Example¶
The TraceScene
class is a way to work with multiple PSFs that make up a spectrum trace. You will need to use a PSF to create a trace from, and inside that PSF object there is the information about the trace pixel, wavelength, and sensitivity distributions.
from pandorapsf import TraceScene, PSF
import matplotlib.pyplot as plt
import numpy as np
import pandorasat as ps
import astropy.units as u
p = PSF.from_name("NIRDA").freeze_dimension(row=0, column=0)
p
1D PSF Model [wavelength] (Frozen: row: 0.000, column: 0.000)
This PSF object is a function of wavelength. We can look at the trace sensitivity as a function of pixel and wavelength.
fig, ax = plt.subplots()
im = ax.scatter(p.trace_pixel.value, p.trace_wavelength.value, c=p.trace_sensitivity.value, cmap='coolwarm_r', vmin=0, vmax=p.trace_sensitivity.max().value, s=1)
cbar = plt.colorbar(im, ax=ax)
cbar.set_label(f"Sensitivity [{p.trace_sensitivity.unit.to_string('latex')}]")
ax.set(ylabel='Wavelength [$\mu$m]', xlabel='Pixel Position', title='Sensitivity of NIR Detector');
This looks like what we expect. This information is baked into the PSF object. This will be used by the TraceScene
class. Let's build one
ts = TraceScene(locations=np.asarray([(200, 40)]), psf=p, shape=(400, 80), corner=(0, 0))
ts
TraceScene Object [1D PSF Model [wavelength] (Frozen: row: 0.000, column: 0.000)]
Here I've made a TraceScene
object where the source is located at (200, 40) in the image. The default wavelength spacing for TraceScene
is one PSF element per quarter pixel. You can change the grid it is evaluated at using the wavelenth
keyword argument. Let's see how many wavelength elements there are in the TraceScene
ts.nwav
880
ts.wavelength
There are many hundreds of elements in this default. This makes the TraceScene
object slower to calculate than if it had a lower wavelength resolution.
We need a spectrum to create an image. We can do this using pandorasat
's SED function, which will return the SED of a target of given effective temperature and magnitude.
target_wavelength, target_spectrum = ps.utils.SED(teff=5777, jmag=13)
fig, ax = plt.subplots()
ax.plot(target_wavelength, target_spectrum, c='k')
ax.set(xlabel='Wavelength [nm]', ylabel=f"Flux [{target_spectrum.unit.to_string('latex')}]", title='Example Target Spectrum');
This is at a different resolution and wavelength range than the TraceScene
object. We need to integrate this spectrum onto the wavelength grid of the TraceScene
object. Let's do that
integrated_spec = ts.integrate_spectrum(*ps.utils.SED(teff=5777, jmag=13))
fig, ax = plt.subplots()
ax.plot(ts.wavelength, integrated_spec, c='k')
ax.set(xlabel='Wavelength [$\mu$m]', ylabel='Flux [e$^{-1}$/s]', title='Example Target Spectrum');
This is now the integrated spectrum in each wavelength element of the trace scene, accounting for Pandora's sensitivity and the NIR detector QE. Let's make this into an image on the detector.
images = ts.model(spectra=integrated_spec)
This contains one image for a single time stamp. Let's plot it
fig, ax = plt.subplots(figsize=(6, 6))
ax.imshow(images[0].value, vmin=0, vmax=10)
ax.set(xlabel='Column [pixel]', ylabel='Row [pixel]', title='Example Trace');
This image has units of $e^{-1}/s$. We can then use this image to calculate models of the detector response.
Just like other Scene
classes this class can be used to create images of traces over time.
nt = 10
images = ts.model(spectra=integrated_spec[:, None, None] * np.arange(nt))
images.shape
(10, 400, 80)
Above I've created spectra that get brighter over 10 time stamps. If we plot the central pixel we see the brightness increasing.
fig, ax = plt.subplots()
ax.plot(images[:, 200, 40], c='k')
ax.set(xlabel='Frame Number', ylabel='Brightness [$e^{-1}$/s]')
[Text(0.5, 0, 'Frame Number'), Text(0, 0.5, 'Brightness [$e^{-1}$/s]')]
You can also create spectra that have motion in them
nt = 10
delta_position = np.random.uniform(-3, 3, (2, nt)).T
images = ts.model(spectra=integrated_spec[:, None, None] * np.ones(nt), delta_pos=delta_position)
Modeling Pixel Positions: 100%|███████████████████| 2/2 [00:01<00:00, 1.91it/s]
images.shape
(10, 400, 80)
Now we have 10 images with the same spectrum, but different positions over time. If we difference image the first and last image we see this change.
fig, ax = plt.subplots(figsize=(6, 6))
ax.imshow(images[0].value - images[1].value)
ax.set(xlabel='Column [pixel]', ylabel='Row [pixel]', title='First Frame - Last Frame');
You can also create a TraceScene
with multiple targets. Let's create a new scene with two targets.
ts = TraceScene(locations=np.asarray([(200, 40), (170, 35)]), psf=p, shape=(400, 80), corner=(0, 0))
Here I'll create spectra for the two targets
target1 = ts.integrate_spectrum(*ps.utils.SED(teff=5777, jmag=13))
target2 = ts.integrate_spectrum(*ps.utils.SED(teff=3500, jmag=14))
images = ts.model(np.vstack([target1, target2]).T)
images.shape
(1, 400, 80)
This has created one image, because we only passed in one time stamp of spectra
fig, ax = plt.subplots(figsize=(6, 6))
ax.imshow(images[0].value, vmin=0, vmax=10)
ax.set(xlabel='Column [pixel]', ylabel='Row [pixel]', title='Example Trace');
Now we have two spectra in our image, one for each target. We can similarly change the spectra for each timestamp, or add in motion to the scene.
nt = 10
delta_position = np.random.uniform(-3, 3, (2, nt)).T
images = ts.model(spectra=np.vstack([target1, target2]).T[:, :, None] * np.ones(nt), delta_pos=delta_position)
Modeling Pixel Positions: 100%|███████████████████| 2/2 [00:02<00:00, 1.00s/it]
fig, ax = plt.subplots(figsize=(6, 6))
ax.imshow(images[0].value - images[1].value)
ax.set(xlabel='Column [pixel]', ylabel='Row [pixel]', title='First Frame - Last Frame');