{"record":{"id":"47e028e5e7ec8b43","repo":"a-b-street/abstreet","slug":"traffic-signal-assignment-for-broken-missing-contains","errorCode":null,"errorMessage":"Traffic signal assignment for {} broken. Missing {:?}, contains irrelevant {:?}","messagePattern":"Traffic signal assignment for (.+?) broken\\. Missing (.+?), contains irrelevant (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"map_model/src/objects/traffic_signals.rs","lineNumber":94,"sourceCode":"                max_distance = max_distance.max(i.movements[movement].geom.length());\n            }\n        }\n        let time = max_distance / CROSSWALK_PACE;\n        assert!(time >= Duration::ZERO);\n        // Round up because it is converted to a usize elsewhere\n        Duration::seconds(time.inner_seconds().ceil())\n    }\n\n    pub fn validate(&self, i: &Intersection) -> Result<()> {\n        // Does the assignment cover the correct set of movements?\n        let expected_movements: BTreeSet<MovementID> = i.movements.keys().cloned().collect();\n        let mut actual_movements: BTreeSet<MovementID> = BTreeSet::new();\n        for stage in &self.stages {\n            actual_movements.extend(stage.protected_movements.iter());\n            actual_movements.extend(stage.yield_movements.iter());\n        }\n        if expected_movements != actual_movements {\n            bail!(\n                \"Traffic signal assignment for {} broken. Missing {:?}, contains irrelevant {:?}\",\n                self.id,\n                expected_movements\n                    .difference(&actual_movements)\n                    .cloned()\n                    .collect::<Vec<_>>(),\n                actual_movements\n                    .difference(&expected_movements)\n                    .cloned()\n                    .collect::<Vec<_>>()\n            );\n        }\n        for (stage_index, stage) in self.stages.iter().enumerate() {\n            // Do any of the priority movements in one stage conflict?\n            for m1 in stage.protected_movements.iter().map(|m| &i.movements[m]) {\n                for m2 in stage.protected_movements.iter().map(|m| &i.movements[m]) {\n                    if m1.conflicts_with(m2) {\n                        bail!(","sourceCodeStart":76,"sourceCodeEnd":112,"githubUrl":"https://github.com/a-b-street/abstreet/blob/0964f29315820c91b171b585eb51e300164e9197/map_model/src/objects/traffic_signals.rs#L76-L112","documentation":"TrafficSignal::validate compares the set of MovementIDs covered by the signal's stages (protected + yield movements) against the expected complete set of movements at the intersection. If the assignment misses required movements or includes movements that should not be signaled, it bails during import so a broken signal never enters the map.","triggerScenarios":"An auto-generated or hand-edited signal assignment where stages collectively do not exactly cover every intersection movement: a movement is omitted from all stages, or a movement appears in a stage that expected_movements doesn't include (e.g. after road edits changed the movement set but the signal wasn't regenerated).","commonSituations":"Importing a map whose traffic-signal assignment file was produced before roads were added/removed at the intersection; manual signal editing that forgot to cover a new turn; upstream algorithm changes altering movement generation.","solutions":["Regenerate the signal assignment for the intersection so stages cover exactly the current movement set","Diff expected_movements vs actual to see which MovementIDs are missing/extra and fix those stages","Re-run the import after any road edit so signals are rebuilt for changed intersections","If hand-tuned signals are stored, re-validate them against the new map and update manually"],"exampleFix":"// before\nstages[0].protected_movements.insert(mov_id); // hand-picked stage\n// after\n// ensure every movement is assigned to some stage\nfor m in expected_movements.difference(&actual_movements) {\n    stages[0].protected_movements.insert(*m);\n}","handlingStrategy":"validation","validationCode":"let actual: BTreeSet<MovementID> = signal.stages.iter()\n    .flat_map(|s| s.protected_movements.iter().chain(s.yield_movements.iter()))\n    .cloned().collect();\nlet expected: BTreeSet<MovementID> = intersection.movements.keys().cloned().collect();\nassert_eq!(expected, actual, \"signal does not cover movements exactly\");","typeGuard":"fn covers_all_movements(signal: &TrafficSignal, i: &Intersection) -> bool {\n    let actual: BTreeSet<MovementID> = signal.stages.iter()\n        .flat_map(|s| s.protected_movements.union(&s.yield_movements).cloned())\n        .collect();\n    actual == i.movements.keys().cloned().collect()\n}","tryCatchPattern":"if let Err(e) = signal.validate(i) {\n    signal = TrafficSignal::new_default(i); // regenerate and retry\n}","preventionTips":["Regenerate signal assignments after every road edit that changes an intersection's movements","Never hand-edit stage movement sets without re-running the full expected-vs-actual comparison","Keep signal assignment files tied to the map version that produced them"],"tags":["traffic-signals","validation","import"],"backgroundTag":"schema-validation-failed","analyzedSha":"0964f29315820c91b171b585eb51e300164e9197","analyzedAt":"2026-09-13T18:02:03.421Z","contentChangedAt":"2026-09-13T18:02:03.421Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}