Skip to content

Instantly share code, notes, and snippets.

@suobset
Last active December 23, 2025 22:31
Show Gist options
  • Select an option

  • Save suobset/4100058dfd3e6a46100e709040002491 to your computer and use it in GitHub Desktop.

Select an option

Save suobset/4100058dfd3e6a46100e709040002491 to your computer and use it in GitHub Desktop.
SaberStat: Interim Data Analysis | Read more on https://skushagra.com
#!/usr/bin/env python3
"""
Lightsaber Hit Detection - Data Visualization & Analysis
Run: python lightsaber_viz.py
Dependencies: pip install pandas numpy matplotlib scipy
"""
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from scipy.signal import find_peaks
from pathlib import Path
import glob
# This function detects all the Log files using Regex
def load_all_logs(pattern="../logs/Log*.csv"):
"""Load all matching CSV files into a dict keyed by filename."""
files = glob.glob(pattern)
if not files:
# Try alternate patterns
files = glob.glob("**/Log*.csv", recursive=True)
datasets = {}
for f in sorted(files):
try:
df = pd.read_csv(f)
datasets[Path(f).stem] = df
print(f"Loaded {f}: {len(df)} samples")
except Exception as e:
print(f"Failed to load {f}: {e}")
return datasets
def compute_features(df):
"""Derive key features for hit detection."""
# Acceleration magnitude (user acceleration, excluding gravity)
df['acc_mag'] = np.sqrt(
df['accelerationX']**2 +
df['accelerationY']**2 +
df['accelerationZ']**2
)
# Rotation rate magnitude
df['rot_mag'] = np.sqrt(
df['rotationRateX']**2 +
df['rotationRateY']**2 +
df['rotationRateZ']**2
)
# Jerk (derivative of acceleration magnitude)
# Using time delta from seconds_elapsed
dt = df['seconds_elapsed'].diff().fillna(0.01) # ~100Hz assumed
df['jerk'] = df['acc_mag'].diff().abs() / dt
df['jerk'] = df['jerk'].fillna(0).replace([np.inf, -np.inf], 0)
# Combined "impact score" - empirical weighting
df['impact_score'] = df['acc_mag'] * 0.6 + df['rot_mag'] * 0.3 + df['jerk'] * 0.0001
return df
def find_potential_hits(df, acc_threshold=1.5, rot_threshold=2.0, min_distance_ms=200):
"""Find peaks that likely correspond to hits."""
# Convert min_distance to samples (assuming ~100Hz)
sample_rate = 1 / df['seconds_elapsed'].diff().median()
min_samples = int(min_distance_ms / 1000 * sample_rate)
# Find peaks in acceleration magnitude
peaks, properties = find_peaks(
df['acc_mag'],
height=acc_threshold,
distance=max(1, min_samples),
prominence=0.5
)
# Filter by rotation threshold
valid_peaks = [p for p in peaks if df.iloc[p]['rot_mag'] > rot_threshold]
return valid_peaks, peaks
def plot_session(df, name, peaks=None, valid_peaks=None):
"""Create visualization of a recording session."""
fig, axes = plt.subplots(4, 1, figsize=(14, 10), sharex=True)
fig.suptitle(f'Lightsaber Session: {name}', fontsize=14, fontweight='bold')
t = df['seconds_elapsed']
# Plot 1: Acceleration magnitude
axes[0].plot(t, df['acc_mag'], 'b-', linewidth=0.8, label='Acceleration Mag')
axes[0].set_ylabel('Acc Mag (g)')
axes[0].legend(loc='upper right')
axes[0].grid(True, alpha=0.3)
if peaks is not None:
axes[0].plot(t.iloc[peaks], df['acc_mag'].iloc[peaks], 'ro',
markersize=4, alpha=0.5, label='All peaks')
if valid_peaks is not None:
axes[0].plot(t.iloc[valid_peaks], df['acc_mag'].iloc[valid_peaks], 'g^',
markersize=8, label='Likely hits')
axes[0].legend(loc='upper right')
# Plot 2: Rotation magnitude
axes[1].plot(t, df['rot_mag'], 'r-', linewidth=0.8)
axes[1].set_ylabel('Rot Mag (rad/s)')
axes[1].grid(True, alpha=0.3)
if valid_peaks is not None:
axes[1].plot(t.iloc[valid_peaks], df['rot_mag'].iloc[valid_peaks], 'g^', markersize=8)
# Plot 3: Jerk (clipped for visibility)
jerk_clipped = df['jerk'].clip(upper=df['jerk'].quantile(0.99))
axes[2].plot(t, jerk_clipped, 'purple', linewidth=0.8)
axes[2].set_ylabel('Jerk (clipped)')
axes[2].grid(True, alpha=0.3)
# Plot 4: Impact score
axes[3].plot(t, df['impact_score'], 'green', linewidth=0.8)
axes[3].set_ylabel('Impact Score')
axes[3].set_xlabel('Time (seconds)')
axes[3].grid(True, alpha=0.3)
if valid_peaks is not None:
axes[3].plot(t.iloc[valid_peaks], df['impact_score'].iloc[valid_peaks], 'g^', markersize=8)
plt.tight_layout()
return fig
def analyze_hits(df, valid_peaks, name):
"""Print statistics about detected hits."""
print(f"\n{'='*50}")
print(f"Analysis: {name}")
print(f"{'='*50}")
print(f"Total samples: {len(df)}")
print(f"Duration: {df['seconds_elapsed'].max():.1f} seconds")
print(f"Sample rate: {1/df['seconds_elapsed'].diff().median():.0f} Hz")
print(f"Detected hits: {len(valid_peaks)}")
if valid_peaks:
hit_data = df.iloc[valid_peaks]
print(f"\nHit statistics:")
print(f" Acc magnitude - mean: {hit_data['acc_mag'].mean():.2f}, max: {hit_data['acc_mag'].max():.2f}")
print(f" Rot magnitude - mean: {hit_data['rot_mag'].mean():.2f}, max: {hit_data['rot_mag'].max():.2f}")
print(f" Impact score - mean: {hit_data['impact_score'].mean():.2f}, max: {hit_data['impact_score'].max():.2f}")
print(f"\nHit timestamps (seconds):")
for i, peak in enumerate(valid_peaks[:10]): # Show first 10
print(f" {i+1}. t={df.iloc[peak]['seconds_elapsed']:.2f}s, "
f"acc={df.iloc[peak]['acc_mag']:.2f}, "
f"rot={df.iloc[peak]['rot_mag']:.2f}")
if len(valid_peaks) > 10:
print(f" ... and {len(valid_peaks)-10} more")
def interactive_threshold_tuning(df, name):
"""Helper to experiment with thresholds."""
print(f"\nData ranges for {name}:")
print(f" Acceleration: {df['acc_mag'].min():.2f} - {df['acc_mag'].max():.2f}")
print(f" Rotation: {df['rot_mag'].min():.2f} - {df['rot_mag'].max():.2f}")
print(f" Jerk: {df['jerk'].min():.2f} - {df['jerk'].quantile(0.99):.2f} (99th pctl)")
print(f"\nSuggested starting thresholds:")
print(f" acc_threshold: {df['acc_mag'].quantile(0.95):.2f} (95th percentile)")
print(f" rot_threshold: {df['rot_mag'].quantile(0.90):.2f} (90th percentile)")
# Main entrypoint of the script.
def main():
# Load all datasets
print("Loading datasets...")
datasets = load_all_logs()
if not datasets:
print("\nNo CSV files found! Place your Log files in the ../logs directory.")
print("Expected files like: '../logs/Log 2 Frank.csv', '../logs/Log 2 Kush.csv', etc.")
return
# Process each dataset
# I do not think we would need to store all figures, but just in case
all_figures = []
for name, df in datasets.items():
print(f"\nProcessing {name}...")
# Compute features
# This computes acceleration magnitude, rotation magnitude, jerk, and impact score
# and adds them as new columns to the dataframe
df = compute_features(df)
# Show data ranges for threshold tuning
# This function prints out the min/max ranges of key features
# and suggests starting thresholds based on percentiles
interactive_threshold_tuning(df, name)
# Find hits with default thresholds (adjust these!)
valid_peaks, all_peaks = find_potential_hits(
df,
acc_threshold=1.5, # Adjust based on your data
rot_threshold=2.0 # Adjust based on your data
)
# Analyze
analyze_hits(df, valid_peaks, name)
# Plot
fig = plot_session(df, name, all_peaks, valid_peaks)
all_figures.append(fig)
# Save plot
fig.savefig(f'{name}_analysis.png', dpi=150, bbox_inches='tight')
print(f"Saved {name}_analysis.png")
plt.show()
if __name__ == "__main__":
main()
Loading datasets...
Loaded ../logs/Log 2 Frank.csv: 1825 samples
Loaded ../logs/Log 2 Kush.csv: 756 samples
Loaded ../logs/Log 3 Frank.csv: 512 samples
Loaded ../logs/Log 3 Kush.csv: 439 samples
Loaded ../logs/Log 4 Frank.csv: 1689 samples
Loaded ../logs/Log 4 Kush.csv: 1596 samples
Loaded ../logs/Log1.csv: 2379 samples
Processing Log 2 Frank...
Data ranges for Log 2 Frank:
Acceleration: 0.00 - 14.34
Rotation: 0.03 - 19.01
Jerk: 0.00 - 277.42 (99th pctl)
Suggested starting thresholds:
acc_threshold: 3.49 (95th percentile)
rot_threshold: 8.44 (90th percentile)
==================================================
Analysis: Log 2 Frank
==================================================
Total samples: 1825
Duration: 29.3 seconds
Sample rate: 100 Hz
Detected hits: 19
Hit statistics:
Acc magnitude - mean: 4.38, max: 14.34
Rot magnitude - mean: 6.81, max: 13.41
Impact score - mean: 4.69, max: 12.14
Hit timestamps (seconds):
1. t=5.32s, acc=5.07, rot=6.38
2. t=5.52s, acc=2.23, rot=3.99
3. t=5.87s, acc=4.66, rot=6.06
4. t=6.23s, acc=1.86, rot=6.72
5. t=6.46s, acc=5.16, rot=2.84
6. t=6.66s, acc=2.05, rot=2.50
7. t=7.02s, acc=5.34, rot=5.88
8. t=7.55s, acc=1.55, rot=3.37
9. t=16.63s, acc=4.97, rot=9.40
10. t=17.56s, acc=3.73, rot=12.71
... and 9 more
Saved Log 2 Frank_analysis.png
Processing Log 2 Kush...
Data ranges for Log 2 Kush:
Acceleration: 0.01 - 2.75
Rotation: 0.07 - 10.39
Jerk: 0.00 - 47.44 (99th pctl)
Suggested starting thresholds:
acc_threshold: 0.84 (95th percentile)
rot_threshold: 2.42 (90th percentile)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment