Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 0 additions & 63 deletions .claude/commands/pytest.md

This file was deleted.

44 changes: 44 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ When to still use grep/`grep_search`: matching exact substrings/regex in prose,
uniformly (e.g. searching for a TODO string or a parameter name that may appear in
comments).

Searching OUTSIDE this workspace (e.g. a separate `C:\repos\ardupilot` checkout):
the `grep_search`/`file_search`/`semantic_search` tools are scoped to this workspace
folder and silently return "No matches found" (a generic VS Code search-exclusion
message) for paths outside it — this does NOT mean the pattern is genuinely absent,
it means the tool couldn't search there at all. For any path outside the current
workspace folder, go straight to a terminal command (`grep`/`sed`/`rg` via
`run_in_terminal`) instead of retrying the workspace-scoped search tools.

## Documentation Ownership (Single Source of Truth)

Use the primary doc for each topic. Other docs should link, not restate.
Expand All @@ -78,6 +86,7 @@ Use the primary doc for each topic. Other docs should link, not restate.
| Flight architecture, mode ownership, AP/Lua boundaries | `design/flight_stack.md` | `design/tension_collective_control_loop.md`, `design/GUIDED_CONTROL_LOOPS.md` |
| Simulation internals (physics, sensors, controller plumbing, module map) | `design/simulation.md` | `simulation/README.md`, code docstrings |
| SITL stack workflow, lockstep, diagnosis procedure | `design/sitl_testing.md` | `simulation/analysis/diagnose_sitl.py` usage text |
| SITL IC-start timeline and event anchors | `design/sitl_flight_timeline.md` | `design/sitl_testing.md`, `simulation/tests/sitl/flight/conftest.py` |
| Aero interfaces and conventions | `design/aero_conventions.md` | `design/aero.md` |
| EKF gating and GPS yaw bring-up | `design/EKF_GATING.md` | `design/ekf_const_pos_mode.md` |
| ArduPilot heli control-loop behavior | `design/GUIDED_CONTROL_LOOPS.md` | `design/flight_stack.md` |
Expand Down Expand Up @@ -185,6 +194,10 @@ SITL behavior:

## Workflow Rules

- Do not use `wsl` to run anything directly. `test.sh` is the only script that uses WSL, and it
already contains the logic to re-invoke itself inside WSL when needed (for Docker access).
Run `bash test.sh ...` (or `./test.sh ...`) from Git Bash directly — do not wrap it in
`wsl -e bash -lc "..."` or run other commands (pytest, analysis scripts, git, etc.) via `wsl`.
- Do not use git history (`git log`, `git show`, `git blame`) for diagnosis unless user asks.
- Do not preserve backward-compatibility parameters, fields, aliases, or shims when making code changes.
- Assume no external callers: prefer a clean cutover and remove legacy paths in the same change to avoid debt.
Expand All @@ -208,6 +221,23 @@ SITL behavior:
exist only to work around circular imports are a sign of bad architecture — fix the circular
dependency by refactoring (e.g. extract a shared module, invert the dependency) rather than
papering over it with a local import.
- `/tmp` is NOT one shared filesystem on this box — Git Bash (mingw/MSYS2, the default terminal)
and WSL2 (used for `docker`/`test.sh stack`) each have their own separate `/tmp`:
- A bare `> /tmp/foo.log` redirection in a Git Bash command writes to Git Bash's own `/tmp`
(really `C:\Users\<user>\AppData\Local\Temp\foo.log` — check with `cygpath -w /tmp/foo.log`).
- A command run via `wsl -e bash -lc "... > /tmp/foo.log"` writes inside WSL's `/tmp`
(`\\wsl$\...\tmp\foo.log` from Windows, or `/tmp/foo.log` from *inside* another `wsl -e` call)
— NOT reachable from a later plain Git Bash `cat /tmp/foo.log`.
- If the redirection is written OUTSIDE the `wsl -e bash -lc "..."` quoted string (e.g.
`wsl -e bash -lc "cmd" > /tmp/foo.log`), it's the OUTER (Git Bash) shell that owns the
redirect, not WSL — easy to mix up.
- A native Windows executable (e.g. `.venv/Scripts/python.exe`) invoked from Git Bash does NOT
understand `/tmp/...` paths passed as arguments (it's a Windows process, not MSYS2-aware) —
convert with `cygpath -w /tmp/foo.log` first, or it'll fail with `FileNotFoundError` even
though `ls /tmp/foo.log` (from Git Bash) shows the file existing.
- Rule of thumb: know which shell environment (Git Bash vs WSL) is actually creating/reading
a `/tmp` path before assuming a file exists or is missing; don't conclude "no output was
produced" just because a naive `cat`/`find` from the wrong shell doesn't see it.

## Test Entry Points

Expand All @@ -219,6 +249,12 @@ There are three tiers, each with a different scope and runtime:
| Simtest | `.venv/Scripts/python.exe -m pytest simulation/tests/simtests` | `simtest` | Python physics loop; seconds–minutes |
| Stack | `bash test.sh stack [-n N]` | `sitl` | ArduPilot SITL in Docker |

SITL IC-start timeline rule (agent-critical):
- For SITL flight diagnosis, use one shared timeline anchored at the IC-start flow.
- Treat `t_sim` with the `kinematic_exit` event as the canonical phase boundary for
release-to-flight comparisons across steady/passive/pumping/landing stack tests.
- Canonical definition and per-phase markers live in `design/sitl_flight_timeline.md`.

Stack-test execution rule (agent-critical):
- For any test under `simulation/tests/sitl/**`, ALWAYS use `bash test.sh stack -n 4 ...`.
- DO NOT run SITL tests with host-side pytest commands like
Expand Down Expand Up @@ -249,6 +285,14 @@ Long-running test commands (agent-critical, efficiency):
- Once a command has been moved to background, do not repeatedly call `get_terminal_output`
in a tight loop — it will not return new content until the process actually produces more
output or exits. Wait for the automatic completion notification instead of polling.
- Do NOT call `get_terminal_output` immediately after a command moves to background "just to
check progress". It returns a byte-limited tail of the WHOLE terminal scrollback, not just
the new command's output — if the new command has only printed a little so far, the tail
can still be dominated by leftover output from earlier unrelated commands in the same
terminal, which looks like stale/wrong output but is really just "not enough new output yet
to push the old stuff out of the tail window". End the turn and wait for the automatic
completion notification instead; only poll if genuinely unsure whether the process is hung
after a long silence.
- Prefer `bash test.sh stack -n 1 -k <test_name>` (single test) while iterating; only widen
to `-n 4`/full suite once the targeted test is confirmed passing, to keep turnaround short.

Expand Down
24 changes: 14 additions & 10 deletions design/flight_stack.md
Original file line number Diff line number Diff line change
Expand Up @@ -620,8 +620,12 @@ jumps (the old zero-inertia algebraic model did, which drove a yaw limit cycle).

**Yaw control — servo-readback trim observer (rawes.lua):**

ArduPilot's yaw rate loop is kept **small but nonzero** (P=0.015, I=0.0015, D=0).
rawes.lua still runs a
ArduPilot's yaw rate loop uses gains sized to ArduCopter-Heli's stock default
(P=0.18, I=0.018, D=0) so it has enough authority to keep heading error under
the hardcoded 45 deg heading-error-max ceiling (`AC_AttitudeControl::thrust_heading_rotation_angles`)
instead of falling into a "target follows spin" mode where the attitude target
gets rewritten onto the current (spinning) body every cycle and the rate loop
sees near-zero error while the vehicle keeps rotating. rawes.lua still runs a
model-based trim observer (`run_yaw_trim`) that writes `H_YAW_TRIM` every 10 ms tick:

```
Expand All @@ -633,19 +637,19 @@ param:set("H_YAW_TRIM", trim)

This drives `H_YAW_TRIM` toward the equilibrium throttle `u_eq = omega_rotor × GEAR_RATIO / RPM_SCALE`
(see `torque_model.py` for constants) at which `psi_dot = 0`. `YFF_A = RAWES_YAW_SLP × SERVO9_SPAN_US × 2π/60` (default ≈ 52.8 rad/s per
throttle unit; RAWES_YAW_SLP=0 uses bench value 0.504 RPM/µs). The AP yaw P-term handles
fast transients; the tiny I-term cleans up residual drift; the observer carries the
bulk DC trim so the AP integrator can stay small.
throttle unit; RAWES_YAW_SLP=0 uses bench value 0.504 RPM/µs). The AP yaw P/I-term handles
fast transients and residual drift; the observer carries the
bulk DC trim so AP's rate loop mainly acts as a fast disturbance-rejection assist.

### 5.3 Key Parameters

| Parameter | Value | Purpose |
|---|---|---|
| H_TAIL_TYPE | 3 (DDFP CW) | No sign flip: positive yaw error → positive motor throttle |
| ATC_RAT_YAW_P | 0.015 | Small AP yaw P-term for fast disturbance rejection |
| ATC_RAT_YAW_I | 0.0015 | Tiny I-term for residual drift cleanup |
| ATC_RAT_YAW_P | 0.18 | AP yaw P-term, sized to ArduCopter-Heli's stock default so the rate loop has enough authority to keep heading error under the 45 deg heading-error-max ceiling (see AC_AttitudeControl::thrust_heading_rotation_angles) |
| ATC_RAT_YAW_I | 0.018 | I-term for residual drift cleanup, scaled with P |
| ATC_RAT_YAW_D | 0.0 | Off |
| ATC_RAT_YAW_IMAX | 0.1 | Clamp (safety; integrator is zero) |
| ATC_RAT_YAW_IMAX | 0.1 | Clamp (safety) |
| RAWES_YAW_SLP | 0 | Yaw motor slope override [RPM/µs]; 0 = bench default 0.504 |

---
Expand Down Expand Up @@ -732,8 +736,8 @@ Current hardware: GB4008 + 10:1 spur gear. See §5.2 and [components.md](compone
| SERVO9_FUNCTION | 36 (Motor4) | Anti-rotation motor ESC on output 9 (AUX 1) |
| SERVO9_MIN | 1000 µs | ESC disarm |
| SERVO9_MAX | 2000 µs | ESC maximum |
| ATC_RAT_YAW_P | 0.015 | Small AP yaw P-term for fast disturbance rejection |
| ATC_RAT_YAW_I | 0.0015 | Tiny I-term for residual drift cleanup |
| ATC_RAT_YAW_P | 0.18 | AP yaw P-term, sized to ArduCopter-Heli's stock default so the rate loop has enough authority to keep heading error under the 45 deg heading-error-max ceiling |
| ATC_RAT_YAW_I | 0.018 | I-term for residual drift cleanup, scaled with P |
| ATC_RAT_YAW_D | 0.0 | Start at zero |

---
Expand Down
97 changes: 97 additions & 0 deletions design/sitl_flight_timeline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# SITL Flight Timeline (IC-Start Canonical)

This document defines the canonical timeline used for SITL flight analysis when
stacks start from the shared IC flow.

Scope:
- SITL stack flight tests under `simulation/tests/sitl/flight/`.
- Steady, passive, pumping, and landing variants that use IC-start fixtures.

Out of scope:
- Windows-native unit/simtests.
- Non-flight SITL infrastructure checks.

## Why this exists

All SITL flight comparisons must use the same event anchors. Without one
canonical timeline, we can mis-attribute divergence to controller behavior when
it is actually a phase-alignment artifact.

## Canonical time anchors

Use these anchors in this order:

1. Absolute simulation time: `t_sim` from telemetry CSV.
2. Kinematic release boundary: row/event where `note == "kinematic_exit"`.
3. Relative flight time: `t_rel = t_sim - t_kin_exit`.

Rules:
- Use `t_rel` for any release-to-flight comparison (simtest vs SITL, run-vs-run,
first-divergence windows, reaction windows).
- Do not use wall clock, container uptime, or MAVLink receive order as primary
comparison axes.
- If `kinematic_exit` is missing, treat the run as invalid for flight-phase
root-cause analysis until telemetry/event logging is fixed.

## Standard phase timeline (IC-start stacks)

This is the expected sequence for IC-start SITL flight stacks.

1. `t_sim = 0`: mediator start; kinematic startup begins.
2. Kinematic hold window: EKF alignment and IC approach while physics hand-off is
gated.
3. `note == "kinematic_exit"`: one-shot release marker; this is `t_rel = 0`.
4. Post-release guided flight: controller/physics behavior under test.

Notes:
- Exact hold duration is fixture-configured and may vary by test.
- Phase duration differences do not change the anchor rule: always compare using
`t_rel` based on `kinematic_exit`.

## Pre-release event matrix (major columns)

Use this table format for steady SITL timeline analysis up to kinematic exit.
Each row is a milestone, and each major subsystem has its own column.

| Time marker | Kinematic / trajectory | GPS | EKF | Vehicle mode / arm | RAWES / Lua | Analysis note |
|---|---|---|---|---|---|---|
| `t_sim = 0`, `t_rel < 0` | Kinematic startup begins | No guaranteed fix yet | Startup alignment state | Disarmed | `RAWES_MODE=0` expected | Run initialization |
| Early hold (`t_rel << 0`) | Trapezoid accel/cruise/decel progressing | `GPS_RAW_INT` transitions 0 -> 1 -> 6 | `EKF_STATUS` transitions toward aiding-ready flags | GUIDED_NOGPS setup starts, then arm pending | Passive pipeline preparing IC seed | Confirm bring-up order |
| Mid hold (`t_rel < 0`) | Kinematic still active | Fix 6 expected to be stable | Origin set, then GPS aiding active | Armed, GUIDED_NOGPS stable | `RAWES_MODE=3` (passive), IC ready status | Verify pre-release stability |
| Late hold (`t_rel -> 0-`) | Near release target state (at IC pose/vel) | Fix remains stable | Aiding-ready flags stable | Armed/mode unchanged | Passive hold commands continue | Ensure no last-second state jumps |
| `note == kinematic_exit`, `t_rel = 0` | Kinematic handoff ends | Should still be locked | Should still be aiding | Armed/mode continuous across handoff | Steady logic eligible to take control | Start post-release comparisons |

Minimum fields to populate per row:
- Time: `t_sim`, `t_rel`.
- GPS: `fix_type`, `satellites_visible`.
- EKF: `EKF_STATUS_REPORT.flags` and key STATUSTEXT milestones.
- Mode/arm: HEARTBEAT `custom_mode` and armed bit.
- RAWES/Lua: mode transitions and passive/steady status text.

## Required analysis convention

For SITL flight diagnostics, report timing in both forms:
- `t_sim` for raw traceability.
- `t_rel` for behavioral comparison.

When reporting first divergence, include:
- first-divergence `t_rel` bucket,
- pre-window and post-window definitions in `t_rel`,
- whether events are before or after `kinematic_exit`.

## Data sources and marker ownership

Canonical marker producers/consumers:
- Producer: `simulation/mediator.py` writes `note = "kinematic_exit"` and event log entry.
- Shared fixture path: `simulation/tests/sitl/flight/conftest.py` (`_ic_trapezoid_stack`).
- Primary diagnosis workflow: `design/sitl_testing.md` and `simulation/analysis/diagnose_sitl.py`.

## Validation checklist (after timeline-related changes)

1. Run one IC-start SITL flight test.
2. Confirm telemetry has a single `kinematic_exit` marker.
3. Confirm analysis scripts compute finite `t_rel` values post-release.
4. Confirm first-divergence output references `t_rel` (not only absolute time).

Suggested command:
- `bash test.sh stack -n 1 -k test_lua_flight_steady_sitl`
6 changes: 6 additions & 0 deletions design/sitl_testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,12 @@ running during the wait. Full reference:

## Kinematic hold timeline

Canonical timeline note:
- The repository-level canonical timeline definition for IC-start SITL flight
analysis is `design/sitl_flight_timeline.md`.
- Use this section for stack-specific execution details; use the canonical doc
for shared event anchors (`t_sim`, `kinematic_exit`, `t_rel`).

The **kinematic hold** (a.k.a. kinematic startup phase) is the artificial,
physics-free trajectory that brings the hub to the IC operating point and holds
it there while the EKF aligns on GPS. It exists for one reason: to leave the EKF
Expand Down
10 changes: 9 additions & 1 deletion simulation/analysis/analyze_ang_error.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,9 +206,15 @@ def _heading_error_max(rat_yaw_p: float, ang_yaw_p: float, acc_y_degss: float) -
# ── log loading ───────────────────────────────────────────────────────────────

def _dynamics_start_tsim(log_dir: Path) -> float | None:
"""t_sim [s] of the dynamics-start reference for t_dyn=0.

Prefers `dynamics_start` (torque-test convention); falls back to
`kinematic_exit` (flight-stack convention: t_dyn=0 at kinematic release).
"""
p = log_dir / "events.jsonl"
if not p.exists():
return None
fallback: float | None = None
for line in p.read_text(encoding="utf-8", errors="replace").splitlines():
line = line.strip()
if not line:
Expand All @@ -219,7 +225,9 @@ def _dynamics_start_tsim(log_dir: Path) -> float | None:
continue
if e.get("event") == "dynamics_start":
return float(e["t_sim"])
return None
if e.get("event") == "kinematic_exit":
fallback = float(e["t_sim"])
return fallback


def _load_dataflash(bin_path: Path, t_dyn_start_s: float) -> dict:
Expand Down
Loading
Loading