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 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()