OSTK Code Example
This document contains confidential data and is not intended to be shared outside of SAGE!
| Authors |
|
| Responsible |
|
| Last Updated | 10/12/2025, 12:48:54 PM |
| Last Author | Kai Berszin |
What's the purpose of this code
This code was developed to provide an example of how OSTK can be used to simulate both an orbit (the MOPS team's main task) and attitude (the ADCS team's main task). The task is to simulate a satellite's orbit and orient it toward the ground station when there is access, and toward the Sun otherwise. The code can also be used to compare orbit propagation results as predicted by OSTK with the predictions from other software such as GMAT. It can be found in sage-flight-dynamics\flight_dynamics\orbit_propagation\ostk under the name DetectAndPoint.py or on https://github.com/aris-space/sage-flight-dynamics
The code primarily takes three types of input:
- Orbit characteristics (altitude, local time at descending node, etc.) that you can change within the code
- Simulation times (initial date-time, timestep, end instant) that you can change within the code
- Information on the ground station (name, latitude, longitude, altitude, etc.) that can be changed in ground_stations.csv. For this code, only the first ground station is important.
In return, it creates (too many) CSV files:
- access_geometries.csv: Pointing information during accesses
- accesses.csv: Access summaries
- orbit.csv: Orbit data (position, velocities, etc.)
- yprs_full.csv: Attitude of the satellite for all timestamps
- yprs_during_access.csv: Attitude of the satellite only during accesses
Altogether, the files contain all the different data we need:
- Timestamps (UTC)
- Julian Date or MJD (UTC)
- 3D Positions (GCRF = Earth-centered, non-rotating inertial frame)
- 3D Velocities (GCRF)
- Latitude, Longitude, and Altitude
- During accesses: Azimuth, Elevation, and Range
- Yaw, Pitch, and Roll : they are rotations around the z-, y-, and x-axes respectively, in the VVLH frame (x is orbital velocity, z is nadir)
- Boolean indicating whether the satellite is in eclipse and/or in access
- During accesses: Acquisition of Signal (AOS), Time of Closest Approach (TCA), Loss of Signal (LOS), and duration of the access
Discussion of results
Examples of Pitch evolution are given below:

In the two plots, we can clearly see 4 discontinuous changes in attitude. These changes correspond exactly to the access moments and correctly point to the ground station. The access detection should be accurate because it relies on an OSTK sample code, orbit_and_accesses_example.py. The ground pointing is also assumed correct, as it was demonstrated in another OSTK sample code, Target Tracking.ipynb. The Sun pointing has been verified since the code also works well with Earth pointing (we can confirm this by inspecting the pitch and roll, which should be very small during Earth pointing because of the VVLH reference frame).
What should be improved
- The attitude changes are performed with an ideal model SGP4
- Ideally, the code should only return three files:
- access_geometries.csv: a summary of each access. ✅
- all_timesteps_orbit_attitude.csv: a CSV containing all timestamps with UTC time, MJD, 3D positions and velocities, latitude, longitude, altitude, eclipse indicator, yaw, pitch, and roll. ❌
- only_access_orbit_attitude.csv: same as all_timesteps_orbit_attitude.csv but only during accesses, including azimuth, elevation, and range : useful data to provide to the ground station so they know where to point and when. ❌
All these data are already created, and we could merge the first 12 columns of orbit.csv, the 5th, 6th, and 7th columns of access.csv, and the last 3 columns of yprs_full.csv (or yprs_during_access.csv). However, I have not managed to merge the data...
Trying with this code:
merged_df = orbit_df.copy()
merged_df.set_index(pd.to_datetime(merged_df["$Time^{UTC}$"]), inplace=True)
merged_df.index = merged_df.index.round("s")
yprs_df = all_yprs_df.copy()
yprs_df.index = pd.to_datetime(yprs_df.index).round("s")
merged_df = merged_df.join(yprs_df[["Yaw [deg]", "Pitch [deg]", "Roll [deg]"]], how="left")
merged_df.reset_index(drop=True, inplace=True)
merged_df[
[
"$Time^{UTC}$",
"$MJD^{UTC}$",
"$x_{sat}^{GCRF}$",
"$y_{sat}^{GCRF}$",
"$z_{sat}^{GCRF}$",
"$vx_{sat}^{GCRF}$",
"$vy_{sat}^{GCRF}$",
"$vz_{sat}^{GCRF}$",
"Latitude",
"Longitude",
"Altitude",
"In Eclipse",
"Yaw [deg]",
"Pitch [deg]",
"Roll [deg]",
]
].to_csv("/console/all_timesteps_orbit_attitude.csv", index=False)
access_geometries_df["Time"] = pd.to_datetime(access_geometries_df["Time"]).dt.round("s")
merged_df["$Time^{UTC}$"] = pd.to_datetime(merged_df["$Time^{UTC}$"]).dt.round("s")
only_access_df = pd.merge(
merged_df[merged_df["In Access"] == 1],
access_geometries_df[["Time", "Azimuth", "Elevation", "Range"]],
left_on="$Time^{UTC}$",
right_on="Time",
how="left",
)
only_access_df[
[
"$Time^{UTC}$",
"$MJD^{UTC}$",
"$x_{sat}^{GCRF}$",
"$y_{sat}^{GCRF}$",
"$z_{sat}^{GCRF}$",
"$vx_{sat}^{GCRF}$",
"$vy_{sat}^{GCRF}$",
"$vz_{sat}^{GCRF}$",
"Latitude",
"Longitude",
"Altitude",
"In Eclipse",
"Yaw [deg]",
"Pitch [deg]",
"Roll [deg]",
"Azimuth",
"Elevation",
"Range",
]
].to_csv("/console/only_access_orbit_attitude.csv", index=False)
I get errors I couldn't fix... Getting only 3 files instead of the current 5 is probably the most important improvement.
Changelog
| Date | Revision | Change |
|---|---|---|
| 2025/07/11 | 01 | Initial commit |
| 2025/07/13 | 02 | Last commit of first version of the code |
| 2025/07/31 | 03 | Minor corrections |