SPEC — Uncertainty Model¶
- Estado: congelado en Fase 0
- ADRs principales: ADR-0008, ADR-0009, ADR-0010 (catálogo revisado y disciplina de acoplamiento de parámetros)
- Versión del contrato:
UNCERTAINTY_PROTOCOL_VERSION = 1
1. Responsabilidades¶
Este documento es el contrato vinculante para todo lo relacionado con incertidumbre en Project Ghost. Define:
- Los tipos canónicos que envuelven cualquier estimación que cruce una frontera de módulo (
Estimate[T],Validity,EstimateSource). - El catálogo de identificadores de modo de fallo (
PerceptionMode) y sus criterios cuantitativos por defecto. - Los modelos de inflación de covarianza que productores y estimadores deben aplicar al degradarse la entrada.
- Las reglas de composición cuando dos
Estimatese combinan. - Los helpers obligatorios en
core.uncertainty.
No es responsabilidad de este spec:
- Decidir la respuesta conductual por modo (eso es ADR-0009).
- Implementar VO, EKF o SLAM (eso es Fase 3+).
- Elegir el algoritmo concreto de detección de fallo perceptual.
2. Tipos congelados¶
class Validity(IntEnum):
INVALID = 0
STALE = 1
DEGRADED = 2
VALID = 3
# Orden total: VALID > DEGRADED > STALE > INVALID (mayor = mejor)
@dataclass(frozen=True)
class EstimateSource:
module_id: str # p.ej. "vo.front", "ekf.nav", "altimeter.gt"
kind: Literal["sensor", "filter", "vo", "slam", "groundtruth", "fused"]
schema_version: int
@dataclass(frozen=True)
class Estimate(Generic[T]):
value: T
covariance: np.ndarray | None
validity: Validity
stamp_sim_ns: int
source: EstimateSource
confidence: float | None = None
# Invariantes verificados en __post_init__:
# - covariance is None iff source.kind == "groundtruth"
# - covariance, si existe, es 2-D, simétrica, dtype float64
# - validity == VALID exige covariance dentro de envelope nominal del producer
@dataclass(frozen=True)
class NavUncertainty:
"""Envelope adjunto a `NavigationState`."""
validity: Validity
pos_sigma_m: np.ndarray # (3,) sigma marginal de posición ENU
vel_sigma_mps: np.ndarray # (3,) sigma marginal de velocidad
att_sigma_rad: np.ndarray # (3,) sigma marginal en tangente del cuaternión
horizon_ns: int # horizonte hasta el cual estas sigmas son válidas
age_ns: int # edad de la observación más antigua que contribuyó
class PerceptionMode(StrEnum):
NOMINAL = "nominal"
LOW_TEXTURE = "low_texture"
LOW_LIGHT = "low_light"
IMU_SATURATION = "imu_saturation"
VIO_LOST = "vio_lost"
MAP_AMBIGUOUS = "map_ambiguous"
MOTION_AGGRESSIVE = "motion_aggressive" # añadido en ADR-0010
PERCEPTION_DEAD = "perception_dead"
Los nombres del enum PerceptionMode son frozen. Añadir o renombrar requiere ADR que enmiende o supersede ADR-0008 (ver ADR-0010 como ejemplo de enmienda al catálogo). Modos considerados y rechazados (DUST, WATER_DROP_ON_LENS, HORIZON_GLARE, THERMAL_SHIMMER, EM_INTERFERENCE, MULTIPATH_VIO) están documentados en ADR-0010 §2 con su razón de rechazo.
3. Contratos vinculantes¶
- Wrapping obligatorio. Toda salida perceptual o de estimación que cruce una frontera de módulo es un
Estimate[T]. DevolverT"pelado" está prohibido fuera de implementación interna del productor. - Sealing recursivo de arrays. El constructor de
Estimateaplicaflags.writeable=Falseno solo avalueycovariancesi sonnp.ndarray, sino también recursivamente a cualquiernp.ndarrayaccesible por traversal de los campos devaluecuando este es una dataclass (consistente con ADR-0005). Productores que envuelvan tipos compuestos (p.ej.Estimate[Pose]) deben tener constructor que selle internamente; elEstimateverifica el sealing tras construcción y rechaza conValueErrorsi encuentra un array escribible. Esta regla cierra el agujero de sealing superficial identificado endocs/reviews/uncertainty_red_team_review.md§3.2. - Covarianza simétrica. Toda matriz
covariancecumple‖C − Cᵀ‖_F / ‖C‖_F < 1e-9. El constructor verifica y simetriza si la diferencia es menor que tolerancia; rechaza conValueErrorsi excede. - Covarianza semidefinida positiva. Toda matriz
covariancecumplemin(eig(C)) ≥ -eps_psd(coneps_psd = 1e-12). El constructor verifica; rechaza si falla. - Stamp del productor.
stamp_sim_nses el instante en que el productor terminó de producir, no el de consumo. El consumidor calculaage_ns = now - stamp_sim_nsy aplica §4 si es relevante. validity == VALIDexige covarianza dentro de envelope nominal. Cada productor declara sunominal_covariance_envelopeen su spec/config; emitirVALIDcon covarianza fuera del envelope es bug.- Sin upgrade silencioso. En cualquier composición de
Estimate, lavalidityde salida es el mínimo (más restrictivo) de las entradas. Solo un productor puede emitir unVALIDoriginal. - Groundtruth tiene covariance None. Por construcción
kind == "groundtruth"implicacovariance is None. Cualquier otrokindconcovariance is Nonees bug. Esta asimetría hace explícito que GT no existe en hardware. confidenceno reemplaza covarianza. Si un productor solo exponeconfidence, debe traducirlo a una covarianza diagonal documentada antes de emitirEstimate.- Validity y covariance son consistentes, no independientes. El productor no puede emitir simultáneamente
validity == VALIDcon covarianza inflada (fuera de envelope nominal), nivalidity == DEGRADEDcon covarianza nominal. El constructor verifica la consistencia: dado elvaliditydeclarado y elnominal_covariance_envelopedeclarado por el productor (víaEstimateSource.module_id), la covarianza debe estar dentro del rango esperado para ese nivel de validez (nominal paraVALID; inflada por §5 paraDEGRADED/STALE). Inconsistencias rechazadas conValueError. Esta regla cierra el agujero identificado endocs/reviews/uncertainty_red_team_review.md§3.1.
4. Reglas de envejecimiento (STALE)¶
Cada EstimateSource declara un max_age_ns aceptable para su canal. Reglas:
| Edad | Validity efectiva |
|---|---|
age ≤ max_age_ns |
sin cambio |
max_age_ns < age ≤ 3 · max_age_ns |
downgrade a STALE; aplicar inflación de §5.4 |
age > 3 · max_age_ns |
downgrade a INVALID; valor no usable |
Consumidores deben evaluar edad usando el SimClock activo, no time.time().
5. Modelos de inflación de covarianza¶
Cuando un productor emite validity in (DEGRADED, STALE), la covarianza no es la nominal: se infla según un modelo documentado. Los modelos por defecto son los siguientes; pueden ser sobreescritos por config pero no eliminados.
Estado de calibración de los valores numéricos de esta sección. Todos los parámetros (
α, factores direccionales,Q_sat,Q_dr) son valores iniciales por ingeniería de orden de magnitud, no calibrados. Vienen de literatura general de VIO/EKF y de intuición sobre PyBullet; no de un experimento. Se recalibran en U2 contra dataset PyBullet y en U6 contra hardware (verdocs/roadmaps/research_track_uncertainty.md). Hasta entonces, tratar cualquier número de esta sección como hipótesis. Cambios deben actualizar también los pares acoplados listados en ADR-0010 §3.
5.1 Inflación isotrópica (default para sensores escalares)¶
severity es producido por la lógica de detección (p.ej. (min_features - features) / min_features, clipado a [0, 1]). α es el factor de penalización, declarado por el productor; default α = 2.0.
5.2 Inflación direccional (default para VO/cámara)¶
VO degrada típicamente más en una dirección (la del eje óptico, o la dirección de menor textura). La inflación se aplica por eje:
donde R es la rotación de cámara a mundo y (s_x, s_y, s_z) son factores de inflación por eje (default (1.0, 1.0, 3.0) para eje óptico hacia delante en FLU).
5.3 Inflación dinámica (default para IMU bajo saturación)¶
Si un eje IMU está saturado, la covarianza del bias correspondiente crece linealmente con el tiempo de saturación:
con Q_sat = diag(5e-2, 5e-2, 5e-2) (m/s²)² / s para acelerómetro y Q_sat = diag(5e-3, 5e-3, 5e-3) (rad/s)² / s para giroscopio, hasta validity cae a STALE.
5.4 Inflación de stale (dead reckoning)¶
Para estimados marcados STALE, la covarianza crece con la edad según:
donde Q_dr por canal está en configs/uncertainty/dead_reckoning.yaml. Defaults (sigma equivalente al final del horizonte STALE):
| Canal | Q_dr por eje |
|---|---|
| Posición ENU | 0.5 (m/s)² |
| Velocidad | 0.2 (m/s²)² |
| Cuaternión tangente | 0.01 (rad/s)² |
Estos números son conservadores para PyBullet; en hardware se recalibran (ADR-0008 lo permite por config).
6. Reglas de composición¶
Dado Estimate[A] y Estimate[B] combinados en Estimate[C] (p.ej. fusión EKF, transformación entre marcos):
- Validity:
out.validity = min(in_a.validity, in_b.validity). - Source:
out.source.kind = "fused";out.source.module_ididentifica al fusor;out.source.schema_versiones el del fusor, no el de los inputs. - Stamp:
out.stamp_sim_ns = max(in_a.stamp_sim_ns, in_b.stamp_sim_ns). - Covarianza: computada por el método del fusor (Kalman update, transformación lineal con jacobiana, etc.). Si cualquier input es
DEGRADEDoSTALE, la covarianza del input correspondiente debe estar ya inflada según §5 antes de la composición. - Confidence: si ambos inputs lo proveen, se compone como producto; si solo uno lo provee, se descarta del output.
Composición con un Estimate.validity == INVALID es bug; debe detectarse y manejarse como fallo perceptual antes de invocar al fusor.
7. Catálogo de modos perceptuales — thresholds por defecto¶
Los nombres del catálogo están frozen en ADR-0008 y enmendados en ADR-0010 (8 modos). Los thresholds son defaults aplicados cuando una config no los sobreescribe. Esta tabla es la única fuente autoritativa para los defaults; configs scenario-específicas heredan estos valores cuando no los redefinen.
Estado de calibración: mismo disclaimer que §5. Todos los valores son hipótesis iniciales; calibración real es responsabilidad de U2/U6. Cualquier ajuste debe respetar la disciplina de acoplamiento de ADR-0010 §3 (mecanismo↔policy en revisión conjunta).
Doble condición de transición FSM. Cada modo declara dos parámetros que actúan conjuntamente para gobernar las transiciones:
window_ms(hold temporal) yk_consecutive(número de muestras consecutivas dentro del envelope). Ambos deben cumplirse antes de declarar el modo activo o liberado. Esta doble condición cierra el agujero de oscilación bajo señal realista identificado endocs/reviews/uncertainty_red_team_review.md§2.3 y endocs/specs/perception.md§4. Detalles operativos de cómo elPerceptionModeDetectoraplica esta regla viven enperception.md§4.
perception_mode_defaults:
nominal_hold_ms: 200
nominal_k_consecutive: 6 # muestras seguidas dentro de envelope
low_texture:
min_features: 30
low_texture_window_ms: 500
low_texture_k_consecutive: 8
min_track_length: 5
severity_alpha: 2.0
low_light:
min_luminance: 0.05
low_light_window_ms: 1000
low_light_k_consecutive: 4
agc_at_max_gain_required: true # criterio original
agc_at_min_gain_required: true # añadido vía ADR-0010 §2 (subsume HORIZON_GLARE)
recovery_timeout_ms: 5000
slow_ascend_mps: 0.3
recovery_altitude_m: 2.0
imu_saturation:
saturation_threshold_frac: 0.90
saturation_window_ms: 50
saturation_k_consecutive: 3
recovery_threshold_frac: 0.70
kill_threshold_ms: 1000
vio_lost:
innovation_fail_count: 5
vio_timeout_ms: 200
vio_lost_k_consecutive: 5 # equivalente al innovation_fail_count para el detector
dr_hover_window_ms: 3000
dr_abort_covariance_pos_m: 5.0
map_ambiguous:
ambiguity_margin: 0.10
ambiguity_window_ms: 500
ambiguity_k_consecutive: 5
motion_aggressive: # añadido por ADR-0010 §1
aggressive_rate_threshold_rps: 3.0
aggressive_accel_threshold_mps2: 12.0
aggressive_window_ms: 200
aggressive_k_consecutive: 4
aggressive_recovery_timeout_ms: 2000
cap_factor: 0.6 # cap aplicado por T2; acoplado con threshold (ADR-0010 §3)
perception_dead:
descent_mps: 0.5
kill_altitude_m: 0.3
dead_k_consecutive: 4 # productores en INVALID consecutivo
8. Helpers obligatorios en core.uncertainty¶
def make_estimate(
value: T,
*,
covariance: np.ndarray | None,
validity: Validity,
stamp_sim_ns: int,
source: EstimateSource,
confidence: float | None = None,
) -> Estimate[T]:
"""Construye `Estimate[T]` aplicando sealing, simetrización y validaciones de §3."""
def inflate_isotropic(C: np.ndarray, severity: float, alpha: float = 2.0) -> np.ndarray: ...
def inflate_directional(C: np.ndarray, R_cam_world: np.ndarray, scales: np.ndarray) -> np.ndarray: ...
def inflate_stale(C: np.ndarray, age_ns: int, Q_dr: np.ndarray) -> np.ndarray: ...
def compose_validity(*validities: Validity) -> Validity: # min over inputs
def age_ns(estimate: Estimate[T], now_ns: int) -> int: ...
def downgrade_by_age(estimate: Estimate[T], now_ns: int, max_age_ns: int) -> Estimate[T]: ...
Toda implementación de estimador o productor consume estos helpers; reimplementar localmente es violación de spec.
9. Telemetría obligatoria¶
- Canal
/perception/mode:PerceptionModeChangedevent en cada transición, confrom,to,reason(cadena humana),producer_ids(qué productores contribuyeron),stamp_sim_ns. - Canal
/nav/uncertainty: muestreo deNavUncertaintya la misma tasa que/state/nav(50 Hz). Persistido en MCAP. - Canal
/perception/{producer_id}/validity: serie temporal deValiditypor productor; muestreado a la tasa nominal del productor.
Estos canales son obligatorios desde Fase 3 (cuando aparecen estimadores reales); en Fases 1–2 quedan declarados pero vacíos.
10. Restricciones¶
- Prohibido emitir
Estimate.validity == VALIDconcovariance == Nonesalvosource.kind == "groundtruth". - Prohibido pasar
np.randomglobal como fuente de ruido a cualquier productor (ADR-0002). Toda aleatoriedad viene deRandomSource.child(...). - Prohibido modificar la enumeración
PerceptionModesin ADR que enmiende o supersede ADR-0008 (precedente: ADR-0010). - Prohibido
time.time()otime.monotonic()en lógica de envejecimiento. SoloSimClock/SystemClockactivos. - Consumidores no pueden inferir
validitypor inspección del valor; deben leerestimate.validity. - Prohibido el uso de colecciones con orden de iteración inestable (
set,frozenset,dict.keys()sin ordenar explícitamente,collections.Counter) dentro de productores enperception/, fusores enstate/, y planners enmission/. El linterscripts/check_no_unstable_collections.py(a implementar en U1; especificación en este §) verifica la regla en CI y pre-commit. Mitiga el riesgo identificado endocs/reviews/uncertainty_red_team_review.md§3.3 sobre determinismo de RANSAC y similares; no lo elimina (algoritmos como RANSAC pueden romper determinismo por razones fuera del alcance del linter), pero cierra la mayor parte de la superficie. Excepciones por archivo con# noqa: stable-collectiony razón en comentario, revisable en code review. - Prohibido modificar un parámetro listado en ADR-0010 §3 (parámetros acoplados mecanismo↔policy) sin actualizar o justificar explícitamente el parámetro acoplado en el mismo PR.
11. Pruebas obligatorias¶
| Test | Cubre |
|---|---|
test_estimate_rejects_asymmetric_covariance |
§3.3 |
test_estimate_rejects_non_psd_covariance |
§3.4 |
test_estimate_seals_arrays_recursively |
§3.2 (sealing recursivo sobre dataclasses anidadas) |
test_estimate_rejects_validity_covariance_inconsistency |
§3.10 (VALID con covarianza inflada, DEGRADED con nominal) |
test_groundtruth_iff_covariance_none |
§3.8 |
test_compose_validity_is_min |
§6.1 |
test_stale_inflation_monotonic_in_age |
§5.4 |
test_isotropic_inflation_recovers_nominal_at_zero_severity |
§5.1 |
test_downgrade_by_age_thresholds |
§4 |
test_perception_mode_change_event_published |
§9 |
test_no_global_random_in_producers |
§10 + scripts/check_no_global_random.py |
test_no_unstable_collections_in_perception_state_mission |
§10 + scripts/check_no_unstable_collections.py |
test_motion_aggressive_entry_at_threshold |
ADR-0010 §1 + §7 |
test_fsm_no_oscillation_under_alternating_signal |
§7 + perception.md §4; señal sintética alternante a 0.45 × window_ms no produce más de 2 transiciones por minuto |
Cobertura objetivo del módulo core.uncertainty: > 90 %.