a-b-street/abstreet · error
Traffic signal has conflicting protected movements in one…
Error message
Traffic signal has conflicting protected movements in one stage:
{:?}
{:?} What it means
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.
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
Example fix
// before stage.protected_movements.insert(left_turn); stage.protected_movements.insert(opposing_through); // conflicts // after stage.protected_movements.insert(left_turn); stage.yield_movements.insert(opposing_through);
Defensive patterns
Strategy: validation
Validate before calling
for stage in &signal.stages {
for m1 in &stage.protected_movements {
for m2 in &stage.protected_movements {
assert!(!i.movements[m1].conflicts_with(&i.movements[m2]));
}
}
} Type guard
fn stage_is_conflict_free(stage: &Stage, i: &Intersection) -> bool {
stage.protected_movements.iter().all(|m1|
stage.protected_movements.iter().all(|m2|
m1 == m2 || !i.movements[m1].conflicts_with(&i.movements[m2])))
} Try / catch
if let Err(e) = signal.validate(i) {
eprintln!("conflicting stage, demoting yields: {e}");
demote_conflicts_to_yield(&mut signal, i);
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
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 assignment for
- Traffic signal does not allow enough time in stage to…
- CityName::new( , ) has a country code that isn't two letters
- Dropdown has default_value , but none of the choices match…
- Bad CityName
AI-assisted analysis of a-b-street/abstreet@0964f29315 (2026-09-13).
Data as JSON: /api/errors/cefeaf9f3b8327e2.
Report an issue: GitHub.
Appendix: source
Thrown at map_model/src/objects/traffic_signals.rs:112
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!(
"Traffic signal has conflicting protected movements in one \
stage:\n{:?}\n\n{:?}",
m1,
m2
);
}
}
}
// Do any of the crosswalks yield?
for m in stage.yield_movements.iter().map(|m| &i.movements[m]) {
// TODO Maybe make UnmarkedCrossing yield
assert!(!m.turn_type.pedestrian_crossing())
}
// Is there enough time in each stage to walk across the crosswalk
let min_crossing_time = self.get_min_crossing_time(stage_index, i);
if stage.stage_type.simple_duration() < min_crossing_time {
bail!(View on GitHub (pinned to 0964f29315)