Skip to content

SSN Sensor Datasets

Overview

The SSN sensor dataset provides representative locations, field-of-view limits, and calibration (bias/noise) values for U.S. Space Surveillance Network sites. This data is essential for:

  • Simulating radar/optical tracking: Build SimpleSSNSensor instances that generate az/el/range measurements consistent with a matching AzElRangeMeasurementModel
  • Access analysis: Determine when a sensor's field of view covers a target orbit
  • Orbit determination testing: Exercise EKF/UKF/BLS estimators against a realistic, multi-site sensor network

Brahe includes embedded GeoJSON data for 21 SSN sites. The data is:

  • Offline-capable: No network requests required
  • Calibrated: Includes bias and noise values for sensors with published Table 4-4 entries
  • Wrap-aware: Azimuth field-of-view windows that cross north are represented correctly

Loading

Load all SSN sensor sites, filter by sensor type, and inspect a site's properties:

import brahe as bh

# Initialize EOP data
bh.initialize_eop()

# Load all SSN sensor sites
sites = bh.datasets.ssn_sensors.load()
print(f"Total SSN sites: {len(sites)}")

# Filter by sensor type: radar/phased-array/mechanical trackers report
# az/el/range, optical trackers report angles-only az/el
radars = [s for s in sites if s.properties["sensor_type"] == "azel_range"]
optical = [s for s in sites if s.properties["sensor_type"] == "azel"]
print(f"Radar/phased-array/mechanical sites: {len(radars)}")
print(f"Optical (angles-only) sites: {len(optical)}")

# Inspect one site's properties
eglin = next(s for s in sites if s.get_name() == "Eglin")
props = eglin.properties
print(f"\n{eglin.get_name()}")
print(f"Location: ({eglin.lat:.2f}, {eglin.lon:.2f})")
print(f"System: {props['system']}")
print(f"Category: {props['category']}")
print(f"Elevation limits: {props.get('el_min_deg')} - {props.get('el_max_deg')} deg")
print(f"Range max: {props.get('range_max_m') / 1e3:.0f} km")
print(f"Azimuth noise: {props.get('az_noise_deg')} deg")

assert len(sites) == 21
assert len(radars) + len(optical) == len(sites)
assert eglin.properties["sensor_type"] == "azel_range"
print("\nExample validated successfully!")
use brahe as bh;
use bh::datasets::ssn_sensors::load_ssn_sensors;
use bh::utils::Identifiable;
use bh::AccessibleLocation;

fn main() {
    bh::initialize_eop().unwrap();

    // Load all SSN sensor sites
    let sites = load_ssn_sensors().unwrap();
    println!("Total SSN sites: {}", sites.len());

    // Filter by sensor type: radar/phased-array/mechanical trackers report
    // az/el/range, optical trackers report angles-only az/el
    let radars: Vec<_> = sites
        .iter()
        .filter(|s| s.properties()["sensor_type"] == "azel_range")
        .collect();
    let optical: Vec<_> = sites
        .iter()
        .filter(|s| s.properties()["sensor_type"] == "azel")
        .collect();
    println!("Radar/phased-array/mechanical sites: {}", radars.len());
    println!("Optical (angles-only) sites: {}", optical.len());

    // Inspect one site's properties
    let eglin = sites.iter().find(|s| s.get_name() == Some("Eglin")).unwrap();
    let props = eglin.properties();
    println!("\n{}", eglin.get_name().unwrap());
    println!("Location: ({:.2}, {:.2})", eglin.lat(), eglin.lon());
    println!("System: {}", props["system"]);
    println!("Category: {}", props["category"]);
    println!(
        "Elevation limits: {} - {} deg",
        props["el_min_deg"], props["el_max_deg"]
    );
    println!(
        "Range max: {:.0} km",
        props["range_max_m"].as_f64().unwrap() / 1e3
    );
    println!("Azimuth noise: {} deg", props["az_noise_deg"]);

    assert_eq!(sites.len(), 21);
    assert_eq!(radars.len() + optical.len(), sites.len());
    assert_eq!(props["sensor_type"], "azel_range");
    println!("\nExample validated successfully!");
}
Output
Total SSN sites: 21
Radar/phased-array/mechanical sites: 15
Optical (angles-only) sites: 6

Eglin
Location: (30.57, -86.21)
System: Phased Array
Category: dedicated
Elevation limits: 1.0 - 90.0 deg
Range max: 13210 km
Azimuth noise: 0.0154 deg

Example validated successfully!
Total SSN sites: 21
Radar/phased-array/mechanical sites: 15
Optical (angles-only) sites: 6

Eglin
Location: (30.57, -86.21)
System: "Phased Array"
Category: "dedicated"
Elevation limits: 1.0 - 90.0 deg
Range max: 13210 km
Azimuth noise: 0.0154 deg

Example validated successfully!

bh.datasets.ssn_sensors.load() returns every site as a PointLocation. SimpleSSNSensor.from_locations() builds a sensor for every site -- radar (azel_range, measuring az/el/range) and optical (azel, angles-only az/el) alike -- defaulting the sites that lack full Table 4-4 calibration to zero noise and bias (flagged calibrated == False, overridable with with_noise()/with_bias()). from_locations_calibrated() restricts the result to the fully-calibrated sites -- this is the set used throughout the SSN Radar Tracking example.

Properties

Each site is a PointLocation with geodetic coordinates (lon(), lat(), alt()) and a properties dictionary:

Field Type Units Description
sensor_type str -- "azel_range" (radar/phased-array/mechanical trackers, az/el/range) or "azel" (angles-only optical trackers, az/el)
system str -- Sensor system description, e.g. "Phased Array", "Radar", "GEODSS"
category str -- Vallado network category, e.g. "dedicated", "collateral", "contributing"
sensor_numbers list[int] -- SSN sensor ID number(s) at the site
az_min_deg float, optional degrees Azimuth field-of-view start. Wrap-aware: when az_min_deg > az_max_deg, the window crosses north
az_max_deg float, optional degrees Azimuth field-of-view end
el_min_deg float, optional degrees Minimum elevation angle
el_max_deg float, optional degrees Maximum elevation angle
range_max_m float, optional meters Maximum range; absent means effectively unlimited
az_bias_deg float, optional degrees Constant azimuth measurement bias (Table 4-4)
el_bias_deg float, optional degrees Constant elevation measurement bias
range_bias_m float, optional meters Constant range measurement bias
az_noise_deg float, optional degrees Azimuth measurement noise standard deviation
el_noise_deg float, optional degrees Elevation measurement noise standard deviation
range_noise_m float, optional meters Range measurement noise standard deviation

sensor_type determines which fields are present: azel sites carry no range fields at all, and sites appearing only in Table 4-2 (location and field-of-view, no calibration) carry no bias/noise fields. SimpleSSNSensor.from_location() accepts both azel_range and azel sites; a site missing one or more noise fields still constructs, defaulted to zero noise and flagged uncalibrated, rather than raising an error.

Building Sensors and Measurement Models

Build a sensor from a single site, generate a measurement, and inspect the matching AzElRangeMeasurementModel that measurement_model() builds from the sensor's own bias and noise -- the model and the sensor stay consistent because both read the same calibration:

import numpy as np

import brahe as bh

# Initialize EOP data
bh.initialize_eop()

# Load a fully-calibrated radar site and build a sensor from it
sites = bh.datasets.ssn_sensors.load()
eglin_site = next(s for s in sites if s.get_name() == "Eglin")
sensor = bh.SimpleSSNSensor.from_location(eglin_site, seed=42)
print(f"Sensor: {sensor.name}")
print(f"Azimuth window: {sensor.az_min:.1f} - {sensor.az_max:.1f} deg")
print(f"Elevation limits: {sensor.el_min:.1f} - {sensor.el_max:.1f} deg")
print(f"Range max: {sensor.range_max / 1e3:.0f} km")
print(f"Calibrated: {sensor.calibrated}")

# Build the matching measurement model: same bias/noise as the sensor, so a
# filter built from it stays consistent with measurements the sensor produces
model = sensor.measurement_model()
print(f"Measurement model: {model.name()}")

# A target 500 km away, due south (within Eglin's southwest-facing azimuth
# window) and 45 deg above the horizon, built by offsetting the site in the
# local East-North-Zenith frame and converting to ECI.
epoch = bh.Epoch(2024, 1, 1, 0, 0, 0.0)
az, el, rng = np.radians(180.0), np.radians(45.0), 500e3
horizontal = rng * np.cos(el)
enz_offset = np.array(
    [horizontal * np.sin(az), horizontal * np.cos(az), rng * np.sin(el)]
)
target_ecef = bh.relative_position_enz_to_ecef(
    eglin_site.center_ecef(), enz_offset, bh.EllipsoidalConversionType.GEODETIC
)
state_eci = bh.state_ecef_to_eci(epoch, np.concatenate([target_ecef, np.zeros(3)]))

# True (noise-free, bias-free) geometry vs. a simulated measurement
truth = sensor.azelrange(epoch, state_eci)
print(
    f"\nTrue az/el/range: [{truth[0]:.2f} deg, {truth[1]:.2f} deg, {truth[2] / 1e3:.1f} km]"
)

measurement = sensor.measure(epoch, state_eci)
print(
    f"Measured az/el/range: [{measurement[0]:.2f} deg, {measurement[1]:.2f} deg, "
    f"{measurement[2] / 1e3:.1f} km]"
)

assert measurement is not None, "Target inside the field of view should be visible"
assert abs(measurement[0] - truth[0]) < 1.0, "azimuth should stay close to truth"
assert abs(measurement[1] - truth[1]) < 1.0, "elevation should stay close to truth"
assert abs(measurement[2] - truth[2]) < 5000.0, "range should stay close to truth"
print("\nExample validated successfully!")
use brahe as bh;
use bh::datasets::ssn_sensors::load_ssn_sensors;
use bh::estimation::SimpleSSNSensor;
use bh::utils::Identifiable;
use bh::AccessibleLocation;
use nalgebra::{SVector, Vector3};

fn main() {
    bh::initialize_eop().unwrap();

    // Load a fully-calibrated radar site and build a sensor from it
    let sites = load_ssn_sensors().unwrap();
    let eglin_site = sites
        .iter()
        .find(|s| s.get_name() == Some("Eglin"))
        .unwrap();
    let mut sensor = SimpleSSNSensor::from_location(eglin_site)
        .unwrap()
        .with_seed(42);
    println!("Sensor: {}", sensor.name());
    println!(
        "Azimuth window: {:.1} - {:.1} deg",
        sensor.az_min(),
        sensor.az_max()
    );
    println!(
        "Elevation limits: {:.1} - {:.1} deg",
        sensor.el_min(),
        sensor.el_max()
    );
    println!("Range max: {:.0} km", sensor.range_max().unwrap() / 1e3);
    println!("Calibrated: {}", sensor.calibrated());

    // Build the matching measurement model: same bias/noise as the sensor,
    // so a filter built from it stays consistent with measurements the
    // sensor produces.
    let model = sensor.measurement_model();
    println!("Measurement model: {}", model.name());

    // A target 500 km away, due south (within Eglin's southwest-facing
    // azimuth window) and 45 deg above the horizon, built by offsetting the
    // site in the local East-North-Zenith frame and converting to ECI.
    let epoch = bh::Epoch::from_datetime(2024, 1, 1, 0, 0, 0.0, 0.0, bh::TimeSystem::UTC);
    let (az, el, rng) = (180.0f64.to_radians(), 45.0f64.to_radians(), 500e3);
    let horizontal = rng * el.cos();
    let enz_offset = Vector3::new(horizontal * az.sin(), horizontal * az.cos(), rng * el.sin());
    let target_ecef = bh::relative_position_enz_to_ecef(
        eglin_site.center_ecef(),
        enz_offset,
        bh::EllipsoidalConversionType::Geodetic,
    );
    let state_ecef = SVector::<f64, 6>::new(
        target_ecef[0],
        target_ecef[1],
        target_ecef[2],
        0.0,
        0.0,
        0.0,
    );
    let state_eci = bh::state_ecef_to_eci(epoch, state_ecef);
    let state_eci_dvec = nalgebra::DVector::from_column_slice(state_eci.as_slice());

    // True (noise-free, bias-free) geometry vs. a simulated measurement
    let truth = sensor.azelrange(&epoch, &state_eci_dvec);
    println!(
        "\nTrue az/el/range: [{:.2} deg, {:.2} deg, {:.1} km]",
        truth[0],
        truth[1],
        truth[2] / 1e3
    );

    let measurement = sensor.measure(&epoch, &state_eci_dvec).unwrap();
    println!(
        "Measured az/el/range: [{:.2} deg, {:.2} deg, {:.1} km]",
        measurement[0],
        measurement[1],
        measurement[2] / 1e3
    );

    assert!(
        (measurement[0] - truth[0]).abs() < 1.0,
        "azimuth should stay close to truth"
    );
    assert!(
        (measurement[1] - truth[1]).abs() < 1.0,
        "elevation should stay close to truth"
    );
    assert!(
        (measurement[2] - truth[2]).abs() < 5000.0,
        "range should stay close to truth"
    );
    println!("\nExample validated successfully!");
}
Output
Sensor: Eglin
Azimuth window: 145.0 - 215.0 deg
Elevation limits: 1.0 - 90.0 deg
Range max: 13210 km
Calibrated: True
Measurement model: AzElRange

True az/el/range: [180.00 deg, 45.00 deg, 500.0 km]
Measured az/el/range: [180.01 deg, 45.01 deg, 500.0 km]

Example validated successfully!
Sensor: Eglin
Azimuth window: 145.0 - 215.0 deg
Elevation limits: 1.0 - 90.0 deg
Range max: 13210 km
Calibrated: true
Measurement model: AzElRange

True az/el/range: [180.00 deg, 45.00 deg, 500.0 km]
Measured az/el/range: [180.01 deg, 45.01 deg, 500.0 km]

Example validated successfully!

See Azimuth/Elevation/Range Measurements for how the resulting model is used in a filter.

Source

Values are from Vallado, Fundamentals of Astrodynamics and Applications, 4th Ed., Tables 4-2 (site locations and systems), 4-3 (field-of-view limits), and 4-4 (bias/noise calibration). NAVSPASUR is excluded from the embedded dataset. These values are representative and dated -- they reflect the published tables, not current SSN configuration or performance, and should not be used for operational sensor modeling.


See Also