Skip to content

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:

1
2
3
4
5
6
7
8
9
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:

1
2
3
# 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:

1
2
3
4
5
6
# 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:

1
2
3
4
5
6
7
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