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
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ In ACCORD, satellites use on-board sensors to observe one another and collaborat

This decentralised approach enhances data integrity, trust, and resilience across heterogeneous constellations. As more satellites join the network, ACCORD scales naturally, enabling secure and autonomous satellite operations—even in zero-trust environments.

![Satellite Constellation Animation](images/orbits.gif)

This code is licensed under a GNU General Public License v3.0.

# Current Status
Expand All @@ -19,20 +21,20 @@ The project is currently at TRL 0. The PoISE consensus mechanism is in the early

## Citation
If you use this work, please cite it as:
> B. Probert, bprobert97/accord: v2.2. (Feb. 25, 2026). Python. University of Strathclyde, Glasgow. [DOI: 10.5281/zenodo.18776049](https://doi.org/10.5281/zenodo.18776049)
> B. Probert, bprobert97/accord: v3.0. (Mar. 24, 2026). Python. University of Strathclyde, Glasgow. [DOI: 10.5281/zenodo.19206200](https://doi.org/10.5281/zenodo.19206200)


# Repository Layout

<pre>
📁 accord/
├── 📁 .github/workflows/ # GitHub Workflow files
│ └── main.yml # CI configuration for github: Pylint, Mypy and Pytest
├── 📁 .github/workflows/ # GitHub Workflow files
│ └── main.yml # CI configuration for github: Pylint, Mypy and Pytest
├── 📁 design/ # Design documents, Jupyter notebooks and PlantUML diagrams
├── 📁 images/ # Image assets
├── 📁 images/ # Image assets
├── 📁 src/ # Main source code
│ └── __init__.py # Empty file, for module creation
Expand All @@ -46,6 +48,7 @@ If you use this work, please cite it as:
│ └── satellite_node.py # Code representing a satellite in the network
│ └── simulation.py # Helper functions for generating and converting satellite orbital elements.
│ └── transaction.py # Code representing a transaction submitted by a satellite
│ └── visualise_orbits.py # Code for generating a gif of truth orbits
├── 📁 tests/ # Unit tests, written with pytest
|
Expand Down
Binary file added images/orbits.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ defusedxml==0.7.1
dill==0.4.0
executing==2.2.0
fastjsonschema==2.21.1
ffmpeg==1.4
filterpy==1.4.5
fonttools==4.58.5
fqdn==1.5.1
Expand Down
Binary file modified requirements_windows.txt
Binary file not shown.
4 changes: 2 additions & 2 deletions sim_data/sim_results.npz
Git LFS file not shown
170 changes: 170 additions & 0 deletions src/visualise_orbits.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# pylint: disable=too-many-locals, too-many-statements, no-member
"""
The Autonomous Cooperative Consensus Orbit Determination (ACCORD) framework.
Author: Beth Probert
Email: beth.probert@strath.ac.uk

Copyright (C) 2025 Applied Space Technology Laboratory

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.

"""

import os
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation, PillowWriter


def visualise_orbits(data_path='sim_data/sim_results.npz',
output_gif='images/orbits.gif', frames=200, interval=50) -> None:
"""
Creates an animated GIF of satellite orbits around Earth.

Args:
data_path: Path to the .npz file containing 'truth' data.
output_gif: Path to save the resulting GIF.
frames: Number of frames to animate (subsamples the data if less than total steps).
interval: Delay between frames in milliseconds. Default 50ms for smoother motion.

Returns:
None. Saves the animation as a GIF at the specified location.
"""
if not os.path.exists(data_path):
print(f"Error: Data file {data_path} not found.")
return

# Load data
print(f"Loading data from {data_path}...")
data = np.load(data_path, allow_pickle=True)
truth = data['truth'] # Shape: (steps, 6*N)

steps, state_size = truth.shape
n_sats = state_size // 6
print(f"Loaded {n_sats} satellites over {steps} time steps.")

# Subsample frame indices
frames = min(frames, steps)

indices = np.linspace(0, steps - 1, frames, dtype=int)

# Setup 3D Plot
fig = plt.figure(figsize=(12, 10))
ax = fig.add_subplot(111, projection='3d')

# Earth model
r_e = 6378e3 # Earth radius in meters
u = np.linspace(0, 2 * np.pi, 50)
v = np.linspace(0, np.pi, 50)
x_earth = r_e * np.outer(np.cos(u), np.sin(v))
y_earth = r_e * np.outer(np.sin(u), np.sin(v))
z_earth = r_e * np.outer(np.ones(np.size(u)), np.cos(v))
ax.plot_surface(x_earth, y_earth, z_earth, color='blue', # type: ignore [attr-defined]
alpha=0.1, rstride=2, cstride=2) # type: ignore [attr-defined]

# Initialise satellite markers
dots = []
trails = []
trail_len = 30 # Number of animation steps to show as a tail

# Randomly select up to 50 satellites to visualise if N is too large for clear animation
viz_n = min(n_sats, 50)
viz_indices = np.random.choice(range(n_sats), viz_n, replace=False)

colors = plt.cm.viridis(np.linspace(0, 1, viz_n)) # type: ignore [attr-defined]

for i in range(viz_n):
dot, = ax.plot([], [], [], 'o', color=colors[i], markersize=4)
dots.append(dot)
trail, = ax.plot([], [], [], '-', color=colors[i], alpha=0.4, linewidth=1.5)
trails.append(trail)

# Add text overlays
_ = ax.text2D(0.05, 0.95, f"Satellites shown: {viz_n} / {n_sats}", # type: ignore [attr-defined]
transform=ax.transAxes, fontsize=12, fontweight='bold')
step_text = ax.text2D(0.05, 0.90, "", transform=ax.transAxes, # type: ignore [attr-defined]
fontsize=12) # type: ignore [attr-defined]

# Set plot limits
# Calculate limits from first step positions to ensure Earth and orbits are visible
max_dist = np.max(np.linalg.norm(truth[0, :3], axis=0))
limit = max_dist * 1.2 if max_dist > 0 else r_e * 2

ax.set_xlim(-limit, limit)
ax.set_ylim(-limit, limit)
ax.set_zlim(-limit, limit) # type: ignore [attr-defined]
ax.set_xlabel('X (m)')
ax.set_ylabel('Y (m)')
ax.set_zlabel('Z (m)') # type: ignore [attr-defined]
ax.set_title('Satellite Constellation Animation', fontsize=16)

def init():
for dot, trail in zip(dots, trails):
dot.set_data([], [])
dot.set_3d_properties([])
trail.set_data([], [])
trail.set_3d_properties([])
step_text.set_text("")
return dots + trails + [step_text]

def update(frame):
# Update each satellite
actual_step = indices[frame]

# Calculate start index for trail in terms of ACTUAL steps to ensure smoothness
start_frame_idx = max(0, frame - trail_len)
start_step = indices[start_frame_idx]

for i, sat_idx in enumerate(viz_indices):
# Current position
pos = truth[actual_step, sat_idx*6 : sat_idx*6+3]
dots[i].set_data([pos[0]], [pos[1]])
dots[i].set_3d_properties([pos[2]])

# Trail using full resolution data between start_step and actual_step
trail_pos = truth[start_step : actual_step + 1, sat_idx*6 : sat_idx*6+3]
if len(trail_pos) > 0:
trails[i].set_data(trail_pos[:, 0], trail_pos[:, 1])
trails[i].set_3d_properties(trail_pos[:, 2])

# Update step counter
step_text.set_text(f"Timestep: {actual_step}")

return dots + trails + [step_text]

print("Creating animation...")
ani = FuncAnimation(fig, update, frames=frames, init_func=init, blit=True, interval=interval)

print(f"Saving to {output_gif}...")
writer = PillowWriter(fps=1000 // interval)
ani.save(output_gif, writer=writer)
print("Done!")

if __name__ == "__main__":
# Check for existing data files
potential_files = [
'sim_data/sim_results.npz',
'sim_data/ekf_simulation_results.npz'
]

CHOSEN_FILE = None
for f in potential_files:
if os.path.exists(f):
CHOSEN_FILE = f
break

if CHOSEN_FILE:
visualise_orbits(data_path=CHOSEN_FILE)
else:
print("Could not find any .npz simulation data in sim_data/.")
2 changes: 1 addition & 1 deletion streamlit_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -566,7 +566,7 @@ def display_config_item(item_label: str, item_value: Any) -> None:
st.markdown("""
If you use this work in your research, please cite it as:

> B. Probert, bprobert97/accord: v2.2. (Feb. 25, 2026). Python. University of Strathclyde, Glasgow. [DOI: 10.5281/zenodo.18776049](https://doi.org/10.5281/zenodo.18776049)
> B. Probert, bprobert97/accord: v3.0. (Mar. 24, 2026). Python. University of Strathclyde, Glasgow. [DOI: 10.5281/zenodo.19206200](https://doi.org/10.5281/zenodo.19206200)
""")

st.divider()
Expand Down
Loading