Compare commits
26 Commits
41ec54f7dd
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| b65de712db | |||
| f4d1f8f73b | |||
| 15e449a00c | |||
| 1e1a29baef | |||
| b98d89c609 | |||
| f6a0cdc1c4 | |||
| 97e8b709a0 | |||
| abd01c661a | |||
| 81bb610459 | |||
| 58df608550 | |||
| ba6b35b3d9 | |||
| 1a5b107b1f | |||
| cd097e4e55 | |||
| 4e9c33778c | |||
| 48737b60c9 | |||
| 83188b1fa1 | |||
| af723c944b | |||
| fa257fb87b | |||
| 9407d8a556 | |||
| 1b32c1eef1 | |||
| ed901a90d5 | |||
| c6fd71dc0b | |||
| eeb039c24a | |||
| 669b65a30f | |||
| 9b9c5254db | |||
| 72dfc39771 |
38
.claude/settings.local.json
Normal file
38
.claude/settings.local.json
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(find /c/ASTERION/GIT/PS_Ballistics/Unreal -name *.bat -o -name Generate*.sh -o -name *Generate*)",
|
||||||
|
"Bash(\"C:\\\\Program Files\\\\Epic Games\\\\UE_5.5\\\\Engine\\\\Build\\\\BatchFiles\\\\Build.bat\" PS_BallisticsEditor Win64 Development -Project=\"C:\\\\ASTERION\\\\GIT\\\\PS_Ballistics\\\\Unreal\\\\PS_Ballistics.uproject\" -WaitMutex -FromMsBuild)",
|
||||||
|
"Bash(\"C:\\\\Program Files\\\\Epic Games\\\\UE_5.5\\\\Engine\\\\Build\\\\BatchFiles\\\\Build.bat\" PS_BallisticsEditor Win64 Development -Project=\"C:\\\\ASTERION\\\\GIT\\\\PS_Ballistics\\\\Unreal\\\\PS_Ballistics.uproject\" -WaitMutex -FromMsBuild -NoLiveCoding)",
|
||||||
|
"Bash(\"C:\\\\Program Files\\\\Epic Games\\\\UE_5.5\\\\Engine\\\\Build\\\\BatchFiles\\\\Build.bat\" PS_BallisticsEditor Win64 Development -Project=\"C:\\\\ASTERION\\\\GIT\\\\PS_Ballistics\\\\Unreal\\\\PS_Ballistics.uproject\" -NoLiveCoding)",
|
||||||
|
"Bash(grep -l \"Shoot\\\\|ClientAim\\\\|ShootRep\" \"E:\\\\ASTERION\\\\GIT\\\\PS_Ballistics\\\\Unreal\\\\Plugins\\\\PS_Ballistics\\\\Source\\\\EasyBallistics\\\\Private\"/*.cpp)",
|
||||||
|
"Bash(xargs grep:*)",
|
||||||
|
"Bash(ls Source/EasyBallistics/Private/*.cpp Source/EasyBallistics/Public/*.h)",
|
||||||
|
"Bash(powershell.exe -Command \"& ''''C:\\\\Program Files\\\\Epic Games\\\\UE_5.5\\\\Engine\\\\Build\\\\BatchFiles\\\\RunUAT.bat'''' BuildEditor -project=''''E:\\\\ASTERION\\\\GIT\\\\PS_Ballistics\\\\Unreal\\\\PS_Ballistics.uproject'''' -notools -noP4 2>&1\")",
|
||||||
|
"Bash(python \"E:\\\\ASTERION\\\\GIT\\\\PS_Ballistics\\\\Tools\\\\analyze_antirecoil.py\" \"E:\\\\ASTERION\\\\SVN\\\\DEV\\\\PROSERVE_UE_5_5\\\\Saved\\\\Logs\\\\AntiRecoil_20260316_150326.csv\")",
|
||||||
|
"Bash(python \"Tools\\\\analyze_antirecoil.py\" \"E:\\\\ASTERION\\\\SVN\\\\DEV\\\\PROSERVE_UE_5_5\\\\Saved\\\\Logs\\\\AntiRecoil_20260316_150326.csv\")",
|
||||||
|
"Bash(python \"Tools\\\\analyze_antirecoil.py\" \"E:\\\\ASTERION\\\\SVN\\\\DEV\\\\PROSERVE_UE_5_5\\\\Saved\\\\Logs\\\\AntiRecoil_20260316_153607.csv\")",
|
||||||
|
"Bash(python \"Tools\\\\analyze_antirecoil.py\" \"E:\\\\ASTERION\\\\SVN\\\\DEV\\\\PROSERVE_UE_5_5\\\\Saved\\\\Logs\\\\AntiRecoil_20260316_160323.csv\")",
|
||||||
|
"Bash(python \"Tools\\\\analyze_antirecoil.py\" \"E:\\\\ASTERION\\\\SVN\\\\DEV\\\\PROSERVE_UE_5_5\\\\Saved\\\\Logs\\\\AntiRecoil_20260316_164341.csv\")",
|
||||||
|
"Bash(python \"Tools\\\\analyze_antirecoil.py\" \"E:\\\\ASTERION\\\\SVN\\\\DEV\\\\PROSERVE_UE_5_5\\\\Saved\\\\Logs\\\\AntiRecoil_20260316_170543.csv\")",
|
||||||
|
"Bash(find C:ASTERIONSVNDEVPROSERVE_UE_5_5Plugins -type f \\\\\\(-name *.cpp -o -name *.h \\\\\\))",
|
||||||
|
"Bash(git add:*)",
|
||||||
|
"Bash(git commit:*)",
|
||||||
|
"Bash(find E:/ASTERION/SVN/DEV/PROSERVE_UE_5_5/Saved -name AntiRecoil* -type f)",
|
||||||
|
"Bash(python analyze_antirecoil.py \"C:/ASTERION/SVN/DEV/PROSERVE_UE_5_5/Saved/Logs/AntiRecoil_20260318_140946.csv\" --grid)",
|
||||||
|
"Bash(python analyze_antirecoil.py \"C:/ASTERION/SVN/DEV/PROSERVE_UE_5_5/Saved/Logs/AntiRecoil_20260318_140946.csv\" \"C:/ASTERION/SVN/DEV/PROSERVE_UE_5_5/Saved/Logs/AntiRecoil_20260318_141329.csv\")",
|
||||||
|
"Bash(python analyze_antirecoil.py \"C:/ASTERION/SVN/DEV/PROSERVE_UE_5_5/Saved/Logs/AntiRecoil_20260318_140946.csv\" \"C:/ASTERION/SVN/DEV/PROSERVE_UE_5_5/Saved/Logs/AntiRecoil_20260318_141329.csv\" --grid)",
|
||||||
|
"Bash(python analyze_antirecoil.py \"C:/ASTERION/SVN/DEV/PROSERVE_UE_5_5/Saved/Logs/AntiRecoil_20260318_140946.csv\" \"C:/ASTERION/SVN/DEV/PROSERVE_UE_5_5/Saved/Logs/AntiRecoil_20260318_141329.csv\" --grid --strategy worst_case)",
|
||||||
|
"Bash(python analyze_antirecoil.py \"C:/ASTERION/SVN/DEV/PROSERVE_UE_5_5/Saved/Logs/AntiRecoil_20260318_162726.csv\" \"C:/ASTERION/SVN/DEV/PROSERVE_UE_5_5/Saved/Logs/AntiRecoil_20260318_162234.csv\" --grid --strategy worst_case)",
|
||||||
|
"Bash(python analyze_shots.py \"C:/ASTERION/SVN/DEV/PROSERVE_UE_5_5/Saved/Logs/AntiRecoil_20260318_162726.csv\")",
|
||||||
|
"Bash(python analyze_shots.py \"C:/ASTERION/SVN/DEV/PROSERVE_UE_5_5/Saved/Logs/AntiRecoil_20260318_163533.csv\")",
|
||||||
|
"Bash(python analyze_shots.py \"C:/ASTERION/SVN/DEV/PROSERVE_UE_5_5/Saved/Logs/AntiRecoil_20260318_181404.csv\")",
|
||||||
|
"Bash(python -c \":*)",
|
||||||
|
"Bash(git -C C:/ASTERION/GIT/PS_Ballistics status --short)",
|
||||||
|
"Bash(git -C C:/ASTERION/GIT/PS_Ballistics ls-files)",
|
||||||
|
"Bash(grep -iE \"\\\\.\\(dll|pdb|exp|lib|exe|dylib|so|a|o\\)$\")",
|
||||||
|
"Bash(git -C C:/ASTERION/GIT/PS_Ballistics add .gitignore)",
|
||||||
|
"Bash(git -C C:/ASTERION/GIT/PS_Ballistics commit -m ' *)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
15
.gitignore
vendored
15
.gitignore
vendored
@@ -3,4 +3,19 @@ Unreal/.vs/
|
|||||||
Unreal/Binaries/
|
Unreal/Binaries/
|
||||||
Unreal/Intermediate/
|
Unreal/Intermediate/
|
||||||
Unreal/Plugins/EasyBallistics/Intermediate/
|
Unreal/Plugins/EasyBallistics/Intermediate/
|
||||||
|
Unreal/Plugins/EasyBallistics/Binaries/
|
||||||
Unreal/Saved/
|
Unreal/Saved/
|
||||||
|
Unreal/Plugins/PS_Ballistics/Intermediate/
|
||||||
|
Unreal/Plugins/PS_Ballistics/Binaries/
|
||||||
|
|
||||||
|
# Binaires compilés
|
||||||
|
*.dll
|
||||||
|
*.exp
|
||||||
|
*.pdb
|
||||||
|
*.lib
|
||||||
|
*.obj
|
||||||
|
*.exe
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
*.a
|
||||||
|
*.o
|
||||||
|
|||||||
1
Bind to PROSERVE.bat
Normal file
1
Bind to PROSERVE.bat
Normal file
@@ -0,0 +1 @@
|
|||||||
|
powershell -Command "New-Item -ItemType Junction -Path 'E:\ASTERION\SVN\DEV\PROSERVE_UE_5_5\Plugins\PS_Ballistics' -Target 'E:\ASTERION\GIT\PS_Ballistics\Unreal\Plugins\PS_Ballistics'"
|
||||||
BIN
Tools/__pycache__/analyze_antirecoil.cpython-312.pyc
Normal file
BIN
Tools/__pycache__/analyze_antirecoil.cpython-312.pyc
Normal file
Binary file not shown.
BIN
Tools/__pycache__/analyze_shots.cpython-312.pyc
Normal file
BIN
Tools/__pycache__/analyze_shots.cpython-312.pyc
Normal file
Binary file not shown.
779
Tools/analyze_antirecoil.py
Normal file
779
Tools/analyze_antirecoil.py
Normal file
@@ -0,0 +1,779 @@
|
|||||||
|
"""
|
||||||
|
Anti-Recoil Parameter Optimizer
|
||||||
|
================================
|
||||||
|
Reads CSV files recorded by the EBBarrel CSV recording feature and finds
|
||||||
|
optimal parameters for the Adaptive Extrapolation mode.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python analyze_antirecoil.py <csv_file> [csv_file2 ...] [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--plot Generate comparison plots (requires matplotlib)
|
||||||
|
--grid Use grid search instead of differential evolution
|
||||||
|
--strategy <s> Multi-file aggregation: mean (default), worst_case
|
||||||
|
--max-iter <n> Max optimizer iterations (default: 200)
|
||||||
|
|
||||||
|
The script:
|
||||||
|
1. Loads per-frame data (real position/aim vs predicted position/aim)
|
||||||
|
2. Simulates adaptive extrapolation offline (matching C++ exactly)
|
||||||
|
3. Optimizes all 4 parameters: Sensitivity, DeadZone, MinSpeed, Damping
|
||||||
|
4. Reports recommended parameters with per-file breakdown
|
||||||
|
"""
|
||||||
|
|
||||||
|
import csv
|
||||||
|
import sys
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
import argparse
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import List, Tuple, Optional
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Frame:
|
||||||
|
timestamp: float
|
||||||
|
real_pos: Tuple[float, float, float]
|
||||||
|
real_aim: Tuple[float, float, float]
|
||||||
|
pred_pos: Tuple[float, float, float]
|
||||||
|
pred_aim: Tuple[float, float, float]
|
||||||
|
safe_count: int
|
||||||
|
buffer_count: int
|
||||||
|
extrap_time: float
|
||||||
|
shot_fired: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AdaptiveParams:
|
||||||
|
sensitivity: float = 3.0
|
||||||
|
dead_zone: float = 0.95
|
||||||
|
min_speed: float = 0.0
|
||||||
|
damping: float = 5.0
|
||||||
|
buffer_time_ms: float = 200.0
|
||||||
|
discard_time_ms: float = 30.0
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ScoreResult:
|
||||||
|
pos_mean: float
|
||||||
|
pos_p95: float
|
||||||
|
aim_mean: float
|
||||||
|
aim_p95: float
|
||||||
|
jitter: float
|
||||||
|
overshoot: float
|
||||||
|
score: float
|
||||||
|
|
||||||
|
|
||||||
|
def load_csv(path: str) -> List[Frame]:
|
||||||
|
frames = []
|
||||||
|
with open(path, 'r') as f:
|
||||||
|
reader = csv.DictReader(f)
|
||||||
|
has_shot_col = False
|
||||||
|
for row in reader:
|
||||||
|
if not has_shot_col and 'ShotFired' in row:
|
||||||
|
has_shot_col = True
|
||||||
|
frames.append(Frame(
|
||||||
|
timestamp=float(row['Timestamp']),
|
||||||
|
real_pos=(float(row['RealPosX']), float(row['RealPosY']), float(row['RealPosZ'])),
|
||||||
|
real_aim=(float(row['RealAimX']), float(row['RealAimY']), float(row['RealAimZ'])),
|
||||||
|
pred_pos=(float(row['PredPosX']), float(row['PredPosY']), float(row['PredPosZ'])),
|
||||||
|
pred_aim=(float(row['PredAimX']), float(row['PredAimY']), float(row['PredAimZ'])),
|
||||||
|
safe_count=int(row['SafeCount']),
|
||||||
|
buffer_count=int(row['BufferCount']),
|
||||||
|
extrap_time=float(row['ExtrapolationTime']),
|
||||||
|
shot_fired=int(row.get('ShotFired', 0)) == 1,
|
||||||
|
))
|
||||||
|
return frames
|
||||||
|
|
||||||
|
|
||||||
|
# --- Vector math helpers ---
|
||||||
|
|
||||||
|
def vec_dist(a, b):
|
||||||
|
return math.sqrt(sum((ai - bi) ** 2 for ai, bi in zip(a, b)))
|
||||||
|
|
||||||
|
|
||||||
|
def vec_sub(a, b):
|
||||||
|
return tuple(ai - bi for ai, bi in zip(a, b))
|
||||||
|
|
||||||
|
|
||||||
|
def vec_add(a, b):
|
||||||
|
return tuple(ai + bi for ai, bi in zip(a, b))
|
||||||
|
|
||||||
|
|
||||||
|
def vec_scale(a, s):
|
||||||
|
return tuple(ai * s for ai in a)
|
||||||
|
|
||||||
|
|
||||||
|
def vec_len(a):
|
||||||
|
return math.sqrt(sum(ai * ai for ai in a))
|
||||||
|
|
||||||
|
|
||||||
|
def vec_normalize(a):
|
||||||
|
l = vec_len(a)
|
||||||
|
if l < 1e-10:
|
||||||
|
return (0, 0, 0)
|
||||||
|
return tuple(ai / l for ai in a)
|
||||||
|
|
||||||
|
|
||||||
|
def angle_between(a, b):
|
||||||
|
"""Angle in degrees between two direction vectors."""
|
||||||
|
dot = sum(ai * bi for ai, bi in zip(a, b))
|
||||||
|
dot = max(-1.0, min(1.0, dot))
|
||||||
|
return math.degrees(math.acos(dot))
|
||||||
|
|
||||||
|
|
||||||
|
# --- Prediction error from recorded data ---
|
||||||
|
|
||||||
|
def compute_prediction_error(frames: List[Frame]) -> dict:
|
||||||
|
"""Compute error between predicted and actual (real) positions/aims."""
|
||||||
|
pos_errors = []
|
||||||
|
aim_errors = []
|
||||||
|
|
||||||
|
for f in frames:
|
||||||
|
pos_err = vec_dist(f.pred_pos, f.real_pos)
|
||||||
|
pos_errors.append(pos_err)
|
||||||
|
|
||||||
|
aim_a = vec_normalize(f.pred_aim)
|
||||||
|
aim_b = vec_normalize(f.real_aim)
|
||||||
|
if vec_len(aim_a) > 0.5 and vec_len(aim_b) > 0.5:
|
||||||
|
aim_err = angle_between(aim_a, aim_b)
|
||||||
|
aim_errors.append(aim_err)
|
||||||
|
|
||||||
|
if not pos_errors:
|
||||||
|
return {'pos_mean': 0, 'pos_p95': 0, 'pos_max': 0, 'aim_mean': 0, 'aim_p95': 0, 'aim_max': 0}
|
||||||
|
|
||||||
|
pos_errors.sort()
|
||||||
|
aim_errors.sort()
|
||||||
|
|
||||||
|
p95_idx_pos = int(len(pos_errors) * 0.95)
|
||||||
|
p95_idx_aim = int(len(aim_errors) * 0.95) if aim_errors else 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
'pos_mean': sum(pos_errors) / len(pos_errors),
|
||||||
|
'pos_p95': pos_errors[min(p95_idx_pos, len(pos_errors) - 1)],
|
||||||
|
'pos_max': pos_errors[-1],
|
||||||
|
'aim_mean': sum(aim_errors) / len(aim_errors) if aim_errors else 0,
|
||||||
|
'aim_p95': aim_errors[min(p95_idx_aim, len(aim_errors) - 1)] if aim_errors else 0,
|
||||||
|
'aim_max': aim_errors[-1] if aim_errors else 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# --- Shot contamination analysis ---
|
||||||
|
|
||||||
|
def analyze_shot_contamination(frames: List[Frame], analysis_window_ms: float = 200.0):
|
||||||
|
"""
|
||||||
|
Analyze how shots contaminate the tracking data.
|
||||||
|
For each shot, measure the velocity/acceleration spike and how long it takes
|
||||||
|
to return to baseline. This tells us the minimum discard_time needed.
|
||||||
|
|
||||||
|
Returns a dict with analysis results, or None if no shots found.
|
||||||
|
"""
|
||||||
|
shot_indices = [i for i, f in enumerate(frames) if f.shot_fired]
|
||||||
|
if not shot_indices:
|
||||||
|
return None
|
||||||
|
|
||||||
|
analysis_window_s = analysis_window_ms / 1000.0
|
||||||
|
|
||||||
|
# Compute per-frame speeds
|
||||||
|
speeds = [0.0]
|
||||||
|
for i in range(1, len(frames)):
|
||||||
|
dt = frames[i].timestamp - frames[i - 1].timestamp
|
||||||
|
if dt > 1e-6:
|
||||||
|
d = vec_dist(frames[i].real_pos, frames[i - 1].real_pos)
|
||||||
|
speeds.append(d / dt)
|
||||||
|
else:
|
||||||
|
speeds.append(speeds[-1] if speeds else 0.0)
|
||||||
|
|
||||||
|
# For each shot, measure the speed profile before and after
|
||||||
|
contamination_durations = []
|
||||||
|
speed_spikes = []
|
||||||
|
|
||||||
|
for si in shot_indices:
|
||||||
|
# Baseline speed: average speed in 100ms BEFORE the shot
|
||||||
|
baseline_speeds = []
|
||||||
|
for j in range(si - 1, -1, -1):
|
||||||
|
if frames[si].timestamp - frames[j].timestamp > 0.1:
|
||||||
|
break
|
||||||
|
baseline_speeds.append(speeds[j])
|
||||||
|
|
||||||
|
if not baseline_speeds:
|
||||||
|
continue
|
||||||
|
|
||||||
|
baseline_mean = sum(baseline_speeds) / len(baseline_speeds)
|
||||||
|
baseline_std = math.sqrt(sum((s - baseline_mean) ** 2 for s in baseline_speeds) / len(baseline_speeds)) if len(baseline_speeds) > 1 else baseline_mean * 0.1
|
||||||
|
|
||||||
|
# Threshold: speed is "contaminated" if it deviates by more than 3 sigma from baseline
|
||||||
|
threshold = baseline_mean + max(3.0 * baseline_std, 10.0) # at least 10 cm/s spike
|
||||||
|
|
||||||
|
# Find how long after the shot the speed stays above threshold
|
||||||
|
max_speed = 0.0
|
||||||
|
last_contaminated_time = 0.0
|
||||||
|
for j in range(si, len(frames)):
|
||||||
|
dt_from_shot = frames[j].timestamp - frames[si].timestamp
|
||||||
|
if dt_from_shot > analysis_window_s:
|
||||||
|
break
|
||||||
|
if speeds[j] > threshold:
|
||||||
|
last_contaminated_time = dt_from_shot
|
||||||
|
if speeds[j] > max_speed:
|
||||||
|
max_speed = speeds[j]
|
||||||
|
|
||||||
|
contamination_durations.append(last_contaminated_time * 1000.0) # in ms
|
||||||
|
speed_spikes.append(max_speed - baseline_mean)
|
||||||
|
|
||||||
|
if not contamination_durations:
|
||||||
|
return None
|
||||||
|
|
||||||
|
contamination_durations.sort()
|
||||||
|
return {
|
||||||
|
'num_shots': len(shot_indices),
|
||||||
|
'contamination_mean_ms': sum(contamination_durations) / len(contamination_durations),
|
||||||
|
'contamination_p95_ms': contamination_durations[int(len(contamination_durations) * 0.95)],
|
||||||
|
'contamination_max_ms': contamination_durations[-1],
|
||||||
|
'speed_spike_mean': sum(speed_spikes) / len(speed_spikes) if speed_spikes else 0,
|
||||||
|
'speed_spike_max': max(speed_spikes) if speed_spikes else 0,
|
||||||
|
'recommended_discard_ms': math.ceil(contamination_durations[int(len(contamination_durations) * 0.95)] / 5.0) * 5.0, # round up to 5ms
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# --- Offline adaptive extrapolation simulation (matches C++ exactly) ---
|
||||||
|
|
||||||
|
def simulate_adaptive(frames: List[Frame], params: AdaptiveParams) -> Tuple[List[float], List[float]]:
|
||||||
|
"""
|
||||||
|
Simulate the adaptive extrapolation offline with given parameters.
|
||||||
|
Matches the C++ PredictAdaptiveExtrapolation algorithm exactly.
|
||||||
|
Optimized for speed: pre-extracts arrays, inlines math, avoids allocations.
|
||||||
|
"""
|
||||||
|
pos_errors = []
|
||||||
|
aim_errors = []
|
||||||
|
|
||||||
|
n_frames = len(frames)
|
||||||
|
if n_frames < 4:
|
||||||
|
return pos_errors, aim_errors
|
||||||
|
|
||||||
|
# Pre-extract into flat arrays for speed
|
||||||
|
ts = [f.timestamp for f in frames]
|
||||||
|
px = [f.real_pos[0] for f in frames]
|
||||||
|
py = [f.real_pos[1] for f in frames]
|
||||||
|
pz = [f.real_pos[2] for f in frames]
|
||||||
|
ax = [f.real_aim[0] for f in frames]
|
||||||
|
ay = [f.real_aim[1] for f in frames]
|
||||||
|
az = [f.real_aim[2] for f in frames]
|
||||||
|
|
||||||
|
buffer_s = params.buffer_time_ms / 1000.0
|
||||||
|
discard_s = params.discard_time_ms / 1000.0
|
||||||
|
sensitivity = params.sensitivity
|
||||||
|
dead_zone = params.dead_zone
|
||||||
|
min_speed = params.min_speed
|
||||||
|
damping = params.damping
|
||||||
|
SMALL = 1e-10
|
||||||
|
_sqrt = math.sqrt
|
||||||
|
_exp = math.exp
|
||||||
|
_acos = math.acos
|
||||||
|
_degrees = math.degrees
|
||||||
|
_pow = pow
|
||||||
|
|
||||||
|
for i in range(2, n_frames - 1):
|
||||||
|
ct = ts[i]
|
||||||
|
safe_cutoff = ct - discard_s
|
||||||
|
oldest_allowed = ct - buffer_s
|
||||||
|
|
||||||
|
# Collect safe sample indices (backward scan, then reverse)
|
||||||
|
safe = []
|
||||||
|
for j in range(i, -1, -1):
|
||||||
|
t = ts[j]
|
||||||
|
if t < oldest_allowed:
|
||||||
|
break
|
||||||
|
if t <= safe_cutoff:
|
||||||
|
safe.append(j)
|
||||||
|
safe.reverse()
|
||||||
|
|
||||||
|
ns = len(safe)
|
||||||
|
if ns < 2:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Build velocity pairs inline
|
||||||
|
vpx = []; vpy = []; vpz = []
|
||||||
|
vax = []; vay = []; vaz = []
|
||||||
|
for k in range(1, ns):
|
||||||
|
p, c = safe[k - 1], safe[k]
|
||||||
|
dt = ts[c] - ts[p]
|
||||||
|
if dt > 1e-6:
|
||||||
|
inv_dt = 1.0 / dt
|
||||||
|
vpx.append((px[c] - px[p]) * inv_dt)
|
||||||
|
vpy.append((py[c] - py[p]) * inv_dt)
|
||||||
|
vpz.append((pz[c] - pz[p]) * inv_dt)
|
||||||
|
vax.append((ax[c] - ax[p]) * inv_dt)
|
||||||
|
vay.append((ay[c] - ay[p]) * inv_dt)
|
||||||
|
vaz.append((az[c] - az[p]) * inv_dt)
|
||||||
|
|
||||||
|
nv = len(vpx)
|
||||||
|
if nv < 2:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Weighted average velocity (quadratic weights, oldest=index 0)
|
||||||
|
tw = 0.0
|
||||||
|
apx = apy = apz = 0.0
|
||||||
|
aax = aay = aaz = 0.0
|
||||||
|
for k in range(nv):
|
||||||
|
w = (k + 1) * (k + 1)
|
||||||
|
apx += vpx[k] * w; apy += vpy[k] * w; apz += vpz[k] * w
|
||||||
|
aax += vax[k] * w; aay += vay[k] * w; aaz += vaz[k] * w
|
||||||
|
tw += w
|
||||||
|
inv_tw = 1.0 / tw
|
||||||
|
apx *= inv_tw; apy *= inv_tw; apz *= inv_tw
|
||||||
|
aax *= inv_tw; aay *= inv_tw; aaz *= inv_tw
|
||||||
|
|
||||||
|
# Recent velocity (last 25%, unweighted)
|
||||||
|
rs = max(0, nv - max(1, nv // 4))
|
||||||
|
rc = nv - rs
|
||||||
|
rpx = rpy = rpz = 0.0
|
||||||
|
rax = ray = raz = 0.0
|
||||||
|
for k in range(rs, nv):
|
||||||
|
rpx += vpx[k]; rpy += vpy[k]; rpz += vpz[k]
|
||||||
|
rax += vax[k]; ray += vay[k]; raz += vaz[k]
|
||||||
|
inv_rc = 1.0 / rc
|
||||||
|
rpx *= inv_rc; rpy *= inv_rc; rpz *= inv_rc
|
||||||
|
rax *= inv_rc; ray *= inv_rc; raz *= inv_rc
|
||||||
|
|
||||||
|
avg_ps = _sqrt(apx*apx + apy*apy + apz*apz)
|
||||||
|
avg_as = _sqrt(aax*aax + aay*aay + aaz*aaz)
|
||||||
|
rec_ps = _sqrt(rpx*rpx + rpy*rpy + rpz*rpz)
|
||||||
|
rec_as = _sqrt(rax*rax + ray*ray + raz*raz)
|
||||||
|
|
||||||
|
# Position confidence
|
||||||
|
pc = 1.0
|
||||||
|
if avg_ps > min_speed:
|
||||||
|
ratio = rec_ps / avg_ps
|
||||||
|
if ratio > 1.0: ratio = 1.0
|
||||||
|
if ratio < dead_zone:
|
||||||
|
rm = ratio / dead_zone if dead_zone > SMALL else 0.0
|
||||||
|
if rm > 1.0: rm = 1.0
|
||||||
|
pc = _pow(rm, sensitivity)
|
||||||
|
|
||||||
|
# Aim confidence
|
||||||
|
ac = 1.0
|
||||||
|
if avg_as > min_speed:
|
||||||
|
ratio = rec_as / avg_as
|
||||||
|
if ratio > 1.0: ratio = 1.0
|
||||||
|
if ratio < dead_zone:
|
||||||
|
rm = ratio / dead_zone if dead_zone > SMALL else 0.0
|
||||||
|
if rm > 1.0: rm = 1.0
|
||||||
|
ac = _pow(rm, sensitivity)
|
||||||
|
|
||||||
|
# Extrapolation time
|
||||||
|
lsi = safe[-1]
|
||||||
|
edt = ct - ts[lsi]
|
||||||
|
if edt <= 0: edt = 0.011
|
||||||
|
|
||||||
|
# Damping
|
||||||
|
ds = _exp(-damping * edt) if damping > 0.0 else 1.0
|
||||||
|
|
||||||
|
# Predict
|
||||||
|
m = edt * pc * ds
|
||||||
|
ppx = px[lsi] + apx * m
|
||||||
|
ppy = py[lsi] + apy * m
|
||||||
|
ppz = pz[lsi] + apz * m
|
||||||
|
|
||||||
|
ma = edt * ac * ds
|
||||||
|
pax_r = ax[lsi] + aax * ma
|
||||||
|
pay_r = ay[lsi] + aay * ma
|
||||||
|
paz_r = az[lsi] + aaz * ma
|
||||||
|
pa_len = _sqrt(pax_r*pax_r + pay_r*pay_r + paz_r*paz_r)
|
||||||
|
|
||||||
|
# Position error
|
||||||
|
dx = ppx - px[i]; dy = ppy - py[i]; dz = ppz - pz[i]
|
||||||
|
pos_errors.append(_sqrt(dx*dx + dy*dy + dz*dz))
|
||||||
|
|
||||||
|
# Aim error
|
||||||
|
if pa_len > 0.5:
|
||||||
|
inv_pa = 1.0 / pa_len
|
||||||
|
pax_n = pax_r * inv_pa; pay_n = pay_r * inv_pa; paz_n = paz_r * inv_pa
|
||||||
|
ra_len = _sqrt(ax[i]*ax[i] + ay[i]*ay[i] + az[i]*az[i])
|
||||||
|
if ra_len > 0.5:
|
||||||
|
inv_ra = 1.0 / ra_len
|
||||||
|
dot = pax_n * ax[i] * inv_ra + pay_n * ay[i] * inv_ra + paz_n * az[i] * inv_ra
|
||||||
|
if dot > 1.0: dot = 1.0
|
||||||
|
if dot < -1.0: dot = -1.0
|
||||||
|
aim_errors.append(_degrees(_acos(dot)))
|
||||||
|
|
||||||
|
return pos_errors, aim_errors
|
||||||
|
|
||||||
|
|
||||||
|
# --- Scoring ---
|
||||||
|
|
||||||
|
def compute_score(pos_errors: List[float], aim_errors: List[float]) -> ScoreResult:
|
||||||
|
"""Compute a combined score from position and aim errors, including stability metrics."""
|
||||||
|
if not pos_errors:
|
||||||
|
return ScoreResult(0, 0, 0, 0, 0, 0, float('inf'))
|
||||||
|
|
||||||
|
pos_sorted = sorted(pos_errors)
|
||||||
|
aim_sorted = sorted(aim_errors) if aim_errors else [0]
|
||||||
|
|
||||||
|
pos_mean = sum(pos_errors) / len(pos_errors)
|
||||||
|
pos_p95 = pos_sorted[int(len(pos_sorted) * 0.95)]
|
||||||
|
aim_mean = sum(aim_errors) / len(aim_errors) if aim_errors else 0
|
||||||
|
aim_p95 = aim_sorted[int(len(aim_sorted) * 0.95)] if aim_errors else 0
|
||||||
|
|
||||||
|
# Jitter: standard deviation of frame-to-frame error change
|
||||||
|
jitter = 0.0
|
||||||
|
if len(pos_errors) > 1:
|
||||||
|
deltas = [abs(pos_errors[i] - pos_errors[i - 1]) for i in range(1, len(pos_errors))]
|
||||||
|
delta_mean = sum(deltas) / len(deltas)
|
||||||
|
jitter = math.sqrt(sum((d - delta_mean) ** 2 for d in deltas) / len(deltas))
|
||||||
|
|
||||||
|
# Overshoot: percentage of frames where error spikes above 2x mean
|
||||||
|
overshoot = 0.0
|
||||||
|
if pos_mean > 0:
|
||||||
|
overshoot_count = sum(1 for e in pos_errors if e > 2.0 * pos_mean)
|
||||||
|
overshoot = overshoot_count / len(pos_errors)
|
||||||
|
|
||||||
|
# Combined score
|
||||||
|
score = (pos_mean * 0.25 + pos_p95 * 0.15 +
|
||||||
|
aim_mean * 0.25 + aim_p95 * 0.15 +
|
||||||
|
jitter * 0.10 + overshoot * 0.10)
|
||||||
|
|
||||||
|
return ScoreResult(pos_mean, pos_p95, aim_mean, aim_p95, jitter, overshoot, score)
|
||||||
|
|
||||||
|
|
||||||
|
def aggregate_scores(per_file_scores: List[Tuple[str, ScoreResult]],
|
||||||
|
strategy: str = "mean") -> float:
|
||||||
|
"""Aggregate scores across multiple files."""
|
||||||
|
scores = [s.score for _, s in per_file_scores]
|
||||||
|
if not scores:
|
||||||
|
return float('inf')
|
||||||
|
if strategy == "worst_case":
|
||||||
|
return max(scores)
|
||||||
|
else: # mean
|
||||||
|
return sum(scores) / len(scores)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Optimizer ---
|
||||||
|
|
||||||
|
def objective(x, all_frames, strategy):
|
||||||
|
"""Objective function for the optimizer."""
|
||||||
|
params = AdaptiveParams(
|
||||||
|
sensitivity=x[0],
|
||||||
|
dead_zone=x[1],
|
||||||
|
min_speed=x[2],
|
||||||
|
damping=x[3],
|
||||||
|
buffer_time_ms=x[4],
|
||||||
|
discard_time_ms=x[5]
|
||||||
|
)
|
||||||
|
per_file_scores = []
|
||||||
|
for name, frames in all_frames:
|
||||||
|
pos_errors, aim_errors = simulate_adaptive(frames, params)
|
||||||
|
score_result = compute_score(pos_errors, aim_errors)
|
||||||
|
per_file_scores.append((name, score_result))
|
||||||
|
return aggregate_scores(per_file_scores, strategy)
|
||||||
|
|
||||||
|
|
||||||
|
def optimize_differential_evolution(all_frames, strategy="mean", max_iter=200, min_discard_ms=10.0):
|
||||||
|
"""Find optimal parameters using scipy differential evolution."""
|
||||||
|
try:
|
||||||
|
from scipy.optimize import differential_evolution
|
||||||
|
except ImportError:
|
||||||
|
print("ERROR: scipy is required for optimization.")
|
||||||
|
print("Install with: pip install scipy")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
bounds = [
|
||||||
|
(0.1, 5.0), # sensitivity
|
||||||
|
(0.0, 0.95), # dead_zone
|
||||||
|
(0.0, 200.0), # min_speed
|
||||||
|
(0.0, 50.0), # damping
|
||||||
|
(100.0, 500.0), # buffer_time_ms
|
||||||
|
(max(10.0, min_discard_ms), 100.0), # discard_time_ms (floor from contamination analysis)
|
||||||
|
]
|
||||||
|
|
||||||
|
print(f"\nRunning differential evolution (maxiter={max_iter}, popsize=25, min_discard={min_discard_ms:.0f}ms)...")
|
||||||
|
print("This may take a few minutes...\n")
|
||||||
|
|
||||||
|
result = differential_evolution(
|
||||||
|
objective,
|
||||||
|
bounds,
|
||||||
|
args=(all_frames, strategy),
|
||||||
|
maxiter=max_iter,
|
||||||
|
seed=42,
|
||||||
|
tol=1e-4,
|
||||||
|
popsize=25,
|
||||||
|
disp=True,
|
||||||
|
workers=1
|
||||||
|
)
|
||||||
|
|
||||||
|
best_params = AdaptiveParams(
|
||||||
|
sensitivity=round(result.x[0], 2),
|
||||||
|
dead_zone=round(result.x[1], 3),
|
||||||
|
min_speed=round(result.x[2], 1),
|
||||||
|
damping=round(result.x[3], 1),
|
||||||
|
buffer_time_ms=round(result.x[4], 0),
|
||||||
|
discard_time_ms=round(result.x[5], 0)
|
||||||
|
)
|
||||||
|
return best_params, result.fun
|
||||||
|
|
||||||
|
|
||||||
|
def optimize_grid_search(all_frames, strategy="mean", min_discard_ms=10.0):
|
||||||
|
"""Find optimal parameters using grid search (slower but no scipy needed)."""
|
||||||
|
print(f"\nRunning grid search over 6 parameters (min_discard={min_discard_ms:.0f}ms)...")
|
||||||
|
|
||||||
|
sensitivities = [1.0, 2.0, 3.0, 4.0]
|
||||||
|
dead_zones = [0.7, 0.8, 0.9]
|
||||||
|
min_speeds = [0.0, 30.0]
|
||||||
|
dampings = [5.0, 10.0, 15.0]
|
||||||
|
buffer_times = [300.0, 400.0, 500.0, 600.0, 800.0]
|
||||||
|
discard_times = [d for d in [20.0, 40.0, 60.0, 100.0, 150.0, 200.0] if d >= min_discard_ms]
|
||||||
|
if not discard_times:
|
||||||
|
discard_times = [min_discard_ms]
|
||||||
|
|
||||||
|
total = (len(sensitivities) * len(dead_zones) * len(min_speeds) *
|
||||||
|
len(dampings) * len(buffer_times) * len(discard_times))
|
||||||
|
print(f"Total combinations: {total}")
|
||||||
|
|
||||||
|
best_score = float('inf')
|
||||||
|
best_params = AdaptiveParams()
|
||||||
|
count = 0
|
||||||
|
|
||||||
|
for sens in sensitivities:
|
||||||
|
for dz in dead_zones:
|
||||||
|
for ms in min_speeds:
|
||||||
|
for damp in dampings:
|
||||||
|
for bt in buffer_times:
|
||||||
|
for dt in discard_times:
|
||||||
|
count += 1
|
||||||
|
if count % 500 == 0:
|
||||||
|
print(f" Progress: {count}/{total} ({100 * count / total:.0f}%) best={best_score:.4f}")
|
||||||
|
|
||||||
|
params = AdaptiveParams(sens, dz, ms, damp, bt, dt)
|
||||||
|
per_file_scores = []
|
||||||
|
for name, frames in all_frames:
|
||||||
|
pos_errors, aim_errors = simulate_adaptive(frames, params)
|
||||||
|
score_result = compute_score(pos_errors, aim_errors)
|
||||||
|
per_file_scores.append((name, score_result))
|
||||||
|
|
||||||
|
score = aggregate_scores(per_file_scores, strategy)
|
||||||
|
if score < best_score:
|
||||||
|
best_score = score
|
||||||
|
best_params = params
|
||||||
|
|
||||||
|
return best_params, best_score
|
||||||
|
|
||||||
|
|
||||||
|
# --- Main ---
|
||||||
|
|
||||||
|
def print_file_stats(name: str, frames: List[Frame]):
|
||||||
|
"""Print basic stats for a CSV file."""
|
||||||
|
duration = frames[-1].timestamp - frames[0].timestamp
|
||||||
|
avg_fps = len(frames) / duration if duration > 0 else 0
|
||||||
|
avg_safe = sum(f.safe_count for f in frames) / len(frames)
|
||||||
|
avg_buffer = sum(f.buffer_count for f in frames) / len(frames)
|
||||||
|
avg_extrap = sum(f.extrap_time for f in frames) / len(frames) * 1000
|
||||||
|
num_shots = sum(1 for f in frames if f.shot_fired)
|
||||||
|
print(f" {os.path.basename(name)}: {len(frames)} frames, {avg_fps:.0f}fps, "
|
||||||
|
f"{duration:.1f}s, safe={avg_safe:.1f}, extrap={avg_extrap:.1f}ms, shots={num_shots}")
|
||||||
|
|
||||||
|
|
||||||
|
def print_score_detail(name: str, score: ScoreResult):
|
||||||
|
"""Print detailed score for a file."""
|
||||||
|
print(f" {os.path.basename(name):30s} Pos: mean={score.pos_mean:.3f}cm p95={score.pos_p95:.3f}cm | "
|
||||||
|
f"Aim: mean={score.aim_mean:.3f}deg p95={score.aim_p95:.3f}deg | "
|
||||||
|
f"jitter={score.jitter:.3f} overshoot={score.overshoot:.1%} | "
|
||||||
|
f"score={score.score:.4f}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Anti-Recoil Parameter Optimizer - finds optimal AdaptiveExtrapolation parameters"
|
||||||
|
)
|
||||||
|
parser.add_argument("csv_files", nargs="+", help="One or more CSV recording files")
|
||||||
|
parser.add_argument("--plot", action="store_true", help="Generate comparison plots (requires matplotlib)")
|
||||||
|
parser.add_argument("--grid", action="store_true", help="Use grid search instead of differential evolution")
|
||||||
|
parser.add_argument("--strategy", choices=["mean", "worst_case"], default="mean",
|
||||||
|
help="Multi-file score aggregation strategy (default: mean)")
|
||||||
|
parser.add_argument("--max-iter", type=int, default=200, help="Max optimizer iterations (default: 200)")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Load all CSV files
|
||||||
|
all_frames = []
|
||||||
|
for csv_path in args.csv_files:
|
||||||
|
if not os.path.exists(csv_path):
|
||||||
|
print(f"Error: File not found: {csv_path}")
|
||||||
|
sys.exit(1)
|
||||||
|
frames = load_csv(csv_path)
|
||||||
|
if len(frames) < 50:
|
||||||
|
print(f"Warning: {csv_path} has only {len(frames)} frames (need at least 50 for good results)")
|
||||||
|
all_frames.append((csv_path, frames))
|
||||||
|
|
||||||
|
print(f"\nLoaded {len(all_frames)} file(s)")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
# Per-file stats
|
||||||
|
print("\n=== FILE STATISTICS ===")
|
||||||
|
for name, frames in all_frames:
|
||||||
|
print_file_stats(name, frames)
|
||||||
|
|
||||||
|
# Shot contamination analysis
|
||||||
|
has_shots = any(any(f.shot_fired for f in frames) for _, frames in all_frames)
|
||||||
|
if has_shots:
|
||||||
|
print("\n=== SHOT CONTAMINATION ANALYSIS ===")
|
||||||
|
max_recommended_discard = 0.0
|
||||||
|
for name, frames in all_frames:
|
||||||
|
result = analyze_shot_contamination(frames)
|
||||||
|
if result:
|
||||||
|
print(f" {os.path.basename(name)}:")
|
||||||
|
print(f" Shots detected: {result['num_shots']}")
|
||||||
|
print(f" Speed spike: mean={result['speed_spike_mean']:.1f} cm/s, max={result['speed_spike_max']:.1f} cm/s")
|
||||||
|
print(f" Contamination duration: mean={result['contamination_mean_ms']:.1f}ms, "
|
||||||
|
f"p95={result['contamination_p95_ms']:.1f}ms, max={result['contamination_max_ms']:.1f}ms")
|
||||||
|
print(f" Recommended discard_time: >= {result['recommended_discard_ms']:.0f}ms")
|
||||||
|
max_recommended_discard = max(max_recommended_discard, result['recommended_discard_ms'])
|
||||||
|
else:
|
||||||
|
print(f" {os.path.basename(name)}: no shots detected")
|
||||||
|
|
||||||
|
if max_recommended_discard > 0:
|
||||||
|
print(f"\n >>> MINIMUM SAFE DiscardTime across all files: {max_recommended_discard:.0f}ms <<<")
|
||||||
|
else:
|
||||||
|
print("\n (No ShotFired data in CSV - record with updated plugin to get contamination analysis)")
|
||||||
|
|
||||||
|
# Baseline: current default parameters
|
||||||
|
default_params = AdaptiveParams()
|
||||||
|
print(f"\n=== BASELINE (defaults: sens={default_params.sensitivity}, dz={default_params.dead_zone}, "
|
||||||
|
f"minspd={default_params.min_speed}, damp={default_params.damping}, "
|
||||||
|
f"buf={default_params.buffer_time_ms}ms, disc={default_params.discard_time_ms}ms) ===")
|
||||||
|
|
||||||
|
baseline_scores = []
|
||||||
|
for name, frames in all_frames:
|
||||||
|
pos_errors, aim_errors = simulate_adaptive(frames, default_params)
|
||||||
|
score = compute_score(pos_errors, aim_errors)
|
||||||
|
baseline_scores.append((name, score))
|
||||||
|
print_score_detail(name, score)
|
||||||
|
|
||||||
|
baseline_agg = aggregate_scores(baseline_scores, args.strategy)
|
||||||
|
print(f"\n Aggregate score ({args.strategy}): {baseline_agg:.4f}")
|
||||||
|
|
||||||
|
# Also show recorded prediction error (as-is from the engine)
|
||||||
|
print(f"\n=== RECORDED PREDICTION ERROR (as captured in-engine) ===")
|
||||||
|
for name, frames in all_frames:
|
||||||
|
err = compute_prediction_error(frames)
|
||||||
|
print(f" {os.path.basename(name):30s} Pos: mean={err['pos_mean']:.3f}cm p95={err['pos_p95']:.3f}cm | "
|
||||||
|
f"Aim: mean={err['aim_mean']:.3f}deg p95={err['aim_p95']:.3f}deg")
|
||||||
|
|
||||||
|
# Compute minimum safe discard time from shot contamination analysis
|
||||||
|
min_discard_ms = 10.0 # absolute minimum
|
||||||
|
if has_shots:
|
||||||
|
for name, frames in all_frames:
|
||||||
|
result = analyze_shot_contamination(frames)
|
||||||
|
if result and result['recommended_discard_ms'] > min_discard_ms:
|
||||||
|
min_discard_ms = result['recommended_discard_ms']
|
||||||
|
|
||||||
|
# Optimize
|
||||||
|
print(f"\n=== OPTIMIZATION ({args.strategy}) ===")
|
||||||
|
|
||||||
|
if args.grid:
|
||||||
|
best_params, best_score = optimize_grid_search(all_frames, args.strategy, min_discard_ms)
|
||||||
|
else:
|
||||||
|
best_params, best_score = optimize_differential_evolution(all_frames, args.strategy, args.max_iter, min_discard_ms)
|
||||||
|
|
||||||
|
# Results
|
||||||
|
print(f"\n{'=' * 70}")
|
||||||
|
print(f" BEST PARAMETERS FOUND:")
|
||||||
|
print(f" AdaptiveSensitivity = {best_params.sensitivity}")
|
||||||
|
print(f" AdaptiveDeadZone = {best_params.dead_zone}")
|
||||||
|
print(f" AdaptiveMinSpeed = {best_params.min_speed}")
|
||||||
|
print(f" ExtrapolationDamping = {best_params.damping}")
|
||||||
|
print(f" AntiRecoilBufferTimeMs = {best_params.buffer_time_ms}")
|
||||||
|
print(f" AntiRecoilDiscardTimeMs= {best_params.discard_time_ms}")
|
||||||
|
print(f"{'=' * 70}")
|
||||||
|
|
||||||
|
# Per-file breakdown with optimized params
|
||||||
|
print(f"\n=== OPTIMIZED RESULTS ===")
|
||||||
|
opt_scores = []
|
||||||
|
for name, frames in all_frames:
|
||||||
|
pos_errors, aim_errors = simulate_adaptive(frames, best_params)
|
||||||
|
score = compute_score(pos_errors, aim_errors)
|
||||||
|
opt_scores.append((name, score))
|
||||||
|
print_score_detail(name, score)
|
||||||
|
|
||||||
|
opt_agg = aggregate_scores(opt_scores, args.strategy)
|
||||||
|
print(f"\n Aggregate score ({args.strategy}): {opt_agg:.4f}")
|
||||||
|
|
||||||
|
# Improvement
|
||||||
|
print(f"\n=== IMPROVEMENT vs BASELINE ===")
|
||||||
|
for (name, baseline), (_, optimized) in zip(baseline_scores, opt_scores):
|
||||||
|
pos_pct = ((baseline.pos_mean - optimized.pos_mean) / baseline.pos_mean * 100) if baseline.pos_mean > 0 else 0
|
||||||
|
aim_pct = ((baseline.aim_mean - optimized.aim_mean) / baseline.aim_mean * 100) if baseline.aim_mean > 0 else 0
|
||||||
|
score_pct = ((baseline.score - optimized.score) / baseline.score * 100) if baseline.score > 0 else 0
|
||||||
|
print(f" {os.path.basename(name):30s} Pos: {pos_pct:+.1f}% | Aim: {aim_pct:+.1f}% | Score: {score_pct:+.1f}%")
|
||||||
|
|
||||||
|
total_pct = ((baseline_agg - opt_agg) / baseline_agg * 100) if baseline_agg > 0 else 0
|
||||||
|
print(f" {'TOTAL':30s} Score: {total_pct:+.1f}%")
|
||||||
|
|
||||||
|
# Plotting
|
||||||
|
if args.plot:
|
||||||
|
try:
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
n_files = len(all_frames)
|
||||||
|
fig, axes = plt.subplots(n_files, 3, figsize=(18, 5 * n_files), squeeze=False)
|
||||||
|
|
||||||
|
for row, (name, frames) in enumerate(all_frames):
|
||||||
|
timestamps = [f.timestamp - frames[0].timestamp for f in frames]
|
||||||
|
short_name = os.path.basename(name)
|
||||||
|
|
||||||
|
# Baseline errors
|
||||||
|
bl_pos, bl_aim = simulate_adaptive(frames, default_params)
|
||||||
|
# Optimized errors
|
||||||
|
op_pos, op_aim = simulate_adaptive(frames, best_params)
|
||||||
|
|
||||||
|
# Time axis for simulated errors (offset by window_size)
|
||||||
|
t_start = window_size = 12
|
||||||
|
sim_timestamps = [frames[i].timestamp - frames[0].timestamp
|
||||||
|
for i in range(t_start + 1, t_start + 1 + len(bl_pos))]
|
||||||
|
|
||||||
|
# Position error
|
||||||
|
ax = axes[row][0]
|
||||||
|
if len(sim_timestamps) == len(bl_pos):
|
||||||
|
ax.plot(sim_timestamps, bl_pos, 'r-', alpha=0.4, linewidth=0.5, label='Baseline')
|
||||||
|
ax.plot(sim_timestamps, op_pos, 'g-', alpha=0.4, linewidth=0.5, label='Optimized')
|
||||||
|
ax.set_ylabel('Position Error (cm)')
|
||||||
|
ax.set_title(f'{short_name} - Position Error')
|
||||||
|
ax.legend()
|
||||||
|
|
||||||
|
# Aim error
|
||||||
|
ax = axes[row][1]
|
||||||
|
if len(sim_timestamps) >= len(bl_aim):
|
||||||
|
t_aim = sim_timestamps[:len(bl_aim)]
|
||||||
|
ax.plot(t_aim, bl_aim, 'r-', alpha=0.4, linewidth=0.5, label='Baseline')
|
||||||
|
if len(sim_timestamps) >= len(op_aim):
|
||||||
|
t_aim = sim_timestamps[:len(op_aim)]
|
||||||
|
ax.plot(t_aim, op_aim, 'g-', alpha=0.4, linewidth=0.5, label='Optimized')
|
||||||
|
ax.set_ylabel('Aim Error (deg)')
|
||||||
|
ax.set_title(f'{short_name} - Aim Error')
|
||||||
|
ax.legend()
|
||||||
|
|
||||||
|
# Speed profile
|
||||||
|
ax = axes[row][2]
|
||||||
|
speeds = [0]
|
||||||
|
for i in range(1, len(frames)):
|
||||||
|
dt = frames[i].timestamp - frames[i - 1].timestamp
|
||||||
|
if dt > 1e-6:
|
||||||
|
d = vec_dist(frames[i].real_pos, frames[i - 1].real_pos)
|
||||||
|
speeds.append(d / dt)
|
||||||
|
else:
|
||||||
|
speeds.append(speeds[-1])
|
||||||
|
ax.plot(timestamps, speeds, 'b-', alpha=0.7, linewidth=0.5)
|
||||||
|
ax.set_ylabel('Speed (cm/s)')
|
||||||
|
ax.set_xlabel('Time (s)')
|
||||||
|
ax.set_title(f'{short_name} - Speed Profile')
|
||||||
|
|
||||||
|
plt.tight_layout()
|
||||||
|
plot_path = args.csv_files[0].replace('.csv', '_optimizer.png')
|
||||||
|
plt.savefig(plot_path, dpi=150)
|
||||||
|
print(f"\nPlot saved: {plot_path}")
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
print("\nmatplotlib not installed. Install with: pip install matplotlib")
|
||||||
|
|
||||||
|
print("\nDone.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
366
Tools/analyze_shots.py
Normal file
366
Tools/analyze_shots.py
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
"""
|
||||||
|
Shot Contamination Analyzer
|
||||||
|
============================
|
||||||
|
Analyzes the precise contamination zone around each shot event.
|
||||||
|
Shows speed/acceleration profiles before and after each shot to identify
|
||||||
|
the exact duration of IMU perturbation vs voluntary movement.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python analyze_shots.py <csv_file> [--plot] [--window 100]
|
||||||
|
|
||||||
|
Protocol for best results:
|
||||||
|
1. Stay stable (no movement) for 2-3 seconds
|
||||||
|
2. Fire a single shot
|
||||||
|
3. Stay stable again for 2-3 seconds
|
||||||
|
4. Repeat 10+ times
|
||||||
|
This isolates the IMU shock from voluntary movement.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import csv
|
||||||
|
import sys
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
import argparse
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import List, Tuple
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Frame:
|
||||||
|
timestamp: float
|
||||||
|
real_pos: Tuple[float, float, float]
|
||||||
|
real_aim: Tuple[float, float, float]
|
||||||
|
pred_pos: Tuple[float, float, float]
|
||||||
|
pred_aim: Tuple[float, float, float]
|
||||||
|
safe_count: int
|
||||||
|
buffer_count: int
|
||||||
|
extrap_time: float
|
||||||
|
shot_fired: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
def load_csv(path: str) -> List[Frame]:
|
||||||
|
frames = []
|
||||||
|
with open(path, 'r') as f:
|
||||||
|
reader = csv.DictReader(f)
|
||||||
|
for row in reader:
|
||||||
|
frames.append(Frame(
|
||||||
|
timestamp=float(row['Timestamp']),
|
||||||
|
real_pos=(float(row['RealPosX']), float(row['RealPosY']), float(row['RealPosZ'])),
|
||||||
|
real_aim=(float(row['RealAimX']), float(row['RealAimY']), float(row['RealAimZ'])),
|
||||||
|
pred_pos=(float(row['PredPosX']), float(row['PredPosY']), float(row['PredPosZ'])),
|
||||||
|
pred_aim=(float(row['PredAimX']), float(row['PredAimY']), float(row['PredAimZ'])),
|
||||||
|
safe_count=int(row['SafeCount']),
|
||||||
|
buffer_count=int(row['BufferCount']),
|
||||||
|
extrap_time=float(row['ExtrapolationTime']),
|
||||||
|
shot_fired=int(row.get('ShotFired', 0)) == 1,
|
||||||
|
))
|
||||||
|
return frames
|
||||||
|
|
||||||
|
|
||||||
|
def vec_dist(a, b):
|
||||||
|
return math.sqrt(sum((ai - bi) ** 2 for ai, bi in zip(a, b)))
|
||||||
|
|
||||||
|
|
||||||
|
def vec_sub(a, b):
|
||||||
|
return tuple(ai - bi for ai, bi in zip(a, b))
|
||||||
|
|
||||||
|
|
||||||
|
def vec_len(a):
|
||||||
|
return math.sqrt(sum(ai * ai for ai in a))
|
||||||
|
|
||||||
|
|
||||||
|
def vec_normalize(a):
|
||||||
|
l = vec_len(a)
|
||||||
|
if l < 1e-10:
|
||||||
|
return (0, 0, 0)
|
||||||
|
return tuple(ai / l for ai in a)
|
||||||
|
|
||||||
|
|
||||||
|
def angle_between(a, b):
|
||||||
|
dot = sum(ai * bi for ai, bi in zip(a, b))
|
||||||
|
dot = max(-1.0, min(1.0, dot))
|
||||||
|
return math.degrees(math.acos(dot))
|
||||||
|
|
||||||
|
|
||||||
|
def compute_per_frame_metrics(frames):
|
||||||
|
"""Compute speed, acceleration, and aim angular speed per frame."""
|
||||||
|
n = len(frames)
|
||||||
|
pos_speed = [0.0] * n
|
||||||
|
aim_speed = [0.0] * n
|
||||||
|
pos_accel = [0.0] * n
|
||||||
|
|
||||||
|
for i in range(1, n):
|
||||||
|
dt = frames[i].timestamp - frames[i - 1].timestamp
|
||||||
|
if dt > 1e-6:
|
||||||
|
pos_speed[i] = vec_dist(frames[i].real_pos, frames[i - 1].real_pos) / dt
|
||||||
|
|
||||||
|
aim_a = vec_normalize(frames[i].real_aim)
|
||||||
|
aim_b = vec_normalize(frames[i - 1].real_aim)
|
||||||
|
if vec_len(aim_a) > 0.5 and vec_len(aim_b) > 0.5:
|
||||||
|
aim_speed[i] = angle_between(aim_a, aim_b) / dt # deg/s
|
||||||
|
|
||||||
|
for i in range(1, n):
|
||||||
|
dt = frames[i].timestamp - frames[i - 1].timestamp
|
||||||
|
if dt > 1e-6:
|
||||||
|
pos_accel[i] = (pos_speed[i] - pos_speed[i - 1]) / dt
|
||||||
|
|
||||||
|
return pos_speed, aim_speed, pos_accel
|
||||||
|
|
||||||
|
|
||||||
|
def analyze_single_shot(frames, shot_idx, pos_speed, aim_speed, pos_accel, window_ms=200.0):
|
||||||
|
"""Analyze contamination around a single shot event."""
|
||||||
|
window_s = window_ms / 1000.0
|
||||||
|
shot_time = frames[shot_idx].timestamp
|
||||||
|
|
||||||
|
# Collect frames in window before and after shot
|
||||||
|
before = [] # (time_relative_ms, pos_speed, aim_speed, pos_accel)
|
||||||
|
after = []
|
||||||
|
|
||||||
|
for i in range(max(0, shot_idx - 100), min(len(frames), shot_idx + 100)):
|
||||||
|
dt_ms = (frames[i].timestamp - shot_time) * 1000.0
|
||||||
|
if -window_ms <= dt_ms < 0:
|
||||||
|
before.append((dt_ms, pos_speed[i], aim_speed[i], pos_accel[i]))
|
||||||
|
elif dt_ms >= 0 and dt_ms <= window_ms:
|
||||||
|
after.append((dt_ms, pos_speed[i], aim_speed[i], pos_accel[i]))
|
||||||
|
|
||||||
|
if not before:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Baseline: average speed in the window before the shot
|
||||||
|
baseline_pos_speed = sum(s for _, s, _, _ in before) / len(before)
|
||||||
|
baseline_aim_speed = sum(s for _, _, s, _ in before) / len(before)
|
||||||
|
baseline_pos_std = math.sqrt(sum((s - baseline_pos_speed) ** 2 for _, s, _, _ in before) / len(before)) if len(before) > 1 else 0.0
|
||||||
|
baseline_aim_std = math.sqrt(sum((s - baseline_aim_speed) ** 2 for _, _, s, _ in before) / len(before)) if len(before) > 1 else 0.0
|
||||||
|
|
||||||
|
# Find contamination end: when speed returns to within 2 sigma of baseline
|
||||||
|
pos_threshold = baseline_pos_speed + max(2.0 * baseline_pos_std, 5.0) # at least 5 cm/s
|
||||||
|
aim_threshold = baseline_aim_speed + max(2.0 * baseline_aim_std, 5.0) # at least 5 deg/s
|
||||||
|
|
||||||
|
pos_contamination_end_ms = 0.0
|
||||||
|
aim_contamination_end_ms = 0.0
|
||||||
|
max_pos_spike = 0.0
|
||||||
|
max_aim_spike = 0.0
|
||||||
|
|
||||||
|
for dt_ms, ps, ais, _ in after:
|
||||||
|
if ps > pos_threshold:
|
||||||
|
pos_contamination_end_ms = dt_ms
|
||||||
|
if ais > aim_threshold:
|
||||||
|
aim_contamination_end_ms = dt_ms
|
||||||
|
max_pos_spike = max(max_pos_spike, ps - baseline_pos_speed)
|
||||||
|
max_aim_spike = max(max_aim_spike, ais - baseline_aim_speed)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'shot_time': shot_time,
|
||||||
|
'baseline_pos_speed': baseline_pos_speed,
|
||||||
|
'baseline_aim_speed': baseline_aim_speed,
|
||||||
|
'baseline_pos_std': baseline_pos_std,
|
||||||
|
'baseline_aim_std': baseline_aim_std,
|
||||||
|
'pos_contamination_ms': pos_contamination_end_ms,
|
||||||
|
'aim_contamination_ms': aim_contamination_end_ms,
|
||||||
|
'max_contamination_ms': max(pos_contamination_end_ms, aim_contamination_end_ms),
|
||||||
|
'max_pos_spike': max_pos_spike,
|
||||||
|
'max_aim_spike': max_aim_spike,
|
||||||
|
'pos_threshold': pos_threshold,
|
||||||
|
'aim_threshold': aim_threshold,
|
||||||
|
'before': before,
|
||||||
|
'after': after,
|
||||||
|
'is_stable': baseline_pos_speed < 30.0 and baseline_aim_speed < 200.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Shot Contamination Analyzer")
|
||||||
|
parser.add_argument("csv_file", help="CSV recording file with ShotFired column")
|
||||||
|
parser.add_argument("--plot", action="store_true", help="Generate per-shot plots (requires matplotlib)")
|
||||||
|
parser.add_argument("--window", type=float, default=200.0, help="Analysis window in ms before/after shot (default: 200)")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if not os.path.exists(args.csv_file):
|
||||||
|
print(f"Error: File not found: {args.csv_file}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
frames = load_csv(args.csv_file)
|
||||||
|
print(f"Loaded {len(frames)} frames from {os.path.basename(args.csv_file)}")
|
||||||
|
|
||||||
|
duration = frames[-1].timestamp - frames[0].timestamp
|
||||||
|
fps = len(frames) / duration if duration > 0 else 0
|
||||||
|
print(f"Duration: {duration:.1f}s | FPS: {fps:.0f}")
|
||||||
|
|
||||||
|
shot_indices = [i for i, f in enumerate(frames) if f.shot_fired]
|
||||||
|
print(f"Shots detected: {len(shot_indices)}")
|
||||||
|
|
||||||
|
if not shot_indices:
|
||||||
|
print("No shots found! Make sure the CSV has a ShotFired column.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
pos_speed, aim_speed, pos_accel = compute_per_frame_metrics(frames)
|
||||||
|
|
||||||
|
# Analyze each shot
|
||||||
|
results = []
|
||||||
|
print(f"\n{'=' * 90}")
|
||||||
|
print(f"{'Shot':>4} {'Time':>8} {'Stable':>7} {'PosSpike':>10} {'AimSpike':>10} "
|
||||||
|
f"{'PosContam':>10} {'AimContam':>10} {'MaxContam':>10}")
|
||||||
|
print(f"{'':>4} {'(s)':>8} {'':>7} {'(cm/s)':>10} {'(deg/s)':>10} "
|
||||||
|
f"{'(ms)':>10} {'(ms)':>10} {'(ms)':>10}")
|
||||||
|
print(f"{'-' * 90}")
|
||||||
|
|
||||||
|
for idx, si in enumerate(shot_indices):
|
||||||
|
result = analyze_single_shot(frames, si, pos_speed, aim_speed, pos_accel, args.window)
|
||||||
|
if result is None:
|
||||||
|
continue
|
||||||
|
results.append(result)
|
||||||
|
|
||||||
|
stable_str = "YES" if result['is_stable'] else "no"
|
||||||
|
print(f"{idx + 1:>4} {result['shot_time']:>8.2f} {stable_str:>7} "
|
||||||
|
f"{result['max_pos_spike']:>10.1f} {result['max_aim_spike']:>10.1f} "
|
||||||
|
f"{result['pos_contamination_ms']:>10.1f} {result['aim_contamination_ms']:>10.1f} "
|
||||||
|
f"{result['max_contamination_ms']:>10.1f}")
|
||||||
|
|
||||||
|
# Summary: only stable shots (user was not moving)
|
||||||
|
stable_results = [r for r in results if r['is_stable']]
|
||||||
|
all_results = results
|
||||||
|
|
||||||
|
print(f"\n{'=' * 90}")
|
||||||
|
print(f"SUMMARY - ALL SHOTS ({len(all_results)} shots)")
|
||||||
|
if all_results:
|
||||||
|
contam_all = sorted([r['max_contamination_ms'] for r in all_results])
|
||||||
|
pos_spikes = sorted([r['max_pos_spike'] for r in all_results])
|
||||||
|
aim_spikes = sorted([r['max_aim_spike'] for r in all_results])
|
||||||
|
p95_idx = int(len(contam_all) * 0.95)
|
||||||
|
print(f" Contamination: mean={sum(contam_all)/len(contam_all):.1f}ms, "
|
||||||
|
f"median={contam_all[len(contam_all)//2]:.1f}ms, "
|
||||||
|
f"p95={contam_all[min(p95_idx, len(contam_all)-1)]:.1f}ms, "
|
||||||
|
f"max={contam_all[-1]:.1f}ms")
|
||||||
|
print(f" Pos spike: mean={sum(pos_spikes)/len(pos_spikes):.1f}cm/s, "
|
||||||
|
f"max={pos_spikes[-1]:.1f}cm/s")
|
||||||
|
print(f" Aim spike: mean={sum(aim_spikes)/len(aim_spikes):.1f}deg/s, "
|
||||||
|
f"max={aim_spikes[-1]:.1f}deg/s")
|
||||||
|
|
||||||
|
print(f"\nSUMMARY - STABLE SHOTS ONLY ({len(stable_results)} shots, baseline speed < 20cm/s)")
|
||||||
|
if stable_results:
|
||||||
|
contam_stable = sorted([r['max_contamination_ms'] for r in stable_results])
|
||||||
|
pos_spikes_s = sorted([r['max_pos_spike'] for r in stable_results])
|
||||||
|
aim_spikes_s = sorted([r['max_aim_spike'] for r in stable_results])
|
||||||
|
p95_idx = int(len(contam_stable) * 0.95)
|
||||||
|
print(f" Contamination: mean={sum(contam_stable)/len(contam_stable):.1f}ms, "
|
||||||
|
f"median={contam_stable[len(contam_stable)//2]:.1f}ms, "
|
||||||
|
f"p95={contam_stable[min(p95_idx, len(contam_stable)-1)]:.1f}ms, "
|
||||||
|
f"max={contam_stable[-1]:.1f}ms")
|
||||||
|
print(f" Pos spike: mean={sum(pos_spikes_s)/len(pos_spikes_s):.1f}cm/s, "
|
||||||
|
f"max={pos_spikes_s[-1]:.1f}cm/s")
|
||||||
|
print(f" Aim spike: mean={sum(aim_spikes_s)/len(aim_spikes_s):.1f}deg/s, "
|
||||||
|
f"max={aim_spikes_s[-1]:.1f}deg/s")
|
||||||
|
|
||||||
|
recommended = math.ceil(contam_stable[min(p95_idx, len(contam_stable)-1)] / 5.0) * 5.0
|
||||||
|
print(f"\n >>> RECOMMENDED DiscardTime (from stable shots P95): {recommended:.0f}ms <<<")
|
||||||
|
else:
|
||||||
|
print(" No stable shots found! Make sure you stay still before firing.")
|
||||||
|
print(" Shots where baseline speed > 20cm/s are excluded as 'not stable'.")
|
||||||
|
|
||||||
|
# Plotting
|
||||||
|
if args.plot:
|
||||||
|
try:
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
# Plot each shot individually
|
||||||
|
n_shots = len(results)
|
||||||
|
cols = min(4, n_shots)
|
||||||
|
rows = math.ceil(n_shots / cols)
|
||||||
|
fig, axes = plt.subplots(rows, cols, figsize=(5 * cols, 4 * rows), squeeze=False)
|
||||||
|
fig.suptitle(f'Per-Shot Speed Profile ({os.path.basename(args.csv_file)})', fontsize=14)
|
||||||
|
|
||||||
|
for idx, result in enumerate(results):
|
||||||
|
r, c = divmod(idx, cols)
|
||||||
|
ax = axes[r][c]
|
||||||
|
|
||||||
|
# Before shot
|
||||||
|
if result['before']:
|
||||||
|
t_before = [b[0] for b in result['before']]
|
||||||
|
s_before = [b[1] for b in result['before']]
|
||||||
|
ax.plot(t_before, s_before, 'b-', linewidth=1, label='Before')
|
||||||
|
|
||||||
|
# After shot
|
||||||
|
if result['after']:
|
||||||
|
t_after = [a[0] for a in result['after']]
|
||||||
|
s_after = [a[1] for a in result['after']]
|
||||||
|
ax.plot(t_after, s_after, 'r-', linewidth=1, label='After')
|
||||||
|
|
||||||
|
# Shot line
|
||||||
|
ax.axvline(x=0, color='red', linestyle='--', alpha=0.7, label='Shot')
|
||||||
|
|
||||||
|
# Threshold
|
||||||
|
ax.axhline(y=result['pos_threshold'], color='orange', linestyle=':', alpha=0.5, label='Threshold')
|
||||||
|
|
||||||
|
# Contamination zone
|
||||||
|
if result['pos_contamination_ms'] > 0:
|
||||||
|
ax.axvspan(0, result['pos_contamination_ms'], alpha=0.15, color='red')
|
||||||
|
|
||||||
|
stable_str = "STABLE" if result['is_stable'] else "MOVING"
|
||||||
|
ax.set_title(f"Shot {idx+1} ({stable_str}) - {result['max_contamination_ms']:.0f}ms",
|
||||||
|
fontsize=9, color='green' if result['is_stable'] else 'orange')
|
||||||
|
ax.set_xlabel('Time from shot (ms)', fontsize=8)
|
||||||
|
ax.set_ylabel('Pos Speed (cm/s)', fontsize=8)
|
||||||
|
ax.tick_params(labelsize=7)
|
||||||
|
if idx == 0:
|
||||||
|
ax.legend(fontsize=6)
|
||||||
|
|
||||||
|
# Hide unused subplots
|
||||||
|
for idx in range(n_shots, rows * cols):
|
||||||
|
r, c = divmod(idx, cols)
|
||||||
|
axes[r][c].set_visible(False)
|
||||||
|
|
||||||
|
plt.tight_layout()
|
||||||
|
plot_path = args.csv_file.replace('.csv', '_shots.png')
|
||||||
|
plt.savefig(plot_path, dpi=150)
|
||||||
|
print(f"\nPlot saved: {plot_path}")
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
# Also plot aim speed
|
||||||
|
fig2, axes2 = plt.subplots(rows, cols, figsize=(5 * cols, 4 * rows), squeeze=False)
|
||||||
|
fig2.suptitle(f'Per-Shot Aim Angular Speed ({os.path.basename(args.csv_file)})', fontsize=14)
|
||||||
|
|
||||||
|
for idx, result in enumerate(results):
|
||||||
|
r, c = divmod(idx, cols)
|
||||||
|
ax = axes2[r][c]
|
||||||
|
|
||||||
|
if result['before']:
|
||||||
|
t_before = [b[0] for b in result['before']]
|
||||||
|
a_before = [b[2] for b in result['before']] # aim_speed
|
||||||
|
ax.plot(t_before, a_before, 'b-', linewidth=1)
|
||||||
|
|
||||||
|
if result['after']:
|
||||||
|
t_after = [a[0] for a in result['after']]
|
||||||
|
a_after = [a[2] for a in result['after']] # aim_speed
|
||||||
|
ax.plot(t_after, a_after, 'r-', linewidth=1)
|
||||||
|
|
||||||
|
ax.axvline(x=0, color='red', linestyle='--', alpha=0.7)
|
||||||
|
ax.axhline(y=result['aim_threshold'], color='orange', linestyle=':', alpha=0.5)
|
||||||
|
|
||||||
|
if result['aim_contamination_ms'] > 0:
|
||||||
|
ax.axvspan(0, result['aim_contamination_ms'], alpha=0.15, color='red')
|
||||||
|
|
||||||
|
stable_str = "STABLE" if result['is_stable'] else "MOVING"
|
||||||
|
ax.set_title(f"Shot {idx+1} ({stable_str}) - Aim {result['aim_contamination_ms']:.0f}ms",
|
||||||
|
fontsize=9, color='green' if result['is_stable'] else 'orange')
|
||||||
|
ax.set_xlabel('Time from shot (ms)', fontsize=8)
|
||||||
|
ax.set_ylabel('Aim Speed (deg/s)', fontsize=8)
|
||||||
|
ax.tick_params(labelsize=7)
|
||||||
|
|
||||||
|
for idx in range(n_shots, rows * cols):
|
||||||
|
r, c = divmod(idx, cols)
|
||||||
|
axes2[r][c].set_visible(False)
|
||||||
|
|
||||||
|
plt.tight_layout()
|
||||||
|
plot_path2 = args.csv_file.replace('.csv', '_shots_aim.png')
|
||||||
|
plt.savefig(plot_path2, dpi=150)
|
||||||
|
print(f"Plot saved: {plot_path2}")
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
print("\nmatplotlib not installed. Install with: pip install matplotlib")
|
||||||
|
|
||||||
|
print("\nDone.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
|
|
||||||
|
|
||||||
[/Script/EngineSettings.GameMapsSettings]
|
[/Script/EngineSettings.GameMapsSettings]
|
||||||
GameDefaultMap=/Engine/Maps/Templates/OpenWorld
|
GameDefaultMap=/Game/PACKS/Blueprints/EasyBallistics/world/ExampleMap.ExampleMap
|
||||||
|
EditorStartupMap=/Game/PACKS/Blueprints/EasyBallistics/world/ExampleMap.ExampleMap
|
||||||
|
|
||||||
[/Script/Engine.RendererSettings]
|
[/Script/Engine.RendererSettings]
|
||||||
r.AllowStaticLighting=False
|
r.AllowStaticLighting=False
|
||||||
|
|||||||
Binary file not shown.
BIN
Unreal/Content/FPWeapon/Materials/BaseMaterial.uasset
Normal file
BIN
Unreal/Content/FPWeapon/Materials/BaseMaterial.uasset
Normal file
Binary file not shown.
Binary file not shown.
BIN
Unreal/Content/FPWeapon/Materials/M_FPGun.uasset
Normal file
BIN
Unreal/Content/FPWeapon/Materials/M_FPGun.uasset
Normal file
Binary file not shown.
BIN
Unreal/Content/FPWeapon/Materials/M_FPPistol.uasset
Normal file
BIN
Unreal/Content/FPWeapon/Materials/M_FPPistol.uasset
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Unreal/Content/FPWeapon/Mesh/FirstPersonProjectileMesh.uasset
Normal file
BIN
Unreal/Content/FPWeapon/Mesh/FirstPersonProjectileMesh.uasset
Normal file
Binary file not shown.
BIN
Unreal/Content/FPWeapon/Mesh/SK_FPGun.uasset
Normal file
BIN
Unreal/Content/FPWeapon/Mesh/SK_FPGun.uasset
Normal file
Binary file not shown.
BIN
Unreal/Content/FPWeapon/Mesh/SK_FPGun_Physics.uasset
Normal file
BIN
Unreal/Content/FPWeapon/Mesh/SK_FPGun_Physics.uasset
Normal file
Binary file not shown.
BIN
Unreal/Content/FPWeapon/Mesh/SK_FPGun_PhysicsAsset.uasset
Normal file
BIN
Unreal/Content/FPWeapon/Mesh/SK_FPGun_PhysicsAsset.uasset
Normal file
Binary file not shown.
BIN
Unreal/Content/FPWeapon/Mesh/SK_FPGun_Skeleton.uasset
Normal file
BIN
Unreal/Content/FPWeapon/Mesh/SK_FPGun_Skeleton.uasset
Normal file
Binary file not shown.
BIN
Unreal/Content/FPWeapon/Mesh/SM_Pistol.uasset
Normal file
BIN
Unreal/Content/FPWeapon/Mesh/SM_Pistol.uasset
Normal file
Binary file not shown.
BIN
Unreal/Content/FPWeapon/Textures/T_FPGun_M.uasset
Normal file
BIN
Unreal/Content/FPWeapon/Textures/T_FPGun_M.uasset
Normal file
Binary file not shown.
BIN
Unreal/Content/FPWeapon/Textures/T_FPGun_N.uasset
Normal file
BIN
Unreal/Content/FPWeapon/Textures/T_FPGun_N.uasset
Normal file
Binary file not shown.
BIN
Unreal/Content/FPWeapon/Textures/T_Pistol_Masks.uasset
Normal file
BIN
Unreal/Content/FPWeapon/Textures/T_Pistol_Masks.uasset
Normal file
Binary file not shown.
BIN
Unreal/Content/FPWeapon/Textures/T_Pistol_N.uasset
Normal file
BIN
Unreal/Content/FPWeapon/Textures/T_Pistol_N.uasset
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Unreal/Content/FirstPerson/Blueprints/BP_Pickup_Rifle.uasset
Normal file
BIN
Unreal/Content/FirstPerson/Blueprints/BP_Pickup_Rifle.uasset
Normal file
Binary file not shown.
BIN
Unreal/Content/FirstPerson/Blueprints/BP_Weapon_Component.uasset
Normal file
BIN
Unreal/Content/FirstPerson/Blueprints/BP_Weapon_Component.uasset
Normal file
Binary file not shown.
BIN
Unreal/Content/FirstPerson/Input/Actions/IA_Jump.uasset
Normal file
BIN
Unreal/Content/FirstPerson/Input/Actions/IA_Jump.uasset
Normal file
Binary file not shown.
BIN
Unreal/Content/FirstPerson/Input/Actions/IA_Look.uasset
Normal file
BIN
Unreal/Content/FirstPerson/Input/Actions/IA_Look.uasset
Normal file
Binary file not shown.
BIN
Unreal/Content/FirstPerson/Input/Actions/IA_Move.uasset
Normal file
BIN
Unreal/Content/FirstPerson/Input/Actions/IA_Move.uasset
Normal file
Binary file not shown.
BIN
Unreal/Content/FirstPerson/Input/Actions/IA_Shoot.uasset
Normal file
BIN
Unreal/Content/FirstPerson/Input/Actions/IA_Shoot.uasset
Normal file
Binary file not shown.
BIN
Unreal/Content/FirstPerson/Input/IMC_Default.uasset
Normal file
BIN
Unreal/Content/FirstPerson/Input/IMC_Default.uasset
Normal file
Binary file not shown.
BIN
Unreal/Content/FirstPerson/Input/IMC_Weapons.uasset
Normal file
BIN
Unreal/Content/FirstPerson/Input/IMC_Weapons.uasset
Normal file
Binary file not shown.
BIN
Unreal/Content/FirstPerson/Input/MobileControls.uasset
Normal file
BIN
Unreal/Content/FirstPerson/Input/MobileControls.uasset
Normal file
Binary file not shown.
BIN
Unreal/Content/FirstPerson/Maps/FirstPersonMap.umap
Normal file
BIN
Unreal/Content/FirstPerson/Maps/FirstPersonMap.umap
Normal file
Binary file not shown.
BIN
Unreal/Content/FirstPerson/Maps/FirstPersonMap_BuiltData.uasset
Normal file
BIN
Unreal/Content/FirstPerson/Maps/FirstPersonMap_BuiltData.uasset
Normal file
Binary file not shown.
BIN
Unreal/Content/FirstPersonArms/Animations/FP_Falling.uasset
Normal file
BIN
Unreal/Content/FirstPersonArms/Animations/FP_Falling.uasset
Normal file
Binary file not shown.
BIN
Unreal/Content/FirstPersonArms/Animations/FP_Idle.uasset
Normal file
BIN
Unreal/Content/FirstPersonArms/Animations/FP_Idle.uasset
Normal file
Binary file not shown.
BIN
Unreal/Content/FirstPersonArms/Animations/FP_Jump.uasset
Normal file
BIN
Unreal/Content/FirstPersonArms/Animations/FP_Jump.uasset
Normal file
Binary file not shown.
BIN
Unreal/Content/FirstPersonArms/Animations/FP_Land.uasset
Normal file
BIN
Unreal/Content/FirstPersonArms/Animations/FP_Land.uasset
Normal file
Binary file not shown.
Binary file not shown.
BIN
Unreal/Content/FirstPersonArms/Animations/FP_Rifle_Fire.uasset
Normal file
BIN
Unreal/Content/FirstPersonArms/Animations/FP_Rifle_Fire.uasset
Normal file
Binary file not shown.
BIN
Unreal/Content/FirstPersonArms/Animations/FP_Rifle_Idle.uasset
Normal file
BIN
Unreal/Content/FirstPersonArms/Animations/FP_Rifle_Idle.uasset
Normal file
Binary file not shown.
BIN
Unreal/Content/FirstPersonArms/Animations/FP_Rifle_Jump.uasset
Normal file
BIN
Unreal/Content/FirstPersonArms/Animations/FP_Rifle_Jump.uasset
Normal file
Binary file not shown.
BIN
Unreal/Content/FirstPersonArms/Animations/FP_Rifle_Land.uasset
Normal file
BIN
Unreal/Content/FirstPersonArms/Animations/FP_Rifle_Land.uasset
Normal file
Binary file not shown.
BIN
Unreal/Content/FirstPersonArms/Animations/FP_Rifle_Run.uasset
Normal file
BIN
Unreal/Content/FirstPersonArms/Animations/FP_Rifle_Run.uasset
Normal file
Binary file not shown.
Binary file not shown.
BIN
Unreal/Content/FirstPersonArms/Animations/FP_Run_Fwd.uasset
Normal file
BIN
Unreal/Content/FirstPersonArms/Animations/FP_Run_Fwd.uasset
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Unreal/Content/LevelPrototyping/Materials/MF_ProcGrid.uasset
Normal file
BIN
Unreal/Content/LevelPrototyping/Materials/MF_ProcGrid.uasset
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Unreal/Content/LevelPrototyping/Materials/MI_Solid_Blue.uasset
Normal file
BIN
Unreal/Content/LevelPrototyping/Materials/MI_Solid_Blue.uasset
Normal file
Binary file not shown.
BIN
Unreal/Content/LevelPrototyping/Materials/M_PrototypeGrid.uasset
Normal file
BIN
Unreal/Content/LevelPrototyping/Materials/M_PrototypeGrid.uasset
Normal file
Binary file not shown.
BIN
Unreal/Content/LevelPrototyping/Materials/M_Solid.uasset
Normal file
BIN
Unreal/Content/LevelPrototyping/Materials/M_Solid.uasset
Normal file
Binary file not shown.
BIN
Unreal/Content/LevelPrototyping/Meshes/SM_ChamferCube.uasset
Normal file
BIN
Unreal/Content/LevelPrototyping/Meshes/SM_ChamferCube.uasset
Normal file
Binary file not shown.
BIN
Unreal/Content/LevelPrototyping/Meshes/SM_Cube.uasset
Normal file
BIN
Unreal/Content/LevelPrototyping/Meshes/SM_Cube.uasset
Normal file
Binary file not shown.
BIN
Unreal/Content/LevelPrototyping/Meshes/SM_Cylinder.uasset
Normal file
BIN
Unreal/Content/LevelPrototyping/Meshes/SM_Cylinder.uasset
Normal file
Binary file not shown.
BIN
Unreal/Content/LevelPrototyping/Meshes/SM_QuarterCylinder.uasset
Normal file
BIN
Unreal/Content/LevelPrototyping/Meshes/SM_QuarterCylinder.uasset
Normal file
Binary file not shown.
BIN
Unreal/Content/LevelPrototyping/Meshes/SM_Ramp.uasset
Normal file
BIN
Unreal/Content/LevelPrototyping/Meshes/SM_Ramp.uasset
Normal file
Binary file not shown.
BIN
Unreal/Content/LevelPrototyping/Textures/T_GridChecker_A.uasset
Normal file
BIN
Unreal/Content/LevelPrototyping/Textures/T_GridChecker_A.uasset
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user