101. LSSTCam focal plane geometry#
101.1 LSSTCam Focal Plane Geometry¶
For the Rubin Science Platform at data.lsst.cloud.
Data Release: Data Preview 2
Container Size: large
LSST Science Pipelines version: r29.2.0
Last verified to run: 2026-06-03
Repository: github.com/lsst/tutorial-notebooks
DOI: 10.11578/rubin/dc.20250909.20
Learning objective: Explore the geometry of the LSSTCam focal plane using the lsst.afw.cameraGeom package.
LSST data products: Camera geometry (accessed via the LSST Science Pipelines).
Packages: lsst.obs.lsst, lsst.afw.cameraGeom, lsst.geom, matplotlib
Credit: Originally developed by the Rubin Community Science team. Based on code from Plazas Malagón et al. 2025, CTN-001. Please consider acknowledging them if this notebook is used for the preparation of journal articles, software releases, or other notebooks.
Get Support: Everyone is encouraged to ask questions or raise issues in the Rubin Community Forum. Rubin staff will respond to all questions posted there.
1. Introduction¶
The LSST Camera (LSSTCam) is a 3.2 gigapixel camera mounted on the Simonyi Survey Telescope at the Vera C. Rubin Observatory. Its focal plane contains 189 science Charge-Coupled Devices (CCDs) arranged in 21 rafts, plus 8 wavefront sensors and 8 guider sensors housed in 4 corner rafts, for a total of 205 detectors. The focal plane covers a 9.6 square-degree field of view, with each CCD having approximately 4000 x 4000 pixels at a scale of 0.2 arcseconds per pixel. Each CCD is read out through 16 amplifiers.
The science CCDs are manufactured by two vendors: ITL (Imaging Technology Laboratory, University of Arizona) and e2v (Teledyne). All nine science detectors within a given raft are from the same vendor.
The lsst.afw.cameraGeom package in the LSST Science Pipelines provides tools for working with the camera's geometry.
It allows access to detector properties (name, ID, physical type, amplifier layout) and coordinate transformations between pixel coordinates and focal plane coordinates (in millimeters).
This tutorial demonstrates how to use lsst.afw.cameraGeom to explore LSSTCam's focal plane layout and create a visualization similar to Figure 1 of CTN-001 (Plazas Malagón et al., 2025).
For more information, see the LSSTCam (DOI: 10.71929/rubin/2571927), lsst.afw.geom, lsst.awf.cameraGeom documentation, and the lsst.afw repository.
1.1. Import packages¶
Import the packages needed for this tutorial.
matplotlib is a widely-used Python plotting library. Here, matplotlib.pyplot provides the plotting interface, and matplotlib.patches provides geometric shapes (rectangles) for drawing focal plane elements.
lsst.obs.lsst contains the instrument-specific definitions for Rubin Observatory cameras. LsstCam provides access to the LSSTCam camera geometry.
lsst.afw.cameraGeom is the LSST Science Pipelines package for camera geometry.
It defines the PIXELS and FOCAL_PLANE coordinate systems and provides methods to access detector properties and perform coordinate transformations.
PIXELS refers to the pixel coordinate system of individual detectors, while FOCAL_PLANE refers to the physical coordinate system of the full focal plane in millimeters.
lsst.geom provides geometric primitives.
Box2D represents a 2D bounding box in floating-point coordinates, and Point2D represents a 2D point.
import re
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import matplotlib.patches as mpatches
from lsst.obs.lsst import LsstCam
from lsst.afw.cameraGeom import PIXELS, FOCAL_PLANE
from lsst.geom import Box2D, Point2D
1.2. Define parameters and functions¶
Set the colorblind-friendly plotting style.
plt.style.use('seaborn-v0_8-colorblind')
2. LSSTCam camera geometry¶
Retrieve the LSSTCam camera object using LsstCam.getCamera().
This camera object contains all the information about the focal plane layout, including every detector, its position, its amplifiers, and coordinate transformations.
camera = LsstCam.getCamera()
print(f"Camera name: {camera.getName()}")
print(f"Number of detectors: {len(camera)}")
Camera name: LSSTCam Number of detectors: 205
3. Detector properties¶
The camera object is iterable: each element is a Detector object. Examine the properties available for each detector.
3.1. Inspect a single detector¶
Access a single detector by its index and examine its key properties.
Each detector has a name (e.g., R22_S11), a numeric ID, a physical type indicating the CCD vendor (e.g., ITL or E2V), and a set of amplifiers.
det = camera[94]
print(f"Detector name: {det.getName()}")
print(f"Detector ID: {det.getId()}")
print(f"Physical type: {det.getPhysicalType()}")
print(f"Number of amplifiers: {len(list(det))}")
Detector name: R22_S11 Detector ID: 94 Physical type: E2V Number of amplifiers: 16
The detector name follows the convention Rxx_Syy, where Rxx identifies the raft and Syy identifies the sensor within that raft.
For the corner rafts (R00, R04, R40, R44), sensor names use SG for guider sensors and SW for wavefront sensors.
3.2. Detector corners¶
Each detector can report its corner positions in focal plane coordinates (in millimeters). The getCorners(FOCAL_PLANE) method returns the four corners of the detector in the focal plane coordinate system.
corners = det.getCorners(FOCAL_PLANE)
for i, corner in enumerate(corners):
x_mm = format(corner.getX(), '.2f')
y_mm = format(corner.getY(), '.2f')
print(f"Corner {i}: x = {x_mm} mm, y = {y_mm} mm")
Corner 0: x = -20.48 mm, y = -20.02 mm Corner 1: x = 20.48 mm, y = -20.02 mm Corner 2: x = 20.48 mm, y = 20.02 mm Corner 3: x = -20.48 mm, y = 20.02 mm
3.3. Amplifier information¶
Each detector is read out through 16 amplifiers. Iterating over a Detector object yields its amplifier objects.
Each amplifier has a name (e.g., C00, C01, ..., C17) and a bounding box in pixel coordinates.
for amp in det:
bbox = amp.getBBox()
print(f"Amplifier {amp.getName()}: "
f"{bbox.getWidth()} x {bbox.getHeight()} pixels")
Amplifier C10: 512 x 2002 pixels Amplifier C11: 512 x 2002 pixels Amplifier C12: 512 x 2002 pixels Amplifier C13: 512 x 2002 pixels Amplifier C14: 512 x 2002 pixels Amplifier C15: 512 x 2002 pixels Amplifier C16: 512 x 2002 pixels Amplifier C17: 512 x 2002 pixels Amplifier C07: 512 x 2002 pixels Amplifier C06: 512 x 2002 pixels Amplifier C05: 512 x 2002 pixels Amplifier C04: 512 x 2002 pixels Amplifier C03: 512 x 2002 pixels Amplifier C02: 512 x 2002 pixels Amplifier C01: 512 x 2002 pixels Amplifier C00: 512 x 2002 pixels
4. Group detectors by raft¶
Parse the detector names to group them by raft. This grouping will be used to draw raft outlines in the focal plane visualization. The raft name is extracted from the first part of the detector name (before the underscore).
raft_dict = {}
for det in camera:
det_name = det.getName()
match = re.match(r'^(R\d{2})_([A-Z0-9]+)$', det_name)
if match:
raft_name = match.group(1)
else:
raft_name = det_name.split('_')[0]
raft_dict.setdefault(raft_name, []).append(det)
print(f"Number of rafts: {len(raft_dict)}")
for raft_name in sorted(raft_dict.keys()):
print(f" {raft_name}: {len(raft_dict[raft_name])} detectors")
Number of rafts: 25 R00: 4 detectors R01: 9 detectors R02: 9 detectors R03: 9 detectors R04: 4 detectors R10: 9 detectors R11: 9 detectors R12: 9 detectors R13: 9 detectors R14: 9 detectors R20: 9 detectors R21: 9 detectors R22: 9 detectors R23: 9 detectors R24: 9 detectors R30: 9 detectors R31: 9 detectors R32: 9 detectors R33: 9 detectors R34: 9 detectors R40: 4 detectors R41: 9 detectors R42: 9 detectors R43: 9 detectors R44: 4 detectors
The 21 science rafts each contain 9 sensors (in a 3x3 grid), while the 4 corner rafts each contain 2 wavefront sensors and 2 guider sensors.
5. Coordinate transformations¶
The lsst.afw.cameraGeom package provides coordinate transformations between the pixel coordinate system of each detector and the focal plane coordinate system (in millimeters).
Use det.getTransform(PIXELS, FOCAL_PLANE) to obtain a transform object that can convert pixel positions to focal plane positions.
Demonstrate this by converting the bounding box of an amplifier from pixel coordinates to focal plane coordinates.
det = camera[94]
transform = det.getTransform(PIXELS, FOCAL_PLANE)
amp = list(det)[0]
bbox = amp.getBBox()
ll_pixel = Point2D(bbox.getMinX(), bbox.getMinY())
ur_pixel = Point2D(bbox.getMaxX(), bbox.getMaxY())
ll_fp = transform.applyForward(ll_pixel)
ur_fp = transform.applyForward(ur_pixel)
ll_px_x = format(ll_pixel.getX(), '.0f')
ll_px_y = format(ll_pixel.getY(), '.0f')
ur_px_x = format(ur_pixel.getX(), '.0f')
ur_px_y = format(ur_pixel.getY(), '.0f')
ll_fp_x = format(ll_fp.getX(), '.2f')
ll_fp_y = format(ll_fp.getY(), '.2f')
ur_fp_x = format(ur_fp.getX(), '.2f')
ur_fp_y = format(ur_fp.getY(), '.2f')
print(f"Amplifier {amp.getName()} of {det.getName()}: ")
print(f" Pixel coords: ({ll_px_x}, {ll_px_y}) "
f"to ({ur_px_x}, {ur_px_y})")
print(f" Focal plane coords: ({ll_fp_x}, {ll_fp_y}) "
f"to ({ur_fp_x}, {ur_fp_y}) mm")
Amplifier C10 of R22_S11: Pixel coords: (0, 2002) to (511, 4003) Focal plane coords: (-20.48, 0.00) to (-15.37, 20.02) mm
6. Visualize the LSSTCam focal plane¶
Create a visualization of the full LSSTCam focal plane layout. The plot will show:
- Raft outlines (computed as the bounding box enclosing all detectors in each raft).
- Individual detector (CCD) outlines.
- Amplifier segment boundaries within each detector.
- Color-coding by CCD vendor/type: ITL (vermillion), e2v (blue), guider (purple), and wavefront (green).
- Labels for each raft and sensor.
This produces a figure similar to Figure 1 of CTN-001.
6.1. Define helper functions¶
Define helper functions that will be used to produce the focal plane plot.
get_sensor_color: returns a colorblind-friendly color based on the detector's physical type.get_focal_plane_bbox: computes the bounding box of a detector in focal plane coordinates.plot_box: draws a rectangular patch on a matplotlib axes.
def get_sensor_color(det):
"""Return a color based on the detector's physical type.
Parameters
----------
det : `lsst.afw.cameraGeom.Detector`
A detector object.
Returns
-------
color : `str`
A hex color string.
"""
ptype = det.getPhysicalType()
color_map = {
"ITL": "#D55E00",
"E2V": "#0072B2",
"ITL_G": "#CC79A7",
"ITL_WF": "#009E73",
}
return color_map.get(ptype, "#000000")
def get_focal_plane_bbox(detector):
"""Compute the bounding box of a detector in focal plane coordinates.
Parameters
----------
detector : `lsst.afw.cameraGeom.Detector`
A detector object.
Returns
-------
bbox : `lsst.geom.Box2D`
The bounding box in focal plane coordinates (mm).
"""
corners = detector.getCorners(FOCAL_PLANE)
xs = [pt.getX() for pt in corners]
ys = [pt.getY() for pt in corners]
return Box2D(Point2D(min(xs), min(ys)),
Point2D(max(xs), max(ys)))
def plot_box(ax, box, edgecolor='black', linewidth=1,
linestyle='-'):
"""Draw a rectangular patch defined by a Box2D on the given axes.
Parameters
----------
ax : `matplotlib.axes.Axes`
The axes to draw on.
box : `lsst.geom.Box2D`
The bounding box to draw.
edgecolor : `str`, optional
Edge color of the rectangle.
linewidth : `float`, optional
Line width of the rectangle.
linestyle : `str`, optional
Line style of the rectangle.
"""
min_pt = box.getMin()
max_pt = box.getMax()
width = max_pt.getX() - min_pt.getX()
height = max_pt.getY() - min_pt.getY()
rect = patches.Rectangle(
(min_pt.getX(), min_pt.getY()),
width, height,
edgecolor=edgecolor,
facecolor='none',
linewidth=linewidth,
linestyle=linestyle)
ax.add_patch(rect)
6.2. Plot the focal plane¶
Loop over all rafts and detectors to draw the focal plane. For each raft, compute the union bounding box of its detectors to draw the raft outline. For each detector, draw the CCD outline and subdivide it into its 16 amplifier segments using the pixel-to-focal-plane coordinate transformation.
Note: In 2025, an overcurrent consistent with a short occurring at the CCD was observed for detector
R30_S12. As a result, this CCD is non-operable and is shown blacked out in the focal plane plot below.
fig, ax = plt.subplots(figsize=(12, 12))
fp_bbox_total = None
for raft, det_list in raft_dict.items():
raft_color = get_sensor_color(det_list[0])
raft_bbox = None
for det in det_list:
bbox = get_focal_plane_bbox(det)
if raft_bbox is None:
raft_bbox = bbox
else:
raft_bbox.include(bbox)
if fp_bbox_total is None:
fp_bbox_total = raft_bbox
else:
fp_bbox_total.include(raft_bbox)
plot_box(ax, raft_bbox, edgecolor=raft_color, linewidth=2)
raft_center = raft_bbox.getCenter()
ax.text(raft_center.getX(), raft_center.getY(), raft,
color='black', fontsize=14, ha='center', va='center')
for det in det_list:
sensor_bbox = get_focal_plane_bbox(det)
det_color = get_sensor_color(det)
det_name = det.getName()
is_nonoperable = (det_name == "R30_S12")
if is_nonoperable:
min_pt = sensor_bbox.getMin()
max_pt = sensor_bbox.getMax()
ax.add_patch(patches.Rectangle(
(min_pt.getX(), min_pt.getY()),
max_pt.getX() - min_pt.getX(),
max_pt.getY() - min_pt.getY(),
edgecolor=det_color,
facecolor='black',
linewidth=0.5))
else:
plot_box(ax, sensor_bbox, edgecolor=det_color,
linewidth=0.5)
sensor_name = det_name.split('_')[1]
label_x = sensor_bbox.getMin().getX() + 2
label_y = sensor_bbox.getMax().getY() - 2
label_color = 'white' if is_nonoperable else 'black'
label_text = f"{sensor_name} ({det.getId()})"
ax.text(label_x, label_y, label_text,
color=label_color, fontsize=4, ha='left', va='top')
if is_nonoperable:
continue
transform = det.getTransform(PIXELS, FOCAL_PLANE)
for amp in det:
amp_bbox = amp.getBBox()
ll = amp_bbox.getCorners()[0]
ur = amp_bbox.getCorners()[2]
p0 = transform.applyForward(
Point2D(ll.getX(), ll.getY()))
p1 = transform.applyForward(
Point2D(ur.getX(), ur.getY()))
rect = patches.Rectangle(
(p0.getX(), p0.getY()),
p1.getX() - p0.getX(),
p1.getY() - p0.getY(),
edgecolor=det_color,
facecolor='none',
linewidth=0.3)
ax.add_patch(rect)
margin = 10.0
min_pt = fp_bbox_total.getMin()
max_pt = fp_bbox_total.getMax()
ax.set_xlim(min_pt.getX() - margin, max_pt.getX() + margin)
ax.set_ylim(min_pt.getY() - margin, max_pt.getY() + margin)
ax.set_title("LSSTCam Focal Plane Layout", fontsize=20)
ax.set_xlabel("Focal Plane X (mm)", fontsize=18)
ax.set_ylabel("Focal Plane Y (mm)", fontsize=18)
ax.tick_params(axis='both', which='major', labelsize=14)
ax.set_aspect('equal', 'box')
ax.grid(False)
legend_handles = [
mpatches.Patch(color="#D55E00", label="ITL"),
mpatches.Patch(color="#0072B2", label="e2v"),
mpatches.Patch(color="#CC79A7", label="Guider"),
mpatches.Patch(color="#009E73", label="Wavefront"),
]
ax.legend(handles=legend_handles, loc='upper right',
fontsize=10)
plt.show()
Figure 1: LSSTCam focal plane layout showing all 205 detectors grouped into 25 rafts. Raft outlines are drawn in the color corresponding to the CCD vendor type. Each CCD is labeled with its sensor name and detector ID. The 16 amplifier segments within each CCD are shown as subdivisions. ITL sensors are shown in orange, e2v sensors in blue, guider sensors in purple, and wavefront sensors in green. See CTN-001 for a more detailed version of this figure.
7. Exercises for the learner¶
- Modify the focal plane plot to label each sensor with its detector name (e.g.,
R22_S11) instead of just the sensor name and ID. - Create a version of the plot that only shows the 9 detectors in a single raft (e.g., R22), with amplifier names labeled inside each segment.
- Count the total number of ITL and e2v science sensors by iterating over the camera object and checking each detector's physical type with
getPhysicalType().