quadcopter-sim 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- quadcopter/__init__.py +77 -0
- quadcopter/__main__.py +332 -0
- quadcopter/controllers.py +295 -0
- quadcopter/dynamics.py +174 -0
- quadcopter/env.py +119 -0
- quadcopter/evaluation.py +419 -0
- quadcopter/gym_env.py +145 -0
- quadcopter/logging.py +717 -0
- quadcopter/plotting.py +393 -0
- quadcopter/simulation.py +202 -0
- quadcopter/utils.py +152 -0
- quadcopter_sim-0.1.0.dist-info/METADATA +589 -0
- quadcopter_sim-0.1.0.dist-info/RECORD +17 -0
- quadcopter_sim-0.1.0.dist-info/WHEEL +5 -0
- quadcopter_sim-0.1.0.dist-info/entry_points.txt +2 -0
- quadcopter_sim-0.1.0.dist-info/licenses/LICENSE +21 -0
- quadcopter_sim-0.1.0.dist-info/top_level.txt +1 -0
quadcopter/__init__.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""
|
|
2
|
+
quadcopter – mini‑package for 6‑DoF quadrotor simulation & control.
|
|
3
|
+
|
|
4
|
+
Typical usage
|
|
5
|
+
-------------
|
|
6
|
+
>>> from quadcopter import simulate, create_pid_position_controller, plot_trajectory
|
|
7
|
+
>>> controller = create_pid_position_controller([1, 1, 2])
|
|
8
|
+
>>> t, states, u = simulate(4.0, 0.02, controller)
|
|
9
|
+
>>> plot_trajectory(t, states, u)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
# ---------------------------------------------------------------------
|
|
15
|
+
# Semantic version (reads from installed package metadata if available)
|
|
16
|
+
# ---------------------------------------------------------------------
|
|
17
|
+
from importlib.metadata import PackageNotFoundError, version as _pkg_version
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
__version__: str = _pkg_version(__name__)
|
|
21
|
+
except PackageNotFoundError: # editable / source checkout
|
|
22
|
+
__version__ = "0.0.0.dev0"
|
|
23
|
+
|
|
24
|
+
__author__: str = "2black0"
|
|
25
|
+
__license__: str = "MIT"
|
|
26
|
+
|
|
27
|
+
# ---------------------------------------------------------------------
|
|
28
|
+
# Public re‑exports – the “one‑stop” API
|
|
29
|
+
# ---------------------------------------------------------------------
|
|
30
|
+
from .dynamics import Params, QuadState, derivative # physics core
|
|
31
|
+
from .simulation import simulate, HoverController # integrators
|
|
32
|
+
from .plotting import plot_trajectory, animate_trajectory # visualisation
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
# physics
|
|
36
|
+
"Params", "QuadState", "derivative",
|
|
37
|
+
# simulation
|
|
38
|
+
"simulate", "HoverController",
|
|
39
|
+
# visualisation
|
|
40
|
+
"plot_trajectory", "animate_trajectory",
|
|
41
|
+
# meta
|
|
42
|
+
"__version__",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
from .env import QuadcopterEnv, RealTimeQuadcopterEnv # add to earlier export block
|
|
46
|
+
__all__.extend(["QuadcopterEnv", "RealTimeQuadcopterEnv"])
|
|
47
|
+
|
|
48
|
+
# Conditional imports for optional features
|
|
49
|
+
try:
|
|
50
|
+
from .controllers import PIDController, PositionController, LQRController
|
|
51
|
+
__all__.extend(["PIDController", "PositionController", "LQRController"])
|
|
52
|
+
except ImportError:
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
from .gym_env import QuadcopterGymEnv
|
|
57
|
+
__all__.append("QuadcopterGymEnv")
|
|
58
|
+
except ImportError:
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
from .logging import SimulationLog, simulate_with_logging, AcademicLog, simulate_with_academic_logging
|
|
63
|
+
__all__.extend(["SimulationLog", "simulate_with_logging", "AcademicLog", "simulate_with_academic_logging"])
|
|
64
|
+
except ImportError:
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
from .evaluation import AcademicEvaluator
|
|
69
|
+
__all__.append("AcademicEvaluator")
|
|
70
|
+
except ImportError:
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
from .utils import create_pid_position_controller, create_pid_attitude_controller, create_lqr_controller, create_hover_controller
|
|
75
|
+
__all__.extend(["create_pid_position_controller", "create_pid_attitude_controller", "create_lqr_controller", "create_hover_controller"])
|
|
76
|
+
except ImportError:
|
|
77
|
+
pass
|
quadcopter/__main__.py
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
"""
|
|
2
|
+
quadcopter.__main__
|
|
3
|
+
-------------------
|
|
4
|
+
Comprehensive CLI for quadcopter simulation, control, and analysis.
|
|
5
|
+
|
|
6
|
+
Examples
|
|
7
|
+
--------
|
|
8
|
+
python -m quadcopter --plot
|
|
9
|
+
python -m quadcopter --controller pid --target-pos 1 1 2 --duration 10 --plot
|
|
10
|
+
python -m quadcopter --controller lqr --academic-log results --duration 5
|
|
11
|
+
quadcopter-demo --duration 6 --csv flight.csv
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import logging
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
import json
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
from numpy.typing import NDArray
|
|
23
|
+
|
|
24
|
+
from .dynamics import Params, QuadState
|
|
25
|
+
from .plotting import plot_trajectory, plot_control_errors, plot_3d_trajectory_comparison
|
|
26
|
+
from .simulation import simulate, BaseController, HoverController
|
|
27
|
+
from .controllers import PIDController, PositionController, LQRController
|
|
28
|
+
from .logging import SimulationLog, simulate_with_logging, AcademicLog, simulate_with_academic_logging
|
|
29
|
+
from .evaluation import AcademicEvaluator
|
|
30
|
+
from .utils import create_pid_position_controller, create_lqr_controller, create_hover_controller
|
|
31
|
+
|
|
32
|
+
# ----------------------------------------------------------------------
|
|
33
|
+
# Controller implementations
|
|
34
|
+
# ----------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
class _HoverController(BaseController):
|
|
37
|
+
"""Return a fixed motor speed that balances weight at t=0."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, omega_hover: float) -> None:
|
|
40
|
+
self._cmd: NDArray[np.float64] = np.full(4, omega_hover, dtype=np.float64)
|
|
41
|
+
|
|
42
|
+
def update(self, t: float, state: QuadState) -> NDArray[np.float64]: # noqa: D401
|
|
43
|
+
return self._cmd
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _create_pid_controller(args: argparse.Namespace, params: Params) -> BaseController:
|
|
47
|
+
"""Create a PID controller based on CLI arguments."""
|
|
48
|
+
if args.controller == "pid":
|
|
49
|
+
if args.target_pos:
|
|
50
|
+
# Use utility function for consistent controller creation
|
|
51
|
+
return create_pid_position_controller(
|
|
52
|
+
target_pos=args.target_pos,
|
|
53
|
+
kp=args.pid_kp,
|
|
54
|
+
ki=args.pid_ki,
|
|
55
|
+
kd=args.pid_kd
|
|
56
|
+
)
|
|
57
|
+
else:
|
|
58
|
+
# Simple hover controller
|
|
59
|
+
return create_hover_controller(params)
|
|
60
|
+
return create_hover_controller(params)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _create_lqr_controller(args: argparse.Namespace, params: Params) -> BaseController:
|
|
64
|
+
"""Create an LQR controller based on CLI arguments."""
|
|
65
|
+
if args.controller == "lqr":
|
|
66
|
+
# Use utility function for consistent controller creation
|
|
67
|
+
return create_lqr_controller(params=params)
|
|
68
|
+
return create_hover_controller(params)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ----------------------------------------------------------------------
|
|
72
|
+
def main(argv: list[str] | None = None) -> None: # noqa: D401
|
|
73
|
+
"""Parse CLI arguments, run simulation with selected controller, and output results."""
|
|
74
|
+
# ------------------------------------------------------------------ CLI
|
|
75
|
+
p = argparse.ArgumentParser(
|
|
76
|
+
prog="python -m quadcopter",
|
|
77
|
+
description="Comprehensive quadcopter simulation and analysis tool.",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Simulation parameters
|
|
81
|
+
p.add_argument("--duration", type=float, default=4.0, help="simulation time [s]")
|
|
82
|
+
p.add_argument("--dt", type=float, default=0.02, help="integration step [s]")
|
|
83
|
+
p.add_argument(
|
|
84
|
+
"--method",
|
|
85
|
+
choices=["rk45", "rk4"],
|
|
86
|
+
default="rk4",
|
|
87
|
+
help="integration method (adaptive RK45 or fixed‑step RK4)",
|
|
88
|
+
)
|
|
89
|
+
p.add_argument("--rtol", type=float, default=1e-5, help="solver rtol")
|
|
90
|
+
p.add_argument("--atol", type=float, default=1e-7, help="solver atol")
|
|
91
|
+
|
|
92
|
+
# Controller selection
|
|
93
|
+
p.add_argument(
|
|
94
|
+
"--controller",
|
|
95
|
+
choices=["hover", "pid", "lqr"],
|
|
96
|
+
default="hover",
|
|
97
|
+
help="controller type to use"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# PID controller parameters
|
|
101
|
+
p.add_argument(
|
|
102
|
+
"--pid-kp",
|
|
103
|
+
type=float,
|
|
104
|
+
nargs=3,
|
|
105
|
+
default=[2.0, 2.0, 4.0],
|
|
106
|
+
help="PID Kp gains for x, y, z axes"
|
|
107
|
+
)
|
|
108
|
+
p.add_argument(
|
|
109
|
+
"--pid-ki",
|
|
110
|
+
type=float,
|
|
111
|
+
nargs=3,
|
|
112
|
+
default=[0.1, 0.1, 0.2],
|
|
113
|
+
help="PID Ki gains for x, y, z axes"
|
|
114
|
+
)
|
|
115
|
+
p.add_argument(
|
|
116
|
+
"--pid-kd",
|
|
117
|
+
type=float,
|
|
118
|
+
nargs=3,
|
|
119
|
+
default=[0.5, 0.5, 1.0],
|
|
120
|
+
help="PID Kd gains for x, y, z axes"
|
|
121
|
+
)
|
|
122
|
+
p.add_argument(
|
|
123
|
+
"--target-pos",
|
|
124
|
+
type=float,
|
|
125
|
+
nargs=3,
|
|
126
|
+
help="target position [x, y, z] for position controller"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Output options
|
|
130
|
+
p.add_argument("--plot", action="store_true", help="show matplotlib figure")
|
|
131
|
+
p.add_argument("--plot-errors", action="store_true", help="show control error plot")
|
|
132
|
+
p.add_argument("--plot-comparison", action="store_true", help="show trajectory comparison plot")
|
|
133
|
+
p.add_argument("--csv", type=Path, help="save (t, state, control) to CSV")
|
|
134
|
+
p.add_argument("--json", type=Path, help="save simulation log to JSON")
|
|
135
|
+
p.add_argument("--matlab", type=Path, help="save simulation log to MATLAB .mat file")
|
|
136
|
+
|
|
137
|
+
# Academic logging and evaluation
|
|
138
|
+
p.add_argument("--academic-log", type=Path, help="enable academic logging and save to directory")
|
|
139
|
+
p.add_argument("--controller-type", choices=["pid", "lqr", "rl"], default="pid",
|
|
140
|
+
help="controller type for academic logging")
|
|
141
|
+
|
|
142
|
+
# Initial conditions
|
|
143
|
+
p.add_argument("--init-pos", type=float, nargs=3, default=[0.0, 0.0, 0.0],
|
|
144
|
+
help="initial position [x, y, z]")
|
|
145
|
+
p.add_argument("--init-vel", type=float, nargs=3, default=[0.0, 0.0, 0.0],
|
|
146
|
+
help="initial velocity [vx, vy, vz]")
|
|
147
|
+
|
|
148
|
+
# Verbosity
|
|
149
|
+
p.add_argument("--quiet", action="store_true", help="suppress info output")
|
|
150
|
+
p.add_argument("--verbose", action="store_true", help="enable verbose output")
|
|
151
|
+
|
|
152
|
+
args = p.parse_args(argv)
|
|
153
|
+
|
|
154
|
+
# ------------------------------------------------------------------ logging
|
|
155
|
+
log_level = logging.WARNING if args.quiet else (logging.DEBUG if args.verbose else logging.INFO)
|
|
156
|
+
logging.basicConfig(level=log_level, format="%(message)s")
|
|
157
|
+
log = logging.getLogger("quadcopter")
|
|
158
|
+
|
|
159
|
+
# ------------------------------------------------------------------ setup
|
|
160
|
+
params = Params()
|
|
161
|
+
|
|
162
|
+
# Initial state
|
|
163
|
+
initial_state = QuadState(
|
|
164
|
+
pos=np.array(args.init_pos),
|
|
165
|
+
vel=np.array(args.init_vel),
|
|
166
|
+
quat=np.array([1.0, 0.0, 0.0, 0.0]),
|
|
167
|
+
ang_vel=np.array([0.0, 0.0, 0.0])
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# ------------------------------------------------------------------ controller selection
|
|
171
|
+
if args.controller == "pid":
|
|
172
|
+
controller = _create_pid_controller(args, params)
|
|
173
|
+
log.info("Using PID controller")
|
|
174
|
+
elif args.controller == "lqr":
|
|
175
|
+
controller = _create_lqr_controller(args, params)
|
|
176
|
+
log.info("Using LQR controller")
|
|
177
|
+
else:
|
|
178
|
+
controller = create_hover_controller(params)
|
|
179
|
+
log.info("Using hover controller")
|
|
180
|
+
|
|
181
|
+
# ------------------------------------------------------------------ simulation
|
|
182
|
+
if args.academic_log:
|
|
183
|
+
log.info("Running simulation with academic logging...")
|
|
184
|
+
# Use academic logging
|
|
185
|
+
log_result = simulate_with_academic_logging(
|
|
186
|
+
duration=args.duration,
|
|
187
|
+
dt=args.dt,
|
|
188
|
+
controller=controller,
|
|
189
|
+
initial_state=initial_state,
|
|
190
|
+
params=params,
|
|
191
|
+
ref_position=np.array(args.target_pos) if args.target_pos else None,
|
|
192
|
+
method=args.method,
|
|
193
|
+
rtol=args.rtol,
|
|
194
|
+
atol=args.atol,
|
|
195
|
+
max_step=args.dt,
|
|
196
|
+
controller_type=args.controller_type
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# Save academic log in multiple formats
|
|
200
|
+
args.academic_log.mkdir(parents=True, exist_ok=True)
|
|
201
|
+
log_result.save_csv(args.academic_log / "simulation.csv")
|
|
202
|
+
log_result.save_json(args.academic_log / "simulation.json")
|
|
203
|
+
try:
|
|
204
|
+
log_result.save_matlab(args.academic_log / "simulation.mat")
|
|
205
|
+
except ImportError:
|
|
206
|
+
log.warning("scipy not available, cannot save MATLAB format")
|
|
207
|
+
|
|
208
|
+
# Generate academic evaluation if requested
|
|
209
|
+
evaluator = AcademicEvaluator(log_result)
|
|
210
|
+
metrics = evaluator.generate_comprehensive_analysis(args.academic_log)
|
|
211
|
+
|
|
212
|
+
log.info("Academic results saved to %s", args.academic_log)
|
|
213
|
+
|
|
214
|
+
# Use regular arrays for plotting/CSV
|
|
215
|
+
t = np.array(log_result.times)
|
|
216
|
+
# Reconstruct states array from logged data
|
|
217
|
+
# For simplicity, we'll use identity quaternion [1,0,0,0] since we don't have the raw quaternions
|
|
218
|
+
states = np.array([[
|
|
219
|
+
pos[0], pos[1], pos[2], # position: px, py, pz
|
|
220
|
+
vel[0], vel[1], vel[2], # velocity: vx, vy, vz
|
|
221
|
+
1.0, 0.0, 0.0, 0.0, # quaternion: qw, qx, qy, qz (identity for simplicity)
|
|
222
|
+
ang_vel[0], ang_vel[1], ang_vel[2] # angular velocity: wx, wy, wz
|
|
223
|
+
] for pos, vel, ang_vel in zip(
|
|
224
|
+
log_result.positions, log_result.velocities,
|
|
225
|
+
log_result.angular_rates
|
|
226
|
+
)])
|
|
227
|
+
controls = np.array(log_result.motor_speeds)
|
|
228
|
+
else:
|
|
229
|
+
log.info("Running simulation...")
|
|
230
|
+
t, states, controls = simulate(
|
|
231
|
+
duration=args.duration,
|
|
232
|
+
dt=args.dt,
|
|
233
|
+
controller=controller,
|
|
234
|
+
initial_state=initial_state,
|
|
235
|
+
params=params,
|
|
236
|
+
method=args.method,
|
|
237
|
+
rtol=args.rtol,
|
|
238
|
+
atol=args.atol,
|
|
239
|
+
max_step=args.dt,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# ------------------------------------------------------------------ reporting
|
|
243
|
+
final_pos = states[-1, 0:3]
|
|
244
|
+
final_z: float = float(final_pos[2])
|
|
245
|
+
log.info("final position: [%.3f, %.3f, %.3f] m", final_pos[0], final_pos[1], final_pos[2])
|
|
246
|
+
|
|
247
|
+
# Standard logging
|
|
248
|
+
if args.csv:
|
|
249
|
+
out = np.column_stack([t, states, controls])
|
|
250
|
+
header = (
|
|
251
|
+
"t,"
|
|
252
|
+
"px,py,pz,vx,vy,vz,"
|
|
253
|
+
"qw,qx,qy,qz,"
|
|
254
|
+
"wx,wy,wz,"
|
|
255
|
+
"u1,u2,u3,u4"
|
|
256
|
+
)
|
|
257
|
+
np.savetxt(args.csv, out, delimiter=",", header=header, comments="")
|
|
258
|
+
log.info("saved data to %s", args.csv)
|
|
259
|
+
|
|
260
|
+
if args.json:
|
|
261
|
+
if args.academic_log:
|
|
262
|
+
log_result.save_json(args.json)
|
|
263
|
+
else:
|
|
264
|
+
sim_log = SimulationLog(duration=args.duration, dt=args.dt, method=args.method)
|
|
265
|
+
for i in range(len(t)):
|
|
266
|
+
state = QuadState.from_vector(states[i])
|
|
267
|
+
sim_log.add_entry(t[i], state, controls[i])
|
|
268
|
+
sim_log.save_json(args.json)
|
|
269
|
+
log.info("saved JSON log to %s", args.json)
|
|
270
|
+
|
|
271
|
+
if args.matlab:
|
|
272
|
+
try:
|
|
273
|
+
if args.academic_log:
|
|
274
|
+
log_result.save_matlab(args.matlab)
|
|
275
|
+
else:
|
|
276
|
+
sim_log = SimulationLog(duration=args.duration, dt=args.dt, method=args.method)
|
|
277
|
+
for i in range(len(t)):
|
|
278
|
+
state = QuadState.from_vector(states[i])
|
|
279
|
+
sim_log.add_entry(t[i], state, controls[i])
|
|
280
|
+
sim_log.save_matlab(args.matlab)
|
|
281
|
+
log.info("saved MATLAB log to %s", args.matlab)
|
|
282
|
+
except ImportError:
|
|
283
|
+
log.warning("scipy not available, cannot save MATLAB format")
|
|
284
|
+
|
|
285
|
+
# Enhanced plotting options
|
|
286
|
+
if args.plot:
|
|
287
|
+
plot_trajectory(t, states, controls)
|
|
288
|
+
|
|
289
|
+
if args.plot_errors and args.target_pos:
|
|
290
|
+
# Create target states for error plotting
|
|
291
|
+
targets = np.zeros_like(states)
|
|
292
|
+
targets[:, 0] = args.target_pos[0] # Target x position
|
|
293
|
+
targets[:, 1] = args.target_pos[1] # Target y position
|
|
294
|
+
targets[:, 2] = args.target_pos[2] # Target z position
|
|
295
|
+
plot_control_errors(t, states, targets)
|
|
296
|
+
|
|
297
|
+
if args.plot_comparison:
|
|
298
|
+
# For trajectory comparison, we'll run another simulation with different gains
|
|
299
|
+
if args.controller == "pid" and args.target_pos:
|
|
300
|
+
# Create second controller with different gains
|
|
301
|
+
controller2 = create_pid_position_controller(
|
|
302
|
+
target_pos=args.target_pos,
|
|
303
|
+
kp=(args.pid_kp[0] * 0.5, args.pid_kp[1] * 0.5, args.pid_kp[2] * 0.5),
|
|
304
|
+
ki=(args.pid_ki[0] * 0.5, args.pid_ki[1] * 0.5, args.pid_ki[2] * 0.5),
|
|
305
|
+
kd=(args.pid_kd[0] * 0.5, args.pid_kd[1] * 0.5, args.pid_kd[2] * 0.5)
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
# Run second simulation
|
|
309
|
+
t2, states2, controls2 = simulate(
|
|
310
|
+
duration=args.duration,
|
|
311
|
+
dt=args.dt,
|
|
312
|
+
controller=controller2,
|
|
313
|
+
initial_state=initial_state,
|
|
314
|
+
params=params,
|
|
315
|
+
method=args.method,
|
|
316
|
+
rtol=args.rtol,
|
|
317
|
+
atol=args.atol,
|
|
318
|
+
max_step=args.dt,
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
# Compare trajectories
|
|
322
|
+
trajectories = [
|
|
323
|
+
(states, f"Controller 1 (Kp={args.pid_kp}, Ki={args.pid_ki}, Kd={args.pid_kd})"),
|
|
324
|
+
(states2, f"Controller 2 (Kp={np.array(args.pid_kp)*0.5}, Ki={np.array(args.pid_ki)*0.5}, Kd={np.array(args.pid_kd)*0.5})")
|
|
325
|
+
]
|
|
326
|
+
plot_3d_trajectory_comparison(trajectories)
|
|
327
|
+
else:
|
|
328
|
+
log.warning("Trajectory comparison requires PID controller with target position")
|
|
329
|
+
|
|
330
|
+
# -------------------------------------------------------------------------
|
|
331
|
+
if __name__ == "__main__":
|
|
332
|
+
main()
|