Earth Observation Imaging Opportunities
In this example we'll find upcoming imaging opportunities for the ICEYE constellation over San Francisco (lon: -122.4194, lat: 37.7749), subject to specific imaging constraints.
Setup
First, we'll import the necessary libraries, initialize Earth orientation parameters, download the latest TLE data for all active spacecraft and filter it to select just the ICEYE spacecraft:
| import time
import csv
import os
import pathlib
import sys
import brahe as bh
import numpy as np
bh.initialize_eop()
|
We download all active satellites from CelesTrak and filter for ICEYE spacecraft:
| # Get all active satellites and filter for ICEYE
all_sats = bh.datasets.celestrak.get_tles_as_propagators("active", 60.0)
iceye_sats = [sat for sat in all_sats if "ICEYE" in sat.get_name().upper()]
|
Constellation Visualization
Before getting further into the analysis, it's useful to visualize the 3D geometry of the constellation. We propagate each satellite for one orbital period and create a 3D visualization:
| for sat in iceye_sats:
orbital_period = bh.orbital_period(sat.semi_major_axis)
sat.propagate_to(sat.epoch + orbital_period)
# Create 3D constellation visualization
fig_3d = bh.plot_trajectory_3d(
[
{
"trajectory": sat.trajectory,
"mode": "lines",
"line_width": 1.5,
"label": sat.get_name(),
}
for sat in iceye_sats
],
units="km",
show_earth=True,
earth_texture="natural_earth_50m",
backend="plotly",
view_azimuth=45.0,
view_elevation=30.0,
view_distance=2.0,
)
|
The resulting plot shows the ICEYE constellation orbits in 3D space:
Target Location Definition
We define San Francisco as our imaging target:
| # Define San Francisco target location
san_francisco = bh.PointLocation(
lon=-122.4194, # longitude in degrees
lat=37.7749, # latitude in degrees
alt=0.0, # altitude in meters
).with_name("San Francisco")
|
Constraint Specification
In this case, we want to collect a descending-pass, right-looking image collected from between 35 and 45 degrees off-nadir angle. We compose these requirements using Brahe's constraint system:
| constraint = bh.ConstraintAll(
constraints=[
bh.AscDscConstraint(allowed=bh.AscDsc.DESCENDING),
bh.LookDirectionConstraint(allowed=bh.LookDirection.RIGHT),
bh.OffNadirConstraint(min_off_nadir_deg=35.0, max_off_nadir_deg=45.0),
]
)
|
This creates a composite constraint that requires all three conditions to be satisfied simultaneously:
AscDscConstraint: Filters for descending passes only LookDirectionConstraint: Requires right-looking geometry OffNadirConstraint: Limits imaging angle to 35-45° off-nadir
Compute Collection Opportunities
Now we'll compute all imaging opportunities between the constellation and San Francisco over a 7-day period:
| epoch_start = iceye_sats[0].epoch
epoch_end = epoch_start + 7 * 86400.0 # 7 days in seconds
# Propagate all satellites for full 7-day period
for sat in iceye_sats:
sat.propagate_to(epoch_end)
# Compute access windows
windows = bh.location_accesses(
[san_francisco], iceye_sats, epoch_start, epoch_end, constraint
)
|
Below is a table of the first 10 imaging opportunities. Click on any column header to sort:
| Spacecraft | Start Time (UTC) | End Time (UTC) | Duration (sec) | Off-Nadir Angle (deg) |
| ICEYE-X25 | 2026-01-03 12:39:25 | 2026-01-03 12:41:55 | 149.7 | 36.9 |
| ICEYE-X58 | 2026-01-03 12:39:25 | 2026-01-03 12:43:06 | 220.8 | 36.3 |
| ICEYE-X59 | 2026-01-03 12:39:25 | 2026-01-03 12:50:26 | 660.9 | 39.2 |
| ICEYE-X61 | 2026-01-03 12:39:25 | 2026-01-03 12:41:12 | 106.8 | 35.6 |
| ICEYE-X62 | 2026-01-03 12:39:25 | 2026-01-03 12:50:34 | 668.1 | 39.3 |
| ICEYE-X35 | 2026-01-03 12:40:32 | 2026-01-03 12:53:16 | 764.6 | 40 |
| ICEYE-X39 | 2026-01-03 12:45:26 | 2026-01-03 12:58:54 | 808.1 | 40 |
| ICEYE-X60 | 2026-01-03 12:46:10 | 2026-01-03 12:59:06 | 775.9 | 40 |
| ICEYE-X4 | 2026-01-03 12:46:32 | 2026-01-03 12:53:16 | 403.3 | 40 |
| ICEYE-X34 | 2026-01-03 12:48:03 | 2026-01-03 13:00:38 | 755.3 | 40 |
Full Code Example
| imaging_opportunities.py |
|---|
| import time
import csv
import os
import pathlib
import sys
import brahe as bh
import numpy as np
bh.initialize_eop()
# Configuration for output files
SCRIPT_NAME = pathlib.Path(__file__).stem
OUTDIR = pathlib.Path(os.getenv("BRAHE_FIGURE_OUTPUT_DIR", "./docs/figures/"))
os.makedirs(OUTDIR, exist_ok=True)
# Download TLE data for ICEYE constellation from CelesTrak
# ICEYE operates a constellation of SAR (Synthetic Aperture Radar) satellites
print("Downloading ICEYE constellation TLEs from CelesTrak...")
start_time = time.time()
# Get all active satellites and filter for ICEYE
all_sats = bh.datasets.celestrak.get_tles_as_propagators("active", 60.0)
iceye_sats = [sat for sat in all_sats if "ICEYE" in sat.get_name().upper()]
elapsed = time.time() - start_time
print(f"Loaded {len(iceye_sats)} ICEYE satellites in {elapsed:.2f} seconds.")
if len(iceye_sats) == 0:
print("ERROR: No ICEYE satellites found in active constellation data.")
print(
"This may indicate the satellites are not currently in the CelesTrak database."
)
sys.exit(1)
# Print satellite information
print("\nICEYE Constellation:")
for i, sat in enumerate(iceye_sats[:5]): # Show first 5
print(f" {i + 1}. {sat.get_name()}")
print(f" Epoch: {sat.epoch}")
print(f" Semi-major axis: {sat.semi_major_axis / 1000:.1f} km")
if len(iceye_sats) > 5:
print(f" ... and {len(iceye_sats) - 5} more")
# Propagate all satellites for one orbital period for visualization
print("\nPropagating constellation for visualization...")
start_time = time.time()
for sat in iceye_sats:
orbital_period = bh.orbital_period(sat.semi_major_axis)
sat.propagate_to(sat.epoch + orbital_period)
# Create 3D constellation visualization
fig_3d = bh.plot_trajectory_3d(
[
{
"trajectory": sat.trajectory,
"mode": "lines",
"line_width": 1.5,
"label": sat.get_name(),
}
for sat in iceye_sats
],
units="km",
show_earth=True,
earth_texture="natural_earth_50m",
backend="plotly",
view_azimuth=45.0,
view_elevation=30.0,
view_distance=2.0,
)
elapsed = time.time() - start_time
print(f"Created 3D visualization in {elapsed:.2f} seconds.")
# Reset propagators for access computation
print("\nResetting propagators for access computation...")
for sat in iceye_sats:
sat.reset()
# Define San Francisco target location
san_francisco = bh.PointLocation(
lon=-122.4194, # longitude in degrees
lat=37.7749, # latitude in degrees
alt=0.0, # altitude in meters
).with_name("San Francisco")
print(f"\nTarget Location: {san_francisco.get_name()}")
print(f" Longitude: {san_francisco.lon:.4f}°")
print(f" Latitude: {san_francisco.lat:.4f}°")
# Define composite imaging constraint
# Requirements:
# - Descending pass only
# - Right-looking geometry
# - Off-nadir angle between 35-45 degrees
print("\nDefining imaging constraints:")
print(" - Descending pass only")
print(" - Right-looking geometry")
print(" - Off-nadir angle: 35-45 degrees")
constraint = bh.ConstraintAll(
constraints=[
bh.AscDscConstraint(allowed=bh.AscDsc.DESCENDING),
bh.LookDirectionConstraint(allowed=bh.LookDirection.RIGHT),
bh.OffNadirConstraint(min_off_nadir_deg=35.0, max_off_nadir_deg=45.0),
]
)
# Compute imaging opportunities over 7-day period
print("\nComputing 7-day imaging opportunities...")
start_time = time.time()
epoch_start = iceye_sats[0].epoch
epoch_end = epoch_start + 7 * 86400.0 # 7 days in seconds
# Propagate all satellites for full 7-day period
for sat in iceye_sats:
sat.propagate_to(epoch_end)
# Compute access windows
windows = bh.location_accesses(
[san_francisco], iceye_sats, epoch_start, epoch_end, constraint
)
elapsed = time.time() - start_time
print(f"Computed {len(windows)} imaging opportunities in {elapsed:.2f} seconds.")
# Print sample of imaging opportunities
print("\n" + "=" * 90)
print("Sample Imaging Opportunities (first 10)")
print("=" * 90)
print(
f"{'Spacecraft':<20} {'Start Time':<25} {'End Time':<25} {'Duration':>10} {'Off-Nadir':>10}"
)
print("-" * 90)
for i, window in enumerate(windows[:10]):
duration_sec = window.duration
off_nadir = (
window.properties.off_nadir_max - window.properties.off_nadir_min
) / 2 + window.properties.off_nadir_min
start_str = str(window.start).split(".")[0] # Remove fractional seconds
end_str = str(window.end).split(".")[0]
print(
f"{window.satellite_name:<20} {start_str:<25} {end_str:<25} {duration_sec:>8.1f} s {off_nadir:>8.1f}°"
)
print("=" * 90)
# Export ~10 imaging opportunities to CSV for documentation
csv_path = OUTDIR / f"{SCRIPT_NAME}_windows.csv"
with open(csv_path, "w", newline="") as csvfile:
writer = csv.writer(csvfile)
writer.writerow(
[
"Spacecraft",
"Start Time (UTC)",
"End Time (UTC)",
"Duration (sec)",
"Off-Nadir Angle (deg)",
]
)
for window in windows[:10]: # Only export first 10 for documentation
duration_sec = window.duration
off_nadir = (
window.properties.off_nadir_max - window.properties.off_nadir_min
) / 2 + window.properties.off_nadir_min
start_str = str(window.start).split(".")[0] # Remove fractional seconds
end_str = str(window.end).split(".")[0]
writer.writerow(
[
window.satellite_name,
start_str,
end_str,
f"{duration_sec:.1f}",
f"{off_nadir:.1f}",
]
)
print(f"✓ Exported first 10 imaging opportunities to {csv_path}")
# Print statistics
unique_spacecraft = len(set(w.satellite_name for w in windows))
print("\nImaging Opportunity Statistics:")
print(f" Total opportunities: {len(windows)}")
print(f" Spacecraft with opportunities: {unique_spacecraft}")
print(f" Average duration: {np.mean([w.duration for w in windows]):.1f} seconds")
print(f" Total imaging time: {sum([w.duration for w in windows]):.1f} seconds")
|
See Also