VEX V5 PYTHON | 05.3 PID STABILITY

Guide objective

Recognize windup, noisy derivative output and saturation, then test bounded integral and filtering policies before applying them to movement.

01 | THREE PROBLEMS THAT LOOK LIKE BAD TUNING

A PID loop can behave poorly even when its basic equation is correct. Three common causes are stored integral error, noisy derivative estimates and commands that exceed the available output.

Inspect the individual terms before changing all the gains
ProblemTypical clueFirst question
Integral windupCorrection persists after the original error has disappearedHas stored error grown while output was limited?
Derivative noiseOutput chatters as small readings changeIs differentiating noise creating a large D term?
Output saturationRequested output is larger than the allowed commandIs the controller staying at its cap?

Log error, P, I, D, the unclamped request and the final command. A final command of 25% alone does not tell you why the controller asked for it.

02 | WHAT INTEGRAL WINDUP MEANS

The integral remembers error over time. That can help correct a small persistent offset, but it can also store a correction the robot could not carry out.

Imagine a simulated robot with an error of +80 mm for two seconds. The integral grows to 160 mm·s. With Ki = 0.5 percent/(mm·s), its I contribution becomes +80%.

Even if position then reaches the target and P falls to zero, that stored I contribution can still request forward movement. The robot may overshoot while the stored correction unwinds.

Windup is accumulated controller state, not a motor fault. A physically blocked or saturated system is a common trigger. Explore blocked behaviour in simulation, rather than holding a real robot against an obstacle.

03 | CLAMPING OUTPUT DOES NOT CLEAR MEMORY

Illustrative unprotected PI calculation
After two seconds of +80 mm errorValue
Integral accumulation160 mm·s
I contribution at Ki = 0.5+80%
P contribution at Kp = 0.8+64%
Unclamped total request+144%
Command with a ±25% cap+25%

The cap prevents a command above 25%. It does not stop the unprotected accumulator from growing.

When the error becomes zero, the same accumulator still gives +80%, so the command stays at +25%. Output limiting and anti-windup solve related but different problems.

04 | CHOOSE AN INTEGRAL POLICY

These policies can be combined, but their behaviour must be tested
PolicyActionTradeoff
Start with Ki = 0Use P or PD firstPersistent offset may remain
Integral activation zoneAccumulate only near the target; clear outside the zoneA small zone may prevent useful correction
Integral contribution capLimit how much I can add to outputDoes not by itself account for all saturation
Conditional integrationFreeze accumulation that drives further into saturationDepends on the actual output-limiting policy
Reset on new move or sign changeDiscard old stored errorMay remove a useful bias correction

The 05.2 starter combines these policies. Its I contribution is capped at 2%, and Ki begins at zero. These values are examples rather than universal settings.

Resetting I every update would make it ineffective. Reset it for a documented reason, such as a new move, disable/restart policy or loss of valid feedback.

A robot that needs sustained output to oppose gravity may need feedforward or a different holding design. The drivetrain move-and-stop policy is not automatically suitable for a lift.

05 | CONDITIONAL INTEGRATION IN PLAIN LANGUAGE

First calculate a candidate accumulator. Then ask whether accepting it would push the total request farther beyond the cap.

If output is already positively saturated and positive error would add more positive integral, freeze that accumulation. If error would pull a saturated command back toward the allowed range, permit useful unwinding.

The lab checks saturation before the command clamp. Its policy assumes that the final output limit is the cap used by the controller.

If your project later adds a speed ramp, combines heading correction or applies another limit, the actual command may differ. Anti-windup should account for that complete output path; merely inspecting an earlier calculation can miss downstream saturation.

06 | WHY DERIVATIVE MAGNIFIES NOISE

Derivative divides the change in error by elapsed time. A small change over a short interval can produce a large rate.

A stationary measurement can appear to alternate in speed
Error sampleChange over 20 msCalculated rate
20.2 mm to 19.8 mm-0.4 mm-20 mm/s
19.8 mm to 20.2 mm+0.4 mm+20 mm/s

With Kd = 0.2 percent·s/mm, those rates produce alternating D contributions of -4% and +4%. The position changed by only 0.4 mm, yet the output swings by 8 percentage points.

Encoder quantization, vibration and sensor noise can all affect the estimate. Check mounting, wiring and sensor behaviour before treating filtering as a cure for every issue.

07 | FILTER D WITHOUT HIDING THE TRADEOFF

A first-order low-pass filter makes the derivative estimate change more gradually:

# dt and filter_time are in seconds.
alpha = dt / (filter_time + dt)
filtered_rate += alpha * (raw_rate - filtered_rate)
d_term = kd * filtered_rate

With dt = 0.02 s and a filter time of 0.05 s, alpha is approximately 0.286. Each update moves the filtered estimate partway toward the new rate.

A longer filter time usually smooths more but adds lag. Too much lag can delay useful damping. Compare output traces and physical movement when changing it.

The first derivative sample needs an initialization policy. The lab starts at zero because there is no previous error. A new movement creates fresh derivative state.

08 | AVOID FALSE DERIVATIVE SPIKES

Heading wrap: an error changing from +179 to -179 degrees has crossed the wrap boundary. A raw subtraction gives -358 degrees. The wrapped difference is +2 degrees for the assumed small inter-sample change.

Setpoint changes: changing a target suddenly changes error even if the robot has not moved. Derivative of error can then produce a kick. For changing targets, consider derivative on measurement and appropriate target profiling.

Timing: compute rates with elapsed seconds. Do not silently assume every loop ran on time. The 05.2 movement loop rejects long gaps rather than treating them as normal updates.

A wrapped difference assumes rotation between samples is small enough to interpret. It cannot reconstruct arbitrary missed revolutions.

09 | UNDERSTAND OUTPUT LIMITS

Saturation means the request has reached an allowed boundary. With a ±25% limit, requests of +30%, +60% and +100% all become the same +25% command.

Increasing Kp while the loop is saturated may not increase the early command at all. It can instead keep the controller saturated closer to the target, increasing overshoot risk.

Use limits with clear roles
LimitPurposeDoes not guarantee
Magnitude capBound the requested motor outputA particular torque or stopping distance
Slew-rate limitBound command change over timeA cap on absolute output unless also applied
Movement timeoutBound a failed movement attemptProtection from every jam before the deadline
Position/rate finish windowsDefine feedback successVerified physical field arrival

VEX PERCENT commands request motor velocity percentage; they are not a fixed percentage of torque. Brake behaviour, traction and load still affect the physical result.

Cancellation and protective stops should not be delayed by a normal acceleration ramp. Keep ordinary smoothing and stop policy deliberate.

10 | MOTOR-FREE COMPARISON LAB

This complete script runs in ordinary Python or below the generated region in VEXcode V5. It needs no hardware declarations because it commands no motors.

The first experiment holds a simulated error while output saturates, then sets error to zero. Compare unprotected integral with the protected policy from 05.2. The second experiment isolates derivative noise using small alternating error readings.

# =============================================================================
# TEAM [MY TEAM NAME / NUMBER] | MOTOR-FREE PID STABILITY LAB
# =============================================================================
# Runs in ordinary Python or below '# Begin project code' in VEXcode V5.
# No ports, motor declarations or movement commands are needed.
# The blocked-error sequence is simulated; do not jam a real robot.

MAX_OUTPUT = 25.0
I_OUTPUT_CAP = 2.0
DERIVATIVE_FILTER_S = 0.05

def clamp(value, low, high):
    return max(low, min(high, value))

# =============================================================================
# PID STATE: ONE INSTANCE PER CONTROLLED SIDE, RESET FOR EACH MOVE
# =============================================================================
class PID:
    def __init__(self, kp, ki, kd, i_zone, wrapped=False):
        self.kp, self.ki, self.kd = kp, ki, kd
        self.i_zone = i_zone
        self.wrapped = wrapped
        self.integral = 0.0
        self.previous = None
        self.filtered_rate = 0.0

    def update(self, error, dt):
        # Caller rejects non-finite error and unsuitable dt first.
        first = self.previous is None
        delta = 0.0 if first else error - self.previous
        if self.wrapped and not first:
            delta = (delta + 180) % 360 - 180
        rate = None if first else delta / dt
        raw_rate = 0.0 if first else rate
        alpha = dt / (DERIVATIVE_FILTER_S + dt)
        self.filtered_rate += alpha * (raw_rate - self.filtered_rate)
        p = self.kp * error
        d = self.kd * self.filtered_rate

        # Clear stored error outside the I zone or on an error sign change.
        if (self.ki <= 0 or abs(error) > self.i_zone
                or (not first and error * self.previous < 0)):
            self.integral = 0.0
        else:
            candidate = self.integral + error * dt
            candidate = clamp(candidate, -I_OUTPUT_CAP / self.ki,
                              I_OUTPUT_CAP / self.ki)
            trial = p + self.ki * candidate + d
            # Freeze accumulation that would push further into saturation.
            if abs(trial) <= MAX_OUTPUT or trial * error < 0:
                self.integral = candidate
        i = self.ki * self.integral
        self.previous = error
        return clamp(p + i + d, -MAX_OUTPUT, MAX_OUTPUT), p, i, d, rate


# =============================================================================
# EXPERIMENT 1: CLAMPING ALONE VERSUS CONDITIONAL INTEGRATION
# =============================================================================
DT = 0.02
KP, KI = 0.8, 0.5
naive_integral = 0.0
protected = PID(KP, KI, 0.0, i_zone=100.0)
print("step,error,naive_I,naive_request,naive_command,protected_I,protected_command")
# Keep an 80-unit error for two seconds, then reach zero error.
for step, error in enumerate([80.0] * 100 + [0.0] * 5):
    naive_integral += error * DT
    naive_i = KI * naive_integral
    naive_request = KP * error + naive_i
    naive_command = clamp(naive_request, -MAX_OUTPUT, MAX_OUTPUT)
    command, p, i, d, rate = protected.update(error, DT)
    if step % 10 == 0 or step >= 100:
        print("%d,%.1f,%.2f,%.2f,%.2f,%.2f,%.2f" %
              (step, error, naive_i, naive_request, naive_command, i, command))

# =============================================================================
# EXPERIMENT 2: SMALL POSITION NOISE, LARGE DERIVATIVE CHANGES
# =============================================================================
filtered = PID(0.0, 0.0, 0.2, i_zone=100.0)
previous = None
print("step,error,raw_rate,raw_D,filtered_D,filtered_command")
for step in range(20):
    # A fixed target and an alternating noisy error measurement.
    error = 20.0 + (0.2 if step % 2 == 0 else -0.2)
    raw_rate = 0.0 if previous is None else (error - previous) / DT
    raw_d = 0.2 * raw_rate
    command, p, i, d, rate = filtered.update(error, DT)
    print("%d,%.2f,%.2f,%.2f,%.2f,%.2f" %
          (step, error, raw_rate, raw_d, d, command))
    previous = error

print("Simulation complete. No motors were commanded.")

Experiment 1 is a prescribed error sequence, not a physics simulation of a moving robot. It demonstrates controller memory and limiting behaviour; it does not predict your robot’s final position.

Experiment 2 uses a D-only calculation to make noise visible. It is not a recommendation to control a drivetrain with D only.

11 | APPLY THE IDEAS TO THE MOVEMENT PROJECT

Use the complete 05.2 PID movement project for hardware integration. The PID class in this lab matches that project’s update policy; do not paste a second competing controller or Competition object into it.

  1. Begin with Ki and Kd at zero and verified feedback signs.
  2. Inspect P-only saturation and the physical finish.
  3. Add small D only when evidence supports it; compare filtered and raw rates.
  4. Enable small I only for a repeatable persistent offset.
  5. Check the actual output path, settling checks and timeout return statuses.
  6. Repeat on the floor with normal load and save the configuration.

The 05.2 finish-rate check intentionally uses the raw estimated rate. Filtering D therefore does not automatically fix a noisy settling decision. Set a justified rate tolerance and investigate the measurements.

12 | DIAGNOSE USING THE TERMS

Change one variable and repeat the same test
Observed behaviourInspectPossible next action
Command stays positive at zero errorStored I contributionReview reset and accumulation policy
Command alternates with tiny error changesRaw and filtered DReduce Kd or adjust filtering; inspect sensor
Command sits at the cap for most of the moveUnclamped request and saturation timeReview gain, target and cap together
Overshoot grows after adding IIntegral historyDisable I and compare; apply bounded policy
Movement feels delayed after smoothingFiltered-rate lagReduce filter time and retest
Changing cap changes tuning behaviourActual output and anti-windupRetune with the new limit
Feedback settles but robot misses targetPhysical position and wheel slipFix measurement/geometry before gain changes

CHALLENGES | EXPLAIN, SIMULATE AND VERIFY

  1. Calculate: find I after +30 mm error persists for one second with Ki = 0.2.
  2. Compare: run the motor-free lab and explain output after error becomes zero.
  3. Modify: change the integral contribution cap and activation zone separately.
  4. Calculate: compare derivative rates for a 0.4 mm change over 20 ms and 40 ms.
  5. Filter: compare filter times of 0.02, 0.05 and 0.10 seconds using the same noisy sequence.
  6. Wrap: explain the difference between raw and wrapped error changes at ±180 degrees.
  7. Trace: draw your actual command path from PID terms through all output limits.
  8. Evaluate: justify whether your movement routine needs I, D, both or neither.
Success evidence

A calculation sheet, recorded lab outputs and a documented controller policy. Identify which results are simulated and which come from repeatable hardware trials. Explain one benefit and one tradeoff of each protection you use.

OFFICIAL REFERENCES

WPILib is a conceptual reference, not a dependency of this VEX Python example. The lab and learning activities are independently written. VEX and VEXcode are trademarks of their respective owners.