Gyro: BT_GYRO_TRACE per-frame integrator trace -- oscillation verified vs authored constants (task #56)

25-hit MP autofire trace: kick -> damped oscillation -> settle, peaks well
inside clamps, no NaN/drift; measured Y:X frequency ratio 7.75x matches
sqrt(springK.y/springK.x) exactly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-11 09:56:55 -05:00
co-authored by Claude Fable 5
parent 8af278a3f5
commit 1ace2e3cb4
4 changed files with 97 additions and 0 deletions
+6
View File
@@ -118,6 +118,12 @@ inverse(eyeWorld)`. No LookAt anywhere.
(projweap.cpp FireWeapon: damage>3 → impulse (0,0.6,1.5) × damage/16, @4bc136-4bc19c). (projweap.cpp FireWeapon: damage>3 → impulse (0,0.6,1.5) × damage/16, @4bc136-4bc19c).
Verified live: `[gyro-dmg]` fires per hit with correct scaling, eyePosition shows the damped Verified live: `[gyro-dmg]` fires per hit with correct scaling, eyePosition shows the damped
oscillation (ramp to ~0.015u, Y oscillating, decay to 0), no NaN, kill chain un-regressed. oscillation (ramp to ~0.015u, Y oscillating, decay to 0), no NaN, kill chain un-regressed.
**Quantitatively verified (2026-07-11 MP trace):** `BT_GYRO_TRACE=1` logs the integrator state
every displaced frame (`[gtrace]`); a 25-hit autofire session (scratchpad/GYRO_TRACE.png)
shows per-hit kick → damped oscillation → settle-to-zero-and-stay, peaks ±0.028u X/Z ±0.005u Y
(well inside the ±0.1/0.15 clamps), body pitch/roll ±0.02 rad, zero NaN/drift — and the
measured frequency ratio matches the authored constants: Y period 9.5 frames vs X/Z ~75 ≈
√(springK.y/springK.x)=√60≈7.75× as dictated by springK=(0.2,12,0.2). [T2]
- **Deferred gyro tail:** (a) the alternate-gait engage JOLT + 0.4s engaged-gait RUMBLE - **Deferred gyro tail:** (a) the alternate-gait engage JOLT + 0.4s engaged-gait RUMBLE
(@4aa158-4aa365 — full byte recipe in the wf_6880e605 synthesis; **flagged [T3] on the gate (@4aa158-4aa365 — full byte recipe in the wf_6880e605 synthesis; **flagged [T3] on the gate
naming** and it mutates the gait state machine tasks #49/#50 stabilized — do NOT wire without naming** and it mutates the gait state machine tasks #49/#50 stabilized — do NOT wire without
+27
View File
@@ -643,6 +643,27 @@ void
DEBUG_STREAM << "[gyro-bounce] eyePos=(" << (float)eyePosition.x << "," DEBUG_STREAM << "[gyro-bounce] eyePos=(" << (float)eyePosition.x << ","
<< (float)eyePosition.y << "," << (float)eyePosition.z << ")\n" << std::flush; << (float)eyePosition.y << "," << (float)eyePosition.z << ")\n" << std::flush;
} }
// BOUNCE TRACE (task #56 verification): BT_GYRO_TRACE=1 logs this gyro's
// integrated state EVERY frame while displaced, uncapped, tagged with the
// instance + a running frame index -- enough to PLOT the oscillation and
// check the damped-spring behaviour against the authored constants.
static const int s_gtrace = getenv("BT_GYRO_TRACE") ? 1 : 0;
if (s_gtrace)
{
static int s_traceFrame = 0;
++s_traceFrame;
const float m2 = eyePosition.x*eyePosition.x + eyePosition.y*eyePosition.y
+ eyePosition.z*eyePosition.z
+ bodyOrientation.x*bodyOrientation.x
+ bodyOrientation.y*bodyOrientation.y
+ bodyOrientation.z*bodyOrientation.z;
if (m2 > 1e-9f)
DEBUG_STREAM << "[gtrace] g=" << (void *)this << " f=" << s_traceFrame
<< " eye=" << (float)eyePosition.x << " " << (float)eyePosition.y
<< " " << (float)eyePosition.z
<< " body=" << (float)bodyOrientation.x << " " << (float)bodyOrientation.y
<< " " << (float)bodyOrientation.z << "\n" << std::flush;
}
if (!NodeRotationEquals(mechJointNode, eyePosition, QuantiseEps)) // FUN_004084fc vs value+0 if (!NodeRotationEquals(mechJointNode, eyePosition, QuantiseEps)) // FUN_004084fc vs value+0
{ {
SetNodeRotation(mechJointNode, eyePosition); // FUN_0041d11c = SetTRANSLATION SetNodeRotation(mechJointNode, eyePosition); // FUN_0041d11c = SetTRANSLATION
@@ -844,6 +865,12 @@ void
<< ") t/p/y/v=" << (float)trans << "/" << (float)pitchRoll << "/" << ") t/p/y/v=" << (float)trans << "/" << (float)pitchRoll << "/"
<< (float)yaw << "/" << (float)vibration << "\n" << std::flush; << (float)yaw << "/" << (float)vibration << "\n" << std::flush;
if (getenv("BT_GYRO_TRACE"))
DEBUG_STREAM << "[gtrace] g=" << (void *)this << " HIT t=" << (float)trans
<< " p=" << (float)pitchRoll << " y=" << (float)yaw << " v=" << (float)vibration
<< " dir=" << (float)dir.x << " " << (float)dir.y << " " << (float)dir.z
<< "\n" << std::flush;
ApplyDamageImpulse (dir.x, dir.y, dir.z, trans); // @4b2d00 the directional knock ApplyDamageImpulse (dir.x, dir.y, dir.z, trans); // @4b2d00 the directional knock
ApplyDamageTorque (dir.x, dir.y, dir.z, pitchRoll); // @4b2d23 ApplyDamageTorque (dir.x, dir.y, dir.z, pitchRoll); // @4b2d23
ApplyDamageImpulse (vibrationDirection.x, vibrationDirection.y, ApplyDamageImpulse (vibrationDirection.x, vibrationDirection.y,
+56
View File
@@ -0,0 +1,56 @@
import re, sys
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
frames, eyes, bodies = [], [], []
hits = [] # (frame_at_hit, t, p, y, v)
last_f = None
for line in open("../content/gtrace_a.log", encoding="utf-8", errors="replace"):
m = re.match(r"\[gtrace\] g=\S+ f=(\d+) eye=(\S+) (\S+) (\S+) body=(\S+) (\S+) (\S+)", line)
if m:
last_f = int(m.group(1))
frames.append(last_f)
eyes.append(tuple(float(m.group(i)) for i in (2,3,4)))
bodies.append(tuple(float(m.group(i)) for i in (5,6,7)))
continue
m = re.match(r"\[gtrace\] g=\S+ HIT t=(\S+) p=(\S+) y=(\S+) v=(\S+)", line)
if m:
hits.append((last_f, *[float(m.group(i)) for i in (1,2,3,4)]))
print(f"trace frames: {len(frames)}, hits: {len(hits)}")
print(f"frame range: {frames[0]}..{frames[-1]}")
# stats per volley: peak |eye|, settle
import math
mag = [math.sqrt(e[0]**2+e[1]**2+e[2]**2) for e in eyes]
peak = max(mag); print(f"peak |eyePos| = {peak:.4f} u (clamp legal range +-0.1/0.15 per axis)")
peakY = max(abs(e[1]) for e in eyes); peakX = max(abs(e[0]) for e in eyes); peakZ = max(abs(e[2]) for e in eyes)
print(f"peak per-axis |x|={peakX:.4f} |y|={peakY:.4f} |z|={peakZ:.4f}")
bad = [e for e in eyes if any(x != x for x in e)]
print(f"NaN frames: {len(bad)}")
# oscillation period of Y (zero crossings) in the first big volley
ys = [e[1] for e in eyes]
cross = [frames[i] for i in range(1,len(ys)) if ys[i-1]*ys[i] < 0 and frames[i]-frames[i-1] == 1]
if len(cross) > 3:
gaps = [b-a for a,b in zip(cross, cross[1:]) if b-a < 30]
if gaps: print(f"Y half-period ~{sum(gaps)/len(gaps):.1f} frames (full period ~{2*sum(gaps)/len(gaps):.1f})")
# settle check: gap in frames = eye at rest (below 1e-9 magnitude threshold not logged)
gapct = sum(1 for a,b in zip(frames, frames[1:]) if b-a > 5)
print(f"rest gaps (>5 frames unlogged = settled to ~0): {gapct}")
fig, ax = plt.subplots(2, 1, figsize=(16, 8), sharex=True)
ax[0].plot(frames, [e[0] for e in eyes], '.', ms=2, label='eye X (lateral)')
ax[0].plot(frames, [e[1] for e in eyes], '.', ms=2, label='eye Y (vert)')
ax[0].plot(frames, [e[2] for e in eyes], '.', ms=2, label='eye Z (fore/aft)')
for h in [h for h in hits if h[0] is not None]: ax[0].axvline(h[0], color='r', alpha=.25, lw=.8)
ax[0].legend(); ax[0].set_ylabel('eyePosition (u)'); ax[0].set_title('Gyro eye spring — red lines = hits received')
ax[1].plot(frames, [b[0] for b in bodies], '.', ms=2, label='body X (pitch)')
ax[1].plot(frames, [b[1] for b in bodies], '.', ms=2, label='body Y')
ax[1].plot(frames, [b[2] for b in bodies], '.', ms=2, label='body Z (roll)')
for h in [h for h in hits if h[0] is not None]: ax[1].axvline(h[0], color='r', alpha=.25, lw=.8)
ax[1].legend(); ax[1].set_ylabel('bodyOrientation (rad)'); ax[1].set_xlabel('frame')
plt.tight_layout(); plt.savefig("GYRO_TRACE.png", dpi=90)
print("wrote GYRO_TRACE.png")
+8
View File
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
cd /c/git/bt411/content || exit 1
rm -f gtrace_a.log gtrace_b.log
BT_LOG=gtrace_a.log BT_AFFINITY=0x1 BT_GYRO_LOG=1 BT_GYRO_TRACE=1 BT_START_INSIDE=1 ../build/Debug/btl4.exe -egg MP.EGG -net 1501 &
sleep 2
BT_LOG=gtrace_b.log BT_AFFINITY=0x2 BT_AUTOFIRE=1 ../build/Debug/btl4.exe -egg MP.EGG -net 1601 &
sleep 8
exec python ../tools/btconsole.py MP.EGG 127.0.0.1:1501 127.0.0.1:1601