"""Module for simulating and visualizing satellite constellations.
This module provides functionality to simulate the trajectories of satellite constellations and visualize their movement in 3D space using Plotly.
Classes:
Constellation: A class representing a satellite constellation, which contains multiple trajectories.
Functions:
get_constellation: A factory function to create different types of satellite constellations based on the given name.
Usage:
To create a satellite constellation and simulate its movement:
```python
from navigator.constellation import Constellation, get_constellation
# Create a default constellation with 8 stationary trajectories
constellation = get_constellation()
# Simulate the constellation movement for a given time array
times = np.linspace(0, 100, 1000) # Example time array
fig = constellation.animate(times, range_dict={"x": [-50, 50], "y": [-50, 50], "z": [-50, 50]})
# Display the animation
fig.show()
```
"""
import numpy as np
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
from ..animation.time_series import timeseries_animation
from ..trajectory.trajectory import Trajectory
__all__ = ["Constellation"]
class Constellation:
"""A class representing a satellite constellation, which contains multiple trajectories.
This class provides functionality to manage a satellite constellation consisting of multiple trajectories. Each trajectory represents the movement path of a satellite within the constellation.
Attributes:
trajectories (dict[str, Trajectory]): A dictionary containing the name of each trajectory and its corresponding Trajectory object.
Note:
- The trajectory dictionary should contain the name of each trajectory as the key and its corresponding Trajectory object as the value.
- Each trajectory's name should be a unique string identifier.
"""
def __init__(self, trajectories: dict[str, Trajectory]) -> None:
"""Initialize the Constellation object with the given trajectories.
Args:
trajectories (dict[str, Trajectory]): A dictionary containing the name of each trajectory and its corresponding Trajectory object.
Raises:
ValueError: If the trajectories dictionary is empty.
ValueError: If the trajectories has unique keys.
ValueError: If all the values in the dictionary are not Trajectory objects.
"""
# If empty dictionary is passed
if len(trajectories) == 0:
raise ValueError("Trajectories dictionary should not be empty.")
# Check if the trajectories has unique keys
if len(trajectories) != len(set(trajectories.keys())):
raise ValueError("Trajectories should have unique keys.")
# Check if all the values in the dictionary are Trajectory objects
if not all(
isinstance(trajectory, Trajectory) for trajectory in trajectories.values()
):
raise ValueError(
"All the values in the dictionary should be Trajectory objects."
)
self.trajectories = trajectories
def state(self, time: float) -> pd.DataFrame:
"""Get the state of all the trajectories at the given time.
Args:
time (float): The time at which the state of the trajectories is required.
Returns:
pd.DataFrame: A pandas DataFrame containing the state of all the trajectories at the given time.
Note:
- State is defined as [x, y, z, vx, vy, vz]
"""
# Get the state of all the trajectories at the given time
state = {}
for name, trajectory in self.trajectories.items():
pos_and_velocity = trajectory(time=time)
state[name] = {
"x": pos_and_velocity[0],
"y": pos_and_velocity[1],
"z": pos_and_velocity[2],
"vx": pos_and_velocity[3],
"vy": pos_and_velocity[4],
"vz": pos_and_velocity[5],
}
return pd.DataFrame.from_dict(state, orient="index")
def get_timeseries(self, times: np.ndarray) -> np.ndarray:
"""Get the time series data for the constellation.
Args:
times (np.ndarray): The time array for which the constellation is to be simulated. (N,)
Returns:
np.ndarray: The time series data for the constellation. (T, N, 6)
Note:
- N is the number of trajectories in the constellation.
- T is the number of time steps in the time array.
- The last dimension contains the position and velocity data for each trajectory.
"""
# Initialize the time series data
time_series = np.zeros(
(len(times), len(self.trajectories), 6), dtype=np.float64
)
# Get the state of the constellation at each time step
for i, t in enumerate(times):
time_series[i] = self.state(t).to_numpy(dtype=np.float64)
return time_series
def animate(
self,
times: np.ndarray,
tracer: bool = True,
no_text: bool = False,
) -> go.Figure:
"""This function simulates the constellation for a given time.
Args:
times (np.ndarray): The time array for which the constellation is to be simulated.
range_dict (dict): A dictionary containing the range of the X, Y, Z axis.
tracer (bool): A boolean value to enable/disable the tracer. Default is True.
no_text (bool): A boolean value to enable/disable the text labels. Default is False.
Returns:
go.Figure: A plotly figure object containing the animation of the constellation.
"""
# Get the timeseries data for the constellation
names = list(self.trajectories.keys())
time_series = self.get_timeseries(times)
# Generate random colors from continuous colormap
colors = px.colors.qualitative.Light24
# If the number of trajectories is greater than the number of colors, generate random colors
if len(names) > len(colors):
colors = px.colors.qualitative.Light24 * (len(names) // len(colors) + 1)
# Get the max and min values for the x, y and z coordinates
range_dict = {
"x": [time_series[:, :, 0].min(), time_series[:, :, 0].max()],
"y": [time_series[:, :, 1].min(), time_series[:, :, 1].max()],
"z": [time_series[:, :, 2].min(), time_series[:, :, 2].max()],
}
# Get the timeseries data for the constellation
fig = timeseries_animation(
time_series,
names,
{name: tracer for name in names},
{name: not no_text for name in names},
{name: colors[i] for i, name in enumerate(names)},
)
# Update the layout
fig.update_layout(
title="Constellation Simulation",
scene=dict(
xaxis_title="X",
yaxis_title="Y",
zaxis_title="Z",
aspectmode="manual",
aspectratio=dict(x=2, y=2, z=1),
xaxis=dict(range=range_dict["x"]),
yaxis=dict(range=range_dict["y"]),
zaxis=dict(range=range_dict["z"]),
),
)
return fig
def __call__(self, time: float) -> pd.DataFrame:
"""Get the state of all the trajectories at the given time.
Args:
time (float): The time at which the state of the trajectories is required.
Returns:
pd.DataFrame: A pandas DataFrame containing the state of all the trajectories at the given time.
"""
return self.state(time)
def __repr__(self) -> str:
"""Get the string representation of the Constellation object.
Returns:
str: String representation of the Constellation object.
"""
return f"Constellation(num_trajectories={len(self.trajectories)})"
def __mul__(self, scale: float) -> "Constellation":
"""Scales the constellation by a factor.
Args:
scale (float): The scale factor.
Returns:
Constellation: A new Constellation object scaled by the given factor.
"""
return Constellation(
{name: trajectory * scale for name, trajectory in self.trajectories.items()}
)