drones-sim 0.2.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.
- drones_sim/__init__.py +15 -0
- drones_sim/control/__init__.py +15 -0
- drones_sim/control/allocation.py +76 -0
- drones_sim/control/cascaded.py +204 -0
- drones_sim/control/geometric.py +170 -0
- drones_sim/control/lqr.py +174 -0
- drones_sim/control/pid.py +50 -0
- drones_sim/dynamics/__init__.py +13 -0
- drones_sim/dynamics/config.py +117 -0
- drones_sim/dynamics/disturbances.py +262 -0
- drones_sim/dynamics/quadcopter.py +317 -0
- drones_sim/estimation/__init__.py +2 -0
- drones_sim/estimation/ahrs.py +79 -0
- drones_sim/estimation/ekf.py +594 -0
- drones_sim/logging/__init__.py +13 -0
- drones_sim/logging/csv_logger.py +76 -0
- drones_sim/logging/json_logger.py +53 -0
- drones_sim/math_utils.py +119 -0
- drones_sim/models/__init__.py +21 -0
- drones_sim/models/quadcopter.urdf +296 -0
- drones_sim/models/urdf_loader.py +444 -0
- drones_sim/rl/__init__.py +16 -0
- drones_sim/rl/actions.py +219 -0
- drones_sim/rl/env.py +195 -0
- drones_sim/rl/observations.py +40 -0
- drones_sim/rl/reward.py +69 -0
- drones_sim/rl/tasks.py +73 -0
- drones_sim/sensors/__init__.py +3 -0
- drones_sim/sensors/gps.py +158 -0
- drones_sim/sensors/imu.py +196 -0
- drones_sim/sensors/models.py +113 -0
- drones_sim/simulation.py +283 -0
- drones_sim/state.py +133 -0
- drones_sim/trajectory.py +391 -0
- drones_sim/visualization/__init__.py +23 -0
- drones_sim/visualization/api.py +50 -0
- drones_sim/visualization/dashboard.py +74 -0
- drones_sim/visualization/plots.py +183 -0
- drones_sim/visualization/rerun_viewer.py +411 -0
- drones_sim/visualization/viewer.py +452 -0
- drones_sim-0.2.0.dist-info/METADATA +323 -0
- drones_sim-0.2.0.dist-info/RECORD +45 -0
- drones_sim-0.2.0.dist-info/WHEEL +5 -0
- drones_sim-0.2.0.dist-info/licenses/LICENSE +21 -0
- drones_sim-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"""Nonlinear six-degree-of-freedom quadcopter rigid-body dynamics.
|
|
2
|
+
|
|
3
|
+
Frames use ENU world coordinates (z up) and a front-right-up body frame.
|
|
4
|
+
Quaternions are Hamilton ``[w, x, y, z]`` and rotate body vectors to world.
|
|
5
|
+
The numerical state remains a 13-vector for compatibility::
|
|
6
|
+
|
|
7
|
+
[position(3), velocity(3), quaternion(4), body_rates(3)]
|
|
8
|
+
|
|
9
|
+
The plant models bounded first-order actuators, arbitrary rigid-body rotation,
|
|
10
|
+
anisotropic linear/quadratic air drag, rotor failures, and external wrenches.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
from numpy.typing import NDArray
|
|
17
|
+
|
|
18
|
+
from ..math_utils import (
|
|
19
|
+
quat_derivative,
|
|
20
|
+
quat_from_euler,
|
|
21
|
+
quat_normalize,
|
|
22
|
+
quat_to_euler,
|
|
23
|
+
quat_to_rotation_matrix,
|
|
24
|
+
)
|
|
25
|
+
from ..state import VehicleState
|
|
26
|
+
from .config import QuadcopterConfig
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class QuadcopterDynamics:
|
|
30
|
+
"""A validated 13-state quaternion quadcopter model.
|
|
31
|
+
|
|
32
|
+
Legacy scalar constructor arguments are retained. New code should prefer a
|
|
33
|
+
:class:`QuadcopterConfig`, which makes units and actuator limits explicit.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
STATE_SIZE = 13
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
mass: float = 1.0,
|
|
41
|
+
arm_length: float = 0.2,
|
|
42
|
+
inertia: NDArray | None = None,
|
|
43
|
+
k_f: float = 1.0e-6,
|
|
44
|
+
k_m: float = 1.0e-7,
|
|
45
|
+
k_d: float | NDArray = 0.1,
|
|
46
|
+
g: float = 9.81,
|
|
47
|
+
motor_time_constant: float = 0.0,
|
|
48
|
+
disturbances: list | None = None,
|
|
49
|
+
*,
|
|
50
|
+
config: QuadcopterConfig | None = None,
|
|
51
|
+
quadratic_drag: float | NDArray = 0.0,
|
|
52
|
+
max_motor_speed: float = 4000.0,
|
|
53
|
+
) -> None:
|
|
54
|
+
cfg = config or QuadcopterConfig(
|
|
55
|
+
mass=mass,
|
|
56
|
+
arm_length=arm_length,
|
|
57
|
+
inertia=np.diag([0.01, 0.01, 0.018]) if inertia is None else inertia,
|
|
58
|
+
thrust_coefficient=k_f,
|
|
59
|
+
moment_coefficient=k_m,
|
|
60
|
+
linear_drag=k_d,
|
|
61
|
+
quadratic_drag=quadratic_drag,
|
|
62
|
+
gravity=g,
|
|
63
|
+
motor_time_constant=motor_time_constant,
|
|
64
|
+
max_motor_speed=max_motor_speed,
|
|
65
|
+
)
|
|
66
|
+
self.config = cfg
|
|
67
|
+
|
|
68
|
+
# Public aliases preserve the long-standing API used by examples/RL.
|
|
69
|
+
self.mass = cfg.mass
|
|
70
|
+
self.arm_length = cfg.arm_length
|
|
71
|
+
self.I = cfg.inertia.copy()
|
|
72
|
+
self.k_f = cfg.thrust_coefficient
|
|
73
|
+
self.k_m = cfg.moment_coefficient
|
|
74
|
+
self.k_d = float(cfg.linear_drag[0]) if np.allclose(
|
|
75
|
+
cfg.linear_drag, cfg.linear_drag[0]
|
|
76
|
+
) else cfg.linear_drag.copy()
|
|
77
|
+
self.g = cfg.gravity
|
|
78
|
+
self.motor_time_constant = cfg.motor_time_constant
|
|
79
|
+
self.min_motor_speed = cfg.min_motor_speed
|
|
80
|
+
self.max_motor_speed = cfg.max_motor_speed
|
|
81
|
+
self.disturbances = list(disturbances or [])
|
|
82
|
+
|
|
83
|
+
self.state = np.zeros(self.STATE_SIZE)
|
|
84
|
+
self.state[6] = 1.0
|
|
85
|
+
self.motor_states = np.zeros(4)
|
|
86
|
+
self.last_acceleration = np.zeros(3)
|
|
87
|
+
self.last_angular_acceleration = np.zeros(3)
|
|
88
|
+
self.last_wrench = np.zeros(4)
|
|
89
|
+
self._sim_time = 0.0
|
|
90
|
+
self._step_wind = np.zeros(3)
|
|
91
|
+
self._rotor_efficiencies = np.ones(4)
|
|
92
|
+
self._ground_effect_multiplier = 1.0
|
|
93
|
+
|
|
94
|
+
def reset(
|
|
95
|
+
self,
|
|
96
|
+
position: NDArray | None = None,
|
|
97
|
+
attitude: NDArray | None = None,
|
|
98
|
+
*,
|
|
99
|
+
state: VehicleState | None = None,
|
|
100
|
+
) -> None:
|
|
101
|
+
"""Reset the plant, actuators, clock, and disturbance episode state."""
|
|
102
|
+
if state is not None and (position is not None or attitude is not None):
|
|
103
|
+
raise ValueError("provide either state or position/attitude, not both")
|
|
104
|
+
if state is None:
|
|
105
|
+
self.state = np.zeros(self.STATE_SIZE)
|
|
106
|
+
self.state[6] = 1.0
|
|
107
|
+
if position is not None:
|
|
108
|
+
value = np.asarray(position, dtype=float)
|
|
109
|
+
if value.shape != (3,):
|
|
110
|
+
raise ValueError("position must have shape (3,)")
|
|
111
|
+
self.state[:3] = value
|
|
112
|
+
if attitude is not None:
|
|
113
|
+
value = np.asarray(attitude, dtype=float)
|
|
114
|
+
if value.shape != (3,):
|
|
115
|
+
raise ValueError("attitude must have shape (3,)")
|
|
116
|
+
self.state[6:10] = quat_from_euler(*value)
|
|
117
|
+
self.motor_states = np.zeros(4)
|
|
118
|
+
else:
|
|
119
|
+
self.state = state.as_vector()
|
|
120
|
+
self.motor_states = state.motor_speeds.copy()
|
|
121
|
+
self._sim_time = 0.0
|
|
122
|
+
self.last_acceleration.fill(0.0)
|
|
123
|
+
self.last_angular_acceleration.fill(0.0)
|
|
124
|
+
self.last_wrench.fill(0.0)
|
|
125
|
+
for disturbance in self.disturbances:
|
|
126
|
+
disturbance.reset()
|
|
127
|
+
disturbance.modify_dynamics(self, 0.0)
|
|
128
|
+
self._prepare_environment(0.0, 0.0)
|
|
129
|
+
|
|
130
|
+
# -- state accessors -------------------------------------------------
|
|
131
|
+
|
|
132
|
+
def get_state(self) -> VehicleState:
|
|
133
|
+
return VehicleState.from_vector(
|
|
134
|
+
self.state, motor_speeds=self.motor_states, time=self._sim_time
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
def get_position(self) -> NDArray:
|
|
138
|
+
return self.state[:3].copy()
|
|
139
|
+
|
|
140
|
+
def get_velocity(self) -> NDArray:
|
|
141
|
+
return self.state[3:6].copy()
|
|
142
|
+
|
|
143
|
+
def get_attitude(self) -> NDArray:
|
|
144
|
+
return quat_to_euler(self.state[6:10])
|
|
145
|
+
|
|
146
|
+
def get_quaternion(self) -> NDArray:
|
|
147
|
+
return self.state[6:10].copy()
|
|
148
|
+
|
|
149
|
+
def get_angular_velocity(self) -> NDArray:
|
|
150
|
+
return self.state[10:13].copy()
|
|
151
|
+
|
|
152
|
+
def get_motor_speeds(self) -> NDArray:
|
|
153
|
+
return self.motor_states.copy()
|
|
154
|
+
|
|
155
|
+
def rotation_matrix(self) -> NDArray:
|
|
156
|
+
return quat_to_rotation_matrix(self.state[6:10])
|
|
157
|
+
|
|
158
|
+
def specific_force_body(self) -> NDArray:
|
|
159
|
+
"""Ideal accelerometer specific force at the current state [m/s²]."""
|
|
160
|
+
gravity_up = np.array([0.0, 0.0, self.g])
|
|
161
|
+
return self.rotation_matrix().T @ (self.last_acceleration + gravity_up)
|
|
162
|
+
|
|
163
|
+
# -- forces and integration -----------------------------------------
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def _linear_drag(self) -> NDArray:
|
|
167
|
+
value = np.asarray(self.k_d, dtype=float)
|
|
168
|
+
return np.full(3, float(value)) if value.ndim == 0 else value
|
|
169
|
+
|
|
170
|
+
def _prepare_environment(self, t: float, dt: float) -> None:
|
|
171
|
+
self._step_wind = np.zeros(3)
|
|
172
|
+
self._rotor_efficiencies = np.ones(4)
|
|
173
|
+
self._ground_effect_multiplier = 1.0
|
|
174
|
+
for disturbance in self.disturbances:
|
|
175
|
+
advance = getattr(disturbance, "advance", None)
|
|
176
|
+
if advance is not None:
|
|
177
|
+
advance(t, dt, self.state)
|
|
178
|
+
wind_velocity = getattr(disturbance, "wind_velocity", None)
|
|
179
|
+
if wind_velocity is not None:
|
|
180
|
+
self._step_wind += np.asarray(wind_velocity(t), dtype=float)
|
|
181
|
+
rotor_efficiencies = getattr(disturbance, "rotor_efficiencies", None)
|
|
182
|
+
if rotor_efficiencies is not None:
|
|
183
|
+
self._rotor_efficiencies *= np.asarray(
|
|
184
|
+
rotor_efficiencies(t), dtype=float
|
|
185
|
+
)
|
|
186
|
+
thrust_multiplier = getattr(disturbance, "thrust_multiplier", None)
|
|
187
|
+
if thrust_multiplier is not None:
|
|
188
|
+
self._ground_effect_multiplier *= float(
|
|
189
|
+
thrust_multiplier(self.state[2])
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
def _wrench(self, motor_speeds: NDArray) -> NDArray:
|
|
193
|
+
allocation = self.allocation_matrix(self._rotor_efficiencies)
|
|
194
|
+
wrench = allocation @ np.square(motor_speeds)
|
|
195
|
+
wrench[0] *= self._ground_effect_multiplier
|
|
196
|
+
return wrench
|
|
197
|
+
|
|
198
|
+
def _derivatives(
|
|
199
|
+
self,
|
|
200
|
+
state: NDArray,
|
|
201
|
+
motor_speeds: NDArray,
|
|
202
|
+
*,
|
|
203
|
+
t: float | None = None,
|
|
204
|
+
dt: float = 0.01,
|
|
205
|
+
) -> NDArray:
|
|
206
|
+
wrench = self._wrench(motor_speeds)
|
|
207
|
+
thrust = wrench[0]
|
|
208
|
+
torque = wrench[1:4]
|
|
209
|
+
velocity = state[3:6]
|
|
210
|
+
quaternion = state[6:10]
|
|
211
|
+
body_rates = state[10:13]
|
|
212
|
+
rotation = quat_to_rotation_matrix(quaternion)
|
|
213
|
+
|
|
214
|
+
air_velocity_body = rotation.T @ (velocity - self._step_wind)
|
|
215
|
+
drag_body = (
|
|
216
|
+
-self._linear_drag * air_velocity_body
|
|
217
|
+
- self.config.quadratic_drag
|
|
218
|
+
* np.abs(air_velocity_body)
|
|
219
|
+
* air_velocity_body
|
|
220
|
+
)
|
|
221
|
+
thrust_world = rotation @ np.array([0.0, 0.0, thrust])
|
|
222
|
+
drag_world = rotation @ drag_body
|
|
223
|
+
gravity_world = np.array([0.0, 0.0, -self.mass * self.g])
|
|
224
|
+
total_force = thrust_world + drag_world + gravity_world
|
|
225
|
+
total_torque = torque.copy()
|
|
226
|
+
|
|
227
|
+
eval_time = self._sim_time if t is None else t
|
|
228
|
+
for disturbance in self.disturbances:
|
|
229
|
+
# Wind disturbances are already represented by relative airspeed.
|
|
230
|
+
if not hasattr(disturbance, "wind_velocity"):
|
|
231
|
+
total_force += disturbance.external_force(eval_time, dt, state)
|
|
232
|
+
total_torque += disturbance.external_torque(eval_time, dt, state)
|
|
233
|
+
|
|
234
|
+
acceleration = total_force / self.mass
|
|
235
|
+
angular_acceleration = np.linalg.solve(
|
|
236
|
+
self.I,
|
|
237
|
+
total_torque - np.cross(body_rates, self.I @ body_rates),
|
|
238
|
+
)
|
|
239
|
+
derivative = np.zeros(self.STATE_SIZE)
|
|
240
|
+
derivative[:3] = velocity
|
|
241
|
+
derivative[3:6] = acceleration
|
|
242
|
+
derivative[6:10] = quat_derivative(quaternion, body_rates)
|
|
243
|
+
derivative[10:13] = angular_acceleration
|
|
244
|
+
return derivative
|
|
245
|
+
|
|
246
|
+
def update(self, dt: float, motor_speeds: NDArray) -> NDArray:
|
|
247
|
+
"""Advance one fixed step with RK4 and an exact motor-lag update."""
|
|
248
|
+
if not np.isfinite(dt) or dt <= 0.0:
|
|
249
|
+
raise ValueError("dt must be a finite positive number")
|
|
250
|
+
commands = np.asarray(motor_speeds, dtype=float)
|
|
251
|
+
if commands.shape != (4,) or not np.all(np.isfinite(commands)):
|
|
252
|
+
raise ValueError("motor_speeds must be a finite shape-(4,) vector")
|
|
253
|
+
commands = np.clip(commands, self.min_motor_speed, self.max_motor_speed)
|
|
254
|
+
|
|
255
|
+
if self.motor_time_constant > 0.0:
|
|
256
|
+
alpha = 1.0 - np.exp(-dt / self.motor_time_constant)
|
|
257
|
+
self.motor_states += alpha * (commands - self.motor_states)
|
|
258
|
+
else:
|
|
259
|
+
self.motor_states = commands.copy()
|
|
260
|
+
self.motor_states = np.clip(
|
|
261
|
+
self.motor_states, self.min_motor_speed, self.max_motor_speed
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
for disturbance in self.disturbances:
|
|
265
|
+
disturbance.modify_dynamics(self, self._sim_time)
|
|
266
|
+
self._prepare_environment(self._sim_time, dt)
|
|
267
|
+
|
|
268
|
+
t0 = self._sim_time
|
|
269
|
+
motors = self.motor_states
|
|
270
|
+
k1 = self._derivatives(self.state, motors, t=t0, dt=dt)
|
|
271
|
+
k2 = self._derivatives(
|
|
272
|
+
self.state + 0.5 * dt * k1, motors, t=t0 + 0.5 * dt, dt=dt
|
|
273
|
+
)
|
|
274
|
+
k3 = self._derivatives(
|
|
275
|
+
self.state + 0.5 * dt * k2, motors, t=t0 + 0.5 * dt, dt=dt
|
|
276
|
+
)
|
|
277
|
+
k4 = self._derivatives(self.state + dt * k3, motors, t=t0 + dt, dt=dt)
|
|
278
|
+
self.state += dt / 6.0 * (k1 + 2.0 * k2 + 2.0 * k3 + k4)
|
|
279
|
+
self.state[6:10] = quat_normalize(self.state[6:10])
|
|
280
|
+
self._sim_time += dt
|
|
281
|
+
|
|
282
|
+
final_derivative = self._derivatives(
|
|
283
|
+
self.state, motors, t=self._sim_time, dt=dt
|
|
284
|
+
)
|
|
285
|
+
self.last_acceleration = final_derivative[3:6].copy()
|
|
286
|
+
self.last_angular_acceleration = final_derivative[10:13].copy()
|
|
287
|
+
wrench = self._wrench(motors)
|
|
288
|
+
self.last_wrench = wrench.copy()
|
|
289
|
+
if not np.all(np.isfinite(self.state)):
|
|
290
|
+
raise FloatingPointError(
|
|
291
|
+
f"quadcopter state became non-finite at t={self._sim_time:.6f}s"
|
|
292
|
+
)
|
|
293
|
+
return self.state.copy()
|
|
294
|
+
|
|
295
|
+
# -- actuator model --------------------------------------------------
|
|
296
|
+
|
|
297
|
+
def allocation_matrix(
|
|
298
|
+
self, efficiencies: NDArray | None = None
|
|
299
|
+
) -> NDArray:
|
|
300
|
+
"""Map squared motor speeds to collective thrust and body torque."""
|
|
301
|
+
positions = self.config.rotor_positions
|
|
302
|
+
efficiency = (
|
|
303
|
+
np.ones(4)
|
|
304
|
+
if efficiencies is None
|
|
305
|
+
else np.asarray(efficiencies, dtype=float)
|
|
306
|
+
)
|
|
307
|
+
if efficiency.shape != (4,):
|
|
308
|
+
raise ValueError("efficiencies must have shape (4,)")
|
|
309
|
+
force_gain = self.k_f * efficiency
|
|
310
|
+
return np.vstack(
|
|
311
|
+
[
|
|
312
|
+
force_gain,
|
|
313
|
+
positions[:, 1] * force_gain,
|
|
314
|
+
-positions[:, 0] * force_gain,
|
|
315
|
+
self.config.rotor_directions * self.k_m * efficiency,
|
|
316
|
+
]
|
|
317
|
+
)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Attitude and Heading Reference System (AHRS) using complementary filtering.
|
|
2
|
+
|
|
3
|
+
Fuses accelerometer, gyroscope, and magnetometer data for attitude estimation.
|
|
4
|
+
Consolidated from imu_ekf_fusion_enhanced.py.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
from numpy.typing import NDArray
|
|
11
|
+
|
|
12
|
+
from ..math_utils import quat_normalize, quat_to_rotation_matrix
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AHRS:
|
|
16
|
+
"""Complementary-filter AHRS with gyro bias learning.
|
|
17
|
+
|
|
18
|
+
Uses accelerometer and magnetometer error feedback to correct
|
|
19
|
+
gyroscope-integrated orientation.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
dt: float,
|
|
25
|
+
accel_weight: float = 0.02,
|
|
26
|
+
mag_weight: float = 0.01,
|
|
27
|
+
bias_learn_rate: float = 0.001,
|
|
28
|
+
gravity: NDArray | None = None,
|
|
29
|
+
mag_ref: NDArray | None = None,
|
|
30
|
+
):
|
|
31
|
+
self.dt = dt
|
|
32
|
+
self.accel_weight = accel_weight
|
|
33
|
+
self.mag_weight = mag_weight
|
|
34
|
+
self.bias_learn_rate = bias_learn_rate
|
|
35
|
+
|
|
36
|
+
self.gravity = gravity if gravity is not None else np.array([0.0, 0.0, 9.81])
|
|
37
|
+
self.mag_ref = mag_ref if mag_ref is not None else np.array([25.0, 5.0, -40.0])
|
|
38
|
+
|
|
39
|
+
self.quaternion = np.array([1.0, 0.0, 0.0, 0.0])
|
|
40
|
+
self.gyro_bias = np.zeros(3)
|
|
41
|
+
|
|
42
|
+
def _normalize(self, v: NDArray) -> NDArray:
|
|
43
|
+
n = np.linalg.norm(v)
|
|
44
|
+
return v / n if n > 0 else v
|
|
45
|
+
|
|
46
|
+
def update(self, gyro: NDArray, accel: NDArray, mag: NDArray) -> tuple[NDArray, NDArray]:
|
|
47
|
+
"""Process one time-step of sensor data.
|
|
48
|
+
|
|
49
|
+
Returns (quaternion, gyro_bias).
|
|
50
|
+
"""
|
|
51
|
+
gyro_corrected = gyro - self.gyro_bias
|
|
52
|
+
|
|
53
|
+
accel_norm = self._normalize(accel)
|
|
54
|
+
mag_norm = self._normalize(mag)
|
|
55
|
+
|
|
56
|
+
R = quat_to_rotation_matrix(self.quaternion)
|
|
57
|
+
|
|
58
|
+
expected_gravity = R.T @ self._normalize(self.gravity)
|
|
59
|
+
expected_mag = R.T @ self._normalize(self.mag_ref)
|
|
60
|
+
|
|
61
|
+
accel_error = np.cross(accel_norm, expected_gravity)
|
|
62
|
+
mag_error = np.cross(mag_norm, expected_mag)
|
|
63
|
+
|
|
64
|
+
error = accel_error * self.accel_weight + mag_error * self.mag_weight
|
|
65
|
+
self.gyro_bias += error * self.bias_learn_rate
|
|
66
|
+
gyro_corrected = gyro_corrected + error
|
|
67
|
+
|
|
68
|
+
w, x, y, z = self.quaternion
|
|
69
|
+
wx, wy, wz = gyro_corrected
|
|
70
|
+
|
|
71
|
+
q_dot = 0.5 * np.array([
|
|
72
|
+
-x * wx - y * wy - z * wz,
|
|
73
|
+
w * wx + y * wz - z * wy,
|
|
74
|
+
w * wy + z * wx - x * wz,
|
|
75
|
+
w * wz + x * wy - y * wx,
|
|
76
|
+
])
|
|
77
|
+
|
|
78
|
+
self.quaternion = quat_normalize(self.quaternion + q_dot * self.dt)
|
|
79
|
+
return self.quaternion.copy(), self.gyro_bias.copy()
|