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.jsonsigue 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:
- una sola publicación de wrench por episodio, sobre
x500_0::base_link; - 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; - 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_TAKEOFFa secas se acepta y no despega en esta build. Lo que manda el despegue es el cambio aAUTO.TAKEOFFseguido del armado;/world/<w>/wrench/persistentsuma 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.