diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 15d5a54..12a1f73 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,14 +31,15 @@ jobs: run: | python -m pip install --upgrade pip python -m pip install -r simulation/requirements.txt + python -m pip install -e . --no-deps - name: Run unit tests run: | - python -m pytest simulation/tests/unit -m "not simtest" + python -m pytest tests/unit -m "not simtest" - name: Run simtests run: | - python -m pytest simulation/tests/simtests -m simtest + python -m pytest tests/simtests -m simtest stack-matrix: name: Discover stack test files @@ -54,7 +55,7 @@ jobs: id: discover shell: bash run: | - files_json=$(find simulation/tests/sitl -name 'test_*.py' | sort | jq -R -s -c 'split("\n") | map(select(length > 0))') + files_json=$(find tests/sitl -name 'test_*.py' | sort | jq -R -s -c 'split("\n") | map(select(length > 0))') echo "files=${files_json}" >> "$GITHUB_OUTPUT" stack-build-image: diff --git a/.gitignore b/.gitignore index a611061..7fd44b3 100644 --- a/.gitignore +++ b/.gitignore @@ -19,7 +19,7 @@ simulation/logs/ *.log # One-off / diagnostic scripts (kept locally for reproducibility, not checked in by default) -simulation/tests/oneoff/ +tests/oneoff/ # Generated flight report plots *.png @@ -30,6 +30,9 @@ tmp/ # Logs *.log +# Python packaging build artifacts +*.egg-info/ + # Node node_modules/ diff --git a/AGENTS.md b/AGENTS.md index f63db95..5f29cbe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,6 +12,33 @@ Current focus: - Run `bash test.sh stack -n 1 -k test_lua_flight_steady_sitl` to validate the steady flight SITL stack. - After steady stack passes, validate pumping and landing stack tests. +## Repository Layout + +The repo root is a single Python distribution (`pyproject.toml`, name `rawes`) containing +8 first-party top-level packages, installed in editable mode +(`pip install -e . --no-deps`, done by `setup.cmd`/`setup.sh`). Import them as plain dotted +packages (`from simulation.controller import ...`, `from analysis.flight_log import ...`) — +there are no `sys.path.insert()` hacks anywhere in the codebase. + +| Package | Contents | +|---|---| +| `simulation/` | Physics/aero/EKF-adjacent simulation runtime, Lua/Python flight-stack modules (`mediator.py`, `controller.py`, `param_defaults.py`, `swashplate.py`, `frames.py`, `gcs.py`, ...), `requirements.txt`, `Dockerfile`, `logs/` | +| `arduloop/` | Self-contained Python port of ArduPilot's traditional-heli attitude/rate-control stack (used by the in-process mock ArduPilot) | +| `calibrate/` | Bench calibration REPL/tooling for hardware bring-up | +| `envelope/` | Flight-envelope map computation (`compute_map.py`) and related analysis | +| `analysis/` | Post-run diagnosis/report scripts (all read `simulation/logs/{test_name}/...`) | +| `viz3d/` | 3D telemetry playback and torque visualizers | +| `scripts/` | Deployed Lua flight scripts (`rawes.lua`, `rawes_test_surface.lua`) and standalone runtime scripts (`sitl_bench.py`, `query_hardware.py`) | +| `tests/` | All test suites: `tests/unit`, `tests/simtests`, `tests/sitl` (Docker/SITL), `tests/hil`, `tests/oneoff`, `tests/common` | + +Non-package top-level directories: `design/` (owner docs), `documents/`, `hardware/`, +`presentations/`, `felix/`, `am32config/` (ESC config tool, separate `package.json`), `tmp/` +(scratch/working files only). + +`simulation/logs/` is the single log root for every test tier (unit fixtures, simtests, and +SITL stack runs all write there) — it did not move when the other packages were promoted to +top-level. + ## Read Order (for agents) 1. `design/flight_stack.md` (system behavior and control ownership) @@ -61,7 +88,7 @@ ast-grep run -p 'def $NAME($$$ARGS): # Find Lua function definitions in a script ast-grep run -p 'function $NAME($$$ARGS) $$$BODY -end' -l lua simulation/scripts/rawes.lua +end' -l lua scripts/rawes.lua ``` When to still use grep/`grep_search`: matching exact substrings/regex in prose, @@ -85,19 +112,19 @@ 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` | +| SITL stack workflow, lockstep, diagnosis procedure | `design/sitl_testing.md` | `analysis/diagnose_sitl.py` usage text | +| SITL IC-start timeline and event anchors | `design/sitl_flight_timeline.md` | `design/sitl_testing.md`, `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` | | Swashplate geometry and sign mapping | `simulation/swashplate.py` | `design/flight_stack.md` | | Hardware assembly and components | `design/hardware.md` | `design/components.md`, `design/dshot.md`, `design/flap_sensor_bench.md` | -| Testing taxonomy and Lua/Python test conventions | `design/testing.md` | `simulation/pytest.ini` | +| Testing taxonomy and Lua/Python test conventions | `design/testing.md` | `pyproject.toml` (`[tool.pytest.ini_options]`) | | Milestones and decisions history | `design/history.md` | this file (summary only) | Parameter-reference ownership note: -- Canonical place for ArduPilot parameter defaults and inline explanations is `simulation/tests/sitl/copter-heli.parm`. -- Canonical place for RAWES_* parameter defaults and inline explanations is `simulation/tests/sitl/rawes_common_defaults.parm`. +- Canonical place for ArduPilot parameter defaults and inline explanations is `tests/sitl/copter-heli.parm`. +- Canonical place for RAWES_* parameter defaults and inline explanations is `tests/sitl/rawes_common_defaults.parm`. - If a parameter explanation changes, update the owning `.parm` file first; other docs should link to it instead of duplicating bitmasks/tables. ## Core Invariants (summary) @@ -188,7 +215,7 @@ For signs, frame details, EKF gating, and mixer conventions, read the primary do Use this as the quick contract-level reference for the yaw-motor ESC path. Canonical long-form owner doc is `design/dshot.md`. -Active hardware-default parameters (from `simulation/tests/sitl/rawes_common_defaults.parm`): +Active hardware-default parameters (from `tests/sitl/rawes_common_defaults.parm`): - `SERVO9_FUNCTION=36` (Motor4 on output 9), `SERVO9_MIN=1000`, `SERVO9_MAX=2000`, `SERVO9_TRIM=1000` - `SERVO_BLH_MASK=256` (output 9) - `SERVO_BLH_BDMASK=256` (bidirectional DShot on output 9) @@ -202,7 +229,7 @@ Active hardware-default parameters (from `simulation/tests/sitl/rawes_common_def SITL behavior: - These BLHeli/DShot params are intentionally excluded from SITL boot verification - (`simulation/tests/sitl/stack_utils.py` -> `SITL_UNSUPPORTED_PARAMS`) because + (`tests/sitl/stack_utils.py` -> `SITL_UNSUPPORTED_PARAMS`) because ArduCopter-heli SITL does not compile the BLHeli backend and drives output 9 as PWM. ## Workflow Rules @@ -222,9 +249,9 @@ SITL behavior: `TelRow` fields in `telemetry_csv.py`, NVF maps in `torque_test_utils.py`, and row-write dicts in `mediator.py` in the same commit. Mismatch causes `AttributeError` in `TelRow.to_dict()` at runtime. -- Keep `controller.py` aligned with `simulation/scripts/rawes.lua` behavior. -- Keep `simulation/scripts/rawes_test_surface.lua` exports in sync with needed Lua test symbols. -- `_PumpingPythonMode` in `simulation/tests/common/mock_ardupilot.py` is a mechanical translation +- Keep `controller.py` aligned with `scripts/rawes.lua` behavior. +- Keep `scripts/rawes_test_surface.lua` exports in sync with needed Lua test symbols. +- `_PumpingPythonMode` in `tests/common/mock_ardupilot.py` is a mechanical translation of `rawes.lua do_steady_loop_inner()`. Variable names mirror Lua. When changing Lua altitude PID logic, update the Python in the same commit. Key state that must stay in sync: `_tension_for_bz` is a RAMPED value (τ=RAWES_TRP≈2 s) toward `_tension_cmd_n` — not a step. @@ -258,8 +285,8 @@ There are three tiers, each with a different scope and runtime: | Tier | Command | Marker | Notes | |---|---|---|---| -| Unit | `.venv/Scripts/python.exe -m pytest simulation/tests/unit` | (none) | Fast; no physics sim | -| Simtest | `.venv/Scripts/python.exe -m pytest simulation/tests/simtests` | `simtest` | Python physics loop; seconds–minutes | +| Unit | `.venv/Scripts/python.exe -m pytest tests/unit` | (none) | Fast; no physics sim | +| Simtest | `.venv/Scripts/python.exe -m pytest 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): @@ -269,9 +296,9 @@ SITL IC-start timeline rule (agent-critical): - 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 ...`. +- For any test under `tests/sitl/**`, ALWAYS use `bash test.sh stack -n 4 ...`. - DO NOT run SITL tests with host-side pytest commands like - `.venv/Scripts/python.exe -m pytest simulation/tests/sitl/...`. + `.venv/Scripts/python.exe -m pytest tests/sitl/...`. Those bypass the Docker stack harness and can fail with host-path issues (for example `/ardupilot/scripts` not existing on Windows host). - For a single SITL test, use: @@ -313,20 +340,20 @@ Long-running test commands (agent-critical, efficiency): Flight telemetry (pumping, steady, passive SITL runs): ``` -.venv/Scripts/python.exe simulation/viz3d/visualize_3d.py simulation/logs//telemetry.csv +.venv/Scripts/python.exe viz3d/visualize_3d.py simulation/logs//telemetry.csv ``` Example — most recent pumping SITL run: ``` -.venv/Scripts/python.exe simulation/viz3d/visualize_3d.py simulation/logs/test_pumping_cycle_lua_sitl/telemetry.csv +.venv/Scripts/python.exe viz3d/visualize_3d.py simulation/logs/test_pumping_cycle_lua_sitl/telemetry.csv ``` Counter-torque motor telemetry (torque SITL runs): ``` -.venv/Scripts/python.exe simulation/viz3d/visualize_torque.py simulation/logs//telemetry.csv +.venv/Scripts/python.exe viz3d/visualize_torque.py simulation/logs//telemetry.csv ``` Example — yaw regulation run: ``` -.venv/Scripts/python.exe simulation/viz3d/visualize_torque.py simulation/logs/test_yaw_regulation_sitl/telemetry.csv +.venv/Scripts/python.exe viz3d/visualize_torque.py simulation/logs/test_yaw_regulation_sitl/telemetry.csv ``` Controls (both visualizers): Space = play/pause, Left/Right = step frame, +/- = speed. @@ -334,8 +361,8 @@ Controls (both visualizers): Space = play/pause, Left/Right = step frame, +/- = ## File Placement Rules - Temporary and working files must go in `tmp/` at repo root. -- One-off diagnostic scripts belong in `simulation/tests/oneoff/`. -- Reusable analysis tooling belongs in `simulation/analysis/`. +- One-off diagnostic scripts belong in `tests/oneoff/`. +- Reusable analysis tooling belongs in `analysis/`. ## Agent Editing Policy for Docs diff --git a/README.md b/README.md index a764624..3f51c67 100644 --- a/README.md +++ b/README.md @@ -154,10 +154,10 @@ Then run tests in three sequential stages. Always run them in order. ```bash # Stage 1 -- Unit tests (Windows, no Docker, ~460 tests, ~65 s) -.venv/Scripts/python.exe -m pytest simulation/tests/unit -m "not simtest" -q +.venv/Scripts/python.exe -m pytest tests/unit -m "not simtest" -q # Stage 2 -- Simtests (Windows, no Docker, ~29 tests, ~5 min) -.venv/Scripts/python.exe -m pytest simulation/tests/simtests -m simtest -q +.venv/Scripts/python.exe -m pytest tests/simtests -m simtest -q # Stage 3 -- Stack tests (Docker, ArduPilot SITL) bash test.sh stack -v diff --git a/analysis/__init__.py b/analysis/__init__.py new file mode 100644 index 0000000..c6911cf --- /dev/null +++ b/analysis/__init__.py @@ -0,0 +1 @@ +"""analysis — diagnostic tooling for SITL/simtest telemetry and MAVLink logs.""" diff --git a/simulation/analysis/aero_strip.py b/analysis/aero_strip.py similarity index 100% rename from simulation/analysis/aero_strip.py rename to analysis/aero_strip.py diff --git a/simulation/analysis/analyse_descent.py b/analysis/analyse_descent.py similarity index 97% rename from simulation/analysis/analyse_descent.py rename to analysis/analyse_descent.py index 8891aa4..3e83d51 100644 --- a/simulation/analysis/analyse_descent.py +++ b/analysis/analyse_descent.py @@ -23,9 +23,9 @@ Usage ----- - python simulation/analysis/analyse_descent.py - python simulation/analysis/analyse_descent.py --wind 0 --col 0.02 --tension 176 - python simulation/analysis/analyse_descent.py --wind 5 --col 0.02 --tension 200 + python analysis/analyse_descent.py + python analysis/analyse_descent.py --wind 0 --col 0.02 --tension 176 + python analysis/analyse_descent.py --wind 5 --col 0.02 --tension 200 """ from __future__ import annotations @@ -36,11 +36,10 @@ import numpy as np -sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from dynbem import rotor_definition as rd from dynbem import create_aero -from frames import build_orb_frame +from simulation.frames import build_orb_frame _BZ_VERT = np.array([0.0, 0.0, -1.0]) @@ -182,7 +181,7 @@ def simulate_descent_bem_ode( Same point-mass ODE as simulate_descent() but uses q_spin_from_aero + step_spin_ode explicitly, with I_ode_kgm2 (not I_spin) — matching PhysicsCore. """ - from physics_core import q_spin_from_aero, step_spin_ode + from simulation.physics_core import q_spin_from_aero, step_spin_ode rotor = rd.default() dk = rotor.dynamics_kwargs() @@ -258,8 +257,8 @@ def simulate_descent_physics( angles, making comparison with the other two functions meaningless. """ from types import SimpleNamespace - from physics_core import PhysicsCore, q_spin_from_aero - from param_defaults import load_collective_phys_range as _lr + from simulation.physics_core import PhysicsCore, q_spin_from_aero + from simulation.param_defaults import load_collective_phys_range as _lr rotor = rd.default() bz_vert = np.array([0.0, 0.0, -1.0]) diff --git a/simulation/analysis/analyse_gps_fusion.py b/analysis/analyse_gps_fusion.py similarity index 98% rename from simulation/analysis/analyse_gps_fusion.py rename to analysis/analyse_gps_fusion.py index 434b337..17a23cf 100644 --- a/simulation/analysis/analyse_gps_fusion.py +++ b/analysis/analyse_gps_fusion.py @@ -13,9 +13,9 @@ Usage ----- - python3 simulation/analysis/analyse_gps_fusion.py test_stationary_gps_fusion - python3 simulation/analysis/analyse_gps_fusion.py test_guided_flight --plot - python3 simulation/analysis/analyse_gps_fusion.py --log /path/to/gcs.log + python3 analysis/analyse_gps_fusion.py test_stationary_gps_fusion + python3 analysis/analyse_gps_fusion.py test_guided_flight --plot + python3 analysis/analyse_gps_fusion.py --log /path/to/gcs.log """ import argparse @@ -23,7 +23,8 @@ import sys from pathlib import Path -_SIM_DIR = Path(__file__).resolve().parents[1] +import simulation as _simulation_pkg +_SIM_DIR = Path(_simulation_pkg.__file__).resolve().parent # simulation/ # --------------------------------------------------------------------------- # EKF flag definitions (EKF_STATUS_REPORT.flags) diff --git a/simulation/analysis/analyse_gps_telemetry.py b/analysis/analyse_gps_telemetry.py similarity index 100% rename from simulation/analysis/analyse_gps_telemetry.py rename to analysis/analyse_gps_telemetry.py diff --git a/simulation/analysis/analyse_hardware_log.py b/analysis/analyse_hardware_log.py similarity index 98% rename from simulation/analysis/analyse_hardware_log.py rename to analysis/analyse_hardware_log.py index dd605d9..942254b 100644 --- a/simulation/analysis/analyse_hardware_log.py +++ b/analysis/analyse_hardware_log.py @@ -12,8 +12,8 @@ Usage ----- - python simulation/analysis/analyse_hardware_log.py - python simulation/analysis/analyse_hardware_log.py # auto-finds latest in simulation/logs/hardware/ + python analysis/analyse_hardware_log.py + python analysis/analyse_hardware_log.py # auto-finds latest in simulation/logs/hardware/ """ from __future__ import annotations @@ -22,8 +22,6 @@ import sys _SIM_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) -if _SIM_DIR not in sys.path: - sys.path.insert(0, _SIM_DIR) def _find_latest_log() -> str: diff --git a/simulation/analysis/analyse_ic_attitude.py b/analysis/analyse_ic_attitude.py similarity index 96% rename from simulation/analysis/analyse_ic_attitude.py rename to analysis/analyse_ic_attitude.py index 0bc928c..e6bdb83 100644 --- a/simulation/analysis/analyse_ic_attitude.py +++ b/analysis/analyse_ic_attitude.py @@ -10,10 +10,10 @@ import numpy as np -_SIM_DIR = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(_SIM_DIR)) +import simulation as _simulation_pkg +_SIM_DIR = Path(_simulation_pkg.__file__).resolve().parent # simulation/ -from telemetry_csv import read_csv # noqa: E402 +from simulation.telemetry_csv import read_csv # noqa: E402 def _angle_deg(a: np.ndarray, b: np.ndarray) -> float: diff --git a/simulation/analysis/analyse_landing.py b/analysis/analyse_landing.py similarity index 97% rename from simulation/analysis/analyse_landing.py rename to analysis/analyse_landing.py index 0c035f4..b208dd2 100644 --- a/simulation/analysis/analyse_landing.py +++ b/analysis/analyse_landing.py @@ -17,8 +17,8 @@ Usage ----- - .venv/Scripts/python.exe simulation/analysis/analyse_landing.py - .venv/Scripts/python.exe simulation/analysis/analyse_landing.py --test test_landing_lua_sitl --bucket 2 + .venv/Scripts/python.exe analysis/analyse_landing.py + .venv/Scripts/python.exe analysis/analyse_landing.py --test test_landing_lua_sitl --bucket 2 """ import sys, math, csv, argparse @@ -27,7 +27,8 @@ import numpy as np -sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import simulation + BREAK_LOAD_N = 620.0 FLOOR_ALT_M = 1.5 @@ -336,7 +337,7 @@ def frac(pred): if args.csv: csv_path = Path(args.csv) else: - csv_path = Path(__file__).resolve().parents[1] / "logs" / args.test / "telemetry.csv" + csv_path = Path(simulation.__file__).resolve().parent / "logs" / args.test / "telemetry.csv" if not csv_path.exists(): print(f"No telemetry at {csv_path}") diff --git a/simulation/analysis/analyse_pumping.py b/analysis/analyse_pumping.py similarity index 94% rename from simulation/analysis/analyse_pumping.py rename to analysis/analyse_pumping.py index a838859..6b037c5 100644 --- a/simulation/analysis/analyse_pumping.py +++ b/analysis/analyse_pumping.py @@ -17,10 +17,10 @@ Usage ----- - .venv/Scripts/python.exe simulation/analysis/analyse_pumping.py - .venv/Scripts/python.exe simulation/analysis/analyse_pumping.py --wind 8 - .venv/Scripts/python.exe simulation/analysis/analyse_pumping.py --el 30 --tension 400 --wind 10 - .venv/Scripts/python.exe simulation/analysis/analyse_pumping.py --col -0.21 # single run, verbose + .venv/Scripts/python.exe analysis/analyse_pumping.py + .venv/Scripts/python.exe analysis/analyse_pumping.py --wind 8 + .venv/Scripts/python.exe analysis/analyse_pumping.py --el 30 --tension 400 --wind 10 + .venv/Scripts/python.exe analysis/analyse_pumping.py --col -0.21 # single run, verbose """ from __future__ import annotations @@ -31,12 +31,11 @@ import numpy as np -sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from dynbem import rotor_definition as rd -from frames import build_orb_frame -from physics_core import PhysicsCore -from ic import load_ic +from simulation.frames import build_orb_frame +from simulation.physics_core import PhysicsCore +from simulation.ic import load_ic _G = 9.81 @@ -99,7 +98,7 @@ def simulate_one( _ic = load_ic() if omega_init is None: omega_init = _ic.omega_spin - from param_defaults import thrust_to_coll_rad as _t2c # noqa: F811 + from simulation.param_defaults import thrust_to_coll_rad as _t2c # noqa: F811 col_warm = _t2c(_ic.eq_thrust) el = math.radians(elevation_deg) diff --git a/simulation/analysis/analyse_pumping_cycle.py b/analysis/analyse_pumping_cycle.py similarity index 98% rename from simulation/analysis/analyse_pumping_cycle.py rename to analysis/analyse_pumping_cycle.py index 5e71764..b01b191 100644 --- a/simulation/analysis/analyse_pumping_cycle.py +++ b/analysis/analyse_pumping_cycle.py @@ -13,9 +13,9 @@ • Anomalies — sustained high tension, altitude floor, tether slack Usage (from repo root): - wsl.exe bash -c 'python3 /mnt/e/repos/windpower/simulation/analysis/analyse_pumping_cycle.py' - wsl.exe bash -c 'python3 /mnt/e/repos/windpower/simulation/analysis/analyse_pumping_cycle.py --csv /path/to/telemetry.csv' - python3 simulation/analysis/analyse_pumping_cycle.py --csv simulation/logs/telemetry_pumping.csv + wsl.exe bash -c 'python3 /mnt/e/repos/windpower/analysis/analyse_pumping_cycle.py' + wsl.exe bash -c 'python3 /mnt/e/repos/windpower/analysis/analyse_pumping_cycle.py --csv /path/to/telemetry.csv' + python3 analysis/analyse_pumping_cycle.py --csv simulation/logs/telemetry_pumping.csv """ from __future__ import annotations @@ -32,11 +32,11 @@ # Defaults # --------------------------------------------------------------------------- -_SIM_DIR = Path(__file__).resolve().parents[1] +import simulation as _simulation_pkg +_SIM_DIR = Path(_simulation_pkg.__file__).resolve().parent # simulation/ _DEFAULT_CSV = _SIM_DIR / "logs" / "telemetry_pumping.csv" -sys.path.insert(0, str(_SIM_DIR)) -from telemetry_csv import TelRow, read_csv # noqa: E402 +from simulation.telemetry_csv import TelRow, read_csv # noqa: E402 # Thresholds (match test_pumping_cycle.py) _MIN_ALT_M = 0.5 # m — crash floor diff --git a/simulation/analysis/analyse_run.py b/analysis/analyse_run.py similarity index 99% rename from simulation/analysis/analyse_run.py rename to analysis/analyse_run.py index 6f2b9da..9ecb39a 100644 --- a/simulation/analysis/analyse_run.py +++ b/analysis/analyse_run.py @@ -17,8 +17,8 @@ gcs.log -- GCS / MAVLink events (STATUSTEXT, EKF flags, setup steps) Usage: - .venv/Scripts/python.exe simulation/analysis/analyse_run.py test_acro_armed - .venv/Scripts/python.exe simulation/analysis/analyse_run.py test_pumping_cycle --plot + .venv/Scripts/python.exe analysis/analyse_run.py test_acro_armed + .venv/Scripts/python.exe analysis/analyse_run.py test_pumping_cycle --plot With no test name, lists available test directories in simulation/logs/. """ @@ -38,18 +38,17 @@ # Defaults # --------------------------------------------------------------------------- -_SIM_DIR = Path(__file__).resolve().parents[1] # simulation/ +import simulation as _simulation_pkg +_SIM_DIR = Path(_simulation_pkg.__file__).resolve().parent # simulation/ _LOG_DIR = _SIM_DIR / "logs" -sys.path.insert(0, str(_SIM_DIR)) -from telemetry_csv import TelRow, read_csv # noqa: E402 -from mavlink_log import iter_messages as _iter_mavlink # noqa: E402 -from ekf_flags import has_warn as _ekf_has_warn # noqa: E402 -from ic import load_ic_dict # noqa: E402 +from simulation.telemetry_csv import TelRow, read_csv # noqa: E402 +from simulation.mavlink_log import iter_messages as _iter_mavlink # noqa: E402 +from simulation.ekf_flags import has_warn as _ekf_has_warn # noqa: E402 +from simulation.ic import load_ic_dict # noqa: E402 -sys.path.insert(0, str(Path(__file__).parent)) -from flight_log import FlightLog # noqa: E402 +from analysis.flight_log import FlightLog # noqa: E402 _KINEMATIC_PHASES = frozenset({"waiting_ekf", "positioning"}) diff --git a/simulation/analysis/analyze_ang_error.py b/analysis/analyze_ang_error.py similarity index 98% rename from simulation/analysis/analyze_ang_error.py rename to analysis/analyze_ang_error.py index 60815d1..c8fdb0c 100644 --- a/simulation/analysis/analyze_ang_error.py +++ b/analysis/analyze_ang_error.py @@ -40,14 +40,14 @@ Usage ----- - python simulation/analysis/analyze_ang_error.py + python analysis/analyze_ang_error.py [settle_s [observe_s]] [--samples] # dump raw per-sample rows in the window [--ratp P] [--angp P] [--accy DEGSS] # override gains Examples - python simulation/analysis/analyze_ang_error.py test_yaw_regulation_sitl 75 20 - python simulation/analysis/analyze_ang_error.py test_yaw_regulation_sitl 75 20 --samples + python analysis/analyze_ang_error.py test_yaw_regulation_sitl 75 20 + python analysis/analyze_ang_error.py test_yaw_regulation_sitl 75 20 --samples """ from __future__ import annotations @@ -58,7 +58,8 @@ import numpy as np -_SIM_DIR = Path(__file__).resolve().parents[1] +import simulation as _simulation_pkg +_SIM_DIR = Path(_simulation_pkg.__file__).resolve().parent # simulation/ _LOGS_DIR = _SIM_DIR / "logs" # ── ArduPilot constants (mirror AC_AttitudeControl.h) ───────────────────────── diff --git a/simulation/analysis/analyze_yaw_limit_cycle.py b/analysis/analyze_yaw_limit_cycle.py similarity index 99% rename from simulation/analysis/analyze_yaw_limit_cycle.py rename to analysis/analyze_yaw_limit_cycle.py index 497f965..c5650cd 100644 --- a/simulation/analysis/analyze_yaw_limit_cycle.py +++ b/analysis/analyze_yaw_limit_cycle.py @@ -27,7 +27,7 @@ duty, integral riding the clamp. Fix = lower KI / IMAX. Usage: - python simulation/analysis/analyze_yaw_limit_cycle.py [csv] [--start SEC] + python analysis/analyze_yaw_limit_cycle.py [csv] [--start SEC] [--idle-tol-us US] [--ceil-frac F] csv defaults to simulation/logs/test_yaw_regulation_sitl/telemetry.csv diff --git a/simulation/analysis/analyze_yaw_oscillation.py b/analysis/analyze_yaw_oscillation.py similarity index 99% rename from simulation/analysis/analyze_yaw_oscillation.py rename to analysis/analyze_yaw_oscillation.py index 52c423f..e442566 100644 --- a/simulation/analysis/analyze_yaw_oscillation.py +++ b/analysis/analyze_yaw_oscillation.py @@ -17,7 +17,7 @@ actuator<->response phase, the ESC deadband duty, and the trim behaviour. Usage: - python simulation/analysis/analyze_yaw_oscillation.py [--plot] + python analysis/analyze_yaw_oscillation.py [--plot] """ from __future__ import annotations diff --git a/simulation/analysis/analyze_yaw_tuning.py b/analysis/analyze_yaw_tuning.py similarity index 98% rename from simulation/analysis/analyze_yaw_tuning.py rename to analysis/analyze_yaw_tuning.py index c4817af..81fe00d 100644 --- a/simulation/analysis/analyze_yaw_tuning.py +++ b/analysis/analyze_yaw_tuning.py @@ -10,9 +10,9 @@ 5. Prints PID tuning hints based on observed behavior. Usage: - python simulation/analysis/analyze_yaw_tuning.py - python simulation/analysis/analyze_yaw_tuning.py --window 5 - python simulation/analysis/analyze_yaw_tuning.py --no-plot + python analysis/analyze_yaw_tuning.py + python analysis/analyze_yaw_tuning.py --window 5 + python analysis/analyze_yaw_tuning.py --no-plot """ from __future__ import annotations diff --git a/simulation/analysis/arduloop_pid_replay.py b/analysis/arduloop_pid_replay.py similarity index 96% rename from simulation/analysis/arduloop_pid_replay.py rename to analysis/arduloop_pid_replay.py index 92f4e91..0fcb2e6 100644 --- a/simulation/analysis/arduloop_pid_replay.py +++ b/analysis/arduloop_pid_replay.py @@ -17,10 +17,10 @@ arduloop's port (or a params/gain mismatch), not a physics/EKF difference. Usage: - python simulation/analysis/arduloop_pid_replay.py [--window START_S END_S] + python analysis/arduloop_pid_replay.py [--window START_S END_S] Example: - python simulation/analysis/arduloop_pid_replay.py simulation/logs/test_lua_flight_steady_sitl \\ + python analysis/arduloop_pid_replay.py simulation/logs/test_lua_flight_steady_sitl \\ --window 60 75 """ from __future__ import annotations @@ -33,7 +33,6 @@ import numpy as np -sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from arduloop.params import RateAxisParams from arduloop.pid import AC_PID diff --git a/simulation/analysis/arm_target_timeline.py b/analysis/arm_target_timeline.py similarity index 96% rename from simulation/analysis/arm_target_timeline.py rename to analysis/arm_target_timeline.py index acd18a9..03af62d 100644 --- a/simulation/analysis/arm_target_timeline.py +++ b/analysis/arm_target_timeline.py @@ -21,8 +21,8 @@ 3) Optional CSV for offline inspection Examples: - .venv/Scripts/python.exe simulation/analysis/arm_target_timeline.py test_lua_flight_ic_passive_sitl - .venv/Scripts/python.exe simulation/analysis/arm_target_timeline.py simulation/logs/test_lua_flight_ic_passive_sitl --csv out.csv + .venv/Scripts/python.exe analysis/arm_target_timeline.py test_lua_flight_ic_passive_sitl + .venv/Scripts/python.exe analysis/arm_target_timeline.py simulation/logs/test_lua_flight_ic_passive_sitl --csv out.csv """ from __future__ import annotations @@ -37,13 +37,12 @@ from typing import Optional -_SIM_DIR = Path(__file__).resolve().parents[1] +import simulation as _simulation_pkg +_SIM_DIR = Path(_simulation_pkg.__file__).resolve().parent # simulation/ _LOGS_DIR = _SIM_DIR / "logs" -if str(Path(__file__).resolve().parent) not in sys.path: - sys.path.insert(0, str(Path(__file__).resolve().parent)) -from flight_log import FlightLog # noqa: E402 +from analysis.flight_log import FlightLog # noqa: E402 @dataclass diff --git a/simulation/analysis/autorotation_equilibrium.py b/analysis/autorotation_equilibrium.py similarity index 97% rename from simulation/analysis/autorotation_equilibrium.py rename to analysis/autorotation_equilibrium.py index 1037ab7..973a925 100644 --- a/simulation/analysis/autorotation_equilibrium.py +++ b/analysis/autorotation_equilibrium.py @@ -10,7 +10,7 @@ before sampling Q_spin, then uses scipy.optimize.brentq for exact root. Usage: - .venv/Scripts/python.exe simulation/analysis/autorotation_equilibrium.py + .venv/Scripts/python.exe analysis/autorotation_equilibrium.py """ import sys @@ -20,7 +20,6 @@ import numpy as np from scipy.optimize import brentq -sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from dynbem import rotor_definition as rd from dynbem.aero_peters_he_jit import PetersHeBEMJit diff --git a/simulation/analysis/build.py b/analysis/build.py similarity index 100% rename from simulation/analysis/build.py rename to analysis/build.py diff --git a/simulation/analysis/check_landing_forces.py b/analysis/check_landing_forces.py similarity index 93% rename from simulation/analysis/check_landing_forces.py rename to analysis/check_landing_forces.py index 38ea0fd..a2357d9 100644 --- a/simulation/analysis/check_landing_forces.py +++ b/analysis/check_landing_forces.py @@ -8,13 +8,11 @@ import sys, math from pathlib import Path import numpy as np -sys.path.insert(0, str(Path(__file__).resolve().parents[1])) -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tests" / "unit")) from dynbem import rotor_definition as rd from dynbem import create_aero -from controller import col_min_for_altitude_rad -from simtest_ic import load_ic +from simulation.controller import col_min_for_altitude_rad +from tests.simtests.simtest_ic import load_ic WIND = np.array([0.0, 10.0, 0.0]) XI_DEG = 80.0 diff --git a/simulation/analysis/compare_aero_beaupoil.py b/analysis/compare_aero_beaupoil.py similarity index 97% rename from simulation/analysis/compare_aero_beaupoil.py rename to analysis/compare_aero_beaupoil.py index 531bec6..670dfe9 100644 --- a/simulation/analysis/compare_aero_beaupoil.py +++ b/analysis/compare_aero_beaupoil.py @@ -12,7 +12,7 @@ 2 panels per tilt: Thrust vs V, v_i vs V Run: - python simulation/analysis/compare_aero_beaupoil.py + python analysis/compare_aero_beaupoil.py """ import math @@ -25,14 +25,8 @@ import matplotlib.pyplot as plt from matplotlib.lines import Line2D -_SIM_DIR = Path(__file__).resolve().parents[1] -_AERO_DIR = _SIM_DIR / "aero" -for _p in [str(_SIM_DIR), str(_AERO_DIR)]: - if _p not in sys.path: - sys.path.insert(0, _p) - from dynbem import rotor_definition as rd -from frames import build_orb_frame +from simulation.frames import build_orb_frame from dynbem import create_aero, PetersHeBEM # --------------------------------------------------------------------------- diff --git a/simulation/analysis/compare_aero_models.py b/analysis/compare_aero_models.py similarity index 97% rename from simulation/analysis/compare_aero_models.py rename to analysis/compare_aero_models.py index aa132e5..696f4bf 100644 --- a/simulation/analysis/compare_aero_models.py +++ b/analysis/compare_aero_models.py @@ -10,7 +10,7 @@ 4. CT vs in-plane advance ratio μ — standard helicopter performance chart Run: - python simulation/analysis/compare_aero_models.py + python analysis/compare_aero_models.py """ import math @@ -23,14 +23,8 @@ import matplotlib.pyplot as plt from matplotlib.lines import Line2D -_SIM_DIR = Path(__file__).resolve().parents[1] -_AERO_DIR = _SIM_DIR / "aero" -for _p in [str(_SIM_DIR), str(_AERO_DIR)]: - if _p not in sys.path: - sys.path.insert(0, _p) - from dynbem import rotor_definition as rd -from frames import build_orb_frame +from simulation.frames import build_orb_frame from dynbem import create_aero, PetersHeBEM # --------------------------------------------------------------------------- diff --git a/simulation/analysis/compare_pumping.py b/analysis/compare_pumping.py similarity index 93% rename from simulation/analysis/compare_pumping.py rename to analysis/compare_pumping.py index 6e9c5fe..d8932b2 100644 --- a/simulation/analysis/compare_pumping.py +++ b/analysis/compare_pumping.py @@ -5,8 +5,8 @@ and prints a table highlighting where they diverge, focusing on the first cycle reel-out. Usage: - python simulation/analysis/compare_pumping.py [--phase cycle1_reel-out] [--bucket 1] - python simulation/analysis/compare_pumping.py --py logs/... --lua logs/... + python analysis/compare_pumping.py [--phase cycle1_reel-out] [--bucket 1] + python analysis/compare_pumping.py --py logs/... --lua logs/... """ from __future__ import annotations @@ -15,25 +15,11 @@ import sys from pathlib import Path -_SIM = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(_SIM)) +import simulation as _simulation_pkg +_SIM = Path(_simulation_pkg.__file__).resolve().parent # simulation/ _LOG = _SIM / "logs" - "thrust_from_alt_ctrl", - "vib_corr", - "alt_pid_integral", - "tension_feedforward_n", - "omega_rotor", - "aero_T", - "aero_v_inplane", - "aero_v_axial", - "pos_z", - "vel_z", - "tether_rest_length", - "winch_speed_ms", -] - def _load(path: Path) -> list[dict]: rows = [] diff --git a/simulation/analysis/compare_pumping_sitl_simtest.py b/analysis/compare_pumping_sitl_simtest.py similarity index 98% rename from simulation/analysis/compare_pumping_sitl_simtest.py rename to analysis/compare_pumping_sitl_simtest.py index f577135..564269b 100644 --- a/simulation/analysis/compare_pumping_sitl_simtest.py +++ b/analysis/compare_pumping_sitl_simtest.py @@ -10,9 +10,9 @@ 5. Prints a compact diagnostic summary with likely divergence contributors. Usage: - python simulation/analysis/compare_pumping_sitl_simtest.py + python analysis/compare_pumping_sitl_simtest.py - python simulation/analysis/compare_pumping_sitl_simtest.py \ + python analysis/compare_pumping_sitl_simtest.py \ --simtest-dir simulation/logs/test_lua_pumping_unified \ --sitl-dir simulation/logs/test_pumping_cycle_lua_sitl """ diff --git a/simulation/analysis/compare_runs.py b/analysis/compare_runs.py similarity index 99% rename from simulation/analysis/compare_runs.py rename to analysis/compare_runs.py index 431b37d..71abf8b 100644 --- a/simulation/analysis/compare_runs.py +++ b/analysis/compare_runs.py @@ -6,11 +6,11 @@ when both share the same startup conditions. Usage (inside Docker): - python3 /rawes/simulation/analysis/compare_runs.py \\ + python3 /rawes/analysis/compare_runs.py \\ lua_full_test_lua_flight_steady \\ pumping_lua_test_pumping_cycle_lua - python3 /rawes/simulation/analysis/compare_runs.py \\ + python3 /rawes/analysis/compare_runs.py \\ lua_full_test_lua_flight_steady \\ pumping_lua_test_pumping_cycle_lua \\ --plot @@ -42,11 +42,11 @@ from pathlib import Path from typing import Optional -_SIM_DIR = Path(__file__).resolve().parents[1] # simulation/ +import simulation as _simulation_pkg +_SIM_DIR = Path(_simulation_pkg.__file__).resolve().parent # simulation/ _LOG_DIR = _SIM_DIR / "logs" -sys.path.insert(0, str(_SIM_DIR)) -from telemetry_csv import TelRow, read_csv # noqa: E402 +from simulation.telemetry_csv import TelRow, read_csv # noqa: E402 # --------------------------------------------------------------------------- diff --git a/simulation/analysis/diag_lua_steady.py b/analysis/diag_lua_steady.py similarity index 98% rename from simulation/analysis/diag_lua_steady.py rename to analysis/diag_lua_steady.py index 5a464fb..74c677b 100644 --- a/simulation/analysis/diag_lua_steady.py +++ b/analysis/diag_lua_steady.py @@ -6,8 +6,8 @@ and after kinematic exit. Usage: - python simulation/analysis/diag_lua_steady.py - python simulation/analysis/diag_lua_steady.py --log + python analysis/diag_lua_steady.py + python analysis/diag_lua_steady.py --log """ import argparse import csv @@ -17,7 +17,9 @@ import sys from pathlib import Path -# ── CLI ────────────────────────────────────────────────────────────────────── +import simulation + +# ── CLI ─────────────────────────────────────────────────────────────────────────────── ap = argparse.ArgumentParser() ap.add_argument("--log", default=None, help="Log directory (default: simulation/logs/lua_full_test_lua_flight_steady)") @@ -27,7 +29,7 @@ help="Seconds before kinematic exit to show (default: 5)") args = ap.parse_args() -REPO = Path(__file__).resolve().parents[2] +REPO = Path(simulation.__file__).resolve().parent.parent LOG_DIR = Path(args.log) if args.log else \ REPO / "simulation/logs/lua_full_test_lua_flight_steady" diff --git a/simulation/analysis/diagnose_sitl.py b/analysis/diagnose_sitl.py similarity index 98% rename from simulation/analysis/diagnose_sitl.py rename to analysis/diagnose_sitl.py index 1d3588d..5015858 100644 --- a/simulation/analysis/diagnose_sitl.py +++ b/analysis/diagnose_sitl.py @@ -21,8 +21,8 @@ Only when both pass is a post-release flight failure a real controller bug. Usage: - python simulation/analysis/diagnose_sitl.py test_lua_flight_steady_sitl - .venv/Scripts/python.exe simulation/analysis/diagnose_sitl.py test_lua_flight_steady_sitl + python analysis/diagnose_sitl.py test_lua_flight_steady_sitl + .venv/Scripts/python.exe analysis/diagnose_sitl.py test_lua_flight_steady_sitl With no test name, lists available test directories in simulation/logs/. """ @@ -36,15 +36,14 @@ from pathlib import Path from typing import Optional -_SIM_DIR = Path(__file__).resolve().parents[1] # simulation/ +import simulation as _simulation_pkg +_SIM_DIR = Path(_simulation_pkg.__file__).resolve().parent # simulation/ _LOG_DIR = _SIM_DIR / "logs" _IC_PATH = _SIM_DIR / "steady_state_starting.json" -sys.path.insert(0, str(_SIM_DIR)) -from ekf_flags import EKF_FLAGS, has_warn, decode_flags # noqa: E402 +from simulation.ekf_flags import EKF_FLAGS, has_warn, decode_flags # noqa: E402 -sys.path.insert(0, str(Path(__file__).parent)) -from flight_log import FlightLog # noqa: E402 +from analysis.flight_log import FlightLog # noqa: E402 # --------------------------------------------------------------------------- diff --git a/simulation/analysis/diagnose_torque.py b/analysis/diagnose_torque.py similarity index 98% rename from simulation/analysis/diagnose_torque.py rename to analysis/diagnose_torque.py index 637afff..d8bb3eb 100644 --- a/simulation/analysis/diagnose_torque.py +++ b/analysis/diagnose_torque.py @@ -19,12 +19,12 @@ Usage ----- - python simulation/analysis/diagnose_torque.py [settle_s [observe_s]] + python analysis/diagnose_torque.py [settle_s [observe_s]] Examples - python simulation/analysis/diagnose_torque.py test_yaw_regulation_sitl - python simulation/analysis/diagnose_torque.py test_yaw_regulation_sitl 75 20 - python simulation/analysis/diagnose_torque.py simulation/logs/test_yaw_regulation_sitl 75 20 + python analysis/diagnose_torque.py test_yaw_regulation_sitl + python analysis/diagnose_torque.py test_yaw_regulation_sitl 75 20 + python analysis/diagnose_torque.py simulation/logs/test_yaw_regulation_sitl 75 20 """ from __future__ import annotations @@ -34,13 +34,14 @@ import sys from pathlib import Path -_SIM_DIR = Path(__file__).resolve().parents[1] +import simulation as _simulation_pkg +_SIM_DIR = Path(_simulation_pkg.__file__).resolve().parent # simulation/ _LOGS_DIR = _SIM_DIR / "logs" # ── torque_model constants (imported from torque_model.py — single source of truth) ─ import sys as _sys, os as _os _sys.path.insert(0, str(_SIM_DIR)) -from torque_model import GEAR_RATIO, RPM_SCALE, OMEGA_ROTOR_NOMINAL as OMEGA_NOM +from simulation.torque_model import GEAR_RATIO, RPM_SCALE, OMEGA_ROTOR_NOMINAL as OMEGA_NOM THROTTLE_EQ = OMEGA_NOM * GEAR_RATIO / RPM_SCALE # ── diagnosis thresholds ───────────────────────────────────────────────────── diff --git a/simulation/analysis/flight_log.py b/analysis/flight_log.py similarity index 98% rename from simulation/analysis/flight_log.py rename to analysis/flight_log.py index d50e969..ccbcf87 100644 --- a/simulation/analysis/flight_log.py +++ b/analysis/flight_log.py @@ -15,13 +15,12 @@ from pathlib import Path from typing import Optional -_SIM_DIR = Path(__file__).resolve().parents[1] -if str(_SIM_DIR) not in sys.path: - sys.path.insert(0, str(_SIM_DIR)) +import simulation as _simulation_pkg +_SIM_DIR = Path(_simulation_pkg.__file__).resolve().parent # simulation/ -from telemetry_csv import read_csv -from mavlink_log import iter_messages as _iter_mavlink # noqa: F401 (re-exported for callers) -from ekf_flags import ( +from simulation.telemetry_csv import read_csv +from simulation.mavlink_log import iter_messages as _iter_mavlink # noqa: F401 (re-exported for callers) +from simulation.ekf_flags import ( MAV_MODE_ARMED, ARDU_MODES, decode_flags, flag_diff, has_warn, ) diff --git a/simulation/analysis/mavlink_jsonl_query.py b/analysis/mavlink_jsonl_query.py similarity index 97% rename from simulation/analysis/mavlink_jsonl_query.py rename to analysis/mavlink_jsonl_query.py index 2a82cd5..9deb0fc 100644 --- a/simulation/analysis/mavlink_jsonl_query.py +++ b/analysis/mavlink_jsonl_query.py @@ -4,7 +4,7 @@ These logs are written by MavlinkLogWriter (simulation/mavlink_log.py) from both simulation/gcs.py (RawesGCS.start_mavlog) and the bench calibration tool -(simulation/scripts/_calibrate/run.py). Every line is one JSON object: +(calibrate/run.py). Every line is one JSON object: {"_t_wall": , "_dir": "rx"|"tx", "mavpackettype": "", ...fields...} @@ -41,12 +41,11 @@ from pathlib import Path from typing import Any, Iterable, Iterator -_SIM_DIR = Path(__file__).resolve().parents[1] -if str(_SIM_DIR) not in sys.path: - sys.path.insert(0, str(_SIM_DIR)) +import simulation as _simulation_pkg +_SIM_DIR = Path(_simulation_pkg.__file__).resolve().parent # simulation/ -from mavlink_log import iter_messages # noqa: E402 -from ekf_flags import MAV_MODE_ARMED, ARDU_MODES, MAV_STATE # noqa: E402 +from simulation.mavlink_log import iter_messages # noqa: E402 +from simulation.ekf_flags import MAV_MODE_ARMED, ARDU_MODES, MAV_STATE # noqa: E402 MAV_SEVERITY = { 0: "EMERGENCY", 1: "ALERT", 2: "CRITICAL", 3: "ERROR", diff --git a/simulation/analysis/merge_logs.py b/analysis/merge_logs.py similarity index 99% rename from simulation/analysis/merge_logs.py rename to analysis/merge_logs.py index 8520fca..950e42f 100644 --- a/simulation/analysis/merge_logs.py +++ b/analysis/merge_logs.py @@ -29,6 +29,8 @@ from dataclasses import dataclass from pathlib import Path +import simulation + # --------------------------------------------------------------------------- # Log sources @@ -156,7 +158,7 @@ def _find_log_dir(name: str | None = None) -> Path | None: 3. Most recent dir in simulation/logs/ with a gcs.log or mediator.log 4. Most recent pytest tmpdir """ - sim_logs = Path(__file__).resolve().parents[1] / "logs" + sim_logs = Path(simulation.__file__).resolve().parent / "logs" if name: # Exact match diff --git a/simulation/analysis/merge_telemetry_dataflash.py b/analysis/merge_telemetry_dataflash.py similarity index 98% rename from simulation/analysis/merge_telemetry_dataflash.py rename to analysis/merge_telemetry_dataflash.py index e60272f..79723dc 100644 --- a/simulation/analysis/merge_telemetry_dataflash.py +++ b/analysis/merge_telemetry_dataflash.py @@ -10,12 +10,12 @@ Usage ----- - python simulation/analysis/merge_telemetry_dataflash.py \ + python analysis/merge_telemetry_dataflash.py \ simulation/logs/test_lua_flight_steady_sitl/telemetry.csv \ --out merged.csv # You may also pass the explicit dataflash path if it is elsewhere. - python simulation/analysis/merge_telemetry_dataflash.py \ + python analysis/merge_telemetry_dataflash.py \ simulation/logs/test_lua_flight_steady_sitl/telemetry.csv \ simulation/logs/test_lua_flight_steady_sitl/dataflash.BIN @@ -35,12 +35,11 @@ import numpy as np -_SIM_DIR = Path(__file__).resolve().parents[1] -if str(_SIM_DIR) not in sys.path: - sys.path.insert(0, str(_SIM_DIR)) +import simulation as _simulation_pkg +_SIM_DIR = Path(_simulation_pkg.__file__).resolve().parent # simulation/ -from flight_log import FlightLog # noqa: E402 -from telemetry_csv import COLUMNS, read_csv # noqa: E402 +from analysis.flight_log import FlightLog # noqa: E402 +from simulation.telemetry_csv import COLUMNS, read_csv # noqa: E402 DEFAULT_OUT = Path("simulation/logs/merged_telemetry_dataflash.csv") diff --git a/simulation/analysis/plot_peters_he_cycle.py b/analysis/plot_peters_he_cycle.py similarity index 95% rename from simulation/analysis/plot_peters_he_cycle.py rename to analysis/plot_peters_he_cycle.py index 59337e7..8ae53cd 100644 --- a/simulation/analysis/plot_peters_he_cycle.py +++ b/analysis/plot_peters_he_cycle.py @@ -3,8 +3,8 @@ for the Peters-He pumping cycle telemetry. Usage: - .venv/Scripts/python.exe simulation/analysis/plot_peters_he_cycle.py - .venv/Scripts/python.exe simulation/analysis/plot_peters_he_cycle.py --csv path/to/telemetry.csv + .venv/Scripts/python.exe analysis/plot_peters_he_cycle.py + .venv/Scripts/python.exe analysis/plot_peters_he_cycle.py --csv path/to/telemetry.csv """ import sys import argparse @@ -16,8 +16,8 @@ import matplotlib.pyplot as plt import matplotlib.patches as mpatches -_SIM = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(_SIM)) +import simulation as _simulation_pkg +_SIM = Path(_simulation_pkg.__file__).resolve().parent # simulation/ DEFAULT_CSV = _SIM / "logs" / "test_pump_cycle_unified" / "telemetry.csv" diff --git a/simulation/analysis/pump_diagnosis.py b/analysis/pump_diagnosis.py similarity index 98% rename from simulation/analysis/pump_diagnosis.py rename to analysis/pump_diagnosis.py index 0e06110..f321843 100644 --- a/simulation/analysis/pump_diagnosis.py +++ b/analysis/pump_diagnosis.py @@ -11,9 +11,9 @@ Usage ----- - .venv/Scripts/python.exe simulation/analysis/pump_diagnosis.py - .venv/Scripts/python.exe simulation/analysis/pump_diagnosis.py --test test_pump_cycle_unified --bucket 1 - .venv/Scripts/python.exe simulation/analysis/pump_diagnosis.py --verbose # full tables to stdout + .venv/Scripts/python.exe analysis/pump_diagnosis.py + .venv/Scripts/python.exe analysis/pump_diagnosis.py --test test_pump_cycle_unified --bucket 1 + .venv/Scripts/python.exe analysis/pump_diagnosis.py --verbose # full tables to stdout pump_diagnosis_corr.csv columns: phase phase segment name @@ -41,11 +41,10 @@ import numpy as np -sys.path.insert(0, str(Path(__file__).resolve().parents[1])) - +import simulation from dynbem import RotorInputs, create_aero from tests.simtests._rotor_helpers import load_default_rotor -from ic import load_ic +from simulation.ic import load_ic # ── Constants ────────────────────────────────────────────────────────────────── @@ -836,7 +835,7 @@ def fv(v): return f"{v:8.1f}" if not math.isnan(v) else " nan" p.add_argument("--out-dir", default=None, help="override output directory for CSV/JSON files") args = p.parse_args() - csv_path = Path(__file__).resolve().parents[1] / "logs" / args.test / "telemetry.csv" + csv_path = Path(simulation.__file__).resolve().parent / "logs" / args.test / "telemetry.csv" if not csv_path.exists(): print(f"No telemetry at {csv_path}") sys.exit(1) diff --git a/simulation/analysis/pump_envelope.py b/analysis/pump_envelope.py similarity index 98% rename from simulation/analysis/pump_envelope.py rename to analysis/pump_envelope.py index b5cb865..7968649 100644 --- a/simulation/analysis/pump_envelope.py +++ b/analysis/pump_envelope.py @@ -29,11 +29,11 @@ Usage ----- # Static envelope analysis only: - .venv/Scripts/python.exe simulation/analysis/pump_envelope.py - .venv/Scripts/python.exe simulation/analysis/pump_envelope.py --wind 8 12 15 + .venv/Scripts/python.exe analysis/pump_envelope.py + .venv/Scripts/python.exe analysis/pump_envelope.py --wind 8 12 15 # Static envelope + actual vs optimal from a telemetry CSV: - .venv/Scripts/python.exe simulation/analysis/pump_envelope.py \\ + .venv/Scripts/python.exe analysis/pump_envelope.py \\ --telemetry simulation/logs/test_pump_cycle/telemetry.csv """ @@ -45,11 +45,10 @@ import numpy as np -sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from dynbem import create_aero from dynbem import rotor_definition as rd -from ic import load_ic +from simulation.ic import load_ic # ── Current simtest parameters (shown as baseline in all tables) ────────────── TENSION_OUT_NOW = 435.0 # N reel-out TensionPI setpoint @@ -358,7 +357,7 @@ def pump_cycle_report(csv_path: str) -> None: - Altitude hold quality """ from collections import defaultdict - from telemetry_csv import read_csv + from simulation.telemetry_csv import read_csv rows = read_csv(csv_path) if not rows: diff --git a/simulation/analysis/sg6042_polar.py b/analysis/sg6042_polar.py similarity index 100% rename from simulation/analysis/sg6042_polar.py rename to analysis/sg6042_polar.py diff --git a/simulation/analysis/telemetry.py b/analysis/telemetry.py similarity index 54% rename from simulation/analysis/telemetry.py rename to analysis/telemetry.py index 18ab5b4..2eae96e 100644 --- a/simulation/analysis/telemetry.py +++ b/analysis/telemetry.py @@ -1,2 +1,2 @@ -# Moved to simulation/viz3d/telemetry.py +# Moved to viz3d/telemetry.py from viz3d.telemetry import * # noqa: F401, F403 diff --git a/simulation/analysis/validate_sitl_sensors.py b/analysis/validate_sitl_sensors.py similarity index 98% rename from simulation/analysis/validate_sitl_sensors.py rename to analysis/validate_sitl_sensors.py index 5093948..49409d2 100644 --- a/simulation/analysis/validate_sitl_sensors.py +++ b/analysis/validate_sitl_sensors.py @@ -37,8 +37,8 @@ Usage ----- - python simulation/analysis/validate_sitl_sensors.py - e.g. python simulation/analysis/validate_sitl_sensors.py kin_gps_test_kinematic_gps + python analysis/validate_sitl_sensors.py + e.g. python analysis/validate_sitl_sensors.py kin_gps_test_kinematic_gps """ from __future__ import annotations @@ -48,7 +48,9 @@ import numpy as np from pathlib import Path -LOG_ROOT = Path(__file__).resolve().parents[2] / "simulation" / "logs" +import simulation + +LOG_ROOT = Path(simulation.__file__).resolve().parent / "logs" GRAVITY = np.array([0.0, 0.0, 9.81]) # NED: +Z is down diff --git a/simulation/arduloop/CONTROL_LOOPS.md b/arduloop/CONTROL_LOOPS.md similarity index 100% rename from simulation/arduloop/CONTROL_LOOPS.md rename to arduloop/CONTROL_LOOPS.md diff --git a/simulation/arduloop/README.md b/arduloop/README.md similarity index 100% rename from simulation/arduloop/README.md rename to arduloop/README.md diff --git a/simulation/arduloop/__init__.py b/arduloop/__init__.py similarity index 100% rename from simulation/arduloop/__init__.py rename to arduloop/__init__.py diff --git a/simulation/arduloop/analysis.py b/arduloop/analysis.py similarity index 100% rename from simulation/arduloop/analysis.py rename to arduloop/analysis.py diff --git a/simulation/arduloop/attitude_heli.py b/arduloop/attitude_heli.py similarity index 100% rename from simulation/arduloop/attitude_heli.py rename to arduloop/attitude_heli.py diff --git a/simulation/arduloop/filters.py b/arduloop/filters.py similarity index 100% rename from simulation/arduloop/filters.py rename to arduloop/filters.py diff --git a/simulation/arduloop/guided.py b/arduloop/guided.py similarity index 100% rename from simulation/arduloop/guided.py rename to arduloop/guided.py diff --git a/simulation/arduloop/log_df_equiv.py b/arduloop/log_df_equiv.py similarity index 100% rename from simulation/arduloop/log_df_equiv.py rename to arduloop/log_df_equiv.py diff --git a/simulation/arduloop/params.py b/arduloop/params.py similarity index 99% rename from simulation/arduloop/params.py rename to arduloop/params.py index e187021..7117621 100644 --- a/simulation/arduloop/params.py +++ b/arduloop/params.py @@ -11,7 +11,7 @@ ------------ Pass a flat AP params dict to build from .parm files:: - from param_defaults import load_ap_params + from simulation.param_defaults import load_ap_params hp = HeliParams.from_ap_dict(load_ap_params()) rp = RateAxisParams.from_ap_dict(load_ap_params(), "RLL") diff --git a/simulation/arduloop/pid.py b/arduloop/pid.py similarity index 100% rename from simulation/arduloop/pid.py rename to arduloop/pid.py diff --git a/simulation/arduloop/plant.py b/arduloop/plant.py similarity index 100% rename from simulation/arduloop/plant.py rename to arduloop/plant.py diff --git a/simulation/arduloop/run_demo.py b/arduloop/run_demo.py similarity index 100% rename from simulation/arduloop/run_demo.py rename to arduloop/run_demo.py diff --git a/simulation/arduloop/signals.py b/arduloop/signals.py similarity index 100% rename from simulation/arduloop/signals.py rename to arduloop/signals.py diff --git a/simulation/arduloop/swash.py b/arduloop/swash.py similarity index 100% rename from simulation/arduloop/swash.py rename to arduloop/swash.py diff --git a/calibrate.cmd b/calibrate.cmd index 038a1dc..58f3fda 100644 --- a/calibrate.cmd +++ b/calibrate.cmd @@ -41,4 +41,4 @@ if not "%REQ_HASH%"=="%STAMP_HASH%" ( echo %REQ_HASH%>"%STAMP%" ) -"%PYTHON%" "%~dp0simulation\scripts\calibrate.py" %* +"%PYTHON%" -m calibrate %* diff --git a/calibrate/__init__.py b/calibrate/__init__.py new file mode 100644 index 0000000..6aa0986 --- /dev/null +++ b/calibrate/__init__.py @@ -0,0 +1,7 @@ +"""calibrate -- interactive hardware calibration/bench CLI (MAVLink over USB/SiK). + +Run as: python -m calibrate [--port P] [--baud B] [--force] [args...] +""" +from .repl import main + +__all__ = ["main"] diff --git a/calibrate/__main__.py b/calibrate/__main__.py new file mode 100644 index 0000000..a0aca97 --- /dev/null +++ b/calibrate/__main__.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python3 +""" +calibrate/__main__.py -- thin entry point; all logic lives in calibrate/repl.py. + +Run as: python -m calibrate [--port P] [--baud B] [--force] [args...] +""" +from calibrate.repl import main + +if __name__ == "__main__": + main() diff --git a/simulation/scripts/_calibrate/constants.py b/calibrate/constants.py similarity index 90% rename from simulation/scripts/_calibrate/constants.py rename to calibrate/constants.py index d5ddb35..bf154e0 100644 --- a/simulation/scripts/_calibrate/constants.py +++ b/calibrate/constants.py @@ -1,5 +1,5 @@ """ -_calibrate/constants.py -- All module-level constants, tables, and sys.path setup. +calibrate/constants.py -- All module-level constants and tables. Every submodule that needs gcs/param_defaults/servo_pwm imports them from here via relative imports (from .constants import ...). @@ -8,21 +8,19 @@ import math import os -import sys -# --------------------------------------------------------------------------- -# sys.path setup — make simulation/ importable (for gcs, param_defaults, etc.) -# --------------------------------------------------------------------------- -_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) # …/scripts/_calibrate/ -_SIM_DIR = os.path.abspath(os.path.join(_SCRIPT_DIR, '..', '..')) # …/simulation/ -if _SIM_DIR not in sys.path: - sys.path.insert(0, _SIM_DIR) +import simulation # Re-exported so submodules can do `from .constants import RawesGCS` etc. -from gcs import RawesGCS, WallClock # noqa: E402 -from param_defaults import load_ap_params # noqa: E402 -from servo_pwm import (SWASH_PWM_MIN, SWASH_PWM_NEUTRAL, SWASH_PWM_MAX, - MOTOR_PWM_MIN, MOTOR_PWM_MAX) # noqa: E402 +from simulation.gcs import RawesGCS, WallClock +from simulation.param_defaults import load_ap_params +from simulation.servo_pwm import (SWASH_PWM_MIN, SWASH_PWM_NEUTRAL, SWASH_PWM_MAX, + MOTOR_PWM_MIN, MOTOR_PWM_MAX) + +# Repo root, resolved via the installed `simulation` package rather than a +# relative sys.path hack -- works regardless of CWD or invocation style. +_SIM_DIR = os.path.dirname(os.path.abspath(simulation.__file__)) +_REPO_ROOT = os.path.dirname(_SIM_DIR) # --------------------------------------------------------------------------- # GB4008 motor constants (used in diag torque estimates) @@ -94,8 +92,8 @@ # --------------------------------------------------------------------------- # Param file paths # --------------------------------------------------------------------------- -_AP_BASE_PARM_PATH = os.path.join(_SIM_DIR, "tests", "sitl", "copter-heli.parm") -_RAWES_COMMON_PARM_PATH = os.path.join(_SIM_DIR, "tests", "sitl", "rawes_common_defaults.parm") +_AP_BASE_PARM_PATH = os.path.join(_REPO_ROOT, "tests", "sitl", "copter-heli.parm") +_RAWES_COMMON_PARM_PATH = os.path.join(_REPO_ROOT, "tests", "sitl", "rawes_common_defaults.parm") # Never push hardware-calibrated sensor values from defaults to a real FC. _CALIBRATION_PARAM_PREFIXES = ( @@ -293,4 +291,4 @@ # --------------------------------------------------------------------------- # Logging directory # --------------------------------------------------------------------------- -_LOG_DIR = os.path.join(_SIM_DIR, "logs", "calibrate") +_LOG_DIR = os.path.join(_SIM_DIR, "logs", "calibrate") # simulation/logs/calibrate/ diff --git a/simulation/scripts/_calibrate/hw.py b/calibrate/hw.py similarity index 99% rename from simulation/scripts/_calibrate/hw.py rename to calibrate/hw.py index 6dd55ff..ad7792c 100644 --- a/simulation/scripts/_calibrate/hw.py +++ b/calibrate/hw.py @@ -1,5 +1,5 @@ """ -_calibrate/hw.py -- ESC telemetry, swashplate mix, MAVLink send helpers, +calibrate/hw.py -- ESC telemetry, swashplate mix, MAVLink send helpers, arm/disarm, probe/ping, monitor_esc, sweep, status/drain. """ from __future__ import annotations diff --git a/simulation/scripts/_calibrate/params.py b/calibrate/params.py similarity index 99% rename from simulation/scripts/_calibrate/params.py rename to calibrate/params.py index 6044b8e..38374c9 100644 --- a/simulation/scripts/_calibrate/params.py +++ b/calibrate/params.py @@ -1,5 +1,5 @@ """ -_calibrate/params.py -- Param config, log download, script management. +calibrate/params.py -- Param config, log download, script management. """ from __future__ import annotations diff --git a/simulation/scripts/_calibrate/repl.py b/calibrate/repl.py similarity index 99% rename from simulation/scripts/_calibrate/repl.py rename to calibrate/repl.py index 0bb7892..28a28e1 100644 --- a/simulation/scripts/_calibrate/repl.py +++ b/calibrate/repl.py @@ -1,5 +1,5 @@ """ -_calibrate/repl.py -- All _cmd_* handlers, REPL loop, CLI parser, main(). +calibrate/repl.py -- All _cmd_* handlers, REPL loop, CLI parser, main(). """ from __future__ import annotations diff --git a/simulation/scripts/_calibrate/run.py b/calibrate/run.py similarity index 99% rename from simulation/scripts/_calibrate/run.py rename to calibrate/run.py index 1b7dab0..af7ef84 100644 --- a/simulation/scripts/_calibrate/run.py +++ b/calibrate/run.py @@ -1,5 +1,5 @@ """ -_calibrate/run.py -- Observation loop engine, run command, oscillate. +calibrate/run.py -- Observation loop engine, run command, oscillate. """ from __future__ import annotations diff --git a/simulation/scripts/_calibrate/util.py b/calibrate/util.py similarity index 98% rename from simulation/scripts/_calibrate/util.py rename to calibrate/util.py index 07e5697..55820df 100644 --- a/simulation/scripts/_calibrate/util.py +++ b/calibrate/util.py @@ -1,5 +1,5 @@ """ -_calibrate/util.py -- Pure-Python utilities with no MAVLink/session dependencies. +calibrate/util.py -- Pure-Python utilities with no MAVLink/session dependencies. These are shared by run.py, watch.py, params.py, and repl.py without risk of circular imports. diff --git a/simulation/scripts/_calibrate/watch.py b/calibrate/watch.py similarity index 99% rename from simulation/scripts/_calibrate/watch.py rename to calibrate/watch.py index 5501b1e..e683f7b 100644 --- a/simulation/scripts/_calibrate/watch.py +++ b/calibrate/watch.py @@ -1,5 +1,5 @@ """ -_calibrate/watch.py -- watch stream command and helpers. +calibrate/watch.py -- watch stream command and helpers. """ from __future__ import annotations diff --git a/design/EKF_GATING.md b/design/EKF_GATING.md index d1ea840..beb510d 100644 --- a/design/EKF_GATING.md +++ b/design/EKF_GATING.md @@ -255,4 +255,4 @@ GPS lag is the classic cause of `recall()` never landing in-window. Related project docs: [ekf_const_pos_mode.md](ekf_const_pos_mode.md), [flight_stack.md](flight_stack.md) (EKF3 config, Appendix D GPS timing). -Diagnostic tool: `simulation/analysis/diagnose_sitl.py`. +Diagnostic tool: `analysis/diagnose_sitl.py`. diff --git a/design/aero_conventions.md b/design/aero_conventions.md index 5423bcd..460da9b 100644 --- a/design/aero_conventions.md +++ b/design/aero_conventions.md @@ -204,7 +204,7 @@ def test_tilt_lat_positive_gives_roll_right_moment(): At hover (`R_hub = I`), NED frame moments equal body-frame moments, so this directly validates the sign convention. -### Regression Test: `simulation/tests/unit/test_cyclic_direction.py` +### Regression Test: `tests/unit/test_cyclic_direction.py` Validates cyclic derivatives at saved IC (tethered equilibrium): diff --git a/design/calibration.md b/design/calibration.md index 4d4d7d2..673dfd7 100644 --- a/design/calibration.md +++ b/design/calibration.md @@ -1,16 +1,17 @@ -# calibrate.py — Hardware Calibration Tool +# calibrate — Hardware Calibration Tool -`simulation/scripts/calibrate.py` connects to the Pixhawk 6C over USB (or SiK radio) +`calibrate` (top-level package, run as `python -m calibrate`, or via `calibrate.cmd` +on Windows) connects to the Pixhawk 6C over USB (or SiK radio) and provides servo control, motor testing, ESC diagnostics, arming, and Lua script upload — all over MAVLink, with no arming required for most commands. ## Connection ```bash -python simulation/scripts/calibrate.py # auto-detect port -python simulation/scripts/calibrate.py --port COM7 -python simulation/scripts/calibrate.py --port COM7 --baud 57600 # SiK radio -python simulation/scripts/calibrate.py --port COM7 [args] # non-interactive +python -m calibrate # auto-detect port +python -m calibrate --port COM7 +python -m calibrate --port COM7 --baud 57600 # SiK radio +python -m calibrate --port COM7 [args] # non-interactive ``` If `--port` is omitted, the tool scans all COM ports and connects to the first one @@ -44,7 +45,7 @@ limiter you want here. ## CLI shape ``` -calibrate.py [--port P] [--baud B] [--force] [args...] +python -m calibrate [--port P] [--baud B] [--force] [args...] ``` Two long-running verbs (`run`, `watch`) handle anything time-bounded and always log @@ -83,10 +84,10 @@ calibrate `RAWES_YAW_SLP` (slope) from a bench measurement. ```bash # Bench check: hold IC swashplate, observer active, 30 s -python calibrate.py --port COM7 run passive --duration 30 --trim tlon=0.02,thr=0.342 +python -m calibrate --port COM7 run passive --duration 30 --trim tlon=0.02,thr=0.342 # Steady-flight bench, unbounded (ESC to stop) -python calibrate.py --port COM7 run steady +python -m calibrate --port COM7 run steady ``` ### `watch [--duration N]` @@ -102,9 +103,9 @@ Read-only observation; never changes vehicle state, never arms. Default duration | `power` | BATTERY_STATUS / SYS_STATUS | t, vbat_v, current_a, power_w | ```bash -python calibrate.py --port COM7 watch servos --duration 15 -python calibrate.py --port COM7 watch attitude --duration 60 -python calibrate.py --port COM7 watch text # default 10 s +python -m calibrate --port COM7 watch servos --duration 15 +python -m calibrate --port COM7 watch attitude --duration 60 +python -m calibrate --port COM7 watch text # default 10 s ``` --- @@ -122,8 +123,8 @@ silent rejects (writes that the FC ACKs but doesn't apply, e.g. swash-channel `SERVOn_MIN/MAX`). ```bash -python calibrate.py --port COM7 set H_COL_MAX 1700 -python calibrate.py --port COM7 get RAWES_MODE +python -m calibrate --port COM7 set H_COL_MAX 1700 +python -m calibrate --port COM7 get RAWES_MODE ``` ### `swash` @@ -165,7 +166,7 @@ toggles `SCR_ENABLE 1→0→1` to restart the scripting engine (no reboot needed ### `config show` / `config apply` Diff the live FC params against shared parm defaults: -`simulation/tests/sitl/copter-heli.parm` + `simulation/tests/sitl/rawes_common_defaults.parm` +`tests/sitl/copter-heli.parm` + `tests/sitl/rawes_common_defaults.parm` (excluding SITL-only and hardware calibration params). `show` prints an `[OK]`/`[DIFF]`/`[FAIL]` table without changes; `apply` writes every `[DIFF]`. @@ -189,7 +190,7 @@ calibrate requests `PID_TUNING`, but the FC must still be configured to emit it. ### `GCS_PID_MASK` (ArduCopter) `GCS_PID_MASK` is documented in the canonical ArduPilot parameter file: -`simulation/tests/sitl/copter-heli.parm`. +`tests/sitl/copter-heli.parm`. Use that `.parm` file as the single source of truth for bit assignments, default, and common values. @@ -217,40 +218,40 @@ Useful for offline analysis. ```bash # 0. Survey ports (first time) -python calibrate.py ping +python -m calibrate ping # 1. Diff against canonical params; apply if needed -python calibrate.py --port COM7 config show -python calibrate.py --port COM7 config apply # if any [DIFF] shown -python calibrate.py --port COM7 reboot +python -m calibrate --port COM7 config show +python -m calibrate --port COM7 config apply # if any [DIFF] shown +python -m calibrate --port COM7 reboot # 2. Verify live state -python calibrate.py --port COM7 status +python -m calibrate --port COM7 status # 3. Swashplate neutral + mixing check (one-shot, no arming) -python calibrate.py --port COM7 swash neutral -python calibrate.py --port COM7 swash 50 0 0 # all servos rise equally? -python calibrate.py --port COM7 swash 0 0 50 # lateral differential? +python -m calibrate --port COM7 swash neutral +python -m calibrate --port COM7 swash 50 0 0 # all servos rise equally? +python -m calibrate --port COM7 swash 0 0 50 # lateral differential? # 4. Limit swash travel if servos can't take full range -python calibrate.py --port COM7 swash range 1300 1700 -python calibrate.py --port COM7 set H_CYC_MAX 1000 +python -m calibrate --port COM7 swash range 1300 1700 +python -m calibrate --port COM7 set H_CYC_MAX 1000 # 5. Flap deflection measurement -- sweep each servo -python calibrate.py --port COM7 servo sweep 1 -python calibrate.py --port COM7 servo sweep 2 +python -m calibrate --port COM7 servo sweep 1 +python -m calibrate --port COM7 servo sweep 2 # 6. Motor spin check -python calibrate.py --port COM7 motor 5 --duration 5 -python calibrate.py --port COM7 watch esc --duration 10 +python -m calibrate --port COM7 motor 5 --duration 5 +python -m calibrate --port COM7 watch esc --duration 10 # 7. Upload updated Lua and verify -python calibrate.py --port COM7 script upload simulation/scripts/rawes.lua -python calibrate.py --port COM7 script list +python -m calibrate --port COM7 script upload scripts/rawes.lua +python -m calibrate --port COM7 script list # 8. Quiet armed bench check -python calibrate.py --port COM7 run passive --duration 30 --trim tlon=0.02,col=-0.15 +python -m calibrate --port COM7 run passive --duration 30 --trim tlon=0.02,col=-0.15 # 9. Passive hold check with current controller settings -python calibrate.py --port COM7 run passive --duration 60 --trim tlon=0.02,thr=0.342 +python -m calibrate --port COM7 run passive --duration 60 --trim tlon=0.02,thr=0.342 ``` diff --git a/design/flight_stack.md b/design/flight_stack.md index 5a262a9..c8f8da4 100644 --- a/design/flight_stack.md +++ b/design/flight_stack.md @@ -291,7 +291,7 @@ def estimate_wind(T_meas, anti_rot_pwm, body_z, theta_col, hub_pos_lpf): ### 4.1 rawes.lua Overview -Single unified controller (`simulation/scripts/rawes.lua`) running at 50 Hz (FLIGHT_PERIOD_MS=20) on a 100 Hz base tick (BASE_PERIOD_MS=10). +Single unified controller (`scripts/rawes.lua`) running at 50 Hz (FLIGHT_PERIOD_MS=20) on a 100 Hz base tick (BASE_PERIOD_MS=10). #### How one 50 Hz tick works @@ -1044,12 +1044,12 @@ level first. | File | Description | |---|---| -| `simulation/scripts/rawes.lua` | Unified Lua controller (modes 0/1/3/4, RAWES_ARM, bz_altitude_hold force balance, altitude-PID collective; pumping runs in mode 1) | -| `simulation/tests/sitl/rawes_sitl_defaults.parm` | Boot-time ArduPilot params (EKF3, GPS, compass, servos) | -| `simulation/tests/sitl/flight/conftest.py` | Flight fixtures for guided and Lua stack tests | -| `simulation/tests/sitl/torque/conftest.py` | Torque fixtures for DDFP and Lua PASSIVE stack paths | -| `simulation/tests/sitl/stack_infra.py` | Shared infra: `_sitl_stack`, torque stack helpers, `StackContext`; `_arm_sequence(...)` for stack bring-up | -| `simulation/scripts/calibrate.py` | Interactive calibration CLI for run/watch/motor/log/config workflows | +| `scripts/rawes.lua` | Unified Lua controller (modes 0/1/3/4, RAWES_ARM, bz_altitude_hold force balance, altitude-PID collective; pumping runs in mode 1) | +| `tests/sitl/rawes_sitl_defaults.parm` | Boot-time ArduPilot params (EKF3, GPS, compass, servos) | +| `tests/sitl/flight/conftest.py` | Flight fixtures for guided and Lua stack tests | +| `tests/sitl/torque/conftest.py` | Torque fixtures for DDFP and Lua PASSIVE stack paths | +| `tests/sitl/stack_infra.py` | Shared infra: `_sitl_stack`, torque stack helpers, `StackContext`; `_arm_sequence(...)` for stack bring-up | +| `calibrate/` (`python -m calibrate`) | Interactive calibration CLI for run/watch/motor/log/config workflows | | `simulation/controller.py` | `compute_bz_altitude_hold`, `AltitudeHoldController`, `TensionPI`, `RatePID`, `compute_rate_cmd`, `OrbitTracker` | | `simulation/ap_controller.py` | `TensionApController` (400 Hz AP side), `LandingApController` | | `simulation/pumping_planner.py` | `TensionCommand`, `PumpingGroundController` (10 Hz phase state machine) | @@ -1063,8 +1063,8 @@ level first. | `simulation/comms.py` | `VirtualComms` (simtest), `MavlinkComms` (SITL/hardware) | | `simulation/gcs.py` | `RawesGCS` MAVLink client: arm, mode, params, `send_named_float` | | `simulation/sensor.py` | `PhysicalSensor` — honest NED sensors (accel, gyro, vel) | -| `simulation/analysis/analyse_run.py` | Post-run report: physics + EKF/GPS + attitude per time bucket | -| `simulation/analysis/analyse_landing.py` | Landing diagnosis: alt/vz/winch/tension/collective per bucket | +| `analysis/analyse_run.py` | Post-run report: physics + EKF/GPS + attitude per time bucket | +| `analysis/analyse_landing.py` | Landing diagnosis: alt/vz/winch/tension/collective per bucket | | `hardware.md` | Assembly layout, rotor geometry, swashplate, Kaman flap mechanism | | `dshot.md` | DShot reference, AM32 EDT, GB4008 wiring | | `theory_pumping.md` | De Schutter 2018 — pumping cycle, aero, structural constraints | diff --git a/design/simulation.md b/design/simulation.md index e278019..be7276a 100644 --- a/design/simulation.md +++ b/design/simulation.md @@ -58,7 +58,7 @@ Always initialise the hub with body Z along the tether, not upright. The `build_ ### arduloop/ — ArduPilot GUIDED mode Python port -`simulation/arduloop/` is a self-contained Python port of ArduPilot's traditional-helicopter attitude and rate-control stack. Used by `MockArdupilot` (simtest adapter) and `_MockArdupilotBase.step_physics()` to close the GUIDED-mode control loop in-process without real ArduPilot SITL. +`arduloop/` is a self-contained Python port of ArduPilot's traditional-helicopter attitude and rate-control stack. Used by `MockArdupilot` (simtest adapter) and `_MockArdupilotBase.step_physics()` to close the GUIDED-mode control loop in-process without real ArduPilot SITL. | Layer | Class | ArduPilot equivalent | |-------|-------|----------------------| @@ -319,7 +319,7 @@ The default initial state is the warmup-settled equilibrium produced by `test_ge **IMPORTANT:** The `vel` in this file is the near-zero physics velocity at settled state. The stack test does **NOT** use it as `vel0` for the kinematic startup ramp. `vel0 = [-0.257, 0.916, -0.093]` from `config.py` DEFAULTS is always used for the ramp — it provides a non-zero heading so the EKF gets a velocity-derived yaw from frame 0. **Workflow for regenerating:** -1. `pytest simulation/tests/unit/test_generate_ic.py::test_create_ic -s` — writes `simulation/steady_state_starting.json` +1. `pytest tests/unit/test_generate_ic.py::test_create_ic -s` — writes `simulation/steady_state_starting.json` 2. Stack test reads this file and passes `pos0`, `body_z`, `omega_spin`, `rest_length` to mediator via config 3. `test_steady_flight.py` reads the file but never writes it @@ -416,7 +416,7 @@ Landing logic is implemented in `LandingGroundController` + `LandingApController ### Fixture -Landing Lua fixture in `simulation/tests/sitl/flight/conftest.py` (`kinematic_vel_ramp_s = 20` so the hub exits kinematic at vel=0 — eliminates linear tether jolt). +Landing Lua fixture in `tests/sitl/flight/conftest.py` (`kinematic_vel_ramp_s = 20` so the hub exits kinematic at vel=0 — eliminates linear tether jolt). ### Diagnosis @@ -446,17 +446,17 @@ Additional physics limitations in the current simulation: ## Module Map +Repo layout note: `simulation/` is one of 8 top-level packages (see `AGENTS.md` -> +Repository Layout). `arduloop/`, `analysis/`, `viz3d/`, `scripts/`, and `tests/` are +siblings of `simulation/` at the repo root, not nested inside it — shown here as +separate trees for clarity. + ``` simulation/ ├── dynamics.py RK4 6-DOF rigid-body integrator ├── aero/ Internal aero package — NOT used by PhysicsCore/PhysicsRunner. │ PetersHeBEMJit, PetersHeBEM, CCBladeBEM, OpenFASTBEM, SimpleBEM. │ Production code uses dynbem.create_aero() (external package at C:/repos/aero). -├── arduloop/ ArduPilot GUIDED/rate control Python port (self-contained package). -│ guided.py: GuidedAttitudeController (input_quaternion + sqrt P-ctrl). -│ attitude_heli.py: HeliRateController (rate PIDs + swash output). -│ params.py: HeliParams, RateAxisParams (1:1 ArduPilot param names). -│ Used by MockArdupilot._LuaBackend and _MockArdupilotBase.step_physics(). ├── tether.py Tension-only elastic tether (Dyneema SK75) ├── swashplate.py H3-120 inverse mixing, cyclic blade pitch ├── frames.py build_orb_frame(), T_ENU_NED (external-data conversion utility) @@ -519,54 +519,65 @@ simulation/ ├── rawes_lua_harness.py RawesLua class — runs rawes.lua in-process via lupa; shared by unit │ tests and simtests. Loads mock_ardupilot.lua then rawes.lua. Python writes │ sensor inputs to `_mock` and calls `_update_fn()` each tick. -├── rawes_modes.py Python constants mirroring rawes.lua mode/substate numbers. -├── scripts/rawes.lua Unified Lua controller (RAWES_MODE: 0=none 1=steady 3=passive 4=landing). -├── scripts/rawes_test_surface.lua Test-surface table (_rawes_fns) splicing internal locals -│ for Python unit tests via lupa. -├── analysis/ -│ ├── flight_log.py Unified data loader — FlightLog.load(log_dir), FlightLog.buckets(bucket_s), -│ │ FlightEvent, Bucket; reads all log sources into one structure. -│ ├── analyse_run.py Post-run report: print_flight_report, compute_steady_metrics, -│ │ validate_ekf_window; CLI: --bucket S. -│ ├── analyse_landing.py Landing diagnosis (alt/vz/winch/tension/collective per bucket). -│ ├── pump_envelope.py Pumping cycle envelope sweep (tension setpoints, wind, tilt). -│ └── pump_diagnosis.py Per-bucket compact summary + CSV (osc, corr) to test log dir. -├── viz3d/ -│ ├── visualize_3d.py Interactive 3D playback of any telemetry.csv (default viz tool). -│ ├── scrub.py Interactive frame scrubber. -│ ├── render_cycle.py Off-screen render to MP4 / GIF. -│ ├── telemetry.py TelemetryFrame dataclass + CSVSource / LiveQueueSource protocol. -│ ├── visualize_torque.py Torque telemetry 3-panel replay. -│ └── torque_telemetry.py TorqueTelemetryFrame dataclass. -└── tests/ - ├── unit/ Windows native, no Docker (~685 fast unit tests; no simtests). - │ └── README.md Unit & simtest reference guide. - ├── simtests/ Windows native, no Docker (~13 full physics simulation tests; marker: simtest). - │ ├── conftest.py simtest marker registration; 600 s auto-timeout. - │ ├── simtest_runner.py PhysicsRunner — thin wrapper around PhysicsCore. - │ │ HeliCyclicController baked in. Two step methods: - │ │ step(dt, col, rate_roll, rate_pitch, omega_body) — Python AP path; - │ │ step_guided(dt, col, heli_out) — GUIDED path (HeliRateOutput from - │ │ GuidedAttitudeController). Re-exports MockArdupilot from - │ │ tests/common/mock_ardupilot.py. - │ └── simtest_ic.py load_ic() — loads steady_state_starting.json. - ├── common/ - │ └── mock_ardupilot.py MockArdupilot — public adapter for simtests. Two factory methods: - │ MockArdupilot.for_lua(sim, wind, dt, initial_thrust=...) — Lua backend - │ wraps RawesLua; reads guided_target/_rate_target/_throttle from - │ _mock and feeds GuidedAttitudeController each tick; - │ MockArdupilot.for_python(mode=..., wind, dt, **kwargs) — Python - │ AP backend (calls the selected mode step each tick). - │ Shared base (_MockArdupilotBase): enable_guided(), step_physics(), - │ log(), write_telemetry(). TelRow.from_physics() written at - │ RAWES_TEL_HZ (default 20 Hz, override via env var). - └── sitl/ Docker; all SITL/stack tests live here. - ├── conftest.py thin re-exporter — pytest_addoption + pytest_configure only. - ├── stack_infra.py StackConfig, SitlContext, _sitl_stack, _acro_stack, _torque_stack. - ├── stack_utils.py port checks, log copy, mediator launcher. - ├── rawes_sitl_defaults.parm boot-time ArduPilot params (EEPROM defaults). - ├── flight/ flight stack tests (mediator + physics + ArduPilot). - └── torque/ torque/anti-rotation tests. +└── rawes_modes.py Python constants mirroring rawes.lua mode/substate numbers. + +scripts/ +├── rawes.lua Unified Lua controller (RAWES_MODE: 0=none 1=steady 3=passive 4=landing). +└── rawes_test_surface.lua Test-surface table (_rawes_fns) splicing internal locals + for Python unit tests via lupa. + +arduloop/ ArduPilot GUIDED/rate control Python port (self-contained package). + guided.py: GuidedAttitudeController (input_quaternion + sqrt P-ctrl). + attitude_heli.py: HeliRateController (rate PIDs + swash output). + params.py: HeliParams, RateAxisParams (1:1 ArduPilot param names). + Used by MockArdupilot._LuaBackend and _MockArdupilotBase.step_physics(). + +analysis/ +├── flight_log.py Unified data loader — FlightLog.load(log_dir), FlightLog.buckets(bucket_s), +│ FlightEvent, Bucket; reads all log sources into one structure. +├── analyse_run.py Post-run report: print_flight_report, compute_steady_metrics, +│ validate_ekf_window; CLI: --bucket S. +├── analyse_landing.py Landing diagnosis (alt/vz/winch/tension/collective per bucket). +├── pump_envelope.py Pumping cycle envelope sweep (tension setpoints, wind, tilt). +└── pump_diagnosis.py Per-bucket compact summary + CSV (osc, corr) to test log dir. + +viz3d/ +├── visualize_3d.py Interactive 3D playback of any telemetry.csv (default viz tool). +├── scrub.py Interactive frame scrubber. +├── render_cycle.py Off-screen render to MP4 / GIF. +├── telemetry.py TelemetryFrame dataclass + CSVSource / LiveQueueSource protocol. +├── visualize_torque.py Torque telemetry 3-panel replay. +└── torque_telemetry.py TorqueTelemetryFrame dataclass. + +tests/ +├── unit/ Windows native, no Docker (~685 fast unit tests; no simtests). +│ └── README.md Unit & simtest reference guide. +├── simtests/ Windows native, no Docker (~13 full physics simulation tests; marker: simtest). +│ ├── conftest.py simtest marker registration; 600 s auto-timeout. +│ ├── simtest_runner.py PhysicsRunner — thin wrapper around PhysicsCore. +│ │ HeliCyclicController baked in. Two step methods: +│ │ step(dt, col, rate_roll, rate_pitch, omega_body) — Python AP path; +│ │ step_guided(dt, col, heli_out) — GUIDED path (HeliRateOutput from +│ │ GuidedAttitudeController). Re-exports MockArdupilot from +│ │ tests/common/mock_ardupilot.py. +│ └── simtest_ic.py load_ic() — loads steady_state_starting.json. +├── common/ +│ └── mock_ardupilot.py MockArdupilot — public adapter for simtests. Two factory methods: +│ MockArdupilot.for_lua(sim, wind, dt, initial_thrust=...) — Lua backend +│ wraps RawesLua; reads guided_target/_rate_target/_throttle from +│ _mock and feeds GuidedAttitudeController each tick; +│ MockArdupilot.for_python(mode=..., wind, dt, **kwargs) — Python +│ AP backend (calls the selected mode step each tick). +│ Shared base (_MockArdupilotBase): enable_guided(), step_physics(), +│ log(), write_telemetry(). TelRow.from_physics() written at +│ RAWES_TEL_HZ (default 20 Hz, override via env var). +└── sitl/ Docker; all SITL/stack tests live here. + ├── conftest.py thin re-exporter — pytest_addoption + pytest_configure only. + ├── stack_infra.py StackConfig, SitlContext, _sitl_stack, _acro_stack, _torque_stack. + ├── stack_utils.py port checks, log copy, mediator launcher. + ├── rawes_sitl_defaults.parm boot-time ArduPilot params (EEPROM defaults). + ├── flight/ flight stack tests (mediator + physics + ArduPilot). + └── torque/ torque/anti-rotation tests. ``` **Data flow (400 Hz):** SITL servo PWM → `ardupilot_h3_120_inverse` swashplate mix → **PhysicsCore** (`dynbem.create_aero` quasi_static + TetherModel + RK4 + spin ODE + yaw damping) → `sensor.py` packet → SITL. `mediator.py` is a thin wrapper; `PhysicsCore` (`physics_core.py`) owns the integration loop. diff --git a/design/sitl_flight_timeline.md b/design/sitl_flight_timeline.md index 18edf39..f8593f0 100644 --- a/design/sitl_flight_timeline.md +++ b/design/sitl_flight_timeline.md @@ -4,7 +4,7 @@ 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/`. +- SITL stack flight tests under `tests/sitl/flight/`. - Steady, passive, pumping, and landing variants that use IC-start fixtures. Out of scope: @@ -132,8 +132,8 @@ When reporting first divergence, include: 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`. +- Shared fixture path: `tests/sitl/flight/conftest.py` (`_ic_trapezoid_stack`). +- Primary diagnosis workflow: `design/sitl_testing.md` and `analysis/diagnose_sitl.py`. ## Validation checklist (after timeline-related changes) diff --git a/design/sitl_testing.md b/design/sitl_testing.md index f1641df..9ebc78b 100644 --- a/design/sitl_testing.md +++ b/design/sitl_testing.md @@ -2,7 +2,7 @@ Everything you need to **run, diagnose, and reason about RAWES SITL stack tests** — the full-stack (ArduPilot + Lua) Docker tests under -[simulation/tests/sitl/](../simulation/tests/sitl/). Read this whenever you are +[tests/sitl/](../tests/sitl/). Read this whenever you are editing or diagnosing a SITL stack test; it is intentionally kept out of the always-on [AGENTS.md](../AGENTS.md) context. @@ -57,7 +57,7 @@ runs in its own fresh Docker container, one per test file. decision.** ``` -.venv/Scripts/python.exe simulation/analysis/diagnose_sitl.py +.venv/Scripts/python.exe analysis/diagnose_sitl.py ``` It answers the two gating questions in order: @@ -84,7 +84,7 @@ all log sources (telemetry CSV, mavlink.jsonl, mediator.log, arducopter.log) int a unified `FlightLog` and prints a single bucketed report. ``` -.venv/Scripts/python.exe simulation/analysis/analyse_run.py # --bucket 10 coarse, --bucket 1 frame-level +.venv/Scripts/python.exe analysis/analyse_run.py # --bucket 10 coarse, --bucket 1 frame-level ``` **Fix telemetry/logging before diagnosing physics.** If telemetry columns are @@ -95,8 +95,8 @@ first — diagnosing from bad telemetry produces wrong conclusions. | Task | Command | |------|---------| -| Pump cycle diagnosis | `.venv/Scripts/python.exe simulation/analysis/pump_diagnosis.py --test test_pump_cycle_unified --bucket 1` | -| Landing diagnosis | `.venv/Scripts/python.exe simulation/analysis/analyse_landing.py [--test test_landing_lua_sitl] [--bucket 2]` | +| Pump cycle diagnosis | `.venv/Scripts/python.exe analysis/pump_diagnosis.py --test test_pump_cycle_unified --bucket 1` | +| Landing diagnosis | `.venv/Scripts/python.exe analysis/analyse_landing.py [--test test_landing_lua_sitl] [--bucket 2]` | | Visualize result | `visualize.cmd simulation/logs//telemetry.csv` | | EKF gating reference | [design/EKF_GATING.md](EKF_GATING.md), [design/ekf_const_pos_mode.md](ekf_const_pos_mode.md) | @@ -137,7 +137,7 @@ contract above. > unit/simtest build their trajectory from it. Every SITL **flight** fixture that > must *start at the IC* goes through the single shared helper > `_ic_trapezoid_stack` in -> [simulation/tests/sitl/flight/conftest.py](../simulation/tests/sitl/flight/conftest.py). +> [tests/sitl/flight/conftest.py](../tests/sitl/flight/conftest.py). > **Do not** fork or re-derive a kinematic trajectory inside a test. If a test > needs to start at the IC, call the shared fixture; if it needs a different > profile, change the shared implementation (and this doc), do not copy it. @@ -153,8 +153,8 @@ contract above. | Driver | `KinematicStartup` | [kinematic.py](../simulation/kinematic.py) | Wraps a `traj_fn(t)->(pos,vel)` (+ optional `R_fn(t)->R`); `state_at(t)` returns the held kinematic state | | Production wiring | mediator startup block | [mediator.py](../simulation/mediator.py) (`_kin_duration`, `make_smooth_trapezoid_traj`, `KinematicStartup`) | Builds the trajectory from config and feeds the SITL sensor stream | | Config knobs | `startup_damp_seconds`, `kinematic_cruise_speed`, `kinematic_accel_s`, `kinematic_decel_s`, `kinematic_vel_ramp_s`, `kinematic_aero_mode` | [config.py](../simulation/config.py) | Default profile; overridden per-fixture | -| Central IC fixture | `_ic_trapezoid_stack` | [flight/conftest.py](../simulation/tests/sitl/flight/conftest.py) | The **only** entry point for "start at the IC" SITL flight tests | -| SITL setup sequence | stack setup helpers (6 steps) | [stack_infra.py](../simulation/tests/sitl/stack_infra.py) | Connect → params → EKF tilt align → arm → confirm GUIDED_NOGPS | +| Central IC fixture | `_ic_trapezoid_stack` | [flight/conftest.py](../tests/sitl/flight/conftest.py) | The **only** entry point for "start at the IC" SITL flight tests | +| SITL setup sequence | stack setup helpers (6 steps) | [stack_infra.py](../tests/sitl/stack_infra.py) | Connect → params → EKF tilt align → arm → confirm GUIDED_NOGPS | The trapezoid path is selected whenever `kinematic_cruise_speed > 0`; otherwise the linear fallback path is used. With dual GPS (`EK3_SRC1_YAW=2`, RELPOSNED @@ -253,7 +253,7 @@ hand-off: ``` bash test.sh stack -n 1 -k test_lua_flight_steady_sitl -.venv/Scripts/python.exe simulation/analysis/diagnose_sitl.py test_lua_flight_steady_sitl +.venv/Scripts/python.exe analysis/diagnose_sitl.py test_lua_flight_steady_sitl ``` - **CHECK 1** — EKF GPS-aiding (out of `const_pos_mode`, `yawAlignComplete` @@ -262,7 +262,7 @@ bash test.sh stack -n 1 -k test_lua_flight_steady_sitl vs `steady_state_starting.json`. The Windows-native guard for the trajectory math is -[simulation/tests/unit/test_startup_trajectory.py](../simulation/tests/unit/test_startup_trajectory.py) +[tests/unit/test_startup_trajectory.py](../tests/unit/test_startup_trajectory.py) (`make_smooth_trapezoid_traj` ends at `pos0` with zero velocity, continuous accel). --- diff --git a/design/testing.md b/design/testing.md index 159fbd7..16e1eac 100644 --- a/design/testing.md +++ b/design/testing.md @@ -14,19 +14,19 @@ Tests are split across three directories: ```bash # Fast unit tests only (~685) -.venv/Scripts/python.exe -m pytest simulation/tests/unit -m "not simtest" -q +.venv/Scripts/python.exe -m pytest tests/unit -m "not simtest" -q # Simtests only (~13) -.venv/Scripts/python.exe -m pytest simulation/tests/simtests -m simtest -q +.venv/Scripts/python.exe -m pytest tests/simtests -m simtest -q # Both together -.venv/Scripts/python.exe -m pytest simulation/tests/unit simulation/tests/simtests -q +.venv/Scripts/python.exe -m pytest tests/unit tests/simtests -q # Single test -.venv/Scripts/python.exe -m pytest simulation/tests/simtests/test_steady_flight.py -q +.venv/Scripts/python.exe -m pytest tests/simtests/test_steady_flight.py -q # Regenerate steady-state IC (required after any aero model change) -.venv/Scripts/python.exe -m pytest simulation/tests/simtests/test_generate_ic.py::test_create_ic -s +.venv/Scripts/python.exe -m pytest tests/simtests/test_generate_ic.py::test_create_ic -s ``` **CRITICAL:** Unit tests run via the Windows venv (`.venv/Scripts/python.exe -m pytest ...`) — never inside Docker, which excludes `tests/unit`. @@ -40,7 +40,7 @@ Tests are split across three directories: | *(none)* | Fast unit test, no physics loop | | `simtest` | Full time-domain physics loop — seconds of compute (auto-timeout 600 s) | -The `simtest` timeout is set globally in `simulation/pytest.ini`. +The `simtest` timeout is set globally in `pyproject.toml` (`[tool.pytest.ini_options]`). --- @@ -176,7 +176,7 @@ embedded in Python). No SITL, no Docker, no real sleeping. |------|----------|---------| | `rawes_lua_harness.py` | `simulation/` root | `RawesLua` class — Python interface to rawes.lua | | `mock_ardupilot.lua` | `simulation/` root | ArduPilot API stub (AHRS, RC, SRV_Channels, param, gcs, arming, vehicle) | -| `rawes_test_surface.lua` | `simulation/scripts/` | Test surface spliced into rawes.lua — exposes internals via `_rawes_fns` | +| `rawes_test_surface.lua` | `scripts/` | Test surface spliced into rawes.lua — exposes internals via `_rawes_fns` | | `rawes_modes.py` | `simulation/` root | Central Python constants mirroring rawes.lua mode/substate numbers | `rawes_lua_harness.py` lives in `simulation/` (not in a test subdirectory) because it is imported @@ -205,8 +205,8 @@ _mock = global state table (inputs/outputs bridged to Python) ### RawesLua API ```python -from rawes_lua_harness import RawesLua -from rawes_modes import MODE_STEADY, PUMP_HOLD +from simulation.rawes_lua_harness import RawesLua +from simulation.rawes_modes import MODE_STEADY, PUMP_HOLD sim = RawesLua(mode=MODE_STEADY) # RAWES_MODE = 1 (pumping schedule runs in steady) sim.armed = True @@ -266,7 +266,7 @@ sim.vec_to_list(bz) # -> [x, y, z] Constants are in `simulation/rawes_modes.py` (Python) and as locals in `rawes.lua` (Lua). ```python -from rawes_modes import ( +from simulation.rawes_modes import ( MODE_NONE, MODE_STEADY, MODE_LANDING, LAND_DESCEND, LAND_FINAL_DROP, @@ -334,7 +334,7 @@ ic = load_ic() Regenerate after any aero model change: ```bash -.venv/Scripts/python.exe -m pytest simulation/tests/simtests/test_generate_ic.py::test_create_ic -s +.venv/Scripts/python.exe -m pytest tests/simtests/test_generate_ic.py::test_create_ic -s ``` --- diff --git a/dockerstup.sh b/dockerstup.sh new file mode 100644 index 0000000..6299340 --- /dev/null +++ b/dockerstup.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash + +# Exit immediately if a command exits with a non-zero status +set -e + +echo "=== Starting Docker Installation for WSL2 ===" + +# 1. Update system and install required system tools +echo "--> Updating packages and installing prerequisites..." +sudo apt-get update -y +sudo apt-get install -y ca-certificates curl gnupg lsb-release + +# 2. Set up Docker's official GPG key securely +echo "--> Adding Docker GPG key..." +sudo install -m 0755 -d /etc/apt/keyrings +curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg +sudo chmod a+r /etc/apt/keyrings/docker.gpg + +# 3. Add the stable Docker repository to apt sources +echo "--> Setting up the stable repository..." +echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ + https://download.docker.com/linux/ubuntu \ + $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ + sudo tee /etc/apt/sources.list.d/docker.list > /dev/null + +# 4. Install Docker packages +echo "--> Installing Docker Engine, Containerd, and Compose..." +sudo apt-get update -y +sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin + +# 5. Grant Docker permissions to the current user +echo "--> Adding user '$USER' to the docker group..." +sudo usermod -aG docker "$USER" + +# 6. Start the Docker service based on available init system +echo "--> Initializing Docker service..." +if pidof systemd >/dev/null; then + echo " Systemd detected. Enabling and starting Docker daemon..." + sudo systemctl enable docker + sudo systemctl start docker +else + echo " Classic init detected. Starting Docker service manually..." + sudo service docker start +fi + +echo "=========================================================" +echo " Installation complete!" +echo " IMPORTANT: Run 'newgrp docker' or restart your terminal" +echo " to apply permissions before running docker without sudo." +echo "=========================================================" diff --git a/simulation/envelope/CLAUDE.md b/envelope/CLAUDE.md similarity index 91% rename from simulation/envelope/CLAUDE.md rename to envelope/CLAUDE.md index 27594bb..b055eea 100644 --- a/simulation/envelope/CLAUDE.md +++ b/envelope/CLAUDE.md @@ -84,19 +84,19 @@ All tests passing after cleanup (117 pass, 27 skip): ```bash # Quick preview (coarse grid, ~90 s) -.venv/Scripts/python.exe simulation/envelope/compute_map.py --quick --save simulation/envelope/map_quick.npz +.venv/Scripts/python.exe envelope/compute_map.py --quick --save envelope/map_quick.npz # Full grid (fine grid, hours) -.venv/Scripts/python.exe simulation/envelope/compute_map.py --full --save simulation/envelope/map_full.npz +.venv/Scripts/python.exe envelope/compute_map.py --full --save envelope/map_full.npz # Reload saved grid and render -.venv/Scripts/python.exe simulation/envelope/compute_map.py --load simulation/envelope/map_quick.npz +.venv/Scripts/python.exe envelope/compute_map.py --load envelope/map_quick.npz # Sweep wind speeds with pump_envelope.py (main CLAUDE.md tool) -.venv/Scripts/python.exe simulation/analysis/pump_envelope.py --wind 8 10 12 +.venv/Scripts/python.exe analysis/pump_envelope.py --wind 8 10 12 ``` -PNG output lands in `simulation/envelope/plots/` when `--save` is specified. +PNG output lands in `envelope/plots/` when `--save` is specified. --- diff --git a/envelope/__init__.py b/envelope/__init__.py new file mode 100644 index 0000000..e259878 --- /dev/null +++ b/envelope/__init__.py @@ -0,0 +1,6 @@ +"""envelope -- pumping-cycle envelope sweep (point-mass tension/collective solver). + +Public modules: + envelope.point_mass -- tether_hat(), balance_bz(), simulate_point() + envelope.compute_map -- compute_grid() sweep over wind/tension/tilt +""" diff --git a/simulation/envelope/analyse_envelope.py b/envelope/analyse_envelope.py similarity index 96% rename from simulation/envelope/analyse_envelope.py rename to envelope/analyse_envelope.py index b26a5f0..4cc628f 100644 --- a/simulation/envelope/analyse_envelope.py +++ b/envelope/analyse_envelope.py @@ -20,11 +20,11 @@ Usage ----- - python simulation/envelope/analyse_envelope.py --phase reel_out - python simulation/envelope/analyse_envelope.py --phase reel_in - python simulation/envelope/analyse_envelope.py --el 80 --tension 200 --wind 10 - python simulation/envelope/analyse_envelope.py --el 80 --tension 200 --wind 10 --col 0.08 - python simulation/envelope/analyse_envelope.py --envelope --wind 10 + python envelope/analyse_envelope.py --phase reel_out + python envelope/analyse_envelope.py --phase reel_in + python envelope/analyse_envelope.py --el 80 --tension 200 --wind 10 + python envelope/analyse_envelope.py --el 80 --tension 200 --wind 10 --col 0.08 + python envelope/analyse_envelope.py --envelope --wind 10 """ from __future__ import annotations @@ -35,7 +35,6 @@ import numpy as np -sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from envelope.point_mass import simulate_point, balance_bz, tether_hat diff --git a/simulation/envelope/compute_map.py b/envelope/compute_map.py similarity index 97% rename from simulation/envelope/compute_map.py rename to envelope/compute_map.py index a7a5390..a7740bc 100644 --- a/simulation/envelope/compute_map.py +++ b/envelope/compute_map.py @@ -7,10 +7,10 @@ Usage ----- - python simulation/envelope/compute_map.py --quick # fast preview - python simulation/envelope/compute_map.py --full # fine grid, save map - python simulation/envelope/compute_map.py --quick --wind 10 - python simulation/envelope/compute_map.py --load map.npz # reload saved grid + python envelope/compute_map.py --quick # fast preview + python envelope/compute_map.py --full # fine grid, save map + python envelope/compute_map.py --quick --wind 10 + python envelope/compute_map.py --load map.npz # reload saved grid """ from __future__ import annotations @@ -23,7 +23,6 @@ import numpy as np -sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from envelope.point_mass import simulate_point @@ -43,7 +42,7 @@ def _ramp_full_column(args: tuple) -> list: vi, wi, ai, v_target, wind, angle, v_tension_list, dt, tension_list_unified = args import rotor_definition as rd from dynbem import create_aero - from frames import build_orb_frame + from simulation.frames import build_orb_frame from envelope.point_mass import tether_hat, balance_bz rotor = rd.default() diff --git a/simulation/envelope/grid_quick.npz b/envelope/grid_quick.npz similarity index 100% rename from simulation/envelope/grid_quick.npz rename to envelope/grid_quick.npz diff --git a/simulation/envelope/grid_quick_converged.npz b/envelope/grid_quick_converged.npz similarity index 100% rename from simulation/envelope/grid_quick_converged.npz rename to envelope/grid_quick_converged.npz diff --git a/simulation/envelope/map_full.npz b/envelope/map_full.npz similarity index 100% rename from simulation/envelope/map_full.npz rename to envelope/map_full.npz diff --git a/simulation/envelope/map_quick.npz b/envelope/map_quick.npz similarity index 100% rename from simulation/envelope/map_quick.npz rename to envelope/map_quick.npz diff --git a/simulation/envelope/map_quick_asymmetric.npz b/envelope/map_quick_asymmetric.npz similarity index 100% rename from simulation/envelope/map_quick_asymmetric.npz rename to envelope/map_quick_asymmetric.npz diff --git a/simulation/envelope/point_mass.py b/envelope/point_mass.py similarity index 99% rename from simulation/envelope/point_mass.py rename to envelope/point_mass.py index 62bd77a..c249bf2 100644 --- a/simulation/envelope/point_mass.py +++ b/envelope/point_mass.py @@ -25,11 +25,10 @@ import numpy as np -sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from dynbem import rotor_definition as rd from dynbem import create_aero -from frames import build_orb_frame +from simulation.frames import build_orb_frame _G = 9.81 diff --git a/pump_lua.cmd b/pump_lua.cmd index febda2b..5b126c4 100644 --- a/pump_lua.cmd +++ b/pump_lua.cmd @@ -51,13 +51,13 @@ if not defined RAWES_PUMP_TIMEOUT ( if defined RAWES_PUMP_TIMEOUT echo Test timeout: %RAWES_PUMP_TIMEOUT% s ^(RAWES_PUMP_TIMEOUT^) echo Running Lua pumping simtest (test_lua_pumping_unified) ... -"%PY%" simulation\run_tests.py simulation\tests\simtests -k test_lua_pumping_unified -m simtest -s +"%PY%" simulation\run_tests.py tests\simtests -k test_lua_pumping_unified -m simtest -s set "RC=%ERRORLEVEL%" if not defined NOVIS ( if exist "%CSV%" ( echo Launching visualization: %CSV% - start "RAWES pumping (Lua)" cmd /k ""%PY%" simulation\viz3d\visualize_3d.py "%CSV%"" + start "RAWES pumping (Lua)" cmd /k ""%PY%" viz3d\visualize_3d.py "%CSV%"" ) else ( echo Telemetry not found: %CSV% ) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3cb9ebf --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,43 @@ +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.build_meta" + +[project] +name = "rawes" +version = "0.1.0" +description = "RAWES tethered autorotating rotor kite -- simulation, flight-stack, and analysis tooling" +requires-python = ">=3.10" + +# Runtime dependencies are NOT declared here on purpose: simulation/requirements.txt +# (installed via setup.cmd/setup.sh, hash-gated) remains the single source of truth +# for the venv and the Docker image. This pyproject.toml exists so that `pip install +# -e .` registers the first-party packages below on sys.path -- no dependency +# resolution is involved. + +[tool.setuptools.packages.find] +where = ["."] +include = [ + "simulation", + "simulation.*", + "arduloop", + "arduloop.*", + "calibrate", + "calibrate.*", + "envelope", + "envelope.*", + "analysis", + "analysis.*", + "viz3d", + "viz3d.*", + "scripts", + "scripts.*", + "tests", + "tests.*", +] + +[tool.pytest.ini_options] +timeout = 600 +markers = [ + "simtest: full physics simulation (seconds-minutes); excluded from quick unit run", + "sitl: ArduPilot SITL integration test (Docker); run via test.sh stack", +] diff --git a/pyrightconfig.json b/pyrightconfig.json index dbd57c9..9c4eb36 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -1,15 +1,20 @@ { "include": [ - "simulation" + "simulation", + "arduloop", + "envelope", + "analysis", + "viz3d", + "scripts", + "tests" ], "exclude": [ "**/__pycache__", - "simulation/.pytest_cache", + ".pytest_cache", ".venv", "simulation/logs" ], "extraPaths": [ - "simulation", "../aero/dynbem/python" ], "venvPath": ".", diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 0000000..91c032d --- /dev/null +++ b/scripts/__init__.py @@ -0,0 +1 @@ +"""scripts — calibration and hardware bench utilities.""" diff --git a/simulation/scripts/analyze_tuning.py b/scripts/analyze_tuning.py similarity index 100% rename from simulation/scripts/analyze_tuning.py rename to scripts/analyze_tuning.py diff --git a/simulation/scripts/query_hardware.py b/scripts/query_hardware.py similarity index 99% rename from simulation/scripts/query_hardware.py rename to scripts/query_hardware.py index 7159a91..a419675 100644 --- a/simulation/scripts/query_hardware.py +++ b/scripts/query_hardware.py @@ -3,7 +3,7 @@ query_hardware.py -- Query Pixhawk 6C over MAVLink and write hardware.md. Usage: - RAWES_HIL_PORT=COM4 .venv/Scripts/python.exe simulation/scripts/query_hardware.py + RAWES_HIL_PORT=COM4 .venv/Scripts/python.exe scripts/query_hardware.py """ from __future__ import annotations import datetime, math, os, struct, sys, threading, time diff --git a/simulation/scripts/rawes.lua b/scripts/rawes.lua similarity index 100% rename from simulation/scripts/rawes.lua rename to scripts/rawes.lua diff --git a/simulation/scripts/rawes_params.json b/scripts/rawes_params.json similarity index 100% rename from simulation/scripts/rawes_params.json rename to scripts/rawes_params.json diff --git a/simulation/scripts/rawes_test_surface.lua b/scripts/rawes_test_surface.lua similarity index 100% rename from simulation/scripts/rawes_test_surface.lua rename to scripts/rawes_test_surface.lua diff --git a/simulation/scripts/rc_hold.py b/scripts/rc_hold.py similarity index 100% rename from simulation/scripts/rc_hold.py rename to scripts/rc_hold.py diff --git a/simulation/scripts/sitl_bench.py b/scripts/sitl_bench.py similarity index 89% rename from simulation/scripts/sitl_bench.py rename to scripts/sitl_bench.py index d0fc69b..4835de4 100644 --- a/simulation/scripts/sitl_bench.py +++ b/scripts/sitl_bench.py @@ -1,19 +1,19 @@ #!/usr/bin/env python3 """ -sitl_bench.py -- Interactive SITL bench session for calibrate.py. +sitl_bench.py -- Interactive SITL bench session for calibrate. Boots the same stack as the torque stack tests (SITL --model JSON + mediator_torque.py) but with omega_rotor=0 (rotor stationary) and a very long startup hold so the mediator never transitions to DYNAMIC. -This lets calibrate.py connect over tcp:localhost:5760 from the Windows host +This lets calibrate connect over tcp:localhost:5760 from the Windows host and exercise rawes.lua / servo commands / arming exactly as on hardware. Usage (inside a rawes-sim container with MAVLink port 5760 published): - /rawes/.venv/bin/python3 /rawes/simulation/scripts/sitl_bench.py [--omega-rotor RAD_S] + /rawes/.venv/bin/python3 /rawes/scripts/sitl_bench.py [--omega-rotor RAD_S] Connect from Windows host (second terminal): - calibrate.py --port sitl + python -m calibrate --port sitl """ from __future__ import annotations @@ -30,15 +30,12 @@ # --------------------------------------------------------------------------- # Path setup — reach simulation/ and tests/sitl/ from any CWD # --------------------------------------------------------------------------- -_SCRIPTS_DIR = Path(__file__).resolve().parent # simulation/scripts/ -_SIM_DIR = _SCRIPTS_DIR.parent # simulation/ -_SITL_DIR = _SIM_DIR / "tests" / "sitl" +_SCRIPTS_DIR = Path(__file__).resolve().parent # scripts/ +_REPO_ROOT = _SCRIPTS_DIR.parent # repo root +_SIM_DIR = _REPO_ROOT / "simulation" # simulation/ +_SITL_DIR = _REPO_ROOT / "tests" / "sitl" # tests/sitl/ -for _p in (str(_SIM_DIR), str(_SITL_DIR)): - if _p not in sys.path: - sys.path.insert(0, _p) - -from stack_utils import ( # noqa: E402 +from tests.sitl.stack_utils import ( # noqa: E402 ParamSetup, _launch_sitl, _prime_sitl_eeprom, @@ -84,8 +81,8 @@ "H_RSC_RUNUP_TIME": 2, # Lua scripting "SCR_ENABLE": 1, - "RAWES_MODE": 0, # MODE_NONE at boot; calibrate.py sets mode - # Arming — skip all prearm checks so force-arm from calibrate.py works + "RAWES_MODE": 0, # MODE_NONE at boot; calibrate sets mode + # Arming — skip all prearm checks so force-arm from calibrate works "ARMING_SKIPCHK": 65535, } @@ -147,7 +144,7 @@ def main() -> None: f"(omega_rotor={args.omega_rotor:.2f} rad/s = " f"{args.omega_rotor * 60 / (2 * math.pi):.0f} RPM) ...") mediator_proc = _launch_mediator_torque( - _SIM_DIR, _SIM_DIR.parent, + _SIM_DIR, _REPO_ROOT, mediator_log, args.omega_rotor, profile="constant", tail_channel=3, @@ -159,7 +156,7 @@ def main() -> None: print(" SITL running on tcp:0.0.0.0:5760") print() print(" Connect from Windows host (second terminal):") - print(" calibrate.py --port sitl") + print(" python -m calibrate --port sitl") print() print(f" Logs: {tmp}/") print(" Ctrl-C to stop.") diff --git a/simulation/scripts/telemetry_maintenance_audit.py b/scripts/telemetry_maintenance_audit.py similarity index 97% rename from simulation/scripts/telemetry_maintenance_audit.py rename to scripts/telemetry_maintenance_audit.py index 76ab68a..10dbffb 100644 --- a/simulation/scripts/telemetry_maintenance_audit.py +++ b/scripts/telemetry_maintenance_audit.py @@ -22,6 +22,8 @@ from pathlib import Path from typing import Iterable +import simulation + EXCLUDED_DIRS = { ".git", @@ -131,9 +133,9 @@ def _classify_kind(rel_path: str, line_text: str, column: str) -> str: return "schema-support" if "simulation/mediator.py" in rel_path and f'fields["{column}"]' in t: return "producer-mediator" - if "simulation/" in rel_path and "analysis/" in rel_path: + if rel_path.startswith("analysis/"): return "consumer-analysis" - if "simulation/tests/" in rel_path: + if rel_path.startswith("tests/"): return "consumer-test" if "telemetry" in t.lower() or "csv" in t.lower(): return "consumer-generic" @@ -246,13 +248,13 @@ def main() -> int: parser.add_argument( "--repo-root", type=Path, - default=Path(__file__).resolve().parents[2], + default=Path(__file__).resolve().parents[1], help="Repository root (default: inferred from script location)", ) parser.add_argument( "--out-dir", type=Path, - default=Path(__file__).resolve().parents[1] / "logs" / "telemetry_maintenance", + default=Path(simulation.__file__).resolve().parent / "logs" / "telemetry_maintenance", help="Output directory", ) parser.add_argument( diff --git a/setup.sh b/setup.sh index 4ad5c8a..fe26878 100644 --- a/setup.sh +++ b/setup.sh @@ -94,9 +94,9 @@ _setup_hw() { exit 1 fi - # Reuse calibrate.py as the canonical hardware param writer. + # Reuse calibrate (python -m calibrate) as the canonical hardware param writer. # This checks all expected params from rawes_params.json and writes DIFFs. - "$PYTHON" "$(_winpath "$SIM_DIR/scripts/calibrate.py")" \ + "$PYTHON" -m calibrate \ --port "$RAWES_HIL_PORT" \ --baud "${RAWES_HIL_BAUD:-115200}" \ config apply diff --git a/setupdocker.sh b/setupdocker.sh new file mode 100644 index 0000000..fd9430f --- /dev/null +++ b/setupdocker.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash + +# Exit immediately if a command exits with a non-zero status +set -e + +echo "=== Starting Docker Installation for WSL2 ===" + +# 1. Update system and install required system tools +echo "--> Updating packages and installing prerequisites..." +sudo apt-get update -y +sudo apt-get install -y ca-certificates curl gnupg lsb-release + +# 2. Set up Docker's official GPG key securely +echo "--> Adding Docker GPG key..." +sudo install -m 0755 -d /etc/apt/keyrings +curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg +sudo chmod a+r /etc/apt/keyrings/docker.gpg + +# 3. Add the stable Docker repository to apt sources +echo "--> Setting up the stable repository..." +echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ + https://download.docker.com/linux/ubuntu \ + $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ + sudo tee /etc/apt/sources.list.d/docker.list > /dev/null + +# 4. Install Docker packages +echo "--> Installing Docker Engine, Containerd, and Compose..." +sudo apt-get update -y +sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin + +# 5. Grant Docker permissions to the current user +echo "--> Adding user '$USER' to the docker group..." +sudo usermod -aG docker "$USER" + +# 6. Start the Docker service based on available init system +echo "--> Initializing Docker service..." +if pidof systemd >/dev/null; then + echo " Systemd detected. Enabling and starting Docker daemon..." + sudo systemctl enable docker + sudo systemctl start docker +else + echo " Classic init detected. Starting Docker service manually..." + sudo service docker start +fi + +echo "=========================================================" +echo " Installation complete!" +echo " IMPORTANT: Run 'newgrp docker' or restart your terminal" +echo " to apply permissions before running docker without sudo." +echo "=========================================================" \ No newline at end of file diff --git a/simulation/README.md b/simulation/README.md index 388e926..117d4f8 100644 --- a/simulation/README.md +++ b/simulation/README.md @@ -95,7 +95,7 @@ Generation and acceptance criteria are documented in [../design/testing.md](../d ### Unit tests (Windows, no Docker) ```bash -.venv/Scripts/python.exe -m pytest simulation/tests/unit -m "not simtest" -q +.venv/Scripts/python.exe -m pytest tests/unit -m "not simtest" -q ``` Key unit tests: @@ -122,10 +122,10 @@ Standalone scripts in `analysis/`. Not part of the simulation runtime; not impor ```bash # List available test runs (newest first) -.venv/Scripts/python.exe simulation/analysis/analyse_run.py +.venv/Scripts/python.exe analysis/analyse_run.py # Analyse a specific test -.venv/Scripts/python.exe simulation/analysis/analyse_run.py test_acro_armed -.venv/Scripts/python.exe simulation/analysis/analyse_run.py test_pumping_cycle --plot +.venv/Scripts/python.exe analysis/analyse_run.py test_acro_armed +.venv/Scripts/python.exe analysis/analyse_run.py test_pumping_cycle --plot ``` | Script | Purpose | Status | diff --git a/simulation/__init__.py b/simulation/__init__.py new file mode 100644 index 0000000..3bd120a --- /dev/null +++ b/simulation/__init__.py @@ -0,0 +1,13 @@ +"""simulation — RAWES core physics/flight-stack package. + +Physics engine, ArduPilot mediator, control-law primitives, and shared +data models for the RAWES tethered autorotating rotor kite. + +Sibling top-level packages (installed from the same repo root): + arduloop — ArduPilot GUIDED/rate control loop Python port + envelope — pumping envelope / point-mass sweep tool + analysis — SITL/simtest telemetry diagnostics + viz3d — 3D telemetry visualizers + scripts — calibration / hardware bench scripts + tests — all pytest suites (unit, simtests, sitl, hil, oneoff) +""" diff --git a/simulation/comms.py b/simulation/comms.py index 5b35b0a..ef5daf9 100644 --- a/simulation/comms.py +++ b/simulation/comms.py @@ -47,8 +47,8 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from pumping_planner import TensionCommand - from gcs import GCSClient + from simulation.pumping_planner import TensionCommand + from simulation.gcs import GCSClient # --------------------------------------------------------------------------- diff --git a/simulation/config.py b/simulation/config.py index 1546001..326368a 100644 --- a/simulation/config.py +++ b/simulation/config.py @@ -12,7 +12,7 @@ Use ``defaults()`` to get a fresh copy of the defaults dict. Example (test fixture): - import config as mcfg + import simulation.config as mcfg cfg = mcfg.defaults() cfg["base_k_ang"] = 0.0 cfg_path = tmp_path / "mediator_config.json" @@ -20,7 +20,7 @@ # pass --config str(cfg_path) to mediator subprocess Example (mediator): - import config as mcfg + import simulation.config as mcfg cfg = mcfg.load(args.config) # args.config may be None → pure defaults """ @@ -33,7 +33,7 @@ # the mediator config uses the exact same IC as simtests and the SITL stack. # The JSON is written only by test_generate_ic.py::test_create_ic. # --------------------------------------------------------------------------- -import ic as _ic +import simulation.ic as _ic _ss: dict = _ic.load_ic_dict() @@ -289,7 +289,6 @@ def make_trajectory(cfg: dict, wind_ned): Mode_RAWES (mediator), not in the trajectory planner. """ import sys, os - sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) - from planner import HoldPlanner + from simulation.planner import HoldPlanner return HoldPlanner() diff --git a/simulation/controller.py b/simulation/controller.py index 427adf2..022811d 100644 --- a/simulation/controller.py +++ b/simulation/controller.py @@ -8,12 +8,12 @@ import math import numpy as np -from frames import build_orb_frame, cross3 # noqa: F401 — build_orb_frame re-exported for callers -from servo_pwm import SWASH_PWM_NEUTRAL, SWASH_PWM_RANGE -from swashplate import SwashplateServoModel +from simulation.frames import build_orb_frame, cross3 # noqa: F401 — build_orb_frame re-exported for callers +from simulation.servo_pwm import SWASH_PWM_NEUTRAL, SWASH_PWM_RANGE +from simulation.swashplate import SwashplateServoModel from dynbem import RotorInputs -from param_defaults import load_ap_params as _load_ap_params -from param_defaults import load_collective_phys_range as _load_col_range +from simulation.param_defaults import load_ap_params as _load_ap_params +from simulation.param_defaults import load_collective_phys_range as _load_col_range from arduloop import HeliRateController, HeliParams, RateAxisParams @@ -789,7 +789,7 @@ def compute_bz_altitude_hold( class YawTrimObserver: """Python port of rawes.lua's yaw trim observer (anti-rotation feedforward). - Mirrors ``yaw_trim_step(dt, u, psi_dot)`` in ``simulation/scripts/rawes.lua`` + Mirrors ``yaw_trim_step(dt, u, psi_dot)`` in ``scripts/rawes.lua`` exactly (see ``test_yaw_trim_lua.py`` for the Lua-side unit tests and ``test_yaw_trim_parity.py`` for the cross-check). Keep these two in sync — per AGENTS.md, any change to the Lua formula must be mirrored here in the diff --git a/simulation/envelope/__init__.py b/simulation/envelope/__init__.py deleted file mode 100644 index f21f229..0000000 --- a/simulation/envelope/__init__.py +++ /dev/null @@ -1,201 +0,0 @@ -""" -test_tension_ramp_30deg.py -- Homotopy ramp at el=30 deg, reel-out v_target=-0.8 m/s. - -Step 1: run the standalone ramp (own ODE loop, omega=28, vel=0, 40 s settle at T=25 N, - then ramp at 5 N/s). This is known to track all the way to 1000 N. -Step 2: capture the full IC dict (vel, omega, col, cyclic, integrators, aero_state) - at each 25 N grid crossing. -Step 3: verify each IC is reproducible — restart simulate_point from that IC and - check it immediately stays within tolerance. -""" -import math -import sys -from pathlib import Path - -import numpy as np - -sys.path.insert(0, str(Path(__file__).resolve().parents[2])) - -from dynbem import rotor_definition as rd -from dynbem import create_aero -from frames import build_orb_frame -from envelope.point_mass import tether_hat, balance_bz, simulate_point - -EL = 30.0 -WIND = 10.0 -V_TARGET = -0.8 -OMEGA_INIT = 28.0 -T_SETTLE = 40.0 -RAMP_RATE = 5.0 # N/s -DT = 0.02 - -TENSION_LIST = list(range(25, 1025, 25)) # 25 … 1000 N in 25 N steps - - -# ── standalone ramp (identical physics to the working _run_ramp test) ────────── - -def _run_standalone_ramp(): - """Own ODE loop — omega=28, vel=0, 40 s settle at T=25 N, then ramp to 1000 N.""" - rotor = rd.default() - aero = create_aero(rotor, model="peters_he") - dk = rotor.dynamics_kwargs() - mass = dk["mass"] - I_spin = dk["I_spin"] - weight = mass * 9.81 - clamp = math.radians(8.6) - wind_v = np.array([0.0, -float(WIND), 0.0]) - t_hat = tether_hat(EL) - - omega = float(OMEGA_INIT) - vel = np.zeros(3) - col_now = 0.0 - c_lon = c_lat = 0.0 - int_vx = int_vy = int_vcol = 0.0 - - T_START = float(TENSION_LIST[0]) - T_MAX = float(TENSION_LIST[-1]) - tension = T_START - t_ramp_start = None - next_grid = T_START # next grid tension to sample - - samples = [] # (ti, v_along, col_rad, ic_dict) - diverged_at = None - - total_s = T_SETTLE + (T_MAX - T_START) / RAMP_RATE + 20.0 - total_steps = int(total_s / DT) - - for i in range(total_steps): - t_sim = i * DT - - if t_sim < T_SETTLE: - tension = T_START - else: - if t_ramp_start is None: - t_ramp_start = t_sim - tension = min(T_START + RAMP_RATE * (t_sim - t_ramp_start), T_MAX) - - bz = balance_bz(EL, tension, mass) - R = build_orb_frame(bz) - F_teth = tension * t_hat - - try: - f = aero.compute_forces(col_now, c_lon, c_lat, R, vel.copy(), - omega, wind_v) - except (OverflowError, ValueError, FloatingPointError): - diverged_at = (t_sim, tension) - break - - thrust = float(np.dot(f.F_world, bz)) - Q_net = float(np.dot(aero.last_M_spin, bz)) - F_net = f.F_world + F_teth + np.array([0.0, 0.0, weight]) - - omega = max(0.0, omega + (Q_net / I_spin) * DT) - vel = vel + (F_net / mass) * DT - - if not np.all(np.isfinite(vel)): - diverged_at = (t_sim, tension) - break - - v_perp = vel - float(np.dot(vel, t_hat)) * t_hat - t_norm = max(abs(thrust), 1.0) - c_lon_r = -(0.5 / t_norm * v_perp[0] + 0.1 / t_norm * int_vx) - c_lat_r = -(0.5 / t_norm * v_perp[1] + 0.1 / t_norm * int_vy) - c_lon = float(np.clip(c_lon_r, -clamp, clamp)) - c_lat = float(np.clip(c_lat_r, -clamp, clamp)) - if abs(c_lon) < clamp: - int_vx += v_perp[0] * DT - if abs(c_lat) < clamp: - int_vy += v_perp[1] * DT - - v_along_now = float(np.dot(vel, t_hat)) - err = v_along_now - V_TARGET - int_vcol += err * DT - col_now = float(np.clip(col_now + 0.02 * err + 0.005 * int_vcol, -0.25, 0.20)) - - # sample when tension crosses the next grid point - if t_sim >= T_SETTLE and tension >= next_grid - 0.5 * RAMP_RATE * DT: - ti = TENSION_LIST.index(int(next_grid)) - v_along = float(np.dot(vel, t_hat)) - ic_out = dict( - col = col_now, - vel = vel.copy(), - omega = omega, - c_lon = c_lon, c_lat = c_lat, - int_vx = int_vx, int_vy = int_vy, - int_vcol = int_vcol, - aero_state = aero.to_dict(), - ) - samples.append((ti, v_along, col_now, ic_out)) - next_grid += float(TENSION_LIST[1] - TENSION_LIST[0]) - if next_grid > T_MAX + 0.1: - break - - return samples, diverged_at - - -_RAMP_RESULT = None - -def _get_ramp(): - global _RAMP_RESULT - if _RAMP_RESULT is None: - _RAMP_RESULT = _run_standalone_ramp() - return _RAMP_RESULT - - -# ── tests ────────────────────────────────────────────────────────────────────── - -def test_ramp_tracks_to_1000n(capsys): - """Standalone ramp must not diverge and must track v_target throughout.""" - samples, diverged_at = _get_ramp() - - print(f"\n el={EL:.0f} wind={WIND} v_target={V_TARGET:+.2f} " - f"omega_init={OMEGA_INIT} ramp={RAMP_RATE} N/s") - if diverged_at: - print(f" Diverged at T={diverged_at[1]:.0f} N t={diverged_at[0]:.1f} s") - else: - print(f" Tracked to T={TENSION_LIST[samples[-1][0]]:.0f} N " - f"({len(samples)} grid points)") - - print(f"\n {'T':>6} {'v_along':>8} {'col_deg':>8} {'omega':>7}") - for ti, v_along, col_rad, ic in samples: - marker = " <--" if abs(v_along - V_TARGET) > 0.15 else "" - print(f" {TENSION_LIST[ti]:6.0f} {v_along:+8.3f} " - f"{math.degrees(col_rad):+8.2f} {ic['omega']:.3f}{marker}") - - assert diverged_at is None, f"diverged at T={diverged_at[1]:.0f} N" - bad = [(TENSION_LIST[ti], v_along) for ti, v_along, *_ in samples - if abs(v_along - V_TARGET) > 0.15] - assert not bad, f"lost tracking at {bad}" - - -def test_ics_are_reproducible(capsys): - """Each captured IC must restart cleanly: 5 s sim stays within tolerance.""" - samples, _ = _get_ramp() - failures = [] - - print(f"\n Verifying {len(samples)} ICs with 5 s restart sims") - print(f" {'T':>6} {'v_along_0':>10} {'v_along_5s':>10} {'ok':>4}") - - for ti, v_along_ramp, col_rad, ic in samples: - tension = float(TENSION_LIST[ti]) - r = simulate_point( - col = col_rad, - elevation_deg = EL, - tension_n = tension, - wind_speed = WIND, - v_target = V_TARGET, - t_end = 5.0, - dt = DT, - ic = ic, - ) - eq = r["eq"] - va5 = eq.get("v_along", float("nan")) - ok = math.isfinite(va5) and abs(va5 - V_TARGET) < 0.15 - print(f" {tension:6.0f} {v_along_ramp:+10.3f} {va5:+10.3f} {'ok' if ok else 'FAIL'}") - if not ok: - failures.append((tension, v_along_ramp, va5)) - - assert not failures, ( - f"{len(failures)} ICs not reproducible: " - f"{[(t, f'{v0:+.3f}', f'{v5:+.3f}') for t, v0, v5 in failures]}" - ) diff --git a/simulation/envelope/tests/__init__.py b/simulation/envelope/tests/__init__.py deleted file mode 100644 index 016840c..0000000 --- a/simulation/envelope/tests/__init__.py +++ /dev/null @@ -1,39 +0,0 @@ -""" -Swashplate phase rotation, matching `AP_MotorsHeli_Swash`. - -Only the *cyclic* rotation matters for the control loop: a positive -`phase_angle_deg` rotates the (roll_cmd, pitch_cmd) vector clockwise when -viewed from above, so that a pure pitch-axis cyclic command produces a roll -moment offset by `phase_angle_deg`. This compensates the gyroscopic precession -of the rotor at the *bandwidth at which the gain is tuned*. - -This module does **not** mix individual servo positions — the simulator works -with normalised (roll_cyclic, pitch_cyclic) outputs in [-1, 1]. -""" - -from __future__ import annotations - -import math - - -class SwashH3: - def __init__(self, phase_angle_deg: float = 0.0): - self.phase_angle_deg = float(phase_angle_deg) - - def set_phase(self, phase_angle_deg: float) -> None: - self.phase_angle_deg = float(phase_angle_deg) - - def mix(self, roll_cmd: float, pitch_cmd: float) -> tuple[float, float]: - """Rotate the (roll, pitch) cyclic command by the phase angle. - - Sign convention matches ArduPilot's `add_servo_angle(.., pos - phase, ..)`: - a positive phase angle subtracts from the servo azimuth, which when - re-projected onto the body frame is equivalent to rotating the cyclic - command by `+phase` about the rotor axis. - """ - a = math.radians(self.phase_angle_deg) - ca = math.cos(a) - sa = math.sin(a) - roll_out = ca * roll_cmd - sa * pitch_cmd - pitch_out = sa * roll_cmd + ca * pitch_cmd - return roll_out, pitch_out diff --git a/simulation/flight_report.py b/simulation/flight_report.py index 85ae64c..598511e 100644 --- a/simulation/flight_report.py +++ b/simulation/flight_report.py @@ -64,7 +64,7 @@ def plot_flight_report( telem: dict = {} if telemetry_path is not None and Path(telemetry_path).exists(): try: - from telemetry_csv import read_csv as _read_csv + from simulation.telemetry_csv import read_csv as _read_csv rows = _read_csv(telemetry_path) if rows: t_end_telem = rows[-1].t_sim diff --git a/simulation/gcs.py b/simulation/gcs.py index f2e5d84..4e4dfa7 100644 --- a/simulation/gcs.py +++ b/simulation/gcs.py @@ -28,7 +28,7 @@ from pymavlink import mavutil -from mavlink_log import MavlinkLogWriter +from simulation.mavlink_log import MavlinkLogWriter log = logging.getLogger(__name__) diff --git a/simulation/ic.py b/simulation/ic.py index d106a3f..50eb142 100644 --- a/simulation/ic.py +++ b/simulation/ic.py @@ -13,10 +13,10 @@ do NOT go through this module. The JSON is written (and regenerated) only by - simulation/tests/simtests/test_generate_ic.py::test_create_ic + tests/simtests/test_generate_ic.py::test_create_ic Run that once after any aero-model or rotor-geometry change: .venv/Scripts/python.exe -m pytest \\ - simulation/tests/simtests/test_generate_ic.py::test_create_ic -s + tests/simtests/test_generate_ic.py::test_create_ic -s """ from __future__ import annotations @@ -59,7 +59,7 @@ def _require_json() -> Path: raise FileNotFoundError( f"steady_state_starting.json not found at {IC_JSON_PATH}.\n" "Run: python -m pytest " - "simulation/tests/simtests/test_generate_ic.py::test_create_ic -s" + "tests/simtests/test_generate_ic.py::test_create_ic -s" ) return IC_JSON_PATH diff --git a/simulation/mediator.py b/simulation/mediator.py index 329a2dd..9072a62 100644 --- a/simulation/mediator.py +++ b/simulation/mediator.py @@ -40,21 +40,20 @@ import numpy as np # Local modules (same directory) -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from physics_core import PhysicsCore -from tether import TetherModel, ConstantTensionTether # noqa: F401 — re-exported for test callers -from sitl_interface import SITLInterface, SIM_CLOCK_HZ -from swashplate import ardupilot_h3_120_inverse, collective_out_to_rad -from param_defaults import load_collective_phys_range as _load_col_range -from sensor import make_sensor, SpinSensor -from kinematic import KinematicStartup, compute_launch_position, make_smooth_trapezoid_traj # noqa: F401 -from winch import GovernedWinchController -from winch_node import GovernedWinchNode, WinchCommand, Anemometer -import config as _mcfg +from simulation.physics_core import PhysicsCore +from simulation.tether import TetherModel, ConstantTensionTether # noqa: F401 — re-exported for test callers +from simulation.sitl_interface import SITLInterface, SIM_CLOCK_HZ +from simulation.swashplate import ardupilot_h3_120_inverse, collective_out_to_rad +from simulation.param_defaults import load_collective_phys_range as _load_col_range +from simulation.sensor import make_sensor, SpinSensor +from simulation.kinematic import KinematicStartup, compute_launch_position, make_smooth_trapezoid_traj # noqa: F401 +from simulation.winch import GovernedWinchController +from simulation.winch_node import GovernedWinchNode, WinchCommand, Anemometer +import simulation.config as _mcfg from dynbem import rotor_definition as _rd -from telemetry_csv import COLUMNS as _TEL_COLUMNS, ASYNC_MAV_COLUMNS -from mediator_base import install_sigterm_handler, run_lockstep, setup_logging -from mediator_events import MediatorEventLog +from simulation.telemetry_csv import COLUMNS as _TEL_COLUMNS, ASYNC_MAV_COLUMNS +from simulation.mediator_base import install_sigterm_handler, run_lockstep, setup_logging +from simulation.mediator_events import MediatorEventLog from types import SimpleNamespace try: @@ -118,7 +117,7 @@ def get_async_armed() -> bool: # Default initial state — warmup-settled equilibrium from test_steady_flight.py. # 50 m tether at ~30° elevation (ENU), body Z aligned with tether direction. -# Generated by running: pytest simulation/tests/unit/test_steady_flight.py +# Generated by running: pytest tests/unit/test_steady_flight.py # Initial state defaults live in config.py (DEFAULTS dict) — single source of truth. diff --git a/simulation/mediator_base.py b/simulation/mediator_base.py index 5aa5e12..1b6a34d 100644 --- a/simulation/mediator_base.py +++ b/simulation/mediator_base.py @@ -7,8 +7,8 @@ from typing import TYPE_CHECKING, Callable if TYPE_CHECKING: - from mediator_events import MediatorEventLog - from sitl_interface import SITLInterface + from simulation.mediator_events import MediatorEventLog + from simulation.sitl_interface import SITLInterface # Single canonical log format for all mediators. # Routes to stdout so output is captured when run as a subprocess diff --git a/simulation/mediator_static.py b/simulation/mediator_static.py index f454e25..ddf1059 100644 --- a/simulation/mediator_static.py +++ b/simulation/mediator_static.py @@ -28,10 +28,9 @@ import numpy as np -sys.path.insert(0, str(Path(__file__).resolve().parent)) -from mediator_base import install_sigterm_handler, run_lockstep, setup_logging -from mediator_events import MediatorEventLog -from sitl_interface import SITLInterface +from simulation.mediator_base import install_sigterm_handler, run_lockstep, setup_logging +from simulation.mediator_events import MediatorEventLog +from simulation.sitl_interface import SITLInterface log = logging.getLogger("mediator_static") diff --git a/simulation/mediator_torque.py b/simulation/mediator_torque.py index 92f98d2..2a128a8 100644 --- a/simulation/mediator_torque.py +++ b/simulation/mediator_torque.py @@ -61,13 +61,12 @@ import numpy as np # simulation/ — add so SITLInterface, torque_model, mediator_events are importable. -sys.path.insert(0, str(Path(__file__).resolve().parent)) -import torque_model as _m -from mediator_base import install_sigterm_handler, run_lockstep, setup_logging -from mediator_events import MediatorEventLog -from servo_pwm import MOTOR_PWM_MIN, MOTOR_PWM_MAX -from sitl_interface import SITLInterface +import simulation.torque_model as _m +from simulation.mediator_base import install_sigterm_handler, run_lockstep, setup_logging +from simulation.mediator_events import MediatorEventLog +from simulation.servo_pwm import MOTOR_PWM_MIN, MOTOR_PWM_MAX +from simulation.sitl_interface import SITLInterface # --------------------------------------------------------------------------- # Timing / port constants diff --git a/simulation/param_defaults.py b/simulation/param_defaults.py index 368b02d..ae2afd0 100644 --- a/simulation/param_defaults.py +++ b/simulation/param_defaults.py @@ -1,9 +1,9 @@ """Load ArduPilot parameters from .parm files. Parameter precedence (later files override earlier files): -1. ArduPilot heli baseline: ``simulation/tests/sitl/copter-heli.parm`` -2. RAWES common defaults: ``simulation/tests/sitl/rawes_common_defaults.parm`` -3. RAWES SITL-only overrides: ``simulation/tests/sitl/rawes_sitl_defaults.parm`` +1. ArduPilot heli baseline: ``tests/sitl/copter-heli.parm`` +2. RAWES common defaults: ``tests/sitl/rawes_common_defaults.parm`` +3. RAWES SITL-only overrides: ``tests/sitl/rawes_sitl_defaults.parm`` This keeps arduloop aligned to real ArduPilot parameters without hardcoded controller values in Python. @@ -21,18 +21,18 @@ def _resolve_default_param_files() -> list[Path]: """Return parameter files in precedence order (base -> overrides).""" - sim_root = Path(__file__).resolve().parent + repo_root = Path(__file__).resolve().parent.parent files: list[Path] = [] - ap_base = sim_root / "tests" / "sitl" / "copter-heli.parm" + ap_base = repo_root / "tests" / "sitl" / "copter-heli.parm" if ap_base.exists(): files.append(ap_base) - rawes_common = sim_root / "tests" / "sitl" / "rawes_common_defaults.parm" + rawes_common = repo_root / "tests" / "sitl" / "rawes_common_defaults.parm" if rawes_common.exists(): files.append(rawes_common) - rawes_sitl_only = sim_root / "tests" / "sitl" / "rawes_sitl_defaults.parm" + rawes_sitl_only = repo_root / "tests" / "sitl" / "rawes_sitl_defaults.parm" if rawes_sitl_only.exists(): files.append(rawes_sitl_only) @@ -41,8 +41,8 @@ def _resolve_default_param_files() -> list[Path]: def _resolve_rawes_lua_file() -> Path: """Return the canonical rawes.lua script path.""" - sim_root = Path(__file__).resolve().parent - return sim_root / "scripts" / "rawes.lua" + repo_root = Path(__file__).resolve().parent.parent + return repo_root / "scripts" / "rawes.lua" def load_rawes_lua_constants(required_names: Sequence[str]) -> dict[str, float]: diff --git a/simulation/physics_core.py b/simulation/physics_core.py index 35cabbf..166413f 100644 --- a/simulation/physics_core.py +++ b/simulation/physics_core.py @@ -37,14 +37,13 @@ import sys import os -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from dynamics import RigidBodyDynamics +from simulation.dynamics import RigidBodyDynamics from dynbem import create_aero, RotorInputs, euler_step_omega -from tether import TetherModel -from rotor_physics import resolve_i_spin_kgm2 -from param_defaults import thrust_to_coll_rad as _t2c -from torque_model import ( +from simulation.tether import TetherModel +from simulation.rotor_physics import resolve_i_spin_kgm2 +from simulation.param_defaults import thrust_to_coll_rad as _t2c +from simulation.torque_model import ( HubState as _HubState, HubParams as _HubParams, step as _hub_step, equilibrium_throttle as _hub_equilibrium_throttle, ) diff --git a/simulation/planner.py b/simulation/planner.py index 6d76cda..061b04b 100644 --- a/simulation/planner.py +++ b/simulation/planner.py @@ -63,7 +63,6 @@ import numpy as np import sys import os -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) # --------------------------------------------------------------------------- # Quaternion utilities (module-level, importable) diff --git a/simulation/pytest.ini b/simulation/pytest.ini deleted file mode 100644 index c591dbb..0000000 --- a/simulation/pytest.ini +++ /dev/null @@ -1,5 +0,0 @@ -[pytest] -timeout = 600 -markers = - simtest: full physics simulation (seconds–minutes); excluded from quick unit run - sitl: ArduPilot SITL integration test (Docker); run via test.sh stack diff --git a/simulation/rawes_lua_harness.py b/simulation/rawes_lua_harness.py index c86d71f..dd768b9 100644 --- a/simulation/rawes_lua_harness.py +++ b/simulation/rawes_lua_harness.py @@ -9,7 +9,7 @@ Simulation runs at Python execution speed with no real sleeping. Usage: - from rawes_lua_harness import RawesLua + from simulation.rawes_lua_harness import RawesLua sim = RawesLua(mode=1) # mode 1 = steady_noyaw sim.armed = True @@ -38,7 +38,7 @@ # ── File paths ──────────────────────────────────────────────────────────────── _SIM_DIR = Path(__file__).resolve().parent -_SCRIPTS_DIR = _SIM_DIR / "scripts" +_SCRIPTS_DIR = _SIM_DIR.parent / "scripts" _MOCK_LUA = (_SIM_DIR / "mock_ardupilot.lua" ).read_text(encoding="utf-8") _RAWES_LUA = (_SCRIPTS_DIR / "rawes.lua" ).read_text(encoding="utf-8") diff --git a/simulation/rawes_modes.py b/simulation/rawes_modes.py index 7fb9ed2..4601483 100644 --- a/simulation/rawes_modes.py +++ b/simulation/rawes_modes.py @@ -14,7 +14,7 @@ Usage ----- - from rawes_modes import MODE_STEADY, PUMP_REEL_OUT, send_anchor_ned + from simulation.rawes_modes import MODE_STEADY, PUMP_REEL_OUT, send_anchor_ned gcs.set_param("RAWES_MODE", MODE_STEADY) # set mode (pumping runs in steady) gcs.send_named_float("RAWES_SUB", PUMP_REEL_OUT) # set substate diff --git a/simulation/references/NACA-TR-552/plot_bem_rotor_c.py b/simulation/references/NACA-TR-552/plot_bem_rotor_c.py index c260f28..e7eb032 100644 --- a/simulation/references/NACA-TR-552/plot_bem_rotor_c.py +++ b/simulation/references/NACA-TR-552/plot_bem_rotor_c.py @@ -29,7 +29,6 @@ HERE = Path(__file__).parent REPO = HERE.parents[2] -sys.path.insert(0, str(REPO / "simulation")) from dynbem import rotor_definition as rd from dynbem import create_aero diff --git a/simulation/run_tests.py b/simulation/run_tests.py index 3ec3c7f..1de4da3 100644 --- a/simulation/run_tests.py +++ b/simulation/run_tests.py @@ -19,9 +19,9 @@ simulation/logs/{test_name}/ and are written by each test fixture. Examples: - python simulation/run_tests.py simulation/tests/unit -m "not simtest" -q - python simulation/run_tests.py --filter failures simulation/tests/unit -v - python simulation/run_tests.py simulation/tests/unit -k test_aero + python simulation/run_tests.py tests/unit -m "not simtest" -q + python simulation/run_tests.py --filter failures tests/unit -v + python simulation/run_tests.py tests/unit -k test_aero """ import hashlib @@ -149,7 +149,7 @@ def main() -> int: return 2 if not pytest_args: - pytest_args = ["simulation/tests/unit", "-m", "not simtest", "-q"] + pytest_args = ["tests/unit", "-m", "not simtest", "-q"] _venv = Path(VENV_PYTHON) python = str(_venv.resolve()) if _venv.exists() else sys.executable diff --git a/simulation/scripts/_calibrate/__init__.py b/simulation/scripts/_calibrate/__init__.py deleted file mode 100644 index 542d47c..0000000 --- a/simulation/scripts/_calibrate/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .repl import main - -__all__ = ["main"] diff --git a/simulation/scripts/calibrate.py b/simulation/scripts/calibrate.py deleted file mode 100644 index b5cdc39..0000000 --- a/simulation/scripts/calibrate.py +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env python3 -""" -calibrate.py -- thin entry point; all logic lives in _calibrate/. -""" -import os -import sys - -# Ensure simulation/scripts/ is on sys.path so `_calibrate` package is findable. -_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) -if _SCRIPT_DIR not in sys.path: - sys.path.insert(0, _SCRIPT_DIR) - -from _calibrate.repl import main # noqa: E402 - -if __name__ == "__main__": - main() diff --git a/simulation/simtest_log.py b/simulation/simtest_log.py index 5f7f5bf..ab0a436 100644 --- a/simulation/simtest_log.py +++ b/simulation/simtest_log.py @@ -3,7 +3,7 @@ Each test module creates one instance at module level: - from simtest_log import SimtestLog + from simulation.simtest_log import SimtestLog _log = SimtestLog(__file__) Then calls _log.write(lines, summary) to flush a full diagnostic table to disk @@ -149,7 +149,7 @@ def write(self, lines: list[str], summary: str) -> None: def dump_params_json(self) -> None: """Write merged ArduPilot parameter chain to log_dir/params.json.""" try: - from param_defaults import load_ap_params + from simulation.param_defaults import load_ap_params params = load_ap_params() out = self.log_dir / "params.json" diff --git a/simulation/sitl_interface.py b/simulation/sitl_interface.py index 5e57dd2..4095b29 100644 --- a/simulation/sitl_interface.py +++ b/simulation/sitl_interface.py @@ -31,7 +31,7 @@ import numpy as np -from servo_pwm import SWASH_PWM_NEUTRAL, SWASH_PWM_RANGE +from simulation.servo_pwm import SWASH_PWM_NEUTRAL, SWASH_PWM_RANGE log = logging.getLogger(__name__) diff --git a/simulation/stack_utils.py b/simulation/stack_utils.py index b84ffef..449e7da 100644 --- a/simulation/stack_utils.py +++ b/simulation/stack_utils.py @@ -11,7 +11,7 @@ import sys sys.path.insert(0, str(Path(__file__).resolve().parents[N])) # reach simulation/ - from stack_utils import ( + from tests.sitl.stack_utils import ( STACK_ENV_FLAG, ARDUPILOT_ENV, SIM_VEHICLE_ENV, SITL_GCS_PORT, SITL_JSON_PORT, GCS_ADDRESS, _resolve_sim_vehicle, _launch_sitl, _prime_sitl_eeprom, diff --git a/simulation/swashplate.py b/simulation/swashplate.py index a30ff61..1fc3529 100644 --- a/simulation/swashplate.py +++ b/simulation/swashplate.py @@ -32,7 +32,7 @@ import math import yaml import numpy as np -from param_defaults import load_collective_phys_range as _load_col_range, _resolve_default_rotor_yaml +from simulation.param_defaults import load_collective_phys_range as _load_col_range, _resolve_default_rotor_yaml # --------------------------------------------------------------------------- diff --git a/simulation/telemetry_csv.py b/simulation/telemetry_csv.py index 98c8040..8e9ea10 100644 --- a/simulation/telemetry_csv.py +++ b/simulation/telemetry_csv.py @@ -44,7 +44,7 @@ import numpy as np -from telemetry_columns import ( +from simulation.telemetry_columns import ( ASYNC_MAV_COLUMNS, COLUMNS, COLUMN_GROUPS, diff --git a/simulation/tests/common/path_setup.py b/simulation/tests/common/path_setup.py deleted file mode 100644 index 96ff0fb..0000000 --- a/simulation/tests/common/path_setup.py +++ /dev/null @@ -1,15 +0,0 @@ -"""Shared import-path setup for tests.""" - -from __future__ import annotations - -import sys -from pathlib import Path - - -def add_simulation_root() -> Path: - """Ensure simulation/ is on sys.path and return that path.""" - sim_root = Path(__file__).resolve().parents[2] - sim_root_str = str(sim_root) - if sim_root_str not in sys.path: - sys.path.insert(0, sim_root_str) - return sim_root diff --git a/simulation/tether.py b/simulation/tether.py index 4c40b81..ea8e7ac 100644 --- a/simulation/tether.py +++ b/simulation/tether.py @@ -7,7 +7,7 @@ import numpy as np -from frames import cross3 +from simulation.frames import cross3 class TetherModel: diff --git a/simulation/unified_ground.py b/simulation/unified_ground.py index 0f94e3f..49feadd 100644 --- a/simulation/unified_ground.py +++ b/simulation/unified_ground.py @@ -16,7 +16,7 @@ from __future__ import annotations -from pumping_planner import TensionCommand +from simulation.pumping_planner import TensionCommand _PHASE_TO_SUB: dict[str, int] = { "hold": 0, diff --git a/simulation/viz3d/__init__.py b/simulation/viz3d/__init__.py deleted file mode 100644 index 0d98692..0000000 --- a/simulation/viz3d/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# simulation/viz3d — 3D playback and telemetry stream for RAWES simulation. diff --git a/simulation/winch_node.py b/simulation/winch_node.py index 1092b93..82995e8 100644 --- a/simulation/winch_node.py +++ b/simulation/winch_node.py @@ -27,7 +27,7 @@ import numpy as np from dataclasses import dataclass -from winch import GovernedWinchController +from simulation.winch import GovernedWinchController class Anemometer: diff --git a/test.sh b/test.sh index 57e37a9..01c4440 100644 --- a/test.sh +++ b/test.sh @@ -6,9 +6,9 @@ # test file, up to N in parallel. The SITL stack is the only suite that needs # Docker. Every other suite runs with plain pytest in the Windows venv: # -# .venv/Scripts/python.exe -m pytest simulation/tests/unit -m "not simtest" -# .venv/Scripts/python.exe simulation/run_tests.py simulation/tests/simtests -m simtest -# .venv/Scripts/python.exe -m pytest simulation/tests/hil # needs RAWES_HIL_PORT=COMx +# .venv/Scripts/python.exe -m pytest tests/unit -m "not simtest" +# .venv/Scripts/python.exe simulation/run_tests.py tests/simtests -m simtest +# .venv/Scripts/python.exe -m pytest tests/hil # needs RAWES_HIL_PORT=COMx # # Usage: # bash test.sh [-n N] [pytest args...] # run the SITL stack suite @@ -47,33 +47,24 @@ _sync_code() { local _c="$1" echo "[INFO] Syncing code to container $_c..." docker exec "$_c" mkdir -p /rawes/simulation/logs - tar -C "$SIM_DIR" \ - --exclude="./logs" \ - --exclude="./__pycache__" \ + tar -C "$REPO_DIR" \ + --exclude="simulation/logs" \ + --exclude="__pycache__" \ --exclude="*/__pycache__" \ - --exclude="./eeprom*.bin" \ - --exclude="./tests/unit" \ - --exclude="./tests/hil" \ - --exclude="./.venv" \ - -cf - . \ - | docker exec -i "$_c" tar -xf - -C /rawes/simulation/ - # Sync the sibling aero workspace (editable-installed on host from ../aero - # relative to the repo root) for local development convenience. - local _AERO_DIR="$REPO_DIR/../aero" - if [ -d "$_AERO_DIR" ]; then - docker exec "$_c" mkdir -p /rawes - tar -C "$_AERO_DIR" \ - --exclude="./__pycache__" \ - --exclude="*/__pycache__" \ - --exclude="./.venv" \ - --exclude="./tests" \ - --exclude="./target" \ - --exclude="./out" \ - --exclude="./Research" \ - --exclude="./envelope" \ - -cf - Cargo.toml Cargo.lock pyproject.toml dynbem dynbem_rs aero \ - | docker exec -i "$_c" tar -xf - -C /rawes/ - docker exec "$_c" bash -lc 'python - <<"PY" + --exclude="simulation/eeprom*.bin" \ + --exclude="tests/unit" \ + --exclude="tests/hil" \ + --exclude=".venv" \ + --exclude="*.egg-info" \ + -cf - pyproject.toml simulation arduloop envelope analysis viz3d scripts tests calibrate \ + | docker exec -i "$_c" tar -xf - -C /rawes/ + # dynbem (the Rust-backed aero core) is installed from a pinned PyPI wheel, + # not synced from the sibling ../aero source workspace -- that source tree + # is not needed at runtime (all rawes code imports only `dynbem`, never + # bare `aero`/`dynbem_rs`) and its pyproject.toml previously clobbered + # rawes's own /rawes/pyproject.toml (pytest timeout/marker config) when + # both were synced to the same container path. + docker exec "$_c" bash -lc 'python - <<"PY" import importlib.metadata as m import pathlib import re @@ -131,7 +122,6 @@ if installed != required: print(f"[ERROR] dynbem version check failed after install (required={required!r}, found={installed!r}); aborting sync.", file=sys.stderr) raise SystemExit(2) PY' - fi echo "[INFO] Code sync complete." } @@ -233,7 +223,7 @@ _run_stack() { } trap _parallel_cleanup EXIT INT TERM - mapfile -t _ALL_FILES < <(find "$SIM_DIR/tests/sitl" -name "test_*.py" | sort) + mapfile -t _ALL_FILES < <(find "$REPO_DIR/tests/sitl" -name "test_*.py" | sort) local _K_EXPR="" local _i _next @@ -307,7 +297,7 @@ _run_stack() { _c="rawes-parallel-${_RUN_ID}-${_short}-${j}" _CONTAINERS+=("$_c") - _f="/rawes/simulation/${_ALL_FILES[$j]#${SIM_DIR}/}" + _f="/rawes/${_ALL_FILES[$j]#${REPO_DIR}/}" _wlog="/tmp/rawes-parallel-${_RUN_ID}-t${j}.log" _WORKER_LOGS+=("$_wlog") diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..a810f03 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,11 @@ +"""tests — all RAWES pytest suites. + +Subpackages (by tier, see AGENTS.md): + unit — fast, no-physics tests + simtests — full Python physics loop simtests (marker: simtest) + sitl — ArduPilot SITL Docker stack tests (marker: sitl) + hil — hardware-in-the-loop smoke tests + oneoff — one-off diagnostic scripts (not part of standard suites) + common — shared test fixtures/helpers (MockArdupilot, etc.) + envelope — tests for the envelope/ point-mass sweep package +""" diff --git a/simulation/tests/common/__init__.py b/tests/common/__init__.py similarity index 100% rename from simulation/tests/common/__init__.py rename to tests/common/__init__.py diff --git a/simulation/tests/common/mock_ardupilot.py b/tests/common/mock_ardupilot.py similarity index 98% rename from simulation/tests/common/mock_ardupilot.py rename to tests/common/mock_ardupilot.py index e296aca..5e28370 100644 --- a/simulation/tests/common/mock_ardupilot.py +++ b/tests/common/mock_ardupilot.py @@ -15,17 +15,17 @@ from scipy.spatial.transform import Rotation from arduloop import GuidedAttitudeController, HeliParams -from controller import (AZ_REF_TAU_S, compute_bz_altitude_hold, +from simulation.controller import (AZ_REF_TAU_S, compute_bz_altitude_hold, compute_rate_cmd, compute_rate_cmd_sqrt, compute_crosswind_rate_cmd, apply_crosswind_rate_to_body_rates, slerp_body_z, update_plane_azimuth, YawTrimObserver) -from landing_planner import LandingCommand -from physics_core import HubObservation -from pumping_planner import TensionCommand -from param_defaults import get_ap_param, load_ap_params, load_rawes_lua_constants, load_collective_phys_range -from telemetry_csv import TelRow, write_csv +from simulation.landing_planner import LandingCommand +from simulation.physics_core import HubObservation +from simulation.pumping_planner import TensionCommand +from simulation.param_defaults import get_ap_param, load_ap_params, load_rawes_lua_constants, load_collective_phys_range +from simulation.telemetry_csv import TelRow, write_csv def _load_rawes_pumping_constants() -> dict[str, float]: @@ -303,7 +303,7 @@ def __init__(self, sim, *, initial_thrust: float = 0.263, wind: "np.ndarray", dt sim._lua.execute(f"_ic_thrust = {float(initial_thrust)}") # ArduPilot heli mixer SERVO9 (Motor4) PWM range -- matches SERVO9_MIN/MAX - # in simulation/tests/sitl/rawes_common_defaults.parm and rawes.lua's + # in tests/sitl/rawes_common_defaults.parm and rawes.lua's # run_yaw_trim() `p("SERVO9_MIN", 1000)` / `p("SERVO9_MAX", 2000)` fallback. _SERVO9_MIN_US = 1000.0 _SERVO9_MAX_US = 2000.0 diff --git a/tests/envelope/__init__.py b/tests/envelope/__init__.py new file mode 100644 index 0000000..e26905c --- /dev/null +++ b/tests/envelope/__init__.py @@ -0,0 +1 @@ +"""tests.envelope — tests for the envelope/ point-mass pumping-envelope sweep package.""" diff --git a/simulation/envelope/tests/test_30deg_convergence.py b/tests/envelope/test_30deg_convergence.py similarity index 98% rename from simulation/envelope/tests/test_30deg_convergence.py rename to tests/envelope/test_30deg_convergence.py index 6f5a687..e223e14 100644 --- a/simulation/envelope/tests/test_30deg_convergence.py +++ b/tests/envelope/test_30deg_convergence.py @@ -10,7 +10,6 @@ import pytest -sys.path.insert(0, str(Path(__file__).resolve().parents[2])) from envelope.point_mass import simulate_point diff --git a/simulation/envelope/tests/test_collective_pid_el0.py b/tests/envelope/test_collective_pid_el0.py similarity index 98% rename from simulation/envelope/tests/test_collective_pid_el0.py rename to tests/envelope/test_collective_pid_el0.py index f48d597..b24714d 100644 --- a/simulation/envelope/tests/test_collective_pid_el0.py +++ b/tests/envelope/test_collective_pid_el0.py @@ -34,7 +34,6 @@ import numpy as np -sys.path.insert(0, str(Path(__file__).resolve().parents[2])) from envelope.point_mass import simulate_point diff --git a/simulation/envelope/tests/test_collective_sign.py b/tests/envelope/test_collective_sign.py similarity index 97% rename from simulation/envelope/tests/test_collective_sign.py rename to tests/envelope/test_collective_sign.py index ac1fd9d..84142dd 100644 --- a/simulation/envelope/tests/test_collective_sign.py +++ b/tests/envelope/test_collective_sign.py @@ -20,7 +20,6 @@ import numpy as np -sys.path.insert(0, str(Path(__file__).resolve().parents[2])) from dynbem import rotor_definition as rd from dynbem import create_aero diff --git a/simulation/envelope/tests/test_compute_map.py b/tests/envelope/test_compute_map.py similarity index 97% rename from simulation/envelope/tests/test_compute_map.py rename to tests/envelope/test_compute_map.py index 80fdaeb..f0fcbfd 100644 --- a/simulation/envelope/tests/test_compute_map.py +++ b/tests/envelope/test_compute_map.py @@ -1,6 +1,6 @@ """ Unit tests for compute_map.py — isolates masking, tension-range split, and PNG rendering. -Run: .venv/Scripts/python.exe -m pytest simulation/envelope/tests/test_compute_map.py -v +Run: .venv/Scripts/python.exe -m pytest tests/envelope/test_compute_map.py -v """ from __future__ import annotations @@ -10,7 +10,6 @@ import numpy as np import pytest -sys.path.insert(0, str(Path(__file__).resolve().parents[2])) from envelope.compute_map import compute_grid, V_REEL_OUT, V_REEL_IN diff --git a/simulation/envelope/tests/test_pid_collective.py b/tests/envelope/test_pid_collective.py similarity index 95% rename from simulation/envelope/tests/test_pid_collective.py rename to tests/envelope/test_pid_collective.py index 99dff01..5184ca0 100644 --- a/simulation/envelope/tests/test_pid_collective.py +++ b/tests/envelope/test_pid_collective.py @@ -7,7 +7,6 @@ import pytest -sys.path.insert(0, str(Path(__file__).resolve().parents[2])) from envelope.point_mass import simulate_point diff --git a/simulation/envelope/tests/test_tension_continuation.py b/tests/envelope/test_tension_continuation.py similarity index 98% rename from simulation/envelope/tests/test_tension_continuation.py rename to tests/envelope/test_tension_continuation.py index 880b8a9..cebdf84 100644 --- a/simulation/envelope/tests/test_tension_continuation.py +++ b/tests/envelope/test_tension_continuation.py @@ -13,11 +13,10 @@ import numpy as np import pytest -sys.path.insert(0, str(Path(__file__).resolve().parents[2])) from dynbem import rotor_definition as rd from dynbem import create_aero -from frames import build_orb_frame +from simulation.frames import build_orb_frame from envelope.point_mass import tether_hat, balance_bz diff --git a/simulation/envelope/tests/test_tension_ramp_30deg.py b/tests/envelope/test_tension_ramp_30deg.py similarity index 98% rename from simulation/envelope/tests/test_tension_ramp_30deg.py rename to tests/envelope/test_tension_ramp_30deg.py index f21f229..13210ba 100644 --- a/simulation/envelope/tests/test_tension_ramp_30deg.py +++ b/tests/envelope/test_tension_ramp_30deg.py @@ -14,11 +14,10 @@ import numpy as np -sys.path.insert(0, str(Path(__file__).resolve().parents[2])) from dynbem import rotor_definition as rd from dynbem import create_aero -from frames import build_orb_frame +from simulation.frames import build_orb_frame from envelope.point_mass import tether_hat, balance_bz, simulate_point EL = 30.0 diff --git a/tests/hil/__init__.py b/tests/hil/__init__.py new file mode 100644 index 0000000..165b9d5 --- /dev/null +++ b/tests/hil/__init__.py @@ -0,0 +1 @@ +"""tests.hil — hardware-in-the-loop smoke tests (requires RAWES_HIL_PORT).""" diff --git a/simulation/tests/hil/test_hil_smoke.py b/tests/hil/test_hil_smoke.py similarity index 99% rename from simulation/tests/hil/test_hil_smoke.py rename to tests/hil/test_hil_smoke.py index 221ad69..1d81b01 100644 --- a/simulation/tests/hil/test_hil_smoke.py +++ b/tests/hil/test_hil_smoke.py @@ -14,7 +14,7 @@ Set the serial port and run with the existing unit-test venv: RAWES_HIL_PORT=COM3 .venv/Scripts/python.exe -m pytest \\ - simulation/tests/hil/test_hil_smoke.py -v + tests/hil/test_hil_smoke.py -v Environment variables --------------------- diff --git a/simulation/tests/oneoff/__init__.py b/tests/oneoff/__init__.py similarity index 100% rename from simulation/tests/oneoff/__init__.py rename to tests/oneoff/__init__.py diff --git a/simulation/tests/oneoff/analyze_downwind_drift_csv.py b/tests/oneoff/analyze_downwind_drift_csv.py similarity index 100% rename from simulation/tests/oneoff/analyze_downwind_drift_csv.py rename to tests/oneoff/analyze_downwind_drift_csv.py diff --git a/simulation/tests/oneoff/analyze_ic_warmup_plane_drift.py b/tests/oneoff/analyze_ic_warmup_plane_drift.py similarity index 98% rename from simulation/tests/oneoff/analyze_ic_warmup_plane_drift.py rename to tests/oneoff/analyze_ic_warmup_plane_drift.py index d7bc9ec..ad6f677 100644 --- a/simulation/tests/oneoff/analyze_ic_warmup_plane_drift.py +++ b/tests/oneoff/analyze_ic_warmup_plane_drift.py @@ -12,20 +12,16 @@ import numpy as np SIM = Path(__file__).resolve().parents[2] -if str(SIM) not in sys.path: - sys.path.insert(0, str(SIM)) SIMTESTS = SIM / "tests" / "simtests" -if str(SIMTESTS) not in sys.path: - sys.path.insert(0, str(SIMTESTS)) from dynbem import RotorInputs, create_aero, euler_step_omega, relax_inflow, solve_trim_cyclic -from frames import build_orb_frame -from pumping_planner import TensionCommand +from simulation.frames import build_orb_frame +from simulation.pumping_planner import TensionCommand from tests.common.mock_ardupilot import MockArdupilot from tests.simtests import test_generate_ic as icgen from tests.simtests.simtest_runner import PhysicsRunner -from controller import HeliCyclicController -from telemetry_csv import TelRow, write_csv +from simulation.controller import HeliCyclicController +from simulation.telemetry_csv import TelRow, write_csv def _basis(): diff --git a/simulation/tests/oneoff/analyze_replay_aero_failure.py b/tests/oneoff/analyze_replay_aero_failure.py similarity index 99% rename from simulation/tests/oneoff/analyze_replay_aero_failure.py rename to tests/oneoff/analyze_replay_aero_failure.py index 1378c27..6af269d 100644 --- a/simulation/tests/oneoff/analyze_replay_aero_failure.py +++ b/tests/oneoff/analyze_replay_aero_failure.py @@ -11,8 +11,6 @@ import numpy as np SIM = Path(__file__).resolve().parents[2] -if str(SIM) not in sys.path: - sys.path.insert(0, str(SIM)) from dynbem import RotorInputs, create_aero from tests.simtests._rotor_helpers import load_default_rotor @@ -122,8 +120,9 @@ def _rotor_inputs_from_row(row: dict[str, str]) -> RotorInputs: ], dtype=float), v_hub_world=_vec(row, "vel_x", "vel_y", "vel_z"), wind_world=_vec(row, "wind_x", "wind_y", "wind_z"), - omega_rad_s=_f(row, "omega_rotor"), "t_sim"), + omega_rad_s=_f(row, "omega_rotor"), rho_kg_m3=1.225, + t=_f(row, "t_sim"), ) diff --git a/simulation/tests/oneoff/bode_attitude.py b/tests/oneoff/bode_attitude.py similarity index 91% rename from simulation/tests/oneoff/bode_attitude.py rename to tests/oneoff/bode_attitude.py index 81df236..997ab79 100644 --- a/simulation/tests/oneoff/bode_attitude.py +++ b/tests/oneoff/bode_attitude.py @@ -8,7 +8,7 @@ one (gyroscopic precession dominating). Run: - .venv/Scripts/python.exe simulation/tests/oneoff/bode_attitude.py + .venv/Scripts/python.exe tests/oneoff/bode_attitude.py """ from __future__ import annotations @@ -17,7 +17,6 @@ import numpy as np -sys.path.insert(0, str(Path(__file__).resolve().parents[2])) from tests.unit._aero_probe import load_rotor from tests.unit._controller_rig import probe_open_loop_plant diff --git a/simulation/tests/oneoff/constant_tether_disturbance_probe.py b/tests/oneoff/constant_tether_disturbance_probe.py similarity index 98% rename from simulation/tests/oneoff/constant_tether_disturbance_probe.py rename to tests/oneoff/constant_tether_disturbance_probe.py index 2dcfc6f..6b6a35b 100644 --- a/simulation/tests/oneoff/constant_tether_disturbance_probe.py +++ b/tests/oneoff/constant_tether_disturbance_probe.py @@ -9,7 +9,6 @@ ROOT = Path(__file__).resolve().parents[3] SIM = ROOT / "simulation" -sys.path.insert(0, str(SIM)) from tests.unit.test_full_loop_stability import ( # noqa: E402 _KD_POS, diff --git a/simulation/tests/oneoff/debug_guided_step.py b/tests/oneoff/debug_guided_step.py similarity index 97% rename from simulation/tests/oneoff/debug_guided_step.py rename to tests/oneoff/debug_guided_step.py index e95ccb5..640ff1b 100644 --- a/simulation/tests/oneoff/debug_guided_step.py +++ b/tests/oneoff/debug_guided_step.py @@ -3,7 +3,7 @@ One-off diagnostic, not a unit test. Run: - .venv\Scripts\python.exe simulation/tests/oneoff/debug_guided_step.py + .venv\Scripts\python.exe tests/oneoff/debug_guided_step.py Build up from the simplest possible scenario: Stage 1 - controller at equilibrium with zero gyro -> must produce zero tilt @@ -16,7 +16,6 @@ import numpy as np from scipy.spatial.transform import Rotation -sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[2])) from arduloop import GuidedAttitudeController, HeliParams, RateAxisParams from arduloop.guided import GuidedAttitudeParams @@ -105,7 +104,7 @@ def q_from_R(R: np.ndarray) -> np.ndarray: R_ic = np.array(R0_list) q_ic = q_from_R(R_ic) -from controller import compute_bz_altitude_hold, damp_bz_eq_lateral +from simulation.controller import compute_bz_altitude_hold, damp_bz_eq_lateral tlen = float(np.linalg.norm(pos_ic)) el_rad = math.asin(max(-1.0, min(1.0, -pos_ic[2] / max(tlen, 0.1)))) tension_ic = float(ic_data.get("tension_ic", 300.0)) diff --git a/simulation/tests/oneoff/dump_lua_reelin.py b/tests/oneoff/dump_lua_reelin.py similarity index 100% rename from simulation/tests/oneoff/dump_lua_reelin.py rename to tests/oneoff/dump_lua_reelin.py diff --git a/simulation/tests/oneoff/gain_sweep.py b/tests/oneoff/gain_sweep.py similarity index 92% rename from simulation/tests/oneoff/gain_sweep.py rename to tests/oneoff/gain_sweep.py index dca5cf3..48ba0dd 100644 --- a/simulation/tests/oneoff/gain_sweep.py +++ b/tests/oneoff/gain_sweep.py @@ -6,7 +6,7 @@ steady-state error per candidate. Run: - .venv/Scripts/python.exe simulation/tests/oneoff/gain_sweep.py + .venv/Scripts/python.exe tests/oneoff/gain_sweep.py """ from __future__ import annotations @@ -15,7 +15,6 @@ import numpy as np -sys.path.insert(0, str(Path(__file__).resolve().parents[2])) from tests.unit._aero_probe import load_rotor from tests.unit._controller_rig import probe_step_response diff --git a/simulation/tests/oneoff/gyro_phase_ident.py b/tests/oneoff/gyro_phase_ident.py similarity index 94% rename from simulation/tests/oneoff/gyro_phase_ident.py rename to tests/oneoff/gyro_phase_ident.py index 06a4190..4b0ada4 100644 --- a/simulation/tests/oneoff/gyro_phase_ident.py +++ b/tests/oneoff/gyro_phase_ident.py @@ -9,14 +9,12 @@ ROOT = Path(__file__).resolve().parents[3] SIM = ROOT / "simulation" -sys.path.insert(0, str(SIM)) -sys.path.insert(0, str(SIM / "tests" / "simtests")) from arduloop.swash import SwashH3 from dynbem import RotorInputs, create_aero -from dynamics import RigidBodyDynamics -from frames import build_orb_frame -from rotor_physics import resolve_i_spin_kgm2 +from simulation.dynamics import RigidBodyDynamics +from simulation.frames import build_orb_frame +from simulation.rotor_physics import resolve_i_spin_kgm2 from tests.simtests._rotor_helpers import load_default_rotor diff --git a/simulation/tests/oneoff/ideal_position_feedback_probe.py b/tests/oneoff/ideal_position_feedback_probe.py similarity index 98% rename from simulation/tests/oneoff/ideal_position_feedback_probe.py rename to tests/oneoff/ideal_position_feedback_probe.py index bf62aa7..e98d02b 100644 --- a/simulation/tests/oneoff/ideal_position_feedback_probe.py +++ b/tests/oneoff/ideal_position_feedback_probe.py @@ -8,7 +8,6 @@ ROOT = Path(__file__).resolve().parents[3] SIM = ROOT / "simulation" -sys.path.insert(0, str(SIM)) from tests.unit.test_python_outer_loop_isolation import _run_ideal_constant_tether # noqa: E402 diff --git a/simulation/tests/oneoff/kinematic_transition.py b/tests/oneoff/kinematic_transition.py similarity index 95% rename from simulation/tests/oneoff/kinematic_transition.py rename to tests/oneoff/kinematic_transition.py index 60a77f0..a6df679 100644 --- a/simulation/tests/oneoff/kinematic_transition.py +++ b/tests/oneoff/kinematic_transition.py @@ -19,7 +19,7 @@ the (ArduPilot-default rate-PID, new aero) combination — independent of SITL. Run: - .venv/Scripts/python.exe simulation/tests/oneoff/kinematic_transition.py + .venv/Scripts/python.exe tests/oneoff/kinematic_transition.py """ import math import sys @@ -29,17 +29,15 @@ import numpy as np ROOT = Path(__file__).resolve().parents[2] -sys.path.insert(0, str(ROOT)) -sys.path.insert(0, str(ROOT / "tests" / "simtests")) from dynbem import solve_trim_cyclic -from physics_core import PhysicsCore -from controller import ( +from simulation.physics_core import PhysicsCore +from simulation.controller import ( HeliCyclicController, compute_bz_altitude_hold, compute_rate_cmd, damp_bz_eq_lateral, ) -from kinematic import KinematicStartup, make_linear_traj -from simtest_ic import load_ic +from simulation.kinematic import KinematicStartup, make_linear_traj +from tests.simtests.simtest_ic import load_ic from tests.simtests._rotor_helpers import load_default_rotor _ROTOR = load_default_rotor() diff --git a/simulation/tests/oneoff/offplane_drift_probe.py b/tests/oneoff/offplane_drift_probe.py similarity index 98% rename from simulation/tests/oneoff/offplane_drift_probe.py rename to tests/oneoff/offplane_drift_probe.py index 90b8d59..e4db5dd 100644 --- a/simulation/tests/oneoff/offplane_drift_probe.py +++ b/tests/oneoff/offplane_drift_probe.py @@ -27,7 +27,7 @@ Run: c:\\repos\\windpower\\simulation\\.venv\\Scripts\\python.exe ^ - simulation/tests/oneoff/offplane_drift_probe.py + tests/oneoff/offplane_drift_probe.py """ from __future__ import annotations @@ -43,15 +43,13 @@ # ── Path setup ──────────────────────────────────────────────────────────────── ROOT = Path(__file__).resolve().parents[3] SIM = ROOT / "simulation" -sys.path.insert(0, str(SIM)) -sys.path.insert(0, str(SIM / "tests" / "simtests")) -from physics_core import PhysicsCore -from simtest_ic import load_ic +from simulation.physics_core import PhysicsCore +from tests.simtests.simtest_ic import load_ic from tests.simtests._rotor_helpers import load_default_rotor -from frames import build_orb_frame -from rotor_physics import resolve_i_spin_kgm2 -from tether import TetherModel +from simulation.frames import build_orb_frame +from simulation.rotor_physics import resolve_i_spin_kgm2 +from simulation.tether import TetherModel # ── Constants ───────────────────────────────────────────────────────────────── WIND = np.array([0.0, 10.0, 0.0]) # NED: 10 m/s East diff --git a/simulation/tests/oneoff/phase_sweep.py b/tests/oneoff/phase_sweep.py similarity index 94% rename from simulation/tests/oneoff/phase_sweep.py rename to tests/oneoff/phase_sweep.py index 8ad0b88..624f64e 100644 --- a/simulation/tests/oneoff/phase_sweep.py +++ b/tests/oneoff/phase_sweep.py @@ -6,7 +6,7 @@ at the given swashplate_phase_deg. Run: - .venv/Scripts/python.exe simulation/tests/oneoff/phase_sweep.py + .venv/Scripts/python.exe tests/oneoff/phase_sweep.py """ from __future__ import annotations @@ -17,10 +17,9 @@ import numpy as np from dataclasses import replace -sys.path.insert(0, str(Path(__file__).resolve().parents[2])) from dynbem import OyeBEMModel, RotorInputs, rotor_definition as rd -from dynamics import RigidBodyDynamics +from simulation.dynamics import RigidBodyDynamics def _settle_state(aero, omega_spin, R_hub, n_steps=50, dt=0.02): diff --git a/simulation/tests/oneoff/pumping_reelin_orientation_analysis.py b/tests/oneoff/pumping_reelin_orientation_analysis.py similarity index 100% rename from simulation/tests/oneoff/pumping_reelin_orientation_analysis.py rename to tests/oneoff/pumping_reelin_orientation_analysis.py diff --git a/simulation/tests/oneoff/trace_ic_replay_aero_state.py b/tests/oneoff/trace_ic_replay_aero_state.py similarity index 96% rename from simulation/tests/oneoff/trace_ic_replay_aero_state.py rename to tests/oneoff/trace_ic_replay_aero_state.py index b7418e1..00097a9 100644 --- a/simulation/tests/oneoff/trace_ic_replay_aero_state.py +++ b/tests/oneoff/trace_ic_replay_aero_state.py @@ -10,14 +10,10 @@ import numpy as np SIM = Path(__file__).resolve().parents[2] -if str(SIM) not in sys.path: - sys.path.insert(0, str(SIM)) SIMTESTS = SIM / "tests" / "simtests" -if str(SIMTESTS) not in sys.path: - sys.path.insert(0, str(SIMTESTS)) from dynbem import RotorInputs, create_aero -from pumping_planner import TensionCommand +from simulation.pumping_planner import TensionCommand from tests.common.mock_ardupilot import MockArdupilot from tests.simtests import test_generate_ic as icgen from tests.simtests.simtest_runner import PhysicsRunner @@ -62,7 +58,7 @@ def main() -> None: omega_spin=omega_spin, ) runner = PhysicsRunner(icgen._ROTOR, ic, icgen.WIND) - from controller import HeliCyclicController as _Heli + from simulation.controller import HeliCyclicController as _Heli runner._acro = _Heli( icgen._ROTOR, P=0.67, I=0.15, D=0.02, IMAX=0.30, diff --git a/simulation/tests/oneoff/tune_lua_reelin.py b/tests/oneoff/tune_lua_reelin.py similarity index 92% rename from simulation/tests/oneoff/tune_lua_reelin.py rename to tests/oneoff/tune_lua_reelin.py index 77687d9..43bd687 100644 --- a/simulation/tests/oneoff/tune_lua_reelin.py +++ b/tests/oneoff/tune_lua_reelin.py @@ -7,7 +7,7 @@ 3-cycle test. Usage: - python simulation/tests/oneoff/tune_lua_reelin.py + python tests/oneoff/tune_lua_reelin.py (edit the SWEEP list below) """ import sys @@ -16,19 +16,16 @@ import numpy as np _SIM = Path(__file__).resolve().parents[2] -sys.path.insert(0, str(_SIM)) -sys.path.insert(0, str(_SIM / "tests" / "simtests")) -sys.path.insert(0, str(_SIM / "tests")) -from winch import GovernedWinchController -from simtest_ic import load_ic -from simtest_runner import PhysicsRunner +from simulation.winch import GovernedWinchController +from tests.simtests.simtest_ic import load_ic +from tests.simtests.simtest_runner import PhysicsRunner from tests.common.mock_ardupilot import MockArdupilot from arduloop import HeliParams, RateAxisParams -from pumping_planner import TensionCommand -from rawes_lua_harness import RawesLua -from rawes_modes import MODE_STEADY -from unified_ground import LuaComms +from simulation.pumping_planner import TensionCommand +from simulation.rawes_lua_harness import RawesLua +from simulation.rawes_modes import MODE_STEADY +from simulation.unified_ground import LuaComms from tests.simtests._rotor_helpers import load_default_rotor _IC = load_ic() diff --git a/simulation/tests/oneoff/vertical_only_offplane_probe.py b/tests/oneoff/vertical_only_offplane_probe.py similarity index 94% rename from simulation/tests/oneoff/vertical_only_offplane_probe.py rename to tests/oneoff/vertical_only_offplane_probe.py index 384be3d..697ae3b 100644 --- a/simulation/tests/oneoff/vertical_only_offplane_probe.py +++ b/tests/oneoff/vertical_only_offplane_probe.py @@ -9,15 +9,13 @@ ROOT = Path(__file__).resolve().parents[3] SIM = ROOT / "simulation" -sys.path.insert(0, str(SIM)) -sys.path.insert(0, str(SIM / "tests" / "simtests")) from arduloop import RateAxisParams -from controller import HeliCyclicController, compute_bz_altitude_hold, compute_rate_cmd_sqrt -from pumping_planner import TensionCommand # noqa: F401 - documents matching command semantics -from simtest_ic import load_ic -from simtest_runner import PhysicsRunner -from telemetry_csv import TelRow, write_csv +from simulation.controller import HeliCyclicController, compute_bz_altitude_hold, compute_rate_cmd_sqrt +from simulation.pumping_planner import TensionCommand # noqa: F401 - documents matching command semantics +from tests.simtests.simtest_ic import load_ic +from tests.simtests.simtest_runner import PhysicsRunner +from simulation.telemetry_csv import TelRow, write_csv from tests.simtests._rotor_helpers import BODY_Z_SLEW_RATE_RAD_S, dynamics_kwargs, load_default_rotor from tests.simtests.test_steady_flight_offplane import _with_azimuth_offset diff --git a/simulation/tests/oneoff/warmup_gain_sweep.py b/tests/oneoff/warmup_gain_sweep.py similarity index 95% rename from simulation/tests/oneoff/warmup_gain_sweep.py rename to tests/oneoff/warmup_gain_sweep.py index cccf456..56120ca 100644 --- a/simulation/tests/oneoff/warmup_gain_sweep.py +++ b/tests/oneoff/warmup_gain_sweep.py @@ -5,7 +5,7 @@ set for the elastic-tether + wind + thrust-command AP environment. Run: - .venv/Scripts/python.exe simulation/tests/oneoff/warmup_gain_sweep.py + .venv/Scripts/python.exe tests/oneoff/warmup_gain_sweep.py """ import math import sys @@ -14,16 +14,14 @@ import numpy as np ROOT = Path(__file__).resolve().parents[2] -sys.path.insert(0, str(ROOT)) -sys.path.insert(0, str(ROOT / "tests" / "simtests")) from dynbem import create_aero, RotorInputs, relax_inflow, solve_trim_cyclic -from frames import build_orb_frame -from simtest_runner import PhysicsRunner +from simulation.frames import build_orb_frame +from tests.simtests.simtest_runner import PhysicsRunner from tests.common.mock_ardupilot import MockArdupilot -from pumping_planner import TensionCommand -from controller import HeliCyclicController -from mediator import TetherModel +from simulation.pumping_planner import TensionCommand +from simulation.controller import HeliCyclicController +from simulation.mediator import TetherModel from tests.simtests._rotor_helpers import load_default_rotor, BODY_Z_SLEW_RATE_RAD_S diff --git a/simulation/tests/oneoff/warmup_trace.py b/tests/oneoff/warmup_trace.py similarity index 94% rename from simulation/tests/oneoff/warmup_trace.py rename to tests/oneoff/warmup_trace.py index 3b210b3..fc7144b 100644 --- a/simulation/tests/oneoff/warmup_trace.py +++ b/tests/oneoff/warmup_trace.py @@ -5,7 +5,7 @@ last 200 samples so we can see the trigger. Run: - .venv/Scripts/python.exe simulation/tests/oneoff/warmup_trace.py + .venv/Scripts/python.exe tests/oneoff/warmup_trace.py """ import math import sys @@ -14,16 +14,14 @@ import numpy as np ROOT = Path(__file__).resolve().parents[2] -sys.path.insert(0, str(ROOT)) -sys.path.insert(0, str(ROOT / "tests" / "simtests")) from dynbem import create_aero, RotorInputs, relax_inflow, solve_trim_cyclic -from frames import build_orb_frame -from simtest_runner import PhysicsRunner +from simulation.frames import build_orb_frame +from tests.simtests.simtest_runner import PhysicsRunner from tests.common.mock_ardupilot import MockArdupilot -from pumping_planner import TensionCommand -from controller import HeliCyclicController -from mediator import TetherModel +from simulation.pumping_planner import TensionCommand +from simulation.controller import HeliCyclicController +from simulation.mediator import TetherModel from tests.simtests._rotor_helpers import load_default_rotor, BODY_Z_SLEW_RATE_RAD_S diff --git a/simulation/tests/oneoff/warmup_with_damping.py b/tests/oneoff/warmup_with_damping.py similarity index 94% rename from simulation/tests/oneoff/warmup_with_damping.py rename to tests/oneoff/warmup_with_damping.py index 422eb8e..d8288e6 100644 --- a/simulation/tests/oneoff/warmup_with_damping.py +++ b/tests/oneoff/warmup_with_damping.py @@ -5,7 +5,7 @@ pendulum mode that wouldn't settle in warmup_gain_sweep is damped by it. Run: - .venv/Scripts/python.exe simulation/tests/oneoff/warmup_with_damping.py + .venv/Scripts/python.exe tests/oneoff/warmup_with_damping.py """ import math import sys @@ -14,17 +14,15 @@ import numpy as np ROOT = Path(__file__).resolve().parents[2] -sys.path.insert(0, str(ROOT)) -sys.path.insert(0, str(ROOT / "tests" / "simtests")) from dynbem import create_aero, RotorInputs, relax_inflow, solve_trim_cyclic -from frames import build_orb_frame -from simtest_runner import PhysicsRunner +from simulation.frames import build_orb_frame +from tests.simtests.simtest_runner import PhysicsRunner from tests.common.mock_ardupilot import MockArdupilot -from pumping_planner import TensionCommand -from controller import (HeliCyclicController, compute_bz_altitude_hold, +from simulation.pumping_planner import TensionCommand +from simulation.controller import (HeliCyclicController, compute_bz_altitude_hold, compute_rate_cmd, damp_bz_eq_lateral) -from mediator import TetherModel +from simulation.mediator import TetherModel from tests.simtests._rotor_helpers import load_default_rotor, BODY_Z_SLEW_RATE_RAD_S diff --git a/simulation/tests/oneoff/wind_aligned_ic_probe.py b/tests/oneoff/wind_aligned_ic_probe.py similarity index 97% rename from simulation/tests/oneoff/wind_aligned_ic_probe.py rename to tests/oneoff/wind_aligned_ic_probe.py index 705db42..4ce1eb5 100644 --- a/simulation/tests/oneoff/wind_aligned_ic_probe.py +++ b/tests/oneoff/wind_aligned_ic_probe.py @@ -10,10 +10,8 @@ ROOT = Path(__file__).resolve().parents[3] SIM = ROOT / "simulation" -sys.path.insert(0, str(SIM)) -sys.path.insert(0, str(SIM / "tests" / "simtests")) -from frames import build_orb_frame +from simulation.frames import build_orb_frame from tests.simtests import test_generate_ic as gen diff --git a/simulation/tests/oneoff/wind_offset_from_trim_probe.py b/tests/oneoff/wind_offset_from_trim_probe.py similarity index 98% rename from simulation/tests/oneoff/wind_offset_from_trim_probe.py rename to tests/oneoff/wind_offset_from_trim_probe.py index df7d001..fc93062 100644 --- a/simulation/tests/oneoff/wind_offset_from_trim_probe.py +++ b/tests/oneoff/wind_offset_from_trim_probe.py @@ -11,8 +11,6 @@ ROOT = Path(__file__).resolve().parents[3] SIM = ROOT / "simulation" -sys.path.insert(0, str(SIM)) -sys.path.insert(0, str(SIM / "tests" / "simtests")) from dynbem import RotorInputs, create_aero, solve_trim_cyclic from tests.simtests._rotor_helpers import load_default_rotor diff --git a/simulation/tests/oneoff/wind_offset_speed_sweep.py b/tests/oneoff/wind_offset_speed_sweep.py similarity index 95% rename from simulation/tests/oneoff/wind_offset_speed_sweep.py rename to tests/oneoff/wind_offset_speed_sweep.py index 5149cb5..c36fd9e 100644 --- a/simulation/tests/oneoff/wind_offset_speed_sweep.py +++ b/tests/oneoff/wind_offset_speed_sweep.py @@ -7,9 +7,8 @@ ROOT = Path(__file__).resolve().parents[3] SIM = ROOT / "simulation" -sys.path.insert(0, str(SIM / "tests" / "oneoff")) -from wind_offset_from_trim_probe import estimate_from_log +from tests.oneoff.wind_offset_from_trim_probe import estimate_from_log def main() -> None: diff --git a/tests/simtests/__init__.py b/tests/simtests/__init__.py new file mode 100644 index 0000000..c933365 --- /dev/null +++ b/tests/simtests/__init__.py @@ -0,0 +1 @@ +"""tests.simtests — full Python physics loop simtests (marker: simtest).""" diff --git a/simulation/tests/simtests/_rotor_helpers.py b/tests/simtests/_rotor_helpers.py similarity index 91% rename from simulation/tests/simtests/_rotor_helpers.py rename to tests/simtests/_rotor_helpers.py index 5684b8e..5b50f46 100644 --- a/simulation/tests/simtests/_rotor_helpers.py +++ b/tests/simtests/_rotor_helpers.py @@ -10,11 +10,12 @@ from __future__ import annotations from pathlib import Path +import simulation from dynbem import rotor_definition as _rd -from rotor_physics import resolve_i_spin_kgm2 +from simulation.rotor_physics import resolve_i_spin_kgm2 -_ROTOR_DEFS = Path(__file__).resolve().parents[2] / "rotor_definitions" +_ROTOR_DEFS = Path(simulation.__file__).resolve().parent / "rotor_definitions" def load_default_rotor(): diff --git a/simulation/tests/simtests/conftest.py b/tests/simtests/conftest.py similarity index 79% rename from simulation/tests/simtests/conftest.py rename to tests/simtests/conftest.py index 7680b72..aa539b2 100644 --- a/simulation/tests/simtests/conftest.py +++ b/tests/simtests/conftest.py @@ -2,15 +2,14 @@ conftest.py — pytest configuration for simtests. Simtests are full time-domain physics simulations (seconds to minutes of compute). -Run with: pytest simulation/tests/simtests -m simtest -Skip slow: pytest simulation/tests/simtests -m "not simtest" +Run with: pytest tests/simtests -m simtest +Skip slow: pytest tests/simtests -m "not simtest" """ import sys from pathlib import Path import pytest -sys.path.insert(0, str(Path(__file__).resolve().parents[2])) SIMTEST_TIMEOUT_S = 600 @@ -31,7 +30,7 @@ def pytest_collection_modifyitems(items): @pytest.fixture def simtest_log(request): """Per-test SimtestLog: logs land in simulation/logs//.""" - from simtest_log import SimtestLog + from simulation.simtest_log import SimtestLog log = SimtestLog(request.node.name) log.dump_params_json() return log diff --git a/simulation/tests/simtests/simtest_ic.py b/tests/simtests/simtest_ic.py similarity index 86% rename from simulation/tests/simtests/simtest_ic.py rename to tests/simtests/simtest_ic.py index 9aeb2d2..4684e6e 100644 --- a/simulation/tests/simtests/simtest_ic.py +++ b/tests/simtests/simtest_ic.py @@ -8,11 +8,11 @@ aero model or rotor geometry changes, run test_generate_ic.py once to update it: .venv/Scripts/python.exe -m pytest - simulation/tests/unit/test_generate_ic.py::test_create_ic -s + tests/unit/test_generate_ic.py::test_create_ic -s Usage ----- - from simtest_ic import load_ic + from tests.simtests.simtest_ic import load_ic ic = load_ic() POS0 = ic.pos VEL0 = ic.vel @@ -27,7 +27,7 @@ from __future__ import annotations -from ic import IC, load_ic, load_ic_dict, IC_JSON_PATH +from simulation.ic import IC, load_ic, load_ic_dict, IC_JSON_PATH # Backward-compatible alias for the JSON path (old name used by some tests). _JSON_PATH = IC_JSON_PATH diff --git a/simulation/tests/simtests/simtest_runner.py b/tests/simtests/simtest_runner.py similarity index 97% rename from simulation/tests/simtests/simtest_runner.py rename to tests/simtests/simtest_runner.py index c656458..6d8bf1b 100644 --- a/simulation/tests/simtests/simtest_runner.py +++ b/tests/simtests/simtest_runner.py @@ -36,11 +36,10 @@ import numpy as np -sys.path.insert(0, str(Path(__file__).resolve().parents[2])) -from physics_core import PhysicsCore, HubObservation -from controller import HeliCyclicController -from param_defaults import load_collective_phys_range as _load_col_range +from simulation.physics_core import PhysicsCore, HubObservation +from simulation.controller import HeliCyclicController +from simulation.param_defaults import load_collective_phys_range as _load_col_range class PhysicsRunner: diff --git a/simulation/tests/simtests/test_generate_ic.py b/tests/simtests/test_generate_ic.py similarity index 97% rename from simulation/tests/simtests/test_generate_ic.py rename to tests/simtests/test_generate_ic.py index 388bbae..a790c16 100644 --- a/simulation/tests/simtests/test_generate_ic.py +++ b/tests/simtests/test_generate_ic.py @@ -7,7 +7,7 @@ (serialize + deserialize round-trip) Regenerate: - .venv/Scripts/python.exe -m pytest simulation/tests/unit/test_generate_ic.py -s + .venv/Scripts/python.exe -m pytest tests/unit/test_generate_ic.py -s WHY STATIC-TETHER ----------------- @@ -28,17 +28,17 @@ import numpy as np import pytest -sys.path.insert(0, str(Path(__file__).resolve().parents[2])) pytestmark = [pytest.mark.simtest, pytest.mark.timeout(300)] -import mediator as _mediator_module +import simulation +import simulation.mediator as _mediator_module from dynbem import create_aero, RotorInputs, relax_inflow, solve_trim_cyclic, euler_step_omega -from frames import build_orb_frame -from simtest_runner import PhysicsRunner +from simulation.frames import build_orb_frame +from tests.simtests.simtest_runner import PhysicsRunner from tests.common.mock_ardupilot import MockArdupilot -from pumping_planner import TensionCommand -from controller import compute_bz_altitude_hold +from simulation.pumping_planner import TensionCommand +from simulation.controller import compute_bz_altitude_hold from tests.simtests._rotor_helpers import ( load_default_rotor, dynamics_kwargs, BODY_Z_SLEW_RATE_RAD_S, ) @@ -103,7 +103,7 @@ def _wind_aligned_tether_hat() -> np.ndarray: _PLANNER_EVERY = max(1, round(_DT_CMD / _DT)) # 40 _AP_EVERY = max(1, round(1.0 / (MockArdupilot.AP_HZ * _DT))) # 8 -_JSON_PATH = Path(__file__).resolve().parents[2] / "steady_state_starting.json" +_JSON_PATH = Path(simulation.__file__).resolve().parent / "steady_state_starting.json" # ── IC computation ───────────────────────────────────────────────────────────── @@ -314,7 +314,7 @@ def _solve_orientation(col_c): # ── IC serialiser ────────────────────────────────────────────────────────────── def _save_ic(path: Path, ic: dict) -> None: - from param_defaults import load_collective_phys_range as _lr + from simulation.param_defaults import load_collective_phys_range as _lr col_min, col_max = _lr() coll_settled = float(ic["coll_settled"]) # eq_thrust is the normalized form of coll_eq_rad — single consistent IC value. @@ -454,7 +454,7 @@ def _run_steady(pos0: np.ndarray, vel0: np.ndarray, R0: np.ndarray, """ target_alt = float(-pos0[2]) - from param_defaults import load_collective_phys_range as _lr + from simulation.param_defaults import load_collective_phys_range as _lr col_min, col_max = _lr() stack_thrust = (float(stack_coll) - col_min) / (col_max - col_min) ic = SimpleNamespace( @@ -466,7 +466,7 @@ def _run_steady(pos0: np.ndarray, vel0: np.ndarray, R0: np.ndarray, omega_spin=float(omega_spin), ) runner = PhysicsRunner(_ROTOR, ic, WIND) - from controller import HeliCyclicController as _Heli + from simulation.controller import HeliCyclicController as _Heli runner._acro = _Heli(_ROTOR) runner._acro._servo.reset(stack_coll) runner._acro.set_trim(trim_tilt_lon, trim_tilt_lat) @@ -574,7 +574,7 @@ def test_ic_force_balance(simtest_log): if not _JSON_PATH.exists(): pytest.skip("steady_state_starting.json not found — run test_create_ic first") - from simtest_ic import load_ic + from tests.simtests.simtest_ic import load_ic ic = load_ic() # IC must start at rest — if it is a true fixed point, velocity grows slowly. @@ -638,7 +638,7 @@ def test_ic_steady_flight(simtest_log): if not _JSON_PATH.exists(): pytest.skip("steady_state_starting.json not found — run test_create_ic first") - from simtest_ic import load_ic + from tests.simtests.simtest_ic import load_ic ic = load_ic() d = json.loads(_JSON_PATH.read_text()) tension_sp = float(d.get("tension_eq_n", 300.0)) @@ -675,7 +675,7 @@ def test_ic_r0_kinematic(simtest_log): if not _JSON_PATH.exists(): pytest.skip("steady_state_starting.json not found — run test_create_ic first") - from simtest_ic import load_ic + from tests.simtests.simtest_ic import load_ic ic = load_ic() pos0 = ic.pos @@ -685,7 +685,7 @@ def test_ic_r0_kinematic(simtest_log): rest = ic.rest_length d = json.loads(_JSON_PATH.read_text()) tension_sp = float(d.get("tension_eq_n", 435.0)) - from param_defaults import thrust_to_coll_rad as _t2c + from simulation.param_defaults import thrust_to_coll_rad as _t2c # Use coll_eq_rad (physics equilibrium collective) for consistent physics. stack_coll = float(ic.coll_eq_rad) trim_tilt_lon = float(d.get("trim_tilt_lon", 0.0)) diff --git a/simulation/tests/simtests/test_ground_liftoff.py b/tests/simtests/test_ground_liftoff.py similarity index 95% rename from simulation/tests/simtests/test_ground_liftoff.py rename to tests/simtests/test_ground_liftoff.py index c6f2542..f03bf19 100644 --- a/simulation/tests/simtests/test_ground_liftoff.py +++ b/tests/simtests/test_ground_liftoff.py @@ -25,14 +25,13 @@ import numpy as np import pytest -sys.path.insert(0, str(Path(__file__).resolve().parents[2])) pytestmark = [pytest.mark.simtest, pytest.mark.timeout(300)] -from simtest_runner import PhysicsRunner +from tests.simtests.simtest_runner import PhysicsRunner from tests.common.mock_ardupilot import MockArdupilot -from rawes_lua_harness import RawesLua -from rawes_modes import MODE_STEADY, send_anchor_ned +from simulation.rawes_lua_harness import RawesLua +from simulation.rawes_modes import MODE_STEADY, send_anchor_ned from tests.simtests._rotor_helpers import load_default_rotor # ── Physical constants ──────────────────────────────────────────────────────── @@ -81,12 +80,12 @@ def _build_ic() -> SimpleNamespace: omega_spin / eq_thrust from IC file if available. """ try: - from simtest_ic import load_ic as _load_ic + from tests.simtests.simtest_ic import load_ic as _load_ic _ic = _load_ic() omega_spin = float(_ic.omega_spin) eq_thrust = float(_ic.eq_thrust) except FileNotFoundError: - from param_defaults import load_collective_phys_range as _lr + from simulation.param_defaults import load_collective_phys_range as _lr col_min, col_max = _lr() omega_spin = 39.42 eq_thrust = (-0.18 - col_min) / (col_max - col_min) diff --git a/simulation/tests/simtests/test_landing.py b/tests/simtests/test_landing.py similarity index 98% rename from simulation/tests/simtests/test_landing.py rename to tests/simtests/test_landing.py index 1b33f7f..7a17738 100644 --- a/simulation/tests/simtests/test_landing.py +++ b/tests/simtests/test_landing.py @@ -24,16 +24,15 @@ import numpy as np import pytest -sys.path.insert(0, str(Path(__file__).resolve().parents[2])) pytestmark = [pytest.mark.simtest, pytest.mark.timeout(600)] -from winch import WinchController -from simtest_log import BadEventLog -from simtest_ic import load_ic -from simtest_runner import PhysicsRunner +from simulation.winch import WinchController +from simulation.simtest_log import BadEventLog +from tests.simtests.simtest_ic import load_ic +from tests.simtests.simtest_runner import PhysicsRunner from tests.common.mock_ardupilot import MockArdupilot -from landing_planner import LandingGroundController +from simulation.landing_planner import LandingGroundController from tests.simtests._rotor_helpers import load_default_rotor, BODY_Z_SLEW_RATE_RAD_S _IC = load_ic() diff --git a/simulation/tests/simtests/test_landing_lua.py b/tests/simtests/test_landing_lua.py similarity index 96% rename from simulation/tests/simtests/test_landing_lua.py rename to tests/simtests/test_landing_lua.py index 7228a57..b37bdc3 100644 --- a/simulation/tests/simtests/test_landing_lua.py +++ b/tests/simtests/test_landing_lua.py @@ -19,18 +19,17 @@ import numpy as np import pytest -sys.path.insert(0, str(Path(__file__).resolve().parents[2])) pytestmark = [pytest.mark.simtest, pytest.mark.timeout(600)] -from winch import WinchController -from simtest_log import BadEventLog -from simtest_ic import load_ic -from simtest_runner import PhysicsRunner +from simulation.winch import WinchController +from simulation.simtest_log import BadEventLog +from tests.simtests.simtest_ic import load_ic +from tests.simtests.simtest_runner import PhysicsRunner from tests.common.mock_ardupilot import MockArdupilot -from landing_planner import LandingGroundController -from rawes_lua_harness import RawesLua -from rawes_modes import MODE_LANDING, LAND_FINAL_DROP +from simulation.landing_planner import LandingGroundController +from simulation.rawes_lua_harness import RawesLua +from simulation.rawes_modes import MODE_LANDING, LAND_FINAL_DROP from tests.simtests._rotor_helpers import load_default_rotor, BODY_Z_SLEW_RATE_RAD_S _IC = load_ic() diff --git a/simulation/tests/simtests/test_pump_cycle_lua.py b/tests/simtests/test_pump_cycle_lua.py similarity index 95% rename from simulation/tests/simtests/test_pump_cycle_lua.py rename to tests/simtests/test_pump_cycle_lua.py index e092a55..d7bac5f 100644 --- a/simulation/tests/simtests/test_pump_cycle_lua.py +++ b/tests/simtests/test_pump_cycle_lua.py @@ -26,21 +26,20 @@ import numpy as np import pytest -sys.path.insert(0, str(Path(__file__).resolve().parents[2])) pytestmark = [pytest.mark.simtest, pytest.mark.timeout(int(os.environ.get("RAWES_PUMP_TIMEOUT", "600")))] -from winch import GovernedWinchController -from winch_node import GovernedWinchNode, WinchCommand, Anemometer -from simtest_ic import load_ic -from simtest_log import BadEventLog -from simtest_runner import PhysicsRunner +from simulation.winch import GovernedWinchController +from simulation.winch_node import GovernedWinchNode, WinchCommand, Anemometer +from tests.simtests.simtest_ic import load_ic +from simulation.simtest_log import BadEventLog +from tests.simtests.simtest_runner import PhysicsRunner from tests.common.mock_ardupilot import MockArdupilot -from pumping_planner import PumpingGroundController -from rawes_lua_harness import RawesLua -from rawes_modes import MODE_STEADY, send_anchor_ned -from unified_ground import LuaComms +from simulation.pumping_planner import PumpingGroundController +from simulation.rawes_lua_harness import RawesLua +from simulation.rawes_modes import MODE_STEADY, send_anchor_ned +from simulation.unified_ground import LuaComms from tests.simtests._rotor_helpers import load_default_rotor _IC = load_ic() @@ -285,8 +284,8 @@ def test_lua_pumping_constants(): sim = RawesLua(mode=MODE_STEADY) f = sim.fns # NV names are ≤ 10 chars (MAVLink hard limit) - from unified_ground import _cmd_to_nv - from pumping_planner import TensionCommand + from simulation.unified_ground import _cmd_to_nv + from simulation.pumping_planner import TensionCommand dummy = TensionCommand( tension_target_n=300.0, alt_m=38.0, diff --git a/simulation/tests/simtests/test_pump_cycle_unified.py b/tests/simtests/test_pump_cycle_unified.py similarity index 97% rename from simulation/tests/simtests/test_pump_cycle_unified.py rename to tests/simtests/test_pump_cycle_unified.py index 2196bcd..58b2e82 100644 --- a/simulation/tests/simtests/test_pump_cycle_unified.py +++ b/tests/simtests/test_pump_cycle_unified.py @@ -22,20 +22,19 @@ import numpy as np import pytest -sys.path.insert(0, str(Path(__file__).resolve().parents[2])) pytestmark = [pytest.mark.simtest, pytest.mark.timeout(int(os.environ.get("RAWES_PUMP_TIMEOUT", "600")))] -from winch import GovernedWinchController -from simtest_log import BadEventLog -from simtest_ic import load_ic -from simtest_runner import PhysicsRunner +from simulation.winch import GovernedWinchController +from simulation.simtest_log import BadEventLog +from tests.simtests.simtest_ic import load_ic +from tests.simtests.simtest_runner import PhysicsRunner from tests.common.mock_ardupilot import MockArdupilot -from pumping_planner import TensionCommand -from comms import VirtualComms +from simulation.pumping_planner import TensionCommand +from simulation.comms import VirtualComms from tests.simtests._rotor_helpers import load_default_rotor, BODY_Z_SLEW_RATE_RAD_S -from param_defaults import thrust_to_coll_rad +from simulation.param_defaults import thrust_to_coll_rad _IC = load_ic() _ROTOR = load_default_rotor() diff --git a/simulation/tests/simtests/test_sensor_closed_loop.py b/tests/simtests/test_sensor_closed_loop.py similarity index 97% rename from simulation/tests/simtests/test_sensor_closed_loop.py rename to tests/simtests/test_sensor_closed_loop.py index 5e75d23..33f7888 100644 --- a/simulation/tests/simtests/test_sensor_closed_loop.py +++ b/tests/simtests/test_sensor_closed_loop.py @@ -55,19 +55,18 @@ import numpy as np import pytest -sys.path.insert(0, str(Path(__file__).resolve().parents[2])) pytestmark = [pytest.mark.simtest, pytest.mark.timeout(120)] from tests.simtests._rotor_helpers import load_default_rotor, BODY_Z_SLEW_RATE_RAD_S _ROTOR_S = load_default_rotor() -from controller import (AltitudeHoldController, compute_rate_cmd, +from simulation.controller import (AltitudeHoldController, compute_rate_cmd, damp_bz_eq_lateral, HeliCyclicController) -from sensor import PhysicalSensor -from simtest_log import BadEventLog -from simtest_ic import load_ic -from simtest_runner import PhysicsRunner +from simulation.sensor import PhysicalSensor +from simulation.simtest_log import BadEventLog +from tests.simtests.simtest_ic import load_ic +from tests.simtests.simtest_runner import PhysicsRunner _IC = load_ic() diff --git a/simulation/tests/simtests/test_steady_flight.py b/tests/simtests/test_steady_flight.py similarity index 95% rename from simulation/tests/simtests/test_steady_flight.py rename to tests/simtests/test_steady_flight.py index 451e77e..b634f8d 100644 --- a/simulation/tests/simtests/test_steady_flight.py +++ b/tests/simtests/test_steady_flight.py @@ -28,15 +28,14 @@ import numpy as np import pytest -sys.path.insert(0, str(Path(__file__).resolve().parents[2])) pytestmark = [pytest.mark.simtest, pytest.mark.timeout(300)] -import mediator as _mediator_module -from simtest_runner import PhysicsRunner +import simulation.mediator as _mediator_module +from tests.simtests.simtest_runner import PhysicsRunner from tests.common.mock_ardupilot import MockArdupilot -from pumping_planner import TensionCommand -from simtest_ic import load_ic +from simulation.pumping_planner import TensionCommand +from tests.simtests.simtest_ic import load_ic from tests.simtests._rotor_helpers import ( load_default_rotor, dynamics_kwargs, BODY_Z_SLEW_RATE_RAD_S, ) @@ -83,7 +82,7 @@ def _run_simulation(log, steps: int = 4000, *, ic=None): # ── Recorded run ────────────────────────────────────────────────────────── runner = PhysicsRunner(_ROTOR, ic, WIND) # Use central loader-backed defaults from arduloop/param_defaults. - from controller import HeliCyclicController as _Heli + from simulation.controller import HeliCyclicController as _Heli runner._acro = _Heli( _ROTOR, # All AP gains/limits come from the merged central .parm chain. @@ -99,7 +98,7 @@ def _run_simulation(log, steps: int = 4000, *, ic=None): dt=DT, ) from arduloop import HeliParams - from param_defaults import load_ap_params as _lp + from simulation.param_defaults import load_ap_params as _lp _hp = HeliParams.from_ap_dict(_lp()) ap.enable_guided(_hp) ap.tel_fn = lambda r, sr: { diff --git a/simulation/tests/simtests/test_steady_flight_ic_angle_only.py b/tests/simtests/test_steady_flight_ic_angle_only.py similarity index 95% rename from simulation/tests/simtests/test_steady_flight_ic_angle_only.py rename to tests/simtests/test_steady_flight_ic_angle_only.py index 9384a57..8ae19a9 100644 --- a/simulation/tests/simtests/test_steady_flight_ic_angle_only.py +++ b/tests/simtests/test_steady_flight_ic_angle_only.py @@ -27,7 +27,6 @@ import pytest from scipy.spatial.transform import Rotation -sys.path.insert(0, str(Path(__file__).resolve().parents[2])) pytestmark = [pytest.mark.simtest, pytest.mark.timeout(300)] @@ -39,11 +38,11 @@ make_df_equiv_row, write_df_equiv_csv, ) -from param_defaults import load_ap_params as _lp -from frames import build_gps_yaw_frame -from telemetry_csv import TelRow, write_csv -from simtest_ic import load_ic -from simtest_runner import PhysicsRunner +from simulation.param_defaults import load_ap_params as _lp +from simulation.frames import build_gps_yaw_frame +from simulation.telemetry_csv import TelRow, write_csv +from tests.simtests.simtest_ic import load_ic +from tests.simtests.simtest_runner import PhysicsRunner from tests.simtests._rotor_helpers import load_default_rotor @@ -89,7 +88,7 @@ def _run_angle_ic_only(steps: int = 4000): pitch_ic = float(euler_ic[1]) yaw_ic = float(euler_ic[2]) - from param_defaults import load_collective_phys_range as _lr + from simulation.param_defaults import load_collective_phys_range as _lr col_min, col_max = _lr() col_norm = (_IC.coll_eq_rad - col_min) / (col_max - col_min) * 2.0 - 1.0 diff --git a/simulation/tests/simtests/test_steady_flight_lua.py b/tests/simtests/test_steady_flight_lua.py similarity index 95% rename from simulation/tests/simtests/test_steady_flight_lua.py rename to tests/simtests/test_steady_flight_lua.py index b167808..53f1776 100644 --- a/simulation/tests/simtests/test_steady_flight_lua.py +++ b/tests/simtests/test_steady_flight_lua.py @@ -22,16 +22,15 @@ import numpy as np import pytest -sys.path.insert(0, str(Path(__file__).resolve().parents[2])) pytestmark = [pytest.mark.simtest, pytest.mark.timeout(300)] -from simtest_ic import load_ic -from simtest_log import BadEventLog -from simtest_runner import PhysicsRunner +from tests.simtests.simtest_ic import load_ic +from simulation.simtest_log import BadEventLog +from tests.simtests.simtest_runner import PhysicsRunner from tests.common.mock_ardupilot import MockArdupilot -from rawes_lua_harness import RawesLua -from rawes_modes import MODE_STEADY, send_anchor_ned +from simulation.rawes_lua_harness import RawesLua +from simulation.rawes_modes import MODE_STEADY, send_anchor_ned from tests.simtests._rotor_helpers import load_default_rotor _IC = load_ic() diff --git a/simulation/tests/simtests/test_steady_flight_offplane.py b/tests/simtests/test_steady_flight_offplane.py similarity index 95% rename from simulation/tests/simtests/test_steady_flight_offplane.py rename to tests/simtests/test_steady_flight_offplane.py index 4d56a5f..dd4b2f8 100644 --- a/simulation/tests/simtests/test_steady_flight_offplane.py +++ b/tests/simtests/test_steady_flight_offplane.py @@ -7,12 +7,11 @@ import numpy as np import pytest -sys.path.insert(0, str(Path(__file__).resolve().parents[2])) pytestmark = [pytest.mark.simtest, pytest.mark.timeout(300)] -from simtest_ic import load_ic -from test_steady_flight import DT, _run_simulation +from tests.simtests.simtest_ic import load_ic +from tests.simtests.test_steady_flight import DT, _run_simulation def _wrap_pi(angle_rad: float) -> float: diff --git a/simulation/tests/simtests/test_yaw_regulation_lua.py b/tests/simtests/test_yaw_regulation_lua.py similarity index 95% rename from simulation/tests/simtests/test_yaw_regulation_lua.py rename to tests/simtests/test_yaw_regulation_lua.py index 1f3c9e3..3c6f630 100644 --- a/simulation/tests/simtests/test_yaw_regulation_lua.py +++ b/tests/simtests/test_yaw_regulation_lua.py @@ -28,15 +28,16 @@ from pathlib import Path import numpy as np + +from simulation.param_defaults import load_collective_phys_range import pytest -sys.path.insert(0, str(Path(__file__).resolve().parents[2])) pytestmark = [pytest.mark.simtest, pytest.mark.timeout(30)] -import torque_model as _m -from rawes_lua_harness import RawesLua -from rawes_modes import MODE_PASSIVE +import simulation.torque_model as _m +from simulation.rawes_lua_harness import RawesLua +from simulation.rawes_modes import MODE_PASSIVE # --------------------------------------------------------------------------- # Plant constants (torque_model defaults — GB4008 66KV, 80:44 gear, 4S LiPo) @@ -76,7 +77,7 @@ _IC_PITCH = -1.1102 # rad (≈ −63.6 deg, tethered-hover IC) _IC_ROLL = 0.0 # IC thrust: derived from -0.150 rad design point via physical collective range -_col_min, _col_max = __import__('param_defaults').load_collective_phys_range() +_col_min, _col_max = load_collective_phys_range() _IC_THRUST = (-0.150 - _col_min) / (_col_max - _col_min) diff --git a/tests/sitl/__init__.py b/tests/sitl/__init__.py new file mode 100644 index 0000000..d13aeab --- /dev/null +++ b/tests/sitl/__init__.py @@ -0,0 +1 @@ +"""tests.sitl — ArduPilot SITL Docker stack tests (marker: sitl). Run via test.sh.""" diff --git a/simulation/tests/sitl/check_eeprom.py b/tests/sitl/check_eeprom.py similarity index 100% rename from simulation/tests/sitl/check_eeprom.py rename to tests/sitl/check_eeprom.py diff --git a/simulation/tests/sitl/conftest.py b/tests/sitl/conftest.py similarity index 84% rename from simulation/tests/sitl/conftest.py rename to tests/sitl/conftest.py index 7799964..982e967 100644 --- a/simulation/tests/sitl/conftest.py +++ b/tests/sitl/conftest.py @@ -10,8 +10,8 @@ guided_nogps_armed_landing_lua, guided_nogps_armed_lua_full sitl/torque/conftest.py — torque_armed, torque_armed_profile, torque_armed_lua """ -from stack_infra import * # noqa: F401,F403 — re-export everything for test imports -from stack_infra import ( +from tests.sitl.stack_infra import * # noqa: F401,F403 — re-export everything for test imports +from tests.sitl.stack_infra import ( _sitl_stack, _acro_stack, _torque_stack, diff --git a/simulation/tests/sitl/copter-heli.parm b/tests/sitl/copter-heli.parm similarity index 98% rename from simulation/tests/sitl/copter-heli.parm rename to tests/sitl/copter-heli.parm index 33148f9..3a1a462 100644 --- a/simulation/tests/sitl/copter-heli.parm +++ b/tests/sitl/copter-heli.parm @@ -3,7 +3,7 @@ # Canonical parameter reference for ArduPilot (ATC_*, H_*, SERVO_*, RC_*, GCS_*): # - Keep inline explanations for ArduPilot parameters in this file. # - RAWES_* script-generated parameters are documented in -# simulation/tests/sitl/rawes_common_defaults.parm. +# tests/sitl/rawes_common_defaults.parm. # # ArduPilot PID telemetry gate (not set here by default): # GCS_PID_MASK bitmask: 0=Roll, 1=Pitch, 2=Yaw, 3=AccelZ diff --git a/tests/sitl/flight/__init__.py b/tests/sitl/flight/__init__.py new file mode 100644 index 0000000..070901e --- /dev/null +++ b/tests/sitl/flight/__init__.py @@ -0,0 +1 @@ +"""tests.sitl.flight — IC-start flight SITL stack tests (steady/passive/pumping/landing).""" diff --git a/simulation/tests/sitl/flight/conftest.py b/tests/sitl/flight/conftest.py similarity index 99% rename from simulation/tests/sitl/flight/conftest.py rename to tests/sitl/flight/conftest.py index 2e42658..e47c5df 100644 --- a/simulation/tests/sitl/flight/conftest.py +++ b/tests/sitl/flight/conftest.py @@ -14,8 +14,8 @@ import pytest -from stack_infra import * # noqa: F401,F403 — re-export everything for test imports -from stack_infra import ( +from tests.sitl.stack_infra import * # noqa: F401,F403 — re-export everything for test imports +from tests.sitl.stack_infra import ( _acro_stack, _RAWES_DEFAULTS_PARM, _install_lua_scripts, @@ -25,8 +25,8 @@ HOME_LON_DEG, HOME_ALT_M, ) -from ic import load_ic -from torque_model import HubParams, equilibrium_throttle +from simulation.ic import load_ic +from simulation.torque_model import HubParams, equilibrium_throttle def _send_anchor_location(ctx) -> None: diff --git a/simulation/tests/sitl/flight/test_kinematic_gps_sitl.py b/tests/sitl/flight/test_kinematic_gps_sitl.py similarity index 91% rename from simulation/tests/sitl/flight/test_kinematic_gps_sitl.py rename to tests/sitl/flight/test_kinematic_gps_sitl.py index 0bbe930..9820715 100644 --- a/simulation/tests/sitl/flight/test_kinematic_gps_sitl.py +++ b/tests/sitl/flight/test_kinematic_gps_sitl.py @@ -17,17 +17,11 @@ from __future__ import annotations import sys -from pathlib import Path -_SIM_DIR = Path(__file__).resolve().parents[3] -_SITL_DIR = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(_SIM_DIR)) -sys.path.insert(0, str(_SITL_DIR)) -from stack_infra import _acro_stack +from tests.sitl.stack_infra import _acro_stack -sys.path.insert(0, str(_SIM_DIR / "analysis")) -from analyse_run import validate_ekf_window +from analysis.analyse_run import validate_ekf_window import pytest pytestmark = pytest.mark.sitl diff --git a/simulation/tests/sitl/flight/test_lua_flight_ic_passive_sitl.py b/tests/sitl/flight/test_lua_flight_ic_passive_sitl.py similarity index 94% rename from simulation/tests/sitl/flight/test_lua_flight_ic_passive_sitl.py rename to tests/sitl/flight/test_lua_flight_ic_passive_sitl.py index 9ede14e..3a37c1a 100644 --- a/simulation/tests/sitl/flight/test_lua_flight_ic_passive_sitl.py +++ b/tests/sitl/flight/test_lua_flight_ic_passive_sitl.py @@ -17,21 +17,14 @@ import logging import math import sys -from pathlib import Path import numpy as np import pytest pytestmark = pytest.mark.sitl -_SIM_DIR = Path(__file__).resolve().parents[3] -_SITL_DIR = Path(__file__).resolve().parents[1] -_ANALYSIS_DIR = _SIM_DIR / "analysis" -sys.path.insert(0, str(_SIM_DIR)) -sys.path.insert(0, str(_SITL_DIR)) -sys.path.insert(0, str(_ANALYSIS_DIR)) -from stack_infra import ( # noqa: E402 +from tests.sitl.stack_infra import ( # noqa: E402 StackContext, assert_no_mediator_criticals, get_arducopter_crash_info, @@ -148,7 +141,7 @@ def _handle(msg, t_rel): if not ctx.telemetry_log.exists(): pytest.fail("Missing telemetry.csv for IC-passive analysis") - from telemetry_csv import read_csv as _read_csv # noqa: PLC0415 + from simulation.telemetry_csv import read_csv as _read_csv # noqa: PLC0415 rows = _read_csv(ctx.telemetry_log) window = [r for r in rows if float(r.t_sim) >= t_obs_start] diff --git a/simulation/tests/sitl/flight/test_lua_flight_steady_sitl.py b/tests/sitl/flight/test_lua_flight_steady_sitl.py similarity index 97% rename from simulation/tests/sitl/flight/test_lua_flight_steady_sitl.py rename to tests/sitl/flight/test_lua_flight_steady_sitl.py index d6995e4..99fa3d4 100644 --- a/simulation/tests/sitl/flight/test_lua_flight_steady_sitl.py +++ b/tests/sitl/flight/test_lua_flight_steady_sitl.py @@ -61,26 +61,18 @@ import logging import math import sys -from pathlib import Path - import numpy as np import pytest pytestmark = [pytest.mark.sitl, pytest.mark.timeout(1800)] -_SIM_DIR = Path(__file__).resolve().parents[3] -_SITL_DIR = Path(__file__).resolve().parents[1] -_ANALYSIS_DIR = _SIM_DIR / "analysis" -sys.path.insert(0, str(_SIM_DIR)) -sys.path.insert(0, str(_SITL_DIR)) -sys.path.insert(0, str(_ANALYSIS_DIR)) -from stack_infra import ( +from tests.sitl.stack_infra import ( StackContext, dump_startup_diagnostics, observe, assert_no_mediator_criticals, get_arducopter_crash_info, ) -from analyse_run import compute_steady_metrics, print_flight_report +from analysis.analyse_run import compute_steady_metrics, print_flight_report # -- Timing ------------------------------------------------------------------- _KINEMATIC_TIMEOUT_S = 60.0 # s from fixture yield to kinematic end (4.7 timing jitter can reduce effective margin near the 80 s kinematic exit) @@ -272,7 +264,7 @@ def _handle(msg, t_rel): metrics = None _rows = [] if ctx.telemetry_log.exists(): - from telemetry_csv import read_csv as _read_csv + from simulation.telemetry_csv import read_csv as _read_csv _rows = _read_csv(ctx.telemetry_log) metrics = compute_steady_metrics(_rows, stable_alt_m=_STABLE_ALT_M) log.info( diff --git a/simulation/tests/sitl/flight/test_pumping_cycle_sitl.py b/tests/sitl/flight/test_pumping_cycle_sitl.py similarity index 97% rename from simulation/tests/sitl/flight/test_pumping_cycle_sitl.py rename to tests/sitl/flight/test_pumping_cycle_sitl.py index 3aa4ac3..77ae4e5 100644 --- a/simulation/tests/sitl/flight/test_pumping_cycle_sitl.py +++ b/tests/sitl/flight/test_pumping_cycle_sitl.py @@ -31,24 +31,19 @@ import math import socket import sys -from pathlib import Path import pytest pytestmark = pytest.mark.sitl -_SIM_DIR = Path(__file__).resolve().parents[3] -_SITL_DIR = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(_SIM_DIR)) -sys.path.insert(0, str(_SITL_DIR)) -from stack_infra import ( +from tests.sitl.stack_infra import ( StackContext, dump_startup_diagnostics, assert_no_mediator_criticals, assert_procs_alive, ) -from telemetry_csv import read_csv -from pumping_planner import PumpingGroundController -from unified_ground import _cmd_to_nv +from simulation.telemetry_csv import read_csv +from simulation.pumping_planner import PumpingGroundController +from simulation.unified_ground import _cmd_to_nv from tests.simtests._rotor_helpers import load_default_rotor _ROTOR = load_default_rotor() diff --git a/simulation/tests/sitl/rawes_common_defaults.parm b/tests/sitl/rawes_common_defaults.parm similarity index 100% rename from simulation/tests/sitl/rawes_common_defaults.parm rename to tests/sitl/rawes_common_defaults.parm diff --git a/simulation/tests/sitl/rawes_sitl_defaults.parm b/tests/sitl/rawes_sitl_defaults.parm similarity index 100% rename from simulation/tests/sitl/rawes_sitl_defaults.parm rename to tests/sitl/rawes_sitl_defaults.parm diff --git a/simulation/tests/sitl/stack_infra.py b/tests/sitl/stack_infra.py similarity index 99% rename from simulation/tests/sitl/stack_infra.py rename to tests/sitl/stack_infra.py index 6a431d4..542cadb 100644 --- a/simulation/tests/sitl/stack_infra.py +++ b/tests/sitl/stack_infra.py @@ -36,12 +36,12 @@ import pytest -_SIM_DIR = Path(__file__).resolve().parents[2] +import simulation as _simulation_pkg + +_SIM_DIR = Path(_simulation_pkg.__file__).resolve().parent _SITL_DIR = Path(__file__).resolve().parent _TORQUE_DIR = _SIM_DIR # mediator_torque.py now lives in simulation/ -sys.path.insert(0, str(_SIM_DIR)) -sys.path.insert(0, str(_SITL_DIR)) import numpy as _np @@ -58,7 +58,7 @@ def _project_default_rotor(): from tests.simtests._rotor_helpers import load_default_rotor return load_default_rotor() -from stack_utils import ( +from tests.sitl.stack_utils import ( ARDUPILOT_ENV, STACK_ENV_FLAG, SIM_VEHICLE_ENV, @@ -83,10 +83,10 @@ def _project_default_rotor(): ) from pymavlink import mavutil as _mavutil -from gcs import GUIDED, GUIDED_NOGPS, STABILIZE, RawesGCS -from mediator_events import MediatorEventLog -from controller import make_hold_controller -from ic import load_ic_dict, IC_JSON_PATH +from simulation.gcs import GUIDED, GUIDED_NOGPS, STABILIZE, RawesGCS +from simulation.mediator_events import MediatorEventLog +from simulation.controller import make_hold_controller +from simulation.ic import load_ic_dict, IC_JSON_PATH _STARTING_STATE = IC_JSON_PATH _RAWES_COMMON_PARM = _SITL_DIR / "rawes_common_defaults.parm" @@ -537,7 +537,7 @@ def _sitl_stack( pytest.importorskip("pymavlink") assert_stack_ports_free() - repo_root = Path(__file__).resolve().parents[3] + repo_root = Path(__file__).resolve().parents[2] sim_dir = repo_root / "simulation" # ── Paths ────────────────────────────────────────────────────────────────── @@ -864,12 +864,12 @@ def _procs_alive(): # Lua scripts helper # --------------------------------------------------------------------------- -_SCRIPTS_DIR = _SIM_DIR / "scripts" # simulation/scripts/ — all Lua scripts +_SCRIPTS_DIR = _SIM_DIR.parent / "scripts" # scripts/ — all Lua scripts def _install_lua_scripts(*names: str) -> None: """ - Copy the named Lua scripts from simulation/scripts/ to SITL's /ardupilot/scripts/. + Copy the named Lua scripts from scripts/ to SITL's /ardupilot/scripts/. Must be called before SITL starts. In normal use, pass only "rawes.lua" (the unified script); RAWES_MODE selects the active mode at runtime. @@ -1674,9 +1674,7 @@ def get_arducopter_crash_info(ctx) -> str: if not acp.exists(): return "" _analysis = Path(__file__).resolve().parents[2] / "analysis" - if str(_analysis) not in sys.path: - sys.path.insert(0, str(_analysis)) - from analyse_run import parse_arducopter, RunReport # type: ignore[import] + from analysis.analyse_run import parse_arducopter, RunReport # type: ignore[import] rpt = RunReport() parse_arducopter(acp, rpt) if rpt.sitl_crash is None: @@ -1949,7 +1947,7 @@ def _torque_stack( tail_channel : ArduPilot tail/motor channel read by mediator (0-based) extra_params : ParamSetup merged into torque params before boot-file write test_name : pytest test node name; used as logger name and for per-test log directory - install_scripts : tuple of Lua script names to install from simulation/scripts/ + install_scripts : tuple of Lua script names to install from scripts/ boot_params : dict of {param_name: value} merged on top of torque_setup (for params only known at fixture call time, e.g. RPM1_TYPE). startup_yaw_rate_deg_s : yaw rate [deg/s] sent during startup hold for EKF init (0=stationary). diff --git a/simulation/tests/sitl/stack_utils.py b/tests/sitl/stack_utils.py similarity index 99% rename from simulation/tests/sitl/stack_utils.py rename to tests/sitl/stack_utils.py index c885be6..6dfdbde 100644 --- a/simulation/tests/sitl/stack_utils.py +++ b/tests/sitl/stack_utils.py @@ -15,8 +15,8 @@ from pathlib import Path import numpy as np -from frames import build_gps_yaw_frame -from rawes_modes import ( +from simulation.frames import build_gps_yaw_frame +from simulation.rawes_modes import ( MOCK_ORIGIN_LAT_DEG as HOME_LAT_DEG, MOCK_ORIGIN_LON_DEG as HOME_LON_DEG, MOCK_ORIGIN_ALT_M as HOME_ALT_M, @@ -593,7 +593,7 @@ def _launch_mediator( # Lazy import: config lives in simulation/, which must be on sys.path. # conftest.py and all test files add simulation/ to sys.path before importing # stack_utils, so this import will succeed. - import config as _mcfg + import simulation.config as _mcfg # Build config dict from defaults, then apply all overrides cfg = _mcfg.defaults() @@ -631,7 +631,7 @@ def _launch_mediator( # warm_coll_rad seeds TensionPI integral at the exact equilibrium value on # kinematic exit — must survive the trajectory.deschutter extra_config merge. if initial_state is not None and "eq_thrust" in initial_state: - from param_defaults import thrust_to_coll_rad as _t2c + from simulation.param_defaults import thrust_to_coll_rad as _t2c cfg.setdefault("trajectory", {}).setdefault("deschutter", {}) cfg["trajectory"]["deschutter"]["warm_coll_rad"] = _t2c(float(initial_state["eq_thrust"])) # tension_out is intentionally NOT read from initial_state["tension_eq_n"]. diff --git a/simulation/tests/sitl/tmp_pid_overrides_backup.parm b/tests/sitl/tmp_pid_overrides_backup.parm similarity index 81% rename from simulation/tests/sitl/tmp_pid_overrides_backup.parm rename to tests/sitl/tmp_pid_overrides_backup.parm index 4a611cd..e45a73f 100644 --- a/simulation/tests/sitl/tmp_pid_overrides_backup.parm +++ b/tests/sitl/tmp_pid_overrides_backup.parm @@ -2,7 +2,7 @@ # Date: 2026-06-29 # Purpose: test stack behavior with ArduPilot default PID params. -# Removed from simulation/tests/sitl/rawes_sitl_defaults.parm: +# Removed from tests/sitl/rawes_sitl_defaults.parm: ATC_RAT_RLL_P 0.15 ATC_RAT_PIT_P 0.15 ATC_RAT_RLL_I 0.15 @@ -21,7 +21,7 @@ ATC_RAT_YAW_IMAX 0.0 ATC_ANG_RLL_P 4.0 ATC_ANG_PIT_P 6.0 -# Removed from simulation/tests/sitl/stack_infra.py _BASE_ACRO_PARAMS: +# Removed from tests/sitl/stack_infra.py _BASE_ACRO_PARAMS: # "ATC_RAT_RLL_IMAX": 0.0 # "ATC_RAT_PIT_IMAX": 0.0 # "ATC_RAT_YAW_IMAX": 0.0 diff --git a/tests/sitl/torque/__init__.py b/tests/sitl/torque/__init__.py new file mode 100644 index 0000000..9e0006c --- /dev/null +++ b/tests/sitl/torque/__init__.py @@ -0,0 +1 @@ +"""tests.sitl.torque — counter-torque motor SITL stack tests.""" diff --git a/simulation/tests/sitl/torque/conftest.py b/tests/sitl/torque/conftest.py similarity index 95% rename from simulation/tests/sitl/torque/conftest.py rename to tests/sitl/torque/conftest.py index 571e9f7..11bf037 100644 --- a/simulation/tests/sitl/torque/conftest.py +++ b/tests/sitl/torque/conftest.py @@ -12,8 +12,8 @@ """ import pytest -from stack_infra import * # noqa: F401,F403 — re-export everything for test imports -from stack_infra import ( +from tests.sitl.stack_infra import * # noqa: F401,F403 — re-export everything for test imports +from tests.sitl.stack_infra import ( _torque_stack, _LUA_TORQUE_EXTRA_PARAMS, _DDFP_TORQUE_EXTRA_PARAMS, @@ -21,8 +21,8 @@ # IC (steady-state tethered-hover) orientation constants — single source of # truth in mediator_torque (derived from steady_state_starting.json R0). -from mediator_torque import _IC_ROLL_RAD, _IC_PITCH_RAD, _IC_YAW_RAD # noqa: E402 -from param_defaults import load_collective_phys_range +from simulation.mediator_torque import _IC_ROLL_RAD, _IC_PITCH_RAD, _IC_YAW_RAD # noqa: E402 +from simulation.param_defaults import load_collective_phys_range # --------------------------------------------------------------------------- @@ -46,7 +46,7 @@ def torque_armed(tmp_path, request): LUA_YAW_IC_COL, RIC/PIC=IC roll/pitch) and the EKF pre-arm attitude is seeded from the live yaw. ArduPilot's DDFP yaw PID regulates hub yaw via Motor4. """ - import torque_model as _m + import simulation.torque_model as _m with _torque_stack( tmp_path, omega_rotor=_m.OMEGA_ROTOR_NOMINAL, @@ -72,7 +72,7 @@ def torque_armed_profile(request, tmp_path): def test_foo(torque_armed_profile): ... """ - import torque_model as _m + import simulation.torque_model as _m profile = getattr(request, "param", "constant") with _torque_stack( tmp_path, @@ -92,7 +92,7 @@ def _lua_torque_stack(tmp_path, request, armon_ms): Arming is handled by GCS; rawes.lua (RAWES_MODE=0) provides optional RAWES_ARM disarm timer behavior only. """ - import torque_model as _m + import simulation.torque_model as _m return _torque_stack( tmp_path, omega_rotor=_m.OMEGA_ROTOR_NOMINAL, @@ -204,7 +204,7 @@ def torque_armed_ddfp_zero(tmp_path, request): artificial spin during startup (ArduPilot can arm without it in the torque SITL environment). """ - import torque_model as _m + import simulation.torque_model as _m with _torque_stack( tmp_path, omega_rotor=_m.OMEGA_ROTOR_NOMINAL, @@ -233,7 +233,7 @@ def torque_armed_ddfp_ramp(tmp_path, request): Assertion: motor throttle > equilibrium + 0.05 at t_dyn = 25-35 s when psi_dot is 8-10 deg/s. """ - import torque_model as _m + import simulation.torque_model as _m with _torque_stack( tmp_path, omega_rotor=_m.OMEGA_ROTOR_NOMINAL, @@ -265,7 +265,7 @@ def torque_armed_ddfp(tmp_path, request): level pre-arm attitude; yaw is still regulated solely by ArduPilot's built-in DDFP controller (the Lua does not touch the yaw motor output in MODE_PASSIVE). """ - import torque_model as _m + import simulation.torque_model as _m with _torque_stack( tmp_path, omega_rotor=_m.OMEGA_ROTOR_NOMINAL, diff --git a/simulation/tests/sitl/torque/test_slow_rpm_sitl.py b/tests/sitl/torque/test_slow_rpm_sitl.py similarity index 91% rename from simulation/tests/sitl/torque/test_slow_rpm_sitl.py rename to tests/sitl/torque/test_slow_rpm_sitl.py index b8e552f..30113ae 100644 --- a/simulation/tests/sitl/torque/test_slow_rpm_sitl.py +++ b/tests/sitl/torque/test_slow_rpm_sitl.py @@ -18,7 +18,7 @@ pytestmark = pytest.mark.sitl -from torque_test_utils import run_observation_loop, save_telemetry, assert_physics_yaw_rate +from tests.sitl.torque.torque_test_utils import run_observation_loop, save_telemetry, assert_physics_yaw_rate _SETTLE_S = 65.0 _OBSERVE_S = 30.0 diff --git a/simulation/tests/sitl/torque/test_wobble_sitl.py b/tests/sitl/torque/test_wobble_sitl.py similarity index 91% rename from simulation/tests/sitl/torque/test_wobble_sitl.py rename to tests/sitl/torque/test_wobble_sitl.py index b91127d..7831555 100644 --- a/simulation/tests/sitl/torque/test_wobble_sitl.py +++ b/tests/sitl/torque/test_wobble_sitl.py @@ -18,7 +18,7 @@ pytestmark = pytest.mark.sitl -from torque_test_utils import run_observation_loop, save_telemetry, assert_physics_yaw_rate +from tests.sitl.torque.torque_test_utils import run_observation_loop, save_telemetry, assert_physics_yaw_rate _SETTLE_S = 95.0 _OBSERVE_S = 20.0 diff --git a/simulation/tests/sitl/torque/test_yaw_regulation_sitl.py b/tests/sitl/torque/test_yaw_regulation_sitl.py similarity index 94% rename from simulation/tests/sitl/torque/test_yaw_regulation_sitl.py rename to tests/sitl/torque/test_yaw_regulation_sitl.py index 06b20f6..868de49 100644 --- a/simulation/tests/sitl/torque/test_yaw_regulation_sitl.py +++ b/tests/sitl/torque/test_yaw_regulation_sitl.py @@ -28,14 +28,14 @@ Run with (inside Docker) ------------------------ - RAWES_RUN_STACK_INTEGRATION=1 pytest simulation/tests/sitl/torque/test_yaw_regulation_sitl.py -v + RAWES_RUN_STACK_INTEGRATION=1 pytest tests/sitl/torque/test_yaw_regulation_sitl.py -v """ from __future__ import annotations import math import pytest -from torque_test_utils import ( +from tests.sitl.torque.torque_test_utils import ( run_observation_loop, save_telemetry, assert_physics_yaw_rate, diff --git a/simulation/tests/sitl/torque/torque_test_utils.py b/tests/sitl/torque/torque_test_utils.py similarity index 95% rename from simulation/tests/sitl/torque/torque_test_utils.py rename to tests/sitl/torque/torque_test_utils.py index 25a2fed..26ed345 100644 --- a/simulation/tests/sitl/torque/torque_test_utils.py +++ b/tests/sitl/torque/torque_test_utils.py @@ -17,22 +17,13 @@ from __future__ import annotations import math -from pathlib import Path import pytest -_SIM_DIR = Path(__file__).resolve().parents[3] # tests/sitl/torque/ -> simulation/ -_SITL_DIR = Path(__file__).resolve().parents[1] # tests/sitl/torque/ -> tests/sitl/ - -import sys as _sys -for _p in (str(_SIM_DIR), str(_SITL_DIR)): - if _p not in _sys.path: - _sys.path.insert(0, _p) - -from telemetry_csv import TelRow, write_csv -from mediator_events import MediatorEventLog -from simtest_log import BadEventLog -from stack_infra import observe # noqa: E402 +from simulation.telemetry_csv import TelRow, write_csv +from simulation.mediator_events import MediatorEventLog +from simulation.simtest_log import BadEventLog +from tests.sitl.stack_infra import observe # noqa: E402 def yaw_motor_pwm_from_servo_output(msg, default: int = 0) -> int: @@ -122,7 +113,7 @@ def save_telemetry(rows: list, path = log_dir / "telemetry.csv" write_csv(rows, path) log.info("Telemetry saved -> %s (%d frames)", path, len(rows)) - log.info("Visualise with: python simulation/viz3d/visualize_torque.py %s", path) + log.info("Visualise with: python viz3d/visualize_torque.py %s", path) def assert_yaw_rate( diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..4b4e16b --- /dev/null +++ b/tests/unit/__init__.py @@ -0,0 +1 @@ +"""tests.unit — fast, no-physics unit tests.""" diff --git a/simulation/tests/unit/_aero_probe.py b/tests/unit/_aero_probe.py similarity index 95% rename from simulation/tests/unit/_aero_probe.py rename to tests/unit/_aero_probe.py index e5046b4..7735047 100644 --- a/simulation/tests/unit/_aero_probe.py +++ b/tests/unit/_aero_probe.py @@ -18,10 +18,11 @@ import numpy as np +import simulation from dynbem import RotorInputs, create_aero, rotor_definition as _rd -_ROTOR_DEFS = Path(__file__).resolve().parents[2] / "rotor_definitions" +_ROTOR_DEFS = Path(simulation.__file__).resolve().parent / "rotor_definitions" def load_rotor(name: str = "beaupoil_2026"): diff --git a/simulation/tests/unit/_controller_rig.py b/tests/unit/_controller_rig.py similarity index 99% rename from simulation/tests/unit/_controller_rig.py rename to tests/unit/_controller_rig.py index 8fb2fbe..6ad5ba7 100644 --- a/simulation/tests/unit/_controller_rig.py +++ b/tests/unit/_controller_rig.py @@ -57,9 +57,9 @@ import numpy as np from dynbem import RotorInputs, OyeBEMModel -from controller import HeliCyclicController -from dynamics import RigidBodyDynamics -from param_defaults import thrust_to_coll_rad +from simulation.controller import HeliCyclicController +from simulation.dynamics import RigidBodyDynamics +from simulation.param_defaults import thrust_to_coll_rad _G_M_S2 = 9.81 diff --git a/simulation/tests/unit/conftest.py b/tests/unit/conftest.py similarity index 57% rename from simulation/tests/unit/conftest.py rename to tests/unit/conftest.py index c602e8d..938bc23 100644 --- a/simulation/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -3,20 +3,8 @@ Fast, no-physics tests only. Simtests (full physics loops) live in tests/simtests/. """ -import sys -from pathlib import Path - import pytest -_SIM_ROOT = Path(__file__).resolve().parents[2] -if str(_SIM_ROOT) not in sys.path: - sys.path.insert(0, str(_SIM_ROOT)) - -from tests.common.path_setup import add_simulation_root - - -add_simulation_root() - def pytest_configure(config): config.addinivalue_line( diff --git a/simulation/tests/unit/test_ap_controller.py b/tests/unit/test_ap_controller.py similarity index 96% rename from simulation/tests/unit/test_ap_controller.py rename to tests/unit/test_ap_controller.py index 588793a..9508cab 100644 --- a/simulation/tests/unit/test_ap_controller.py +++ b/tests/unit/test_ap_controller.py @@ -6,12 +6,12 @@ import pytest -from physics_core import HubObservation -from pumping_planner import TensionCommand +from simulation.physics_core import HubObservation +from simulation.pumping_planner import TensionCommand from tests.common.mock_ardupilot import MockArdupilot # ── Shared helpers ──────────────────────────────────────────────────────────── -from param_defaults import load_collective_phys_range +from simulation.param_defaults import load_collective_phys_range _COL_MIN, _COL_MAX = load_collective_phys_range() T_IC = 300.0 # N COLL_IC = -0.240 # rad IC collective (test-specific value) diff --git a/simulation/tests/unit/test_ap_lua_constants.py b/tests/unit/test_ap_lua_constants.py similarity index 95% rename from simulation/tests/unit/test_ap_lua_constants.py rename to tests/unit/test_ap_lua_constants.py index 8c33b57..7184bd8 100644 --- a/simulation/tests/unit/test_ap_lua_constants.py +++ b/tests/unit/test_ap_lua_constants.py @@ -11,7 +11,7 @@ import pytest -from rawes_lua_harness import RawesLua +from simulation.rawes_lua_harness import RawesLua from tests.common.mock_ardupilot import MockArdupilot diff --git a/simulation/tests/unit/test_armon_lua.py b/tests/unit/test_armon_lua.py similarity index 99% rename from simulation/tests/unit/test_armon_lua.py rename to tests/unit/test_armon_lua.py index 72df441..b4de789 100644 --- a/simulation/tests/unit/test_armon_lua.py +++ b/tests/unit/test_armon_lua.py @@ -11,7 +11,7 @@ from pathlib import Path -from rawes_lua_harness import RawesLua +from simulation.rawes_lua_harness import RawesLua # --------------------------------------------------------------------------- diff --git a/simulation/tests/unit/test_attitude_convergence.py b/tests/unit/test_attitude_convergence.py similarity index 96% rename from simulation/tests/unit/test_attitude_convergence.py rename to tests/unit/test_attitude_convergence.py index 00d1f01..637fffa 100644 --- a/simulation/tests/unit/test_attitude_convergence.py +++ b/tests/unit/test_attitude_convergence.py @@ -23,11 +23,11 @@ from dynbem import RotorInputs -from controller import HeliCyclicController, compute_rate_cmd -from dynamics import RigidBodyDynamics -from frames import build_orb_frame -from param_defaults import coll_rad_to_thrust -from rotor_physics import resolve_i_spin_kgm2 +from simulation.controller import HeliCyclicController, compute_rate_cmd +from simulation.dynamics import RigidBodyDynamics +from simulation.frames import build_orb_frame +from simulation.param_defaults import coll_rad_to_thrust +from simulation.rotor_physics import resolve_i_spin_kgm2 from tests.unit._aero_probe import load_rotor, make_probe @@ -195,7 +195,7 @@ def test_acro_trim_feedforward_cancels_baseline_disturbance(): package guarantees nulls the hub moment to within tolerance. """ from dynbem import solve_trim_cyclic - from controller import HeliCyclicController + from simulation.controller import HeliCyclicController R = build_orb_frame(np.array([0.0, 0.0, 1.0])) wind = np.array([0.0, 10.0, 0.0]) diff --git a/simulation/tests/unit/test_controller.py b/tests/unit/test_controller.py similarity index 99% rename from simulation/tests/unit/test_controller.py rename to tests/unit/test_controller.py index 2eb0f9d..d0c3bea 100644 --- a/simulation/tests/unit/test_controller.py +++ b/tests/unit/test_controller.py @@ -16,7 +16,7 @@ import pytest -from controller import ( +from simulation.controller import ( compute_rc_rates, compute_rc_from_attitude, compute_rc_from_physical_attitude, diff --git a/simulation/tests/unit/test_cyclic_direction.py b/tests/unit/test_cyclic_direction.py similarity index 97% rename from simulation/tests/unit/test_cyclic_direction.py rename to tests/unit/test_cyclic_direction.py index bba918d..4d6b6c0 100644 --- a/simulation/tests/unit/test_cyclic_direction.py +++ b/tests/unit/test_cyclic_direction.py @@ -7,11 +7,12 @@ import numpy as np +import simulation from dynbem import RotorInputs, create_aero, rotor_definition -from ic import load_ic +from simulation.ic import load_ic -SIM = Path(__file__).resolve().parents[2] +SIM = Path(simulation.__file__).resolve().parent ROTOR_PATH = SIM / "rotor_definitions" / "beaupoil_2026.yaml" RHO = 1.225 diff --git a/simulation/tests/unit/test_cyclic_direction_mapping.py b/tests/unit/test_cyclic_direction_mapping.py similarity index 98% rename from simulation/tests/unit/test_cyclic_direction_mapping.py rename to tests/unit/test_cyclic_direction_mapping.py index b8ab323..12b2046 100644 --- a/simulation/tests/unit/test_cyclic_direction_mapping.py +++ b/tests/unit/test_cyclic_direction_mapping.py @@ -7,7 +7,7 @@ """ import math import pytest -from swashplate import ( +from simulation.swashplate import ( ardupilot_h3_120_forward, ardupilot_h3_120_inverse, ) @@ -91,7 +91,7 @@ def test_servo_model_parameter_order(self): This validates that the servo.step() correctly maps parameter order to forward_mix. """ - from swashplate import SwashplateServoModel + from simulation.swashplate import SwashplateServoModel # Create servo model servo = SwashplateServoModel( diff --git a/simulation/tests/unit/test_dynamics_physics.py b/tests/unit/test_dynamics_physics.py similarity index 99% rename from simulation/tests/unit/test_dynamics_physics.py rename to tests/unit/test_dynamics_physics.py index e80ad41..2af73ae 100644 --- a/simulation/tests/unit/test_dynamics_physics.py +++ b/tests/unit/test_dynamics_physics.py @@ -21,7 +21,7 @@ import pytest -from dynamics import RigidBodyDynamics +from simulation.dynamics import RigidBodyDynamics # ── shared constants ──────────────────────────────────────────────────────── diff --git a/simulation/tests/unit/test_force_balance.py b/tests/unit/test_force_balance.py similarity index 98% rename from simulation/tests/unit/test_force_balance.py rename to tests/unit/test_force_balance.py index 660529d..842c155 100644 --- a/simulation/tests/unit/test_force_balance.py +++ b/tests/unit/test_force_balance.py @@ -11,8 +11,8 @@ import pytest -from dynamics import RigidBodyDynamics -from frames import build_orb_frame +from simulation.dynamics import RigidBodyDynamics +from simulation.frames import build_orb_frame from tests.unit._aero_probe import load_rotor, make_probe, probe_steady diff --git a/simulation/tests/unit/test_full_loop_stability.py b/tests/unit/test_full_loop_stability.py similarity index 97% rename from simulation/tests/unit/test_full_loop_stability.py rename to tests/unit/test_full_loop_stability.py index 5034778..c8f684f 100644 --- a/simulation/tests/unit/test_full_loop_stability.py +++ b/tests/unit/test_full_loop_stability.py @@ -16,11 +16,11 @@ import pytest -from controller import HeliCyclicController, compute_rate_cmd, compute_bz_tether -from frames import build_orb_frame -from physics_core import PhysicsCore -from param_defaults import coll_rad_to_thrust -from rotor_physics import resolve_i_spin_kgm2 +from simulation.controller import HeliCyclicController, compute_rate_cmd, compute_bz_tether +from simulation.frames import build_orb_frame +from simulation.physics_core import PhysicsCore +from simulation.param_defaults import coll_rad_to_thrust +from simulation.rotor_physics import resolve_i_spin_kgm2 from tests.unit._aero_probe import load_rotor @@ -97,7 +97,7 @@ def _run_attitude_only_at_fixed_equilibrium( the angle to the fixed body_z_eq decays to < 1° within t_total. """ from dynbem import RotorInputs, solve_trim_cyclic - from controller import HeliCyclicController, compute_rate_cmd + from simulation.controller import HeliCyclicController, compute_rate_cmd el = math.radians(elevation_deg) pos_fixed = 100.0 * np.array([0.0, math.cos(el), -math.sin(el)]) @@ -279,13 +279,13 @@ def _run_with_constant_tether_force( are the only torques on the body). """ from dynbem import RotorInputs, solve_trim_cyclic - from controller import ( + from simulation.controller import ( HeliCyclicController, AltitudeHoldController, compute_rate_cmd, compute_bz_tether, damp_bz_eq_lateral, position_feedback_bz_eq, compute_crosswind_rate_cmd, apply_crosswind_rate_to_body_rates, ) - from dynamics import RigidBodyDynamics + from simulation.dynamics import RigidBodyDynamics from tests.unit._aero_probe import make_probe el = math.radians(elevation_deg) @@ -558,7 +558,7 @@ def _run_elastic_free_flight_with_python_ap( """ from types import SimpleNamespace from arduloop import HeliParams - from pumping_planner import TensionCommand + from simulation.pumping_planner import TensionCommand from tests.common.mock_ardupilot import MockArdupilot from tests.simtests.simtest_ic import load_ic from tests.simtests.simtest_runner import PhysicsRunner @@ -583,11 +583,11 @@ def _run_elastic_free_flight_with_python_ap( runner = PhysicsRunner( _ROTOR, ic, WIND, ) - from controller import HeliCyclicController as _Heli + from simulation.controller import HeliCyclicController as _Heli runner._acro = _Heli( _ROTOR, ) - from param_defaults import thrust_to_coll_rad as _t2c + from simulation.param_defaults import thrust_to_coll_rad as _t2c runner._acro._servo.reset(_t2c(ic.eq_thrust)) ap = MockArdupilot.for_pumping( @@ -599,7 +599,7 @@ def _run_elastic_free_flight_with_python_ap( wind=WIND, dt=DT, ) - from param_defaults import load_ap_params as _lp + from simulation.param_defaults import load_ap_params as _lp heli_params = HeliParams.from_ap_dict(_lp()) ap.enable_guided(heli_params) diff --git a/simulation/tests/unit/test_guided_attitude.py b/tests/unit/test_guided_attitude.py similarity index 99% rename from simulation/tests/unit/test_guided_attitude.py rename to tests/unit/test_guided_attitude.py index 515db66..7ca2cf0 100644 --- a/simulation/tests/unit/test_guided_attitude.py +++ b/tests/unit/test_guided_attitude.py @@ -44,7 +44,7 @@ def _make_ctrl( accel_max_cdss: float = 0.0, # 0 = linear P input_tc: float = 0.0, # 0 = no shaping delay (instantaneous) ) -> GuidedAttitudeController: - from param_defaults import load_ap_params as _lp + from simulation.param_defaults import load_ap_params as _lp hp = HeliParams.from_ap_dict(_lp()) gp = GuidedAttitudeParams( ATC_ANG_RLL_P=ang_p, @@ -323,7 +323,7 @@ def test_yaw_locked_to_gyro_at_large_thrust_error(self): # --- set_target_rotation matches Euler path --- def test_set_target_rotation_equals_euler(self): - from param_defaults import load_ap_params as _lp + from simulation.param_defaults import load_ap_params as _lp hp = HeliParams.from_ap_dict(_lp()) gp = GuidedAttitudeParams() roll, pitch, yaw = 5.0, -25.0, 45.0 diff --git a/simulation/tests/unit/test_hover_sign.py b/tests/unit/test_hover_sign.py similarity index 97% rename from simulation/tests/unit/test_hover_sign.py rename to tests/unit/test_hover_sign.py index b97930d..396f32b 100644 --- a/simulation/tests/unit/test_hover_sign.py +++ b/tests/unit/test_hover_sign.py @@ -17,7 +17,7 @@ import pytest -from frames import build_orb_frame +from simulation.frames import build_orb_frame from tests.unit._aero_probe import make_probe, probe_steady diff --git a/simulation/tests/unit/test_interfaces.py b/tests/unit/test_interfaces.py similarity index 96% rename from simulation/tests/unit/test_interfaces.py rename to tests/unit/test_interfaces.py index 27f6a2b..ac37a7d 100644 --- a/simulation/tests/unit/test_interfaces.py +++ b/tests/unit/test_interfaces.py @@ -8,7 +8,7 @@ import pytest -from sitl_interface import SIM_CLOCK_HZ, SITLInterface +from simulation.sitl_interface import SIM_CLOCK_HZ, SITLInterface def _get_free_udp_port(): @@ -18,7 +18,7 @@ def _get_free_udp_port(): def _make_servo_pkt_16(pwm: list[int], frame_rate: int = 400, frame_count: int = 1) -> bytes: - from sitl_interface import _SERVO_FMT_16, _SERVO_MAGIC_16 + from simulation.sitl_interface import _SERVO_FMT_16, _SERVO_MAGIC_16 pwm_full = (pwm + [1500] * 16)[:16] return struct.pack(_SERVO_FMT_16, _SERVO_MAGIC_16, frame_rate, frame_count, *pwm_full) @@ -128,7 +128,7 @@ def test_sitl_interface_recv_servos_keeps_last_good_packet_on_bad_packet(): def test_sitl_interface_recv_servos_32ch_reads_first_16(): - from sitl_interface import _SERVO_FMT_32, _SERVO_MAGIC_32 + from simulation.sitl_interface import _SERVO_FMT_32, _SERVO_MAGIC_32 recv_port = _get_free_udp_port() interface = SITLInterface(recv_port=recv_port, watchdog_timeout=0.05) interface.bind() diff --git a/simulation/tests/unit/test_math_lua.py b/tests/unit/test_math_lua.py similarity index 98% rename from simulation/tests/unit/test_math_lua.py rename to tests/unit/test_math_lua.py index afdfa65..c30f2de 100644 --- a/simulation/tests/unit/test_math_lua.py +++ b/tests/unit/test_math_lua.py @@ -20,12 +20,12 @@ import numpy as np import pytest - -from controller import ( +import simulation +from simulation.controller import ( compute_bz_altitude_hold, ) -from rawes_lua_harness import RawesLua -from rawes_modes import send_anchor_ned +from simulation.rawes_lua_harness import RawesLua +from simulation.rawes_modes import send_anchor_ned # ── Module-level harness ────────────────────────────────────────────────────── @@ -295,7 +295,7 @@ def test_first_angle_command_is_close_to_ic_body_z(self, sim): Using steady_state_starting.json, once GPS capture occurs in Lua steady mode, the first angle target should be close to the IC body_z. """ - ic_path = Path(__file__).resolve().parents[2] / "steady_state_starting.json" + ic_path = Path(simulation.__file__).resolve().parent / "steady_state_starting.json" ic = json.loads(ic_path.read_text(encoding="utf-8")) pos0 = np.array(ic["pos"], dtype=float) diff --git a/simulation/tests/unit/test_mock_uint32.py b/tests/unit/test_mock_uint32.py similarity index 97% rename from simulation/tests/unit/test_mock_uint32.py rename to tests/unit/test_mock_uint32.py index de06a59..4c2eb98 100644 --- a/simulation/tests/unit/test_mock_uint32.py +++ b/tests/unit/test_mock_uint32.py @@ -14,7 +14,7 @@ import pytest from lupa import lua54 -from rawes_lua_harness import RawesLua +from simulation.rawes_lua_harness import RawesLua class TestMockUint32: diff --git a/simulation/tests/unit/test_python_outer_loop_isolation.py b/tests/unit/test_python_outer_loop_isolation.py similarity index 98% rename from simulation/tests/unit/test_python_outer_loop_isolation.py rename to tests/unit/test_python_outer_loop_isolation.py index feb48a5..06be014 100644 --- a/simulation/tests/unit/test_python_outer_loop_isolation.py +++ b/tests/unit/test_python_outer_loop_isolation.py @@ -9,7 +9,7 @@ import numpy as np import pytest -from controller import TensionPI, compute_bz_tether, position_feedback_bz_eq +from simulation.controller import TensionPI, compute_bz_tether, position_feedback_bz_eq from tests.unit._aero_probe import load_rotor diff --git a/simulation/tests/unit/test_roll_sign_chain.py b/tests/unit/test_roll_sign_chain.py similarity index 98% rename from simulation/tests/unit/test_roll_sign_chain.py rename to tests/unit/test_roll_sign_chain.py index ea874d7..ea7038b 100644 --- a/simulation/tests/unit/test_roll_sign_chain.py +++ b/tests/unit/test_roll_sign_chain.py @@ -12,12 +12,11 @@ import numpy as np import pytest -sys.path.insert(0, str(Path(__file__).resolve().parents[2])) from tests.simtests.simtest_ic import load_ic from tests.simtests._rotor_helpers import load_default_rotor -from controller import HeliCyclicController, compute_rate_cmd -from swashplate import ardupilot_h3_120_forward, ardupilot_h3_120_inverse +from simulation.controller import HeliCyclicController, compute_rate_cmd +from simulation.swashplate import ardupilot_h3_120_forward, ardupilot_h3_120_inverse from dynbem import create_aero, RotorInputs diff --git a/simulation/tests/unit/test_rotor_definition.py b/tests/unit/test_rotor_definition.py similarity index 99% rename from simulation/tests/unit/test_rotor_definition.py rename to tests/unit/test_rotor_definition.py index d6081ba..debe1e8 100644 --- a/simulation/tests/unit/test_rotor_definition.py +++ b/tests/unit/test_rotor_definition.py @@ -12,7 +12,7 @@ from dynbem import rotor_definition as rd -from rotor_physics import resolve_i_spin_kgm2 +from simulation.rotor_physics import resolve_i_spin_kgm2 from tests.unit._aero_probe import load_rotor diff --git a/simulation/tests/unit/test_sensor.py b/tests/unit/test_sensor.py similarity index 98% rename from simulation/tests/unit/test_sensor.py rename to tests/unit/test_sensor.py index 57f31ed..435419f 100644 --- a/simulation/tests/unit/test_sensor.py +++ b/tests/unit/test_sensor.py @@ -5,8 +5,8 @@ import numpy as np -from frames import build_orb_frame -from sensor import PhysicalSensor, make_sensor, _rotation_matrix_to_euler_zyx +from simulation.frames import build_orb_frame +from simulation.sensor import PhysicalSensor, make_sensor, _rotation_matrix_to_euler_zyx def test_rotation_matrix_to_euler_identity_is_zero(): diff --git a/simulation/tests/unit/test_spin_safety_lua.py b/tests/unit/test_spin_safety_lua.py similarity index 98% rename from simulation/tests/unit/test_spin_safety_lua.py rename to tests/unit/test_spin_safety_lua.py index 68e5767..1c681ec 100644 --- a/simulation/tests/unit/test_spin_safety_lua.py +++ b/tests/unit/test_spin_safety_lua.py @@ -10,7 +10,7 @@ import pytest -from rawes_lua_harness import RawesLua +from simulation.rawes_lua_harness import RawesLua _OVER = 7.0 # rad/s > 2*pi (~6.283) -> over the 60 RPM limit _UNDER = 6.0 # rad/s < 2*pi -> within limits diff --git a/simulation/tests/unit/test_startup_trajectory.py b/tests/unit/test_startup_trajectory.py similarity index 98% rename from simulation/tests/unit/test_startup_trajectory.py rename to tests/unit/test_startup_trajectory.py index 27cf9bb..036ffcc 100644 --- a/simulation/tests/unit/test_startup_trajectory.py +++ b/tests/unit/test_startup_trajectory.py @@ -31,8 +31,8 @@ # --------------------------------------------------------------------------- _SIM_DIR = os.path.join(os.path.dirname(__file__), "..", "..") -from mediator import compute_launch_position -import config as _mcfg +from simulation.mediator import compute_launch_position +import simulation.config as _mcfg DEFAULT_POS0 = _mcfg.DEFAULTS["pos0"] DEFAULT_VEL0 = _mcfg.DEFAULTS["vel0"] @@ -315,7 +315,7 @@ class TestSmoothTrapezoidTrajectory: TARGET = np.array([3.0, -4.0, -43.0]) def _traj(self): - from kinematic import make_smooth_trapezoid_traj + from simulation.kinematic import make_smooth_trapezoid_traj direction = np.array([math.cos(self.YAW), math.sin(self.YAW), 0.0]) return make_smooth_trapezoid_traj( self.TARGET, direction, self.T, self.VMAX, self.ACCEL_S, self.DECEL_S @@ -385,7 +385,7 @@ def test_launch_offset_matches_formula(self): np.testing.assert_allclose(fn(0.0)[0], expected_launch, atol=1e-9) def test_zero_direction_raises(self): - from kinematic import make_smooth_trapezoid_traj + from simulation.kinematic import make_smooth_trapezoid_traj with pytest.raises(ValueError): make_smooth_trapezoid_traj( self.TARGET, np.zeros(3), self.T, self.VMAX, self.ACCEL_S, self.DECEL_S diff --git a/simulation/tests/unit/test_swashplate.py b/tests/unit/test_swashplate.py similarity index 99% rename from simulation/tests/unit/test_swashplate.py rename to tests/unit/test_swashplate.py index acdc21b..1787c94 100644 --- a/simulation/tests/unit/test_swashplate.py +++ b/tests/unit/test_swashplate.py @@ -13,7 +13,7 @@ import pytest -from swashplate import ( +from simulation.swashplate import ( ardupilot_h3_120_forward, ardupilot_h3_120_inverse, collective_out_to_rad, diff --git a/simulation/tests/unit/test_swashplate_aero.py b/tests/unit/test_swashplate_aero.py similarity index 99% rename from simulation/tests/unit/test_swashplate_aero.py rename to tests/unit/test_swashplate_aero.py index e7a287e..c1ce605 100644 --- a/simulation/tests/unit/test_swashplate_aero.py +++ b/tests/unit/test_swashplate_aero.py @@ -20,8 +20,8 @@ import pytest -from frames import build_orb_frame -from swashplate import ( +from simulation.frames import build_orb_frame +from simulation.swashplate import ( ardupilot_h3_120_forward, ardupilot_h3_120_inverse, collective_out_to_rad, diff --git a/simulation/tests/unit/test_swashplate_servo_directions.py b/tests/unit/test_swashplate_servo_directions.py similarity index 97% rename from simulation/tests/unit/test_swashplate_servo_directions.py rename to tests/unit/test_swashplate_servo_directions.py index 145d98d..370cc6d 100644 --- a/simulation/tests/unit/test_swashplate_servo_directions.py +++ b/tests/unit/test_swashplate_servo_directions.py @@ -14,8 +14,8 @@ import pytest -from swashplate import SwashplateServoModel, ardupilot_h3_120_forward, collective_rad_to_out -from param_defaults import load_collective_phys_range as _lr +from simulation.swashplate import SwashplateServoModel, ardupilot_h3_120_forward, collective_rad_to_out +from simulation.param_defaults import load_collective_phys_range as _lr _COL_MIN, _COL_MAX = _lr() diff --git a/simulation/tests/unit/test_swashplate_servo_model.py b/tests/unit/test_swashplate_servo_model.py similarity index 98% rename from simulation/tests/unit/test_swashplate_servo_model.py rename to tests/unit/test_swashplate_servo_model.py index d42d93f..da51e97 100644 --- a/simulation/tests/unit/test_swashplate_servo_model.py +++ b/tests/unit/test_swashplate_servo_model.py @@ -18,9 +18,9 @@ import pytest -from swashplate import SwashplateServoModel, collective_rad_to_out +from simulation.swashplate import SwashplateServoModel, collective_rad_to_out from dynbem import rotor_definition as rd -from param_defaults import load_collective_phys_range as _lr +from simulation.param_defaults import load_collective_phys_range as _lr _COL_MIN, _COL_MAX = _lr() diff --git a/simulation/tests/unit/test_tether_stability.py b/tests/unit/test_tether_stability.py similarity index 99% rename from simulation/tests/unit/test_tether_stability.py rename to tests/unit/test_tether_stability.py index 7506c6d..40f9f6b 100644 --- a/simulation/tests/unit/test_tether_stability.py +++ b/tests/unit/test_tether_stability.py @@ -22,7 +22,7 @@ import numpy as np -import mediator as _mediator_module +import simulation.mediator as _mediator_module from tests.unit._aero_probe import make_probe, probe_steady TetherModel = _mediator_module.TetherModel diff --git a/simulation/tests/unit/test_torque_model.py b/tests/unit/test_torque_model.py similarity index 99% rename from simulation/tests/unit/test_torque_model.py rename to tests/unit/test_torque_model.py index cc38feb..d9f6860 100644 --- a/simulation/tests/unit/test_torque_model.py +++ b/tests/unit/test_torque_model.py @@ -5,7 +5,7 @@ Docker, no ArduPilot SITL required). Run with: - .venv/Scripts/python.exe -m pytest simulation/tests/unit/test_torque_model.py -v + .venv/Scripts/python.exe -m pytest tests/unit/test_torque_model.py -v Tests ----- @@ -29,7 +29,7 @@ import pytest # simulation/ is two levels up from tests/unit/ -import torque_model as m +import simulation.torque_model as m # ── Simulation helpers ──────────────────────────────────────────────────────── diff --git a/simulation/tests/unit/test_vertical_landing_phases.py b/tests/unit/test_vertical_landing_phases.py similarity index 99% rename from simulation/tests/unit/test_vertical_landing_phases.py rename to tests/unit/test_vertical_landing_phases.py index 5dc3df0..61a220e 100644 --- a/simulation/tests/unit/test_vertical_landing_phases.py +++ b/tests/unit/test_vertical_landing_phases.py @@ -54,7 +54,7 @@ def _R_disk_horizontal() -> np.ndarray: """FRD: body_z = [0,0,+1] (down through the disk) for level hover.""" - from frames import build_orb_frame + from simulation.frames import build_orb_frame return build_orb_frame(np.array([0.0, 0.0, 1.0])) diff --git a/simulation/tests/unit/test_winch_controller.py b/tests/unit/test_winch_controller.py similarity index 99% rename from simulation/tests/unit/test_winch_controller.py rename to tests/unit/test_winch_controller.py index 9810aa0..fe4d666 100644 --- a/simulation/tests/unit/test_winch_controller.py +++ b/tests/unit/test_winch_controller.py @@ -2,7 +2,7 @@ import math import pytest -from winch import WinchController +from simulation.winch import WinchController # --------------------------------------------------------------------------- diff --git a/simulation/tests/unit/test_winch_tension_control.py b/tests/unit/test_winch_tension_control.py similarity index 99% rename from simulation/tests/unit/test_winch_tension_control.py rename to tests/unit/test_winch_tension_control.py index e1cc31e..91e4031 100644 --- a/simulation/tests/unit/test_winch_tension_control.py +++ b/tests/unit/test_winch_tension_control.py @@ -43,7 +43,7 @@ import pytest -from winch import WinchController +from simulation.winch import WinchController DT = 1.0 / 400.0 REST_INIT = 100.0 diff --git a/simulation/tests/unit/test_yaw_trim_lua.py b/tests/unit/test_yaw_trim_lua.py similarity index 99% rename from simulation/tests/unit/test_yaw_trim_lua.py rename to tests/unit/test_yaw_trim_lua.py index bb8e363..77f8c60 100644 --- a/simulation/tests/unit/test_yaw_trim_lua.py +++ b/tests/unit/test_yaw_trim_lua.py @@ -12,7 +12,7 @@ import math -from rawes_lua_harness import RawesLua +from simulation.rawes_lua_harness import RawesLua _DT = 0.01 # 100 Hz nominal tick diff --git a/simulation/tests/unit/test_yaw_trim_parity.py b/tests/unit/test_yaw_trim_parity.py similarity index 97% rename from simulation/tests/unit/test_yaw_trim_parity.py rename to tests/unit/test_yaw_trim_parity.py index 9ba49cf..5c5a645 100644 --- a/simulation/tests/unit/test_yaw_trim_parity.py +++ b/tests/unit/test_yaw_trim_parity.py @@ -11,8 +11,8 @@ import pytest -from controller import YawTrimObserver -from rawes_lua_harness import RawesLua +from simulation.controller import YawTrimObserver +from simulation.rawes_lua_harness import RawesLua _DT = 0.01 # 100 Hz nominal tick diff --git a/visualize.cmd b/visualize.cmd index 2d0a9d8..c7e3c5b 100644 --- a/visualize.cmd +++ b/visualize.cmd @@ -17,5 +17,5 @@ if not exist "%PY%" ( ) pushd "%ROOT%" >nul -start "RAWES visualization" cmd /k ""%PY%" simulation\viz3d\visualize_3d.py "%CSV%"" +start "RAWES visualization" cmd /k ""%PY%" viz3d\visualize_3d.py "%CSV%"" popd >nul diff --git a/viz3d/__init__.py b/viz3d/__init__.py new file mode 100644 index 0000000..69b0188 --- /dev/null +++ b/viz3d/__init__.py @@ -0,0 +1 @@ +# viz3d — 3D playback and telemetry stream for RAWES simulation. diff --git a/simulation/viz3d/render_cycle.py b/viz3d/render_cycle.py similarity index 98% rename from simulation/viz3d/render_cycle.py rename to viz3d/render_cycle.py index 58bb423..e23475d 100644 --- a/simulation/viz3d/render_cycle.py +++ b/viz3d/render_cycle.py @@ -28,8 +28,6 @@ # Ensure simulation/ is on sys.path when run from anywhere _HERE = Path(__file__).resolve().parent _SIM = _HERE.parent -if str(_SIM) not in sys.path: - sys.path.insert(0, str(_SIM)) # Add ffmpeg to PATH if it lives in the standard Windows location _FFMPEG = Path(r"C:\ffmpeg\bin") diff --git a/simulation/viz3d/scrub.py b/viz3d/scrub.py similarity index 97% rename from simulation/viz3d/scrub.py rename to viz3d/scrub.py index 738cfc8..7272e09 100644 --- a/simulation/viz3d/scrub.py +++ b/viz3d/scrub.py @@ -25,8 +25,6 @@ # Ensure simulation/ is on sys.path when run from anywhere _HERE = Path(__file__).resolve().parent _SIM = _HERE.parent -if str(_SIM) not in sys.path: - sys.path.insert(0, str(_SIM)) try: import pyvista as pv # noqa: F401 — fail fast with a clear message diff --git a/simulation/viz3d/telemetry.py b/viz3d/telemetry.py similarity index 95% rename from simulation/viz3d/telemetry.py rename to viz3d/telemetry.py index 0e2ebec..add77b0 100644 --- a/simulation/viz3d/telemetry.py +++ b/viz3d/telemetry.py @@ -12,7 +12,7 @@ orientation in the 3D renderer is exact. Usage: - from telemetry_csv import TelRow, write_csv + from simulation.telemetry_csv import TelRow, write_csv write_csv([TelRow.from_tel(d) for d in telemetry], "telemetry.csv") source = CSVSource("telemetry.csv") @@ -90,10 +90,7 @@ def __init__(self, path: str | Path) -> None: self._path = Path(path) def frames(self) -> Iterator[TelemetryFrame]: - _sim = str(Path(__file__).resolve().parents[1]) - if _sim not in sys.path: - sys.path.insert(0, _sim) - from telemetry_csv import read_csv as _read_csv # noqa: PLC0415 + from simulation.telemetry_csv import read_csv as _read_csv # noqa: PLC0415 for r in _read_csv(self._path): bzeq = r.body_z_eq diff --git a/simulation/viz3d/torque_telemetry.py b/viz3d/torque_telemetry.py similarity index 100% rename from simulation/viz3d/torque_telemetry.py rename to viz3d/torque_telemetry.py diff --git a/simulation/viz3d/visualize_3d.py b/viz3d/visualize_3d.py similarity index 99% rename from simulation/viz3d/visualize_3d.py rename to viz3d/visualize_3d.py index 6e59ffb..8807480 100644 --- a/simulation/viz3d/visualize_3d.py +++ b/viz3d/visualize_3d.py @@ -91,9 +91,8 @@ # Ensure viz3d package is importable when run as a script _HERE = Path(__file__).resolve().parent -_SIM = _HERE.parent -if str(_SIM) not in sys.path: - sys.path.insert(0, str(_SIM)) +import simulation as _simulation_pkg +_SIM = Path(_simulation_pkg.__file__).resolve().parent from viz3d.telemetry import TelemetryFrame, CSVSource @@ -101,7 +100,6 @@ # Rotor geometry — loaded from rotor_definitions/de_schutter_2018.yaml. # Falls back to the project default() if one is configured. # --------------------------------------------------------------------------- -import sys as _sys; _sys.path.insert(0, str(__import__('pathlib').Path(__file__).parents[1])) from dynbem import rotor_definition as _rd try: _ROTOR = _rd.default() diff --git a/simulation/viz3d/visualize_torque.py b/viz3d/visualize_torque.py similarity index 98% rename from simulation/viz3d/visualize_torque.py rename to viz3d/visualize_torque.py index adaefdd..18d7a50 100644 --- a/simulation/viz3d/visualize_torque.py +++ b/viz3d/visualize_torque.py @@ -28,8 +28,8 @@ Usage ----- - python simulation/viz3d/visualize_torque.py simulation/logs/torque_telemetry.csv - python simulation/viz3d/visualize_torque.py telemetry.csv --export out.gif --fps 15 + python viz3d/visualize_torque.py simulation/logs/torque_telemetry.csv + python viz3d/visualize_torque.py telemetry.csv --export out.gif --fps 15 """ from __future__ import annotations @@ -42,17 +42,12 @@ import numpy as np -_HERE = Path(__file__).resolve().parent -_SIM = _HERE.parent -for _p in [str(_HERE), str(_SIM)]: - if _p not in sys.path: - sys.path.insert(0, _p) - +import simulation import dataclasses as _dc -from torque_telemetry import TorqueTelemetryFrame -from telemetry_csv import read_csv as _read_csv -from mediator_torque import PROFILES as _MEDIATOR_PROFILES -from torque_model import HubParams as _HubParams, equilibrium_throttle as _eq_throttle +from viz3d.torque_telemetry import TorqueTelemetryFrame +from simulation.telemetry_csv import read_csv as _read_csv +from simulation.mediator_torque import PROFILES as _MEDIATOR_PROFILES +from simulation.torque_model import HubParams as _HubParams, equilibrium_throttle as _eq_throttle _MODEL_PARAMS = _HubParams() # default GB4008 params (same as mediator default) @@ -845,7 +840,7 @@ def main() -> None: if args.telemetry: all_paths = [Path(args.telemetry)] else: - log_dir = Path(__file__).resolve().parents[1] / "logs" + log_dir = Path(simulation.__file__).resolve().parent / "logs" all_paths = sorted(log_dir.glob("torque_telemetry*.csv")) if not all_paths: print(f"No torque_telemetry*.csv found in {log_dir}")