OrbitalSimulator/inspect_trajectories.py

348 lines
12 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Inspect orbital trajectory data from binary files in a tabular format.
This script reads the binary trajectory files generated by the orbital simulator
and displays the data in a nicely formatted table for inspection and debugging.
Usage:
python inspect_trajectories.py <trajectory_file.bin>
"""
import sys
import struct
import argparse
import numpy as np
from collections import defaultdict
class BinaryReader:
def __init__(self, data):
self.data = data
self.pos = 0
def read_u64(self):
result = struct.unpack('<Q', self.data[self.pos:self.pos+8])[0]
self.pos += 8
return result
def read_f64(self):
result = struct.unpack('<d', self.data[self.pos:self.pos+8])[0]
self.pos += 8
return result
def read_string(self):
# Read length (u64) then string bytes
length = self.read_u64()
result = self.data[self.pos:self.pos+length].decode('utf-8')
self.pos += length
return result
def read_vec3(self):
# Read 3 f64 values for position/velocity/acceleration
x = self.read_f64()
y = self.read_f64()
z = self.read_f64()
return np.array([x, y, z])
def read_trajectory_file(filename):
"""Read the binary trajectory file and return parsed data."""
with open(filename, 'rb') as f:
data = f.read()
reader = BinaryReader(data)
snapshots = []
try:
while reader.pos < len(data):
# Read snapshot
time = reader.read_f64()
# Read number of bodies (u64)
num_bodies = reader.read_u64()
bodies = []
for _ in range(num_bodies):
# Read Body struct
name = reader.read_string()
mass = reader.read_f64()
position = reader.read_vec3()
velocity = reader.read_vec3()
acceleration = reader.read_vec3()
bodies.append({
'name': name,
'mass': mass,
'position': position,
'velocity': velocity,
'acceleration': acceleration
})
snapshots.append({
'time': time,
'bodies': bodies
})
except struct.error:
# End of file or corrupted data
pass
return snapshots
def format_scientific(value, precision=3):
"""Format number in scientific notation with specified precision."""
if abs(value) < 1e-6 or abs(value) >= 1e6:
return f"{value:.{precision}e}"
else:
return f"{value:.{precision}f}"
def format_vector(vec, precision=3):
"""Format a 3D vector in a compact form."""
x, y, z = vec
return f"({format_scientific(x, precision)}, {format_scientific(y, precision)}, {format_scientific(z, precision)})"
def format_time(seconds):
"""Format time in a human-readable format."""
if seconds == 0:
return "0.0s"
elif seconds < 60:
return f"{seconds:.1f}s"
elif seconds < 3600:
return f"{seconds/60:.1f}m"
elif seconds < 86400:
return f"{seconds/3600:.1f}h"
else:
return f"{seconds/86400:.1f}d"
def print_summary_table(snapshots, max_rows=15):
"""Print a summary table of all snapshots."""
print("📊 TRAJECTORY SUMMARY")
print("=" * 100)
# Header
print(f"{'Step':<6} {'Time':<12} {'Bodies':<8} {'Sample Position (first body)':<35} {'Sample Velocity (first body)':<35}")
print("-" * 100)
# Determine which snapshots to show
total_snapshots = len(snapshots)
if total_snapshots <= max_rows:
indices = list(range(total_snapshots))
else:
# Show first few, middle few, and last few
start_count = max_rows // 3
end_count = max_rows // 3
middle_count = max_rows - start_count - end_count
indices = []
indices.extend(range(start_count))
if middle_count > 0:
middle_start = total_snapshots // 2 - middle_count // 2
indices.extend(range(middle_start, middle_start + middle_count))
indices.extend(range(total_snapshots - end_count, total_snapshots))
indices = sorted(set(indices)) # Remove duplicates and sort
prev_index = -1
for i, idx in enumerate(indices):
if idx > prev_index + 1 and prev_index >= 0:
print(" ...")
snapshot = snapshots[idx]
time_str = format_time(snapshot['time'])
body_count = len(snapshot['bodies'])
if snapshot['bodies']:
first_body = snapshot['bodies'][0]
pos_str = format_vector(first_body['position'], 2)
vel_str = format_vector(first_body['velocity'], 2)
else:
pos_str = "N/A"
vel_str = "N/A"
print(f"{idx:<6} {time_str:<12} {body_count:<8} {pos_str:<35} {vel_str:<35}")
prev_index = idx
print("-" * 100)
print(f"Total snapshots: {total_snapshots}")
def print_detailed_table(snapshots, body_name=None, max_rows=15):
"""Print detailed information for a specific body."""
if not snapshots:
print("No data to display!")
return
# Get all body names
all_bodies = set()
for snapshot in snapshots:
for body in snapshot['bodies']:
all_bodies.add(body['name'])
if body_name and body_name not in all_bodies:
print(f"❌ Body '{body_name}' not found. Available bodies: {', '.join(sorted(all_bodies))}")
return
# If no body specified, show the first one
if not body_name:
body_name = sorted(all_bodies)[0]
print(f"🌍 DETAILED VIEW: {body_name}")
print("=" * 120)
# Header
print(f"{'Step':<6} {'Time':<12} {'Position (x, y, z)':<40} {'Velocity (x, y, z)':<40} {'|v|':<12} {'|a|':<12}")
print("-" * 120)
# Determine which snapshots to show
total_snapshots = len(snapshots)
if total_snapshots <= max_rows:
indices = list(range(total_snapshots))
else:
# Show first few, middle few, and last few
start_count = max_rows // 3
end_count = max_rows // 3
middle_count = max_rows - start_count - end_count
indices = []
indices.extend(range(start_count))
if middle_count > 0:
middle_start = total_snapshots // 2 - middle_count // 2
indices.extend(range(middle_start, middle_start + middle_count))
indices.extend(range(total_snapshots - end_count, total_snapshots))
indices = sorted(set(indices))
prev_index = -1
for idx in indices:
if idx > prev_index + 1 and prev_index >= 0:
print(" ...")
snapshot = snapshots[idx]
time_str = format_time(snapshot['time'])
# Find the body in this snapshot
body_data = None
for body in snapshot['bodies']:
if body['name'] == body_name:
body_data = body
break
if body_data:
pos_str = format_vector(body_data['position'], 3)
vel_str = format_vector(body_data['velocity'], 3)
vel_mag = np.linalg.norm(body_data['velocity'])
acc_mag = np.linalg.norm(body_data['acceleration'])
vel_mag_str = format_scientific(vel_mag, 2)
acc_mag_str = format_scientific(acc_mag, 2)
print(f"{idx:<6} {time_str:<12} {pos_str:<40} {vel_str:<40} {vel_mag_str:<12} {acc_mag_str:<12}")
else:
print(f"{idx:<6} {time_str:<12} {'BODY NOT FOUND':<40}")
prev_index = idx
print("-" * 120)
def print_statistics(snapshots):
"""Print statistical information about the trajectory."""
if not snapshots:
return
print("\n📈 TRAJECTORY STATISTICS")
print("=" * 80)
# Time statistics
times = [s['time'] for s in snapshots]
time_start, time_end = times[0], times[-1]
duration = time_end - time_start
print(f"Time range: {format_time(time_start)} to {format_time(time_end)}")
print(f"Total duration: {format_time(duration)}")
print(f"Number of snapshots: {len(snapshots)}")
if len(times) > 1:
time_steps = [times[i+1] - times[i] for i in range(len(times)-1)]
avg_step = np.mean(time_steps)
print(f"Average time step: {format_time(avg_step)}")
# Body statistics
body_names = set()
for snapshot in snapshots:
for body in snapshot['bodies']:
body_names.add(body['name'])
print(f"Bodies tracked: {', '.join(sorted(body_names))}")
# Position and velocity ranges for each body
for body_name in sorted(body_names):
positions = []
velocities = []
for snapshot in snapshots:
for body in snapshot['bodies']:
if body['name'] == body_name:
positions.append(body['position'])
velocities.append(body['velocity'])
if positions:
positions = np.array(positions)
velocities = np.array(velocities)
pos_min = np.min(positions, axis=0)
pos_max = np.max(positions, axis=0)
vel_min = np.min(np.linalg.norm(velocities, axis=1))
vel_max = np.max(np.linalg.norm(velocities, axis=1))
print(f"\n{body_name}:")
print(f" Position range: X[{format_scientific(pos_min[0])}, {format_scientific(pos_max[0])}]")
print(f" Y[{format_scientific(pos_min[1])}, {format_scientific(pos_max[1])}]")
print(f" Z[{format_scientific(pos_min[2])}, {format_scientific(pos_max[2])}]")
print(f" Speed range: {format_scientific(vel_min)} to {format_scientific(vel_max)} m/s")
def main():
parser = argparse.ArgumentParser(description='Inspect orbital trajectory data in tabular format')
parser.add_argument('trajectory_file', help='Binary trajectory file to inspect')
parser.add_argument('--rows', '-r', type=int, default=15, help='Maximum number of rows to display (default: 15)')
parser.add_argument('--body', '-b', type=str, help='Show detailed view for specific body')
parser.add_argument('--summary', '-s', action='store_true', help='Show summary of all snapshots')
parser.add_argument('--stats', action='store_true', help='Show trajectory statistics')
parser.add_argument('--all', '-a', action='store_true', help='Show all available information')
args = parser.parse_args()
print(f"🔍 Inspecting trajectory file: {args.trajectory_file}")
print()
try:
snapshots = read_trajectory_file(args.trajectory_file)
if not snapshots:
print("❌ No data found in file!")
return
# Determine what to show
show_summary = args.summary or args.all or (not args.body and not args.stats)
show_detailed = args.body or args.all
show_stats = args.stats or args.all
if show_summary:
print_summary_table(snapshots, args.rows)
print()
if show_detailed:
print_detailed_table(snapshots, args.body, args.rows)
print()
if show_stats:
print_statistics(snapshots)
except FileNotFoundError:
print(f"❌ Error: File '{args.trajectory_file}' not found!")
except Exception as e:
print(f"❌ Error reading file: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
main()