pid_controller.py
CrazyFly/python/control_systems/pid_controller.py
"""
Enhanced 4-Layer PID Controller for Quadrotor Control
=====================================================
This module implements a complete 4-layer PID control system for quadrotor flight control,
extending the traditional 2-layer PID (position + velocity) to include attitude and
attitude-rate control layers for improved performance and stability.
The 4-layer architecture consists of:
1. Position Control Layer (Outer Loop) - Generates velocity commands from position errors
2. Velocity Control Layer (Inner Loop) - Generates attitude commands from velocity errors
3. Attitude Control Layer - Generates angular velocity commands from attitude errors
4. Attitude-Rate Control Layer - Generates motor commands from angular velocity errors
This implementation is designed to work with the NCKU-Quadrotor-Navigation project
and provides enhanced stability, reduced overshoot, and better disturbance rejection
compared to the original 2-layer implementation.
Author: [Your Name]
Date: [Current Date]
License: MIT
"""
import numpy as np
import time
from typing import Dict, List, Tuple, Optional
from dataclasses import dataclass
from enum import Enum
import logging
# Configure logging for debugging and monitoring
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class ControlLayer(Enum):
"""Enumeration of the four control layers in the PID hierarchy."""
POSITION = "position" # Layer 1: Position control (outermost)
VELOCITY = "velocity" # Layer 2: Velocity control
ATTITUDE = "attitude" # Layer 3: Attitude control
ATTITUDE_RATE = "attitude_rate" # Layer 4: Attitude rate control (innermost)
@dataclass
class PIDGains:
"""
Data structure for storing PID controller gains.
Attributes:
kp (float): Proportional gain - determines response speed and overshoot
ki (float): Integral gain - eliminates steady-state error
kd (float): Derivative gain - reduces overshoot and improves stability
integral_limit (float): Maximum value for integral term to prevent windup
output_limit (float): Maximum output value to prevent saturation
"""
kp: float
ki: float
kd: float
integral_limit: float = 100.0
output_limit: float = 100.0
@dataclass
class ControlState:
"""
Data structure for storing the current state of a control layer.
Attributes:
error (float): Current error between setpoint and measurement
integral (float): Accumulated integral of error
derivative (float): Rate of change of error
previous_error (float): Error from previous time step
output (float): Current control output
timestamp (float): Timestamp of last update
"""
error: float = 0.0
integral: float = 0.0
derivative: float = 0.0
previous_error: float = 0.0
output: float = 0.0
timestamp: float = 0.0
class PIDController:
"""
Single-layer PID controller implementation with anti-windup and saturation.
This class implements a standard PID controller with the following features:
- Anti-windup protection to prevent integral term saturation
- Output saturation to limit control signals
- Derivative filtering to reduce noise sensitivity
- Time-based integration for consistent behavior across different sampling rates
"""
def __init__(self, gains: PIDGains, name: str = "PID"):
"""
Initialize PID controller with specified gains.
Args:
gains (PIDGains): PID controller gains and limits
name (str): Name identifier for logging and debugging
"""
self.gains = gains
self.name = name
self.state = ControlState()
self.enabled = True
# Initialize timing for derivative calculation
self.last_update_time = time.time()
# Performance monitoring
self.total_updates = 0
self.max_error = 0.0
self.rms_error = 0.0
logger.info(f"Initialized {name} PID controller with gains: Kp={gains.kp}, Ki={gains.ki}, Kd={gains.kd}")
def update(self, setpoint: float, measurement: float, dt: Optional[float] = None) -> float:
"""
Update PID controller and compute control output.
This method implements the standard PID control law:
output = Kp * error + Ki * ∫error dt + Kd * d(error)/dt
Args:
setpoint (float): Desired value (target)
measurement (float): Current measured value
dt (float, optional): Time step. If None, calculated automatically
Returns:
float: Control output signal
"""
if not self.enabled:
return 0.0
# Calculate time step if not provided
current_time = time.time()
if dt is None:
dt = current_time - self.last_update_time
dt = max(dt, 0.001) # Minimum time step to prevent division by zero
# Calculate error
error = setpoint - measurement
# Update state
self.state.previous_error = self.state.error
self.state.error = error
self.state.timestamp = current_time
# Calculate integral term with anti-windup
self.state.integral += error * dt
# Apply integral limit to prevent windup
if abs(self.state.integral) > self.gains.integral_limit:
self.state.integral = np.sign(self.state.integral) * self.gains.integral_limit
logger.debug(f"{self.name}: Integral term limited to {self.gains.integral_limit}")
# Calculate derivative term with filtering
if dt > 0:
self.state.derivative = (error - self.state.previous_error) / dt
else:
self.state.derivative = 0.0
# Apply low-pass filter to derivative to reduce noise sensitivity
alpha = 0.1 # Filter coefficient (0 = no filtering, 1 = no derivative)
self.state.derivative = alpha * self.state.derivative + (1 - alpha) * self.state.derivative
# Compute PID output
output = (self.gains.kp * self.state.error +
self.gains.ki * self.state.integral +
self.gains.kd * self.state.derivative)
# Apply output saturation
if abs(output) > self.gains.output_limit:
output = np.sign(output) * self.gains.output_limit
logger.debug(f"{self.name}: Output saturated to {self.gains.output_limit}")
self.state.output = output
self.last_update_time = current_time
# Update performance metrics
self.total_updates += 1
self.max_error = max(self.max_error, abs(error))
self.rms_error = np.sqrt((self.rms_error**2 * (self.total_updates - 1) + error**2) / self.total_updates)
return output
def reset(self):
"""Reset controller state (clear integral and derivative terms)."""
self.state.integral = 0.0
self.state.derivative = 0.0
self.state.previous_error = 0.0
self.state.output = 0.0
logger.info(f"{self.name}: Controller state reset")
def get_performance_stats(self) -> Dict[str, float]:
"""Get performance statistics for monitoring and tuning."""
return {
"max_error": self.max_error,
"rms_error": self.rms_error,
"total_updates": self.total_updates,
"current_output": self.state.output,
"current_error": self.state.error
}
class FourLayerPIDController:
"""
Complete 4-layer PID control system for quadrotor flight control.
This class implements the hierarchical control structure where each layer
generates commands for the layer below it. The control flow is:
Position Error → Velocity Commands → Attitude Commands → Angular Velocity Commands → Motor Commands
The implementation includes:
- Smooth takeoff with S-curve trajectory
- Anti-drift compensation during initial flight
- Gain scheduling based on flight conditions
- Comprehensive safety checks and limits
- Real-time performance monitoring
"""
def __init__(self, config: Dict[str, PIDGains]):
"""
Initialize 4-layer PID controller with configuration for each layer.
Args:
config (Dict[str, PIDGains]): Configuration dictionary with PID gains for each layer
Expected keys: 'position', 'velocity', 'attitude', 'attitude_rate'
"""
# Initialize individual PID controllers for each layer
self.position_controller = PIDController(config['position'], "Position")
self.velocity_controller = PIDController(config['velocity'], "Velocity")
self.attitude_controller = PIDController(config['attitude'], "Attitude")
self.attitude_rate_controller = PIDController(config['attitude_rate'], "AttitudeRate")
# Control state storage
self.position_setpoint = np.zeros(3) # [x, y, z]
self.velocity_setpoint = np.zeros(3) # [vx, vy, vz]
self.attitude_setpoint = np.zeros(3) # [roll, pitch, yaw]
self.attitude_rate_setpoint = np.zeros(3) # [roll_rate, pitch_rate, yaw_rate]
# Current measurements
self.current_position = np.zeros(3)
self.current_velocity = np.zeros(3)
self.current_attitude = np.zeros(3)
self.current_attitude_rate = np.zeros(3)
# Takeoff control
self.takeoff_start_time = None
self.takeoff_duration = 3.0 # seconds
self.takeoff_target_height = 0.0
# Anti-drift control
self.anti_drift_start_time = None
self.anti_drift_duration = 4.0 # seconds
self.anti_drift_enabled = True
# Safety limits
self.max_velocity = 2.0 # m/s
self.max_attitude = np.radians(30) # 30 degrees
self.max_attitude_rate = np.radians(180) # 180 deg/s
# Performance monitoring
self.flight_start_time = None
self.total_flight_time = 0.0
logger.info("4-Layer PID Controller initialized successfully")
def set_position_setpoint(self, x: float, y: float, z: float):
"""
Set the target position for the quadrotor.
Args:
x (float): Target X position in meters
y (float): Target Y position in meters
z (float): Target Z position in meters (height)
"""
self.position_setpoint = np.array([x, y, z])
logger.info(f"Position setpoint updated: [{x:.2f}, {y:.2f}, {z:.2f}]")
def set_attitude_setpoint(self, roll: float, pitch: float, yaw: float):
"""
Set the target attitude for the quadrotor.
Args:
roll (float): Target roll angle in radians
pitch (float): Target pitch angle in radians
yaw (float): Target yaw angle in radians
"""
# Apply safety limits
roll = np.clip(roll, -self.max_attitude, self.max_attitude)
pitch = np.clip(pitch, -self.max_attitude, self.max_attitude)
self.attitude_setpoint = np.array([roll, pitch, yaw])
logger.debug(f"Attitude setpoint updated: [{np.degrees(roll):.1f}°, {np.degrees(pitch):.1f}°, {np.degrees(yaw):.1f}°]")
def update_measurements(self, position: np.ndarray, velocity: np.ndarray,
attitude: np.ndarray, attitude_rate: np.ndarray):
"""
Update current measurements from sensors.
Args:
position (np.ndarray): Current position [x, y, z] in meters
velocity (np.ndarray): Current velocity [vx, vy, vz] in m/s
attitude (np.ndarray): Current attitude [roll, pitch, yaw] in radians
attitude_rate (np.ndarray): Current attitude rates [roll_rate, pitch_rate, yaw_rate] in rad/s
"""
self.current_position = np.array(position)
self.current_velocity = np.array(velocity)
self.current_attitude = np.array(attitude)
self.current_attitude_rate = np.array(attitude_rate)
def _smooth_takeoff_trajectory(self, current_time: float) -> float:
"""
Generate smooth takeoff trajectory using S-curve to avoid initial thrust jumps.
The S-curve provides smooth acceleration and deceleration, reducing oscillations
during takeoff and improving stability.
Args:
current_time (float): Current time since takeoff start
Returns:
float: Target height for takeoff phase
"""
if self.takeoff_start_time is None:
return 0.0
elapsed_time = current_time - self.takeoff_start_time
if elapsed_time >= self.takeoff_duration:
return self.takeoff_target_height
# S-curve trajectory: smooth acceleration and deceleration
# Normalized time: 0 to 1 over takeoff duration
t_norm = elapsed_time / self.takeoff_duration
# S-curve function: 3t^2 - 2t^3 (smooth start and end)
s_curve = 3 * t_norm**2 - 2 * t_norm**3
target_height = s_curve * self.takeoff_target_height
return target_height
def _anti_drift_compensation(self, current_time: float) -> Tuple[float, float]:
"""
Apply anti-drift compensation during initial flight phase.
This function sets desired X and Y velocities to 0 during the first 4 seconds
of flight and compensates for actual drift by adjusting roll and pitch commands.
Args:
current_time (float): Current time since flight start
Returns:
Tuple[float, float]: Compensated roll and pitch commands
"""
if not self.anti_drift_enabled or self.anti_drift_start_time is None:
return 0.0, 0.0
elapsed_time = current_time - self.anti_drift_start_time
if elapsed_time >= self.anti_drift_duration:
return 0.0, 0.0
# Calculate drift compensation based on current velocity
# Positive velocity in X requires negative pitch (forward tilt)
# Positive velocity in Y requires positive roll (right tilt)
pitch_compensation = -self.current_velocity[0] * 0.5 # Scale factor
roll_compensation = self.current_velocity[1] * 0.5 # Scale factor
return roll_compensation, pitch_compensation
def compute_control_output(self, dt: float) -> Tuple[np.ndarray, Dict[str, float]]:
"""
Compute control outputs for all four layers.
This is the main control loop that processes the hierarchical control structure:
1. Position control generates velocity commands
2. Velocity control generates attitude commands
3. Attitude control generates angular velocity commands
4. Attitude rate control generates motor commands
Args:
dt (float): Time step for control computation
Returns:
Tuple[np.ndarray, Dict[str, float]]: Motor commands and performance metrics
Motor commands: [motor1, motor2, motor3, motor4] PWM values
Performance metrics: Dictionary with control performance statistics
"""
current_time = time.time()
# Initialize flight timing
if self.flight_start_time is None:
self.flight_start_time = current_time
self.takeoff_start_time = current_time
self.anti_drift_start_time = current_time
self.total_flight_time = current_time - self.flight_start_time
# Layer 1: Position Control (Outer Loop)
# Generate velocity commands from position errors
velocity_commands = np.zeros(3)
for i in range(3):
# Apply smooth takeoff for Z-axis
if i == 2 and self.takeoff_start_time: # Z-axis (height)
target_height = self._smooth_takeoff_trajectory(current_time)
velocity_commands[i] = self.position_controller.update(
target_height, self.current_position[i], dt)
else:
velocity_commands[i] = self.position_controller.update(
self.position_setpoint[i], self.current_position[i], dt)
# Apply velocity limits
velocity_commands = np.clip(velocity_commands, -self.max_velocity, self.max_velocity)
# Layer 2: Velocity Control (Inner Loop)
# Generate attitude commands from velocity errors
attitude_commands = np.zeros(3)
for i in range(3):
attitude_commands[i] = self.velocity_controller.update(
velocity_commands[i], self.current_velocity[i], dt)
# Apply anti-drift compensation
roll_comp, pitch_comp = self._anti_drift_compensation(current_time)
attitude_commands[0] += roll_comp # Roll
attitude_commands[1] += pitch_comp # Pitch
# Apply attitude limits
attitude_commands[:2] = np.clip(attitude_commands[:2], -self.max_attitude, self.max_attitude)
# Layer 3: Attitude Control
# Generate angular velocity commands from attitude errors
attitude_rate_commands = np.zeros(3)
for i in range(3):
attitude_rate_commands[i] = self.attitude_controller.update(
attitude_commands[i], self.current_attitude[i], dt)
# Apply attitude rate limits
attitude_rate_commands = np.clip(attitude_rate_commands, -self.max_attitude_rate, self.max_attitude_rate)
# Layer 4: Attitude Rate Control
# Generate motor commands from angular velocity errors
motor_commands = np.zeros(4)
for i in range(3):
motor_output = self.attitude_rate_controller.update(
attitude_rate_commands[i], self.current_attitude_rate[i], dt)
# Distribute motor output to appropriate motors based on control axis
if i == 0: # Roll: motors 1,3 vs 2,4
motor_commands[0] += motor_output
motor_commands[2] += motor_output
motor_commands[1] -= motor_output
motor_commands[3] -= motor_output
elif i == 1: # Pitch: motors 1,2 vs 3,4
motor_commands[0] += motor_output
motor_commands[1] += motor_output
motor_commands[2] -= motor_output
motor_commands[3] -= motor_output
else: # Yaw: motors 1,3 vs 2,4 (opposite direction)
motor_commands[0] += motor_output
motor_commands[2] -= motor_output
motor_commands[1] -= motor_output
motor_commands[3] += motor_output
# Add base thrust for hover
base_thrust = 0.5 # 50% throttle for hover
motor_commands += base_thrust
# Apply motor limits (0 to 1)
motor_commands = np.clip(motor_commands, 0.0, 1.0)
# Collect performance metrics
performance_metrics = {
'position_error': np.linalg.norm(self.position_setpoint - self.current_position),
'velocity_error': np.linalg.norm(velocity_commands - self.current_velocity),
'attitude_error': np.linalg.norm(attitude_commands - self.current_attitude),
'attitude_rate_error': np.linalg.norm(attitude_rate_commands - self.current_attitude_rate),
'flight_time': self.total_flight_time,
'position_controller_stats': self.position_controller.get_performance_stats(),
'velocity_controller_stats': self.velocity_controller.get_performance_stats(),
'attitude_controller_stats': self.attitude_controller.get_performance_stats(),
'attitude_rate_controller_stats': self.attitude_rate_controller.get_performance_stats()
}
return motor_commands, performance_metrics
def reset_controllers(self):
"""Reset all controller states (useful for takeoff or emergency situations)."""
self.position_controller.reset()
self.velocity_controller.reset()
self.attitude_controller.reset()
self.attitude_rate_controller.reset()
self.takeoff_start_time = None
self.anti_drift_start_time = None
self.flight_start_time = None
logger.info("All controllers reset successfully")
def enable_anti_drift(self, enabled: bool):
"""Enable or disable anti-drift compensation."""
self.anti_drift_enabled = enabled
logger.info(f"Anti-drift compensation {'enabled' if enabled else 'disabled'}")
def set_takeoff_parameters(self, duration: float, target_height: float):
"""
Set takeoff trajectory parameters.
Args:
duration (float): Takeoff duration in seconds
target_height (float): Target height after takeoff in meters
"""
self.takeoff_duration = duration
self.takeoff_target_height = target_height
logger.info(f"Takeoff parameters set: duration={duration}s, target_height={target_height}m")
# Example usage and testing
if __name__ == "__main__":
"""
Example usage of the 4-layer PID controller.
This demonstrates how to set up and use the controller for quadrotor flight control.
"""
# Define PID gains for each layer (these would typically be tuned experimentally)
config = {
'position': PIDGains(kp=1.0, ki=0.1, kd=0.5, integral_limit=10.0, output_limit=2.0),
'velocity': PIDGains(kp=2.0, ki=0.2, kd=1.0, integral_limit=5.0, output_limit=0.5),
'attitude': PIDGains(kp=3.0, ki=0.0, kd=0.8, integral_limit=1.0, output_limit=0.3),
'attitude_rate': PIDGains(kp=4.0, ki=0.0, kd=1.2, integral_limit=0.5, output_limit=0.2)
}
# Initialize controller
controller = FourLayerPIDController(config)
# Set target position
controller.set_position_setpoint(1.0, 0.5, 2.0) # 1m forward, 0.5m right, 2m up
# Set takeoff parameters
controller.set_takeoff_parameters(duration=3.0, target_height=2.0)
# Simulation loop
dt = 0.01 # 100Hz control loop
simulation_time = 10.0 # 10 seconds simulation
print("Starting 4-layer PID controller simulation...")
print("=" * 50)
for t in np.arange(0, simulation_time, dt):
# Simulate current measurements (in real implementation, these come from sensors)
current_position = np.array([0.1 * t, 0.05 * t, 0.2 * t]) # Simulated position
current_velocity = np.array([0.1, 0.05, 0.2]) # Simulated velocity
current_attitude = np.array([0.0, 0.0, 0.0]) # Simulated attitude
current_attitude_rate = np.array([0.0, 0.0, 0.0]) # Simulated attitude rates
# Update measurements
controller.update_measurements(current_position, current_velocity,
current_attitude, current_attitude_rate)
# Compute control output
motor_commands, metrics = controller.compute_control_output(dt)
# Print progress every second
if int(t) != int(t - dt):
print(f"Time: {t:.1f}s | Position Error: {metrics['position_error']:.3f}m | "
f"Motor Commands: [{motor_commands[0]:.3f}, {motor_commands[1]:.3f}, "
f"{motor_commands[2]:.3f}, {motor_commands[3]:.3f}]")
print("=" * 50)
print("Simulation completed!")
print(f"Final position error: {metrics['position_error']:.3f}m")
print(f"Total flight time: {metrics['flight_time']:.1f}s")
相關文章
real_time_controller.cpp
real_time_controller.cpp — cpp source code from the CrazyFly learning materials (CrazyFly/cpp/high_freq_control/real_time_controller.cpp).
閱讀文章 →four_layer_pid.m
four_layer_pid.m — objectivec source code from the CrazyFly learning materials (CrazyFly/matlab/analysis/four_layer_pid.m).
閱讀文章 →hybrid_controller.m
hybrid_controller.m — objectivec source code from the CrazyFly learning materials (CrazyFly/matlab/analysis/hybrid_controller.m).
閱讀文章 →l1_adaptive_model.m
l1_adaptive_model.m — objectivec source code from the CrazyFly learning materials (CrazyFly/matlab/analysis/l1_adaptive_model.m).
閱讀文章 →parameter_optimizer.m
parameter_optimizer.m — objectivec source code from the CrazyFly learning materials (CrazyFly/matlab/analysis/parameter_optimizer.m).
閱讀文章 →performance_analyzer.m
performance_analyzer.m — objectivec source code from the CrazyFly learning materials (CrazyFly/matlab/analysis/performance_analyzer.m).
閱讀文章 →