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

  1. Regenerate the signal assignment for the intersection so stages cover exactly the current movement set
  2. Diff expected_movements vs actual to see which MovementIDs are missing/extra and fix those stages
  3. Re-run the import after any road edit so signals are rebuilt for changed intersections
  4. 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

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


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)