{"record":{"id":"cefeaf9f3b8327e2","repo":"a-b-street/abstreet","slug":"traffic-signal-has-conflicting-protected-movements-in-one","errorCode":null,"errorMessage":"Traffic signal has conflicting protected movements in one stage:\n{:?}\n\n{:?}","messagePattern":"Traffic signal has conflicting protected movements in one stage:\n(.+?)\n\n(.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"map_model/src/objects/traffic_signals.rs","lineNumber":112,"sourceCode":"            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!(\n                            \"Traffic signal has conflicting protected movements in one \\\n                             stage:\\n{:?}\\n\\n{:?}\",\n                            m1,\n                            m2\n                        );\n                    }\n                }\n            }\n\n            // Do any of the crosswalks yield?\n            for m in stage.yield_movements.iter().map(|m| &i.movements[m]) {\n                // TODO Maybe make UnmarkedCrossing yield\n                assert!(!m.turn_type.pedestrian_crossing())\n            }\n            // Is there enough time in each stage to walk across the crosswalk\n            let min_crossing_time = self.get_min_crossing_time(stage_index, i);\n            if stage.stage_type.simple_duration() < min_crossing_time {\n                bail!(","sourceCodeStart":94,"sourceCodeEnd":130,"githubUrl":"https://github.com/a-b-street/abstreet/blob/0964f29315820c91b171b585eb51e300164e9197/map_model/src/objects/traffic_signals.rs#L94-L130","documentation":"TrafficSignal::validate checks that no two protected movements within a single stage conflict geometrically (e.g. left turn crossing opposing through traffic). A stage that simultaneously protects conflicting movements would allow a collision, so import bails immediately.","triggerScenarios":"A stage's protected_movements contains two movements m1, m2 where i.movements[m].conflicts_with() is true — typically after manually adding a protected movement to an existing stage, or an auto-generator bug assigning opposing turns to the same stage instead of a yield.","commonSituations":"Hand-editing signal stages to 'speed up' a cycle by promoting yields to protected; importing third-party signal data that assumes different conflict rules; movement geometry changed after road edits making previously-safe pairings conflict.","solutions":["Demote one of the conflicting movements to yield_movements (or a later stage) instead of protecting both in one stage","Split the stage into two sequential stages, each protecting one side of the conflict","Regenerate the default signal assignment for the intersection","Re-run import after road geometry edits so conflict relationships are recomputed"],"exampleFix":"// before\nstage.protected_movements.insert(left_turn);\nstage.protected_movements.insert(opposing_through); // conflicts\n// after\nstage.protected_movements.insert(left_turn);\nstage.yield_movements.insert(opposing_through);","handlingStrategy":"validation","validationCode":"for stage in &signal.stages {\n    for m1 in &stage.protected_movements {\n        for m2 in &stage.protected_movements {\n            assert!(!i.movements[m1].conflicts_with(&i.movements[m2]));\n        }\n    }\n}","typeGuard":"fn stage_is_conflict_free(stage: &Stage, i: &Intersection) -> bool {\n    stage.protected_movements.iter().all(|m1|\n        stage.protected_movements.iter().all(|m2|\n            m1 == m2 || !i.movements[m1].conflicts_with(&i.movements[m2])))\n}","tryCatchPattern":"if let Err(e) = signal.validate(i) {\n    eprintln!(\"conflicting stage, demoting yields: {e}\");\n    demote_conflicts_to_yield(&mut signal, i);\n}","preventionTips":["Promote a yield movement to protected only after checking conflicts_with against every other protected movement in the stage","Prefer adding a new stage over stuffing more protected turns into an existing stage","Re-run conflict validation after any geometry or movement-set change"],"tags":["traffic-signals","validation","safety"],"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"}