Skip to content

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:

bandit_r = (-û_BG · û_BT) * exp(-decay * (d_BG - d_target)²) * 1[visible]
guard_r  = -bandit_r

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_deg from −60° to +60° to study how target latitude relative to orbit inclination affects observability windows and bandit difficulty.
  • Elevation gate study. Vary min_elevation_deg from 5° (relaxed) to 30° (strict). Higher thresholds shorten visibility windows and harden the bandit problem.
  • Coordinated guards. Extend to n_guards > 1 and 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:

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