Skip to content

Games

Typed Game subtypes (knob bundles) and per-game make_<game>(...) builders.

Base

orbitalgym.games.base.Game dataclass

Game()

Marker base — subclasses MUST override default_reward_fn/termination_fn.

advance_state

advance_state(
    prev_state: Any, next_state: Any, cfg: Any
) -> Any

Game-owned bookkeeping on the state leaving a step.

OrbitalGymEnv.step calls this once the dynamics have produced next_state and before the observations, rewards and termination read it, so a game that needs history across steps — how long a vehicle has held a radius, say — can keep it on the state rather than recomputing it. The default returns the state untouched.

validate

validate(cfg: Any) -> None

Check the cfg supports this game's knobs. Called by ScenarioConfig.post_init.

Games whose knobs depend on the state layout (a propellant gate needs a mass component, say) reject the incoherent combination here, so the failure lands at config construction rather than mid-rollout.

orbitalgym.games.base.NoGame dataclass

NoGame()

Bases: Game

Default — for users running custom scenarios with no preset semantics.

advance_state

advance_state(
    prev_state: Any, next_state: Any, cfg: Any
) -> Any

Game-owned bookkeeping on the state leaving a step.

OrbitalGymEnv.step calls this once the dynamics have produced next_state and before the observations, rewards and termination read it, so a game that needs history across steps — how long a vehicle has held a radius, say — can keep it on the state rather than recomputing it. The default returns the state untouched.

validate

validate(cfg: Any) -> None

Check the cfg supports this game's knobs. Called by ScenarioConfig.post_init.

Games whose knobs depend on the state layout (a propellant gate needs a mass component, say) reject the incoherent combination here, so the failure lands at config construction rather than mid-rollout.

orbitalgym.registry.GameKey

Bases: StrEnum

Game catalog key. NoGame is the default for custom scenarios.

Lady-Bandit-Guard

orbitalgym.games.lady_bandit_guard.LadyBanditGuard dataclass

LadyBanditGuard(
    breach_radius_m: float = 5.0,
    catch_radius_m: float = 50.0,
    breach_speed_mps: float = float("inf"),
    catch_speed_mps: float = float("inf"),
    catch_dwell_steps: int = 0,
    breach_dwell_steps: int = 0,
    escape_radius_m: float = 0.0,
    repel_on_empty_tank: bool = False,
    dv_cost: float = 0.0,
    shaping_scale_m: float = 300.0,
    shaping_gain: float = 50.0,
    shaping_discount: float = 1.0,
    home_weight: float = 0.0,
    guard_separation_m: float = 20.0,
    lady_keepout_m: float = 20.0,
    separation_cost: float = 10.0,
)

Bases: Game

Guard protects the lady (RTN origin) from bandits.

Knobs

breach_radius_m: bandit-vs-lady distance below which the bandit is considered to have breached (bandit win). catch_radius_m: guard-vs-bandit distance below which the guard catches the bandit (guard win). breach_speed_mps: bandit-vs-lady relative speed below which a pass inside breach_radius_m counts as a breach. Infinite gates on radius alone. catch_speed_mps: guard-vs-bandit relative speed below which a pass inside catch_radius_m counts as a catch. Infinite gates on radius alone. catch_dwell_steps: consecutive steps a bandit must spend inside the catch radius of at least one guard — the same guard or not — before the catch fires. Zero fires on the first step that meets the radius and speed gates. breach_dwell_steps: consecutive steps a bandit must spend inside the breach radius of the lady before the breach fires. Zero fires on the first such step. escape_radius_m: bandit-vs-lady distance above which the bandit counts as repelled. Zero disables the gate. repel_on_empty_tank: when True, a bandit out of propellant whose coast cannot reach the lady before the horizon ends counts as repelled. Requires a mass-tracked bandit in an RTN frame. dv_cost: reward units charged per m/s of delta-v. Each side pays only for its own fuel; a side without a MASS component pays nothing. shaping_scale_m: distance that makes the shaping potential unity. shaping_gain: scale on the potential-difference shaping term. Zero leaves a terminal-only reward. The default is large enough that the shaping steers a horizon-limited planner rather than being lost under the terminal payoffs. shaping_discount: discount inside the potential difference. Matching the planner's per-step discount makes the shaped value exactly the unshaped value minus the potential. home_weight: weight on the guard-lady distance inside the potential, pulling the guard back toward the asset it defends. guard_separation_m: guard-guard distance below which the guard side pays a crowding hinge. lady_keepout_m: guard-lady distance below which the guard side pays the same hinge. separation_cost: reward units at the bottom of either hinge.

The episode is a guard win by repulsion once every bandit is repelled.

advance_state

advance_state(
    prev_state: Any, next_state: Any, cfg: Any
) -> Any

Advance the per-bandit dwell counters onto the state leaving the step.

Runs whatever the dwell knobs are set to, so the counters describe the engagement even when the win conditions do not read them.

orbitalgym.games.lady_bandit_guard.make_lady_bandit_guard

make_lady_bandit_guard(
    *,
    breach_radius_m: float = 5.0,
    catch_radius_m: float = 50.0,
    breach_speed_mps: float = float("inf"),
    catch_speed_mps: float = float("inf"),
    catch_dwell_steps: int = 0,
    breach_dwell_steps: int = 0,
    escape_radius_m: float = 0.0,
    repel_on_empty_tank: bool = False,
    dv_cost: float = 0.0,
    shaping_scale_m: float = 300.0,
    shaping_gain: float = 50.0,
    shaping_discount: float = 1.0,
    home_weight: float = 0.0,
    guard_separation_m: float = 20.0,
    lady_keepout_m: float = 20.0,
    separation_cost: float = 10.0,
    n_guards: int = 1,
    n_bandits: int = 1,
    dt: float = 10.0,
    max_horizon_s: float = 2000.0,
    seed: int = 0,
    epoch_mjd_utc: float = 60067.0,
    reference_orbit: ReferenceOrbitState | None = None,
    guard_components: tuple[StateComponentKey, ...] = (
        StateComponentKey.RTN,
        StateComponentKey.MASS,
    ),
    bandit_components: tuple[StateComponentKey, ...] = (
        StateComponentKey.RTN,
    ),
    guard_params: VehicleParamsSpec | None = None,
    bandit_params: VehicleParamsSpec | None = None,
    ic_sampler: ICSpec | None = None,
    truth_dynamics: DynamicsKey = DynamicsKey.HCW_RTN,
    policy_dynamics: DynamicsKey = DynamicsKey.HCW_RTN,
    guard_observation_fn: Any = None,
    bandit_observation_fn: Any = None,
    guard_ground_station_network: Any = None,
    bandit_ground_station_network: Any = None,
    with_communication: bool = False,
    comm_cost: float = 5.0,
    **config_kwargs: Any,
)

Builder for an LBG scenario.

Wires LadyBanditGuard(breach_radius_m=..., catch_radius_m=...) as cfg.game; reward and termination are populated from LadyBanditGuard.default_*_fn by ScenarioConfig.__post_init__.

Defaults assume a 1v1 RTN scenario with mass-tracked guard. To switch to 2D RT dynamics, pass matching truth_dynamics/policy_dynamics and corresponding guard_components/bandit_components; otherwise ScenarioConfig.__post_init__ will reject the incoherent combo.

Additional keyword arguments are forwarded to ScenarioConfig.

The reward these knobs configure — the per-side potential-based shaping term (shaping_scale_m, shaping_gain, shaping_discount, home_weight), the guard-side separation charge (guard_separation_m, lady_keepout_m, separation_cost) and the dv_cost fuel term — is documented under Extending → Customize rewards.

Pursuit-Evasion

orbitalgym.games.pursuit_evasion.PursuitEvasion dataclass

PursuitEvasion(capture_distance_m: float = 10.0)

Bases: Game

1v1 pursuit/evasion.

Knobs

capture_distance_m: episode terminates when relative distance falls below this threshold.

advance_state

advance_state(
    prev_state: Any, next_state: Any, cfg: Any
) -> Any

Game-owned bookkeeping on the state leaving a step.

OrbitalGymEnv.step calls this once the dynamics have produced next_state and before the observations, rewards and termination read it, so a game that needs history across steps — how long a vehicle has held a radius, say — can keep it on the state rather than recomputing it. The default returns the state untouched.

validate

validate(cfg: Any) -> None

Check the cfg supports this game's knobs. Called by ScenarioConfig.post_init.

Games whose knobs depend on the state layout (a propellant gate needs a mass component, say) reject the incoherent combination here, so the failure lands at config construction rather than mid-rollout.

orbitalgym.games.pursuit_evasion.PursuitEvasionReward dataclass

PursuitEvasionReward(scope: str = 'per_side')

Zero-sum reward on |r_guard - r_bandit|. Requires cfg.game: PursuitEvasion.

orbitalgym.games.pursuit_evasion.PursuitEvasionTermination dataclass

PursuitEvasionTermination()

Episode ends on max_steps OR capture (relative distance below threshold).

Reads max_steps from params.max_steps and capture_distance_m from params.game.

orbitalgym.games.pursuit_evasion.make_pursuit_evasion

make_pursuit_evasion(
    *,
    capture_distance_m: float = 10.0,
    n_guards: int = 1,
    n_bandits: int = 1,
    dt: float = 10.0,
    max_horizon_s: float = 2000.0,
    seed: int = 0,
    epoch_mjd_utc: float = 60067.0,
    reference_orbit: ReferenceOrbitState | None = None,
    guard_components: tuple[StateComponentKey, ...] = (
        StateComponentKey.RTN,
    ),
    bandit_components: tuple[StateComponentKey, ...] = (
        StateComponentKey.RTN,
    ),
    guard_params: VehicleParamsSpec | None = None,
    bandit_params: VehicleParamsSpec | None = None,
    ic_sampler: ICSpec | None = None,
    truth_dynamics: DynamicsKey = DynamicsKey.HCW_RTN,
    policy_dynamics: DynamicsKey = DynamicsKey.HCW_RTN,
    guard_observation_fn: Any = None,
    bandit_observation_fn: Any = None,
)

Builder for a PE scenario.

Sun-Blocking

orbitalgym.games.sun_blocking.SunBlocking dataclass

SunBlocking(
    target_viewing_distance_m: float = 500.0,
    range_decay_coef: float = 4e-06,
)

Bases: Game

1v1 sun-blocking game.

Knobs

target_viewing_distance_m: desired bandit-guard standoff at which the range factor peaks (meters). range_decay_coef: Gaussian decay coefficient (1/m²) controlling the range-factor peak width.

advance_state

advance_state(
    prev_state: Any, next_state: Any, cfg: Any
) -> Any

Game-owned bookkeeping on the state leaving a step.

OrbitalGymEnv.step calls this once the dynamics have produced next_state and before the observations, rewards and termination read it, so a game that needs history across steps — how long a vehicle has held a radius, say — can keep it on the state rather than recomputing it. The default returns the state untouched.

validate

validate(cfg: Any) -> None

Check the cfg supports this game's knobs. Called by ScenarioConfig.post_init.

Games whose knobs depend on the state layout (a propellant gate needs a mass component, say) reject the incoherent combination here, so the failure lands at config construction rather than mid-rollout.

orbitalgym.games.sun_blocking.SunBlockingReward dataclass

SunBlockingReward(scope: str = 'per_side')

KSP-DG-style sun-blocking reward.

Requires cfg.game: SunBlocking. Pulls vehicle ECI positions via games/_frames.vehicle_eci_position, looks up Sun ECI via astrojax.sun_position, then calls sun_blocking_kernel.

orbitalgym.games.sun_blocking.make_sun_blocking

make_sun_blocking(
    *,
    target_viewing_distance_m: float = 500.0,
    range_decay_coef: float = 4e-06,
    n_guards: int = 1,
    n_bandits: int = 1,
    dt: float = 10.0,
    max_horizon_s: float = 5400.0,
    seed: int = 0,
    epoch_mjd_utc: float = 60067.0,
    reference_orbit: ReferenceOrbitState | None = None,
    guard_components: tuple[StateComponentKey, ...] = (
        StateComponentKey.RTN,
    ),
    bandit_components: tuple[StateComponentKey, ...] = (
        StateComponentKey.RTN,
    ),
    guard_params: VehicleParamsSpec | None = None,
    bandit_params: VehicleParamsSpec | None = None,
    ic_sampler: ICSpec | None = None,
    truth_dynamics: DynamicsKey = DynamicsKey.HCW_RTN,
    policy_dynamics: DynamicsKey = DynamicsKey.HCW_RTN,
    guard_observation_fn: Any = None,
    bandit_observation_fn: Any = None,
)

Builder for an SB scenario.

Observation-Blocking

orbitalgym.games.observation_blocking.ObservationBlocking dataclass

ObservationBlocking(
    target_lat_deg: float = 37.4,
    target_lon_deg: float = -122.2,
    target_alt_m: float = 0.0,
    min_elevation_deg: float = 5.0,
    target_viewing_distance_m: float = 500.0,
    range_decay_coef: float = 4e-06,
)

Bases: Game

Bandit blocks guard's view of an Earth surface target.

Knobs

target_lat_deg, target_lon_deg, target_alt_m: WGS84 geodetic coordinates of the surface target. min_elevation_deg: target visibility threshold (degrees above horizon). Reward is gated to 0 when the target is below this elevation as seen from the guard. target_viewing_distance_m: desired bandit-guard standoff at which the range factor peaks (meters). range_decay_coef: Gaussian decay coefficient (1/m²) controlling the range-factor peak width.

target_ecef_m is precomputed in post_init (one-time geodetic→ECEF conversion) and cached on the instance for use in the reward fn.

advance_state

advance_state(
    prev_state: Any, next_state: Any, cfg: Any
) -> Any

Game-owned bookkeeping on the state leaving a step.

OrbitalGymEnv.step calls this once the dynamics have produced next_state and before the observations, rewards and termination read it, so a game that needs history across steps — how long a vehicle has held a radius, say — can keep it on the state rather than recomputing it. The default returns the state untouched.

validate

validate(cfg: Any) -> None

Check the cfg supports this game's knobs. Called by ScenarioConfig.post_init.

Games whose knobs depend on the state layout (a propellant gate needs a mass component, say) reject the incoherent combination here, so the failure lands at config construction rather than mid-rollout.

orbitalgym.games.observation_blocking.ObservationBlockingReward dataclass

ObservationBlockingReward(scope: str = 'per_side')

KSP-DG-style observation-blocking reward, gated on visibility.

Requires cfg.game: ObservationBlocking. Pulls vehicle ECI positions via games/_frames.vehicle_eci_position, rotates the target ECEF→ECI per timestep (GMST-only via astrojax.zero_eop), then calls observation_blocking_kernel.

orbitalgym.games.observation_blocking.make_observation_blocking

make_observation_blocking(
    *,
    target_lat_deg: float = 37.4,
    target_lon_deg: float = -122.2,
    target_alt_m: float = 0.0,
    min_elevation_deg: float = 5.0,
    target_viewing_distance_m: float = 500.0,
    range_decay_coef: float = 4e-06,
    n_guards: int = 1,
    n_bandits: int = 1,
    dt: float = 10.0,
    max_horizon_s: float = 5400.0,
    seed: int = 0,
    epoch_mjd_utc: float = 60067.0,
    reference_orbit: ReferenceOrbitState | None = None,
    guard_components: tuple[StateComponentKey, ...] = (
        StateComponentKey.RTN,
    ),
    bandit_components: tuple[StateComponentKey, ...] = (
        StateComponentKey.RTN,
    ),
    guard_params: VehicleParamsSpec | None = None,
    bandit_params: VehicleParamsSpec | None = None,
    ic_sampler: ICSpec | None = None,
    truth_dynamics: DynamicsKey = DynamicsKey.HCW_RTN,
    policy_dynamics: DynamicsKey = DynamicsKey.HCW_RTN,
    guard_observation_fn: Any = None,
    bandit_observation_fn: Any = None,
)

Builder for an OB scenario.

Get-Off-My-Lawn

orbitalgym.games.get_off_my_lawn.GetOffMyLawn dataclass

GetOffMyLawn(
    r_min_keep_m: float = 100.0,
    r_max_keep_m: float = 500.0,
    catch_radius_m: float = 50.0,
    keep_out_radius_m: float = 1500.0,
    r_loiter: float = 1.0,
    alpha: float = 0.001,
    r_catch: float = 1000.0,
    r_pushout: float = 500.0,
    alpha_chase: float = 0.001,
)

Bases: Game

Bandit wants to loiter near the lady; guard wants to intercept or repel.

Geometric knobs

r_min_keep_m: lower bound of bandit desired loiter band (m, HCW metric). r_max_keep_m: upper bound of bandit desired loiter band (m, HCW metric). catch_radius_m: guard-vs-bandit Euclidean distance below which the guard intercepts (terminal event). keep_out_radius_m: HCW standoff beyond which the bandit is considered "pushed out". Must be > r_max_keep_m so the keep-out shell sits outside the loiter band; __post_init__ enforces this.

Reward-weight knobs (tune which term dominates): r_loiter: per-step bonus when at least one bandit is inside the keep band. Default 1.0. alpha: dense band-distance shaping coefficient (per meter). Default 1e-3 — a 1000 m gap from the band costs ~1.0 per step. r_catch: terminal penalty on the bandit (and bonus on the guard) when the guard catches a bandit. Default 1000.0; raise to make catch dominate, lower to weight loiter / chase shaping more heavily. r_pushout: bonus on the guard when every bandit is beyond the keep-out shell. Default 500.0. alpha_chase: dense guard-side shaping per meter of guard-bandit distance (a chase prior). Default 1e-3. Set to 0.0 (along with r_pushout=0.0) for a strictly zero-sum reward.

advance_state

advance_state(
    prev_state: Any, next_state: Any, cfg: Any
) -> Any

Game-owned bookkeeping on the state leaving a step.

OrbitalGymEnv.step calls this once the dynamics have produced next_state and before the observations, rewards and termination read it, so a game that needs history across steps — how long a vehicle has held a radius, say — can keep it on the state rather than recomputing it. The default returns the state untouched.

validate

validate(cfg: Any) -> None

Check the cfg supports this game's knobs. Called by ScenarioConfig.post_init.

Games whose knobs depend on the state layout (a propellant gate needs a mass component, say) reject the incoherent combination here, so the failure lands at config construction rather than mid-rollout.

orbitalgym.games.get_off_my_lawn.GetOffMyLawnReward dataclass

GetOffMyLawnReward(
    r_min_keep_m: float = 100.0,
    r_max_keep_m: float = 500.0,
    catch_radius_m: float = 50.0,
    keep_out_radius_m: float = 1500.0,
    alpha: float = 0.001,
    alpha_chase: float = 0.001,
    r_loiter: float = 1.0,
    r_catch: float = 1000.0,
    r_pushout: float = 500.0,
    scope: str = "per_side",
)

Dense + terminal reward for Get-Off-My-Lawn.

See :class:GetOffMyLawn module docstring for the full formula.

orbitalgym.games.get_off_my_lawn.GetOffMyLawnTermination dataclass

GetOffMyLawnTermination(
    catch_radius_m: float, keep_out_radius_m: float
)

Terminate on max_steps OR any bandit caught OR all bandits pushed out.

catch_radius_m=0 disables the catch gate. keep_out_radius_m should match :class:GetOffMyLawn to keep reward and termination in agreement.

orbitalgym.games.get_off_my_lawn.make_get_off_my_lawn

make_get_off_my_lawn(
    *,
    r_min_keep_m: float = 100.0,
    r_max_keep_m: float = 500.0,
    catch_radius_m: float = 50.0,
    keep_out_radius_m: float = 1500.0,
    r_loiter: float = 1.0,
    alpha: float = 0.001,
    r_catch: float = 1000.0,
    r_pushout: float = 500.0,
    alpha_chase: float = 0.001,
    n_guards: int = 1,
    n_bandits: int = 1,
    dt: float = 10.0,
    max_horizon_s: float = 3000.0,
    seed: int = 0,
    epoch_mjd_utc: float = 60067.0,
    reference_orbit: ReferenceOrbitState | None = None,
    guard_components: tuple[StateComponentKey, ...] = (
        StateComponentKey.RTN,
    ),
    bandit_components: tuple[StateComponentKey, ...] = (
        StateComponentKey.RTN,
    ),
    guard_params: VehicleParamsSpec | None = None,
    bandit_params: VehicleParamsSpec | None = None,
    ic_sampler: ICSpec | None = None,
    truth_dynamics: DynamicsKey = DynamicsKey.HCW_RTN,
    policy_dynamics: DynamicsKey = DynamicsKey.HCW_RTN,
    guard_observation_fn: Any = None,
    bandit_observation_fn: Any = None,
)

Builder for a Get-Off-My-Lawn scenario.

Defaults place a 1v1 bandit/guard in the RTN frame around a circular LEO reference orbit. The bandit IC sits on a 2 km radial-ellipse co-orbit (well outside the keep band), so a passive bandit will not naturally drift into the loiter window — solving requires control.

Proximity events

Closest-approach geometry shared by the Lady-Bandit-Guard reward and termination, so both resolve catch and breach over the whole step, and the dwell counters that turn either event into a hold over several steps.

orbitalgym.games.proximity.positions

positions(side_state) -> jax.Array

Per-vehicle (..., N, 3) positions, zero-padded from a 2D rt layout.

Leading axes pass through, so a time-stacked side state returns (T, N, 3).

orbitalgym.games.proximity.closest_approach

closest_approach(
    r0: Array, v: Array, dt: float
) -> tuple[jax.Array, jax.Array]

Minimum distance over a coast of length dt, and the relative speed.

Parameters:

Name Type Description Default
r0 Array

Relative position at the start of the step, shape (..., 3).

required
v Array

Constant relative velocity over the step, shape (..., 3).

required
dt float

Step duration in seconds.

required

Returns:

Type Description
Array

(distance, speed), both shape (...): the minimum of

Array

|r0 + v * s| over s in [0, dt], and |v|.

orbitalgym.games.proximity.proximity_event

proximity_event(
    r0: Array,
    v: Array,
    dt: float,
    radius_m: float,
    speed_mps: float,
) -> jax.Array

Whether the coast passes within radius_m while slower than speed_mps.

orbitalgym.games.proximity.lbg_events

lbg_events(
    prev_state,
    next_state,
    dt: float,
    catch_radius_m: float,
    catch_speed_mps: float,
    breach_radius_m: float,
    breach_speed_mps: float,
) -> tuple[jax.Array, jax.Array, jax.Array, jax.Array]

Catch/breach events and closest-approach distances over one step.

Catch geometry is guard minus bandit over every guard-bandit pair; breach geometry is bandit minus lady, the RTN origin. catch_radius_m of zero disables the catch event while still reporting d_gb_min.

Returns:

Type Description
Array

(caught, breached, d_gb_min, d_bl_min) — two scalar booleans and

Array

the two minimum closest-approach distances over the step.

orbitalgym.games.proximity.lbg_events_from_positions

lbg_events_from_positions(
    guard_prev: Array,
    guard_next: Array,
    bandit_prev: Array,
    bandit_next: Array,
    dt: float,
    catch_radius_m: float,
    catch_speed_mps: float,
    breach_radius_m: float,
    breach_speed_mps: float,
) -> tuple[jax.Array, jax.Array, jax.Array, jax.Array]

Catch/breach events and closest-approach distances from position arrays.

The events are :func:lbg_proximity_masks reduced over the bandits: an event holds when some bandit is inside the corresponding radius.

Returns:

Type Description
Array

(caught, breached, d_gb_min, d_bl_min) with the leading shape of

Array

the inputs.

orbitalgym.games.proximity.lbg_proximity_masks

lbg_proximity_masks(
    guard_prev: Array,
    guard_next: Array,
    bandit_prev: Array,
    bandit_next: Array,
    dt: float,
    catch_radius_m: float,
    catch_speed_mps: float,
    breach_radius_m: float,
    breach_speed_mps: float,
) -> tuple[jax.Array, jax.Array, jax.Array, jax.Array]

Per-bandit in-radius masks and closest-approach distances.

Positions are (..., n_g, 3) and (..., n_b, 3); leading axes pass through, so a whole trajectory is scored in one call. A bandit is inside the catch radius when some guard both passes within catch_radius_m of it and is slower than catch_speed_mps over the step — the per-pair conjunction, not the closest pair's speed. It is inside the breach radius when its own pass at the lady meets the same two conditions.

Returns:

Type Description
Array

(in_catch, in_breach, d_gb_min, d_bl_min). The two masks are

Array

(..., n_b); the two distances are the minima over every pair and

Array

over every bandit, with the leading shape of the inputs.

orbitalgym.games.proximity.advance_dwell

advance_dwell(count: Array, inside: Array) -> jax.Array

One step of a consecutive-step counter: increment inside, reset outside.

count and inside are per-bandit arrays of the same shape. The result counts how many consecutive steps, up to and including this one, each bandit has spent inside the radius; a single step outside sends it back to zero.

A step counts as inside when its within-step closest approach is inside, the same test the single-step event uses. The step a bandit departs on therefore still counts — it was inside for part of it — and the first step spent wholly outside is the one that breaks the hold.

orbitalgym.games.proximity.lbg_dwell_step

lbg_dwell_step(
    prev_state,
    next_state,
    dt: float,
    catch_radius_m: float,
    catch_speed_mps: float,
    breach_radius_m: float,
    breach_speed_mps: float,
) -> tuple[jax.Array, jax.Array]

The dwell counters leaving a step, from the counters entering it.

Advances prev_state.dwell_catch and prev_state.dwell_breach with :func:advance_dwell on the per-bandit masks :func:lbg_proximity_masks reports for the step. A step whose closest approach fails either the radius or the speed gate resets that counter, so a positive count means the gate held on every one of those steps.

Returns:

Type Description
tuple[Array, Array]

(dwell_catch, dwell_breach), both (..., n_b) integers.

orbitalgym.games.proximity.lbg_events_with_dwell

lbg_events_with_dwell(
    prev_state,
    next_state,
    dt: float,
    catch_radius_m: float,
    catch_speed_mps: float,
    breach_radius_m: float,
    breach_speed_mps: float,
    catch_dwell_steps: int,
    breach_dwell_steps: int,
) -> tuple[jax.Array, jax.Array, jax.Array, jax.Array]

Catch/breach events under an optional consecutive-step dwell requirement.

A dwell of zero leaves the event exactly as :func:lbg_events reports it: the within-step closest approach on this step alone. A positive dwell replaces it with the requirement that some bandit has held the radius — and the speed gate, which enters through the counter — for that many consecutive steps, read off the counters next_state carries.

Returns:

Type Description
Array

(caught, breached, d_gb_min, d_bl_min); the distances are the

Array

step's own closest approaches either way.

orbitalgym.games.proximity.stm_power_stack

stm_power_stack(
    mean_motion: float, dt: float, max_steps: int
) -> np.ndarray

Powers phi^0 ... phi^max_steps of the one-step HCW-RTN STM, (max_steps + 1, 6, 6).

Coasting a state to every step of the horizon is one batched matmul against this stack rather than a chain of max_steps dependent ones, so :func:ballistic_breach_possible costs the same whichever step of the episode calls it. The stack is built with an associative scan under compile-time evaluation and returned as a concrete NumPy array, so it is memoised on its three scalar arguments (fixed for a scenario) and is safe to build from inside a jax.jit trace without leaking a tracer.

Parameters:

Name Type Description Default
mean_motion float

Reference-orbit mean motion in rad/s.

required
dt float

Step duration in seconds.

required
max_steps int

Highest power to build.

required

orbitalgym.games.proximity.ballistic_breach_possible

ballistic_breach_possible(
    x_rtn: Array,
    phi_powers: Array | ndarray,
    n_remaining: Array,
    dt: float,
    radius_m: float,
    speed_mps: float,
) -> jax.Array

Whether a coasting RTN state can still reach the lady before the clock runs out.

Coasts x_rtn to every step of the horizon at once through phi_powers, then tests :func:proximity_event against the origin on each of the resulting segments, keeping only the first n_remaining of them. The horizon length is static — it is the length of phi_powers — while n_remaining is traced, so one compiled evaluation serves every step of an episode.

Parameters:

Name Type Description Default
x_rtn Array

Coasting state (..., 6) as [R, T, N, Rdot, Tdot, Ndot].

required
phi_powers Array | ndarray

Powers of the one-step STM, (max_steps + 1, 6, 6), from :func:stm_power_stack.

required
n_remaining Array

Steps left in the episode; segments beyond it are masked.

required
dt float

Step duration in seconds, matching phi_powers.

required
radius_m float

Breach radius about the origin.

required
speed_mps float

Relative-speed gate for the breach.

required

Returns:

Type Description
Array

Boolean with the leading shape of x_rtn.

orbitalgym.games.proximity.lbg_repelled

lbg_repelled(
    state,
    params,
    escape_radius_m: float,
    repel_on_empty_tank: bool,
    breach_radius_m: float,
    breach_speed_mps: float,
) -> jax.Array

Whether every bandit has been repelled — no longer a threat to the lady.

A bandit is repelled when it is farther than escape_radius_m from the lady (zero disables that gate), or when repel_on_empty_tank is set and it has burnt its propellant on a coast that cannot reach the lady within the episode's remaining steps. Both gates off returns False.

params is the scenario config: dt, max_steps and, for the empty-tank gate, reference_orbit supply the coast model.

Dispatch

orbitalgym.games.make_game

make_game(key: GameKey, **kwargs)

Dispatch to the per-game builder by GameKey.

Example

cfg = make_game(GameKey.LADY_BANDIT_GUARD, breach_radius_m=5.0, catch_radius_m=50.0)