From e901fa957909e466a5c144c00c2f7b20f8baf255 Mon Sep 17 00:00:00 2001 From: robinson <18656133516@qq.com> Date: Fri, 3 Jul 2026 04:19:44 +0800 Subject: [PATCH] init --- assets/mujoco/quadcopter.xml | 2 + assets/mujoco/scene.xml | 2 + control.toml | 54 +++ environment.toml | 26 ++ examples/eval_control.py | 320 +++++++++++++++ examples/fly_geometric.py | 121 ++++++ examples/fly_hover.py | 121 ++++++ examples/fly_takeoff_forward.py | 119 ++++++ output/eval_control/circle.png | Bin 0 -> 216119 bytes output/eval_control/summary.png | Bin 0 -> 38040 bytes pyproject.toml | 27 ++ requirements.txt | 6 + src/uavcatch.egg-info/PKG-INFO | 11 + src/uavcatch.egg-info/SOURCES.txt | 17 + src/uavcatch.egg-info/dependency_links.txt | 1 + src/uavcatch.egg-info/requires.txt | 9 + src/uavcatch.egg-info/top_level.txt | 1 + src/uavcatch/__init__.py | 31 ++ .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 766 bytes .../__pycache__/config.cpython-313.pyc | Bin 0 -> 21629 bytes src/uavcatch/config.py | 382 ++++++++++++++++++ src/uavcatch/control/__init__.py | 30 ++ .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 941 bytes .../__pycache__/allocator.cpython-313.pyc | Bin 0 -> 2602 bytes .../__pycache__/attitude.cpython-313.pyc | Bin 0 -> 3031 bytes .../__pycache__/cascade.cpython-313.pyc | Bin 0 -> 8658 bytes .../__pycache__/factory.cpython-313.pyc | Bin 0 -> 2332 bytes .../__pycache__/math3d.cpython-313.pyc | Bin 0 -> 5843 bytes .../control/__pycache__/pid.cpython-313.pyc | Bin 0 -> 11108 bytes .../__pycache__/position.cpython-313.pyc | Bin 0 -> 6831 bytes .../control/__pycache__/types.cpython-313.pyc | Bin 0 -> 2016 bytes src/uavcatch/control/allocator.py | 46 +++ src/uavcatch/control/attitude.py | 46 +++ src/uavcatch/control/cascade.py | 148 +++++++ src/uavcatch/control/factory.py | 49 +++ src/uavcatch/control/math3d.py | 98 +++++ src/uavcatch/control/pid.py | 208 ++++++++++ src/uavcatch/control/position.py | 100 +++++ src/uavcatch/control/types.py | 42 ++ src/uavcatch/dynamics/__init__.py | 5 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 367 bytes .../__pycache__/model.cpython-313.pyc | Bin 0 -> 10157 bytes .../__pycache__/state.cpython-313.pyc | Bin 0 -> 6305 bytes .../dynamics/__pycache__/wind.cpython-313.pyc | Bin 0 -> 3280 bytes src/uavcatch/dynamics/model.py | 146 +++++++ src/uavcatch/dynamics/state.py | 110 +++++ src/uavcatch/dynamics/wind.py | 52 +++ src/uavcatch/eval/__init__.py | 18 + .../eval/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 645 bytes .../__pycache__/maneuvers.cpython-313.pyc | Bin 0 -> 7062 bytes .../eval/__pycache__/metrics.cpython-313.pyc | Bin 0 -> 8663 bytes .../eval/__pycache__/plots.cpython-313.pyc | Bin 0 -> 8458 bytes .../eval/__pycache__/recorder.cpython-313.pyc | Bin 0 -> 6160 bytes src/uavcatch/eval/maneuvers.py | 175 ++++++++ src/uavcatch/eval/metrics.py | 208 ++++++++++ src/uavcatch/eval/plots.py | 126 ++++++ src/uavcatch/eval/recorder.py | 72 ++++ src/uavcatch/sim/__init__.py | 4 + .../sim/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 299 bytes .../sim/__pycache__/engine.cpython-313.pyc | Bin 0 -> 5388 bytes .../__pycache__/mujoco_model.cpython-313.pyc | Bin 0 -> 10362 bytes .../__pycache__/mujoco_viz.cpython-313.pyc | Bin 0 -> 8919 bytes src/uavcatch/sim/engine.py | 90 +++++ src/uavcatch/sim/mujoco_model.py | 162 ++++++++ src/uavcatch/sim/mujoco_viz.py | 142 +++++++ .../test_control.cpython-313-pytest-9.1.1.pyc | Bin 0 -> 8291 bytes ...test_dynamics.cpython-313-pytest-9.1.1.pyc | Bin 0 -> 23496 bytes .../test_eval.cpython-313-pytest-9.1.1.pyc | Bin 0 -> 11759 bytes tests/test_control.py | 105 +++++ tests/test_dynamics.py | 183 +++++++++ tests/test_eval.py | 72 ++++ uav.toml | 28 ++ 72 files changed, 3715 insertions(+) create mode 100644 assets/mujoco/quadcopter.xml create mode 100644 assets/mujoco/scene.xml create mode 100644 control.toml create mode 100644 environment.toml create mode 100644 examples/eval_control.py create mode 100644 examples/fly_geometric.py create mode 100644 examples/fly_hover.py create mode 100644 examples/fly_takeoff_forward.py create mode 100644 output/eval_control/circle.png create mode 100644 output/eval_control/summary.png create mode 100644 pyproject.toml create mode 100644 requirements.txt create mode 100644 src/uavcatch.egg-info/PKG-INFO create mode 100644 src/uavcatch.egg-info/SOURCES.txt create mode 100644 src/uavcatch.egg-info/dependency_links.txt create mode 100644 src/uavcatch.egg-info/requires.txt create mode 100644 src/uavcatch.egg-info/top_level.txt create mode 100644 src/uavcatch/__init__.py create mode 100644 src/uavcatch/__pycache__/__init__.cpython-313.pyc create mode 100644 src/uavcatch/__pycache__/config.cpython-313.pyc create mode 100644 src/uavcatch/config.py create mode 100644 src/uavcatch/control/__init__.py create mode 100644 src/uavcatch/control/__pycache__/__init__.cpython-313.pyc create mode 100644 src/uavcatch/control/__pycache__/allocator.cpython-313.pyc create mode 100644 src/uavcatch/control/__pycache__/attitude.cpython-313.pyc create mode 100644 src/uavcatch/control/__pycache__/cascade.cpython-313.pyc create mode 100644 src/uavcatch/control/__pycache__/factory.cpython-313.pyc create mode 100644 src/uavcatch/control/__pycache__/math3d.cpython-313.pyc create mode 100644 src/uavcatch/control/__pycache__/pid.cpython-313.pyc create mode 100644 src/uavcatch/control/__pycache__/position.cpython-313.pyc create mode 100644 src/uavcatch/control/__pycache__/types.cpython-313.pyc create mode 100644 src/uavcatch/control/allocator.py create mode 100644 src/uavcatch/control/attitude.py create mode 100644 src/uavcatch/control/cascade.py create mode 100644 src/uavcatch/control/factory.py create mode 100644 src/uavcatch/control/math3d.py create mode 100644 src/uavcatch/control/pid.py create mode 100644 src/uavcatch/control/position.py create mode 100644 src/uavcatch/control/types.py create mode 100644 src/uavcatch/dynamics/__init__.py create mode 100644 src/uavcatch/dynamics/__pycache__/__init__.cpython-313.pyc create mode 100644 src/uavcatch/dynamics/__pycache__/model.cpython-313.pyc create mode 100644 src/uavcatch/dynamics/__pycache__/state.cpython-313.pyc create mode 100644 src/uavcatch/dynamics/__pycache__/wind.cpython-313.pyc create mode 100644 src/uavcatch/dynamics/model.py create mode 100644 src/uavcatch/dynamics/state.py create mode 100644 src/uavcatch/dynamics/wind.py create mode 100644 src/uavcatch/eval/__init__.py create mode 100644 src/uavcatch/eval/__pycache__/__init__.cpython-313.pyc create mode 100644 src/uavcatch/eval/__pycache__/maneuvers.cpython-313.pyc create mode 100644 src/uavcatch/eval/__pycache__/metrics.cpython-313.pyc create mode 100644 src/uavcatch/eval/__pycache__/plots.cpython-313.pyc create mode 100644 src/uavcatch/eval/__pycache__/recorder.cpython-313.pyc create mode 100644 src/uavcatch/eval/maneuvers.py create mode 100644 src/uavcatch/eval/metrics.py create mode 100644 src/uavcatch/eval/plots.py create mode 100644 src/uavcatch/eval/recorder.py create mode 100644 src/uavcatch/sim/__init__.py create mode 100644 src/uavcatch/sim/__pycache__/__init__.cpython-313.pyc create mode 100644 src/uavcatch/sim/__pycache__/engine.cpython-313.pyc create mode 100644 src/uavcatch/sim/__pycache__/mujoco_model.cpython-313.pyc create mode 100644 src/uavcatch/sim/__pycache__/mujoco_viz.cpython-313.pyc create mode 100644 src/uavcatch/sim/engine.py create mode 100644 src/uavcatch/sim/mujoco_model.py create mode 100644 src/uavcatch/sim/mujoco_viz.py create mode 100644 tests/__pycache__/test_control.cpython-313-pytest-9.1.1.pyc create mode 100644 tests/__pycache__/test_dynamics.cpython-313-pytest-9.1.1.pyc create mode 100644 tests/__pycache__/test_eval.cpython-313-pytest-9.1.1.pyc create mode 100644 tests/test_control.py create mode 100644 tests/test_dynamics.py create mode 100644 tests/test_eval.py create mode 100644 uav.toml diff --git a/assets/mujoco/quadcopter.xml b/assets/mujoco/quadcopter.xml new file mode 100644 index 0000000..a77d5d3 --- /dev/null +++ b/assets/mujoco/quadcopter.xml @@ -0,0 +1,2 @@ +# Static snapshot for reference only. +# Runtime visualization uses procedural MJCF from uavcatch.sim.mujoco_model. diff --git a/assets/mujoco/scene.xml b/assets/mujoco/scene.xml new file mode 100644 index 0000000..a77d5d3 --- /dev/null +++ b/assets/mujoco/scene.xml @@ -0,0 +1,2 @@ +# Static snapshot for reference only. +# Runtime visualization uses procedural MJCF from uavcatch.sim.mujoco_model. diff --git a/control.toml b/control.toml new file mode 100644 index 0000000..4d561f3 --- /dev/null +++ b/control.toml @@ -0,0 +1,54 @@ +[controller] +# linear: 世界系位置 PID + 姿态内环 +# geometric: SE(3) 几何位置控制 + 四元数姿态内环 +mode = "geometric" +max_tilt = 0.35 # rad,仅 linear 外环限幅 + +[position.linear] +kp = 0.8 +ki = 0.2 +kd = 0.9 + +[position.geometric] +kp = 2.5 +ki = 0.4 +kd = 2.0 + +[attitude.linear] +kp = 8.0 +ki = 0.5 +kd = 0.6 + +[attitude.geometric] +kp = 10.0 +ki = 0.8 +kd = 0.8 + +[eval] +initial_height = 2.0 + +[eval.hover] +duration = 8.0 + +[eval.step] +step_time = 2.0 +duration = 12.0 +step_z = 3.0 +step_x = 3.0 +step_y = 2.0 + +[eval.takeoff_forward] +ground_z = 0.0 +hover_z = 2.0 +forward_x = 4.0 +takeoff_time = 3.0 +hover_time = 2.0 +forward_time = 6.0 + +[eval.circle] +ground_z = 0.0 +takeoff_time = 2.0 +radius = 2.5 +height = 2.0 +period = 10.0 +duration = 20.0 diff --git a/environment.toml b/environment.toml new file mode 100644 index 0000000..34c6ca5 --- /dev/null +++ b/environment.toml @@ -0,0 +1,26 @@ +[scene] +# 场景边界(米),以原点为中心 +size_x = 20.0 +size_y = 20.0 +size_z = 10.0 +ground_z = 0.0 + +[physics] +gravity = 9.81 +air_density = 1.225 + +[simulation] +dt = 0.003 +duration = 10.0 +integrator = "rk4" + +[viewer] +enable = true +fps = 60 +camera_distance = 8.0 + +[wind] +# 环境风(世界系, m/s)。drag 基于相对空速 v_rel = 机体速度 - 风速 计算 +velocity = [0.0, 0.0, 0.0] # 恒定风速 [vx, vy, vz] +gust_amplitude = 0.0 # 阵风幅值(m/s),沿风向叠加正弦扰动 +gust_frequency = 0.0 # 阵风频率(Hz) diff --git a/examples/eval_control.py b/examples/eval_control.py new file mode 100644 index 0000000..c05b4d7 --- /dev/null +++ b/examples/eval_control.py @@ -0,0 +1,320 @@ +#!/usr/bin/env python3 + +"""Control system performance evaluation: maneuvers + time-domain metrics.""" + + + +from __future__ import annotations + + + +import argparse + +import sys + +from pathlib import Path + + + +import numpy as np + + + +ROOT = Path(__file__).resolve().parents[1] + +if str(ROOT) not in sys.path: + + sys.path.insert(0, str(ROOT / "src")) + + + +from uavcatch.config import load_control_config, load_environment_config, load_uav_config + +from uavcatch.control import TrackingController + +from uavcatch.control.factory import build_cascaded_controller + +from uavcatch.eval import ( + + SimRecorder, + + default_maneuver_suite, + + evaluate_maneuver, + + plot_maneuver_report, + + plot_summary_comparison, + +) + +from uavcatch.eval.maneuvers import ManeuverSpec + +from uavcatch.sim.engine import SimEngine + + + + + +def parse_args() -> argparse.Namespace: + + parser = argparse.ArgumentParser(description="Evaluate cascaded flight controller performance") + + parser.add_argument("--environment", type=Path, default=ROOT / "environment.toml") + + parser.add_argument("--uav", type=Path, default=ROOT / "uav.toml") + + parser.add_argument("--control", type=Path, default=ROOT / "control.toml") + + parser.add_argument( + + "--output", + + type=Path, + + default=ROOT / "output" / "eval_control", + + help="Directory for plots", + + ) + + parser.add_argument("--show", action="store_true", help="Show matplotlib windows") + + parser.add_argument( + + "--maneuver", + + type=str, + + default="all", + + help="Run one maneuver by name, or 'all'", + + ) + + return parser.parse_args() + + + + + +def run_maneuver( + + spec: ManeuverSpec, + + env_cfg, + + uav_cfg, + + control_cfg, + +) -> tuple[dict[str, np.ndarray], object]: + + env_cfg.simulation.duration = spec.duration + + if spec.initial_position is not None: + + uav_cfg.initial.position = list(spec.initial_position) + + engine = SimEngine(env_cfg=env_cfg, uav_cfg=uav_cfg) + + engine.reset() + + + + init = spec.target_fn(0.0) + + if hasattr(init, "position"): + + init_pos, init_vel = init.position, init.velocity + + else: + + init_pos, init_vel = np.asarray(init), np.zeros(3) + + + + controller = build_cascaded_controller( + + control_cfg, + + engine.dynamics, + + uav_cfg, + + target_position=init_pos, + + target_velocity=init_vel, + + ) + + controller.reset() + + tracker = TrackingController( + + controller=controller, + + target_fn=spec.target_fn, + + yaw_fn=spec.yaw_fn, + + accel_fn=spec.accel_fn, + + ) + + + + recorder = SimRecorder() + + + + def on_step(state, t, omega): + + tgt_pos, tgt_vel = recorder.resolve_target(spec.target_fn, t) + + recorder.record(t, state, tgt_pos, tgt_vel, omega) + + + + engine.run(tracker, duration=spec.duration, on_step=on_step) + + return recorder.as_arrays(), engine + + + + + +def print_metrics_table(metrics_list: list) -> None: + + print("\n" + "=" * 72) + + print(f"{'Maneuver':<22} {'Mode':<10} {'RMSE3D':>8} {'MaxErr':>8} {'Sat%':>6}") + + print("-" * 72) + + for m in metrics_list: + + print( + + f"{m.name:<22} {m.mode:<10} " + + f"{m.tracking.rmse_3d:8.4f} {m.tracking.max_error_3d:8.4f} " + + f"{m.control.saturation_ratio * 100:6.1f}" + + ) + + print("=" * 72) + + for m in metrics_list: + + if m.step is not None: + + rise = f"{m.step.rise_time:.3f}s" if m.step.rise_time is not None else "N/A" + + settle = f"{m.step.settling_time:.3f}s" if m.step.settling_time is not None else "N/A" + + print( + + f" [{m.name}] step {m.step.axis}: rise={rise}, settle={settle}, " + + f"overshoot={m.step.overshoot_pct:.1f}%, ss_err={m.step.steady_state_error:.4f} m" + + ) + + + + + +def main() -> int: + + args = parse_args() + + env_cfg = load_environment_config(args.environment) + + uav_cfg = load_uav_config(args.uav) + + control_cfg = load_control_config(args.control) + + uav_cfg.initial.position = [0.0, 0.0, control_cfg.eval.initial_height] + + uav_cfg.initial.velocity = [0.0, 0.0, 0.0] + + + + suite = default_maneuver_suite(control_cfg.eval) + + if args.maneuver != "all": + + suite = [s for s in suite if s.name == args.maneuver] + + if not suite: + + names = ", ".join(s.name for s in default_maneuver_suite(control_cfg.eval)) + + print(f"Unknown maneuver '{args.maneuver}'. Available: {names}") + + return 1 + + + + mode = control_cfg.controller.mode + + print(f"Controller mode: {mode} (from {args.control})") + + + + all_metrics = [] + + for spec in suite: + + print(f"Running: {spec.name} - {spec.description}") + + data, engine = run_maneuver(spec, env_cfg, uav_cfg, control_cfg) + + metrics = evaluate_maneuver( + + spec, + + data, + + omega_max=uav_cfg.rotor.omega_max, + + mode=mode, + + ) + + all_metrics.append(metrics) + + + + out_file = args.output / f"{spec.name}.png" + + plot_maneuver_report(data, metrics, save_path=out_file, show=args.show) + + import matplotlib.pyplot as plt + + plt.close("all") + + print(f" -> saved {out_file}") + + + + summary_path = args.output / "summary.png" + + plot_summary_comparison(all_metrics, save_path=summary_path, show=args.show) + + print(f"\nSummary plot: {summary_path}") + + print_metrics_table(all_metrics) + + return 0 + + + + + +if __name__ == "__main__": + + raise SystemExit(main()) + diff --git a/examples/fly_geometric.py b/examples/fly_geometric.py new file mode 100644 index 0000000..0c9a1ff --- /dev/null +++ b/examples/fly_geometric.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +"""SE(3) geometric control demo: circular trajectory tracking.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import numpy as np + +ROOT = Path(__file__).resolve().parents[1] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT / "src")) + +from uavcatch.config import load_control_config, load_environment_config, load_uav_config +from uavcatch.control import TrackingController, circle_trajectory +from uavcatch.control.factory import build_cascaded_controller +from uavcatch.dynamics.state import QuadState +from uavcatch.sim.engine import SimEngine +from uavcatch.sim.mujoco_viz import MuJoCoViewer + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Geometric SE(3) circular trajectory tracking demo", + ) + parser.add_argument("--environment", type=Path, default=ROOT / "environment.toml") + parser.add_argument("--uav", type=Path, default=ROOT / "uav.toml") + parser.add_argument("--control", type=Path, default=ROOT / "control.toml") + parser.add_argument("--radius", type=float, default=3.0, help="Circle radius (m)") + parser.add_argument("--height", type=float, default=2.0, help="Flight height (m)") + parser.add_argument("--period", type=float, default=12.0, help="One lap period (s)") + parser.add_argument("--duration", type=float, default=24.0, help="Simulation duration (s)") + parser.add_argument("--no-viewer", action="store_true") + parser.add_argument( + "--speed", + type=float, + default=1.0, + help="Playback speed (1.0 = real time, <1 slow motion)", + ) + return parser.parse_args() + + +def main() -> int: + args = parse_args() + env_cfg = load_environment_config(args.environment) + uav_cfg = load_uav_config(args.uav) + control_cfg = load_control_config(args.control) + env_cfg.simulation.duration = args.duration + + if control_cfg.controller.mode != "geometric": + print( + f"Warning: control.toml mode is '{control_cfg.controller.mode}'; " + "circle tracking works best with mode = \"geometric\"." + ) + + engine = SimEngine(env_cfg=env_cfg, uav_cfg=uav_cfg) + engine.reset() + + center = np.array([0.0, 0.0, 0.0], dtype=np.float64) + pos_fn, yaw_fn, accel_fn = circle_trajectory( + center=center, + radius=args.radius, + height=args.height, + period=args.period, + ) + init_target = pos_fn(0.0) + controller = build_cascaded_controller( + control_cfg, + engine.dynamics, + uav_cfg, + target_position=init_target.position, + target_velocity=init_target.velocity, + target_yaw=float(yaw_fn(0.0)), + ) + tracker = TrackingController( + controller=controller, + target_fn=pos_fn, + yaw_fn=yaw_fn, + accel_fn=accel_fn, + ) + + duration = args.duration + dt = env_cfg.simulation.dt + use_viewer = env_cfg.viewer.enable and not args.no_viewer + + if use_viewer: + with MuJoCoViewer(env_cfg, uav_cfg=uav_cfg) as viewer: + def on_step(state: QuadState, t: float, omega: np.ndarray) -> None: + viewer.render_realtime(state, omega, dt, speed=args.speed) + if not viewer.is_running(): + raise KeyboardInterrupt + + try: + engine.run(tracker, duration=duration, on_step=on_step) + except KeyboardInterrupt: + pass + else: + engine.run(tracker, duration=duration) + + final = engine.state + final_target = pos_fn(duration) + pos_err = float(np.linalg.norm(final.position - final_target.position)) + print(f"Simulation finished at t={engine.time:.2f}s") + print(f"Final position: {final.position}") + print(f"Target position: {final_target.position}") + print(f"Position error: {pos_err:.4f} m") + print(f"Final velocity: {final.velocity}") + + if pos_err > 1.0: + print("Warning: circular tracking error exceeds 1.0 m") + return 1 + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except KeyboardInterrupt: + raise SystemExit(130) from None diff --git a/examples/fly_hover.py b/examples/fly_hover.py new file mode 100644 index 0000000..1f1e126 --- /dev/null +++ b/examples/fly_hover.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +"""Hover demo: PID-controlled single quadcopter in MuJoCo viewer.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import numpy as np + +ROOT = Path(__file__).resolve().parents[1] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT / "src")) + +from uavcatch.config import load_control_config, load_environment_config, load_uav_config +from uavcatch.control.pid import HoverPIDController +from uavcatch.dynamics.state import QuadState +from uavcatch.sim.engine import SimEngine +from uavcatch.sim.mujoco_viz import MuJoCoViewer + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Single-UAV hover simulation demo") + parser.add_argument( + "--environment", + type=Path, + default=ROOT / "environment.toml", + help="Path to environment.toml", + ) + parser.add_argument( + "--uav", + type=Path, + default=ROOT / "uav.toml", + help="Path to uav.toml", + ) + parser.add_argument( + "--control", + type=Path, + default=ROOT / "control.toml", + help="Path to control.toml", + ) + parser.add_argument( + "--target-z", + type=float, + default=2.0, + help="Target hover height (m)", + ) + parser.add_argument( + "--no-viewer", + action="store_true", + help="Run without MuJoCo viewer", + ) + parser.add_argument( + "--speed", + type=float, + default=1.0, + help="Playback speed (1.0 = real time, <1 slow motion)", + ) + return parser.parse_args() + + +def main() -> int: + args = parse_args() + env_cfg = load_environment_config(args.environment) + uav_cfg = load_uav_config(args.uav) + control_cfg = load_control_config(args.control) + engine = SimEngine(env_cfg=env_cfg, uav_cfg=uav_cfg) + engine.reset() + + target = np.array([0.0, 0.0, args.target_z], dtype=np.float64) + controller = HoverPIDController( + dynamics=engine.dynamics, + uav_cfg=uav_cfg, + target_position=target, + control_cfg=control_cfg, + ) + + duration = env_cfg.simulation.duration + dt = env_cfg.simulation.dt + use_viewer = env_cfg.viewer.enable and not args.no_viewer + + if use_viewer: + with MuJoCoViewer(env_cfg, uav_cfg=uav_cfg) as viewer: + last_state: QuadState | None = None + last_omega: np.ndarray | None = None + + def on_step(state: QuadState, t: float, omega: np.ndarray) -> None: + nonlocal last_state, last_omega + last_state = state + last_omega = omega + viewer.render_realtime(state, omega, dt, speed=args.speed) + if not viewer.is_running(): + raise KeyboardInterrupt + + try: + history = engine.run(controller, duration=duration, on_step=on_step) + except KeyboardInterrupt: + history = [] + else: + history = engine.run(controller, duration=duration) + + final = engine.state + pos_err = np.linalg.norm(final.position - target) + print(f"Simulation finished at t={engine.time:.2f}s") + print(f"Final position: {final.position}") + print(f"Target position: {target}") + print(f"Position error: {pos_err:.4f} m") + print(f"Final velocity: {final.velocity}") + + if pos_err > 0.1: + print("Warning: position error exceeds 0.1 m threshold") + return 1 + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except KeyboardInterrupt: + raise SystemExit(130) from None diff --git a/examples/fly_takeoff_forward.py b/examples/fly_takeoff_forward.py new file mode 100644 index 0000000..d65559a --- /dev/null +++ b/examples/fly_takeoff_forward.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +"""Take off from ground, hover, then fly forward along +X.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import numpy as np + +ROOT = Path(__file__).resolve().parents[1] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT / "src")) + +from uavcatch.config import load_control_config, load_environment_config, load_uav_config +from uavcatch.control.pid import ( + HoverPIDController, + TrackingPIDController, + phased_takeoff_hover_forward, +) +from uavcatch.dynamics.state import QuadState +from uavcatch.sim.engine import SimEngine +from uavcatch.sim.mujoco_viz import MuJoCoViewer + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Ground takeoff -> hover -> forward flight demo", + ) + parser.add_argument("--environment", type=Path, default=ROOT / "environment.toml") + parser.add_argument("--uav", type=Path, default=ROOT / "uav.toml") + parser.add_argument("--control", type=Path, default=ROOT / "control.toml") + parser.add_argument("--hover-z", type=float, default=2.0, help="Hover height (m)") + parser.add_argument("--forward-x", type=float, default=5.0, help="Forward distance (m)") + parser.add_argument("--takeoff-time", type=float, default=4.0, help="Takeoff phase (s)") + parser.add_argument("--hover-time", type=float, default=3.0, help="Hover phase (s)") + parser.add_argument("--forward-time", type=float, default=8.0, help="Forward phase (s)") + parser.add_argument("--no-viewer", action="store_true") + parser.add_argument( + "--speed", + type=float, + default=1.0, + help="Playback speed (1.0 = real time, <1 slow motion)", + ) + return parser.parse_args() + + +def main() -> int: + args = parse_args() + env_cfg = load_environment_config(args.environment) + uav_cfg = load_uav_config(args.uav) + control_cfg = load_control_config(args.control) + + ground_z = env_cfg.scene.ground_z + uav_cfg.initial.position = [0.0, 0.0, ground_z + 0.05] + uav_cfg.initial.velocity = [0.0, 0.0, 0.0] + uav_cfg.initial.attitude_euler = [0.0, 0.0, 0.0] + uav_cfg.initial.angular_rate = [0.0, 0.0, 0.0] + + duration = args.takeoff_time + args.hover_time + args.forward_time + env_cfg.simulation.duration = duration + + engine = SimEngine(env_cfg=env_cfg, uav_cfg=uav_cfg) + engine.reset() + + target_fn = phased_takeoff_hover_forward( + ground_z=ground_z, + hover_z=args.hover_z, + forward_x=args.forward_x, + takeoff_duration=args.takeoff_time, + hover_duration=args.hover_time, + forward_duration=args.forward_time, + ) + hover_pid = HoverPIDController( + dynamics=engine.dynamics, + uav_cfg=uav_cfg, + target_position=target_fn(0.0).position, + control_cfg=control_cfg, + ) + controller = TrackingPIDController(pid=hover_pid, target_fn=target_fn) + + final_target = target_fn(duration).position + dt = env_cfg.simulation.dt + use_viewer = env_cfg.viewer.enable and not args.no_viewer + + if use_viewer: + with MuJoCoViewer(env_cfg, uav_cfg=uav_cfg) as viewer: + def on_step(state: QuadState, t: float, omega: np.ndarray) -> None: + viewer.render_realtime(state, omega, dt, speed=args.speed) + if not viewer.is_running(): + raise KeyboardInterrupt + + try: + engine.run(controller, duration=duration, on_step=on_step) + except KeyboardInterrupt: + pass + else: + engine.run(controller, duration=duration) + + final = engine.state + pos_err = np.linalg.norm(final.position - final_target) + print(f"Simulation finished at t={engine.time:.2f}s") + print(f"Final position: {final.position}") + print(f"Expected target: {final_target}") + print(f"Position error: {pos_err:.4f} m") + print(f"Final velocity: {final.velocity}") + + if pos_err > 0.5: + print("Warning: final tracking error exceeds 0.5 m") + return 1 + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except KeyboardInterrupt: + raise SystemExit(130) from None diff --git a/output/eval_control/circle.png b/output/eval_control/circle.png new file mode 100644 index 0000000000000000000000000000000000000000..37eb5622efb3117d87f9847002ffc9ce7c567063 GIT binary patch literal 216119 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 ~^0K%91W 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 z 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 O(P+ua#c>ONI`m&%k8M=| literal 0 HcmV?d00001 diff --git a/output/eval_control/summary.png b/output/eval_control/summary.png new file mode 100644 index 0000000000000000000000000000000000000000..b854b2c397e4c086402bc17dcd933ca9134b0561 GIT binary patch literal 38040 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

4Pruv2&X!liIA(#%+vHV^Z{v`%H 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 literal 0 HcmV?d00001 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..2c3e686 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,27 @@ +[build-system] +requires = ["setuptools>=61"] +build-backend = "setuptools.build_meta" + +[project] +name = "uavcatch" +version = "0.1.0" +description = "Multi-UAV pursuit simulation with explicit quadcopter dynamics" +requires-python = ">=3.10" +dependencies = [ + "numpy>=1.24", + "mujoco>=3.0", + "scipy>=1.10", + "matplotlib>=3.7", + "tomli>=2.0.1; python_version < '3.11'", +] + +[project.optional-dependencies] +dev = [ + "pytest>=7.0", +] + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.pytest.ini_options] +testpaths = ["tests"] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..783dfa7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +numpy>=1.24 +mujoco>=3.0 +scipy>=1.10 +matplotlib>=3.7 +tomli>=2.0.1; python_version < '3.11' +pytest>=7.0 diff --git a/src/uavcatch.egg-info/PKG-INFO b/src/uavcatch.egg-info/PKG-INFO new file mode 100644 index 0000000..5d77f39 --- /dev/null +++ b/src/uavcatch.egg-info/PKG-INFO @@ -0,0 +1,11 @@ +Metadata-Version: 2.4 +Name: uavcatch +Version: 0.1.0 +Summary: Multi-UAV pursuit simulation with explicit quadcopter dynamics +Requires-Python: >=3.10 +Requires-Dist: numpy>=1.24 +Requires-Dist: mujoco>=3.0 +Requires-Dist: scipy>=1.10 +Requires-Dist: tomli>=2.0.1; python_version < "3.11" +Provides-Extra: dev +Requires-Dist: pytest>=7.0; extra == "dev" diff --git a/src/uavcatch.egg-info/SOURCES.txt b/src/uavcatch.egg-info/SOURCES.txt new file mode 100644 index 0000000..10482f8 --- /dev/null +++ b/src/uavcatch.egg-info/SOURCES.txt @@ -0,0 +1,17 @@ +pyproject.toml +src/uavcatch/__init__.py +src/uavcatch/config.py +src/uavcatch.egg-info/PKG-INFO +src/uavcatch.egg-info/SOURCES.txt +src/uavcatch.egg-info/dependency_links.txt +src/uavcatch.egg-info/requires.txt +src/uavcatch.egg-info/top_level.txt +src/uavcatch/control/__init__.py +src/uavcatch/control/pid.py +src/uavcatch/dynamics/__init__.py +src/uavcatch/dynamics/model.py +src/uavcatch/dynamics/state.py +src/uavcatch/sim/__init__.py +src/uavcatch/sim/engine.py +src/uavcatch/sim/mujoco_viz.py +tests/test_dynamics.py \ No newline at end of file diff --git a/src/uavcatch.egg-info/dependency_links.txt b/src/uavcatch.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/uavcatch.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/src/uavcatch.egg-info/requires.txt b/src/uavcatch.egg-info/requires.txt new file mode 100644 index 0000000..0a89b41 --- /dev/null +++ b/src/uavcatch.egg-info/requires.txt @@ -0,0 +1,9 @@ +numpy>=1.24 +mujoco>=3.0 +scipy>=1.10 + +[:python_version < "3.11"] +tomli>=2.0.1 + +[dev] +pytest>=7.0 diff --git a/src/uavcatch.egg-info/top_level.txt b/src/uavcatch.egg-info/top_level.txt new file mode 100644 index 0000000..6cfa3d5 --- /dev/null +++ b/src/uavcatch.egg-info/top_level.txt @@ -0,0 +1 @@ +uavcatch diff --git a/src/uavcatch/__init__.py b/src/uavcatch/__init__.py new file mode 100644 index 0000000..6197fab --- /dev/null +++ b/src/uavcatch/__init__.py @@ -0,0 +1,31 @@ +"""UAVCatch: multi-UAV pursuit simulation with explicit quadcopter dynamics.""" + +from __future__ import annotations + +from uavcatch.config import ( + ControlConfig, + EnvironmentConfig, + UavConfig, + load_control_config, + load_environment_config, + load_uav_config, +) +from uavcatch.dynamics import QuadDynamics, QuadState, WindModel +from uavcatch.sim import MuJoCoViewer, SimEngine + +__version__ = "0.1.0" + +__all__ = [ + "ControlConfig", + "EnvironmentConfig", + "UavConfig", + "QuadDynamics", + "QuadState", + "WindModel", + "SimEngine", + "MuJoCoViewer", + "load_control_config", + "load_environment_config", + "load_uav_config", + "__version__", +] diff --git a/src/uavcatch/__pycache__/__init__.cpython-313.pyc b/src/uavcatch/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..423ad19d4dd20b75b8fe1e76f8c6a2ffea9e1899 GIT binary patch literal 766 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 literal 0 HcmV?d00001 diff --git a/src/uavcatch/config.py b/src/uavcatch/config.py new file mode 100644 index 0000000..b366199 --- /dev/null +++ b/src/uavcatch/config.py @@ -0,0 +1,382 @@ +"""Load project settings from TOML configuration files.""" + +from __future__ import annotations + +import sys +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Literal + +IntegratorKind = Literal["euler", "rk4"] +RotorLayout = Literal["x", "plus"] +ControlMode = Literal["linear", "geometric"] + + +def _load_toml(path: Path) -> dict[str, Any]: + data = path.read_bytes() + if sys.version_info >= (3, 11): + import tomllib + + return tomllib.loads(data.decode("utf-8")) + import tomli + + return tomli.loads(data.decode("utf-8")) + + +@dataclass +class SceneConfig: + size_x: float = 20.0 + size_y: float = 20.0 + size_z: float = 10.0 + ground_z: float = 0.0 + + +@dataclass +class PhysicsConfig: + gravity: float = 9.81 + air_density: float = 1.225 + + +@dataclass +class SimulationConfig: + dt: float = 0.002 + duration: float = 10.0 + integrator: IntegratorKind = "rk4" + + +@dataclass +class ViewerConfig: + enable: bool = True + fps: int = 60 + camera_distance: float = 8.0 + + +@dataclass +class WindConfig: + """Ambient wind field (world frame).""" + + velocity: list[float] = field(default_factory=lambda: [0.0, 0.0, 0.0]) + gust_amplitude: float = 0.0 + gust_frequency: float = 0.0 + + +@dataclass +class EnvironmentConfig: + scene: SceneConfig = field(default_factory=SceneConfig) + physics: PhysicsConfig = field(default_factory=PhysicsConfig) + simulation: SimulationConfig = field(default_factory=SimulationConfig) + viewer: ViewerConfig = field(default_factory=ViewerConfig) + wind: WindConfig = field(default_factory=WindConfig) + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> EnvironmentConfig: + return EnvironmentConfig( + scene=_parse_scene(data.get("scene", {})), + physics=_parse_physics(data.get("physics", {})), + simulation=_parse_simulation(data.get("simulation", {})), + viewer=_parse_viewer(data.get("viewer", {})), + wind=_parse_wind(data.get("wind", {})), + ) + + +@dataclass +class BodyConfig: + mass: float = 1.0 + arm_length: float = 0.25 + Ixx: float = 0.01 + Iyy: float = 0.01 + Izz: float = 0.02 + + +@dataclass +class RotorConfig: + kf: float = 8.11e-6 + km: float = 2.03e-7 + omega_min: float = 0.0 + omega_max: float = 838.0 + layout: RotorLayout = "x" + + +@dataclass +class InitialConfig: + position: list[float] = field(default_factory=lambda: [0.0, 0.0, 1.0]) + velocity: list[float] = field(default_factory=lambda: [0.0, 0.0, 0.0]) + attitude_euler: list[float] = field(default_factory=lambda: [0.0, 0.0, 0.0]) + angular_rate: list[float] = field(default_factory=lambda: [0.0, 0.0, 0.0]) + + +@dataclass +class DragConfig: + """Aerodynamic drag acting on the air-relative velocity. + + Total translational drag force (world frame): + F = -linear * v_rel - 0.5 * rho * cd * area * |v_rel| * v_rel + where v_rel = v_body - v_wind, rho = physics.air_density. + """ + + linear: float = 0.0 + angular: float = 0.05 + cd: float = 0.8 + area: float = 0.1 + + +@dataclass +class UavConfig: + body: BodyConfig = field(default_factory=BodyConfig) + rotor: RotorConfig = field(default_factory=RotorConfig) + initial: InitialConfig = field(default_factory=InitialConfig) + drag: DragConfig = field(default_factory=DragConfig) + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> UavConfig: + return UavConfig( + body=_parse_body(data.get("body", {})), + rotor=_parse_rotor(data.get("rotor", {})), + initial=_parse_initial(data.get("initial", {})), + drag=_parse_drag(data.get("drag", {})), + ) + + +def _parse_scene(d: dict[str, Any]) -> SceneConfig: + return SceneConfig( + size_x=float(d.get("size_x", 20.0)), + size_y=float(d.get("size_y", 20.0)), + size_z=float(d.get("size_z", 10.0)), + ground_z=float(d.get("ground_z", 0.0)), + ) + + +def _parse_physics(d: dict[str, Any]) -> PhysicsConfig: + return PhysicsConfig( + gravity=float(d.get("gravity", 9.81)), + air_density=float(d.get("air_density", 1.225)), + ) + + +def _parse_simulation(d: dict[str, Any]) -> SimulationConfig: + integrator = d.get("integrator", "rk4") + if integrator not in ("euler", "rk4"): + integrator = "rk4" + return SimulationConfig( + dt=float(d.get("dt", 0.002)), + duration=float(d.get("duration", 10.0)), + integrator=integrator, + ) + + +def _parse_viewer(d: dict[str, Any]) -> ViewerConfig: + return ViewerConfig( + enable=bool(d.get("enable", True)), + fps=int(d.get("fps", 60)), + camera_distance=float(d.get("camera_distance", 8.0)), + ) + + +def _parse_wind(d: dict[str, Any]) -> WindConfig: + velocity = list(d.get("velocity", [0.0, 0.0, 0.0])) + return WindConfig( + velocity=[float(v) for v in velocity], + gust_amplitude=float(d.get("gust_amplitude", 0.0)), + gust_frequency=float(d.get("gust_frequency", 0.0)), + ) + + +def _parse_body(d: dict[str, Any]) -> BodyConfig: + return BodyConfig( + mass=float(d.get("mass", 1.0)), + arm_length=float(d.get("arm_length", 0.25)), + Ixx=float(d.get("Ixx", 0.01)), + Iyy=float(d.get("Iyy", 0.01)), + Izz=float(d.get("Izz", 0.02)), + ) + + +def _parse_rotor(d: dict[str, Any]) -> RotorConfig: + layout = d.get("layout", "x") + if layout not in ("x", "plus"): + layout = "x" + return RotorConfig( + kf=float(d.get("kf", 3.16e-10)), + km=float(d.get("km", 7.94e-12)), + omega_min=float(d.get("omega_min", 0.0)), + omega_max=float(d.get("omega_max", 838.0)), + layout=layout, + ) + + +def _parse_initial(d: dict[str, Any]) -> InitialConfig: + return InitialConfig( + position=list(d.get("position", [0.0, 0.0, 1.0])), + velocity=list(d.get("velocity", [0.0, 0.0, 0.0])), + attitude_euler=list(d.get("attitude_euler", [0.0, 0.0, 0.0])), + angular_rate=list(d.get("angular_rate", [0.0, 0.0, 0.0])), + ) + + +def _parse_drag(d: dict[str, Any]) -> DragConfig: + return DragConfig( + linear=float(d.get("linear", 0.0)), + angular=float(d.get("angular", 0.05)), + cd=float(d.get("cd", 0.8)), + area=float(d.get("area", 0.1)), + ) + + +def load_environment_config(path: str | Path = "environment.toml") -> EnvironmentConfig: + config_path = Path(path) + if not config_path.is_file(): + raise FileNotFoundError(f"Environment config not found: {config_path}") + return EnvironmentConfig.from_dict(_load_toml(config_path)) + + +def load_uav_config(path: str | Path = "uav.toml") -> UavConfig: + config_path = Path(path) + if not config_path.is_file(): + raise FileNotFoundError(f"UAV config not found: {config_path}") + return UavConfig.from_dict(_load_toml(config_path)) + + +@dataclass +class GainsConfig: + kp: float = 0.8 + ki: float = 0.2 + kd: float = 0.9 + + +@dataclass +class ControllerSection: + mode: ControlMode = "linear" + max_tilt: float = 0.35 + + +@dataclass +class EvalHoverConfig: + duration: float = 8.0 + + +@dataclass +class EvalStepConfig: + step_time: float = 2.0 + duration: float = 12.0 + step_z: float = 3.0 + step_x: float = 3.0 + step_y: float = 2.0 + + +@dataclass +class EvalTakeoffForwardConfig: + ground_z: float = 0.0 + hover_z: float = 2.0 + forward_x: float = 4.0 + takeoff_time: float = 3.0 + hover_time: float = 2.0 + forward_time: float = 6.0 + + +@dataclass +class EvalCircleConfig: + ground_z: float = 0.0 + takeoff_time: float = 2.0 + radius: float = 2.5 + height: float = 2.0 + period: float = 10.0 + duration: float = 20.0 + + +@dataclass +class EvalConfig: + initial_height: float = 2.0 + hover: EvalHoverConfig = field(default_factory=EvalHoverConfig) + step: EvalStepConfig = field(default_factory=EvalStepConfig) + takeoff_forward: EvalTakeoffForwardConfig = field(default_factory=EvalTakeoffForwardConfig) + circle: EvalCircleConfig = field(default_factory=EvalCircleConfig) + + +@dataclass +class ControlConfig: + controller: ControllerSection = field(default_factory=ControllerSection) + position_linear: GainsConfig = field(default_factory=lambda: GainsConfig(kp=0.8, ki=0.2, kd=0.9)) + position_geometric: GainsConfig = field( + default_factory=lambda: GainsConfig(kp=2.5, ki=0.4, kd=2.0) + ) + attitude_linear: GainsConfig = field( + default_factory=lambda: GainsConfig(kp=8.0, ki=0.5, kd=0.6) + ) + attitude_geometric: GainsConfig = field( + default_factory=lambda: GainsConfig(kp=10.0, ki=0.8, kd=0.8) + ) + eval: EvalConfig = field(default_factory=EvalConfig) + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> ControlConfig: + return ControlConfig( + controller=_parse_controller(data.get("controller", {})), + position_linear=_parse_gains(data.get("position", {}).get("linear", {}), 0.8, 0.2, 0.9), + position_geometric=_parse_gains( + data.get("position", {}).get("geometric", {}), 2.5, 0.4, 2.0 + ), + attitude_linear=_parse_gains(data.get("attitude", {}).get("linear", {}), 8.0, 0.5, 0.6), + attitude_geometric=_parse_gains( + data.get("attitude", {}).get("geometric", {}), 10.0, 0.8, 0.8 + ), + eval=_parse_eval(data.get("eval", {})), + ) + + +def _parse_gains(d: dict[str, Any], kp: float, ki: float, kd: float) -> GainsConfig: + return GainsConfig( + kp=float(d.get("kp", kp)), + ki=float(d.get("ki", ki)), + kd=float(d.get("kd", kd)), + ) + + +def _parse_controller(d: dict[str, Any]) -> ControllerSection: + mode = d.get("mode", "linear") + if mode not in ("linear", "geometric"): + mode = "linear" + return ControllerSection( + mode=mode, + max_tilt=float(d.get("max_tilt", 0.35)), + ) + + +def _parse_eval(d: dict[str, Any]) -> EvalConfig: + hover_d = d.get("hover", {}) + step_d = d.get("step", {}) + takeoff_d = d.get("takeoff_forward", {}) + circle_d = d.get("circle", {}) + return EvalConfig( + initial_height=float(d.get("initial_height", 2.0)), + hover=EvalHoverConfig(duration=float(hover_d.get("duration", 8.0))), + step=EvalStepConfig( + step_time=float(step_d.get("step_time", 2.0)), + duration=float(step_d.get("duration", 12.0)), + step_z=float(step_d.get("step_z", 3.0)), + step_x=float(step_d.get("step_x", 3.0)), + step_y=float(step_d.get("step_y", 2.0)), + ), + takeoff_forward=EvalTakeoffForwardConfig( + ground_z=float(takeoff_d.get("ground_z", 0.0)), + hover_z=float(takeoff_d.get("hover_z", 2.0)), + forward_x=float(takeoff_d.get("forward_x", 4.0)), + takeoff_time=float(takeoff_d.get("takeoff_time", 3.0)), + hover_time=float(takeoff_d.get("hover_time", 2.0)), + forward_time=float(takeoff_d.get("forward_time", 6.0)), + ), + circle=EvalCircleConfig( + ground_z=float(circle_d.get("ground_z", 0.0)), + takeoff_time=float(circle_d.get("takeoff_time", 2.0)), + radius=float(circle_d.get("radius", 2.5)), + height=float(circle_d.get("height", 2.0)), + period=float(circle_d.get("period", 10.0)), + duration=float(circle_d.get("duration", 20.0)), + ), + ) + + +def load_control_config(path: str | Path = "control.toml") -> ControlConfig: + config_path = Path(path) + if not config_path.is_file(): + raise FileNotFoundError(f"Control config not found: {config_path}") + return ControlConfig.from_dict(_load_toml(config_path)) diff --git a/src/uavcatch/control/__init__.py b/src/uavcatch/control/__init__.py new file mode 100644 index 0000000..35ad3c8 --- /dev/null +++ b/src/uavcatch/control/__init__.py @@ -0,0 +1,30 @@ +from uavcatch.control.allocator import ControlAllocator +from uavcatch.control.attitude import AttitudeController +from uavcatch.control.cascade import CascadedController, TrackingController +from uavcatch.control.factory import build_cascaded_controller +from uavcatch.control.pid import ( + HoverPIDController, + TrackingPIDController, + circle_trajectory, + phased_takeoff_hover_forward, + smoothstep, +) +from uavcatch.control.position import GeometricPositionController, LinearPositionController +from uavcatch.control.types import PIDGains, PhaseTarget + +__all__ = [ + "ControlAllocator", + "AttitudeController", + "CascadedController", + "TrackingController", + "build_cascaded_controller", + "GeometricPositionController", + "LinearPositionController", + "HoverPIDController", + "TrackingPIDController", + "PIDGains", + "PhaseTarget", + "circle_trajectory", + "phased_takeoff_hover_forward", + "smoothstep", +] diff --git a/src/uavcatch/control/__pycache__/__init__.cpython-313.pyc b/src/uavcatch/control/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1aa54be1ba7a228b428e4498de7da2dc73497e8f GIT binary patch literal 941 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 O2ev7|@Bt1P4F3n!+@E^@ literal 0 HcmV?d00001 diff --git a/src/uavcatch/control/__pycache__/cascade.cpython-313.pyc b/src/uavcatch/control/__pycache__/cascade.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2d0f842b86cf1dc7cd0022528d1a0543e351513e GIT binary patch literal 8658 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 F{|%jM%*g-% literal 0 HcmV?d00001 diff --git a/src/uavcatch/control/__pycache__/factory.cpython-313.pyc b/src/uavcatch/control/__pycache__/factory.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..07b33f8eaef90a51ffdbfb17b882fb389019cbf1 GIT binary patch literal 2332 zcmZ`)-HRJl6uk?!g+olMfK$xOU6 zsm)Sw^+81+tU_rELhF+j1Q7)PjUf=mgLg)bu!X^ZO+y9h;tEh-DDsn}>z~y<&X8{WZ0gJR33sOFUBl#$f zGF~dg@-mhgjue!99LE`s77}?Cs}w86*kQCI;^Z8fL&Z4Q@qficQ3lT)4yUSurtZG< zTB}wsXHCcUuv4#F*v(e3(+G*2=QQdtsMM-0Y24mqkOt;8;zRjnp_YpW~0>7 z;BosWDSH)_(B{Zw2sil<4f`Uu_6|Zu2EiI}$cM=0C?5$&!`|l4&d{0IkO>i6!BQMD zL2vV0YRHth&18s&J!L0`qYzPcGQ`8a$e$2K_T$Iy$B*5QKWM*IehF#929c|l)37{T zGc|$m?>FhMMt@~ZA&NN=0TCNc*&>ro$E`um^}#AKX?R}EYn81LE;aD0*KEtcBsFjf z2n;#IaWDxm5G;Crynmu@R;omxn-GC6bm2|OX0znz>9y@9c6Kb&+b*4Yd)@HN3)?O> zw_CgRzYpxPuPpc306r8GSA=VkuOruw-aK~W*e&z>%6AoCeE#kM zd`}V-v4;df+^6Rwb5wE=4kqsRn7FqF!Y`qh`?y7=-K=gwR9Gu~<;IAEu<3nM<`?<0JFTwp{I1S!UgP8*}4<-xd888cAXl9TLNv5w1Yj~OtNaj2gcHQjP-SAzYg%@jd z{xGcilA1mAcmf?>`Jr$pbKK{r_O!e5Y*#(hO+49^(;a!|+5%<S9M-@zs^TW|Q!CBn1)EN=>sAJT%m662X?&!uD>)Yc=cE zR`|JC$%g1%jo+wJPu8^LaA^=U2ulz-`ZW&1x2xFAoLy#7XQAAEVn}2!XEz+%V$mfM zO9_4!Rzm8u2@=)yvSaExQFOi1qF1wY9fOpJWVag4HeLpt3hPsj79jd5#PBQ_Za`*6 zr{Ig5>nDhvK&JjT4Ptd!`6SbW$xyU8sM2-Jo)T&t%j|8$;uxqPc9OcZ>ej1xmCi#! zacydP0>^Q`qvL;|g$HQzPqcauy>bsdcMrYzIHGW3ZwVb)=}u*O>NF>IQ&05-+TTyi q^d#ETtS1ijqJUy3J=c=~DJZ?%ivvoaRGO{pB~fJN?-b?Oef|ZG_WM@= literal 0 HcmV?d00001 diff --git a/src/uavcatch/control/__pycache__/math3d.cpython-313.pyc b/src/uavcatch/control/__pycache__/math3d.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6fdc413c92f0a6081e5f6d19509052bd847f2305 GIT binary patch literal 5843 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 literal 0 HcmV?d00001 diff --git a/src/uavcatch/control/__pycache__/types.cpython-313.pyc b/src/uavcatch/control/__pycache__/types.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2cb0db34ac989bbbfe546f5bba49e17040195e07 GIT binary patch literal 2016 zcma)7&1>976dy^ekNvPWad$Uq($dA)X*aEH3U*qT5=fmUt_`khH9e#PF_yHpT&=Vj zjRN*55NNJB)R2}!daR+)e`Onw>kqLQ;LVu_kBlQF@o5UoHn35^aOLH=lo5(Bk%3OgJQob;+&S^}W)0v*i z>b${9WYRYL$UC~>HFc!bKdY$@VyjzB?a7_?VF68gJ@cHn$TV!EN6NYVvwBy;fDA{%= za+3i0if!LYXpn2Fw%ztw%mY94!U)u`aNupQP`B+EU+UPDbLKB6+>33S4Z_Vm_=x!M z>L*s0MQfhJts7UqnW5ZSwPNO23BB#4LXM%8Vc1<~2AoR?`X8SLa*sUM%6D7aIr+ZD z8vQ*d!*>zX{|i19K+{^fC@-$k*!!BY6_4)@!RfzCVRt-qDPwe97~LrLF`UAmEeD+@kDNHR~k>{Mz$4q9df<^3oxYKHK50xXApZ! zHM6Lzz-btDOM`0BAybWMiRmV;7t?GgF=M6Ns)*`h?)psB>Cn*KJrAcgCcaE~H{s2w z-Q=rYv;Vy2L^Fa2vaukFy8Yx1JT}g-=!M2tSrT)GCd82I#Xi&k<_jDN0i8q7!AgU4 zmU^?mY?5z*W@9NY>%NRLJVH9KBZuPP4pW|%M@UOq*af3lWU7k^(^7;Sc7?j(u_zXL zn(LP)k3>Wmp9ZwE?9$8P>=8J|`BY3PDymmF_j%%a{p7g-XOAWi6n?q~lE)9jlaeRR7Yz`*mGsfk zPeO-{f%y!Xlkk{8EfVx1#^#XYQF|)avR9+Fbvm_!ZLOW~gh5+l_;^uuq9A}igl&18 zE;|gCwoy!Vm}?P)|V|0OF@Wsd;qDIo5pVkK3`(wRL1Bv+tV zB9+?x8~3k2SbBK#$D6;t|M=~xN42TPntF=h Ezogm2=>Px# literal 0 HcmV?d00001 diff --git a/src/uavcatch/control/allocator.py b/src/uavcatch/control/allocator.py new file mode 100644 index 0000000..2cb3c46 --- /dev/null +++ b/src/uavcatch/control/allocator.py @@ -0,0 +1,46 @@ +"""Motor mixing / control allocation.""" + +from __future__ import annotations + +from dataclasses import dataclass, field + +import numpy as np + +from uavcatch.config import UavConfig + + +@dataclass +class ControlAllocator: + """Map total thrust and body torques to per-rotor squared speeds.""" + + uav_cfg: UavConfig + _mixing_matrix: np.ndarray = field(init=False) + + def __post_init__(self) -> None: + self._mixing_matrix = np.array( + [ + [1.0, 1.0, 1.0, 1.0], + [0.0, -1.0, 0.0, 1.0], + [-1.0, 0.0, 1.0, 0.0], + [1.0, -1.0, 1.0, -1.0], + ], + dtype=np.float64, + ) + + def allocate(self, thrust: float, torque: np.ndarray) -> np.ndarray: + kf = self.uav_cfg.rotor.kf + km = self.uav_cfg.rotor.km + arm = self.uav_cfg.body.arm_length + tx, ty, tz = np.asarray(torque, dtype=np.float64) + + if self.uav_cfg.rotor.layout == "plus": + tx, ty = ty, tx + + rhs = np.array( + [thrust / kf, tx / (kf * arm), ty / (kf * arm), tz / km], + dtype=np.float64, + ) + sq = np.linalg.solve(self._mixing_matrix, rhs) + max_sq = self.uav_cfg.rotor.omega_max ** 2 + sq = np.clip(sq, 0.0, max_sq) + return np.sqrt(sq) diff --git a/src/uavcatch/control/attitude.py b/src/uavcatch/control/attitude.py new file mode 100644 index 0000000..007a884 --- /dev/null +++ b/src/uavcatch/control/attitude.py @@ -0,0 +1,46 @@ +"""Quaternion-based attitude controller.""" + +from __future__ import annotations + +from dataclasses import dataclass, field + +import numpy as np + +from uavcatch.control.math3d import quaternion_attitude_error +from uavcatch.control.types import AttitudeCommand, PIDGains +from uavcatch.dynamics.state import QuadState, normalize_quaternion + + +@dataclass +class AttitudeController: + """Inner-loop attitude control using quaternion error and PID.""" + + gains: PIDGains = field(default_factory=lambda: PIDGains(kp=8.0, ki=0.5, kd=0.6)) + _integral: np.ndarray = field(default_factory=lambda: np.zeros(3, dtype=np.float64)) + _integral_limit: float = 0.5 + + def reset(self) -> None: + self._integral[:] = 0.0 + + def compute( + self, + state: QuadState, + desired_quaternion: np.ndarray, + desired_angular_rate: np.ndarray | None = None, + dt: float = 0.002, + ) -> AttitudeCommand: + q_des = normalize_quaternion(desired_quaternion) + e_att = quaternion_attitude_error(q_des, state.quaternion) + self._integral += e_att * dt + self._integral = np.clip(self._integral, -self._integral_limit, self._integral_limit) + + if desired_angular_rate is None: + desired_angular_rate = np.zeros(3, dtype=np.float64) + rate_err = state.angular_rate - desired_angular_rate + + torque = ( + -self.gains.kp * e_att + + self.gains.ki * self._integral + - self.gains.kd * rate_err + ) + return AttitudeCommand(torque=torque.astype(np.float64)) diff --git a/src/uavcatch/control/cascade.py b/src/uavcatch/control/cascade.py new file mode 100644 index 0000000..221225c --- /dev/null +++ b/src/uavcatch/control/cascade.py @@ -0,0 +1,148 @@ +"""Cascaded 6-DOF flight controller.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Literal + +import numpy as np + +from uavcatch.config import UavConfig +from uavcatch.control.allocator import ControlAllocator +from uavcatch.control.attitude import AttitudeController +from uavcatch.control.position import GeometricPositionController, LinearPositionController +from uavcatch.control.types import PIDGains, PhaseTarget, TargetFn +from uavcatch.dynamics.model import QuadDynamics +from uavcatch.dynamics.state import QuadState + +PositionMode = Literal["linear", "geometric"] + + +@dataclass +class CascadedController: + """Modular cascade: position -> attitude (quaternion) -> motor allocation.""" + + dynamics: QuadDynamics + uav_cfg: UavConfig + mode: PositionMode = "linear" + target_position: np.ndarray = field(default_factory=lambda: np.zeros(3)) + target_velocity: np.ndarray = field(default_factory=lambda: np.zeros(3)) + target_yaw: float = 0.0 + target_acceleration: np.ndarray | None = None + pos_gains: PIDGains = field( + default_factory=lambda: PIDGains(kp=0.8, ki=0.2, kd=0.9) + ) + att_gains: PIDGains = field( + default_factory=lambda: PIDGains(kp=8.0, ki=0.5, kd=0.6) + ) + max_tilt: float = 0.35 + allocator: ControlAllocator = field(init=False) + attitude: AttitudeController = field(init=False) + _position_linear: LinearPositionController = field(init=False) + _position_geometric: GeometricPositionController = field(init=False) + + def __post_init__(self) -> None: + mass = self.uav_cfg.body.mass + gravity = self.dynamics.env_cfg.physics.gravity + max_thrust = 4.0 * self.uav_cfg.rotor.kf * self.uav_cfg.rotor.omega_max ** 2 + self.allocator = ControlAllocator(self.uav_cfg) + self.attitude = AttitudeController(gains=self.att_gains) + self._position_linear = LinearPositionController( + mass=mass, + gravity=gravity, + max_thrust=max_thrust, + gains=self.pos_gains, + max_tilt=self.max_tilt, + ) + self._position_geometric = GeometricPositionController( + mass=mass, + gravity=gravity, + max_thrust=max_thrust, + gains=self.pos_gains, + ) + + def reset(self) -> None: + self.attitude.reset() + self._position_linear.reset() + self._position_geometric.reset() + + def __call__(self, state: QuadState, t: float) -> np.ndarray: + del t + dt = self.dynamics.env_cfg.simulation.dt + return self.compute(state, dt) + + def compute(self, state: QuadState, dt: float | None = None) -> np.ndarray: + if dt is None: + dt = self.dynamics.env_cfg.simulation.dt + + if self.mode == "geometric": + pos_cmd = self._position_geometric.compute( + state, + self.target_position, + self.target_velocity, + self.target_yaw, + dt, + target_acceleration=self.target_acceleration, + ) + else: + pos_cmd = self._position_linear.compute( + state, + self.target_position, + self.target_velocity, + self.target_yaw, + dt, + ) + + att_cmd = self.attitude.compute( + state, + pos_cmd.desired_quaternion, + pos_cmd.desired_angular_rate, + dt, + ) + omega = self.allocator.allocate(pos_cmd.thrust, att_cmd.torque) + return self.dynamics.clip_omega(omega) + + +@dataclass +class TrackingController: + """Time-varying setpoint wrapper for CascadedController.""" + + controller: CascadedController + target_fn: TargetFn + yaw_fn: TargetFn | None = None + accel_fn: TargetFn | None = None + _prev_t: float = field(default=-1.0, init=False) + _prev_pos: np.ndarray | None = field(default=None, init=False) + + def __call__(self, state: QuadState, t: float) -> np.ndarray: + target = self.target_fn(t) + if isinstance(target, PhaseTarget): + pos = np.asarray(target.position, dtype=np.float64) + vel = np.asarray(target.velocity, dtype=np.float64) + else: + pos = np.asarray(target, dtype=np.float64) + vel = self._estimate_target_velocity(pos, t) + + self.controller.target_position = pos + self.controller.target_velocity = vel + if self.yaw_fn is not None: + yaw = self.yaw_fn(t) + self.controller.target_yaw = float(yaw if np.isscalar(yaw) else yaw[0]) + if self.accel_fn is not None: + accel = self.accel_fn(t) + self.controller.target_acceleration = np.asarray(accel, dtype=np.float64) + else: + self.controller.target_acceleration = None + + return self.controller(state, t) + + def _estimate_target_velocity(self, pos: np.ndarray, t: float) -> np.ndarray: + if self._prev_pos is None or self._prev_t < 0.0: + self._prev_pos = pos.copy() + self._prev_t = t + return np.zeros(3, dtype=np.float64) + dt = max(t - self._prev_t, 1e-9) + vel = (pos - self._prev_pos) / dt + self._prev_pos = pos.copy() + self._prev_t = t + return vel.astype(np.float64) diff --git a/src/uavcatch/control/factory.py b/src/uavcatch/control/factory.py new file mode 100644 index 0000000..807da5e --- /dev/null +++ b/src/uavcatch/control/factory.py @@ -0,0 +1,49 @@ +"""Build controllers from control.toml configuration.""" + +from __future__ import annotations + +import numpy as np + +from uavcatch.config import ControlConfig, UavConfig +from uavcatch.control.cascade import CascadedController +from uavcatch.control.types import PIDGains +from uavcatch.dynamics.model import QuadDynamics + + +def gains_from_control(control_cfg: ControlConfig) -> tuple[PIDGains, PIDGains]: + if control_cfg.controller.mode == "geometric": + pos = control_cfg.position_geometric + att = control_cfg.attitude_geometric + else: + pos = control_cfg.position_linear + att = control_cfg.attitude_linear + return ( + PIDGains(kp=pos.kp, ki=pos.ki, kd=pos.kd), + PIDGains(kp=att.kp, ki=att.ki, kd=att.kd), + ) + + +def build_cascaded_controller( + control_cfg: ControlConfig, + dynamics: QuadDynamics, + uav_cfg: UavConfig, + target_position: np.ndarray, + target_velocity: np.ndarray | None = None, + target_yaw: float = 0.0, +) -> CascadedController: + pos_gains, att_gains = gains_from_control(control_cfg) + return CascadedController( + dynamics=dynamics, + uav_cfg=uav_cfg, + mode=control_cfg.controller.mode, + target_position=np.asarray(target_position, dtype=np.float64), + target_velocity=( + np.zeros(3, dtype=np.float64) + if target_velocity is None + else np.asarray(target_velocity, dtype=np.float64) + ), + target_yaw=target_yaw, + pos_gains=pos_gains, + att_gains=att_gains, + max_tilt=control_cfg.controller.max_tilt, + ) diff --git a/src/uavcatch/control/math3d.py b/src/uavcatch/control/math3d.py new file mode 100644 index 0000000..77f5983 --- /dev/null +++ b/src/uavcatch/control/math3d.py @@ -0,0 +1,98 @@ +"""Quaternion and rotation helpers for control.""" + +from __future__ import annotations + +import numpy as np + +from uavcatch.dynamics.state import euler_to_quaternion, normalize_quaternion + + +def quaternion_conjugate(q: np.ndarray) -> np.ndarray: + q = np.asarray(q, dtype=np.float64) + return np.array([q[0], -q[1], -q[2], -q[3]], dtype=np.float64) + + +def quaternion_multiply(q1: np.ndarray, q2: np.ndarray) -> np.ndarray: + w1, x1, y1, z1 = q1 + w2, x2, y2, z2 = q2 + return normalize_quaternion( + np.array( + [ + w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2, + w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2, + w1 * y2 - x1 * z2 + y1 * w2 + z1 * x2, + w1 * z2 + x1 * y2 - y1 * x2 + z1 * w2, + ], + dtype=np.float64, + ) + ) + + +def quaternion_from_yaw(yaw: float) -> np.ndarray: + return euler_to_quaternion(0.0, 0.0, yaw) + + +def rotation_matrix_to_quaternion(R: np.ndarray) -> np.ndarray: + """Convert body-to-world rotation matrix to quaternion [w, x, y, z].""" + R = np.asarray(R, dtype=np.float64) + trace = float(np.trace(R)) + if trace > 0.0: + s = np.sqrt(trace + 1.0) * 2.0 + w = 0.25 * s + x = (R[2, 1] - R[1, 2]) / s + y = (R[0, 2] - R[2, 0]) / s + z = (R[1, 0] - R[0, 1]) / s + elif R[0, 0] > R[1, 1] and R[0, 0] > R[2, 2]: + s = np.sqrt(1.0 + R[0, 0] - R[1, 1] - R[2, 2]) * 2.0 + w = (R[2, 1] - R[1, 2]) / s + x = 0.25 * s + y = (R[0, 1] + R[1, 0]) / s + z = (R[0, 2] + R[2, 0]) / s + elif R[1, 1] > R[2, 2]: + s = np.sqrt(1.0 + R[1, 1] - R[0, 0] - R[2, 2]) * 2.0 + w = (R[0, 2] - R[2, 0]) / s + x = (R[0, 1] + R[1, 0]) / s + y = 0.25 * s + z = (R[1, 2] + R[2, 1]) / s + else: + s = np.sqrt(1.0 + R[2, 2] - R[0, 0] - R[1, 1]) * 2.0 + w = (R[1, 0] - R[0, 1]) / s + x = (R[0, 2] + R[2, 0]) / s + y = (R[1, 2] + R[2, 1]) / s + z = 0.25 * s + return normalize_quaternion(np.array([w, x, y, z], dtype=np.float64)) + + +def desired_attitude_from_thrust_and_yaw( + thrust_direction_world: np.ndarray, + yaw: float, +) -> np.ndarray: + """Build desired body attitude quaternion from thrust axis and yaw.""" + z_b = np.asarray(thrust_direction_world, dtype=np.float64) + norm = np.linalg.norm(z_b) + if norm < 1e-9: + z_b = np.array([0.0, 0.0, 1.0], dtype=np.float64) + else: + z_b = z_b / norm + + x_c = np.array([np.cos(yaw), np.sin(yaw), 0.0], dtype=np.float64) + y_b = np.cross(z_b, x_c) + y_norm = np.linalg.norm(y_b) + if y_norm < 1e-6: + x_c = np.array([1.0, 0.0, 0.0], dtype=np.float64) + y_b = np.cross(z_b, x_c) + y_norm = np.linalg.norm(y_b) + y_b = y_b / y_norm + x_b = np.cross(y_b, z_b) + R = np.column_stack([x_b, y_b, z_b]) + return rotation_matrix_to_quaternion(R) + + +def quaternion_attitude_error(q_des: np.ndarray, q: np.ndarray) -> np.ndarray: + """Rotation error vector (body frame) from desired/current quaternions.""" + q_des = normalize_quaternion(q_des) + q = normalize_quaternion(q) + q_e = quaternion_multiply(quaternion_conjugate(q_des), q) + if q_e[0] < 0.0: + q_e = -q_e + return (2.0 * np.sign(q_e[0]) * q_e[1:4]).astype(np.float64) diff --git a/src/uavcatch/control/pid.py b/src/uavcatch/control/pid.py new file mode 100644 index 0000000..e10e6e5 --- /dev/null +++ b/src/uavcatch/control/pid.py @@ -0,0 +1,208 @@ +"""Trajectory helpers and backward-compatible controller aliases.""" + +from __future__ import annotations + +import math +from collections.abc import Callable + +import numpy as np + +from uavcatch.config import UavConfig, ControlConfig +from uavcatch.control.cascade import CascadedController, TrackingController +from uavcatch.control.factory import build_cascaded_controller, gains_from_control +from uavcatch.control.types import PhaseTarget, PIDGains, TargetFn +from uavcatch.dynamics.model import QuadDynamics +from uavcatch.dynamics.state import QuadState + + +def smoothstep(u: float) -> float: + u = float(np.clip(u, 0.0, 1.0)) + return u * u * (3.0 - 2.0 * u) + + +def smoothstep_deriv(u: float) -> float: + u = float(np.clip(u, 0.0, 1.0)) + return 6.0 * u * (1.0 - u) + + +def phased_takeoff_hover_forward( + ground_z: float, + hover_z: float, + forward_x: float, + takeoff_duration: float, + hover_duration: float, + forward_duration: float, + ground_clearance: float = 0.05, +) -> TargetFn: + """Build setpoint: ground -> hover -> move along +X.""" + t1 = takeoff_duration + t2 = t1 + hover_duration + start_z = ground_z + ground_clearance + + def target_at(t: float) -> PhaseTarget: + if t < t1: + u = smoothstep(t / max(t1, 1e-9)) + z = start_z + u * (hover_z - start_z) + vz = (hover_z - start_z) * smoothstep_deriv(t / max(t1, 1e-9)) / max(t1, 1e-9) + return PhaseTarget( + position=np.array([0.0, 0.0, z], dtype=np.float64), + velocity=np.array([0.0, 0.0, vz], dtype=np.float64), + ) + if t < t2: + return PhaseTarget( + position=np.array([0.0, 0.0, hover_z], dtype=np.float64), + velocity=np.zeros(3, dtype=np.float64), + ) + u = (t - t2) / max(forward_duration, 1e-9) + s = smoothstep(u) + ds = smoothstep_deriv(u) / max(forward_duration, 1e-9) + return PhaseTarget( + position=np.array([forward_x * s, 0.0, hover_z], dtype=np.float64), + velocity=np.array([forward_x * ds, 0.0, 0.0], dtype=np.float64), + ) + + return target_at + + +def circle_trajectory( + center: np.ndarray, + radius: float, + height: float, + period: float, + clockwise: bool = False, +) -> tuple[TargetFn, TargetFn, TargetFn]: + """Return position, yaw, and feedforward acceleration callbacks for a circle.""" + center = np.asarray(center, dtype=np.float64) + omega = 2.0 * math.pi / max(period, 1e-9) + sign = -1.0 if clockwise else 1.0 + + def position_at(t: float) -> PhaseTarget: + angle = sign * omega * t + c = math.cos(angle) + s = math.sin(angle) + pos = center + np.array([radius * c, radius * s, height], dtype=np.float64) + vel = np.array( + [-sign * radius * omega * s, sign * radius * omega * c, 0.0], + dtype=np.float64, + ) + return PhaseTarget(position=pos, velocity=vel) + + def yaw_at(t: float) -> float: + angle = sign * omega * t + tangent = np.array( + [-sign * math.sin(angle), sign * math.cos(angle)], + dtype=np.float64, + ) + return float(math.atan2(tangent[1], tangent[0])) + + def accel_at(t: float) -> np.ndarray: + angle = sign * omega * t + c = math.cos(angle) + s = math.sin(angle) + centripetal = -(omega ** 2) * radius + return np.array([centripetal * c, centripetal * s, 0.0], dtype=np.float64) + + return position_at, yaw_at, accel_at + + +def circle_trajectory_with_takeoff( + center: np.ndarray, + radius: float, + height: float, + period: float, + takeoff_time: float, + ground_z: float = 0.0, + ground_clearance: float = 0.05, + clockwise: bool = False, +) -> tuple[TargetFn, TargetFn, TargetFn]: + """Take off from ground, then track a horizontal circle.""" + pos_fn, yaw_fn, accel_fn = circle_trajectory( + center=center, + radius=radius, + height=height, + period=period, + clockwise=clockwise, + ) + center = np.asarray(center, dtype=np.float64) + start_z = ground_z + ground_clearance + start_pos = np.array([center[0], center[1], start_z], dtype=np.float64) + circle_entry = pos_fn(0.0) + entry_pos = np.asarray(circle_entry.position, dtype=np.float64) + entry_yaw = float(yaw_fn(0.0)) + t_takeoff = max(takeoff_time, 1e-9) + + def position_at(t: float) -> PhaseTarget: + if t < t_takeoff: + u = t / t_takeoff + s = smoothstep(u) + ds = smoothstep_deriv(u) / t_takeoff + pos = start_pos + s * (entry_pos - start_pos) + vel = ds * (entry_pos - start_pos) + return PhaseTarget(position=pos, velocity=vel) + return pos_fn(t - t_takeoff) + + def yaw_at(t: float) -> float: + if t < t_takeoff: + return entry_yaw + return yaw_fn(t - t_takeoff) + + def accel_at(t: float) -> np.ndarray: + if t < t_takeoff: + return np.zeros(3, dtype=np.float64) + return accel_fn(t - t_takeoff) + + return position_at, yaw_at, accel_at + + +class HoverPIDController(CascadedController): + """Backward-compatible alias for cascaded hover control.""" + + def __init__( + self, + dynamics: QuadDynamics, + uav_cfg: UavConfig, + target_position: np.ndarray, + target_velocity: np.ndarray | None = None, + control_cfg: ControlConfig | None = None, + pos_gains: PIDGains | None = None, + att_gains: PIDGains | None = None, + yaw_gains: PIDGains | None = None, + ) -> None: + del yaw_gains + if control_cfg is not None: + pos_g, att_g = gains_from_control(control_cfg) + super().__init__( + dynamics=dynamics, + uav_cfg=uav_cfg, + mode=control_cfg.controller.mode, + target_position=np.asarray(target_position, dtype=np.float64), + target_velocity=( + np.zeros(3, dtype=np.float64) + if target_velocity is None + else np.asarray(target_velocity, dtype=np.float64) + ), + pos_gains=pos_g, + att_gains=att_g, + max_tilt=control_cfg.controller.max_tilt, + ) + return + super().__init__( + dynamics=dynamics, + uav_cfg=uav_cfg, + mode="linear", + target_position=np.asarray(target_position, dtype=np.float64), + target_velocity=( + np.zeros(3, dtype=np.float64) + if target_velocity is None + else np.asarray(target_velocity, dtype=np.float64) + ), + pos_gains=pos_gains or PIDGains(kp=0.8, ki=0.2, kd=0.9), + att_gains=att_gains or PIDGains(kp=8.0, ki=0.5, kd=0.6), + ) + + +class TrackingPIDController(TrackingController): + """Backward-compatible alias for time-varying linear cascade.""" + + def __init__(self, pid: HoverPIDController, target_fn: TargetFn) -> None: + super().__init__(controller=pid, target_fn=target_fn) diff --git a/src/uavcatch/control/position.py b/src/uavcatch/control/position.py new file mode 100644 index 0000000..ff3673b --- /dev/null +++ b/src/uavcatch/control/position.py @@ -0,0 +1,100 @@ +"""Outer-loop position controllers (linear and SE(3) geometric).""" + +from __future__ import annotations + +from dataclasses import dataclass, field + +import numpy as np + +from uavcatch.control.math3d import desired_attitude_from_thrust_and_yaw +from uavcatch.control.types import PIDGains, PositionCommand +from uavcatch.dynamics.state import QuadState, euler_to_quaternion + + +@dataclass +class LinearPositionController: + """Small-angle position controller (legacy / hover-friendly).""" + + mass: float + gravity: float + max_thrust: float + gains: PIDGains = field(default_factory=lambda: PIDGains(kp=0.8, ki=0.2, kd=0.9)) + max_tilt: float = 0.35 + _integral: np.ndarray = field(default_factory=lambda: np.zeros(3, dtype=np.float64)) + _integral_limit: float = 3.0 + + def reset(self) -> None: + self._integral[:] = 0.0 + + def compute( + self, + state: QuadState, + target_position: np.ndarray, + target_velocity: np.ndarray, + target_yaw: float, + dt: float, + ) -> PositionCommand: + pos_err = target_position - state.position + vel_err = target_velocity - state.velocity + self._integral += pos_err * dt + self._integral = np.clip(self._integral, -self._integral_limit, self._integral_limit) + + ax = self.gains.kp * pos_err[0] + self.gains.kd * vel_err[0] + ay = self.gains.kp * pos_err[1] + self.gains.kd * vel_err[1] + desired_pitch = float(np.clip(ax / self.gravity, -self.max_tilt, self.max_tilt)) + desired_roll = float(np.clip(-ay / self.gravity, -self.max_tilt, self.max_tilt)) + q_des = euler_to_quaternion(desired_roll, desired_pitch, target_yaw) + + thrust = self.mass * ( + self.gravity + + self.gains.kp * pos_err[2] + + self.gains.ki * self._integral[2] + + self.gains.kd * vel_err[2] + ) + thrust = float(np.clip(thrust, 0.1 * self.mass * self.gravity, self.max_thrust)) + return PositionCommand(thrust=thrust, desired_quaternion=q_des) + + +@dataclass +class GeometricPositionController: + """SE(3) geometric position controller (Lee et al. style).""" + + mass: float + gravity: float + max_thrust: float + gains: PIDGains = field(default_factory=lambda: PIDGains(kp=2.5, ki=0.4, kd=2.0)) + _integral: np.ndarray = field(default_factory=lambda: np.zeros(3, dtype=np.float64)) + _integral_limit: float = 2.0 + + def reset(self) -> None: + self._integral[:] = 0.0 + + def compute( + self, + state: QuadState, + target_position: np.ndarray, + target_velocity: np.ndarray, + target_yaw: float, + dt: float, + target_acceleration: np.ndarray | None = None, + ) -> PositionCommand: + pos_err = target_position - state.position + vel_err = target_velocity - state.velocity + self._integral += pos_err * dt + self._integral = np.clip(self._integral, -self._integral_limit, self._integral_limit) + + a_cmd = ( + self.gains.kp * pos_err + + self.gains.kd * vel_err + + self.gains.ki * self._integral + ) + if target_acceleration is not None: + a_cmd = a_cmd + np.asarray(target_acceleration, dtype=np.float64) + a_cmd[2] += self.gravity + + thrust = self.mass * float(np.linalg.norm(a_cmd)) + thrust = float(np.clip(thrust, 0.1 * self.mass * self.gravity, self.max_thrust)) + + z_b_des = a_cmd / max(np.linalg.norm(a_cmd), 1e-9) + q_des = desired_attitude_from_thrust_and_yaw(z_b_des, target_yaw) + return PositionCommand(thrust=thrust, desired_quaternion=q_des) diff --git a/src/uavcatch/control/types.py b/src/uavcatch/control/types.py new file mode 100644 index 0000000..d320905 --- /dev/null +++ b/src/uavcatch/control/types.py @@ -0,0 +1,42 @@ +"""Shared control types.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass, field + +import numpy as np + + +@dataclass +class PIDGains: + kp: float + ki: float + kd: float + + +@dataclass +class PhaseTarget: + position: np.ndarray + velocity: np.ndarray + + +TargetFn = Callable[[float], np.ndarray | PhaseTarget] + + +@dataclass +class PositionCommand: + """Output of the position / outer-loop controller.""" + + thrust: float + desired_quaternion: np.ndarray + desired_angular_rate: np.ndarray = field( + default_factory=lambda: np.zeros(3, dtype=np.float64) + ) + + +@dataclass +class AttitudeCommand: + """Output of the attitude / inner-loop controller.""" + + torque: np.ndarray diff --git a/src/uavcatch/dynamics/__init__.py b/src/uavcatch/dynamics/__init__.py new file mode 100644 index 0000000..6f49cb9 --- /dev/null +++ b/src/uavcatch/dynamics/__init__.py @@ -0,0 +1,5 @@ +from uavcatch.dynamics.model import QuadDynamics +from uavcatch.dynamics.state import QuadState +from uavcatch.dynamics.wind import WindModel + +__all__ = ["QuadDynamics", "QuadState", "WindModel"] diff --git a/src/uavcatch/dynamics/__pycache__/__init__.cpython-313.pyc b/src/uavcatch/dynamics/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e3e9dc0112121bbce6cf1cec6ccf02a87b4f7444 GIT binary patch literal 367 zcmey&%ge<81P{GkvyKDl#~=<2FhLog4S?f;i!s zc`3g6DXBS{K*N9cSdyHfmjX3FFBha3tQ4DUG00r78MnkR z^_BxI1Dkz|Jw84$Cnr9BCBtW+RSdUGU94gXit@8klS^Vk9mAYKcE%JJCC9)FiGevF zCO$qhFS8^*Uaz3?7KaVcIi)$Nc165EQy77`SQtorU}j`wyvty4mqGU~gU%x^lScL; HZlE{-$w*}1 literal 0 HcmV?d00001 diff --git a/src/uavcatch/dynamics/__pycache__/model.cpython-313.pyc b/src/uavcatch/dynamics/__pycache__/model.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e32fe69c6dd29390043b8052bb94244b3e9df8bc GIT binary patch literal 10157 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 J!C+3u`5&(S?V|ty literal 0 HcmV?d00001 diff --git a/src/uavcatch/dynamics/__pycache__/state.cpython-313.pyc b/src/uavcatch/dynamics/__pycache__/state.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b9cbb4711a119554f3cb90a824e12829a607d9f5 GIT binary patch literal 6305 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 Na8;b`YaG(6`X8s^x4r-X literal 0 HcmV?d00001 diff --git a/src/uavcatch/dynamics/__pycache__/wind.cpython-313.pyc b/src/uavcatch/dynamics/__pycache__/wind.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..65fdd9630c3fd8eaaa0d22c9e3ec61e376028995 GIT binary patch literal 3280 zcmbVO?Q0yz8J~T*-Mia6NtUhXt&-M??d zNx%Y(;*z)+!?C(d!ZOHWOYy}7PN*mw8$jbomxhoo&+yTlGxdMq9MkuKN&X1j<~_GyujlF$ zvq6X$ZvQ3_J1C1V2aDi!4hxe==dq{@*_bYVAGCT5OS%M{T<2uQ#VMDK>)m={2yKeZ zuoHTZo}?0-%qsd3T{)ml>AiZ2Y14Wdcdd76M@fRlc$u#0n>5UM*>$%5%PKKwHGwiYxqf8g@GWk}!cZVfh|MV)CcIlBvgt<~7jNttk=a(wp2PG9Z4NvUa*hvOVcn;zS5@+Zg}H1Adz*k$cJ<<0 z^Jb9S$oe>!Et}VKjLC1gX3@_1Sqf;Tv_+Hx_KF7N8=M)^*UZgKn|Kf=_b)=XjlNR4 z9v^$|_Top2cl=Lo{^sU`vk&L)&xOY>JnkF1^FnoaIvk#^^koimPh~MB)sPsI_F)o~ zt8X5q5iKnY)VDyo^S=Q_{~w^M$Y~}>3$(UG=m7Ex#pY03?fCx&%#G3IR-ru3eT)|3v80^qO@gOmls~S&4-*O6g z6g=vwhzEcsX~QUaP;&YFco{xKP4}Lg!d-TK6U9qa34b`)iBQI2xbK47hvhvIPPk*x4KV0qC z!hY@ULfC(1JO6X{pZ$Zk-}~sjPrD!Bhd1uu2>V~BQalX*>zs;z1`VWQGfl5GQ>PP< zD@_2wt=<6gc!O?@jr3@YIyd(-gkIrJ)=l!0aENKTbVx?91<+cyW_R7TL)D3Fq2BPz z5wvntIvBnq@G}O~;rhd21_NES8lji4eZz$N;kreRwvcNIHL&zAL${5-m5`kJ)$1Qz zdjHY~%kMAWIajX)sZ2#W{rU5uH1iB`@*CVd!k(x_ zE7 next state.""" + + def __init__(self, env_cfg: EnvironmentConfig, uav_cfg: UavConfig) -> None: + self.env_cfg = env_cfg + self.uav_cfg = uav_cfg + self._mass = uav_cfg.body.mass + self._arm = uav_cfg.body.arm_length + self._kf = uav_cfg.rotor.kf + self._km = uav_cfg.rotor.km + self._layout = uav_cfg.rotor.layout + self._gravity = env_cfg.physics.gravity + self._inertia = np.diag( + [uav_cfg.body.Ixx, uav_cfg.body.Iyy, uav_cfg.body.Izz] + ).astype(np.float64) + self._inertia_inv = np.linalg.inv(self._inertia) + self._linear_drag = uav_cfg.drag.linear + self._angular_drag = uav_cfg.drag.angular + # Quadratic aerodynamic drag coefficient: 0.5 * rho * Cd * A + self._quad_drag = 0.5 * env_cfg.physics.air_density * uav_cfg.drag.cd * uav_cfg.drag.area + self._wind = WindModel.from_config(env_cfg.wind) + self._integrator = env_cfg.simulation.integrator + + def clip_omega(self, omega: np.ndarray) -> np.ndarray: + omega = np.asarray(omega, dtype=np.float64) + return np.clip(omega, self.uav_cfg.rotor.omega_min, self.uav_cfg.rotor.omega_max) + + def hover_omega(self) -> float: + """Per-rotor angular speed for approximate hover.""" + thrust_per_rotor = self._mass * self._gravity / 4.0 + return float(np.sqrt(max(thrust_per_rotor / self._kf, 0.0))) + + def rotor_forces_and_torques(self, omega: np.ndarray) -> tuple[float, np.ndarray]: + """Map rotor speeds to total thrust and body torques.""" + omega = self.clip_omega(omega) + thrusts = self._kf * omega * omega + total_thrust = float(np.sum(thrusts)) + + w1, w2, w3, w4 = omega + if self._layout == "x": + tau_x = self._kf * self._arm * (w4 * w4 - w2 * w2) + tau_y = self._kf * self._arm * (w3 * w3 - w1 * w1) + else: + tau_x = self._kf * self._arm * (w3 * w3 - w1 * w1) + tau_y = self._kf * self._arm * (w4 * w4 - w2 * w2) + tau_z = self._km * (w1 * w1 - w2 * w2 + w3 * w3 - w4 * w4) + return total_thrust, np.array([tau_x, tau_y, tau_z], dtype=np.float64) + + def aerodynamic_force(self, velocity: np.ndarray, wind_velocity: np.ndarray) -> np.ndarray: + """Drag force (world frame) on the air-relative velocity. + + F = -linear * v_rel - 0.5 * rho * Cd * A * |v_rel| * v_rel + """ + v_rel = velocity - wind_velocity + force = np.zeros(3, dtype=np.float64) + if self._linear_drag > 0.0: + force -= self._linear_drag * v_rel + if self._quad_drag > 0.0: + speed = float(np.linalg.norm(v_rel)) + if speed > 0.0: + force -= self._quad_drag * speed * v_rel + return force + + def derivatives( + self, + state: QuadState, + omega: np.ndarray, + wind_velocity: np.ndarray | None = None, + ) -> np.ndarray: + """Compute time derivative of the 13-D state vector.""" + thrust, torque = self.rotor_forces_and_torques(omega) + q = normalize_quaternion(state.quaternion) + R = quaternion_to_rotation_matrix(q) + body_thrust = np.array([0.0, 0.0, thrust], dtype=np.float64) + gravity = np.array([0.0, 0.0, -self._gravity], dtype=np.float64) + + if wind_velocity is None: + wind_velocity = np.zeros(3, dtype=np.float64) + drag_force = self.aerodynamic_force(state.velocity, wind_velocity) + accel = (R @ body_thrust) / self._mass + gravity + drag_force / self._mass + + p_dot = state.velocity + v_dot = accel + + w, x, y, z = q + p, q_rate, r = state.angular_rate + q_dot = 0.5 * np.array( + [ + -x * p - y * q_rate - z * r, + w * p + y * r - z * q_rate, + w * q_rate - x * r + z * p, + w * r + x * q_rate - y * p, + ], + dtype=np.float64, + ) + + omega_body = state.angular_rate + gyro = np.cross(omega_body, self._inertia @ omega_body) + omega_dot = self._inertia_inv @ (torque - gyro) + if self._angular_drag > 0.0: + omega_dot -= self._angular_drag * omega_body + + return np.concatenate([p_dot, v_dot, q_dot, omega_dot]) + + def step(self, state: QuadState, omega: np.ndarray, dt: float, t: float = 0.0) -> QuadState: + # Wind is held constant across the step (and RK4 substeps); dt is small. + wind = self._wind.sample(t) + if self._integrator == "euler": + return self._step_euler(state, omega, dt, wind) + return self._step_rk4(state, omega, dt, wind) + + def _step_euler( + self, state: QuadState, omega: np.ndarray, dt: float, wind: np.ndarray + ) -> QuadState: + x = state.as_vector() + dx = self.derivatives(state, omega, wind) + x_next = x + dt * dx + x_next[6:10] = normalize_quaternion(x_next[6:10]) + return QuadState.from_vector(x_next) + + def _step_rk4( + self, state: QuadState, omega: np.ndarray, dt: float, wind: np.ndarray + ) -> QuadState: + x = state.as_vector() + + def f(x_vec: np.ndarray) -> np.ndarray: + return self.derivatives(QuadState.from_vector(x_vec), omega, wind) + + k1 = f(x) + k2 = f(x + 0.5 * dt * k1) + k3 = f(x + 0.5 * dt * k2) + k4 = f(x + dt * k3) + x_next = x + (dt / 6.0) * (k1 + 2.0 * k2 + 2.0 * k3 + k4) + x_next[6:10] = normalize_quaternion(x_next[6:10]) + return QuadState.from_vector(x_next) diff --git a/src/uavcatch/dynamics/state.py b/src/uavcatch/dynamics/state.py new file mode 100644 index 0000000..6a7ea04 --- /dev/null +++ b/src/uavcatch/dynamics/state.py @@ -0,0 +1,110 @@ +"""Quadcopter state representation.""" + +from __future__ import annotations + +from dataclasses import dataclass + +import numpy as np + + +def euler_to_quaternion(roll: float, pitch: float, yaw: float) -> np.ndarray: + """Convert ZYX Euler angles to quaternion [w, x, y, z].""" + cr, sr = np.cos(roll * 0.5), np.sin(roll * 0.5) + cp, sp = np.cos(pitch * 0.5), np.sin(pitch * 0.5) + cy, sy = np.cos(yaw * 0.5), np.sin(yaw * 0.5) + w = cr * cp * cy + sr * sp * sy + x = sr * cp * cy - cr * sp * sy + y = cr * sp * cy + sr * cp * sy + z = cr * cp * sy - sr * sp * cy + q = np.array([w, x, y, z], dtype=np.float64) + return q / np.linalg.norm(q) + + +def quaternion_to_rotation_matrix(q: np.ndarray) -> np.ndarray: + """Body-to-world rotation matrix from unit quaternion [w, x, y, z].""" + w, x, y, z = q + return np.array( + [ + [1 - 2 * (y * y + z * z), 2 * (x * y - z * w), 2 * (x * z + y * w)], + [2 * (x * y + z * w), 1 - 2 * (x * x + z * z), 2 * (y * z - x * w)], + [2 * (x * z - y * w), 2 * (y * z + x * w), 1 - 2 * (x * x + y * y)], + ], + dtype=np.float64, + ) + + +def quaternion_to_euler(q: np.ndarray) -> tuple[float, float, float]: + """Return roll, pitch, yaw (rad) from quaternion [w, x, y, z].""" + w, x, y, z = q + sinr_cosp = 2.0 * (w * x + y * z) + cosr_cosp = 1.0 - 2.0 * (x * x + y * y) + roll = float(np.arctan2(sinr_cosp, cosr_cosp)) + + sinp = 2.0 * (w * y - z * x) + pitch = float(np.arcsin(np.clip(sinp, -1.0, 1.0))) + + siny_cosp = 2.0 * (w * z + x * y) + cosy_cosp = 1.0 - 2.0 * (y * y + z * z) + yaw = float(np.arctan2(siny_cosp, cosy_cosp)) + return roll, pitch, yaw + + +def normalize_quaternion(q: np.ndarray) -> np.ndarray: + norm = np.linalg.norm(q) + if norm < 1e-12: + return np.array([1.0, 0.0, 0.0, 0.0], dtype=np.float64) + return q / norm + + +@dataclass +class QuadState: + """13-DOF quadcopter state in world frame.""" + + position: np.ndarray + velocity: np.ndarray + quaternion: np.ndarray + angular_rate: np.ndarray + + def as_vector(self) -> np.ndarray: + return np.concatenate( + [ + self.position, + self.velocity, + self.quaternion, + self.angular_rate, + ] + ) + + def copy(self) -> QuadState: + return QuadState( + position=self.position.copy(), + velocity=self.velocity.copy(), + quaternion=self.quaternion.copy(), + angular_rate=self.angular_rate.copy(), + ) + + @classmethod + def from_vector(cls, x: np.ndarray) -> QuadState: + x = np.asarray(x, dtype=np.float64) + return cls( + position=x[0:3].copy(), + velocity=x[3:6].copy(), + quaternion=normalize_quaternion(x[6:10].copy()), + angular_rate=x[10:13].copy(), + ) + + @classmethod + def from_initial( + cls, + position: list[float] | np.ndarray, + velocity: list[float] | np.ndarray, + attitude_euler: list[float] | np.ndarray, + angular_rate: list[float] | np.ndarray, + ) -> QuadState: + euler = np.asarray(attitude_euler, dtype=np.float64) + return cls( + position=np.asarray(position, dtype=np.float64), + velocity=np.asarray(velocity, dtype=np.float64), + quaternion=euler_to_quaternion(float(euler[0]), float(euler[1]), float(euler[2])), + angular_rate=np.asarray(angular_rate, dtype=np.float64), + ) diff --git a/src/uavcatch/dynamics/wind.py b/src/uavcatch/dynamics/wind.py new file mode 100644 index 0000000..dbaf8b6 --- /dev/null +++ b/src/uavcatch/dynamics/wind.py @@ -0,0 +1,52 @@ +"""Ambient wind field model.""" + +from __future__ import annotations + +import math +from dataclasses import dataclass + +import numpy as np + +from uavcatch.config import WindConfig + + +@dataclass +class WindModel: + """Deterministic wind: constant base plus a sinusoidal gust. + + wind(t) = base + gust_amplitude * sin(2*pi*gust_frequency*t) * direction + where direction is the unit base-wind vector (falls back to +X if base ~0). + """ + + base_velocity: np.ndarray + gust_amplitude: float = 0.0 + gust_frequency: float = 0.0 + + @classmethod + def from_config(cls, cfg: WindConfig) -> WindModel: + return cls( + base_velocity=np.asarray(cfg.velocity, dtype=np.float64), + gust_amplitude=float(cfg.gust_amplitude), + gust_frequency=float(cfg.gust_frequency), + ) + + def __post_init__(self) -> None: + self.base_velocity = np.asarray(self.base_velocity, dtype=np.float64) + norm = float(np.linalg.norm(self.base_velocity)) + if norm > 1e-9: + self._gust_dir = self.base_velocity / norm + else: + self._gust_dir = np.array([1.0, 0.0, 0.0], dtype=np.float64) + + @property + def is_active(self) -> bool: + return ( + float(np.linalg.norm(self.base_velocity)) > 0.0 + or (self.gust_amplitude != 0.0 and self.gust_frequency != 0.0) + ) + + def sample(self, t: float) -> np.ndarray: + if self.gust_amplitude == 0.0 or self.gust_frequency == 0.0: + return self.base_velocity + gust = self.gust_amplitude * math.sin(2.0 * math.pi * self.gust_frequency * t) + return self.base_velocity + gust * self._gust_dir diff --git a/src/uavcatch/eval/__init__.py b/src/uavcatch/eval/__init__.py new file mode 100644 index 0000000..3b41d1c --- /dev/null +++ b/src/uavcatch/eval/__init__.py @@ -0,0 +1,18 @@ +"""Control evaluation utilities.""" + +from __future__ import annotations + +from uavcatch.eval.maneuvers import ManeuverSpec, default_maneuver_suite +from uavcatch.eval.metrics import ManeuverMetrics, evaluate_maneuver +from uavcatch.eval.plots import plot_maneuver_report, plot_summary_comparison +from uavcatch.eval.recorder import SimRecorder + +__all__ = [ + "ManeuverSpec", + "ManeuverMetrics", + "SimRecorder", + "default_maneuver_suite", + "evaluate_maneuver", + "plot_maneuver_report", + "plot_summary_comparison", +] diff --git a/src/uavcatch/eval/__pycache__/__init__.cpython-313.pyc b/src/uavcatch/eval/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4de3f3871c1e4cd766f4b7f806e59a777d17d240 GIT binary patch literal 645 zcmZ9I(Q4E{6ozNA*-du0YbmweD`nBE+(f(;5u`1Y#1%9sB7tF;o$8b%6VA*mw0Ay) z579^Jfb>E{yc72YoXPI0^+0~|&-ruCH(5<4BjoD+i<9$dfY4WOw&lDpyn5&KGb#|E z0vAD!vjBpbfA8hJEQB!YL%;K3KFA`7vKZpd_w!*kf>Abxapwp5PBwwb3_U_c^a{hB z4{X4yp}EWLZgX?4K`>?aHe2|gtV`W(EBVjIz*`C0PPYBm@VX|&12qha& zQ;7OCXA%}1k_bY_2m>$vcS7Gw$3v#Y=R?cwbR&sh>88leeE+Y1{%t>wa0{23g$3D`aAzLTFljPW&k_zgYy Xj!tjT@eP`PLFunB9tWQy*msfN7EiqP literal 0 HcmV?d00001 diff --git a/src/uavcatch/eval/__pycache__/maneuvers.cpython-313.pyc b/src/uavcatch/eval/__pycache__/maneuvers.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dc0e3709dca671f1a963fd5995e1ae30f41ea279 GIT binary patch literal 7062 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 literal 0 HcmV?d00001 diff --git a/src/uavcatch/eval/__pycache__/plots.cpython-313.pyc b/src/uavcatch/eval/__pycache__/plots.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0d32f52c5ce93a841c3b0226aca1cfe4157d488d GIT binary patch literal 8458 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 TargetFn: + pos = np.asarray(position, dtype=np.float64) + + def target_at(t: float) -> PhaseTarget: + del t + return PhaseTarget(position=pos.copy(), velocity=np.zeros(3, dtype=np.float64)) + + return target_at + + +def step_position( + initial: np.ndarray, + final: np.ndarray, + step_time: float, + duration: float, + axis: int, +) -> ManeuverSpec: + """Step change on one position axis after an initial hold.""" + initial = np.asarray(initial, dtype=np.float64) + final = np.asarray(final, dtype=np.float64) + axis_names = ["x", "y", "z"] + + def target_at(t: float) -> PhaseTarget: + pos = initial.copy() if t < step_time else final.copy() + vel = np.zeros(3, dtype=np.float64) + return PhaseTarget(position=pos, velocity=vel) + + return ManeuverSpec( + name=f"step_{axis_names[axis]}", + description=f"Step {axis_names[axis]}: {initial[axis]:.2f} -> {final[axis]:.2f} m at t={step_time:.1f}s", + duration=duration, + target_fn=target_at, + step_axis=axis, + step_time=step_time, + step_from=float(initial[axis]), + step_to=float(final[axis]), + ) + + +def hover_hold(height: float = 2.0, duration: float = 8.0) -> ManeuverSpec: + return ManeuverSpec( + name="hover", + description=f"Hover at z={height:.1f} m", + duration=duration, + target_fn=_hold(np.array([0.0, 0.0, height])), + ) + + +def takeoff_forward( + ground_z: float = 0.0, + hover_z: float = 2.0, + forward_x: float = 4.0, + takeoff_time: float = 3.0, + hover_time: float = 2.0, + forward_time: float = 6.0, +) -> ManeuverSpec: + duration = takeoff_time + hover_time + forward_time + return ManeuverSpec( + name="takeoff_forward", + description="Takeoff -> hover -> forward +X", + duration=duration, + target_fn=phased_takeoff_hover_forward( + ground_z, hover_z, forward_x, takeoff_time, hover_time, forward_time + ), + ) + + +def circle_track( + radius: float = 2.5, + height: float = 2.0, + period: float = 10.0, + duration: float = 20.0, + takeoff_time: float = 2.0, + ground_z: float = 0.0, + ground_clearance: float = 0.05, +) -> ManeuverSpec: + pos_fn, yaw_fn, accel_fn = circle_trajectory_with_takeoff( + center=np.zeros(3), + radius=radius, + height=height, + period=period, + takeoff_time=takeoff_time, + ground_z=ground_z, + ground_clearance=ground_clearance, + ) + return ManeuverSpec( + name="circle", + description=( + f"Takeoff {takeoff_time:.1f}s from ground, then circle r={radius:.1f} m, T={period:.1f} s" + ), + duration=duration, + target_fn=pos_fn, + yaw_fn=yaw_fn, + accel_fn=accel_fn, + initial_position=(0.0, 0.0, ground_z + ground_clearance), + ) + + +def default_maneuver_suite(eval_cfg: EvalConfig) -> list[ManeuverSpec]: + home = np.array([0.0, 0.0, eval_cfg.initial_height]) + step = eval_cfg.step + takeoff = eval_cfg.takeoff_forward + circle = eval_cfg.circle + return [ + hover_hold(height=eval_cfg.initial_height, duration=eval_cfg.hover.duration), + step_position( + home, + np.array([0.0, 0.0, step.step_z]), + step_time=step.step_time, + duration=step.duration, + axis=2, + ), + step_position( + home, + np.array([step.step_x, 0.0, eval_cfg.initial_height]), + step_time=step.step_time, + duration=step.duration, + axis=0, + ), + step_position( + home, + np.array([0.0, step.step_y, eval_cfg.initial_height]), + step_time=step.step_time, + duration=step.duration, + axis=1, + ), + takeoff_forward( + ground_z=takeoff.ground_z, + hover_z=takeoff.hover_z, + forward_x=takeoff.forward_x, + takeoff_time=takeoff.takeoff_time, + hover_time=takeoff.hover_time, + forward_time=takeoff.forward_time, + ), + circle_track( + radius=circle.radius, + height=circle.height, + period=circle.period, + duration=circle.duration, + takeoff_time=circle.takeoff_time, + ground_z=circle.ground_z, + ), + ] diff --git a/src/uavcatch/eval/metrics.py b/src/uavcatch/eval/metrics.py new file mode 100644 index 0000000..2813ee2 --- /dev/null +++ b/src/uavcatch/eval/metrics.py @@ -0,0 +1,208 @@ +"""Control performance metrics.""" + +from __future__ import annotations + +from dataclasses import dataclass, field + +import numpy as np + +from uavcatch.eval.maneuvers import ManeuverSpec + + +@dataclass +class StepMetrics: + axis: str + rise_time: float | None + settling_time: float | None + overshoot_pct: float + steady_state_error: float + peak_value: float + final_value: float + + +@dataclass +class TrackingMetrics: + rmse: np.ndarray + mae: np.ndarray + max_error: np.ndarray + rmse_3d: float + max_error_3d: float + + +@dataclass +class ControlEffortMetrics: + omega_mean: np.ndarray + omega_std: np.ndarray + omega_max: np.ndarray + saturation_ratio: float + + +@dataclass +class ManeuverMetrics: + name: str + description: str + mode: str + tracking: TrackingMetrics + attitude_max_deg: np.ndarray + control: ControlEffortMetrics + step: StepMetrics | None = None + summary: dict[str, float] = field(default_factory=dict) + + +_AXIS_NAMES = ("x", "y", "z") + + +def _step_response_metrics( + time: np.ndarray, + signal: np.ndarray, + step_time: float, + y0: float, + yf: float, + settle_band: float = 0.02, +) -> StepMetrics: + axis_idx = 0 + mask = time >= step_time + t = time[mask] - step_time + y = signal[mask] + if len(t) < 2: + return StepMetrics( + axis=_AXIS_NAMES[axis_idx], + rise_time=None, + settling_time=None, + overshoot_pct=0.0, + steady_state_error=0.0, + peak_value=float(y[-1]) if len(y) else yf, + final_value=yf, + ) + + delta = yf - y0 + if abs(delta) < 1e-9: + return StepMetrics( + axis="", + rise_time=0.0, + settling_time=0.0, + overshoot_pct=0.0, + steady_state_error=float(np.mean(y[-max(10, len(y) // 10):] - yf)), + peak_value=float(y[-1]), + final_value=yf, + ) + + y10 = y0 + 0.1 * delta + y90 = y0 + 0.9 * delta + band = settle_band * abs(delta) + + if delta > 0: + rise_idx_10 = np.argmax(y >= y10) + rise_idx_90 = np.argmax(y >= y90) + overshoot = max(0.0, float(np.max(y) - yf)) + else: + rise_idx_10 = np.argmax(y <= y10) + rise_idx_90 = np.argmax(y <= y90) + overshoot = max(0.0, float(yf - np.min(y))) + + rise_time = float(t[rise_idx_90] - t[rise_idx_10]) if rise_idx_90 > rise_idx_10 else None + + settling_time = None + for i in range(len(t)): + if np.all(np.abs(y[i:] - yf) <= band): + settling_time = float(t[i]) + break + + tail = y[-max(20, len(y) // 10):] + ss_err = float(np.mean(tail - yf)) + overshoot_pct = 100.0 * overshoot / abs(delta) + + return StepMetrics( + axis="", + rise_time=rise_time, + settling_time=settling_time, + overshoot_pct=overshoot_pct, + steady_state_error=ss_err, + peak_value=float(np.max(y) if delta > 0 else np.min(y)), + final_value=yf, + ) + + +def compute_tracking_metrics(error: np.ndarray) -> TrackingMetrics: + if error.size == 0: + z = np.zeros(3) + return TrackingMetrics(rmse=z, mae=z, max_error=z, rmse_3d=0.0, max_error_3d=0.0) + rmse = np.sqrt(np.mean(error * error, axis=0)) + mae = np.mean(np.abs(error), axis=0) + max_error = np.max(np.abs(error), axis=0) + err_norm = np.linalg.norm(error, axis=1) + return TrackingMetrics( + rmse=rmse, + mae=mae, + max_error=max_error, + rmse_3d=float(np.sqrt(np.mean(err_norm * err_norm))), + max_error_3d=float(np.max(err_norm)), + ) + + +def compute_control_metrics(omega: np.ndarray, omega_max: float) -> ControlEffortMetrics: + if omega.size == 0: + z4 = np.zeros(4) + return ControlEffortMetrics(omega_mean=z4, omega_std=z4, omega_max=z4, saturation_ratio=0.0) + sat = float(np.mean(np.any(omega >= 0.99 * omega_max, axis=1))) + return ControlEffortMetrics( + omega_mean=np.mean(omega, axis=0), + omega_std=np.std(omega, axis=0), + omega_max=np.max(omega, axis=0), + saturation_ratio=sat, + ) + + +def evaluate_maneuver( + spec: ManeuverSpec, + data: dict[str, np.ndarray], + omega_max: float, + mode: str, +) -> ManeuverMetrics: + time = data["time"] + pos = data["position"] + err = data["tracking_error"] + euler = data["euler"] + omega = data["omega"] + + tracking = compute_tracking_metrics(err) + control = compute_control_metrics(omega, omega_max) + att_max = np.degrees(np.max(np.abs(euler), axis=0)) + + step_metrics = None + if spec.step_axis is not None and spec.step_time is not None: + step_metrics = _step_response_metrics( + time, + pos[:, spec.step_axis], + spec.step_time, + spec.step_from or 0.0, + spec.step_to or 0.0, + ) + step_metrics.axis = _AXIS_NAMES[spec.step_axis] + + summary = { + "rmse_3d": tracking.rmse_3d, + "max_err_3d": tracking.max_error_3d, + "saturation_ratio": control.saturation_ratio, + "max_roll_deg": float(att_max[0]), + "max_pitch_deg": float(att_max[1]), + "max_yaw_deg": float(att_max[2]), + } + if step_metrics is not None: + if step_metrics.rise_time is not None: + summary["rise_time"] = step_metrics.rise_time + if step_metrics.settling_time is not None: + summary["settling_time"] = step_metrics.settling_time + summary["overshoot_pct"] = step_metrics.overshoot_pct + summary["steady_state_error"] = abs(step_metrics.steady_state_error) + + return ManeuverMetrics( + name=spec.name, + description=spec.description, + mode=mode, + tracking=tracking, + attitude_max_deg=att_max, + control=control, + step=step_metrics, + summary=summary, + ) diff --git a/src/uavcatch/eval/plots.py b/src/uavcatch/eval/plots.py new file mode 100644 index 0000000..52b2456 --- /dev/null +++ b/src/uavcatch/eval/plots.py @@ -0,0 +1,126 @@ +"""Time-domain plots for control evaluation.""" + +from __future__ import annotations + +from pathlib import Path + +import matplotlib.pyplot as plt +import numpy as np + +from uavcatch.eval.metrics import ManeuverMetrics + + +def plot_maneuver_report( + data: dict[str, np.ndarray], + metrics: ManeuverMetrics, + save_path: Path | None = None, + show: bool = False, +) -> plt.Figure: + time = data["time"] + pos = data["position"] + target = data["target_position"] + err = data["tracking_error"] + euler = np.degrees(data["euler"]) + omega = data["omega"] + + fig = plt.figure(figsize=(12, 10)) + fig.suptitle(f"{metrics.name} [{metrics.mode}] - {metrics.description}", fontsize=11) + gs = fig.add_gridspec(4, 3, hspace=0.35, wspace=0.28) + + labels = ("x", "y", "z") + colors = ("#2563eb", "#16a34a", "#dc2626") + + ax_pos = fig.add_subplot(gs[0, :]) + for i, (lab, c) in enumerate(zip(labels, colors)): + ax_pos.plot(time, target[:, i], "--", color=c, alpha=0.55, linewidth=1.2, label=f"{lab} ref") + ax_pos.plot(time, pos[:, i], "-", color=c, linewidth=1.6, label=f"{lab} act") + ax_pos.set_ylabel("Position (m)") + ax_pos.legend(loc="upper right", ncol=3, fontsize=8) + ax_pos.grid(alpha=0.3) + ax_pos.set_xlabel("Time (s)") + + for i, (lab, c) in enumerate(zip(labels, colors)): + ax = fig.add_subplot(gs[1, i]) + ax.plot(time, err[:, i], color=c, linewidth=1.4) + ax.axhline(0.0, color="k", linewidth=0.6, alpha=0.4) + ax.set_title(f"Error {lab} RMSE={metrics.tracking.rmse[i]:.3f} m") + ax.set_ylabel("Error (m)") + ax.set_xlabel("Time (s)") + ax.grid(alpha=0.3) + + ax_att = fig.add_subplot(gs[2, 0]) + att_labels = ("roll", "pitch", "yaw") + for i, (lab, c) in enumerate(zip(att_labels, colors)): + ax_att.plot(time, euler[:, i], color=c, linewidth=1.2, label=lab) + ax_att.set_ylabel("Attitude (deg)") + ax_att.set_xlabel("Time (s)") + ax_att.legend(fontsize=8) + ax_att.grid(alpha=0.3) + + ax_om = fig.add_subplot(gs[2, 1:]) + for i in range(omega.shape[1]): + ax_om.plot(time, omega[:, i], linewidth=1.0, alpha=0.85, label=f"m{i + 1}") + ax_om.set_ylabel("Motor ω (rad/s)") + ax_om.set_xlabel("Time (s)") + ax_om.legend(fontsize=7, ncol=2) + ax_om.grid(alpha=0.3) + + ax_txt = fig.add_subplot(gs[3, :]) + ax_txt.axis("off") + lines = [ + f"3D RMSE = {metrics.tracking.rmse_3d:.4f} m | Max error = {metrics.tracking.max_error_3d:.4f} m", + f"RMSE (x,y,z) = ({metrics.tracking.rmse[0]:.4f}, {metrics.tracking.rmse[1]:.4f}, {metrics.tracking.rmse[2]:.4f}) m", + f"Max |roll,pitch,yaw| = ({metrics.attitude_max_deg[0]:.1f}, {metrics.attitude_max_deg[1]:.1f}, {metrics.attitude_max_deg[2]:.1f}) deg", + f"Motor saturation ratio = {metrics.control.saturation_ratio * 100:.1f}%", + ] + if metrics.step is not None: + s = metrics.step + rise = f"{s.rise_time:.3f}s" if s.rise_time is not None else "N/A" + settle = f"{s.settling_time:.3f}s" if s.settling_time is not None else "N/A" + lines.append( + f"Step {s.axis}: rise={rise}, settle={settle}, overshoot={s.overshoot_pct:.1f}%, ss_err={s.steady_state_error:.4f} m" + ) + ax_txt.text(0.02, 0.6, "\n".join(lines), fontsize=10, family="monospace", va="top") + + if save_path is not None: + save_path.parent.mkdir(parents=True, exist_ok=True) + fig.savefig(save_path, dpi=140, bbox_inches="tight") + + if show: + plt.show() + return fig + + +def plot_summary_comparison( + all_metrics: list[ManeuverMetrics], + save_path: Path | None = None, + show: bool = False, +) -> plt.Figure: + names = [m.name for m in all_metrics] + rmse = [m.tracking.rmse_3d for m in all_metrics] + max_err = [m.tracking.max_error_3d for m in all_metrics] + sat = [m.control.saturation_ratio * 100.0 for m in all_metrics] + + x = np.arange(len(names)) + fig, axes = plt.subplots(1, 3, figsize=(12, 4)) + fig.suptitle("Control Performance Summary", fontsize=12) + + axes[0].bar(x, rmse, color="#2563eb") + axes[0].set_title("3D RMSE (m)") + axes[1].bar(x, max_err, color="#dc2626") + axes[1].set_title("Max 3D Error (m)") + axes[2].bar(x, sat, color="#ca8a04") + axes[2].set_title("Saturation (%)") + + for ax in axes: + ax.set_xticks(x) + ax.set_xticklabels(names, rotation=35, ha="right", fontsize=8) + ax.grid(axis="y", alpha=0.3) + + fig.tight_layout() + if save_path is not None: + save_path.parent.mkdir(parents=True, exist_ok=True) + fig.savefig(save_path, dpi=140, bbox_inches="tight") + if show: + plt.show() + return fig diff --git a/src/uavcatch/eval/recorder.py b/src/uavcatch/eval/recorder.py new file mode 100644 index 0000000..7871f08 --- /dev/null +++ b/src/uavcatch/eval/recorder.py @@ -0,0 +1,72 @@ +"""Simulation data recorder.""" + +from __future__ import annotations + +from dataclasses import dataclass, field + +import numpy as np + +from uavcatch.control.types import PhaseTarget, TargetFn +from uavcatch.dynamics.state import QuadState, quaternion_to_euler + + +@dataclass +class SimRecorder: + """Collect time-series during SimEngine.run.""" + + time: list[float] = field(default_factory=list) + position: list[np.ndarray] = field(default_factory=list) + velocity: list[np.ndarray] = field(default_factory=list) + target_position: list[np.ndarray] = field(default_factory=list) + target_velocity: list[np.ndarray] = field(default_factory=list) + euler: list[np.ndarray] = field(default_factory=list) + omega: list[np.ndarray] = field(default_factory=list) + tracking_error: list[np.ndarray] = field(default_factory=list) + + def reset(self) -> None: + self.time.clear() + self.position.clear() + self.velocity.clear() + self.target_position.clear() + self.target_velocity.clear() + self.euler.clear() + self.omega.clear() + self.tracking_error.clear() + + def record( + self, + t: float, + state: QuadState, + target_pos: np.ndarray, + target_vel: np.ndarray, + omega: np.ndarray, + ) -> None: + self.time.append(float(t)) + self.position.append(state.position.copy()) + self.velocity.append(state.velocity.copy()) + self.target_position.append(np.asarray(target_pos, dtype=np.float64).copy()) + self.target_velocity.append(np.asarray(target_vel, dtype=np.float64).copy()) + roll, pitch, yaw = quaternion_to_euler(state.quaternion) + self.euler.append(np.array([roll, pitch, yaw], dtype=np.float64)) + self.omega.append(np.asarray(omega, dtype=np.float64).copy()) + self.tracking_error.append(target_pos - state.position) + + @staticmethod + def resolve_target(target_fn: TargetFn, t: float) -> tuple[np.ndarray, np.ndarray]: + target = target_fn(t) + if isinstance(target, PhaseTarget): + return target.position.copy(), target.velocity.copy() + pos = np.asarray(target, dtype=np.float64) + return pos, np.zeros(3, dtype=np.float64) + + def as_arrays(self) -> dict[str, np.ndarray]: + return { + "time": np.asarray(self.time, dtype=np.float64), + "position": np.vstack(self.position) if self.position else np.empty((0, 3)), + "velocity": np.vstack(self.velocity) if self.velocity else np.empty((0, 3)), + "target_position": np.vstack(self.target_position) if self.target_position else np.empty((0, 3)), + "target_velocity": np.vstack(self.target_velocity) if self.target_velocity else np.empty((0, 3)), + "euler": np.vstack(self.euler) if self.euler else np.empty((0, 3)), + "omega": np.vstack(self.omega) if self.omega else np.empty((0, 4)), + "tracking_error": np.vstack(self.tracking_error) if self.tracking_error else np.empty((0, 3)), + } diff --git a/src/uavcatch/sim/__init__.py b/src/uavcatch/sim/__init__.py new file mode 100644 index 0000000..78cb08e --- /dev/null +++ b/src/uavcatch/sim/__init__.py @@ -0,0 +1,4 @@ +from uavcatch.sim.engine import SimEngine +from uavcatch.sim.mujoco_viz import MuJoCoViewer + +__all__ = ["SimEngine", "MuJoCoViewer"] diff --git a/src/uavcatch/sim/__pycache__/__init__.cpython-313.pyc b/src/uavcatch/sim/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..723da72666500b98047a531da3f764f575ab5087 GIT binary patch literal 299 zcmey&%ge<81P{GkvpRtEV-N=hn4pZ$DnQ0mhG2$ZMsEf$#v(=q5Sz)H$&0y&8OUZ1 zX3=LVVo7J!WP1tHqRDuRGdMHXH7`9gFBQn)@h$brcg_#XOf63>Dq;qzC}IH;ewwVe zgi904k`qgkGxUlxbM;ceI>0J!iKEEmmS*KA=f{_2R)MwNVvmnc%*lz5U&-(pWaBLZ z7ps_pqWrAXlIYq;;_lhPbtkwwJYKV m8o&s|#iBss12ZEd<6Q>Ly9@>oxW#7hUEor0WG~_b$^ZcHwNL~A literal 0 HcmV?d00001 diff --git a/src/uavcatch/sim/__pycache__/engine.cpython-313.pyc b/src/uavcatch/sim/__pycache__/engine.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..41a1b5d951a037ccef35562c72877ae46b3792cf GIT binary patch literal 5388 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 G?&3cp<|sb^ literal 0 HcmV?d00001 diff --git a/src/uavcatch/sim/__pycache__/mujoco_model.cpython-313.pyc b/src/uavcatch/sim/__pycache__/mujoco_model.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e285d2222e4f25763f41021a6ccab710b3bcc015 GIT binary patch literal 10362 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 Q{g5P&|9VQ_ODfL!KYt9kGynhq literal 0 HcmV?d00001 diff --git a/src/uavcatch/sim/__pycache__/mujoco_viz.cpython-313.pyc b/src/uavcatch/sim/__pycache__/mujoco_viz.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e4817e6083c99d9a4869d75aa7f8a6ae3535fe41 GIT binary patch literal 8919 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 literal 0 HcmV?d00001 diff --git a/src/uavcatch/sim/engine.py b/src/uavcatch/sim/engine.py new file mode 100644 index 0000000..3cdf142 --- /dev/null +++ b/src/uavcatch/sim/engine.py @@ -0,0 +1,90 @@ +"""Fixed-step quadcopter simulation engine.""" + +from __future__ import annotations + +import logging +from collections.abc import Callable +from dataclasses import dataclass, field + +import numpy as np + +from uavcatch.config import EnvironmentConfig, UavConfig +from uavcatch.dynamics.model import QuadDynamics +from uavcatch.dynamics.state import QuadState + +logger = logging.getLogger(__name__) + +ControllerFn = Callable[[QuadState, float], np.ndarray] + + +@dataclass +class SimEngine: + env_cfg: EnvironmentConfig + uav_cfg: UavConfig + dynamics: QuadDynamics = field(init=False) + state: QuadState = field(init=False) + time: float = 0.0 + _out_of_bounds_warned: bool = field(default=False, init=False) + + def __post_init__(self) -> None: + self.dynamics = QuadDynamics(self.env_cfg, self.uav_cfg) + self.state = self._initial_state() + + def _initial_state(self) -> QuadState: + init = self.uav_cfg.initial + return QuadState.from_initial( + position=init.position, + velocity=init.velocity, + attitude_euler=init.attitude_euler, + angular_rate=init.angular_rate, + ) + + def reset(self) -> QuadState: + self.time = 0.0 + self._out_of_bounds_warned = False + self.state = self._initial_state() + return self.state.copy() + + def step(self, omega: np.ndarray) -> QuadState: + dt = self.env_cfg.simulation.dt + self.state = self.dynamics.step(self.state, omega, dt, self.time) + self.time += dt + self._check_bounds() + return self.state.copy() + + def run( + self, + controller: ControllerFn, + duration: float | None = None, + on_step: Callable[[QuadState, float, np.ndarray], None] | None = None, + ) -> list[QuadState]: + if duration is None: + duration = self.env_cfg.simulation.duration + dt = self.env_cfg.simulation.dt + steps = int(np.ceil(duration / dt)) + history: list[QuadState] = [] + + for _ in range(steps): + omega = controller(self.state, self.time) + omega = self.dynamics.clip_omega(np.asarray(omega, dtype=np.float64)) + self.step(omega) + history.append(self.state.copy()) + if on_step is not None: + on_step(self.state, self.time, omega) + + return history + + def _check_bounds(self) -> None: + scene = self.env_cfg.scene + half_x = scene.size_x * 0.5 + half_y = scene.size_y * 0.5 + p = self.state.position + out = ( + abs(p[0]) > half_x + or abs(p[1]) > half_y + or p[2] < scene.ground_z + or p[2] > scene.size_z + ) + if out and not self._out_of_bounds_warned: + logger.warning("UAV left scene bounds at t=%.3f, position=%s", self.time, p) + self._out_of_bounds_warned = True diff --git a/src/uavcatch/sim/mujoco_model.py b/src/uavcatch/sim/mujoco_model.py new file mode 100644 index 0000000..a1d0c82 --- /dev/null +++ b/src/uavcatch/sim/mujoco_model.py @@ -0,0 +1,162 @@ +"""Procedural MuJoCo scene and quadcopter model generation.""" + +from __future__ import annotations + +import math +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from uavcatch.config import EnvironmentConfig, UavConfig + + +def _motor_xy(layout: str, arm_length: float) -> list[tuple[float, float]]: + """Return motor hub XY offsets matching QuadDynamics indexing.""" + a = arm_length + if layout == "plus": + return [(0.0, a), (-a, 0.0), (0.0, -a), (a, 0.0)] + return [(a, a), (-a, a), (-a, -a), (a, -a)] + + +def build_quad_body_xml(uav_cfg: UavConfig) -> str: + """Build detailed quadcopter MJCF body subtree (visual only, negligible mass).""" + arm = uav_cfg.body.arm_length + layout = uav_cfg.rotor.layout + motors = _motor_xy(layout, arm) + + prop_radius = max(0.06, arm * 0.38) + hub_radius = max(0.012, arm * 0.06) + hub_half_h = 0.012 + arm_radius = max(0.005, arm * 0.024) + fuselage_r = max(0.035, arm * 0.16) + deck_z = 0.018 + hub_z = deck_z + hub_half_h + leg_drop = 0.05 + + parts: list[str] = [ + '', + ' ', + ' ', + f' ', + f' ', + ' ', + ' ', + ] + + for idx, (mx, my) in enumerate(motors, start=1): + dist = math.hypot(mx, my) + if dist < 1e-9: + continue + arm_start_z = deck_z * 0.5 + parts.append( + f' ' + ) + parts.append( + f' ' + ) + parts.append(f' ') + parts.append( + f' ' + ) + parts.append( + f' ' + ) + blade_len = prop_radius + blade_w = max(0.012, prop_radius * 0.11) + blade_t = 0.0025 + for sign, suffix in ((1.0, "a"), (-1.0, "b")): + parts.append( + f' ' + ) + parts.append( + f' ' + ) + parts.append( + f' ' + ) + parts.append(" ") + + for idx, (mx, my) in enumerate(motors, start=1): + dist = math.hypot(mx, my) + if dist < 1e-9: + continue + sx = mx * 0.55 + sy = my * 0.55 + ex = mx * 0.92 + ey = my * 0.92 + parts.append( + f' ' + ) + parts.append( + f' ' + ) + + parts.append("") + return "\n".join(parts) + + +def build_scene_mjcf(env_cfg: EnvironmentConfig, uav_cfg: UavConfig) -> str: + """Build full MuJoCo scene XML string.""" + scene = env_cfg.scene + half_x = scene.size_x * 0.5 + half_y = scene.size_y * 0.5 + ground = max(scene.ground_z, 0.0) + quad_body = build_quad_body_xml(uav_cfg) + + return f""" + + +""" diff --git a/src/uavcatch/sim/mujoco_viz.py b/src/uavcatch/sim/mujoco_viz.py new file mode 100644 index 0000000..a006ba9 --- /dev/null +++ b/src/uavcatch/sim/mujoco_viz.py @@ -0,0 +1,142 @@ +"""MuJoCo passive viewer for quadcopter visualization.""" + +from __future__ import annotations + +import time +from pathlib import Path + +import mujoco +import mujoco.viewer +import numpy as np + +from uavcatch.config import EnvironmentConfig, UavConfig +from uavcatch.dynamics.state import QuadState +from uavcatch.sim.mujoco_model import build_scene_mjcf + +_ASSETS_DIR = Path(__file__).resolve().parents[3] / "assets" / "mujoco" + + +class MuJoCoViewer: + """Sync explicit dynamics state to MuJoCo scene (render only, no physics).""" + + def __init__( + self, + env_cfg: EnvironmentConfig, + uav_cfg: UavConfig | None = None, + scene_path: Path | None = None, + ) -> None: + self.env_cfg = env_cfg + self.uav_cfg = uav_cfg + if scene_path is not None: + if not scene_path.is_file(): + raise FileNotFoundError(f"MuJoCo scene not found: {scene_path}") + self.model = mujoco.MjModel.from_xml_path(str(scene_path)) + else: + if uav_cfg is None: + raise ValueError("uav_cfg is required when scene_path is not provided") + mjcf = build_scene_mjcf(env_cfg, uav_cfg) + self.model = mujoco.MjModel.from_xml_string(mjcf) + self.data = mujoco.MjData(self.model) + self._viewer = None + self._rotor_joint_ids = [ + mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_JOINT, f"rotor{i}_hinge") + for i in range(1, 5) + ] + self._rotor_angles = np.zeros(4, dtype=np.float64) + # Real-time rendering bookkeeping + self._sim_clock = 0.0 + self._next_frame_sim = 0.0 + self._wall_anchor: float | None = None + + def __enter__(self) -> MuJoCoViewer: + self._viewer = mujoco.viewer.launch_passive(self.model, self.data) + self._setup_camera() + self._sim_clock = 0.0 + self._next_frame_sim = 0.0 + self._wall_anchor = None + return self + + def __exit__(self, exc_type, exc, tb) -> None: + if self._viewer is not None: + self._viewer.close() + self._viewer = None + + def _setup_camera(self) -> None: + if self._viewer is None: + return + dist = self.env_cfg.viewer.camera_distance + self._viewer.cam.distance = dist + self._viewer.cam.elevation = -22.0 + self._viewer.cam.azimuth = 140.0 + init_z = 1.0 + if self.uav_cfg is not None: + init_z = float(self.uav_cfg.initial.position[2]) + self._viewer.cam.lookat[:] = [0.0, 0.0, init_z] + + def _write_state(self, state: QuadState) -> None: + """Push pose and accumulated rotor angles into MuJoCo qpos.""" + self.data.qpos[0:3] = state.position + self.data.qpos[3:7] = state.quaternion + for i in range(4): + joint_adr = self.model.jnt_qposadr[self._rotor_joint_ids[i]] + self.data.qpos[joint_adr] = self._rotor_angles[i] + mujoco.mj_forward(self.model, self.data) + + def sync(self, state: QuadState, omega: np.ndarray | None = None, dt: float = 0.002) -> None: + """Immediately render one frame (no frame skipping / throttling).""" + if omega is not None: + self._rotor_angles += np.asarray(omega, dtype=np.float64) * dt + self._write_state(state) + if self._viewer is not None: + self._viewer.sync() + + def render_realtime( + self, + state: QuadState, + omega: np.ndarray | None, + sim_dt: float, + speed: float = 1.0, + rotor_visual_scale: float = 1.0, + ) -> bool: + """Advance one physics step; render + throttle only on frame boundaries. + + Decouples physics rate from display rate: the rotor angle integrates every + physics step (so spin speed is physically correct), but we only draw at + `viewer.fps` and sleep to keep wall-clock time aligned with sim time scaled + by `speed`. Returns True when a frame was actually drawn. + + `rotor_visual_scale` only affects the visual spin (useful to tame the + stroboscopic / wagon-wheel aliasing of a very fast rotor); set < 1 to slow + the displayed blades without changing the physics. + """ + if omega is not None: + self._rotor_angles += np.asarray(omega, dtype=np.float64) * sim_dt * rotor_visual_scale + self._sim_clock += sim_dt + + frame_interval = 1.0 / max(self.env_cfg.viewer.fps, 1) + if self._sim_clock + 1e-12 < self._next_frame_sim: + return False + self._next_frame_sim += frame_interval + if self._next_frame_sim < self._sim_clock: + # Fell behind (e.g. heavy step); resync schedule to avoid a render burst. + self._next_frame_sim = self._sim_clock + frame_interval + + self._write_state(state) + if self._viewer is not None: + self._viewer.sync() + + now = time.perf_counter() + if self._wall_anchor is None: + self._wall_anchor = now + target = self._wall_anchor + self._sim_clock / max(speed, 1e-9) + delay = target - time.perf_counter() + if delay > 0.0: + time.sleep(delay) + return True + + def is_running(self) -> bool: + return self._viewer is not None and self._viewer.is_running() + + def sleep_for_fps(self) -> None: + fps = max(self.env_cfg.viewer.fps, 1) + time.sleep(1.0 / fps) diff --git a/tests/__pycache__/test_control.cpython-313-pytest-9.1.1.pyc b/tests/__pycache__/test_control.cpython-313-pytest-9.1.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a8e14178194fc7ee4bb2f101ee95106f62bc8890 GIT binary patch literal 8291 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 b_=A9l-Tgs}m)-e6TbS+qFxJO*Qg7?uiWfG< literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_dynamics.cpython-313-pytest-9.1.1.pyc b/tests/__pycache__/test_dynamics.cpython-313-pytest-9.1.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..93b4e7c064cd455abc6046246036c8ccce76b8a0 GIT binary patch literal 23496 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 SJ3g None: + q = euler_to_quaternion(0.1, -0.05, 0.2) + err = quaternion_attitude_error(q, q) + np.testing.assert_allclose(err, 0.0, atol=1e-12) + + +def test_control_allocator_hover(dynamics, uav_cfg) -> None: + allocator = ControlAllocator(uav_cfg) + thrust = uav_cfg.body.mass * dynamics.env_cfg.physics.gravity + omega = allocator.allocate(thrust, np.zeros(3)) + assert np.all(omega > 0.0) + assert np.allclose(omega, omega[0], rtol=1e-3) + + +def test_cascaded_linear_hover(env_cfg, uav_cfg, dynamics, control_cfg) -> None: + engine = SimEngine(env_cfg, uav_cfg) + engine.reset() + controller = build_cascaded_controller( + control_cfg, + dynamics, + uav_cfg, + target_position=np.array([0.0, 0.0, 2.0]), + ) + engine.run(controller, duration=5.0) + err = np.linalg.norm(engine.state.position - np.array([0.0, 0.0, 2.0])) + assert err < 0.5 + + +def test_geometric_circle_tracking(env_cfg, uav_cfg, dynamics, control_cfg) -> None: + control_cfg.controller.mode = "geometric" + env_cfg.simulation.duration = 12.0 + engine = SimEngine(env_cfg, uav_cfg) + engine.reset() + + pos_fn, yaw_fn, accel_fn = circle_trajectory( + center=np.zeros(3), + radius=2.0, + height=2.0, + period=10.0, + ) + init = pos_fn(0.0) + controller = build_cascaded_controller( + control_cfg, + dynamics, + uav_cfg, + target_position=init.position, + target_velocity=init.velocity, + target_yaw=float(yaw_fn(0.0)), + ) + tracker = TrackingController( + controller=controller, + target_fn=pos_fn, + yaw_fn=yaw_fn, + accel_fn=accel_fn, + ) + engine.run(tracker, duration=12.0) + target = pos_fn(12.0) + err = np.linalg.norm(engine.state.position - target.position) + assert err < 1.5 diff --git a/tests/test_dynamics.py b/tests/test_dynamics.py new file mode 100644 index 0000000..be315a3 --- /dev/null +++ b/tests/test_dynamics.py @@ -0,0 +1,183 @@ +"""Tests for quadcopter dynamics and configuration.""" + +from __future__ import annotations + +from pathlib import Path + +import mujoco +import numpy as np +import pytest + +from uavcatch.config import EnvironmentConfig, UavConfig, load_control_config, load_environment_config, load_uav_config +from uavcatch.dynamics.model import QuadDynamics +from uavcatch.dynamics.state import QuadState, euler_to_quaternion, normalize_quaternion +from uavcatch.dynamics.wind import WindModel +from uavcatch.sim.mujoco_model import _motor_xy, build_scene_mjcf + +ROOT = Path(__file__).resolve().parents[1] + + +@pytest.fixture +def env_cfg() -> EnvironmentConfig: + return load_environment_config(ROOT / "environment.toml") + + +@pytest.fixture +def uav_cfg() -> UavConfig: + return load_uav_config(ROOT / "uav.toml") + + +@pytest.fixture +def dynamics(env_cfg: EnvironmentConfig, uav_cfg: UavConfig) -> QuadDynamics: + return QuadDynamics(env_cfg, uav_cfg) + + +def test_config_load_defaults(env_cfg: EnvironmentConfig, uav_cfg: UavConfig) -> None: + assert env_cfg.simulation.dt > 0.0 + assert env_cfg.simulation.integrator == "rk4" + assert uav_cfg.body.mass == 1.3 + assert uav_cfg.rotor.layout == "x" + + +def test_control_config_load() -> None: + cfg = load_control_config(ROOT / "control.toml") + assert cfg.controller.mode in ("linear", "geometric") + assert cfg.position_linear.kp == 0.8 + assert cfg.position_geometric.kp == 2.5 + assert cfg.eval.circle.radius == 2.5 + + +def test_config_missing_file() -> None: + with pytest.raises(FileNotFoundError): + load_environment_config(ROOT / "missing.toml") + + +def test_quaternion_normalization_after_integration(dynamics: QuadDynamics) -> None: + state = QuadState.from_initial( + position=[0.0, 0.0, 1.0], + velocity=[0.0, 0.0, 0.0], + attitude_euler=[0.1, -0.05, 0.2], + angular_rate=[0.5, -0.3, 0.1], + ) + omega = np.full(4, dynamics.hover_omega()) + dt = dynamics.env_cfg.simulation.dt + + for _ in range(1000): + state = dynamics.step(state, omega, dt) + q_norm = np.linalg.norm(state.quaternion) + assert abs(q_norm - 1.0) < 1e-6 + + +def test_hover_acceleration_near_zero(dynamics: QuadDynamics) -> None: + state = QuadState.from_initial( + position=[0.0, 0.0, 1.0], + velocity=[0.0, 0.0, 0.0], + attitude_euler=[0.0, 0.0, 0.0], + angular_rate=[0.0, 0.0, 0.0], + ) + omega_hover = dynamics.hover_omega() + omega = np.full(4, omega_hover) + deriv = dynamics.derivatives(state, omega) + assert abs(deriv[5]) < 0.5 + + +def test_free_fall(dynamics: QuadDynamics) -> None: + state = QuadState.from_initial( + position=[0.0, 0.0, 5.0], + velocity=[0.0, 0.0, 0.0], + attitude_euler=[0.0, 0.0, 0.0], + angular_rate=[0.0, 0.0, 0.0], + ) + omega = np.zeros(4) + dt = dynamics.env_cfg.simulation.dt + z_prev = state.position[2] + + for _ in range(200): + state = dynamics.step(state, omega, dt) + assert state.position[2] <= z_prev + 1e-9 + z_prev = state.position[2] + + assert state.position[2] < 5.0 + + +def test_state_vector_roundtrip() -> None: + q = euler_to_quaternion(0.1, 0.2, 0.3) + state = QuadState( + position=np.array([1.0, 2.0, 3.0]), + velocity=np.array([0.1, 0.2, 0.3]), + quaternion=normalize_quaternion(q), + angular_rate=np.array([0.01, 0.02, 0.03]), + ) + restored = QuadState.from_vector(state.as_vector()) + np.testing.assert_allclose(restored.position, state.position) + np.testing.assert_allclose(restored.velocity, state.velocity) + np.testing.assert_allclose(restored.angular_rate, state.angular_rate) + + +def test_drag_opposes_motion(dynamics: QuadDynamics) -> None: + forward = np.array([5.0, 0.0, 0.0], dtype=np.float64) + force = dynamics.aerodynamic_force(forward, np.zeros(3)) + # Drag must oppose velocity and be purely along -X here. + assert force[0] < 0.0 + np.testing.assert_allclose(force[1:], 0.0, atol=1e-12) + + +def test_quadratic_drag_scaling(dynamics: QuadDynamics) -> None: + f1 = dynamics.aerodynamic_force(np.array([1.0, 0.0, 0.0]), np.zeros(3)) + f2 = dynamics.aerodynamic_force(np.array([2.0, 0.0, 0.0]), np.zeros(3)) + # Quadratic drag: doubling speed quadruples the force (when linear term is 0). + np.testing.assert_allclose(abs(f2[0]) / abs(f1[0]), 4.0, rtol=1e-6) + + +def test_drag_uses_relative_wind(dynamics: QuadDynamics) -> None: + wind = np.array([3.0, 0.0, 0.0], dtype=np.float64) + # Moving with the wind -> zero relative airspeed -> zero drag. + force = dynamics.aerodynamic_force(wind.copy(), wind) + np.testing.assert_allclose(force, 0.0, atol=1e-12) + + +def test_terminal_velocity_with_drag(dynamics: QuadDynamics) -> None: + state = QuadState.from_initial( + position=[0.0, 0.0, 100.0], + velocity=[0.0, 0.0, 0.0], + attitude_euler=[0.0, 0.0, 0.0], + angular_rate=[0.0, 0.0, 0.0], + ) + omega = np.zeros(4) + dt = dynamics.env_cfg.simulation.dt + for _ in range(5000): + state = dynamics.step(state, omega, dt) + # With quadratic drag, fall speed must saturate well below the drag-free + # value (g * t = 9.81 * 10 ~ 98 m/s). + assert abs(state.velocity[2]) < 60.0 + + +def test_wind_model_gust_oscillates() -> None: + wind = WindModel( + base_velocity=np.array([2.0, 0.0, 0.0]), + gust_amplitude=1.0, + gust_frequency=1.0, + ) + assert wind.is_active + # Quarter period -> peak gust along +X direction. + np.testing.assert_allclose(wind.sample(0.25)[0], 3.0, atol=1e-6) + np.testing.assert_allclose(wind.sample(0.0)[0], 2.0, atol=1e-6) + + +def test_wind_config_parsed(env_cfg: EnvironmentConfig) -> None: + assert env_cfg.wind.velocity == [0.0, 0.0, 0.0] + assert hasattr(env_cfg.wind, "gust_amplitude") + + +def test_mujoco_model_motor_alignment(env_cfg: EnvironmentConfig, uav_cfg: UavConfig) -> None: + mjcf = build_scene_mjcf(env_cfg, uav_cfg) + model = mujoco.MjModel.from_xml_string(mjcf) + arm = uav_cfg.body.arm_length + expected = _motor_xy(uav_cfg.rotor.layout, arm) + + for idx, (mx, my) in enumerate(expected, start=1): + body_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_BODY, f"rotor{idx}") + assert body_id >= 0 + pos = model.body_pos[body_id] + np.testing.assert_allclose(pos[0], mx, rtol=0, atol=1e-6) + np.testing.assert_allclose(pos[1], my, rtol=0, atol=1e-6) diff --git a/tests/test_eval.py b/tests/test_eval.py new file mode 100644 index 0000000..ceb7332 --- /dev/null +++ b/tests/test_eval.py @@ -0,0 +1,72 @@ +"""Tests for control evaluation metrics.""" + +from __future__ import annotations + +import numpy as np + +from uavcatch.eval.metrics import compute_tracking_metrics, evaluate_maneuver, _step_response_metrics +from uavcatch.control.pid import circle_trajectory_with_takeoff +from uavcatch.eval.maneuvers import circle_track, step_position + + +def test_tracking_rmse() -> None: + err = np.array([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 0.0]]) + m = compute_tracking_metrics(err) + assert abs(m.rmse[0] - np.sqrt(1 / 3)) < 1e-9 + assert m.rmse_3d > 0.0 + + +def test_step_rise_and_settle() -> None: + t = np.linspace(0, 10, 1000) + y = np.where(t < 1.0, 0.0, 1.0 - np.exp(-(t - 1.0) / 0.5)) + m = _step_response_metrics(t, y, step_time=1.0, y0=0.0, yf=1.0) + assert m.rise_time is not None + assert m.settling_time is not None + assert m.overshoot_pct < 5.0 + + +def test_evaluate_maneuver_step_spec() -> None: + spec = step_position( + np.array([0.0, 0.0, 2.0]), + np.array([0.0, 0.0, 3.0]), + step_time=2.0, + duration=10.0, + axis=2, + ) + n = 200 + time = np.linspace(0, 10, n) + z = np.where(time < 2.0, 2.0, 2.0 + 0.9 * (1 - np.exp(-(time - 2.0) / 1.0))) + data = { + "time": time, + "position": np.column_stack([np.zeros(n), np.zeros(n), z]), + "tracking_error": np.column_stack([np.zeros(n), np.zeros(n), 3.0 - z]), + "euler": np.zeros((n, 3)), + "omega": np.full((n, 4), 550.0), + } + metrics = evaluate_maneuver(spec, data, omega_max=838.0, mode="linear") + assert metrics.step is not None + assert metrics.tracking.rmse_3d < 0.6 + + +def test_circle_takeoff_phase() -> None: + spec = circle_track(takeoff_time=2.0, ground_z=0.0, height=2.0, radius=2.5) + assert spec.initial_position == (0.0, 0.0, 0.05) + + t0 = spec.target_fn(0.0) + assert t0.position[2] == 0.05 + + t1 = spec.target_fn(1.0) + assert 0.05 < t1.position[2] < 2.0 + + t2 = spec.target_fn(2.0) + np.testing.assert_allclose(t2.position, [2.5, 0.0, 2.0], atol=1e-9) + + pos_fn, _, _ = circle_trajectory_with_takeoff( + center=np.zeros(3), + radius=2.5, + height=2.0, + period=10.0, + takeoff_time=2.0, + ) + t3 = pos_fn(3.0) + assert t3.position[0] > 2.0 diff --git a/uav.toml b/uav.toml new file mode 100644 index 0000000..fcbaf6d --- /dev/null +++ b/uav.toml @@ -0,0 +1,28 @@ +[body] +mass = 1.3 +arm_length = 0.23 +Ixx = 0.0075 +Iyy = 0.0075 +Izz = 0.013 + +[rotor] +# T_i = kf * omega_i^2; 悬停标定: kf = m*g / (4 * omega_hover^2), omega_hover ≈ 550 rad/s +kf = 1.054e-5 +km = 2.64e-7 +omega_min = 0.0 +omega_max = 838.0 +layout = "x" + +[initial] +position = [0.0, 0.0, 1.0] +velocity = [0.0, 0.0, 0.0] +attitude_euler = [0.0, 0.0, 0.0] +angular_rate = [0.0, 0.0, 0.0] + +[drag] +# 平动阻力(世界系): F = -linear * v_rel - 0.5 * rho * cd * area * |v_rel| * v_rel +# v_rel = 机体速度 - 风速; rho = environment.toml 的 air_density +linear = 0.0 # 线性阻力系数 N/(m/s),低速近似,0 则只用二次项 +angular = 0.05 # 角速度阻尼 N·m/(rad/s) +cd = 0.8 # 气动阻力系数 +area = 0.1 # 等效迎风面积 m^2