The Orbit Visualiser is a 3D Keplerian orbit visualisation tool for modelling the motion of a satellite around a central body, written in Python. The focus of this tool is on orbital geometry and satellite kinematics. Has basic orbital propagation (currently no GUI functionality).
Orbits are modelled and visualised in the Earth Centred Equatorial (ECI) frame, with the orbital geometry parametrised using:
- Eccentricity
- Radius of periapsis
- Right ascension of the ascending node
- Inclination
- Argument of periapsis
The central body has a radius of 6738km and an adjustable gravitational parameter. The radius of periapsis has therefore been given a lower limit of 6739km (meaning that we are assuming this Earth sized body has no atmosphere, or at least that any atmosphere has no effect on orbital motion). The true anomaly of the orbiting satellite is also adjustable to evaluate the kinematic state at different orbital positions. Various orbital and kinematic quantities are calculated and displayed, including (but not limited to):
- Semi-major/minor axis
- Radius of apoapsis
- True anomaly of the asymptote (for parabolic and hyperbolic trajectories)
- Specific angular momentum
- Radial velocity
- Azimuthal velocity
- Escape velocity
- Hyperbolic excess velocity
- Time since periapsis
See the gif above for the full list of displayed orbital and kinematic properties.
There are several features that are in development or planned:
- Additional display options
- A web interface
- Option to change reference frame
- Option to change parametrisation of orbital geometry
The orbiting satellite is assumed to have negligible mass. All higher-order perturbations, such as atmospheric drag or oblateness, are ignored.
- Improve simulation of orbital motion
- Include perturbations into the model
- Expand modelling to R3BP or N-body system
- Visualise gravitational field
- Python 3.8 or later
- Git (optional but recommended, for cloning the repository)
- numpy
- matplotlib
- scipy
These are also listed in requirements.txt.
Clone the repository:
git clone https://github.com/Phim226/orbit-visualiser.gitNavigate to the project folder:
cd orbit-visualiserCreate a virtual environment:
python3 -m venv .venvActivate the environment:
Windows Powershell:
.venv\Scripts\Activate.ps1-
Warning: On some Windows systems, Powershell might block this command depending on the script execution policy. If you see an error like:
...\.venv\Scripts\Activate.ps1 cannot be loaded because running scripts is disabled on this system.
Then you can run:
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process
to temporarily allow scripts in the current terminal session.
Windows Command Prompt:
.venv\Scripts\activate.batThis should work without changing any execution policies.
source .venv/bin/activateIf you see (.venv) at the start of your prompt, then the virtual environment is active.
Install dependencies and the package in editable mode (make sure this is done within the virtual environment):
python3 -m pip install --upgrade pip
pip install -r requirements.txtThe -e in requirements.txt ensures that the package is editable, which ensures that any changes to the source code are immediately reflected when running the program.
Run the program:
python src/orbit_visualiser/main.pyWhen you're finished using the program or making changes then run:
deactivateto deactivate the virtual environment. When returning to the program, activate venv again using the commands above. So long as the project folder or .venv folder hasn't changed then you shouldn't need to reinstall any dependencies.
As of the current version (v0.4.5) there is no CLI or GUI functionality for the orbit propagation. Navigate to src/orbit_visualiser/core/propagation.py, and go to the bottom of the file to the code snippet:
if __name__ == "__main__":
orbit = Orbit.from_orbital_elements(e = 0.0, rp = 50_000.0, nu = 0.0, raan = 0.0, i = 0.0,
omega = 0.0, mu = 398_600.0,)
sol = run_orbit_prop(orbit, orbit.orbital_period)
init_conditions = get_init_conditions_from_orbit(orbit)
print(init_conditions)
print(sol.y[:, -1])
r0 = np.linalg.norm(init_conditions[:3])
rf = np.linalg.norm(sol.y[:3, -1])
print(f"Difference in radius after propagating: {rf - r0}")The orbit propagation function takes the Orbit object and propagation end time (the start time is always 0, and begins the propagation at the true anomaly (nu) set in the constructor Orbit.from_orbital_elements). The values set in the code snippet above result in the propagation of a circular orbit of radius 50,000km, starting at the perifocal position (50000, 0, 0) lasting for one orbital period. For non-circular orbits nu = 0.0 is periapsis. The true anomaly is in radians, so nu = pi is apoapsis.
In order to get accurate results you should keep the end time on the order of 10 or at most 100 periods. The integrators used by scipy aren't symplectic, so there is significant energy drift over longer propagations.
There is a third optional argument of run_orbit_prop called period_frac_per_step: int = 500, which determines the time step as a function of the fraction of a single period. So the default value results in a time step equal to 1/500 of the orbital period (rounded up to the nearest integer).
The current print statements are just quick sanity checks to show that after a whole number of orbits the satellite is returning to roughly the same spot and with roughly the same velocity.
As an example if you wanted to propagate the ISS then you might edit the above code snippet to:
if __name__ == "__main__":
# Approximate eccentricity, radius of periapsis (km), right ascension of the ascending node (rad),
# inclination (rad) and argument of perigee (rad) for the ISS, with mu representing the
# gravitational parameter of earth in km^3/s^2
iss_orbit = Orbit.from_orbital_elements(e = 0.0002267, rp = 6778, nu = 0.0, raan = 4.319,
i = 0.901, omega = 2.278, mu = 398_600.0)
# Runs the propagation for 1 period, returning the ISS back to periapsis
sol = run_orbit_prop(iss_orbit, iss_orbit.orbital_period)
init_conditions = get_init_conditions_from_orbit(iss_orbit)
print(init_conditions)
# Grabs the array of solutions (sol.y) and prints the last entry
print(sol.y[:, -1])
r0 = np.linalg.norm(init_conditions[:3])
rf = np.linalg.norm(sol.y[:3, -1])
print(f"Difference in radius after propagating: {rf - r0}")Then running the script will run the propagation and print out the results.