The orbits module provides functions to compute essential properties of satellite orbits, including orbital period, mean motion, periapsis/apoapsis characteristics, and specialized orbits like sun-synchronous configurations. These properties are fundamental for mission design, orbit determination, and trajectory analysis.
The orbital period \(T\) of a satellite is the time it takes to complete one full revolution around the central body. It is related to the semi-major axis \(a\) and gravitational parameter \(\mu\) by:
\[ T = 2\pi\sqrt{\frac{a^3}{\mu}} \]
The orbital_period function computes the period for Earth-orbiting objects, while orbital_period_general accepts an explicit gravitational parameter for any celestial body.
importbraheasbhbh.initialize_eop()# Define orbit parametersa=bh.R_EARTH+500.0e3# Semi-major axis (m) - LEO orbit at 500 km altitude# Compute orbital period for Earth orbit (uses GM_EARTH internally)period_earth=bh.orbital_period(a)print(f"Orbital period (Earth): {period_earth:.3f} s")print(f"Orbital period (Earth): {period_earth/60:.3f} min")# Compute orbital period for general body (explicit GM)period_general=bh.orbital_period_general(a,bh.GM_EARTH)print(f"Orbital period (general): {period_general:.3f} s")# Verify they matchprint(f"Difference: {abs(period_earth-period_general):.2e} s")# Example with approximate GEO altitudea_geo=bh.R_EARTH+35786e3period_geo=bh.orbital_period(a_geo)print(f"\nGEO orbital period: {period_geo/3600:.3f} hours")# Expected output:# Orbital period (Earth): 5676.977 s# Orbital period (Earth): 94.616 min# Orbital period (general): 5676.977 s# Difference: 0.00e0 s# GEO orbital period: 23.934 hours
usebraheasbh;fnmain(){bh::initialize_eop().unwrap();// Define orbit parametersleta=bh::constants::R_EARTH+500.0e3;// Semi-major axis (m) - LEO orbit at 500 km altitude// Compute orbital period for Earth orbit (uses GM_EARTH internally)letperiod_earth=bh::orbits::orbital_period(a);println!("Orbital period (Earth): {:.3} s",period_earth);println!("Orbital period (Earth): {:.3} min",period_earth/60.0);// Compute orbital period for general body (explicit GM)letperiod_general=bh::orbits::orbital_period_general(a,bh::constants::GM_EARTH);println!("Orbital period (general): {:.3} s",period_general);// Verify they matchprintln!("Difference: {:.2e} s",(period_earth-period_general).abs());// Example with approximate GEO altitudeleta_geo=bh::constants::R_EARTH+35786e3;letperiod_geo=bh::orbits::orbital_period(a_geo);println!("\nGEO orbital period: {:.3} hours",period_geo/3600.0);// Expected output:// Orbital period (Earth): 5676.977 s// Orbital period (Earth): 94.616 min// Orbital period (general): 5676.977 s// Difference: 0.00e0 s// GEO orbital period: 23.934 hours}
The plot below shows how orbital period and velocity vary with altitude for circular Earth orbits:
importosimportpathlibimportsysimportnumpyasnpimportplotly.graph_objectsasgofromplotly.subplotsimportmake_subplotsimportbraheasbh# Add plots directory to path for importing brahe_themesys.path.insert(0,str(pathlib.Path(__file__).parent))frombrahe_themeimportget_theme_colors,save_themed_html# ConfigurationSCRIPT_NAME=pathlib.Path(__file__).stemOUTDIR=pathlib.Path(os.getenv("BRAHE_FIGURE_OUTPUT_DIR","./docs/figures/"))# Ensure output directory existsos.makedirs(OUTDIR,exist_ok=True)# Generate data# Generate range of altitudes from 0 to 40,000 km in 500 km incrementsalt=np.arange(0,41000*1e3,500*1e3)# Compute velocity over altitude (km/s)vp=[bh.perigee_velocity(bh.R_EARTH+a,0.0)/1e3forainalt]# Compute orbital period over altitude (hours)period=[bh.orbital_period(bh.R_EARTH+a)/3600forainalt]# Create figure with theme supportdefcreate_figure(theme):"""Create figure with theme-specific colors."""colors=get_theme_colors(theme)# Create subplot with secondary y-axisfig=make_subplots(specs=[[{"secondary_y":True}]])# Add velocity trace (primary y-axis)fig.add_trace(go.Scatter(x=alt/1e6,y=vp,mode="lines",line=dict(color=colors["primary"],width=2),name="Velocity",showlegend=True,),secondary_y=False,)# Add orbital period trace (secondary y-axis)fig.add_trace(go.Scatter(x=alt/1e6,y=period,mode="lines",line=dict(color=colors["secondary"],width=2),name="Orbital Period",showlegend=True,),secondary_y=True,)# Configure primary x-axisfig.update_xaxes(tickmode="linear",tick0=0,dtick=5,title_text="Satellite Altitude [1000 km]",range=[0,40],showgrid=False,)# Configure primary y-axis (velocity)fig.update_yaxes(tickmode="linear",tick0=0,dtick=1,title_text="Velocity [km/s]",range=[0,10],showgrid=False,secondary_y=False,)# Configure secondary y-axis (period)fig.update_yaxes(tickmode="linear",tick0=0,dtick=5,title_text="Orbital Period [hours]",range=[0,30],showgrid=False,secondary_y=True,)returnfig# Generate and save both themed versionslight_path,dark_path=save_themed_html(create_figure,OUTDIR/SCRIPT_NAME)print(f"✓ Generated {light_path}")print(f"✓ Generated {dark_path}")
When orbital elements are unknown but you have a Cartesian state vector, orbital_period_from_state computes the period directly from position and velocity:
importbraheasbhimportnumpyasnpbh.initialize_eop()# Define orbital elements for a LEO satellitea=bh.R_EARTH+500.0e3# Semi-major axis (m)e=0.01# Eccentricityi=97.8# Inclination (degrees)raan=15.0# Right ascension of ascending node (degrees)argp=30.0# Argument of periapsis (degrees)nu=45.0# True anomaly (degrees)# Convert to Cartesian stateoe=np.array([a,e,i,raan,argp,nu])state_eci=bh.state_koe_to_eci(oe,bh.AngleFormat.DEGREES)print("ECI State (position in km, velocity in km/s):")print(f" r = [{state_eci[0]/1e3:.3f}, {state_eci[1]/1e3:.3f}, {state_eci[2]/1e3:.3f}] km")print(f" v = [{state_eci[3]/1e3:.3f}, {state_eci[4]/1e3:.3f}, {state_eci[5]/1e3:.3f}] km/s")# Compute orbital period from state vectorperiod=bh.orbital_period_from_state(state_eci,bh.GM_EARTH)print(f"\nOrbital period from state: {period:.3f} s")print(f"Orbital period from state: {period/60:.3f} min")# Verify against period computed from semi-major axisperiod_from_sma=bh.orbital_period(a)print(f"\nOrbital period from SMA: {period_from_sma:.3f} s")print(f"Difference: {abs(period-period_from_sma):.2e} s")# Expected output:# ECI State (position in km, velocity in km/s):# r = [1848.964, -434.937, 6560.411] km# v = [-7.098, -2.173, 1.913] km/s# Orbital period from state: 5676.977 s# Orbital period from state: 94.616 min# Orbital period from SMA: 5676.977 s# Difference: 3.64e-12 s
usebraheasbh;usenalgebraasna;fnmain(){bh::initialize_eop().unwrap();// Define orbital elements for a LEO satelliteleta=bh::constants::R_EARTH+500.0e3;// Semi-major axis (m)lete=0.01;// Eccentricityleti=97.8;// Inclination (degrees)letraan=15.0;// Right ascension of ascending node (degrees)letargp=30.0;// Argument of periapsis (degrees)letnu=45.0;// True anomaly (degrees)// Convert to Cartesian stateletoe=na::SVector::<f64,6>::new(a,e,i,raan,argp,nu);letstate_eci=bh::state_koe_to_eci(oe,bh::constants::AngleFormat::Degrees);println!("ECI State (position in km, velocity in km/s):");println!(" r = [{:.3}, {:.3}, {:.3}] km",state_eci[0]/1e3,state_eci[1]/1e3,state_eci[2]/1e3);println!(" v = [{:.3}, {:.3}, {:.3}] km/s",state_eci[3]/1e3,state_eci[4]/1e3,state_eci[5]/1e3);// Compute orbital period from state vectorletperiod=bh::orbits::orbital_period_from_state(&state_eci,bh::constants::GM_EARTH);println!("\nOrbital period from state: {:.3} s",period);println!("Orbital period from state: {:.3} min",period/60.0);// Verify against period computed from semi-major axisletperiod_from_sma=bh::orbits::orbital_period(a);println!("\nOrbital period from SMA: {:.3} s",period_from_sma);println!("Difference: {:.2e} s",(period-period_from_sma).abs());// Expected output:// ECI State (position in km, velocity in km/s):// r = [1848.964, -434.937, 6560.411] km// v = [-7.098, -2.173, 1.913] km/s// Orbital period from state: 5676.977 s// Orbital period from state: 94.616 min// Orbital period from SMA: 5676.977 s// Difference: 3.64e-12 s}
A satellite's average angular rate over one orbit is its mean motion\(n\), calculated from the semi-major axis and gravitational parameter:
\[ n = \sqrt{\frac{\mu}{a^3}} \]
The mean_motion function computes this for Earth-orbiting objects, while mean_motion_general works for any celestial body. Both functions support output in radians or degrees per second via the angle_format parameter.
importbraheasbhbh.initialize_eop()# Define orbit parametersa_leo=bh.R_EARTH+500.0e3# LEO satellite at 500 km altitudea_geo=bh.R_EARTH+35786e3# GEO satellite# Compute mean motion in radians/s (Earth-specific)n_leo_rad=bh.mean_motion(a_leo,bh.AngleFormat.RADIANS)n_geo_rad=bh.mean_motion(a_geo,bh.AngleFormat.RADIANS)print("Mean Motion in radians/second:")print(f" LEO (500 km): {n_leo_rad:.6f} rad/s")print(f" GEO: {n_geo_rad:.6f} rad/s")# Compute mean motion in degrees/sn_leo_deg=bh.mean_motion(a_leo,bh.AngleFormat.DEGREES)n_geo_deg=bh.mean_motion(a_geo,bh.AngleFormat.DEGREES)print("\nMean Motion in degrees/second:")print(f" LEO (500 km): {n_leo_deg:.6f} deg/s")print(f" GEO: {n_geo_deg:.6f} deg/s")# Convert to degrees/day (common unit for TLEs)print("\nMean Motion in degrees/day:")print(f" LEO (500 km): {n_leo_deg*86400:.3f} deg/day")print(f" GEO: {n_geo_deg*86400:.3f} deg/day")# Verify using general functionn_leo_general=bh.mean_motion_general(a_leo,bh.GM_EARTH,bh.AngleFormat.RADIANS)print(f"\nVerification (general function): {n_leo_general:.6f} rad/s")print(f"Difference: {abs(n_leo_rad-n_leo_general):.2e} rad/s")# Expected output:# Mean Motion in radians/second:# LEO (500 km): 0.001107 rad/s# GEO: 0.000073 rad/s# Mean Motion in degrees/second:# LEO (500 km): 0.063414 deg/s# GEO: 0.004178 deg/s# Mean Motion in degrees/day:# LEO (500 km): 5478.972 deg/day# GEO: 360.986 deg/day# Verification (general function): 0.001107 rad/s# Difference: 0.00e+00 rad/s
usebraheasbh;fnmain(){bh::initialize_eop().unwrap();// Define orbit parametersleta_leo=bh::constants::R_EARTH+500.0e3;// LEO satellite at 500 km altitudeleta_geo=bh::constants::R_EARTH+35786e3;// GEO satellite// Compute mean motion in radians/s (Earth-specific)letn_leo_rad=bh::orbits::mean_motion(a_leo,bh::constants::AngleFormat::Radians);letn_geo_rad=bh::orbits::mean_motion(a_geo,bh::constants::AngleFormat::Radians);println!("Mean Motion in radians/second:");println!(" LEO (500 km): {:.6} rad/s",n_leo_rad);println!(" GEO: {:.6} rad/s",n_geo_rad);// Compute mean motion in degrees/sletn_leo_deg=bh::orbits::mean_motion(a_leo,bh::constants::AngleFormat::Degrees);letn_geo_deg=bh::orbits::mean_motion(a_geo,bh::constants::AngleFormat::Degrees);println!("\nMean Motion in degrees/second:");println!(" LEO (500 km): {:.6} deg/s",n_leo_deg);println!(" GEO: {:.6} deg/s",n_geo_deg);// Convert to degrees/day (common unit for TLEs)println!("\nMean Motion in degrees/day:");println!(" LEO (500 km): {:.3} deg/day",n_leo_deg*86400.0);println!(" GEO: {:.3} deg/day",n_geo_deg*86400.0);// Verify using general functionletn_leo_general=bh::orbits::mean_motion_general(a_leo,bh::constants::GM_EARTH,bh::constants::AngleFormat::Radians);println!("\nVerification (general function): {:.6} rad/s",n_leo_general);println!("Difference: {:.2e} rad/s",(n_leo_rad-n_leo_general).abs());// Expected output:// Mean Motion in radians/second:// LEO (500 km): 0.001107 rad/s// GEO: 0.000073 rad/s// Mean Motion in degrees/second:// LEO (500 km): 0.063414 deg/s// GEO: 0.004178 deg/s// Mean Motion in degrees/day:// LEO (500 km): 5478.972 deg/day// GEO: 360.986 deg/day// Verification (general function): 0.001107 rad/s// Difference: 0.00e+00 rad/s}
The periapsis is the point of closest approach to the central body, where orbital velocity is greatest.
Info
The word periapsis is formed by combination of the Greek words "peri-" (meaning around, about) and "apsis" (meaning "arch or vault"). An apsis is the farthest or nearest point in the orbit of a planetary body about its primary body.
Therefore periapsis is the point of closest approach of the orbiting body with respect to its central body. The suffix can be modified to indicate the closest approach to a specific celestial body: perigee for Earth, perihelion for the Sun.
Brahe provides functions to compute periapsis velocity, distance, and altitude based on orbital elements.
The periapsis altitude is the height above the surface of the central body:
\[ h_p = r_p - R_{body} = a(1-e) - R_{body} \]
where \(R_{body}\) is the radius of the central body. For Earth orbits, the perigee_altitude function provides a convenient wrapper using \(R_{\oplus}\).
The apoapsis is the farthest point from the central body, where orbital velocity is lowest.
Info
The word apoapsis is formed by combination of the Greek words "apo-" (meaning away from, separate, or apart from) and "apsis".
Therefore apoapsis is the farthest point of an orbiting body with respect to its central body. The suffix can be modified to indicate the farthest point from a specific celestial body: apogee for Earth, aphelion for the Sun.
Brahe provides functions to compute apoapsis velocity, distance, and altitude based on orbital elements.
Warning
Apoapsis position, velocity, and altitude are only defined for elliptic and circular orbits. For parabolic and hyperbolic orbits, these quantities are undefined.
The apoapsis altitude is the height above the surface of the central body:
\[ h_a = r_a - R_{body} = a(1+e) - R_{body} \]
where \(R_{body}\) is the radius of the central body. For Earth orbits, the apogee_altitude function provides a convenient wrapper using \(R_{\oplus}\).
A sun-synchronous orbit maintains a constant angle relative to the Sun by matching its nodal precession rate to Earth's annual revolution. The right ascension of the ascending node (\(\Omega\)) advances at the same rate as the Sun's apparent motion: approximately 0.9856°/day. This configuration is highly valuable for Earth observation satellites requiring consistent illumination conditions—a sun-synchronous satellite crosses the equator at the same local time on each pass (e.g., always at 2 PM).
Earth's oblateness, characterized by the \(J_2\) zonal harmonic, causes secular drift in \(\Omega\):
importosimportpathlibimportsysimportnumpyasnpimportplotly.graph_objectsasgoimportbraheasbh# Add plots directory to path for importing brahe_themesys.path.insert(0,str(pathlib.Path(__file__).parent))frombrahe_themeimportget_theme_colors,save_themed_html# ConfigurationSCRIPT_NAME=pathlib.Path(__file__).stemOUTDIR=pathlib.Path(os.getenv("BRAHE_FIGURE_OUTPUT_DIR","./docs/figures/"))# Ensure output directory existsos.makedirs(OUTDIR,exist_ok=True)# Generate data# Generate range of altitudes from 300 to 1000 km in 1 km incrementsalt=np.arange(300e3,1000e3,1e3)# Compute sun-synchronous inclination for range of eccentricitieseccentricities=[0.0,0.1,0.3,0.5]ssi_data={}foreineccentricities:ssi_data[e]=[bh.sun_synchronous_inclination(bh.R_EARTH+a,e,angle_format=bh.AngleFormat.DEGREES)forainalt]# Create figure with theme supportdefcreate_figure(theme):"""Create figure with theme-specific colors."""colors=get_theme_colors(theme)fig=go.Figure()# Color palette for different eccentricitiescolor_palette=[colors["primary"],colors["secondary"],colors["accent"],colors["error"],]# Add traces for each eccentricityfori,einenumerate(eccentricities):fig.add_trace(go.Scatter(x=alt/1e3,y=ssi_data[e],mode="lines",line=dict(color=color_palette[i%len(color_palette)],width=2),name=f"e = {e:.1f}",showlegend=True,))# Configure axesfig.update_xaxes(tickmode="linear",tick0=300,dtick=100,title_text="Satellite Altitude [km]",range=[300,1000],showgrid=False,)fig.update_yaxes(tickmode="linear",title_text="Inclination [deg]",showgrid=False,)returnfig# Generate and save both themed versionslight_path,dark_path=save_themed_html(create_figure,OUTDIR/SCRIPT_NAME)print(f"✓ Generated {light_path}")print(f"✓ Generated {dark_path}")
Most sun-synchronous Earth observation missions operate at altitudes between 500-1000 km with near-zero eccentricity. The launch provider selects the precise inclination based on the above equation to achieve the desired sun-synchronous behavior.