diff --git a/app/autonomy/LocalizationMeasurements.js b/app/autonomy/LocalizationMeasurements.js index d5156419..d6c29fd2 100644 --- a/app/autonomy/LocalizationMeasurements.js +++ b/app/autonomy/LocalizationMeasurements.js @@ -296,8 +296,17 @@ export function buildGnssMeasurement(snapshot, sensorConfig, rng, state = {}) { } const outageProbability = Number(faults.outageProbability ?? 0); - const inOutage = state.inOutage || (outageProbability > 0 && rng.next() < outageProbability); - if (inOutage) { + // A missing or zero duration means the outage lasts to the end of the run. + const outageDurationNs = Math.max(0, Number(faults.outageDurationSec || 0)) * 1e9; + const outageEnded = state.inOutage + && state.outageEndNs != null + && snapshot.captureTimeNs >= state.outageEndNs; + const continuing = state.inOutage && !outageEnded; + const starting = !continuing && outageProbability > 0 && rng.next() < outageProbability; + if (continuing || starting) { + const outageEndNs = starting + ? (outageDurationNs > 0 ? snapshot.captureTimeNs + outageDurationNs : null) + : state.outageEndNs ?? null; return { measurement: { status: -1, @@ -309,7 +318,7 @@ export function buildGnssMeasurement(snapshot, sensorConfig, rng, state = {}) { positionCovarianceType: 0, noFix: true, }, - nextState: { ...state, multipath: nextMultipath, inOutage: true, lastCaptureTimeNs: snapshot.captureTimeNs }, + nextState: { ...state, multipath: nextMultipath, inOutage: true, outageEndNs, lastCaptureTimeNs: snapshot.captureTimeNs }, }; } @@ -338,7 +347,7 @@ export function buildGnssMeasurement(snapshot, sensorConfig, rng, state = {}) { positionCovarianceType: 2, noFix: false, }, - nextState: { ...state, multipath: nextMultipath, inOutage: false, lastCaptureTimeNs: snapshot.captureTimeNs, dropped: false }, + nextState: { ...state, multipath: nextMultipath, inOutage: false, outageEndNs: null, lastCaptureTimeNs: snapshot.captureTimeNs, dropped: false }, }; } diff --git a/app/simulation/sensors/SensorTypeRegistry.js b/app/simulation/sensors/SensorTypeRegistry.js index 9df88d2d..10162f06 100644 --- a/app/simulation/sensors/SensorTypeRegistry.js +++ b/app/simulation/sensors/SensorTypeRegistry.js @@ -174,6 +174,7 @@ const gnssRunFields = [ { label: "Position noise N (m)", path: ["calibration", "positionNoiseEnu", "y"], control: "number", min: 0, step: 0.001, advanced: true }, { label: "Dropout probability", path: ["calibration", "faults", "dropoutProbability"], control: "number", min: 0, max: 1, step: 0.001, advanced: true }, { label: "Outage probability", path: ["calibration", "faults", "outageProbability"], control: "number", min: 0, max: 1, step: 0.001, advanced: true }, + { label: "Outage duration (s, 0 = rest of run)", path: ["calibration", "faults", "outageDurationSec"], control: "number", min: 0, step: 0.1, advanced: true }, ]; const wheelOdometryRunFields = [ diff --git a/docs/run-manifests.md b/docs/run-manifests.md index d134a41c..664a97cb 100644 --- a/docs/run-manifests.md +++ b/docs/run-manifests.md @@ -342,7 +342,7 @@ appearance/analytic readback. | Sensor | Rate | Key calibration | Units | | --- | --- | --- | --- | | IMU | 100 Hz | `gravity`, per-axis `angularVelocityStdDev`, `linearAccelerationStdDev`, drift τ, saturation | rad/s, m/s² | -| GNSS | 10 Hz | `datum` (lat/lng/alt), `positionNoiseEnu`, `faults.dropoutProbability`, `faults.outageProbability`, multipath τ | deg, m | +| GNSS | 10 Hz | `datum` (lat/lng/alt), `positionNoiseEnu`, `faults.dropoutProbability`, `faults.outageProbability`, `faults.outageDurationSec` (0 or absent: outage lasts to the end of the run), multipath τ | deg, m | | Wheel odometry | 50 Hz | `wheelRadius`, `ticksPerRevolution`, `trackWidth`, `slipFactor`, pose/twist noise | m, ticks/rev | GNSS treats map/odom position as ENU offset from the manifest datum. Wheel odometry integrates an independent measured `odom → base_link` estimate; oracle truth on `/oracle/vehicle/odometry` remains isolated for scoring. diff --git a/tests/localization-sensors.test.js b/tests/localization-sensors.test.js index fe0a249b..d444c41b 100644 --- a/tests/localization-sensors.test.js +++ b/tests/localization-sensors.test.js @@ -113,6 +113,30 @@ test("WGS84 conversion returns datum at zero ENU offset", () => { assert.ok(Math.abs(converted.lng - datum.lng) < 1e-4); }); +test("GNSS outage ends after outageDurationSec and is permanent without it", () => { + const run = (faults) => { + const start = normalizeRunSensor({ type: "gnss", calibration: { faults: { ...faults, outageProbability: 1 } } }); + const after = normalizeRunSensor({ type: "gnss", calibration: { faults: { ...faults, outageProbability: 0 } } }); + const rng = new SeededRNG("gnss-outage-duration"); + let state = createMeasurementSeedState(start, rng); + const noFix = []; + // 10 Hz fixes. The outage starts on the first fix only. + for (let index = 1; index <= 20; index += 1) { + const snapshot = captureVehicleSnapshot(mockVehicle(), index * 100_000_000); + const result = buildGnssMeasurement(snapshot, index === 1 ? start : after, rng, state); + state = { ...state, ...result.nextState }; + noFix.push(result.measurement.noFix); + } + return noFix; + }; + // Starts at 0.1 s and ends at 0.6 s: fixes at 0.1 through 0.5 s have no fix. + const timed = run({ outageDurationSec: 0.5 }); + assert.deepEqual(timed.slice(0, 5), [true, true, true, true, true]); + assert.ok(timed.slice(5).every((value) => value === false)); + assert.ok(run({}).every((value) => value === true)); + assert.ok(run({ outageDurationSec: 0 }).every((value) => value === true)); +}); + test("GNSS dropout returns no sample and outage emits no-fix status", () => { const config = normalizeRunSensor({ type: "gnss",