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
Fuente: GRAFO-DE-CONTROL.md Módulo: airsim-loop LangGraph · Depth Anything V2 · LFM2.5-VL-1.6B (plan de ruta) · Cosys-AirSim · UE 5.5

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:

  1. 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.
  2. 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.
  3. 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).
  4. Sin profundidad del simulador. Ni el grafo, ni main.py, ni experiments/runner.py leen el canal de profundidad de AirSim, ni para control ni como métrica. Toda la percepción del lazo rápido sale de ObstacleField (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

capture degraded _router perception ObstacleField navigate StallDetector · 8 reglas reactive · evasive · GIRAR_90 · deadlock degraded_hover FRENAR, sin percepción motor gobernador · 1 cmd/ciclo END ok degradado capa estratégica (VLM) hilo asíncrono · 3–6 s por consulta frame + pose (ancla) respuesta → sub-meta WaypointTracker lazo externo · inject_corner Lazo rápido: 5 Hz, único que comanda velocidades. Lazo lento (teal): propone sub-metas en coordenadas del mundo; nunca comanda velocidades.
Cuatro nodos y una arista condicional. El único punto de decisión del grafo es 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.
Grafo de control
Figura del informe (cap. 5, Figura 5.1). Lazo rápido a la izquierda, lazo lento del VLM a la derecha.
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:

Regla de declaració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.

Auditoría DistMin

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: un vz = 0 reemitido es un controlador de velocidad, no de altura; el motor corrige la deriva hacia la altitud de anclaje (zona muerta 0.3 m, Kp 0.35, tope 0.8 m/s).
  • Gobernador de velocidad: sin evidencia perceptual o tras un bloqueo reciente, limita vx de MANTENER_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.

Fuente por defecto: profundidad

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

  1. 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.
  2. 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). Con deep_vlm, una sola consulta con las imágenes numeradas (≤ 256 px). Para cada una, el modelo indica tipo (libre, fachada, muro, vegetación, interior, indeterminado), ok (se puede volar recto 15 m) y conf. El modelo nunca reporta ángulos.
  3. 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.
  4. 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.
  5. Sin rumbo transitable: GANAR_ALTURA (con el VLM, PERDER_ALTURA si 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.

Por qué subir

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

VariableValorEfecto
LOOP_HZ5.0Frecuencia del lazo (200 ms por ciclo).
TTC_EVASION_THRESHOLD3.2 sPeligro frontal crítico (Youden, τ = 2 s; AUC 0.97).
TTC_SAFE_THRESHOLD4.6 sVentana de advertencia (Youden, τ = 3 s; AUC 0.96).
FOV_BLOCKED_THRESHOLD0.6Fracción bloqueada a partir de la cual se gira 90° en vez de evadir.
OBSTACLE_OCCUPANCY_BLOCKED0.011Ocupación a partir de la cual un sector se considera bloqueado.
OPTICAL_MIN_ALT_M4.5 mPiso óptico: debajo, ni evasión por flujo ni señales de deadlock.
REACTIVE_FORWARD_SPEED3.0 m/sVelocidad de crucero del guiado.
WAYPOINT_ACCEPTANCE_RADIUS3.5 mRadio para dar un objetivo por alcanzado.
GOV_ENABLEDtrueGobernador de velocidad del motor.
TAKEOFF_VERTICAL / TAKEOFF_ALT_TOL_Mtrue / 1.0 mDespegue vertical hasta la altitud del primer objetivo antes de avanzar.
DISTMIN_TRACK_PERIOD_S1.0 sPeríodo del hilo de auditoría de DistMin (fuera del lazo).

Atasco y escape

VariableValorEfecto
STOPPED_DELIBERATIVE_CYCLES / STOPPED_MIN_CMD_MPS10 / 0.30Dron trabado: ciclos con avance ordenado sin movimiento real.
WP_NO_PROGRESS_THRESHOLD / WP_NO_PROGRESS_MIN_M50 / 2.0 mSin progreso: ciclos sin mejorar la distancia al objetivo actual.
DEADLOCK_STRATEGYdepthdepth, deep_vlm o blind; cualquier otro valor aborta el arranque.
SCAN_HEADING_COUNT_DEEP / SCAN_SETTLE_CYCLES_DEEP4 / 2Rumbos del barrido y ciclos de asentamiento por rumbo.
SCAN_ROT_TIMEOUT_CYCLES10Rotación imposible: el barrido se abandona.
SLM_DEEP_WATCHDOG_MS / SLM_DEEP_HTTP_TIMEOUT_S18 000 / 20 sWatchdog del barrido; siempre menor que el corte HTTP.
CORNER_OFFSET_M15 mDesvío determinista del brazo fsm (el barrido usa VLM_SUBGOAL_DIST_M).
MAX_ESCAPE_ALT_M30 mPor encima, el escape determinista es GIRAR_90.
FREEZE_CYCLES / FREEZE_RECOVERY25 / abortVigilante de congelamiento (physics_locked).

Capa estratégica del VLM

VariableValorEfecto
STRATEGIC_SOURCEdepthFuente de la capa estratégica: depth, vlm u off.
DEPTH_MODELDA-V2 Metric Outdoor SRed de profundidad métrica (~25 M parámetros, ~440 MiB de VRAM).
DEPTH_FREE_M / DEPTH_STRATEGIC_PERIOD_S15 m / 1.0 sUmbral de sector libre (p5 de la profundidad) y período de la capa con la profundidad.
LOCAL_LLM_MODEL_NAMElfm2.5-vl-1.6b:q4km-ctx8kModelo servido por Ollama local en la misma GPU (plan de ruta; capa y barrido si se eligen vlm/deep_vlm).
VLM_STRATEGIC_ENABLEDtrueHabilita la capa estratégica.
VLM_STRATEGIC_PERIOD_S3.0 sPeríodo mínimo entre consultas con el VLM.
VLM_STRATEGIC_MAX_AGE_S10 sEdad máxima de una respuesta para aplicarla.
CAMERA_HFOV_DEG90°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_M15 / 4 mDistancia de la sub-meta desde el ancla y mínima por delante del dron.
VLM_MAX_DZ_M / VLM_SUBGOAL_MIN_ALT_M4 / 6 mCambio máximo de altitud de una sub-meta y altitud mínima.
VLM_NEAR_WP_M8 mSin consultas ni barridos a menos de esta distancia del waypoint real.
VLM_STRATEGIC_MAX_GOAL_OFF_DEG40°Azimut máximo de la meta para consultar; rige el recorte cuadrado (±33.7°).
SLM_MIN_ALT_M8 mAltura mínima para consultar.
SUBGOAL_DEDUP_M10 mUna 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
Dónde corren

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. FRENAR no 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.
  • vy es 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