"""Simulate a receiver for a trajectory simulator.
This module provides the `Reciever` class which simulates a receiver capable of
receiving signals from a satellite constellation based on a specified trajectory.
Classes:
Reciever: Simulate a receiver for a trajectory simulator.
"""
import typing as tp
import numpy as np
import pandas as pd
import plotly.express as px
from plotly.graph_objs import Figure
from ....epoch import Epoch
from ..animation.time_series import timeseries_animation
from ..constellation.constellation import Constellation
from ..trajectory.trajectory import Trajectory
__all__ = ["Reciever"]
class Reciever:
"""Simulate a receiver that can receive a signal from a Constellation.
Args:
name (str): The name of the receiver.
trajectory (Trajectory): The path of the receiver as a Trajectory object.
constellation (Constellation): The constellation to receive signals from.
Attributes:
name (str): The name of the receiver.
path (Trajectory): The path of the receiver.
constellation (Constellation): The constellation to receive signals from.
"""
START_EPOCH = pd.Timestamp("2021-01-01 00:00:00")
def __init__(
self, name: str, trajectory: Trajectory, constellation: Constellation
) -> None:
"""Initialize the Reciever class.
Args:
name (str): The name of the receiver.
trajectory (Trajectory): The trajectory of the receiver as a Trajectory object.
constellation (Constellation): The constellation to receive signals from.
Returns:
None
"""
self.name = name
self.path = trajectory
self.constellation = constellation
def record(self, time: float) -> tp.Tuple[pd.DataFrame, pd.DataFrame, pd.Series]:
"""Record the signal received by the receiver at a given time.
Args:
time (float): The time at which the signal is received.
Returns:
Tuple[pd.DataFrame, pd.DataFrame, pd.Series]:
- observation_data (pd.DataFrame): DataFrame containing the observed range data for different signals.
- navigation_data (pd.DataFrame): DataFrame containing the state of the constellation at the given time.
- true_state (pd.Series): Series containing the true state (position and velocity) of the receiver.
"""
# Get the current position and velocity of the receiver
reciever_state = self.path(time=time) # (6,)
positions = reciever_state[:3]
# Get the constellation state at the current time
constellation_state = self.constellation.state(time)
# Calculate the range between the receiver and the satellites
range = np.linalg.norm(positions - constellation_state.values[:, :3], axis=1)
# Create observation data dictionary
observation_data = {
Epoch.L1_CODE_ON: range,
Epoch.L2_CODE_ON: range,
Epoch.L1_PHASE_ON: range,
Epoch.L2_PHASE_ON: range,
}
observation_data = pd.DataFrame.from_dict(observation_data, orient="columns")
# Navigation data
navigation_data = constellation_state
# Apply same index to observation data
observation_data.index = navigation_data.index
# True state
true_state = pd.Series(
{
"x": reciever_state[0],
"y": reciever_state[1],
"z": reciever_state[2],
"vx": reciever_state[3],
"vy": reciever_state[4],
"vz": reciever_state[5],
}
)
return observation_data, navigation_data, true_state
def timeseries(
self, times: np.ndarray
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
"""Get the epoch objects for the receiver at given times.
Args:
times (np.ndarray): Array of times at which the signal is received.(T,)
Returns:
tuple[np.ndarray, np.ndarray, np.ndarray]: Tuple containing the observation data, navigation data and true states. (T,N,4), (T,N,6), (T,6)
"""
obs_data = []
nav_data = []
true_states = []
for time in times:
obs, nav, true = self.record(time)
obs_data.append(obs.values)
nav_data.append(nav.values)
true_states.append(true.values)
return np.stack(obs_data), np.stack(nav_data), np.stack(true_states)
def get_dummy_epoch(self, time: float) -> Epoch:
"""Get a dummy epoch object for the receiver at a given time.
Args:
time (float): The time at which the signal is received.
Returns:
Epoch: The dummy epoch object.
"""
# Get the obs data, nav data and true state
obs_data, nav_data, true_state = self.record(time)
# Create the dummy epoch object
epoch = Epoch(
timestamp=self.START_EPOCH + pd.Timedelta(seconds=time),
obs_data=obs_data,
nav_data=nav_data,
real_coord=true_state,
obs_meta=pd.Series(),
nav_meta=pd.Series(),
trim=False,
purify=False,
station=f"SIM000_{self.name}",
)
# Activate the dummy profile
epoch.profile = Epoch.DUMMY
return epoch
def animate(
self,
times: np.ndarray,
tracer_map: tp.Optional[dict[str, bool]] = None,
text_map: tp.Optional[dict[str, bool]] = None,
) -> Figure:
"""Animate the receiver and the constellation.
Args:
times (np.ndarray): Array of times at which the signal is received.
tracer_map (dict[str, bool]): Dictionary containing the trace status for different signals.
text_map (dict[str, str]): Dictionary containing the text for different signals.
Returns:
None
"""
# Get the timeseries data
obs_data, nav_data, true_states = self.timeseries(times)
# Get the name of the constellation and the receiver
names = [self.name] + list(self.constellation.trajectories.keys())
# Initialize the tracer map and text map
if tracer_map is None:
tracer_map = {name: False for name in names}
else:
for name in names:
if name not in tracer_map:
tracer_map[name] = False
if text_map is None:
text_map = {name: True for name in names}
else:
for name in names:
if name not in text_map:
text_map[name] = True
# Stack the position measurements as (T,1 + C, 3)
positions = np.concatenate(
[true_states[:, None, :3], nav_data[:, :, :3]], axis=1
)
# Get the max and min values for the x, y and z coordinates
range_dict = {
"x": [positions[:, :, 0].min(), positions[:, :, 0].max()],
"y": [positions[:, :, 1].min(), positions[:, :, 1].max()],
"z": [positions[:, :, 2].min(), positions[:, :, 2].max()],
}
# 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)
color_map = dict(zip(names, colors))
# Generate the animation
fig = timeseries_animation(
ts_data=positions,
names=names,
tracer_map=tracer_map,
text_map=text_map,
color_map=color_map,
)
# Update the layout
fig.update_layout(
title="Reciever 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) -> Epoch:
"""Get the epoch object for the receiver at a given time.
Args:
time (float): The time at which the signal is received.
Returns:
Epoch: The epoch object.
"""
return self.get_dummy_epoch(time)