@@ -43,15 +43,24 @@ class AutolevelController:
4343 mode : float = input_var (desc = 'mode: 0=manual, 1=stabilized' )
4444
4545 # === Parameters ===
46- # Outer loop: angle control (slow, 1-3 Hz)
47- Kp_phi : float = param (2.5 , desc = 'P gain roll angle (per rad)' )
48- Kp_theta : float = param (2.0 , desc = 'P gain pitch angle (per rad)' )
49-
50- # Inner loop: rate damping (fast, 15-25 rad/s)
51- Kp_p : float = param (0.6 , desc = 'P gain roll rate (per rad/s)' )
52- Ki_p : float = param (0.0 , desc = 'I gain roll rate' )
53- Kp_q : float = param (0.6 , desc = 'P gain pitch rate (per rad/s)' )
54- Ki_q : float = param (0.0 , desc = 'I gain pitch rate' )
46+ # Rate command sensitivity (how much stick deflection causes rate change)
47+ # Stick-to-rate scaling (rad/s per full stick)
48+ stick_rate_scale_phi : float = param (3.5 * np .pi , desc = 'stick to roll rate (rad/s)' )
49+ stick_rate_scale_theta : float = param (2.0 * np .pi , desc = 'stick to pitch rate (rad/s)' )
50+
51+ # Outer loop: auto-level gains (smooth return to level when stick centered)
52+ Kp_phi : float = param (3.0 , desc = 'P gain roll auto-level (per rad error)' )
53+ Kp_theta : float = param (2.0 , desc = 'P gain pitch auto-level (per rad error)' )
54+
55+ # Inner loop: rate tracking (gyro-based, fast stabilization)
56+ Kp_p : float = param (0.5 , desc = 'P gain roll rate tracking' )
57+ Ki_p : float = param (0.15 , desc = 'I gain roll rate tracking' )
58+ Kp_q : float = param (0.3 , desc = 'P gain pitch rate tracking' )
59+ Ki_q : float = param (0.1 , desc = 'I gain pitch rate tracking' )
60+
61+ # Manual mode gyro damping (gentle - helps with oscillations)
62+ Kd_p_manual : float = param (0.15 , desc = 'damping roll rate in manual mode' )
63+ Kd_q_manual : float = param (0.15 , desc = 'damping pitch rate in manual mode' )
5564
5665 # Yaw damping (passive)
5766 Kp_r : float = param (0.3 , desc = 'P gain yaw rate damping' )
@@ -65,10 +74,6 @@ class AutolevelController:
6574 trim_elevator : float = param (0.0 , desc = 'elevator trim offset (rad)' )
6675 trim_rudder : float = param (0.0 , desc = 'rudder trim offset (rad)' )
6776
68- # Stick to attitude mapping
69- stick_to_phi : float = param (np .deg2rad (45 ), desc = 'aileron stick to roll ref (rad)' )
70- stick_to_theta : float = param (np .deg2rad (20 ), desc = 'elevator stick to pitch ref (rad)' )
71-
7277 # Limits
7378 phi_max : float = param (np .deg2rad (50 ), desc = 'max bank angle (rad)' )
7479 theta_max : float = param (np .deg2rad (30 ), desc = 'max pitch angle (rad)' )
@@ -131,54 +136,89 @@ def autolevel_controller() -> Model:
131136 # Airspeed from velocity
132137 speed_meas = cy .norm_2 (m .vel .sym )
133138
134- # In stabilized mode, map stick inputs to attitude references
135- phi_ref_from_stick = m .ail_manual .sym * m .stick_to_phi .sym
136- theta_ref_from_stick = m .elev_manual .sym * m .stick_to_theta .sym
137-
138- # Saturate reference angles to max limits
139- phi_ref_sat = _saturate (phi_ref_from_stick , - m .phi_max .sym , m .phi_max .sym )
140- theta_ref_sat = _saturate (theta_ref_from_stick , - m .theta_max .sym , m .theta_max .sym )
141-
142- # Outer loop: angle errors -> rate commands
143- e_phi = phi_ref_sat - phi
144- e_theta = theta_ref_sat - theta
145-
146- p_cmd = m .Kp_phi .sym * e_phi
147- q_cmd = m .Kp_theta .sym * e_theta
148-
149- # Inner loop: rate errors
150- e_p = p_cmd - p_meas
151- e_q = q_cmd - q_meas
152-
153- # ODEs: Integral states (only accumulate in stabilized mode)
139+ # SAFE/AS3X Style Stabilization:
140+ # In stabilized mode, stick commands RATE; autopilot auto-levels when stick centered
141+ # This is rate-based stabilization + automatic leveling, not attitude hold
142+
143+ # Stick input to desired rate commands (pilot commands rate directly)
144+ p_cmd_stick = m .ail_manual .sym * m .stick_rate_scale_phi .sym
145+ q_cmd_stick = m .elev_manual .sym * m .stick_rate_scale_theta .sym
146+
147+ # Auto-level: when stick is centered, command returns to level attitude
148+ # Proportional feedback from ACTUAL attitude error to drive back to level (0°)
149+ # This ensures wings-level return regardless of current bank/pitch
150+ p_cmd_level = - m .Kp_phi .sym * phi # Roll error: always drives to phi=0 (wings-level)
151+ q_cmd_level = - m .Kp_theta .sym * theta # Pitch error: always drives to theta=0 (level)
152+
153+ # Apply angle limits to STICK commands in stabilized mode (only limit pilot input)
154+ # If near max bank angle, reduce stick authority to prevent exceeding limit
155+ # Margin factor: 0.9 = start limiting at 90% of max angle
156+ angle_margin = 0.9
157+ phi_saturation_factor = cy .fmin (1.0 , (m .phi_max .sym - cy .fabs (phi )) / (m .phi_max .sym * (1.0 - angle_margin )))
158+ theta_saturation_factor = cy .fmin (1.0 , (m .theta_max .sym - cy .fabs (theta )) / (m .theta_max .sym * (1.0 - angle_margin )))
159+
160+ # Limit stick-commanded rates by saturation factor (soft limit on pilot input)
161+ p_cmd_stick_limited = p_cmd_stick * phi_saturation_factor
162+ q_cmd_stick_limited = q_cmd_stick * theta_saturation_factor
163+
164+ # In stabilized mode: blend LIMITED stick rate commands with auto-level
165+ # Auto-level is NEVER saturated - it always tries to level wings
166+ # In manual mode: stick directly controls surfaces
167+ p_cmd = p_cmd_stick_limited * (1.0 - m .mode .sym ) + (p_cmd_stick_limited + p_cmd_level ) * m .mode .sym
168+ q_cmd = q_cmd_stick_limited * (1.0 - m .mode .sym ) + (q_cmd_stick_limited + q_cmd_level ) * m .mode .sym
169+
170+ # Inner loop: rate tracking (gyro-based rate damping)
171+ # Always active in stabilized mode to track commanded rates
172+ e_p = (p_cmd - p_meas ) * m .mode .sym # Rate error (only in stabilized)
173+ e_q = (q_cmd - q_meas ) * m .mode .sym # Rate error (only in stabilized)
174+
175+ # In manual mode, provide some gyro damping to improve handling
176+ e_p_manual = - m .Kp_p .sym * p_meas # Damping proportional to rate
177+ e_q_manual = - m .Kp_q .sym * q_meas # Damping proportional to rate
178+
179+ # ODEs: Integral states for rate tracking (only in stabilized mode)
154180 model .ode (m .i_p , e_p * m .mode .sym )
155181 model .ode (m .i_q , e_q * m .mode .sym )
156182
157- # Stabilized mode control outputs
183+ # Stabilized mode control outputs (rate-based)
184+ # PID rate controller to track commanded rates
185+ ail_attitude_fb = - 1.5 * phi # Direct roll error feedback (only for roll leveling)
186+
158187 ail_stabilized = _saturate (
159- m .Kp_p .sym * e_p + m .Ki_p .sym * m .i_p .sym ,
188+ m .Kp_p .sym * e_p + m .Ki_p .sym * m .i_p .sym + ail_attitude_fb ,
160189 m .ail_min .sym , m .ail_max .sym
161190 )
162191 elev_stabilized = _saturate (
163192 m .Kp_q .sym * e_q + m .Ki_q .sym * m .i_q .sym ,
164193 m .elev_min .sym , m .elev_max .sym
165194 )
195+
196+ # Manual mode outputs (direct pass-through + gentle gyro damping)
197+ ail_manual_out = m .ail_manual .sym + e_p_manual
198+ elev_manual_out = m .elev_manual .sym + e_q_manual
166199 rud_stabilized = _saturate (- m .Kp_r .sym * r_meas , m .rud_min .sym , m .rud_max .sym )
167200 e_speed = m .speed_ref .sym - speed_meas
168201 thr_stabilized = _saturate (m .Kp_speed .sym * e_speed , m .thr_min .sym , m .thr_max .sym )
169202
170- # Mode switch: 0 = manual (pass-through ), 1 = stabilized (autopilot )
171- # Apply trim offsets to all outputs
203+ # Mode switch: 0 = manual (direct + damping ), 1 = stabilized (rate-based auto-level )
204+ # Aileron/Elevator: switch between manual and stabilized
172205 ail_out = (
173- m .ail_manual .sym * (1.0 - m .mode .sym ) + ail_stabilized * m .mode .sym
206+ _saturate (ail_manual_out , m .ail_min .sym , m .ail_max .sym ) * (1.0 - m .mode .sym ) +
207+ ail_stabilized * m .mode .sym
174208 ) + m .trim_aileron .sym
209+
175210 elev_out = (
176- m .elev_manual .sym * (1.0 - m .mode .sym ) + elev_stabilized * m .mode .sym
211+ _saturate (elev_manual_out , m .elev_min .sym , m .elev_max .sym ) * (1.0 - m .mode .sym ) +
212+ elev_stabilized * m .mode .sym
177213 ) + m .trim_elevator .sym
214+
215+ # Rudder: yaw damping in both modes
178216 rud_out = (
179217 m .rud_manual .sym * (1.0 - m .mode .sym ) + rud_stabilized * m .mode .sym
180218 ) + m .trim_rudder .sym
181- thr_out = m .thr_manual .sym * (1.0 - m .mode .sym ) + thr_stabilized * m .mode .sym
219+
220+ # Throttle: pilot always controls throttle directly
221+ thr_out = m .thr_manual .sym
182222
183223 # Define outputs
184224 model .output (m .ail , ail_out )
0 commit comments