F.7 — Sensor Geometry
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.
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
- Propagate sensor and object states over $[t_0, t_0 + 24~\mathrm{h}]$.
- Compute $\boldsymbol{\rho}(t)$, $\rho(t)$, and boresight $\hat{\mathbf{b}}(t)$.
- Compute $\theta(t)$ from the dot-product definition.
- Generate boolean crossing series: $C(t)=\big(\theta(t)\le\theta_{\mathrm{FOV}}\big)$.
- 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
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
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
- Build a coarse time grid (e.g., 60 s) over 24 hours.
- Propagate sensors and objects (batched) on the coarse grid.
- Apply relaxed gates (range + cone with margins) to create hit flags.
- Convert hit flags into padded candidate windows.
- Inside each window, resample on a fine grid (5–10 s) and compute exact crossing intervals.
- Run detectability only inside refined crossings.
- 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)