a-b-street/abstreet · error
Traffic signal assignment for
Error message
Traffic signal assignment for {} broken. Missing {:?}, contains irrelevant {:?} What it means
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.
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
Example fix
// before
stages[0].protected_movements.insert(mov_id); // hand-picked stage
// after
// ensure every movement is assigned to some stage
for m in expected_movements.difference(&actual_movements) {
stages[0].protected_movements.insert(*m);
} Defensive patterns
Strategy: validation
Validate before calling
let actual: BTreeSet<MovementID> = signal.stages.iter()
.flat_map(|s| s.protected_movements.iter().chain(s.yield_movements.iter()))
.cloned().collect();
let expected: BTreeSet<MovementID> = intersection.movements.keys().cloned().collect();
assert_eq!(expected, actual, "signal does not cover movements exactly"); Type guard
fn covers_all_movements(signal: &TrafficSignal, i: &Intersection) -> bool {
let actual: BTreeSet<MovementID> = signal.stages.iter()
.flat_map(|s| s.protected_movements.union(&s.yield_movements).cloned())
.collect();
actual == i.movements.keys().cloned().collect()
} Try / catch
if let Err(e) = signal.validate(i) {
signal = TrafficSignal::new_default(i); // regenerate and retry
} Prevention
- 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
When it happens
Trigger: 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).
Common situations: 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.
Understand the failure class
Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.
Related errors
- Traffic signal has conflicting protected movements in one…
- Traffic signal does not allow enough time in stage to…
- {}
- Some trip has negative departure time
- CityName::new( , ) has a country code that isn't two letters
AI-assisted analysis of a-b-street/abstreet@0964f29315 (2026-09-13).
Data as JSON: /api/errors/47e028e5e7ec8b43.
Report an issue: GitHub.
Appendix: source
Thrown at map_model/src/objects/traffic_signals.rs:94
max_distance = max_distance.max(i.movements[movement].geom.length());
}
}
let time = max_distance / CROSSWALK_PACE;
assert!(time >= Duration::ZERO);
// Round up because it is converted to a usize elsewhere
Duration::seconds(time.inner_seconds().ceil())
}
pub fn validate(&self, i: &Intersection) -> Result<()> {
// Does the assignment cover the correct set of movements?
let expected_movements: BTreeSet<MovementID> = i.movements.keys().cloned().collect();
let mut actual_movements: BTreeSet<MovementID> = BTreeSet::new();
for stage in &self.stages {
actual_movements.extend(stage.protected_movements.iter());
actual_movements.extend(stage.yield_movements.iter());
}
if expected_movements != actual_movements {
bail!(
"Traffic signal assignment for {} broken. Missing {:?}, contains irrelevant {:?}",
self.id,
expected_movements
.difference(&actual_movements)
.cloned()
.collect::<Vec<_>>(),
actual_movements
.difference(&expected_movements)
.cloned()
.collect::<Vec<_>>()
);
}
for (stage_index, stage) in self.stages.iter().enumerate() {
// Do any of the priority movements in one stage conflict?
for m1 in stage.protected_movements.iter().map(|m| &i.movements[m]) {
for m2 in stage.protected_movements.iter().map(|m| &i.movements[m]) {
if m1.conflicts_with(m2) {
bail!(View on GitHub (pinned to 0964f29315)