CCD-Level ObsTables

This notebook describes how to load and use survey data that is provided at a CCD level. While using CCD-level data provides additional complexities, it can allow more accurate simulation by simulating finer grained noise characteristics.

In this notebook we use a subsampling of the Rubin DP1 CCDVisit table. This data is included in LightCurveLynx’s github with the testing data.

Loading The (Subsampled) DP1 CCDVisits

We start by loading the subsample of the DP1 CCDVisits from the testing directory. As you can see from the first 5 rows of the loaded table, the times (expMidptMJD) are all identical. This is because each row represents a single CCD at a single time.

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

# We provide a relative path so the notebook renders on readthedocs. In practice, you would
# load the table from your local filesystem or a remote source.
table_file = "../../tests/lightcurvelynx/data/dp1_ccdvisit_subsampled.parquet"
pdf = pd.read_parquet(table_file)

pdf.head()
[1]:
ccdVisitId expMidptMJD ra dec band skyRotation magLim seeing skyBg skyNoise pixelScale xSize ySize zeroPoint
9 1145019889152 60623.259329 53.004909 -28.191185 r 102.799341 24.5735 0.896197 889.033020 32.938400 0.200393 4071 3999 32.071701
10 1145019889153 60623.259329 53.064737 -27.962022 r 102.799341 24.6319 0.873288 904.945984 29.584101 0.200324 4071 3999 32.073002
11 1145019889154 60623.259329 53.124301 -27.732423 r 102.799341 24.6158 0.898227 886.961975 31.682600 0.200390 4071 3999 32.072102
12 1145019889155 60623.259329 53.265134 -28.243372 r 102.799341 24.6035 0.903278 884.517029 32.218601 0.200328 4071 3999 32.071098
13 1145019889156 60623.259329 53.324200 -28.014305 r 102.799341 24.6334 0.889979 896.465027 32.111099 0.200265 4071 3999 32.073200

We can plot all of the pointings (all CCDs, all times).

[2]:
_ = plt.scatter(pdf["ra"], pdf["dec"])
plt.xlabel("RA")
plt.ylabel("Dec")
plt.title("CCD Visit Positions")
plt.show()
../_images/notebooks_ccd_obstables_4_0.png

We can also look at the pointings for a single time step. Here we see nine coordinates denoting the center of each CCD.

[3]:
first_time_inds = np.abs(pdf["expMidptMJD"] - 60623.259329) < 1e-6
one_timestep_pdf = pdf[first_time_inds]

_ = plt.scatter(one_timestep_pdf["ra"], one_timestep_pdf["dec"])
plt.xlabel("RA")
plt.ylabel("Dec")
plt.title("CCD Visit Positions for a Single Visit")
plt.show()
../_images/notebooks_ccd_obstables_6_0.png

CCDs without Detector Footprints

By default ObsTable will use a circular radius to determine which points fall within the viewing area of each pointing. While this is a fast and reasonable approximation for full images (all CCDs), it can lead to problems when the pointings are provided on a per-CCD level.

To see this, let’s load our single time step into an LSSTObsTable with make_detector_footprint set to False. Note that this will give us a warning because we are loading CCD-level information.

[4]:
from lightcurvelynx.obstable.lsst_obstable import LSSTObsTable

obs_table = LSSTObsTable.from_ccdvisit_table(one_timestep_pdf, make_detector_footprint=False)
/home/docs/checkouts/readthedocs.org/user_builds/lightcurvelynx/envs/latest/lib/python3.12/site-packages/lightcurvelynx/obstable/lsst_obstable.py:288: UserWarning: Not creating a detector footprint for a CCD pointing table. This may lead to duplicate results for points near the edges of the CCDs.
  warnings.warn(

Now we look at a grid of (RA, Dec) points to see which ones are observed. The range_search() function is what LightCurveLynx’s simulation uses to determine when each sampled (RA, Dec) is observed.

[5]:
ra, dec = np.meshgrid(np.linspace(52.5, 54, 50), np.linspace(-27.5, -28.5, 50))
ra = ra.flatten()
dec = dec.flatten()

# Find the matches with the default radius for an LSST CCD, which is ~0.1574 degrees.
matching_inds = obs_table.range_search(ra, dec)
num_matches = np.array([len(matches) for matches in matching_inds])

for count in np.unique(num_matches):
    print(f"Number of points with {count} matches: {(num_matches == count).sum()}")

    plt.scatter(ra[num_matches == count], dec[num_matches == count], label=f"{count} matches")

plt.xlabel("RA")
plt.ylabel("Dec")
plt.title("CCD Visit Positions for a Single Visit")
plt.legend()
plt.show()
Number of points with 0 matches: 1470
Number of points with 1 matches: 781
Number of points with 2 matches: 249
../_images/notebooks_ccd_obstables_10_1.png

As we can see, a fair number of points are observed multiple times. This is incorrect and an artifact of using a circular radius. In reality, each point will only be observed by a single CCD.

CCDs with Detector Footprints

The ObsTable has the ability to use a detector footprint in the range search. While you can manually set a detector footprint, the LSSTObsTable.from_ccdvisit_table can also automatically generate one from the known information about Rubin’s CCDs. For more information on detector footprints, see the detector footprint notebook.

When we rerun the range search, we now see the expected behavior. We even see chip gaps!

[6]:
obs_table2 = LSSTObsTable.from_ccdvisit_table(one_timestep_pdf, make_detector_footprint=True)

# Find the matches with the default radius for an LSST CCD, which is ~0.1574 degrees.
matching_inds = obs_table2.range_search(ra, dec)
num_matches = np.array([len(matches) for matches in matching_inds])

for count in np.unique(num_matches):
    print(f"Number of points with {count} matches: {(num_matches == count).sum()}")

    plt.scatter(ra[num_matches == count], dec[num_matches == count], label=f"{count} matches")

plt.xlabel("RA")
plt.ylabel("Dec")
plt.title("CCD Visit Positions for a Single Visit")
plt.legend()
plt.show()
Number of points with 0 matches: 1694
Number of points with 1 matches: 806
../_images/notebooks_ccd_obstables_13_1.png

Conclusion

When simulating from CCD-level pointing information, users will almost always want to apply a detector footprint.