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

  1. Demote one of the conflicting movements to yield_movements (or a later stage) instead of protecting both in one stage
  2. Split the stage into two sequential stages, each protecting one side of the conflict
  3. Regenerate the default signal assignment for the intersection
  4. 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

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


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)