V-BAR: Verified Benchmark for Autonomous Rendezvous
V-BAR is a headless Python benchmark for spacecraft rendezvous and proximity-operations controllers. Classical guidance laws, reinforcement learning agents and LLM agents fly the same scenarios and are scored with the same metrics.
Status: early development (0.2). The dynamics, spacecraft, sensor and constraint layers are validated (see Tests). The environment has one scenario, the safety-filter interface has only its pass-through ("filter off") implementation, and interfaces may change before 1.0.
Why
KSPDG, the main existing environment for this kind of work, runs on Kerbal Space Program in real time and needs a copy of the game. V-BAR is a plain Python package that runs much faster than real time on Linux, macOS and Windows, so experiments with hundreds of seeds and many controllers are practical. Its physics is checked against closed-form solutions and published textbook examples, so a result reflects the controller and not a simulator error.
Install
pip install vbar
Python 3.12 or later. The core dependencies are numpy, scipy and gymnasium. Plotting needs the
plot extra: pip install "vbar[plot]". To work on V-BAR itself, see
CONTRIBUTING.md.
Quickstart
from vbar import ApproachConfig, ApproachEnv, run_episode
from vbar.agents import PDAgent
env = ApproachEnv(ApproachConfig(start_state=(0, -400, 0, 0, 0, 0)))
result = run_episode(env, PDAgent())
print(result["captured"], result["keep_out_violated"], result["dv_used_mps"])
This flies the PD baseline from 400 m behind the target to the hold point 100 m behind it, about
620 s of flight, and prints True False 2.41...: it arrived, it never entered the keep-out zone,
and it used 2.4 m/s of delta-v.
Benchmarks need many runs, not one. With the default random starts, which put the chaser 200 to 600 m ahead of the target so the target sits between it and the hold point:
import numpy as np
from vbar import ApproachEnv, run_many
from vbar.agents import PDAgent
results = run_many(ApproachEnv(), PDAgent(), seeds=range(100))
print(np.mean([r["captured"] for r in results]), np.mean([r["keep_out_violated"] for r in results]))
The PD baseline reaches the hold point in every run and flies through the keep-out zone in every
run, because it ignores the target. That gap is what safety filters and better controllers are
measured against. In a clone of the repository, python examples/v0_demo.py shows both cases
and saves a plot to outputs/ (needs the plot extra).
Frame and units
Everything is in the target's Hill frame (also called RIC, RSW or RTN), centred on the target and rotating with its orbit:
- x: radial, pointing away from Earth (the R-bar)
- y: along-track, the direction the target is moving (the V-bar); "behind the target" is -y
- z: orbit normal, completing a right-handed frame
States are [x, y, z, vx, vy, vz] in metres and metres per second, with velocities measured in
the rotating frame. All units are SI and angles are in radians. The ISS-style LVLH frame used in
much of the operations literature orders and signs the axes differently;
vbar.conventions.hill_to_iss and iss_to_hill convert between the two.
The scenario: approach to a hold point
The chaser must reach a hold point 100 m behind the target and stop there without entering a 50 m
keep-out sphere around the target. The target is on a circular 500 km orbit. Defaults, all set in
ApproachConfig:
| Item | Default |
|---|---|
| Start | position uniform in x in [-200, 200] m, y in [200, 600] m, z in [-50, 50] m; each velocity component in [-0.1, 0.1] m/s; or a fixed start_state |
| Action | 3 numbers in [-1, 1], scaled by max_accel = 0.02 m/s^2 per axis, held for one step; action_mode="accel" or "thrust" takes m/s^2 or newtons instead |
| Observation | the chaser's relative state [x, y, z, vx, vy, vz] from the navigation sensor, exact by default (PerfectNav); GaussianNav, RangeScaledNav and GaussMarkovNav add errors, and observation_channels appends time left, propellant left and pending commands |
| Spacecraft | 500 kg, Isp 220 s, propellant following the rocket equation; per-axis ("box") or total ("ball") thrust limit, minimum impulse bit, actuation delay, constant-acceleration or constant-thrust drive and thrust execution error are options |
| Step and length | dt = 10 s, at most 360 steps (one hour) |
| Success | within 5 m of the hold point and slower than 0.05 m/s; the episode ends on success |
| Keep-out | checked in continuous time: 20 samples per step, refined between them, with a certified lower bound; entering it does not end the episode unless terminate_on_violation is set |
Each run reports whether it captured, whether and in how many steps it entered the keep-out zone,
the minimum range to the target, the final distance to the hold point, delta-v used (each step's
delta-v summed, the usual proxy for propellant), flight time and wall-clock time, plus the
intervention count when a safety filter wraps the agent. run_episode also returns the full true
state and action history and the seed of the agent's random stream. Propellant used, mass and the
certified bound on the keep-out margin are in info.
The environment propagates with Clohessy-Wiltshire dynamics by default;
ApproachConfig(dynamics="twobody") or "twobody_j2" selects one of the nonlinear models below.
Writing a controller
Any object with a name, a reset(view, rng) method and an act(obs, info) method that returns
three numbers in [-1, 1] is an agent:
import numpy as np
class BrakeAgent:
name = "brake"
def reset(self, view, rng=None):
self.max_accel = view.max_accel
def act(self, obs, info):
return np.clip(-obs[3:6] / (10.0 * self.max_accel), -1.0, 1.0)
reset receives a read-only AgentView with the scenario's public description (time step, mean
motion, actuator limits, goal and constraints) and a random stream for the agent's own use. obs
comes from the navigation sensor and info holds only the time, so a controller never sees the
true state. The agent's stream is seeded by run_episode(env, agent, seed, agent_seed)
independently of the episode seed; without an agent_seed one is drawn and returned in the
result, so the run can be repeated.
LLM agents
LLMAgent wraps any model you can call as a function from a prompt string to a reply string:
from vbar.agents import LLMAgent
def complete(prompt: str) -> str:
"""Call any hosted or local model and return its text reply."""
return '{"ax": 0.0, "ay": 0.0, "az": 0.0}' # replace with the model call
agent = LLMAgent(complete, name="my-model")
result = run_episode(env, agent)
agent.log # every prompt, reply, parsed action, parse failure and latency
Each step's prompt gives the frame, the goal, the keep-out radius, the mean motion, the current
state and the thrust limit, and asks for a JSON reply such as {"ax": 0.1, "ay": -0.5, "az": 0.0}.
A reply that cannot be parsed counts as a parse failure and the spacecraft coasts for that step.
The simulation clock is paused while the model answers.
Reinforcement learning
ApproachEnv follows the Gymnasium API (reset(seed=...), step(action)), by default with a
Box(-1, 1, (3,)) action space and a 6-element observation, so it works with standard RL
libraries. Its reward is a shaped training signal (progress toward the hold point, minus delta-v,
minus a penalty for each step in the keep-out zone, plus a bonus on capture) and is not the
benchmark score; score agents on the metrics in info. The reward and info come from the true
state, so evaluate trained policies through run_episode, which passes them only the observation
and the time.
Dynamics
The dynamics can be used without the environment:
import numpy as np
from vbar.dynamics import make_dynamics
dyn = make_dynamics("twobody_j2", altitude=500e3, inclination=np.radians(51.6))
dyn.reset([0.0, -200.0, 0.0, 0.0, 0.0, 0.0]) # Hill-frame state, m and m/s
x, _ = dyn.advance([0.0, 0.0, 0.0], 600.0) # hold zero acceleration for 600 s
| Model | What it is |
|---|---|
"cw" |
Clohessy-Wiltshire: relative motion linearized about a circular orbit, with the exact solution for an acceleration held constant over a step. Accurate while the separation is small compared with the orbit radius. |
"twobody" |
Nonlinear relative motion under point-mass gravity, integrated numerically in a form that keeps the relative state accurate far below a millimetre. |
"twobody_j2" |
The same with Earth's oblateness (J2). The target starts on a circular orbit in J2 mean elements, and the Hill frame includes the extra rotation J2 causes. |
Also included, each documented in docs/:
- differential drag and solar radiation pressure with Earth shadow for the two-body model
(
vbar.dynamics.perturbations), and elliptic or phased target orbits; - the Yamanaka-Ankersen elliptic model, curvilinear Clohessy-Wiltshire and linearized two-body
motion with J2 (
vbar.dynamics.relative); transition matrices from the variational equations; model-error measures between models (vbar.dynamics.fidelity); - Earth-Moon three-body models: the circular restricted problem, linear models about libration
points and periodic orbits, and the bicircular Sun perturbation (
vbar.dynamics.cr3bp), with halo-orbit seeds, correctors, continuation and Floquet analysis (vbar.dynamics.periodic_orbits); - Kepler propagation for any conic (
vbar.dynamics.kepler), conversion between states and classical elements for elliptic orbits, and J2 secular rates with a first-order mean-osculating element map (vbar.dynamics.mean_elements).
Earth uses the WGS 84 constants; a Kerbin body with KSP's values is available for comparison with KSPDG.
Documentation
How each part is built, with its equations, methods, assumptions and limits, is in docs/, one page per part: frames, dynamics, perturbations, relative-motion models, transition matrices, three-body models, spacecraft, sensors, constraints, the environment, and validation.
Tests
python -m pytest -q
The models are tested against closed-form solutions, an independent integrator and published textbook examples (listed in the validation page). CI runs the tests on Linux, macOS and Windows.
Contributing
See CONTRIBUTING.md.
Citation
If you use V-BAR in your work, please cite it using the metadata in CITATION.cff (GitHub's "Cite this repository" button gives BibTeX and APA).
License
Metadata
Release files for vbar 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| vbar-0.2.0.tar.gz | 199.2 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| vbar-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 275.4 kB
Release files / vbar-0.2.0.tar.gz
| Download URL | vbar-0.2.0.tar.gz |
|---|---|
| Size | 199.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a42ce1bf32a98475ba59b1c26069da4131fe0c54adaf89695218bdad0a5c87f2
|
|
BLAKE2b-256 checksum How to use checksums |
22b267a70ddcb9539d2658000168e0d3978eb94beb5b0a07174922049fcadc68
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.
Transparency logRelease files / vbar-0.2.0-py3-none-any.whl
| Download URL | vbar-0.2.0-py3-none-any.whl |
|---|---|
| Size | 76.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c0499711e57aee55f1158b213d7d788978cb0c1ef0d7fe12fca7bb5f8e473b71
|
|
BLAKE2b-256 checksum How to use checksums |
d4021b0f94e8296ea61a09289d41c320f79f82002bd5b3d4d39adba81e0b831e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.
Transparency logSecurity for vbar 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for vbar-0.2.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.