Footprint Analysis
The across.tools.footprint module provides classes and functions for working with
astronomical instrument fields of view (footprints). It enables projection of detector
geometries onto the celestial sphere and spatial queries using HEALPix pixelization.
Overview
The footprint module supports:
Footprint Definition: Define instrument footprints as collections of polygon detectors
Sky Projection: Project footprints to specific sky coordinates with arbitrary roll angles
HEALPix Queries: Determine which HEALPix pixels are covered by a footprint
Spatial Joins: Compute overlaps, unions, and differences between multiple footprints
This functionality is essential for:
Planning tiled observations
Computing sky coverage
Cross-matching observations with catalogs
Determining overlap between different instruments or pointings
Core Data Types
Coordinate
The Coordinate class represents a point on the celestial sphere.
from across.tools import Coordinate
# Create a coordinate
coord = Coordinate(ra=180.0, dec=45.0)
print(coord) # Coordinate(ra=180.0, dec=45.0)
Parameter |
Type |
Description |
|---|---|---|
|
float |
Right ascension in degrees (-360 to 360, normalized to 0-360) |
|
float |
Declination in degrees (-90 to 90) |
Polygon
The Polygon class represents a spherical polygon defined by a list of coordinates.
from across.tools import Coordinate, Polygon
# Define a square region on the sky
square = Polygon(coordinates=[
Coordinate(ra=0.0, dec=0.0),
Coordinate(ra=1.0, dec=0.0),
Coordinate(ra=1.0, dec=1.0),
Coordinate(ra=0.0, dec=1.0),
# First coordinate is automatically appended if not provided
])
# Define a triangular region
triangle = Polygon(coordinates=[
Coordinate(ra=10.0, dec=10.0),
Coordinate(ra=11.0, dec=10.0),
Coordinate(ra=10.5, dec=11.0),
])
Note
Polygons must have at least 3 unique coordinates. The first coordinate is automatically appended to close the polygon if not already present.
Requirement |
Description |
|---|---|
Minimum vertices |
At least 3 unique coordinates |
Closure |
First and last coordinates must match (auto-appended if missing) |
Winding order |
Counter-clockwise for proper HEALPix queries |
Footprint Class
The Footprint class represents an instrument’s imaging footprint, which can consist
of one or more detector polygons.
Creating a Footprint
from across.tools import Coordinate, Polygon
from across.tools.footprint import Footprint
# Single detector footprint (e.g., a simple CCD)
detector = Polygon(coordinates=[
Coordinate(ra=-0.5, dec=-0.5),
Coordinate(ra=0.5, dec=-0.5),
Coordinate(ra=0.5, dec=0.5),
Coordinate(ra=-0.5, dec=0.5),
])
single_footprint = Footprint(detectors=[detector])
# Multi-detector footprint (e.g., a mosaic camera)
detector1 = Polygon(coordinates=[
Coordinate(ra=-1.0, dec=-0.5),
Coordinate(ra=0.0, dec=-0.5),
Coordinate(ra=0.0, dec=0.5),
Coordinate(ra=-1.0, dec=0.5),
])
detector2 = Polygon(coordinates=[
Coordinate(ra=0.0, dec=-0.5),
Coordinate(ra=1.0, dec=-0.5),
Coordinate(ra=1.0, dec=0.5),
Coordinate(ra=0.0, dec=0.5),
])
mosaic_footprint = Footprint(detectors=[detector1, detector2])
Note
When defining footprints for projection, coordinates should be specified as offsets from the boresight (pointing center) in degrees. The origin (0, 0) represents the instrument boresight.
Projecting Footprints
The project() method projects a footprint onto the celestial sphere at a specified
position and roll angle.
from across.tools import Coordinate
from across.tools.footprint import Footprint
# Define footprint centered at origin
footprint = Footprint(detectors=[
Polygon(coordinates=[
Coordinate(ra=-0.25, dec=-0.25),
Coordinate(ra=0.25, dec=-0.25),
Coordinate(ra=0.25, dec=0.25),
Coordinate(ra=-0.25, dec=0.25),
])
])
# Project to a specific sky position
target = Coordinate(ra=180.0, dec=45.0)
roll_angle = 0.0 # degrees
projected = footprint.project(coordinate=target, roll_angle=roll_angle)
# Access projected detector coordinates
for detector in projected.detectors:
print("Detector vertices:")
for coord in detector.coordinates:
print(f" RA={coord.ra:.4f}°, Dec={coord.dec:.4f}°")
Roll Angle
The roll angle rotates the footprint around the boresight axis:
# Project with different roll angles
roll_0 = footprint.project(coordinate=target, roll_angle=0.0)
roll_45 = footprint.project(coordinate=target, roll_angle=45.0)
roll_90 = footprint.project(coordinate=target, roll_angle=90.0)
Parameter |
Type |
Description |
|---|---|---|
|
Coordinate |
Sky position to project the footprint center to |
|
float |
Rotation angle around the boresight in degrees (-360 to 360) |
HEALPix Pixel Queries
The query_pixels() method returns the HEALPix pixel indices covered by the footprint.
# Query pixels at HEALPix order 10 (default)
pixels = projected.query_pixels()
print(f"Footprint covers {len(pixels)} pixels at order 10")
# Query at different orders
pixels_low = projected.query_pixels(order=6) # Coarse resolution
pixels_high = projected.query_pixels(order=12) # Fine resolution
print(f"Order 6: {len(pixels_low)} pixels")
print(f"Order 12: {len(pixels_high)} pixels")
HEALPix Order and Resolution
Order |
NSIDE |
Pixel Area |
Typical Use |
|---|---|---|---|
6 |
64 |
~0.84 deg² |
Large-scale surveys |
8 |
256 |
~0.05 deg² |
Wide-field instruments |
10 |
1024 |
~0.003 deg² (~12 arcmin²) |
Standard queries (default) |
12 |
4096 |
~0.0002 deg² (~0.7 arcmin²) |
High-resolution instruments |
Parameter |
Type |
Description |
|---|---|---|
|
int |
HEALPix order (1-12, default: 10) |
Footprint Containment
The contains() method checks if a coordinate lies inside a footprint.
from across.tools import Coordinate, Polygon
from across.tools.footprint import Footprint
footprint = Footprint(detectors=[
Polygon(coordinates=[
Coordinate(ra=-0.25, dec=-0.25),
Coordinate(ra=0.25, dec=-0.25),
Coordinate(ra=0.25, dec=0.25),
Coordinate(ra=-0.25, dec=0.25),
])
])
target = Coordinate(ra=180.0, dec=45.0)
projected = footprint.project(coordinate=target, roll_angle=0.0)
inside = projected.contains(Coordinate(ra=180.0, dec=45.0), order=10)
print(f"Target inside footprint: {inside}")
Parameter |
Type |
Description |
|---|---|---|
|
Coordinate |
Sky position to test for containment |
|
int |
HEALPix order (1-12, default: 10) |
Footprint Plotting
The plot() method renders footprint outlines on a Plotly sky projection.
from across.tools import Coordinate, Polygon
from across.tools.footprint import Footprint
footprint = Footprint(detectors=[
Polygon(coordinates=[
Coordinate(ra=-0.25, dec=-0.25),
Coordinate(ra=0.25, dec=-0.25),
Coordinate(ra=0.25, dec=0.25),
Coordinate(ra=-0.25, dec=0.25),
])
])
fig = footprint.plot(name="Detector A", color="red", lat_axis_tick=30, lon_axis_tick=60)
fig.show()
Parameter |
Type |
Description |
|---|---|---|
|
go.Figure | None |
Existing figure to add traces to |
|
str | None |
Legend label for detector traces |
|
str | None |
Line color for detector traces |
|
int |
Latitude axis tick interval |
|
int |
Longitude axis tick interval |
HEALPix Spatial Joins
The footprint module provides functions for computing spatial relationships between multiple footprints using HEALPix pixels.
Inner Join (Intersection)
The inner() function returns pixels that are covered by all footprints
(intersection/overlap region).
from across.tools import Coordinate, Polygon
from across.tools.footprint import Footprint, inner
# Helper function to create a projected footprint at a given position
def create_footprint_at(ra: float, dec: float, roll_angle: float = 0.0) -> Footprint:
"""Create a 0.5° x 0.5° footprint projected to the given sky position."""
detector = Polygon(coordinates=[
Coordinate(ra=-0.25, dec=-0.25),
Coordinate(ra=0.25, dec=-0.25),
Coordinate(ra=0.25, dec=0.25),
Coordinate(ra=-0.25, dec=0.25),
])
base_footprint = Footprint(detectors=[detector])
return base_footprint.project(coordinate=Coordinate(ra=ra, dec=dec), roll_angle=roll_angle)
# Two overlapping footprints
footprint1 = create_footprint_at(ra=180.0, dec=45.0)
footprint2 = create_footprint_at(ra=180.1, dec=45.0) # Slightly offset
# Find overlapping pixels
overlap_pixels = inner([footprint1, footprint2], order=10)
print(f"Overlap region: {len(overlap_pixels)} pixels")
Outer Join (Non-overlapping)
The outer() function returns pixels that are covered by exactly one footprint
(non-overlapping regions).
from across.tools.footprint import outer
# Find non-overlapping pixels
unique_pixels = outer([footprint1, footprint2], order=10)
print(f"Non-overlapping region: {len(unique_pixels)} pixels")
Union (All Coverage)
The union() function returns all unique pixels covered by any of the footprints.
from across.tools.footprint import union
# Find total coverage
all_pixels = union([footprint1, footprint2], order=10)
print(f"Total coverage: {len(all_pixels)} pixels")
Join Function Summary
Function |
Returns |
Use Case |
|---|---|---|
|
Pixels in ALL footprints |
Find overlap regions |
|
Pixels in EXACTLY ONE footprint |
Find unique coverage |
|
Pixels in ANY footprint |
Find total sky coverage |
Example: Tiled Observations
from across.tools import Coordinate, Polygon
from across.tools.footprint import Footprint, inner, outer, union
# Define a 0.5° x 0.5° detector
detector = Polygon(coordinates=[
Coordinate(ra=-0.25, dec=-0.25),
Coordinate(ra=0.25, dec=-0.25),
Coordinate(ra=0.25, dec=0.25),
Coordinate(ra=-0.25, dec=0.25),
])
base_footprint = Footprint(detectors=[detector])
# Create a 3x3 tiled observation pattern
tiles = []
for i in range(3):
for j in range(3):
target = Coordinate(ra=180.0 + i * 0.4, dec=45.0 + j * 0.4)
projected = base_footprint.project(coordinate=target, roll_angle=0.0)
tiles.append(projected)
# Analyze coverage
total_coverage = union(tiles, order=10)
overlap_regions = inner(tiles, order=10)
print(f"Total coverage: {len(total_coverage)} pixels")
print(f"Overlap regions: {len(overlap_regions)} pixels")
print(f"Coverage efficiency: {len(total_coverage) / (len(total_coverage) + len(overlap_regions)) * 100:.1f}%")
Working with Real Instruments
Example: Simple CCD Camera
from across.tools import Coordinate, Polygon
from across.tools.footprint import Footprint
# 10 arcmin x 10 arcmin CCD
fov = 10.0 / 60.0 # Convert arcmin to degrees
half_fov = fov / 2
ccd = Polygon(coordinates=[
Coordinate(ra=-half_fov, dec=-half_fov),
Coordinate(ra=half_fov, dec=-half_fov),
Coordinate(ra=half_fov, dec=half_fov),
Coordinate(ra=-half_fov, dec=half_fov),
])
camera = Footprint(detectors=[ccd])
# Point at M31
m31 = Coordinate(ra=10.6847, dec=41.2690)
observation = camera.project(coordinate=m31, roll_angle=0.0)
Example: Multi-CCD Mosaic Camera
from across.tools import Coordinate, Polygon
from across.tools.footprint import Footprint
# 2x2 CCD mosaic with gaps
ccd_size = 0.5 # degrees
gap = 0.05 # degrees between CCDs
detectors = []
for i in range(2):
for j in range(2):
# Calculate CCD center position
x_offset = (i - 0.5) * (ccd_size + gap)
y_offset = (j - 0.5) * (ccd_size + gap)
# Define CCD corners
ccd = Polygon(coordinates=[
Coordinate(ra=x_offset - ccd_size/2, dec=y_offset - ccd_size/2),
Coordinate(ra=x_offset + ccd_size/2, dec=y_offset - ccd_size/2),
Coordinate(ra=x_offset + ccd_size/2, dec=y_offset + ccd_size/2),
Coordinate(ra=x_offset - ccd_size/2, dec=y_offset + ccd_size/2),
])
detectors.append(ccd)
mosaic_camera = Footprint(detectors=detectors)
print(f"Camera has {len(mosaic_camera.detectors)} detectors")
Example: Irregular Detector Shape
from across.tools import Coordinate, Polygon
from across.tools.footprint import Footprint
# L-shaped detector
l_shape = Polygon(coordinates=[
Coordinate(ra=-0.5, dec=-0.5),
Coordinate(ra=0.0, dec=-0.5),
Coordinate(ra=0.0, dec=0.0),
Coordinate(ra=0.5, dec=0.0),
Coordinate(ra=0.5, dec=0.5),
Coordinate(ra=-0.5, dec=0.5),
])
instrument = Footprint(detectors=[l_shape])
Computing Sky Coverage Statistics
import healpy as hp
from across.tools.footprint import Footprint, union
def compute_coverage_stats(footprints: list[Footprint], order: int = 10) -> dict:
"""Compute sky coverage statistics for a list of footprints."""
# Get total pixels covered
all_pixels = union(footprints, order=order)
# Calculate sky area
nside = hp.order2nside(order)
npix_total = hp.nside2npix(nside)
pixel_area_deg2 = hp.nside2pixarea(nside, degrees=True)
coverage_area = len(all_pixels) * pixel_area_deg2
coverage_fraction = len(all_pixels) / npix_total
return {
"num_pixels": len(all_pixels),
"area_deg2": coverage_area,
"area_arcmin2": coverage_area * 3600,
"sky_fraction": coverage_fraction,
"sky_fraction_percent": coverage_fraction * 100,
}
# Example usage
stats = compute_coverage_stats(tiles, order=10)
print(f"Coverage: {stats['area_deg2']:.4f} deg² ({stats['sky_fraction_percent']:.6f}% of sky)")
Coordinate Systems and Conventions
Footprint Definition Coordinates
When defining a footprint (before projection), coordinates represent offsets from the instrument boresight:
ra: Offset in the RA direction (positive = East)dec: Offset in the Dec direction (positive = North)Origin (0, 0): Instrument boresight/pointing center
Projected Coordinates
After projection, coordinates are absolute sky positions in the ICRS frame:
ra: Right ascension (0° to 360°)dec: Declination (-90° to 90°)
Roll Angle Convention
Roll angle = 0°: Detector “up” direction aligned with celestial North
Positive roll: Counter-clockwise rotation when viewed from behind the telescope
API Reference
See the API Reference for complete class and function documentation.