a-b-street/abstreet · error

This intersection didn't already group major/minor roads…

Error message

This intersection didn't already group major/minor roads together.

What it means

After rewriting the two stages, adjust_major_minor_timing requires the original simple_cycle_duration to equal major + minor — i.e. the signal previously grouped all major-road time in one stage and all minor time in the other. If the total doesn't match, the signal wasn't in the expected major/minor grouped form, so it bails (the method restores `orig` semantics by failing before committing unclear changes).

Solutions

  1. Pass major/minor values matching the signal's current simple_cycle_duration (read it first and derive the split)
  2. Reset the signal to the default generated assignment before applying major/minor timing
  3. Verify the two stages actually correspond to major and minor road groups
  4. Log simple_cycle_duration and adjust the requested values accordingly

Example fix

// before
signal.adjust_major_minor_timing(Duration::seconds(30), Duration::seconds(30), map)?;
// after
let total = signal.simple_cycle_duration();
let major = total * 2 / 3;
let minor = total - major;
signal.adjust_major_minor_timing(major, minor, map)?;
Defensive patterns

Strategy: validation

Validate before calling

let total = signal.simple_cycle_duration();
if total != major + minor {
    return Err(anyhow!("cycle {} != major {} + minor {}", total, major, minor));
}

Type guard

fn matches_cycle(signal: &TrafficSignal, major: Duration, minor: Duration) -> bool {
    signal.simple_cycle_duration() == major + minor
}

Try / catch

if matches_cycle(signal, major, minor) {
    signal.adjust_major_minor_timing(major, minor, map)?;
} else {
    let (m, n) = split_current_cycle(signal, map);
    signal.adjust_major_minor_timing(m, n, map)?;
}

Prevention

When it happens

Trigger: Calling adjust_major_minor_timing on a 2-stage fixed signal whose cycle duration != major + minor: e.g. durations previously set to arbitrary values, or stages don't correspond to major/minor grouping (extra crosswalk-only time in the cycle).

Common situations: Signals whose timings were previously hand-tuned individually; applying an assumed (major, minor) pair that doesn't match the existing cycle; crosswalk time altering simple cycle duration.

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/3474dd3433431b16. Report an issue: GitHub.

Appendix: source

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

        for stage in &mut self.stages {
            match stage.stage_type {
                StageType::Fixed(_) => {}
                _ => bail!("This intersection doesn't use fixed timing."),
            }
            // Ignoring crosswalks, do any of the turns come from a major road?
            if stage
                .protected_movements
                .iter()
                .any(|m| !m.crosswalk && highest_rank == rank_per_road[&m.from.road])
            {
                stage.stage_type = StageType::Fixed(major);
            } else {
                stage.stage_type = StageType::Fixed(minor);
            }
        }

        if self.simple_cycle_duration() != major + minor {
            bail!("This intersection didn't already group major/minor roads together.");
        }

        if self == &orig {
            bail!("This change had no effect.");
        }

        Ok(())
    }

    pub fn missing_turns(&self, i: &Intersection) -> BTreeSet<MovementID> {
        let mut missing: BTreeSet<MovementID> = i.movements.keys().cloned().collect();
        for stage in &self.stages {
            for m in &stage.protected_movements {
                missing.remove(m);
            }
            for m in &stage.yield_movements {
                missing.remove(m);
            }

View on GitHub (pinned to 0964f29315)