Técnicas avanzadas de optimización de controladores PID para robótica de competencia — más allá del tuning empírico básico. Cubre gain scheduling por régimen, derivative filtering (D-on-measurement, D-low-pass), anti-windup con back-calculation, feedforward del modelo, two-degree-of-freedom (setpoint weighting), control bang-bang en saturación, deadband, métricas formales (ITAE, ISE, IAE), gain scheduling adaptativo, y consideraciones de tiempo de muestreo. Usar SIEMPRE que se requiera optimizar un PID que ya funciona pero hay que pulirlo, exprimir velocidad sin perder estabilidad, manejar régimenes muy distintos (recta vs curva), saturación de actuadores, ruido alto en la medición, o se mencione 'gain scheduling', 'derivative kick', 'D-on-measurement', 'derivative filter', 'back-calculation anti-windup', 'feedforward', 'two-degree-of-freedom PID', 'setpoint weighting', 'ITAE', 'ISE', 'control optimizado', 'PID adaptativo'. NO usar para el primer tuning empírico básico (eso es robotics-control-theory).
Técnicas avanzadas de optimización de controladores PID para robótica de competencia — más allá del tuning empírico básico. Cubre gain scheduling por régimen, derivative filtering (D-on-measurement, D-low-pass), anti-windup con back-calculation, feedforward del modelo, two-degree-of-freedom (setpoint weighting), control bang-bang en saturación, deadband, métricas formales (ITAE, ISE, IAE), gain scheduling adaptativo, y consideraciones de tiempo de muestreo. Usar SIEMPRE que se requiera optimizar un PID que ya funciona pero hay que pulirlo, exprimir velocidad sin perder estabilidad, manejar régimenes muy distintos (recta vs curva), saturación de actuadores, ruido alto en la medición, o se mencione 'gain scheduling', 'derivative kick', 'D-on-measurement', 'derivative filter', 'back-calculation anti-windup', 'feedforward', 'two-degree-of-freedom PID', 'setpoint weighting', 'ITAE', 'ISE', 'control optimizado', 'PID adaptativo'. NO usar para el primer tuning empírico básico (eso es robotics-control-theory).
PID optimizado para robots de competencia
Esta skill cubre las técnicas que toman un PID que ya funciona y lo llevan al siguiente nivel: más velocidad, menos overshoot, mejor desempeño en condiciones cambiantes, robustez frente a ruido y saturación. Asume conocimiento de robotics-control-theory (los fundamentos: qué es P, I, D, anti-windup básico, Ziegler-Nichols).
El problema con el PID "naïve"
# PID clásico (lo que la mayoría implementa)
error = setpoint - measurement
integral += error * dt
derivative = (error - last_error) / dt
last_error = error
output = Kp*error + Ki*integral + Kd*derivative
Este código funciona, pero tiene cuatro problemas serios que aparecen en competencia real:
Derivative kick: cuando el setpoint cambia bruscamente, la derivada del error explota → spike enorme en el output → robot da tirones.
Integral windup: si el actuador satura (motor a 100%), el integral sigue creciendo, y cuando el error revierte tarda MUCHO en bajarse el integral acumulado.
D amplifica ruido: la derivada del error es muy sensible a ruido de medición. Si la señal del sensor tiene 2% de ruido, Kd=10 lo amplifica a 20% en el output.
No usa el conocimiento del modelo: el PID descubre cada curva como si fuera nueva. Si sabés la geometría, podés hacer feedforward.
Vamos por cada uno.
1. Derivative on measurement (mata el "derivative kick")
Insight: la derivada del error = derivada del (setpoint - measurement). Si el setpoint es constante, da igual derivar el error o -measurement. Pero cuando el setpoint cambia, derivar el error genera un impulso. Derivar solo la medición evita el impulso.
# Forma "D-on-measurement" — preferida en industria
error = setpoint - measurement
integral += error * dt
derivative = -(measurement - last_measurement) / dt # OJO el signo
last_measurement = measurement
output = Kp*error + Ki*integral + Kd*derivative
Cuándo sí cambia: en line following el setpoint es constante (50, o 35 si es array de 8), no hay derivative kick. Pero en sistemas con setpoint dinámico (control de posición a un target que cambia, gyro-driving a heading variable) esta forma es estrictamente mejor.
2. Derivative low-pass filter (mata el ruido amplificado)
El término D amplifica ruido de medición. Si tu sensor lee con ±2% de ruido y Kd=10, tu output tiembla ±20% solo por ruido. Solución: filtro low-pass sobre la derivada.
Regla práctica: si Kd>0 y ves jitter en el output sin razón aparente (sin que se mueva el error), agregá un low-pass con alpha=0.3. Vas a poder subir Kd 2-3× sin temblequera.
3. Anti-windup avanzado — back-calculation
El anti-windup "clamp" (if integral > MAX: integral = MAX) es lo básico. Funciona, pero deja al integral pegado en el límite hasta que el error revierte completo.
Back-calculation descarga el integral progresivamente cuando el actuador satura, devolviendo control suave en cuanto el sistema se descomprime.
classPIDBackCalc:
def__init__(self, kp, ki, kd, out_min, out_max, kbc=None):
self.kp, self.ki, self.kd = kp, ki, kd
self.out_min, self.out_max = out_min, out_max
# Coeficiente back-calculation. Regla práctica: 1/Tt donde Tt ≈ sqrt(Ti*Td)self.kbc = kbc if kbc isnotNoneelse1.0self.integral = 0self.last_meas = Nonedefupdate(self, setpoint, measurement, dt):
error = setpoint - measurement
ifself.last_meas isNone:
d = 0else:
d = -(measurement - self.last_meas) / dt
self.last_meas = measurement
# Output sin saturar
out_unsat = self.kp * error + self.ki * self.integral + self.kd * d
# Saturar
out_sat = max(self.out_min, min(self.out_max, out_unsat))
# Actualizar integral con back-calc: si saturó, "tira" el integral# hacia abajo proporcionalmente al excess
saturation_excess = out_sat - out_unsat
self.integral += (error + self.kbc * saturation_excess) * dt
return out_sat
Cuándo importa: cuando el actuador satura por períodos largos. En line following normal no es crítico (el output rara vez satura). En control de velocidad con cargas variables, sí.
4. Two-degree-of-freedom PID (setpoint weighting)
Cuando querés que el PID rechace bien las perturbaciones (es decir, vuelva al setpoint rápido si algo lo empuja) PERO no ataque agresivo al cambio de setpoint, usás dos ganancias distintas para cada caso.
Aplicación típica: control de posición de un brazo robótico donde a veces el target cambia (no querés tirones) y a veces hay carga variable (querés rechazo fuerte).
Para line following el setpoint es constante, así que da igual. Para gyro-driving con cambios de target (girar a 90°, después a -45°) es clave — evita que cada cambio de target genere un spike.
5. Feedforward — el upgrade silencioso
Feedforward = calcular el control que se necesita SIN esperar al error. Es proactivo, el PID solo corrige residuos.
Feedforward en curva conocida
Si vas a hacer una curva de radio R a velocidad v, sabés que el turn rate ideal es ω = v/R. Setealo directamente; el PID solo compensa drift.
Motores reales tienen fricción estática (Coulomb friction) — necesitan un mínimo de potencia para arrancar. PID puro lo "descubre" lento.
FRICTION_OFFSET = 5# PWM units necesarios para vencer friccióndefvelocity_pid_with_ff(target_v, measured_v, dt):
error = target_v - measured_v
pid_out = Kp * error + Ki * integral + Kd * derivative
# FF: dirección de la velocidad target + offset por fricción
ff = sign(target_v) * FRICTION_OFFSET + Kv * target_v
return ff + pid_out
Donde Kv es el inverso de la ganancia velocidad↔PWM del motor (medible empíricamente: ¿qué PWM te da 100 deg/s en velocidad estable?).
6. Gain scheduling — múltiples PIDs por régimen
Un solo PID raramente es óptimo para todos los regímenes. Gain scheduling = cambiar las ganancias según el "régimen" actual.
Caso típico en line following: recta vs curva
defselect_gains(error):
"""Switch entre dos sets de ganancias según magnitud del error."""ifabs(error) < 8: # casi rectoreturn (1.0, 0.0, 6.0) # KP, KI, KD agresivo pero limpioelifabs(error) < 20: # curva moderadareturn (1.4, 0.0, 10.0)
else: # curva cerradareturn (2.0, 0.0, 15.0)
Versión continua (smooth gain scheduling)
Saltos discretos pueden generar discontinuidades. Mejor interpolación:
defgains_smooth(error):
"""Interpolación lineal de Kp, Kd según |error|."""
e = abs(error)
if e <= 8:
t = e / 8
kp = 1.0 + t * 0.4
kd = 6.0 + t * 4.0elif e <= 20:
t = (e - 8) / 12
kp = 1.4 + t * 0.6
kd = 10.0 + t * 5.0else:
kp = 2.0
kd = 15.0return kp, kd
Schedule por velocidad
Tu Kp óptimo escala con la velocidad. Más velocidad = más Kp (porque el error crece más rápido).
Cuando el error es enorme (línea perdida, robot girado 90° de su heading), el PID puede mandar un output muy chico relativo al máximo y eso es ineficiente. Detectar saturación y mandar full power.
Si tu sensor tiene resolución limitada y el error es minúsculo (1-2 unidades), correr el PID con un error tan chico es mover el actuador por ruido. Definir un deadband evita micro-correcciones.
defdeadband_pid(error, deriv, deadband=2):
ifabs(error) < deadband:
return0# Que el output sea continuo en el borde:
adjusted_error = error - sign(error) * deadband
return Kp * adjusted_error + Kd * deriv
Usar con cuidado en line following — un deadband muy grande causa zigzag suave.
9. Métricas formales para comparar tunings
Cuando comparás dos sets de ganancias, "se ve mejor" no escala. Usar métricas numéricas hace el tuning comparable y reproducible.
Correr 5 vueltas con cada set de ganancias, calcular ITAE promedio, elegir el mínimo. Es así de simple y funciona.
10. Tiempo de muestreo (dt) — la trampa que arruina tunings
Los términos I y D dependen de dt. Si tu loop a veces tarda 10 ms y a veces 25 ms, las ganancias efectivas cambian.
Tres reglas:
Medir dt real, no asumirlo:
from pybricks.tools import StopWatch
watch = StopWatch()
last = watch.time()
whileTrue:
now = watch.time()
dt = (now - last) / 1000.0# segundos
last = now
...
Loop rate constante: forzar wait(target_ms - elapsed) al final del loop.
Si el loop varía mucho, normalizar las ganancias para dt fijo:
DT_NOMINAL = 0.02
factor = dt / DT_NOMINAL
integral += error * dt # ya usa dt
derivative = (error - last_error) / dt # ya usa dt# Si tunearon con dt=20 ms, las fórmulas con dt explícito son robustas
Regla práctica: 50-100 Hz suele ser suficiente para robots LEGO. Más alto, el muestreo del sensor empieza a ser el cuello de botella; más bajo, perdés ancho de banda de control.
11. PID con saturación dinámica del integral
Más robusto que clamp fijo: limitar el integral según cuánto "headroom" queda en el output.
defsmart_clamp_integral(integral, error, ki, out_min, out_max):
"""Limita el integral para que NUNCA pueda saturar por sí solo."""
headroom_max = (out_max - 0) / ki # 0 = sin P ni D
headroom_min = (out_min - 0) / ki
returnmax(headroom_min, min(headroom_max, integral))
Más sofisticado: el "conditional integration" — solo integrar cuando el output NO está saturado:
out_unsat = Kp*error + Ki*integral + Kd*deriv
out_sat = clamp(out_unsat, out_min, out_max)
if out_sat == out_unsat: # no saturó
integral += error * dt # integrar normal# si saturó, no se integra → no hay windup
Checklist de "PID optimizado" para competencia
Tildalo todo cuando salgas a competir con un PID serio:
D-on-measurement en vez de D-on-error (evita derivative kick si setpoint cambia)
Low-pass sobre la derivada (alpha 0.3-0.4)
Anti-windup: clamp, mejor back-calculation o conditional integration
Saturación explícita del output (max(out_min, min(out_max, output)))
Loop rate constante medido (no esperás "≈10 ms")
Gain scheduling si los regímenes son MUY distintos
Feedforward donde haya información a priori (radio de curva, peso del brazo)
Métricas (ITAE) grabadas para comparar tunings
Ganancias guardadas como constantes named en un solo lugar, no hard-coded
DataLog grabando: error, P, I, D, output, dt para análisis post-corrida