Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ All notable changes to this project will be documented here.
# Unreleased
- Certain orbital parameters can be displayed in the plot
- PerifocalOrbitEq refactored to live in the Satellite class
- Better separation of orbital entity behaviour and calculation

# 0.4.3 - 09/02/2026
- Implemented automatic testing
Expand Down
45 changes: 45 additions & 0 deletions src/orbit_visualiser/core/common/orbit_formulae.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import numpy as np
from numpy.typing import NDArray
from typing import Callable

def perifocal_position_eq(e: float, p: float) -> Callable[[float], NDArray[np.float64]]:
"""
Perifocal orbit equation factory.

Parameters
----------
e : float
Eccentricity
p : float
Semi-parameter

Returns
-------
Callable[[float], NDArray[np.float64]]
The perifocal orbit equation, taking the true anomaly as an argument
"""
def _callable(nu: float) -> NDArray[np.float64]:
return p*(1/(1 + e*np.cos(nu)))*np.array([np.cos(nu), np.sin(nu)])
return _callable

def perifocal_velocity_eq(e: float, mu: float, h: float) -> Callable[[float], NDArray[np.float64]]:
"""
Perifocal velocity equation factory.

Parameters
----------
e : float
Eccentricity
mu : float
Gravitational parameter
h : float
Specific angular momentum

Returns
-------
Callable[[float], NDArray[np.float64]]
The perifocal velocity equation, taking the true anomaly as an argument
"""
def _callable(nu: float) -> NDArray[np.float64]:
return (mu/h)*np.array([-np.sin(nu), e + np.cos(nu)])
return _callable
24 changes: 2 additions & 22 deletions src/orbit_visualiser/core/orbit.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,9 @@
from typing import Callable
from dataclasses import dataclass
from math import pi
import numpy as np

# TODO: Move PerifocalOrbitEq to satellite.py.
# TODO: Expand PerifocalOrbitEq to include velocity calculation as well.
class PerifocalOrbitEq():

def __init__(self, e: float, p: float):
self._x: Callable[[float], float] = lambda t : p*(np.cos(t)/(1+e*np.cos(t)))
self._y: Callable[[float], float] = lambda t : p*(np.sin(t)/(1+e*np.cos(t)))

@property
def x(self) -> Callable[[float], float]:
return self._x

@property
def y(self) -> Callable[[float], float]:
return self._y


# TODO: Split formulae from Orbit class.
# TODO: Update orbit properties when eccentricity of radius of periapsis is set rather than calling it explicitly outside the class
class Orbit():


Expand Down Expand Up @@ -81,10 +65,6 @@ def orbit_type(self) -> str:
def is_closed(self) -> bool:
return self._is_closed

@property
def orbit_eq(self) -> PerifocalOrbitEq:
return PerifocalOrbitEq(self._e, self._p)

def orbital_angles(self) -> tuple[float]:
if self._e < 1:
return 0, 2*pi
Expand Down
38 changes: 29 additions & 9 deletions src/orbit_visualiser/core/satellite.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import numpy as np
from numpy.typing import ArrayLike
from numpy.typing import NDArray
from typing import Callable
from math import pi
from orbit_visualiser.core import Orbit, CentralBody
from orbit_visualiser.core.common.orbit_formulae import perifocal_position_eq, perifocal_velocity_eq
from orbit_visualiser.core.orbit import Orbit, CentralBody

# TODO: Split formulae from Satellite class.
# TODO: Update satellite properties when true anomaly is set rather than calling it explicitly outside the class
class Satellite():


Expand All @@ -22,11 +26,19 @@ def nu(self, nu: float) -> None:
self._nu = nu

@property
def pos_pf(self) -> ArrayLike:
def pos_pf_eq(self) -> Callable[[float], NDArray[np.float64]]:
return self._pos_pf_eq

@property
def vel_pf_eq(self) -> Callable[[float], NDArray[np.float64]]:
return self._vel_pf_eq

@property
def pos_pf(self) -> NDArray:
return self._pos_pf

@property
def vel_pf(self) -> ArrayLike:
def vel_pf(self) -> NDArray:
return self._vel_pf

@property
Expand Down Expand Up @@ -94,6 +106,10 @@ def update_satellite_properties(self) -> None:
h, t_asymp, a = self._specific_ang_momentum(mu, p), self._orbit.t_asymp, self._orbit.a
self._h = h

# Perifocal equations
self._pos_pf_eq = perifocal_position_eq(e, p)
self._vel_pf_eq = perifocal_velocity_eq(e, mu, h)

# Helper quantities
den = 1 + e*np.cos(nu) # Denominator of the orbit equation
mu_over_h = mu/h
Expand All @@ -103,6 +119,7 @@ def update_satellite_properties(self) -> None:
self._r = self._radius(nu, t_asymp, p, den)

# Kinematics
self._vel_pf = self._pf_velocity(nu)
v_azim = self._azimuthal_velocity(nu, t_asymp, mu_over_h, den)
self._v_azim = v_azim
v_radial = self._radial_velocity(mu_over_h, e, nu)
Expand Down Expand Up @@ -130,15 +147,18 @@ def update_satellite_properties(self) -> None:
def _specific_ang_momentum(mu: float, p: float) -> float:
return np.sqrt(mu*p)

def _pf_position(self, nu: float, t_asymp: float) -> tuple[float]:
orbit_eq = self._orbit.orbit_eq
def _pf_position(self, nu: float, t_asymp: float) -> NDArray[np.float64]:
pos_eq = self._pos_pf_eq

if np.isclose(abs(nu), t_asymp, atol = 0.0001, rtol = 0):
nu_offset = (nu/abs(nu))*np.deg2rad(0.01)
x_close_to_inf, y_close_to_inf = orbit_eq.x(nu - nu_offset), orbit_eq.y(nu - nu_offset)
return (x_close_to_inf/abs(x_close_to_inf))*np.inf, (y_close_to_inf/abs(y_close_to_inf))*np.inf
x_close_to_inf, y_close_to_inf = pos_eq(nu_offset)
return np.array([(x_close_to_inf/abs(x_close_to_inf))*np.inf, (y_close_to_inf/abs(y_close_to_inf))*np.inf])

return orbit_eq.x(nu), orbit_eq.y(nu)
return pos_eq(nu)

def _pf_velocity(self, nu: float) -> NDArray[np.float64]:
return self._vel_pf_eq(nu)

def _azimuthal_velocity(self, nu: float, t_asymp: float, mu_over_h: float, den: float) -> float:
if np.isclose(abs(nu), t_asymp, atol = 0.0001, rtol = 0):
Expand Down
2 changes: 1 addition & 1 deletion src/orbit_visualiser/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def __init__(self, root: Tk):

# TODO: Write tests as I go.
# TODO: Add variable presets (Earth - ISS, Earth - Geostationary, Mars - Phobos etc).
# Test change
# TODO: Write proper docstrings
if __name__ == "__main__":
root = Tk()

Expand Down
13 changes: 6 additions & 7 deletions src/orbit_visualiser/ui/figure/orbit_figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,9 @@ def _configure_axes(self) -> None:

def _initialise_plot(self) -> None:
# Plot the initial orbit
t = self._get_anomaly_data(self._orbit.orbital_angles())
orbit_eq = self._orbit.orbit_eq
x, y = orbit_eq.x, orbit_eq.y
self._line, = self._ax.plot(x(t) , y(t), color = "#2F2F2F", alpha = 0.5, linewidth = 1.5)
nu = self._get_anomaly_data(self._orbit.orbital_angles())
x, y = self._sat.pos_pf_eq(nu)
self._line, = self._ax.plot(x, y, color = "#2F2F2F", alpha = 0.5, linewidth = 1.5)

# Plot the central body
self._ax.add_patch(
Expand Down Expand Up @@ -118,9 +117,9 @@ def _get_anomaly_data(self, angles: tuple[float]) -> NDArray[np.float64]:
return np.linspace(lower_lim + delta, upper_lim - delta, OrbitFigure.NUM_POINTS)

def redraw_orbit(self) -> None:
t = self._get_anomaly_data(self._orbit.orbital_angles())
orbit_eq = self._orbit.orbit_eq
self._line.set_data(orbit_eq.x(t), orbit_eq.y(t))
nu = self._get_anomaly_data(self._orbit.orbital_angles())
x, y = self._sat.pos_pf_eq(nu)
self._line.set_data(x, y)

self._canvas.draw_idle()

Expand Down