a-b-street/abstreet · error

This intersection doesn't have 2 stages.

Error message

This intersection doesn't have 2 stages.

What it means

TrafficSignal::adjust_major_minor_timing rewrites a two-stage signal so the major road gets `major` time and the minor road `minor` time. It requires exactly two stages; if the signal has any other number of stages the transformation is undefined and the method bails before touching anything.

Solutions

  1. Check signal.stages.len() == 2 before calling and skip non-2-stage signals
  2. Use a signal-retiming API suited to multi-stage signals instead
  3. Regenerate a simple 2-stage assignment first if a major/minor scheme is really appropriate
  4. Filter target intersections (e.g. minor 4-way stops) where the default 2-stage signal applies

Example fix

// before
signal.adjust_major_minor_timing(major, minor, map)?;
// after
if signal.stages.len() == 2 {
    signal.adjust_major_minor_timing(major, minor, map)?;
}
Defensive patterns

Strategy: validation

Validate before calling

if signal.stages.len() != 2 {
    return Err(anyhow!("signal {} has {} stages, expected 2", signal.id, signal.stages.len()));
}

Type guard

fn is_two_stage(signal: &TrafficSignal) -> bool {
    signal.stages.len() == 2
}

Try / catch

if is_two_stage(signal) {
    signal.adjust_major_minor_timing(major, minor, map)?;
} else {
    skip_and_log(signal.id);
}

Prevention

When it happens

Trigger: Calling adjust_major_minor_timing on a signal with 1, 3, or more stages — e.g. complex intersections with dedicated turn stages, or after a prior transformation changed the stage count.

Common situations: Applying a major/minor retiming script uniformly to all signals, some of which are multi-stage; editing an intersection that gained turn lanes and stages since the script was written.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of a-b-street/abstreet@0964f29315 (2026-09-13). Data as JSON: /api/errors/15e220a906c17125. Report an issue: GitHub.

Appendix: source

Thrown at map_model/src/objects/traffic_signals.rs:215

        if !has_all_walk {
            self.stages.push(all_walk_stage);
        }
        self != &orig
    }

    /// Modifies the fixed timing of all stages, applying either a major or minor duration,
    /// depending on the relative rank of the roads involved in the intersection. If this
    /// transformation couldn't be applied, returns an error. Even if an error is returned, the
    /// signal may have been changed -- so only call this on a cloned signal.
    pub fn adjust_major_minor_timing(
        &mut self,
        major: Duration,
        minor: Duration,
        map: &Map,
    ) -> Result<()> {
        if self.stages.len() != 2 {
            bail!("This intersection doesn't have 2 stages.");
        }

        // What's the rank of each road?
        let mut rank_per_road: BTreeMap<RoadID, usize> = BTreeMap::new();
        for r in &map.get_i(self.id).roads {
            rank_per_road.insert(*r, map.get_r(*r).get_detailed_rank());
        }
        let mut ranks: Vec<usize> = rank_per_road.values().cloned().collect();
        ranks.sort_unstable();
        ranks.dedup();
        if ranks.len() == 1 {
            bail!("This intersection doesn't have major/minor roads; they're all the same rank.");
        }
        let highest_rank = ranks.pop().unwrap();

        // Try to apply the transformation
        let orig = self.clone();
        for stage in &mut self.stages {

View on GitHub (pinned to 0964f29315)