airsim-loop · lazo táctico · 2026-10-14
Configuración del grafo de control
Referencia técnica del lazo de navegación autónoma tal como está en el código: topología, esquema de estado, lazo rápido de 8 reglas, capa estratégica asíncrona con sub-metas ancladas a la pose del fotograma (fuente por defecto: profundidad monocular estimada), detección de atasco, barrido panorámico juzgado por la misma red, cinemática por macro-acción y la tabla de umbrales.
- Frecuencia
- 5.0 Hz
- Brazo activo
- slm
- TTC evasión
- 3.2 s
- TTC seguro
- 4.6 s
- Capa estratégica
- depth · 1 s
- Edad máx.
- 10 s
- Deadlock
- depth
- Tests
- 299
01Panorama
El grafo es un lazo de control compilado con LangGraph que corre a LOOP_HZ = 5.0 (período nominal
200 ms). Cada ciclo captura un fotograma y telemetría, estima el campo de obstáculos por flujo óptico, elige
una macro-acción y la traduce a un comando de velocidad en body frame. En paralelo,
una capa estratégica propone rodeos como sub-metas en coordenadas del mundo. Su fuente es configurable
(STRATEGIC_SOURCE): en la configuración final es una red de profundidad monocular estimada
desde el mismo RGB; el VLM queda en la planificación de ruta previa al vuelo.
Cuatro principios estructuran la configuración:
- Dos lazos desacoplados. El lazo rápido (guiado, evasión por flujo óptico,
GIRAR_90, detección de atasco) es el único que comanda velocidades. La capa estratégica corre en un lazo lento y asíncrono (~110 ms con la profundidad, ~0.8 s con el VLM local) y nunca detiene al dron en vuelo normal. - La respuesta es válida cuando llega. Cada pedido guarda la pose del fotograma (ancla); la respuesta se traduce con esa pose, no con la actual, y se expresa en rumbos absolutos, no en «izquierda/derecha» del cuerpo. El resultado es una sub-meta en el mundo que el guiado normal persigue.
- La fuente decide; las reglas no la reescriben. No hay reglas deterministas sobre la respuesta de la red o del modelo. El fallback determinista existe solo ante falla (timeout, formato inválido, rotación imposible durante un barrido).
- Sin profundidad del simulador. Ni el grafo, ni
main.py, niexperiments/runner.pyleen el canal de profundidad de AirSim, ni para control ni como métrica. Toda la percepción del lazo rápido sale deObstacleField(flujo óptico monocular). La profundidad estimada desde el RGB sí se usa en el lazo lento: es lo que haría una red a bordo de un dron sin sensor de profundidad.
El brazo se selecciona con AGENT_ARM (slm · fsm ·
reactive) sobre el mismo pipeline de percepción y la misma cinemática; la
estrategia de resolución de atascos, con DEADLOCK_STRATEGY (depth ·
deep_vlm · blind); la fuente de la capa estratégica, con
STRATEGIC_SOURCE (depth · vlm · off). Con
depth/depth el VLM no corre durante el vuelo.
02Topología
degraded_router; la política vive dentro de navigate, que invoca los
comportamientos (guiado, evasión, giro, escape, barrido) como funciones. En teal, el lazo lento: la capa
estratégica recibe el fotograma con su pose y devuelve, varios ciclos después, una sub-meta en el mundo que
el bucle externo inserta en el WaypointTracker.| Origen | Tipo | Destinos |
|---|---|---|
| (entry) | fija | capture |
| capture | condicional · degraded_router |
degraded_hover, perception |
| perception | fija | navigate |
| navigate, degraded_hover | fijas | motor |
| motor | fija | END |
03El estado DroneState
DroneState es un TypedDict y LangGraph construye los canales del
grafo a partir de él. Esto tiene una consecuencia operativa que es parte de la configuración, no un detalle de
implementación:
Toda clave que cruce la frontera nodo ↔ lazo externo debe estar declarada en
DroneState. Una clave que un nodo escriba sin declarar se descarta en silencio en
graph.invoke() — no hay error, no hay warning, el dato simplemente no existe del otro lado
(informe, cap. 9 §9.5).
| Rol | Campos | Escrito por |
|---|---|---|
| Percepción y sensores | rgb_image, prev_image, prev_telemetry, frame_history, frame_history_ts, telemetry, degraded, obstacle_field, estimated_ttc, scene_summary | capture, perception |
| Misión y guiado | waypoints, current_wp_index, target_waypoint, waypoint_guidance, mission_completed, evasion_stuck_cycles | lazo externo |
| Decisión y actuación | next_action, velocity_command, route, flight_status, active_maneuver, maneuver_cycles_left, maneuver_command, _hover_alt_anchor, _speed_cap | navigate, motor |
| Lazo lento: auditoría y canales al lazo externo | deliberations[], last_deliberation, slm_request_id, _deliberation_pending, _pending_delib_prompt, _pending_delib_frames, _last_delib_frames, _vlm_strategic, inject_corner | navigate + lazo |
| Deadlock y barrido panorámico | _escape_reset, _deadlock_cycles, _deadlock_event, _scan_phase, _scan_heading_index, _scan_frames, _scan_start_yaw_deg, _scan_settle_left, _scan_rot_stall, _deep_scan_request_id, _deep_scan_request_ts, _scan_started_ts | navigate (deep_scan.py) + lazo |
StallDetector (publicado para registro) |
imu_jitter_level, imu_contact_event, blind_wall_event, _stopped_cycles, _wp_no_progress_cycles | navigate |
| Vigilancia de congelamiento | _freeze_cycles | lazo externo |
La auditoría de cada consulta del lazo lento queda en deliberations[]: brazo
(depth_strategic, vlm_strategic o barrido), prompt (vacío con la profundidad),
respuesta cruda (con la profundidad, el p5 por sector en metros), sub-meta resultante, motivo, latencia y si
adhirió al esquema. El fotograma de cada consulta se guarda como photo-*.png. El
capítulo 4 del informe describe cada archivo de una
corrida (traza JSONL con el DroneState completo, CSV, resúmenes, DistMin, videos) y el visor HTML
que los sincroniza por ciclo.
04El lazo externo
No todo vive en el grafo. El orden por ciclo en main.py (y en experiments/runner.py) es:
| # | Paso | Detalle |
|---|---|---|
| 1 | límites | Corta la misión por detención solicitada, MISSION_MAX_CYCLES o
MISSION_MAX_SECONDS |
| 2 | get_telemetry() | Telemetría fresca, independiente de la captura de imagen |
| 3 | freeze_wd.update() | Vigilante de congelamiento: FREEZE_CYCLES (25) ciclos con estado físico idéntico →
la corrida termina como physics_locked |
| 4 | tracker.update(pos) | Avanza el objetivo activo si se entró en el radio de aceptación (3.5 m) |
| 5 | compute_guidance() | Vector de guiado (rumbo, vz, despegue vertical, detección de techo,
z_path_blocked) con histéresis de régimen y suavizado EMA |
| 6 | record_progress(dist_xy) | Contador de progreso del tracker sobre la distancia horizontal, salvo con un barrido esperando al VLM |
| 7 | graph.invoke(state) | Un ciclo completo del grafo |
| 8 | note_executed_command() | Informa al tracker el comando realmente ejecutado: la detección de techo cuenta solo los ciclos en que se ordenó subir |
| 8b | pop("_escape_reset") | Si un deadlock se resolvió → tracker.reset_progress() |
| 9 | inject_corner | Inserta la sub-meta del VLM delante del waypoint activo, reemplazando la pendiente (salvo un
duplicado a menos de SUBGOAL_DEDUP_M = 10 m) |
| 10 | Salidas | Consola, registro JSONL/CSV, video anotado; al cerrar, land_smooth() y
finalize_run (visor HTML y consolidación de DistMin, ver nota) |
| 11 | sleep | Hasta completar el período de LOOP_HZ |
Los pasos 8b y 9 son exactamente los que dependen de la regla de declaración de la sección 3.
En paralelo al lazo corre un hilo de auditoría con su propia conexión al simulador: cada
1 s captura la profundidad de la cámara frontal (percentil 5 del tercio central) y la escribe en
<corrida>.distmin.ndjson. No comparte ningún objeto con el grafo ni con
DroneState; finalize_run vuelca el mínimo en el summary.json. Es la
única lectura de profundidad de una corrida, y la guardia estática lo verifica.
05Nodos
spatial_scan · pre-vuelo, fuera del grafo
Antes del primer ciclo, run_initial_scan() rota al dron detenido y propone un rumbo de arranque;
si difiere más de 90° del rumbo al primer waypoint, emite un aviso para revisar el plan de misión. Es
consultivo: nunca reemplaza al ObstacleField por ciclo.
capture_node
Guarda el fotograma y la telemetría anteriores, captura RGB + telemetría de AirSim y marca
degraded = True si no hay imagen o la telemetría no proviene del simulador. Mantiene el
historial de fotogramas (VLM_FRAME_HISTORY_SIZE = 1) con su marca de tiempo. No captura
profundidad.
degraded_hover_node
Sin datos no se mueve: emite FRENAR con estado degradado. No consulta al VLM.
perception_node
FlowTTCEstimator: flujo óptico denso (DIS) sobre el par de fotogramas, derotación por la
telemetría de actitud, foco de expansión y TTC por celda. Produce ObstacleField (grilla 3×3 con
ocupación, TTC, divergencia y confianza), estimated_ttc y un resumen textual. Es la
única fuente de percepción del lazo rápido.
navigate_node
Contiene la política de los tres brazos. Para reactive delega en reactive_node;
para fsm, en fsm_node (máquina de estados determinista que comparte el barrido de la
sección 8 si DEADLOCK_STRATEGY es depth o deep_vlm); para slm, ejecuta las 8 reglas de la
sección 6.
reactive_node: guiado nominal hacia el objetivo activo (MANTENER_RUMBO), con la tasa de guiñada acotada cerca de un obstáculo.evasive_node: corrección lateral hacia el sector con menor ocupación y mayor TTC, o continuación de la maniobra comprometida (anti flip-flop).GIRAR_90: giro en el lugar hacia el lado del waypoint cuando un muro ocupa más del 60 % del campo visual; adelanta la consulta a la capa estratégica.
motor_node
- Ancla de altitud en
FRENAR: unvz = 0reemitido es un controlador de velocidad, no de altura; el motor corrige la deriva hacia la altitud de anclaje (zona muerta 0.3 m,Kp0.35, tope 0.8 m/s). - Gobernador de velocidad: sin evidencia perceptual o tras un bloqueo reciente, limita
vxdeMANTENER_RUMBO(1.0–1.5 m/s) y luego lo libera en rampa. - Actuación no bloqueante:
execute_velocity()con last-command-wins; exactamente un comando por ciclo.
07Capa estratégica
Implementada en vlm_strategic.py.
Corre en un hilo aparte a través de DeliberationService; el lazo nunca espera su respuesta.
La pregunta —qué sectores de una grilla 3×3 están libres— la responde la fuente de
STRATEGIC_SOURCE; las dos fuentes devuelven la misma estructura, de modo que el ancla y la
traducción a sub-metas son las mismas.
depth_client.py:
Depth Anything V2 Metric Outdoor Small (~25 M parámetros) estima la profundidad en metros desde el
fotograma RGB; un sector es libre si el percentil 5 de su profundidad es ≥ DEPTH_FREE_M
(15 m). ~110 ms por consulta con la GPU compartida, período de 1 s. En el banco de prueba separó sectores
libres de bloqueados con AUC 0.83–0.87; la grilla del VLM, con 0.53. En vuelo ninguna fuente aceleró la
misión; en el corredor arbolado de TownSim la profundidad mantuvo al dron más lejos del follaje en las
5 semillas (informe, cap. 11 §11.6). Las tarjetas que siguen describen el diseño con el VLM
(STRATEGIC_SOURCE=vlm).
Cuándo se consulta
Cada ≥ VLM_STRATEGIC_PERIOD_S (3 s), a ≥ 8 m de altura, con el waypoint real a más de
8 m y la meta dentro del recorte cuadrado que ve el modelo, ±33.7° (o fuera de cuadro si el dron lleva 3 s sin acercarse a su
objetivo). También con una sub-meta activa: la pregunta es siempre sobre el waypoint real y la
respuesta nueva reemplaza a la vieja. No durante un barrido ni en el
despegue. De inmediato si la regla 7 ve un muro (expedite()).
Qué ve
Un recorte cuadrado del centro del fotograma (384 × 384 px; se cortan los dos costados del ancho) con una grilla de 3×3 por tercios, con sectores laterales y verticales a la misma distancia angular (±24°) (columnas A–C; filas 1 arriba, 2 a la altura del dron, 3 abajo) y una marca roja «META» en la dirección del destino. Cada sector tiene una dirección absoluta (rumbo y elevación) fijada con la pose del fotograma. La anotación nunca toca el fotograma del flujo óptico.
Qué responde
JSON con esquema estricto: cada uno de los nueve sectores libre/bloqueado
(volar 15 m en esa dirección). El modelo describe; no decide acciones.
Cómo se usa
Si el sector de la meta está libre, no agrega nada y descarta el desvío pendiente. Si no, sub-meta en
la dirección del sector libre más cercano a la meta, a min(15 m, distancia al waypoint) desde
el ancla, con la altitud que da la elevación del sector (±4 m como máximo).
Ancla y sincronización
Cada pedido guarda la posición, el yaw y el waypoint del momento de la captura. Cuando la respuesta llega, la sub-meta se calcula con esa pose, de modo que es correcta aunque el dron haya girado o avanzado mientras el modelo pensaba. Se descarta, con su motivo registrado, si:
- tiene más de
VLM_STRATEGIC_MAX_AGE_S(10 s); - el waypoint de misión cambió;
- la sub-meta quedó a menos de
VLM_SUBGOAL_MIN_AHEAD_M(4 m) por delante del dron, es decir, ya fue superada; - la respuesta no cumple el esquema (
no_parseable): nunca se sustituye por un valor por defecto.
Al entrar en deadlock, cualquier consulta estratégica en vuelo se cancela: el barrido es más reciente y su sub-meta no debe ser pisada por una respuesta más vieja.
08Resolución de deadlock
Barrido panorámico (DEADLOCK_STRATEGY=depth, default, o deep_vlm) — deep_scan.py
- Rotación: el dron gira en el lugar a
SCAN_HEADING_COUNT_DEEP= 4 rumbos (frente, derecha, atrás, izquierda), se asienta 2 ciclos en cada uno y captura un fotograma. Si no alcanza el rumbo en 10 ciclos (2 s), el barrido se abandona. - Juicio de cada rumbo: con
depth, la red estima la profundidad de cada fotograma y el rumbo es transitable si el p5 del sector central (B2) es ≥ 15 m (AUC 0.87 en el banco; en TownSim resolvió 19 de 30 deadlocks). Condeep_vlm, una sola consulta con las imágenes numeradas (≤ 256 px). Para cada una, el modelo indicatipo(libre, fachada, muro, vegetación, interior, indeterminado),ok(se puede volar recto 15 m) yconf. El modelo nunca reporta ángulos. - Geometría: el código asigna a cada imagen el yaw medido al
capturarla y calcula el rumbo a la meta desde las posiciones. La salida es una sub-meta a
15 m como máximo (sin pasar del waypoint real) sobre el rumbo transitable más cercano a la meta, con la
misma función que la capa estratégica (
subgoal.build_subgoal) y a la altitud actual. - Salida del deadlock: se descartan los rumbos a menos de 45° del que falló (hacia el
objetivo activo) o de uno ya probado en un deadlock a menos de 10 m; con deadlocks repetidos se elige el
rumbo libre más distinto de lo ya probado; si no queda ninguno,
GANAR_ALTURA. La selección completa queda en_deadlock_event.scan_selection. - Sin rumbo transitable:
GANAR_ALTURA(con el VLM,PERDER_ALTURAsi todo es vegetación).
Al resolverse, se reinician el StallDetector y el contador de progreso del tracker
(_escape_reset).
Falla del barrido o estrategia blind
Watchdog del barrido (SLM_DEEP_WATCHDOG_MS = 18 000 ms, menor que el timeout HTTP de 20 s),
formato inválido o rotación imposible: escape determinista GANAR_ALTURA durante 1.6 s; por
encima de MAX_ESCAPE_ALT_M (30 m), GIRAR_90 hacia el waypoint. Se registra un
_deadlock_event con fell_back_to_blind = True.
El ascenso es la maniobra que libera al dron de una estructura horizontal sobre la que quedó apoyado
(cornisa, tablero de una autopista elevada), el caso que el flujo óptico no representa. El brazo
slm no emite RETROCEDER (informe, cap. 5 §5.3.4 y cap. 9 §9.8.6).
09Cinemática por macro-acción
action_to_command() es la única fuente de verdad; el lazo rápido, la
fsm, el barrido y los escapes la comparten. Body frame, NED (vz negativo = sube).
| Macro-acción | vx | vy | vz | yaw_rate | target_yaw |
|---|---|---|---|---|---|
| MANTENER_RUMBO | guiado | 0 | guiado | guiado§ | — |
| EVADIR_IZQUIERDA | 0.8 · 0.3† | 0 | guiado | −15 °/s | snap −90° |
| EVADIR_DERECHA | 0.8 · 0.3† | 0 | guiado | +15 °/s | snap +90° |
| GANAR_ALTURA | 0 | 0 | −1.5 | guiado | — |
| PERDER_ALTURA | 1.0 | 0 | +0.8 | 0 | — |
| GIRAR_90 | 0 | 0 | 0 | ±20 °/s‡ | snap ±90° |
| FRENAR | 0 | 0 | 0¶ | 0 | — |
† 0.3 m/s si hay estructura cercana al frente. ‡ El signo lo fija el
lado del waypoint. § Acotado a 5 °/s cerca de un obstáculo. ¶ Más la
corrección del ancla de altitud del motor. Toda acción desconocida cae a FRENAR.
Duración de las maniobras comprometidas: EVADIR_* 2.0 s
(MANEUVER_DURATION_S), GANAR_ALTURA/PERDER_ALTURA 1.6 s,
GIRAR_90 1.0 s. El snap Manhattan redondea el rumbo objetivo al múltiplo de 90° más
cercano, alineando las maniobras con la cuadrícula urbana.
10Umbrales y variables
Fuente única: config/.env. Detalle completo en el Anexo 6 del informe.
Percepción y lazo rápido
| Variable | Valor | Efecto |
|---|---|---|
| LOOP_HZ | 5.0 | Frecuencia del lazo (200 ms por ciclo). |
| TTC_EVASION_THRESHOLD | 3.2 s | Peligro frontal crítico (Youden, τ = 2 s; AUC 0.97). |
| TTC_SAFE_THRESHOLD | 4.6 s | Ventana de advertencia (Youden, τ = 3 s; AUC 0.96). |
| FOV_BLOCKED_THRESHOLD | 0.6 | Fracción bloqueada a partir de la cual se gira 90° en vez de evadir. |
| OBSTACLE_OCCUPANCY_BLOCKED | 0.011 | Ocupación a partir de la cual un sector se considera bloqueado. |
| OPTICAL_MIN_ALT_M | 4.5 m | Piso óptico: debajo, ni evasión por flujo ni señales de deadlock. |
| REACTIVE_FORWARD_SPEED | 3.0 m/s | Velocidad de crucero del guiado. |
| WAYPOINT_ACCEPTANCE_RADIUS | 3.5 m | Radio para dar un objetivo por alcanzado. |
| GOV_ENABLED | true | Gobernador de velocidad del motor. |
| TAKEOFF_VERTICAL / TAKEOFF_ALT_TOL_M | true / 1.0 m | Despegue vertical hasta la altitud del primer objetivo antes de avanzar. |
| DISTMIN_TRACK_PERIOD_S | 1.0 s | Período del hilo de auditoría de DistMin (fuera del lazo). |
Atasco y escape
| Variable | Valor | Efecto |
|---|---|---|
| STOPPED_DELIBERATIVE_CYCLES / STOPPED_MIN_CMD_MPS | 10 / 0.30 | Dron trabado: ciclos con avance ordenado sin movimiento real. |
| WP_NO_PROGRESS_THRESHOLD / WP_NO_PROGRESS_MIN_M | 50 / 2.0 m | Sin progreso: ciclos sin mejorar la distancia al objetivo actual. |
| DEADLOCK_STRATEGY | depth | depth, deep_vlm o blind; cualquier otro valor aborta el arranque. |
| SCAN_HEADING_COUNT_DEEP / SCAN_SETTLE_CYCLES_DEEP | 4 / 2 | Rumbos del barrido y ciclos de asentamiento por rumbo. |
| SCAN_ROT_TIMEOUT_CYCLES | 10 | Rotación imposible: el barrido se abandona. |
| SLM_DEEP_WATCHDOG_MS / SLM_DEEP_HTTP_TIMEOUT_S | 18 000 / 20 s | Watchdog del barrido; siempre menor que el corte HTTP. |
| CORNER_OFFSET_M | 15 m | Desvío determinista del brazo fsm (el barrido usa VLM_SUBGOAL_DIST_M). |
| MAX_ESCAPE_ALT_M | 30 m | Por encima, el escape determinista es GIRAR_90. |
| FREEZE_CYCLES / FREEZE_RECOVERY | 25 / abort | Vigilante de congelamiento (physics_locked). |
Capa estratégica del VLM
| Variable | Valor | Efecto |
|---|---|---|
| STRATEGIC_SOURCE | depth | Fuente de la capa estratégica: depth, vlm u off. |
| DEPTH_MODEL | DA-V2 Metric Outdoor S | Red de profundidad métrica (~25 M parámetros, ~440 MiB de VRAM). |
| DEPTH_FREE_M / DEPTH_STRATEGIC_PERIOD_S | 15 m / 1.0 s | Umbral de sector libre (p5 de la profundidad) y período de la capa con la profundidad. |
| LOCAL_LLM_MODEL_NAME | lfm2.5-vl-1.6b:q4km-ctx8k | Modelo servido por Ollama local en la misma GPU (plan de ruta; capa y barrido si se eligen vlm/deep_vlm). |
| VLM_STRATEGIC_ENABLED | true | Habilita la capa estratégica. |
| VLM_STRATEGIC_PERIOD_S | 3.0 s | Período mínimo entre consultas con el VLM. |
| VLM_STRATEGIC_MAX_AGE_S | 10 s | Edad máxima de una respuesta para aplicarla. |
| CAMERA_HFOV_DEG | 90° | Campo visual con el que se calculan las direcciones de los sectores de la grilla 3×3. |
| VLM_SUBGOAL_DIST_M / VLM_SUBGOAL_MIN_AHEAD_M | 15 / 4 m | Distancia de la sub-meta desde el ancla y mínima por delante del dron. |
| VLM_MAX_DZ_M / VLM_SUBGOAL_MIN_ALT_M | 4 / 6 m | Cambio máximo de altitud de una sub-meta y altitud mínima. |
| VLM_NEAR_WP_M | 8 m | Sin consultas ni barridos a menos de esta distancia del waypoint real. |
| VLM_STRATEGIC_MAX_GOAL_OFF_DEG | 40° | Azimut máximo de la meta para consultar; rige el recorte cuadrado (±33.7°). |
| SLM_MIN_ALT_M | 8 m | Altura mínima para consultar. |
| SUBGOAL_DEDUP_M | 10 m | Una sub-meta a menos de esta distancia de la pendiente se ignora. |
Los umbrales de la capa estratégica son decisiones de diseño; todavía no están calibrados en vuelo.
11Invariantes
Lo que la configuración garantiza, y dónde se verifica:
| Invariante | Test |
|---|---|
| Ningún componente del lazo de vuelo lee el canal de profundidad del simulador (y la guardia detecta una lectura reintroducida); la única lectura es el hilo aislado de auditoría DistMin | test_no_depth_in_flight_path.py |
| Como máximo una consulta al VLM por ciclo | test_at_most_one_vlm_request_per_cycle |
| Un dron trabado barre, y la sub-meta del VLM cruza la frontera del grafo compilado | test_stuck_drone_scans_and_vlm_subgoal_crosses_graph_boundary |
| La sub-meta estratégica sobrevive al grafo compilado | test_strategic_subgoal_survives_compiled_graph |
Con blind, el escape sube sin consultar al VLM |
test_blind_strategy_climbs_without_calling_the_vlm |
| «Libre hacia la meta» se sigue, no se evade (marco de referencia del barrido) | test_regression_goal_heading_free_is_followed_not_evaded test_absolute_deg_in_model_answer_is_ignored |
| Gana el rumbo transitable más cercano a la meta; sin ninguno, se sube | test_closest_transitable_to_goal_wins test_nothing_transitable_climbs |
| El sector bloqueado lleva al sector libre más cercano, en marco mundo; nunca más allá del waypoint | test_blocked_goal_sector_goes_to_nearest_free_sector_in_world_frame test_subgoal_never_beyond_the_real_waypoint |
El waypoint real se acepta con una sub-meta activa; una entrada ok:true no libre no se usa |
test_pilot_190842z_regressions.py |
| Respuestas viejas, de otro waypoint o ya superadas se descartan | test_stale_or_wrong_wp_answers_are_discarded test_subgoal_already_passed_is_discarded |
| Una respuesta mal formada no produce acción | test_parse_rejects_malformed |
| La anotación de la grilla nunca toca el fotograma del flujo óptico | test_annotation_never_touches_the_flow_frame |
| El despegue y el giro en el lugar no son atasco; avanzar sin moverse sí | test_takeoff_climb_and_rotation_is_not_a_stall test_rotation_in_place_at_cruise_is_not_a_stall test_commanded_forward_without_motion_still_counts |
| El contador de no-progreso se reinicia con un objetivo nuevo | test_no_progress_counter_ignores_takeoff_and_restarts_on_new_subgoal |
| El tracker inserta la sub-meta del VLM tal cual y reemplaza la pendiente | test_tracker_inserts_vlm_subgoal_as_is_and_replaces_pending |
Un descenso por techo contra una estructura llega a deadlock; un giro con vz = 0 no
fabrica un techo; la posición inmóvil cuenta como detenido aunque AirSim reporte velocidad |
test_pilot_seed99_regressions.py |
| El despegue es vertical hasta la altitud de crucero y no cuenta como atasco | test_takeoff_climbs_in_place_until_cruise_altitude test_takeoff_is_not_a_stall |
El hilo DistMin está aislado del lazo; finalize_run consolida sus muestras |
test_distmin_thread_is_isolated_from_the_control_loop test_distmin_audit.py |
| Sin AirSim, hover explícito y sin consulta al VLM | test_degraded_mode.py |
| El giro puro no dispara freno espurio | test_flow_ttc.py |
246 tests, sin AirSim (stub con la misma interfaz):
python -m pytest tests -q
Los tests que cubren la frontera del grafo corren el grafo compilado, no el nodo suelto: el descarte silencioso de claves no declaradas solo existe en esa frontera.
12Limitaciones conocidas
- Validación en vuelo limitada. La capa estratégica y el barrido tienen dos lotes de vuelo (CitySim y el corredor arbolado de TownSim, 25 corridas, 5 semillas por celda); sus umbrales son decisiones de diseño, no valores calibrados, y la comparación entre brazos con el sistema final (lotes D, E y F) está pendiente.
- El VLM no discrimina en primera persona. En el banco de prueba a bordo, LFM2.5-VL-1.6B no distinguió sectores volables de bloqueados (AUC 0.53) y declaró transitables todos los rumbos del barrido. Por eso la capa y el barrido usan la profundidad estimada; el VLM aporta solo en el plan de ruta sobre el mapa cenital (AUC 0.81).
- Punto ciego del foco de expansión. El flujo óptico traslacional se anula en los obstáculos centrados en la trayectoria. Las señales de contacto, la detección de atasco y la profundidad estimada del lazo lento lo mitigan; la solución estructural sigue siendo un canal de profundidad instantánea (Anexo 7).
- El TTC no es confiable durante el propio escape.
FRENARno produce traslación (sin flujo → sin FOE → sin evidencia) y el ascenso puro produce flujo vertical con FOE espurio. Por eso las maniobras de escape se comprometen por duración y no se re-evalúan con el TTC. - La derotación no está validada para yaw fuerte. El dataset de calibración quedó casi todo
en el bin
|yaw_rate| ∈ [0, 0.05)rad/s. vyes siempre 0 en el guiado nominal. El desplazamiento es puramente frontal + yaw (estrategia car-like); no hay vuelo lateral coordinado.- El barrido depende de un único juicio, sin verificación cruzada. El watchdog y el esquema estricto cubren timeout y formato inválido, pero un juicio válido y erróneo se ejecuta igual: la red de seguridad es el resto del lazo, no una segunda opinión. Los errores de la profundidad estimada fueron conservadores en el banco (2 % de libres con un obstáculo a menos de 5 m).
Referencias
airsim-loop/GRAFO-DE-CONTROL.md— esta referencia, versionada junto al código.src/agents/graph.py— topología,DroneStatey reglas del lazo rápido (secciones 2, 3 y 6).src/agents/vlm_strategic.py— capa estratégica del VLM (sección 7).src/agents/deep_scan.py— barrido panorámico en marco mundo (sección 8).src/agents/stall_detector.py— señales de atasco (sección 6).- Yang et al. (2024), Depth Anything V2 — red de profundidad de la capa estratégica y del barrido; capítulo 6 del informe, §6.14.
- Informe, capítulo 5 — arquitectura del lazo táctico; capítulo 9 — modos de falla y mecanismos de contención.