Documento originalAbrir .md

FLIGHT-1S — resultados medidos

Nota añadida el 2026-09-10, después de v2. Nada de este documento se ha alterado: sus afirmaciones, sus hashes y su veredicto rojo son los de la corrida original, y lab/echo3_flight1s_report.json sigue reconstruyendo con 375 comprobaciones y cero hallazgos. El defecto de medición descrito en §4 se reparó en un protocolo aparte, con hash, lock, informe y evidencia propios: FLIGHT1S-V2-DESIGN.md y FLIGHT1S-V2-RESULTS.md. v2 también salió rojo, por dos causas nuevas y distintas.

Estado: ROJO. El banco se ejecutó entero y no cierra. Fecha: 2026-09-10. Diseño: FLIGHT1S-DESIGN.md. Informe: lab/echo3_flight1s_report.json · report_sha256 = 57ccea7f19ebc3b3851abe6faf9c5f1a25365be1f6181291c8e748414944e809. Auditoría: lab/echo3_flight1s_audit.json.

Ocho de los nueve grupos preregistrados están en verde y los 48 episodios volaron completos. flight1s_green=false porque el grupo disturbance falla en 2 de 24 episodios perturbados. El rojo se conserva; §4 dice qué variable concreta lo impide y qué haría falta, sin tocarla.


1. Qué se ejecutó

cd /home/roger/REALRXOS/RXos/echoai

PYTHONPATH=.. python3 -m echoai.nexus0.flight1s validate                                   # exit 0
PYTHONPATH=.. python3 -m echoai.nexus0.flight1s preflight --output lab/flight1s/runtime-lock.json
PYTHONPATH=.. python3 -m echoai.nexus0.flight1s bench --rehearsal --output lab/echo3_flight1s_report.json
PYTHONPATH=.. python3 -m echoai.nexus0.flight1s audit lab/echo3_flight1s_report.json

PYTHONPATH=.. python3 tests/test_flight1s.py           # 24 tests, sin backend
PYTHONPATH=.. python3 tests/test_flight1s_backend.py   # 13 tests, con PX4 real

Salidas reales:

preflight exit=0
bench     exit=1        # 3.807,2 s de reloj de pared, flight1s_green=false
audit     exit=1        # evidence_valid=true, pero el informe era rojo
tests/test_flight1s.py          Ran 24 tests in  39.4s  OK
tests/test_flight1s_backend.py  Ran 13 tests in 215.7s  OK

bench exige el lock congelado antes de medir y se niega con lock_stale si el código cambió desde que se congeló. audit devuelve 0 sólo si la evidencia reconstruye y el informe era verde; aquí reconstruye y el informe es rojo, así que devuelve 1. Son dos hechos distintos y el código los separa.


2. Runtime realmente usado

lab/flight1s/runtime-lock.json, lock_sha256 = d41c76a0d8d739cba690c21c3681a27ff223a9a4f516e0de16f2191607e2d818, code_sha256 = c4dd8fdb6f4f7dbcfb4e473f6ae8464a86def6cf58ba7dce0b0cf08b7ee3102c.

Pieza Fijación real
Autopiloto PX4-Autopilot v1.15.4, commit 99c40407ffd7ac184e2d7b4b293f36f10fe561ef
Modelos PX4/PX4-gazebo-models d754381a1cecdd7f17050acd72bf5bf1327bced6
Binario PX4 px4_sitl_default, SHA-256 05bc7c4cb8d882e93cb8743c24e9f1ee6a94aab2acb6657a1e340ca63600b6b9
Cuerpo x500 → x500_base, base_link 2,0 kg + 4 rotores de 16,077 g = 2,064 kg
Assets del cuerpo x500/model.sdf 76d8668c…, x500_base/model.sdf f0f04e43…, airframe 4001_gz_x500 4ffa85e8…
Simulador gz-sim 8.15.0, libsdformat 14.9.0, DARTsim 7.8.0 (imagen SIM-3 sin tocar)
Imagen echoai-flight1s-px4:v1.15.4 0911af28…, FROM echoai-sim3-gz:harmonic 97893d2e…

Composición, no sobrescritura: el lock de SIM-3 y su informe siguen intactos y reproducibles. --pull=never, --network=none y GZ_FUEL_STATIC_ONLY=1 en todos los episodios; el control de red lo comprueba desde dentro del propio contenedor de episodio (§5).


3. Calibración en A, publicada entera

La magnitud de la perturbación y las bandas son lo único que se elige en vez de declararse, así que se eligieron sólo en split A, con reglas fijadas antes de correr:

  • radio de recuperación = ceil(máximo horizontal del hover nominal en A, 50 mm);
  • una fuerza admisible debe empujar el cuerpo por encima de 2 × ese radio en los cuatro mundos A y no pasar de 25° de actitud;
  • entre las admisibles, la más pequeña.

Ronda 1 (1–4 N) — lab/flight1s/sweep-round1-1to4N.json, conservada en rojo:

F (mN) admisible excursión mín..máx (mm) actitud máx (mdeg) impulso (ppk)
1000 no 118,4 .. 156,4 2 975 1116, 890, 1014, 867
2000 no 170,6 .. 300,3 5 050 978, 1054, 980, 962
3000 no 264,9 .. 387,7 7 847 930, 936, 877, 990
4000 no 303,1 .. 529,9 10 567 1001, 892, 1007, 933

Ninguna admisible: ni 4 N movía el mundo A más tranquilo más de 303 mm, por debajo de 2 × 200 mm. No se tocó la regla; se amplió el rango de candidatos y se conservó la ronda 1. Ronda 2 (5–8 N), en lab/flight1s/sweep.json:

F (mN) admisible excursión mín..máx (mm) actitud máx (mdeg) impulso (ppk)
5000 sí 455,3 .. 596,5 12 816 1069, 903, 954, 941
6000 sí 613,7 .. 679,2 15 406 916, 987, 906, 916
8000 sí 816,1 .. 869,4 19 720 909, 930, 923, 1007

Elegida la más pequeña: 5 000 mN durante 600 ms. 36 episodios de A en total. Protocolo congelado en protocol.json, sha256 = ac1db6f9abf849f96ee3671e73ce63c116d62a43dc4d9c5184c4598e65c98229, con order_seed = 20260910. Ese hash es el freeze: el auditor rechaza un informe cuyas bandas no coincidan con esos bytes.


4. El rojo

grupo disturbance: FAIL  — 22 de 24 episodios perturbados cumplen
  perturbed-73e0dcb3241663a5-r1   excursión 389,4 mm   (mínimo 400)   impulso 963 ppk
  perturbed-ef7f22a49738b970-r2   excursión 291,7 mm   (mínimo 400)   impulso 957 ppk

En los dos episodios la fuerza sí se aplicó: una sola publicación de wrench y un cociente de impulso de 963 y 957 por mil, indistinguible del resto. Lo que no alcanzó el suelo preregistrado fue el desplazamiento, porque PX4 rechazó el empujón mejor en esos dos vuelos.

La variable concreta que impide cerrar es bands.min_disturb_excursion_mm = 400. Es un criterio de suficiencia del estímulo — existe para que la prueba no sea vacía — pero está redactado sobre un resultado de lazo cerrado: la fuerza es abierta y constante, y la excursión es lo que queda después de que el controlador reaccione. Un controlador mejor produce una excursión menor y por tanto acerca el estímulo a su propio suelo.

El muestreo de A también lo subestimó: cuatro episodios por nivel dieron una excursión mínima de 455 mm a 5 N; los 24 episodios del banco completo bajaron a 291,7 mm. n=4 no acotaba la dispersión.

No se ha cambiado el umbral, ni la fuerza, ni N, ni las bandas. Cualquier corrección exige un protocolo v2 con hash nuevo, una recalibración y el banco completo otra vez; esta evidencia se conserva. Lo que un v2 tendría que arreglar, dicho aquí y no aplicado: expresar la suficiencia del estímulo sobre la magnitud aplicada (impulso medido, que fue 874–1008 ppk en las 24) en vez de sobre el desplazamiento resultante, y calibrar con más de cuatro episodios por nivel.

Separación medida entre condiciones, que sí es limpia:

excursión nominal      n=24   92,0 .. 195,4 mm
excursión perturbada   n=24  291,7 .. 694,3 mm
hueco entre ambas                    96,3 mm, sin solape

5. Lo que sí quedó demostrado

Ocho grupos en verde, 48/48 episodios completos.

Grupo Medido
envelope 12/12 mundos SIM-3 admiten la envolvente del X500; margen de pared peor caso 2 500 mm
episodes 48/48 arman, despegan, mantienen la referencia, aterrizan y desarman
recovery 24/24 perturbados vuelven a 200 mm de la referencia y aguantan 3 s; 894 .. 6 154 ms, mediana 1 412 ms
repetition 24/24 parejas dentro de las tolerancias congeladas, con el mismo SDF efectivo
hygiene 48/48 sin regresiones de reloj, sin contacto no autorizado, aterrizadas a ≤10 mm de su pose de reposo, firma PX4 presente, un solo lock
controls 4/4
mutants 9/9
regression bancos NEXUS-0 y SIM-3 en verde

Vuelo medido sobre los 48 episodios:

Magnitud min p50 max
Hover, error horizontal RMS (mm) 34,7 63,2 130,8
Hover, error horizontal máx (mm) 53,8 121,4 211,5
Hover, error vertical máx (mm) 42,7 104,3 276,2
Desvío de altitud respecto a la referencia (mm) −444,1 −39,0 +468,1
Excursión perturbada (mm) 291,7 497,9 694,3
Cociente de impulso perturbado (ppk) 874 957 1008
Tiempo de recuperación (ms) 894 1 412 6 154
Actitud máxima perturbada (mdeg) 12 158 12 634 13 182
Frecuencia efectiva de muestreo (Hz) 46,8 46,9 46,9
P99 de hueco entre muestras (ms) 22 22 23
Reloj de pared por episodio (s) 52,7 55,7 58,6
RTF 0,802 0,819 0,832

El desvío de altitud de ±450 mm respecto a la referencia de 1,5 m es real y se publica tal cual: es la altitud que EKF2 sostiene con el barómetro y el navsat simulados de este SITL. La estabilidad alrededor de donde se queda es otra cosa y es buena (RMS horizontal de 63 mm mediano). Las dos se miden por separado a propósito, para que un sesgo no se esconda dentro de una dispersión.

Que la fuerza fue física, no declarada

Tres vías independientes, y la tercera es la que decide:

  1. una sola publicación de wrench por episodio, sobre x500_0::base_link;
  2. el cambio de momento del cuerpo medido en el flujo de poses del backend contra F·Δt: 874–1008 por mil en los 24 episodios;
  3. el mutante que declara la fuerza y nunca publica el wrench: 15 por mil frente a 913 de su control positivo. Un factor de sesenta.

Controles

config_carries_no_room_identity   24 configuraciones; sólo varían spawn_pose y condition
no_network_during_bench           TCP "Network is unreachable", DNS "Temporary failure"
paired_nominal_vs_perturbed       101,9 mm frente a 524,9 mm en el mismo mundo y guion
world_hash_mismatch_refused       sdf_hash_mismatch antes de que vuele nada

Mutantes

Cada uno desde un control positivo que voló:

pose_teleported_or_replayed        sin ulog, sin armado, sin cambio de modo -> rechazado
fixture_controller_labelled_as_sitl  misma firma ausente -> rechazado
force_declared_without_wrench      impulso 15 ppk frente a 913 del control
fabricated_or_frozen_clock         el episodio no puede completarse: episode_timeout
branch_by_split                    el inspector de configuración lo ve
ground_truth_as_setpoint           idem, con station_mm
asset_download_during_bench        URLError: no hay ruta fuera del contenedor
positive_control_episode           vuela y se mide
report_level_mutants               los comprueba el auditor

6. Auditoría independiente

evidence_valid   true
checks           375
findings         []
recomputed       48 episodios

El auditor no lee flight1s_green. Regenera los doce mundos desde las semillas publicadas, re-hashea las 48 trazas y los 48 ulogs de PX4, recalcula por su cuenta el RMS del hover, el desvío de altitud, la excursión, el cociente de impulso y el estado de recuperación desde las muestras crudas, reconstruye la permutación de condiciones desde order_seed, y comprueba que el denominador sea 48 con 24 por condición y 4 por mundo.

--self-test muta copias del informe y exige que cada una sea rechazada, partiendo de un control que aceptó:

positive_control                aceptado
omitted_red_episode             rechazado   episode_count, episodes_per_condition
relaxed_band_in_report          rechazado   protocol_bands
two_locks_across_episodes       rechazado   single_runtime_lock
rewritten_trace_hash            rechazado   trace_sha/…
rewritten_ulog_hash             rechazado   ulog_sha/…
altered_measurement             rechazado   recompute/…/hover_horiz_rms_mm
green_without_groups            rechazado   flight1s_green_consistent
condition_order_rewritten       rechazado   condition_order_from_seed
wrong_code_hash                 rechazado   code_sha256

Un informe rojo con evidencia perfectamente reconstruible es exactamente el aspecto de un fallo honesto.


7. Cómo está construido

supervisor (host)   mundo efectivo, pose de spawn, guion congelado, orden por seed
      │  un contenedor por episodio, --network=none --pull=never
      ▼
  flightrunner.py   único proceso que habla con Gazebo y con PX4
      ├── gz sim -s -r --headless-rendering  (mundo SIM-3 + sistemas fijos)
      ├── PX4 SITL con PX4_GZ_STANDALONE=1: crea su propio cuerpo por el backend
      └── driver MAVLink: MIS_TAKEOFF_ALT, AUTO.TAKEOFF, armar, aterrizar, desarmar

Lo único que varía entre los 24 configs que cruzan al contenedor es spawn_pose —la columna libre validada del propio mundo— y condition, la variable manipulada. Ni split, ni seed, ni package_id, ni roles, ni estación.

PX4 estabiliza y mezcla actuadores; el driver sólo fija la referencia. Dos hallazgos de implementación, registrados porque costaron episodios:

  • sin <magnetic_field> en el mundo, PX4 simula su magnetómetro sobre un campo que no existe, EKF2 se queda sin referencia de rumbo y el cuerpo arma y se desarma solo diez segundos después con «Yaw estimate error». El valor es el de PX4 y es idéntico en los doce mundos;
  • MAV_CMD_NAV_TAKEOFF a secas se acepta y no despega en esta build. Lo que manda el despegue es el cambio a AUTO.TAKEOFF seguido del armado;
  • /world/<w>/wrench/persistent suma una wrench por mensaje. Publicarla en el bucle de muestreo acumulaba kilonewtons y lanzaba el cuerpo 54 m. Se publica exactamente una vez y se limpia al cerrar la ventana.

El mundo fuente es dato y la copia de ejecución lleva código: se hashean por separado, y el perfil sim3-boxes-v1 rechaza la copia efectiva. Los bytes de los doce paquetes SIM-3 no se han tocado.


8. Lo que esto no demuestra

  • Software in the loop y nada más: no hay aerodinámica realista, viento real, autonomía, seguridad universal ni sim-to-real.
  • echoAI está desconectado: agent_in_gazebo=false, cortex_calls=0, wsp_writes=0. Ningún sensor simulado se publica como evidencia ni se traduce a WSP. Eso es SENSOR-1 y GROUND-1.
  • PX4 conserva estabilización y mezcla; la interfaz de intención y los failsafes son PX4-1 y SAFE-1.
  • La velocidad, y por tanto el impulso, es una diferencia finita del flujo de poses del backend, no un sensor de velocidad independiente.
  • La perturbación es una wrench constante. No es viento y no afirma nada atmosférico.
  • Los mundos son los paquetes rehearsal de SIM-3, revalidados para la envolvente. Son datos públicos de ensayo y no consumen ningún examen reservado; no existe C de campaña en este árbol.
  • Tiempos y RTF describen un host. No son energía ni autonomía.
  • hardware_status="not_available": no hay aeronave y no hay Akida.

9. Estado

FLIGHT-1S no cierra. Ocho grupos en verde, 48 episodios volados, evidencia auditada y reconstruible, y un criterio preregistrado incumplido en 2 de 24 episodios. Para cerrarlo hace falta un protocolo v2 con hash nuevo, una recalibración con más episodios por nivel y el banco completo repetido — no un umbral movido.

Vista previa

Abrir original