Observation-Blocking¶
Quickstart¶
import jax
from orbitalgym import OrbitalGymEnv, BySide, make_observation_blocking
from orbitalgym.policies import ZeroControl
from orbitalgym.rollout import rollout
cfg = make_observation_blocking()
env = OrbitalGymEnv(cfg)
guard = ZeroControl(n_vehicles=cfg.n_guards, command_cls=env.guard_command_cls)
bandit = ZeroControl(n_vehicles=cfg.n_bandits, command_cls=env.bandit_command_cls)
traj = rollout(env, BySide(guard=guard, bandit=bandit),
BySide(guard=lambda c, s, k: None, bandit=lambda c, s, k: None),
jax.random.PRNGKey(0), n_steps=cfg.max_steps)
The bandit blocks the guard's view of an Earth surface target — but only when the target is observable in the first place. This models a spaceborne ISR-denial scenario: the guard satellite is tasked with observing a fixed lat/lon target, and the bandit positions itself to occlude that line of sight whenever the target is actually visible from the guard.
What the reward shapes¶
The reward at each timestep is the KSP-DG-aligned shape with a visibility gate:
where û_BT is the bandit-to-target unit vector. Same three regimes as Sun-Blocking (peak / trough / zero), multiplied by a hard visibility gate: if the target's elevation as seen from the guard is below min_elevation_deg, both sides' rewards are zero regardless of bandit geometry.
The game is zero-sum and bounded [-1, +1]. The visibility gate applies to both lobes — the guard "wins" (gets +reward) only when the target is visible and the bandit is in the wrong place.
Implementation notes¶
The Earth target's ECEF position is precomputed once in Game.__post_init__ from (target_lat_deg, target_lon_deg, target_alt_m) via astrojax.position_geodetic_to_ecef. Per step, ECEF is rotated to ECI via GMST (astrojax.zero_eop() — no EOP corrections), vehicle RTN is converted to ECI via the same frame helper as Sun-Blocking, and the elevation angle is computed between geocentric "up" at the target and the target-to-observer direction.
The pure formula is exposed as orbitalgym.games.observation_blocking.observation_blocking_kernel(...) and the visibility gate as target_visible_from_guard(...). Both are used directly by the position-sweep diagnostic.
Termination¶
max_steps only.
Builder¶
from orbitalgym import make_observation_blocking
cfg = make_observation_blocking(
target_lat_deg=51.5, # London
target_lon_deg=-0.1,
min_elevation_deg=10.0,
target_viewing_distance_m=500.0,
range_decay_coef=4.0e-6,
max_horizon_s=5400.0,
seed=0,
)
Knobs at a glance¶
| Field | Type | Default | What it does |
|---|---|---|---|
n_guards |
int |
1 |
Observer count. |
n_bandits |
int |
1 |
Blocker count. |
target_lat_deg |
float |
37.4 |
Target latitude. |
target_lon_deg |
float |
-122.2 |
Target longitude. |
target_alt_m |
float |
0.0 |
Target altitude. |
min_elevation_deg |
float |
5.0 |
Visibility gate threshold. |
target_viewing_distance_m |
float |
500.0 |
Desired bandit-guard standoff at the reward peak. |
range_decay_coef |
float |
4.0e-6 |
Gaussian decay coefficient (1/m²). |
max_horizon_s |
float |
5400.0 |
Total duration. |
dt |
float |
10.0 |
Step size. |
seed |
int |
0 |
PRNG seed. |
Suggested experiments¶
- Target latitude sweep. Vary
target_lat_degfrom −60° to +60° to study how target latitude relative to orbit inclination affects observability windows and bandit difficulty. - Elevation gate study. Vary
min_elevation_degfrom 5° (relaxed) to 30° (strict). Higher thresholds shorten visibility windows and harden the bandit problem. - Coordinated guards. Extend to
n_guards > 1and test whether multiple guards spread along the orbit can keep target observability above some threshold.
Variants¶
The four customization axes (reward, termination, IC, observation) all
swap by dataclasses.replace. Two OB-flavored recipes:
Variant 1: chasing bandit¶
Drop in LeadInterceptPursuer as the bandit. The bandit closes on the
guard rather than tracking the bandit-to-target line — a baseline
adversary that doesn't yet exploit the visibility gate, useful for
sanity-checking that an OB-aware bandit policy you're training actually
beats this naive chaser.
import dataclasses
from orbitalgym import make_observation_blocking
from orbitalgym.policies.heuristic import LeadInterceptPursuer
cfg = make_observation_blocking(seed=0, max_horizon_s=5400.0)
bandit = LeadInterceptPursuer(max_dv_mps=0.05, dt=cfg.dt)
cfg = dataclasses.replace(cfg, bandit_policy=bandit)
Variant 2: tighter IC for evaluation¶
Shrink sigma_radial_ellipse_m so evaluation rollouts draw from a
narrow band around the canonical phase-0/phase-π/2 IC. Lower IC
variance reduces episode-return variance, which matters more here than
in PE because OB returns are gated by visibility windows.
import jax.numpy as jnp
from orbitalgym.sampling.side import RelativeEllipse
from orbitalgym.sampling.spec import ICSpec
tight_ic = ICSpec(
guard_sampler=RelativeEllipse(
radial_ellipse_m=1000.0,
cross_track_m=0.0,
along_track_offset_m=0.0,
phase_rad=0.0,
sigma_radial_ellipse_m=1.0,
),
bandit_sampler=RelativeEllipse(
radial_ellipse_m=500.0,
cross_track_m=100.0,
along_track_offset_m=200.0,
phase_rad=jnp.pi / 2.0,
sigma_radial_ellipse_m=1.0,
),
validators=(),
max_attempts=100,
)
cfg2 = dataclasses.replace(cfg, ic_sampler=tight_ic)
For more axes, see Extending → Customize rewards, …termination, …IC sampling, and …observations.
Built-in policies and adversaries¶
Sensible gallery picks for Observation-Blocking:
- Heuristic policies (opponent):
LeadInterceptPursuer,JitteredPolicy(wrap any of the above). - Controlled side: any class from Controlled-side cookbook
(
HeuristicWithFallbackPolicy,CompositeActionPolicy,MCTSPolicy).
Sanity-check notebook¶
examples/games/observation_blocking.ipynb is a full walkthrough that builds an OB scenario, runs a rollout, plots the rollout-time diagnostic (with shaded invisible windows), and renders the 2D position-sweep surface.
Where to next¶
- API:
ObservationBlocking. - In depth: Symmetric core, Observations.