Domain F — Applied Case Studies & Mission Reasoning

SSA tracking logic — from sensor geometry to detectability gates to scalable architecture.

Domain F.7–F.9 — SSA & Tracking (Geometry → Detectability → Scaling)

These case studies translate “SSA visibility” into an engineering workflow. We start with the simplest question (“is it inside the FOV?”), then add what real sensors require (“can it be detected?”), and finally design the architecture needed to scale from one object to an operational catalog.

How to use this page

Treat each section as a reusable template. Replace the orbit, sensor FOV, pointing law, and thresholds — keep the reasoning chain and the event-based workflow.

F.7 — Sensor Geometry

Crossing events = pure geometry (no physics-of-detection yet)

Crossing answers only one question: “Does the object pass through the sensor’s geometric view?” It does not ask whether the object is bright enough, sunlit, or measurable.

1) The similar problem (mission-style question)

You are given a catalog object described by a TLE (propagated using SGP4 in a TEME / ECI-like frame), and a space-based tracking sensor with a known orbit (propagated via a two-body Kepler model or SGP4). The sensor is modeled as a conical field-of-view with a known half-angle (e.g., full-angle 30°, half-angle 15°), and a pointing rule such as: the boresight aligns with the tracker’s instantaneous velocity vector.

Question: Over the next 24 hours, determine all crossing events — the time intervals when the object is geometrically inside the sensor cone. This is not detection; it is only the “FOV membership” problem.

Figure placeholder: sensor orbit + boresight cone + object trajectory. Highlight crossing intervals when the LOS vector lies inside the cone.
Suggested: draw the sensor cone in inertial space and show LOS angle thresholding.

2) Definitions: what “crossing” means in SSA

A crossing event occurs when the target line-of-sight falls inside the sensor’s angular FOV cone about the boresight. Let:

  • Tracker position: $\mathbf{r}_s(t)$
  • Object position: $\mathbf{r}_o(t)$
  • Line-of-sight vector: $\boldsymbol{\rho}(t)=\mathbf{r}_o(t)-\mathbf{r}_s(t)$
  • Range: $\rho(t)=\lVert\boldsymbol{\rho}(t)\rVert$
  • Boresight unit vector: $\hat{\mathbf{b}}(t)$ (from pointing law; e.g., velocity direction)
  • FOV half-angle: $\theta_{\mathrm{FOV}}$ (e.g., $15^\circ$)

Compute the cone angle:

\[ \theta(t)=\cos^{-1}\left( \frac{\hat{\mathbf{b}}(t)\cdot \boldsymbol{\rho}(t)} {\lVert \boldsymbol{\rho}(t)\rVert} \right) \]

Crossing condition (geometry-only):

\[ \theta(t)\le \theta_{\mathrm{FOV}} \]

That’s the entire crossing logic — no sunlight constraint, no station-night constraint, no SNR / brightness model, and no measurement feasibility model.

Algorithm (F.7) — Crossing detection from state histories

  1. Propagate sensor and object states over $[t_0, t_0 + 24~\mathrm{h}]$.
  2. Compute $\boldsymbol{\rho}(t)$, $\rho(t)$, and boresight $\hat{\mathbf{b}}(t)$.
  3. Compute $\theta(t)$ from the dot-product definition.
  4. Generate boolean crossing series: $C(t)=\big(\theta(t)\le\theta_{\mathrm{FOV}}\big)$.
  5. Convert boolean samples into event intervals (AOS/LOS style).

3) Event extraction: turning samples into intervals

Operationally, you do not want a million True/False samples. You want intervals that can be scheduled, filtered, and stored efficiently. Create a time grid:

\[ t_k=t_0+k\Delta t \]

for 24 hours with a reasonable step (e.g., $\Delta t=10~\mathrm{s}$ for single-object analysis), and compute the boolean crossing sequence $C_k \in \{0,1\}$. Convert transitions into intervals:

  • Start: when $C_k$ goes False→True
  • End: when $C_k$ goes True→False

This yields “Crossing #1: $[t_{\mathrm{start}}, t_{\mathrm{end}}]$”, “Crossing #2: …”, etc. The key payoff is compute efficiency: downstream detectability checks are only evaluated inside crossing windows.

Why event intervals matter

Crossing intervals become your “candidate list.” Everything expensive (illumination models, higher-rate sampling, detection probability) should be executed only on these candidates.

4) Geometry pitfalls that break SSA pipelines (and how you guard them)

  • Frame mismatch: mixing inertial object states with Earth-fixed sensor coordinates can yield plausible but incorrect results. Guard by making the frame chain explicit (TEME/ECI-like throughout, or a correct ECI↔ECEF transform model).
  • Coarse time step: if $\Delta t$ is too large, short crossings vanish or boundary times drift. Guard by using a two-stage time strategy (coarse scan + refined boundary search).
  • Pointing law realism: “boresight aligns with velocity” is a baseline, but real sensors have slew limits, keep-out zones, duty cycles, and stabilization limits. Start ideal, then add constraints incrementally.

Python Snippets (F.7) — Crossing events

# pip install sgp4 numpy
import numpy as np
from sgp4.api import Satrec, jday

def make_time_grid(start_utc, duration_hours=24, dt_sec=10):
    """
    start_utc: (Y, M, D, h, m, s) tuple
    returns: (jd, fr) arrays for sgp4, plus seconds-from-start array
    """
    Y, M, D, hh, mm, ss = start_utc
    t0 = 0.0
    tf = duration_hours * 3600.0
    ts = np.arange(t0, tf + dt_sec, dt_sec, dtype=float)

    jd0, fr0 = jday(Y, M, D, hh, mm, ss)
    fr = fr0 + ts / 86400.0
    jd = np.full_like(fr, jd0, dtype=float)

    carry = np.floor(fr)
    fr = fr - carry
    jd = jd + carry
    return jd, fr, ts

def propagate_tle_positions(tle_line1, tle_line2, jd, fr):
    """
    Returns TEME position vectors r (N,3) in km.
    """
    sat = Satrec.twoline2rv(tle_line1, tle_line2)
    r = np.zeros((len(jd), 3), dtype=float)
    for k in range(len(jd)):
        e, rk, vk = sat.sgp4(jd[k], fr[k])
        r[k, :] = np.nan if e != 0 else rk
    return r

def boresight_from_velocity(v_s):
    """
    v_s: (N,3) velocity vectors (km/s)
    returns: (N,3) unit boresight vectors
    """
    norm = np.linalg.norm(v_s, axis=1, keepdims=True)
    return v_s / np.maximum(norm, 1e-12)

def crossing_boolean(r_obj, r_sens, b_hat, fov_half_angle_deg):
    """
    r_obj: (N,3) object position
    r_sens: (N,3) sensor position
    b_hat: (N,3) boresight unit vectors
    returns: boolean array C (N,)
    """
    rho = r_obj - r_sens
    rho_norm = np.linalg.norm(rho, axis=1)
    dot = np.einsum('ij,ij->i', b_hat, rho)
    cos_theta = dot / np.maximum(rho_norm, 1e-12)
    cos_theta = np.clip(cos_theta, -1.0, 1.0)
    theta = np.degrees(np.arccos(cos_theta))
    return theta <= fov_half_angle_deg

def boolean_to_intervals(ts_sec, flags):
    """
    ts_sec: (N,) seconds-from-start
    flags: (N,) boolean
    returns: list of (t_start, t_end) in seconds
    """
    flags = np.asarray(flags, dtype=bool)
    intervals = []
    if len(flags) == 0:
        return intervals

    in_event = False
    t_start = None
    for k in range(len(flags)):
        if (not in_event) and flags[k]:
            in_event = True
            t_start = ts_sec[k]
        elif in_event and (not flags[k]):
            in_event = False
            intervals.append((t_start, ts_sec[k]))
            t_start = None

    if in_event:
        intervals.append((t_start, ts_sec[-1]))
    return intervals

F.8 — Detectability Logic & Illumination

Crossing → Detection = geometry + illumination + range (operational truth)

A crossing event does not mean you can detect the object. Crossing ≠ detection. Detection is the subset of crossings that satisfy illumination and sensor feasibility constraints.

1) The key idea: “crossing ≠ detection”

SSA pipelines become misleading if they treat “inside the FOV” as “detected.” In practice, detection depends on whether the target is illuminated (for optical), whether geometry supports measurement, and whether the object is within useful range. Therefore we define two layers:

  • Crossing: inside FOV cone (from F.7)
  • Detectable event: crossing AND additional feasibility constraints

2) Typical detectability gates (space-based optical baseline)

A simple and effective baseline rule is:

\[ D(t)=C(t)\ \wedge\ \mathrm{SunlitTarget}(t)\ \wedge\ \big(\rho(t)<\rho_{\max}\big) \]

This reflects a practical “first-pass” definition: the object must be inside the cone, sunlit, and within a maximum useful range (e.g., $\rho_{\max}=1000~\mathrm{km}$). More advanced models replace the hard range threshold with probability-of-detection and brightness physics.

Gate A — Sunlit constraint (illumination)

“Sunlit” means the object is not inside Earth’s shadow. For screening and algorithm development, a cylindrical eclipse model is often used because it is fast and conservative.

Practical logic:

  • Compute Earth→Sun unit direction $\hat{\mathbf{s}}(t)$
  • Project the target position onto $\hat{\mathbf{s}}(t)$
  • If the target lies “behind Earth” and within the shadow cylinder → it is eclipsed

Why this gate dominates optical SSA

Many crossings are geometrically valid yet useless because the target is eclipsed. A sunlit gate can reduce “candidate detections” dramatically and improves realism immediately.

Gate B — Range constraint (sensitivity / brightness proxy)

Even if the target is inside the cone and sunlit, it can be too far to be detectable. A hard range gate is a clean baseline:

\[ \rho(t)<\rho_{\max} \]

Later upgrades (optional) include phase angle, albedo, aperture, exposure time, streak/blur constraints, and probabilistic detection. But as an engineering filter for early design and scaling, range gating is extremely effective.

3) Ground-based twist (optional extension)

If you apply similar logic to a ground optical station, the “illumination story” changes: the object should be sunlit, but the station should be in darkness. That introduces a station-night gate based on local solar time or Sun elevation at the site.

  • Space-based optical: target sunlit is typically beneficial.
  • Ground-based optical: target sunlit AND station dark is required.

4) Turning detectability samples into detectable intervals

Just like crossings, you want detectable intervals rather than raw samples. Compute detectability boolean samples $D_k$, then convert them into:

  • Detectable #1: $[t_{\mathrm{start}}, t_{\mathrm{end}}]$
  • Detectable #2: $[t_{\mathrm{start}}, t_{\mathrm{end}}]$

These intervals are mission-facing. You can directly report: how many windows exist, total detectable time, and closest approach range during detectable windows.

5) Practical interpretation: what operators learn

  • Crossings exist but detections do not: geometry is fine; illumination/range kills feasibility.
  • Detections exist but are very short: cadence, integration time, and tasking must be tuned.
  • Detections cluster: scheduling contention and handoff logic become important.

Python Snippets (F.8) — Detectability (Sunlit + Range)

import numpy as np

def range_gate(r_obj, r_sens, rho_max_km=1000.0):
    rho = r_obj - r_sens
    rho_norm = np.linalg.norm(rho, axis=1)
    return rho_norm < rho_max_km, rho_norm

R_E_KM = 6378.137

def sun_direction_placeholder(jd, fr):
    """
    Placeholder: returns a fixed inertial Sun direction for the whole window.
    Replace with an ephemeris-based sun vector when needed.
    """
    s = np.array([1.0, 0.0, 0.0], dtype=float)
    return np.tile(s, (len(jd), 1))

def sunlit_cylindrical(r_obj, s_hat):
    """
    r_obj: (N,3) target position in an inertial-like frame (km)
    s_hat: (N,3) unit Sun direction (Earth -> Sun)
    returns: boolean sunlit (N,) True if NOT in Earth shadow
    """
    proj = np.einsum('ij,ij->i', r_obj, s_hat)  # scalar projection
    r_perp = r_obj - proj[:, None] * s_hat
    d_perp = np.linalg.norm(r_perp, axis=1)

    in_shadow = (proj < 0.0) & (d_perp < R_E_KM)
    return ~in_shadow

def detectability_boolean(C_cross, sunlit, range_ok):
    return C_cross & sunlit & range_ok

# Example usage:
# range_ok, rho = range_gate(r_obj, r_sens, rho_max_km=1000.0)
# s_hat = sun_direction_placeholder(jd, fr)
# sunlit = sunlit_cylindrical(r_obj, s_hat)
# D = detectability_boolean(C, sunlit, range_ok)
# detect_intervals = boolean_to_intervals(ts, D)

F.9 — Scalable SSA Architecture Design

Scale to 30,000 objects × 100 trackers using coarse→refine + batching

Scaling SSA is not “do the same loop 3 billion times.” It is about designing a pipeline that rejects 99.9% of impossible pairs cheaply, then spends computation only where events are plausible.

1) The scaling problem (the “real SSA” question)

Now perform the F.7–F.8 workflow not for a single object and sensor, but for a catalog-scale scenario:

  • up to 30,000 objects
  • up to 100 trackers
  • over a 24-hour horizon

A naive 10-second scan would produce on the order of tens of billions of checks per day. That is not an operational solution. Operational solutions rely on: layered screening, windowing, refinement, and vectorized computation.

2) Core strategy: “cheap rejection first, expensive checks last”

A scalable SSA pipeline is always layered:

  • Coarse geometric screening (fast, approximate, margin included)
  • Window building (convert scattered hits into candidate time intervals)
  • Refined boundary search (accurate AOS/LOS timing only inside candidate windows)
  • Detectability checks (sunlit, strict range, optional brightness) only inside refined crossings
  • Event-store outputs (intervals + metrics, not dense time series)

Algorithm (F.9) — Coarse→Refine event pipeline

  1. Build a coarse time grid (e.g., 60 s) over 24 hours.
  2. Propagate sensors and objects (batched) on the coarse grid.
  3. Apply relaxed gates (range + cone with margins) to create hit flags.
  4. Convert hit flags into padded candidate windows.
  5. Inside each window, resample on a fine grid (5–10 s) and compute exact crossing intervals.
  6. Run detectability only inside refined crossings.
  7. Store event intervals and key metrics in a compact event table.

3) Recommended system architecture (modules you can implement)

A) Ingestion

  • Parse the TLE catalog into propagator objects.
  • Store metadata (object ID, epoch, object class, etc.).

B) Time grid + shared caches

  • Coarse grid: $\Delta t_{\mathrm{coarse}} = 60~\mathrm{s}$
  • Cache Sun direction $\hat{\mathbf{s}}(t)$ vs time (shared across all sensors).

C) Propagation layer

  • Objects: batched SGP4 propagation (vectorized or chunked).
  • Sensors: propagate and cache $\mathbf{r}_s(t_k)$ and $\hat{\mathbf{b}}(t_k)$ on the coarse grid.

D) Coarse candidate scan

At each sensor time sample, compute LOS/range cheaply for object batches and apply relaxed gates (with margins):

  • Range gate with buffer: $\rho < \rho_{\max} + 50~\mathrm{km}$
  • Angle gate with buffer: $\theta < \theta_{\mathrm{FOV}} + 1^\circ$

The margins prevent missing boundary events due to coarse sampling.

E) Window builder

  • Convert “hit samples” into candidate windows per (sensor, object).
  • Merge adjacent hits into continuous windows.
  • Pad windows to protect against coarse discretization.

F) Refined scan within windows

  • Resample at $\Delta t_{\mathrm{fine}} = 5$–$10~\mathrm{s}$ inside each candidate window.
  • Compute exact crossings and key geometry metrics (min range, max off-boresight).

G) Detectability filter

  • Sunlit gate (and optional station-night gate if using ground sensors).
  • Strict range gate.
  • Optional brightness/phase probability model.

H) Event store + analytics

Store compact outputs such as:

  • (sensor_id, object_id, event_type, t_start, t_end)
  • min_range, max_offboresight, sunlit_fraction

What makes this “operational”

You store events (intervals + metrics), not dense time series. That makes analysis, scheduling, and downstream fusion feasible at catalog scale.

4) Parallelization and compute model (how to make it actually run)

A robust scaling pattern is:

  • Parallelize over sensors (natural process-level parallelism).
  • Vectorize within each sensor over object batches (matrix operations).
  • Avoid inner Python loops in the compute-heavy region.

5) Algorithm design principles that make SSA “operational”

  • Principle 1: Output events, not samples.
  • Principle 2: Two-resolution time stepping (coarse → refine).
  • Principle 3: Gate order matters (range first, angle next, illumination last).
  • Principle 4: Deterministic pipeline (fixed epochs, explicit frames, explicit assumptions).
  • Principle 5: Add realism gradually (slew limits, duty cycles, probabilistic detection).

6) What “success” looks like (SSA engineering deliverables)

  • Daily event report: crossing vs detection counts per sensor.
  • Ranked opportunity list: which objects have the most detectable time.
  • Timing products: detect window start/end times for scheduling.
  • Quality metrics: min range, max off-boresight, sunlit fraction.
  • Performance metrics: runtime, throughput (checks/s), memory footprint.

Python Snippets (F.9) — Coarse→Refine + batching + parallelism

import numpy as np
import multiprocessing as mp

def refine_windows_from_coarse(ts_coarse, hit_flags, pad_sec=120):
    """
    Convert coarse hit flags into candidate windows, padded for safety.
    pad_sec expands each window to avoid missing boundaries.
    """
    raw = boolean_to_intervals(ts_coarse, hit_flags)
    windows = []
    for a, b in raw:
        windows.append((max(0.0, a - pad_sec), b + pad_sec))
    return windows

def coarse_scan_one_sensor(r_sens, b_hat, r_objs, fov_half_deg, rho_max_km, margin_deg=1.0, margin_km=50.0):
    """
    r_sens: (T,3)
    b_hat:  (T,3)
    r_objs: (Nobj, T,3)  -- store objects in (batch, time, xyz)
    returns: hit matrix (Nobj, T) booleans for coarse scan
    """
    rho = r_objs - r_sens[None, :, :]
    rho_norm = np.linalg.norm(rho, axis=2)

    # cheap range prune first (with margin)
    range_ok = rho_norm < (rho_max_km + margin_km)

    # cos(theta) = (b · rho)/|rho|
    dot = np.einsum('tj,ntj->nt', b_hat, rho)
    cos_theta = dot / np.maximum(rho_norm, 1e-12)
    cos_theta = np.clip(cos_theta, -1.0, 1.0)
    theta = np.degrees(np.arccos(cos_theta))

    angle_ok = theta <= (fov_half_deg + margin_deg)
    return range_ok & angle_ok

def process_one_sensor(sensor_id, r_sens, b_hat, objects_batches, cfg):
    """
    objects_batches: list of r_objs shaped (Nb, T,3)
    returns: compact list of objects that had coarse hits (for later refinement)
    """
    events = []
    for batch_idx, r_objs in enumerate(objects_batches):
        hits = coarse_scan_one_sensor(
            r_sens=r_sens,
            b_hat=b_hat,
            r_objs=r_objs,
            fov_half_deg=cfg["fov_half_deg"],
            rho_max_km=cfg["rho_max_km"],
            margin_deg=cfg.get("margin_deg", 1.0),
            margin_km=cfg.get("margin_km", 50.0),
        )
        any_hit = np.any(hits, axis=1)
        hit_obj_indices = np.where(any_hit)[0]
        for i in hit_obj_indices:
            events.append((sensor_id, batch_idx, int(i)))
    return events

def run_parallel_over_sensors(sensor_states, objects_batches, cfg):
    """
    sensor_states: list of (sensor_id, r_sens(T,3), b_hat(T,3))
    """
    with mp.Pool(processes=cfg.get("nproc", 8)) as pool:
        jobs = []
        for (sid, r_sens, b_hat) in sensor_states:
            jobs.append(pool.apply_async(process_one_sensor, (sid, r_sens, b_hat, objects_batches, cfg)))
        out = [j.get() for j in jobs]
    return out

# Write-up summary (assignment style):
# 1) Coarse scan (60 s): range+cone with margins → candidate windows
# 2) Refine scan (5–10 s) inside windows: exact crossing boundaries
# 3) Detectability only inside refined crossings: sunlit + strict range (+ optional brightness)
# 4) Store intervals, not time series (Parquet/CSV event table)

Continue in Domain F

Next: F.10- F.11 Integrated Mission Walkthroughs →

← Back to Domain F Overview