Flight¶
simweave.flight simulates multirotor flight dynamics: a quadrotor model, a
cascaded position/attitude controller, reference trajectories and missions
(take off, figure-8, land), wind and fault injection. It needs only numpy. See
FLIGHT_DESIGN.md in the repository for the full plan.
Fly a mission¶
from simweave.flight import Mission, Quadrotor, ConstantWind, fly
mission = Mission.figure8(height=3.0, a=4.0, b=2.0, period=20.0)
flight = fly(Quadrotor(wind=ConstantWind((4.0, 0.0, 0.0))), mission)
print(flight.metrics()) # rms_error, max_error, energy_wh, touchdown_speed, ...
print(flight.phase_rms()) # tracking error per phase
data = flight.to_dict() # time, x, y, z, roll, pitch, yaw, ... (Z up, radians)
viewer = flight.to_viewer() # same, mapped for a Y-up 3D viewer
Mission.figure8 chains take off, hover, a quintic blend into the figure-8,
the figure-8 laps, a blend out, hover and a soft landing. Position, velocity
and acceleration are continuous through every join. Build your own from
Hold, Quintic (minimum-jerk move between two RefPoint states) and
Figure8, or pass any callable ref(t) -> RefPoint.
fly uses a CascadedController: position P, velocity PI, thrust-vector and
attitude (SO(3) error), body-rate PI, then the mixer. It models the vehicle
with vehicle.params, so changing vehicle.mass afterwards (a payload) or a
motor_eff_* value tests it against model mismatch; the integral terms absorb
it. Tune with ControllerGains. The motors switch off once the reference and
the vehicle are on the ground.
Run demos/27_quadcopter_figure8.py for a complete example.
Plots¶
from simweave.viz import plot_trajectory_3d, plot_flight_summary
plot_trajectory_3d(flight).show() # flown path coloured by speed, plus the reference
plot_flight_summary(flight).show() # error, tilt, rotor thrusts and power, phases shaded
plot_landing_dispersion(points) scatters (x, y) landing points with circles
containing 50% and 95% of them. Plotly is imported lazily, so the rest of
simweave.flight works without the viz extra.
Monte Carlo and faults¶
fly is a plain function of a vehicle and a mission, so it drops straight into
run_monte_carlo. Demo 28 randomises steady wind, seeded turbulence and an
unknown payload and summarises tracking error, touchdown speed and landing
dispersion. Define the scenario at module level so the process pool can pickle it.
fly also accepts a FaultInjector wrapping the quadrotor. Demo 29 degrades
one rotor during the figure-8: the integral terms push that rotor harder to
compensate until it reaches its thrust limit, which for the default vehicle is
a loss of about 58% (1 - hover_thrust / max_thrust). Past that, the vehicle
can no longer hold altitude and attitude together.
Conventions¶
- World frame is Z up; body frame is x forward, y left, z up.
- Euler angles are ZYX. Positive roll banks right, positive pitch is nose down, positive yaw is counter-clockwise seen from above.
- Rotors are numbered 1 front-right, 2 front-left, 3 rear-left, 4 rear-right.
Quick example¶
import numpy as np
from simweave.continuous import simulate
from simweave.flight import Quadrotor, Mixer
quad = Quadrotor(hover=True, position=(0, 0, 5))
mixer = Mixer(quad.params)
class Hover:
def update(self, t, state, dt):
return mixer.allocate(quad.params.weight, (0.0, 0.0, 0.0))
result = simulate(quad, (0, 5), dt=0.005, controller=Hover(), control_dt=0.01)
print(result.state[-1, :3]) # still at (0, 0, 5)
The quadrotor has 16 states (x y z vx vy vz roll pitch yaw p q r f1 f2 f3 f4,
where f1..f4 are the actual rotor thrusts, which follow the commanded
thrusts with time constant motor_tau). Its input is the four commanded rotor
thrusts. Mixer.allocate(collective, torques) converts a collective thrust and
body torques into rotor thrusts within limits, giving attitude torques priority
over collective thrust when the request does not fit.
Disturbances and faults¶
wind=takesConstantWind,GustWind(smooth "1 - cos" gust) orTurbulentWind(seeded Gauss-Markov turbulence). Drag acts on the velocity relative to the air.motor_eff_1..motor_eff_4scale each rotor's thrust, so aFaultInjectorwith aParameterFaultcan degrade a single rotor.Quadrotor.power(thrusts)gives rotor power by momentum theory, for energy estimates.
Ground contact¶
The floor at z = 0 stops downward motion. While the rotors cannot lift the
vehicle it stays upright and still, so a landed vehicle does not tumble.
API¶
Flight dynamics: quadrotor physics, control allocation and wind models.
Importing this package needs only numpy. Conventions (Z-up world, body x
forward / y left / z up, ZYX Euler angles) are described in
:mod:simweave.flight.frames.
CascadedController
¶
CascadedController(reference: Callable[[float], RefPoint], params: QuadrotorParams | None = None, gains: ControllerGains | None = None, disarm_height: float = 0.03)
Tracks a reference ref(t) -> RefPoint (a trajectory or a mission).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
reference
|
Callable[[float], RefPoint]
|
Callable returning the desired :class: |
required |
params
|
QuadrotorParams | None
|
The controller's model of the vehicle. |
None
|
gains
|
ControllerGains | None
|
:class: |
None
|
disarm_height
|
float
|
When both the reference and the vehicle are at or below this height the motors are switched off (as a real flight controller does after landing) and the integrators are reset. |
0.03
|
Source code in src/simweave/flight/control.py
ControllerGains
dataclass
¶
ControllerGains(kp_pos_xy: float = 1.5, kp_pos_z: float = 2.0, kp_vel_xy: float = 3.5, ki_vel_xy: float = 0.8, kp_vel_z: float = 5.0, ki_vel_z: float = 1.5, kp_att: float = 8.0, kp_yaw: float = 3.0, kp_rate_rp: float = 20.0, kp_rate_yaw: float = 10.0, ki_rate: float = 2.0, max_tilt: float = radians(35.0), max_speed_xy: float = 8.0, max_speed_up: float = 3.0, max_speed_down: float = 2.0, max_accel_xy: float = 6.0, max_accel_up: float = 6.0, max_accel_down: float = 5.0, max_angular_accel: float = 300.0)
Gains and limits for :class:CascadedController.
Rate-loop gains act on angular acceleration (rad/s^2 per rad/s), so they do not need rescaling when the inertia changes.
Mixer
¶
Mixer(params: QuadrotorParams | None = None)
Maps collective thrust and body torques to four rotor thrusts.
wrench = A @ thrusts with wrench = [T, tau_x, tau_y, tau_z].
Source code in src/simweave/flight/mixer.py
wrench
¶
thrusts
¶
Unconstrained rotor thrusts for a requested wrench.
allocate
¶
Rotor thrusts within [0, max_thrust] for a requested wrench.
When the request does not fit, attitude torques take priority over collective thrust: the collective is moved first (keeping the torques exact), and only if the torques alone cannot be produced are they scaled down uniformly.
Source code in src/simweave/flight/mixer.py
QuadrotorParams
dataclass
¶
QuadrotorParams(mass: float = 1.2, inertia: tuple[float, float, float] = (0.012, 0.012, 0.022), arm_length: float = 0.17, max_thrust: float = 7.0, torque_ratio: float = 0.016, motor_tau: float = 0.03, drag_lin: float = 0.1, drag_quad: float = 0.0, gravity: float = 9.81, air_density: float = 1.225, rotor_radius: float = 0.12, figure_of_merit: float = 0.65)
Rigid-body and propulsion parameters (SI units; unit objects accepted).
The defaults describe a roughly 1.2 kg sport quadrotor with a thrust-to-weight ratio of about 2.4. They are plausible, not a model of any particular airframe.
Attributes:
| Name | Type | Description |
|---|---|---|
mass |
float
|
Total mass, kg. |
inertia |
tuple[float, float, float]
|
Principal moments |
arm_length |
float
|
Distance from the centre of mass to each rotor, m. Rotors sit at 45 degrees to the body axes ("X" layout). |
max_thrust |
float
|
Maximum thrust of one rotor, N. |
torque_ratio |
float
|
Rotor reaction torque per unit thrust (yaw), m. |
motor_tau |
float
|
First-order rotor response time constant, s. Must be positive. |
drag_lin, drag_quad |
Linear (N s/m) and quadratic (N s^2/m^2) drag coefficients on the velocity relative to the air. |
|
gravity |
float
|
Gravitational acceleration, m/s^2. |
air_density |
float
|
Air density, kg/m^3 (used by the energy model). |
rotor_radius |
float
|
Propeller radius, m (used by the energy model). |
figure_of_merit |
float
|
Hover efficiency of the rotors (ideal power / actual power). |
Quadrotor
¶
Quadrotor(params: QuadrotorParams | None = None, position=(0.0, 0.0, 0.0), yaw: float = 0.0, hover: bool = False, wind=None, motor_eff=(1.0, 1.0, 1.0, 1.0))
Bases: DynamicSystem
Quadrotor in "X" layout. See the module docstring for the state layout.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
params
|
QuadrotorParams | None
|
Physical parameters; defaults to :class: |
None
|
position
|
Initial position (m) and heading (rad). |
(0.0, 0.0, 0.0)
|
|
yaw
|
Initial position (m) and heading (rad). |
(0.0, 0.0, 0.0)
|
|
hover
|
bool
|
If true, start with all rotors already producing hover thrust; otherwise the rotors start at zero thrust (vehicle on the ground). |
False
|
wind
|
Optional :class: |
None
|
|
motor_eff
|
Per-rotor thrust effectiveness (1.0 = healthy). These are exposed as
the scalar attributes |
(1.0, 1.0, 1.0, 1.0)
|
Notes
The commanded rotor thrusts are supplied through simulate(...,
controller=...) or inputs=; with no input the rotors are commanded
to zero thrust.
Source code in src/simweave/flight/quadrotor.py
hover_state
¶
State at rest with every rotor already producing hover thrust.
Source code in src/simweave/flight/quadrotor.py
post_step
¶
Ground contact: the floor at z = 0 stops downward motion, and a
vehicle that cannot lift off stays upright and still.
Source code in src/simweave/flight/quadrotor.py
power
¶
Electrical rotor power (W) for rotor thrusts, by momentum theory:
P = f^1.5 / (sqrt(2 rho A) * figure_of_merit) summed over rotors.
Accepts (..., 4) and returns the total over the last axis.
Source code in src/simweave/flight/quadrotor.py
FlightResult
dataclass
¶
A simulated flight plus the reference it was flying.
reference is the callable the controller tracked (a
:class:~simweave.flight.trajectory.Mission, usually).
to_dict
¶
Columns time, x, y, z, roll, pitch, yaw first (angles in
radians, Z up), then velocity, body rates and rotor thrusts.
Source code in src/simweave/flight/results.py
to_frame
¶
to_viewer
¶
Columns for the Y-up 3D viewer (model faces +Z, Euler order YXZ).
Viewer (X, Y, Z) = (y, z, x) of this package's Z-up frame, which
keeps the frame right-handed. The three angles keep their values:
roll about the forward axis, pitch (positive nose down) about the
left axis, yaw about the up axis. Angles are radians.
Source code in src/simweave/flight/results.py
tracking_error
¶
Distance (m) between the vehicle and the reference at each sample.
tilt
¶
Angle (rad) between the body z axis and vertical at each sample.
power
¶
energy_wh
¶
saturation_fraction
¶
Fraction of airborne control steps with any rotor command at a limit.
Source code in src/simweave/flight/results.py
touchdown
¶
Time, vertical speed, horizontal position and miss distance at touchdown.
Touchdown is the first sample at or below 2 cm after the vehicle has
been above 0.5 m; None if it never lands.
Source code in src/simweave/flight/results.py
metrics
¶
Headline numbers for a flight.
Source code in src/simweave/flight/results.py
phase_rms
¶
RMS tracking error per mission phase (needs a :class:Mission).
Source code in src/simweave/flight/results.py
Figure8
¶
Figure8(center=(0.0, 0.0, 3.0), a: float = 4.0, b: float = 2.0, period: float = 20.0, laps: float = 1.0, z_amp: float = 0.0, yaw_mode: str = 'fixed', yaw: float = 0.0, name: str = 'figure8')
A figure-of-eight (lemniscate) flown at constant altitude.
x = a sin(w t), y = b sin(2 w t) about center with
w = 2 pi / period; the path starts at the crossing point moving
diagonally. An optional vertical oscillation z_amp sin(w t) makes one
lobe climb and the other dip.
yaw_mode is "fixed" (hold yaw) or "heading" (point along the
direction of travel).
Source code in src/simweave/flight/trajectory.py
Hold
¶
Stay at a point (zero velocity and acceleration) for duration s.
Source code in src/simweave/flight/trajectory.py
Mission
¶
A chain of segments flown back to back.
mission(t) returns the :class:RefPoint at time t (held at the
first/last point outside the mission). phase(t) names the active
segment. Use :meth:figure8 for the standard take off, fly a figure-8,
land sequence.
Source code in src/simweave/flight/trajectory.py
figure8
classmethod
¶
figure8(height: float = 3.0, a: float = 4.0, b: float = 2.0, period: float = 20.0, laps: float = 1.0, center=(0.0, 0.0), start=(0.0, 0.0), yaw_mode: str = 'fixed', z_amp: float = 0.0, climb_time: float = 4.0, settle_time: float = 2.0, blend_time: float = 3.0, hover_time: float = 2.0, land_time: float = 6.0, land_speed: float = 0.3, ground_time: float = 2.0) -> 'Mission'
Take off at start, fly laps figure-8s about center, land.
The transitions into and out of the figure-8 are quintic blends, so
position, velocity and acceleration are continuous throughout.
land_speed is the descent rate at touchdown.
Source code in src/simweave/flight/trajectory.py
Quintic
¶
Minimum-jerk move between two :class:RefPoint states.
Each axis follows the quintic polynomial that meets the position,
velocity and acceleration of start and end after duration
seconds. Heading changes along a smooth quintic in the shortest direction.
Source code in src/simweave/flight/trajectory.py
RefPoint
¶
Bases: NamedTuple
Desired state at one instant (yaw_rate is optional feed-forward).
ConstantWind
¶
GustWind
¶
A steady wind plus one smooth "1 - cos" gust.
The gust adds amplitude * 0.5 * (1 - cos(2 pi (t - start) / duration))
for start <= t <= start + duration.
Source code in src/simweave/flight/wind.py
TurbulentWind
¶
TurbulentWind(sigma, correlation_time: float, duration: float, mean=(0.0, 0.0, 0.0), dt: float = 0.01, rng: Generator | None = None)
Steady wind plus first-order Gauss-Markov turbulence.
Each axis is an Ornstein-Uhlenbeck process with standard deviation
sigma (scalar or per-axis) and correlation time correlation_time.
The realisation is sampled once on a grid of spacing dt covering
duration seconds and linearly interpolated, so repeated evaluations at
the same time agree. After duration the last value is held.
Source code in src/simweave/flight/wind.py
Wind
¶
Bases: Protocol
Velocity of the air (m/s, world frame) at time t and position.
euler_from_rotation
¶
(roll, pitch, yaw) from a body-to-world rotation matrix.
Pitch is returned in [-pi/2, pi/2]. At the gimbal-lock singularity
(pitch = +/-90 degrees) yaw is set to zero and roll absorbs the rotation.
Source code in src/simweave/flight/frames.py
euler_to_quaternion
¶
Unit quaternion [w, x, y, z] for ZYX Euler angles.
Source code in src/simweave/flight/frames.py
quaternion_to_euler
¶
ZYX Euler angles (roll, pitch, yaw) from a quaternion [w, x, y, z].
Source code in src/simweave/flight/frames.py
rotation_matrix
¶
Body-to-world rotation matrix for ZYX Euler angles (radians).
Source code in src/simweave/flight/frames.py
wrap_angle
¶
fly
¶
fly(vehicle: Quadrotor, mission: Callable[[float], RefPoint], *, gains: ControllerGains | None = None, dt: float = 0.004, control_dt: float = 0.008, t_end: float | None = None, method: str = 'rk4') -> FlightResult
Fly vehicle along mission with a :class:CascadedController.
The controller models the vehicle with vehicle.params, so changing
vehicle.mass (a payload) or motor effectiveness afterwards tests the
controller against model mismatch. vehicle may also be a wrapper with a
.system attribute, such as a :class:~simweave.faults.FaultInjector.
t_end defaults to the mission duration.