# MOBIL Lane-Change Model

## What Is MOBIL?

MOBIL stands for **Minimizing Overall Braking Induced by Lane Changes**. It is a lane-change decision model developed by Martin Treiber, Arne Kesting, and Dirk Helbing (2007) as a companion to the Intelligent Driver Model (IDM) for longitudinal car-following.

Where IDM controls _how fast_ a car goes, MOBIL controls _which lane_ it chooses. The two models are designed to work together: IDM supplies the acceleration estimates that MOBIL uses to judge whether a lane change is worthwhile.

The core insight is that a lane change is only rational when it is simultaneously:

1. **Safe** — the new follower behind us in the target lane does not have to brake harder than some maximum deceleration limit.
2. **Beneficial** — the net acceleration gain across all affected drivers (us, the new follower, the old follower we leave behind) exceeds a minimum threshold, weighted by a _politeness factor_ that controls how selfish the driver is.

This combination prevents both dangerous cuts and pointless "lane hopping" that creates turbulence without real speed gain.

---

## The IDM Backbone

MOBIL's incentive calculations are based purely on IDM acceleration, so the IDM model is worth understanding first.

### IDM Acceleration Formula

```
a = a_max × ( 1 − (v / v_max)⁴ − (s* / s)² )
```

| Symbol | Meaning |
|---|---|
| `a_max` | Maximum (comfortable) acceleration |
| `v` | Current speed |
| `v_max` | Desired free-road speed |
| `s` | Current bumper-to-bumper gap to the car ahead |
| `s*` | Desired safety gap (see below) |

The **desired safety gap** `s*` combines a minimum standstill gap, a desired time gap, and a braking term:

```
s* = s₀  +  v · T  +  v · Δv / (2 · √(a_max · b))
```

| Symbol | Meaning |
|---|---|
| `s₀` | Minimum gap at standstill (2 m) |
| `T` | Desired time headway (1.5 s) |
| `Δv` | Speed difference to the car ahead (`v_self − v_leader`) |
| `b` | Comfortable deceleration (3 m/s²) |

When `s >> s*` (open road), the IDM term `(s*/s)²` approaches zero and the car accelerates toward `v_max`. When `s ≈ s*`, the car follows at the safe distance. When `s < s*`, the term exceeds 1 and the car brakes.

In this application, IDM is implemented in `Car.getAcceleration()` ([js/model/Car.js](js/model/Car.js)), which adds a third term for stop-line braking at red lights.

### Pure IDM (No Stop-Line Term)

MOBIL needs to evaluate _hypothetical_ scenarios — "what would my acceleration be if I were in the other lane?" — without the stop-line braking distorting the comparison. A second method, `Car.getAccelerationGiven(speed, gap, leaderSpeed)`, provides this: same IDM formula, no intersection term, explicit gap and leader speed as inputs.

---

## The MOBIL Criteria

### Actors

Every MOBIL evaluation involves up to three drivers:

| Name | Who |
|---|---|
| **Subject** | The car considering a lane change |
| **New follower** | The car currently behind the subject in the _target_ lane |
| **Old follower** | The car currently behind the subject in the _current_ lane |

The subject's current leader and the target-lane leader are also needed to compute accelerations, but they are not "affected" — their situation does not change in the hypothetical.

### Safety Criterion

The lane change is rejected immediately if the new follower's hypothetical acceleration after the merge would fall below a hard deceleration limit:

```
a_new_follower_after_merge  ≥  −b_safe
```

`b_safe` is `Settings.mobil.safeDecel` (default 4 m/s²). This prevents cutting in front of a car and forcing it into emergency braking.

### Incentive Criterion

If the safety gate passes, the lane change proceeds only when the _weighted net gain_ exceeds a minimum threshold:

```
ΔA_subject  +  p × (ΔA_new_follower  +  ΔA_old_follower)  >  Δa_th
```

Where:

| Symbol | Meaning |
|---|---|
| `ΔA_subject` | Subject's gain: `accel_in_target − accel_in_current` |
| `ΔA_new_follower` | New follower's gain: `accel_after_merge − accel_before_merge` |
| `ΔA_old_follower` | Old follower's gain: `accel_after_we_leave − accel_before_we_leave` |
| `p` | Politeness factor `Settings.mobil.politeness` |
| `Δa_th` | Minimum gain threshold `Settings.mobil.threshold` |

The politeness factor `p` controls how much the driver cares about the traffic it affects:

- `p = 0` — purely selfish; only the subject's own gain matters.
- `p = 1` — fully cooperative; a lane change is only worthwhile if the whole system benefits.
- `p = 0.3` (default) — slightly selfish, typical of most drivers.

The threshold `Δa_th = 0.1 m/s²` prevents lane changes triggered by negligible or transient speed differences.

---

## Implementation in This Application

### File: `js/model/Car.js`

#### `getAccelerationGiven(speed, gap, leaderSpeed)`

Pure IDM evaluation with explicit inputs. Used by MOBIL to evaluate hypothetical scenarios without touching the simulation state.

```
return a_max × (1 − (v/v_max)⁴ − (s*/max(gap, 0.1))²)
```

Gap is clamped to 0.1 m to avoid a division-by-zero singularity when computing extremely small gaps.

#### `_evalMobil(targetLane)`

Evaluates the full MOBIL criterion for a candidate adjacent lane. Returns `true` if the lane change should proceed, `false` if it should be rejected.

**Step 1 — Subject's gain in target lane vs current lane:**

```javascript
const targetLeader    = targetLane.getNextAt(myPos);
const targetLeaderGap = targetLeader ? (rear of target leader) − (front of us) : Infinity;
const accelInTarget   = this.getAccelerationGiven(mySpeed, targetLeaderGap, targetLeader?.speed ?? mySpeed);

const { car: currentLeader, distance: currentLeaderGap } = this.trajectory.current.nextCarDistance;
const accelNow        = this.getAccelerationGiven(mySpeed, currentLeaderGap, currentLeader?.speed ?? mySpeed);

const ourGain = accelInTarget − accelNow;
```

**Step 2 — New follower: safety check + gain:**

```javascript
const newFollower     = targetLane.getPrevAt(myPos);
const newGap          = myPos − this.length/2 − newFollower.position − fc.length/2;
const accelAfterMerge = fc.getAccelerationGiven(fc.speed, newGap, mySpeed);

if (accelAfterMerge < −Settings.mobil.safeDecel)  return false;   // SAFETY GATE

const followerGain = accelAfterMerge − fc.getAccelerationGiven(fc.speed, existingLeaderGap, existingLeaderSpeed);
```

**Step 3 — Old follower's gain when we vacate:**

When the subject leaves its current lane, the old follower's new leader becomes the subject's former leader. The gap they gain is the subject's length plus the subject's gap to its current leader:

```javascript
const gapAfterLeave = gapToUs + this.length + currentLeaderGap;
const oldFollowerGain = accelAfterLeave − accelBeforeLeave;
```

**Step 4 — MOBIL incentive criterion:**

```javascript
const netGain = ourGain + Settings.mobil.politeness × (followerGain + oldFollowerGain);
return netGain > Settings.mobil.threshold;
```

#### Adjacent-lane queries: `Lane.getNextAt(pos)` and `Lane.getPrevAt(pos)`

Added to `js/model/Lane.js` specifically for MOBIL. These scan the lane's `carsPositions` registry for the nearest registered car strictly ahead of or behind a raw absolute position, without requiring the caller to be registered in that lane.

### How MOBIL Is Called: `Car.move(delta)`

MOBIL runs inside `move()` on every simulation tick, but only when all preconditions are met:

```
!isChangingLanes      — cannot start a change while another is in progress
!needsTurnLane        — cannot override turn-lane repositioning (see below)
laneChangeCooldown ≤ 0 — must wait after the previous voluntary change
```

When both adjacent lanes are candidates, left is evaluated before right. The first lane that passes MOBIL triggers the change; the loop breaks.

---

## Interaction with Turn-Lane Positioning

Cars in this simulation must reach a specific lane before an intersection to execute their intended turn:

- Left turn  → leftmost lane
- Right turn → rightmost lane
- Forward    → any lane

This **turn-lane logic** runs earlier in `move()` and has higher priority than MOBIL. The `needsTurnLane` flag is set to `true` whenever the car has a left or right turn queued (`nextLane` is set and `turnNumber ∈ {0, 2}`). When `needsTurnLane` is true, the MOBIL block is skipped entirely for that tick.

Critically, `needsTurnLane` is set to `true` even if the car is _already_ in the correct turn lane. This prevents a subtle oscillation loop that would otherwise occur:

1. Car reaches leftmost lane for a left turn. Turn-lane logic: nothing to do (`preferred === currentLane`).
2. Without the guard: MOBIL fires. Sees no benefit in leftmost, moves the car right.
3. Next tick: turn-lane fires again, moves car back left.
4. Next tick: MOBIL fires again. Repeat forever.

Forward-intent cars (`turnNumber === 1`) do not set `needsTurnLane`, so MOBIL runs freely for them — they can use any lane and benefit from speed optimisation.

---

## Cooldown Timer

After any MOBIL-triggered voluntary lane change succeeds, `Car.laneChangeCooldown` is set to **4.0 simulation seconds**. MOBIL is suppressed until this timer reaches zero.

Without a cooldown, borderline-gain situations (both adjacent lanes have approximately equal speed conditions) can cause the car to oscillate immediately after completing a change, because the same asymmetry that made lane B look better from lane A can make lane A look better from lane B.

The 4-second default corresponds to roughly the time it takes to settle into new following conditions after a merge. It can be tuned in `Settings.mobil` if desired.

---

## Settings Reference

All MOBIL parameters live in `Settings.mobil` in `js/Settings.js`:

| Parameter | Default | Effect |
|---|---|---|
| `politeness` | `0.3` | How much the driver weighs impacts on other drivers. Lower = more aggressive, more lane changes. |
| `safeDecel` | `4.0 m/s²` | Maximum braking that may be imposed on the new follower. Lower = stricter safety gate. |
| `threshold` | `0.1 m/s²` | Minimum net gain required to trigger a change. Lower = more frequent, hair-trigger changes. |

**Tuning guide:**

- **Too many lane changes / oscillation**: raise `threshold` (try 0.2–0.3), raise `politeness`, or increase the cooldown in `Car.move()`.
- **Too few lane changes / cars stuck behind slow leaders**: lower `threshold`, lower `politeness`.
- **Unsafe merges**: raise `safeDecel` (4–6 range).
- **General aggressiveness**: `politeness` is the primary dial. `0.0` = purely selfish highway driver. `1.0` = cooperative zipper merge.

---

## Bugs Found and Fixed

Three bugs were discovered and corrected during implementation and testing.

### Bug 1 — Oscillation Near Intersections

**Symptom:** Cars approaching an intersection bounce rapidly between the leftmost/rightmost lane and an adjacent lane.

**Root cause:** `needsTurnLane` was only set when the car needed to _move_ (i.e., `preferred !== currentLane`). When the car was already in the correct turn lane, `needsTurnLane = false` and MOBIL ran, moving the car to a suboptimal lane. The turn-lane block fired on the next tick to correct this, then MOBIL fired again. Infinite loop.

**Fix:** `needsTurnLane = true` is now set whenever the car has a left or right turn queued, regardless of whether it is already positioned correctly.

### Bug 2 — Oscillation on Open Roads

**Symptom:** A lone car on an empty multi-lane road repeatedly switches between lanes for no apparent reason.

**Root cause:** No cooldown existed after a MOBIL-triggered change. In borderline-gain situations, the gain calculation is not exactly symmetric between the two directions (the code paths computing "current leader gap" vs "target leader gap" use slightly different mechanisms), so a tiny positive gain could persist in both directions alternately.

**Fix:** `laneChangeCooldown = 4.0` simulation seconds is applied after every successful MOBIL change. See the Cooldown Timer section above.

### Bug 3 — Cars Leaving the Road

**Symptom:** Rarely, a car would float off the road surface and drive straight across the canvas in a fixed direction.

**Root cause:** `Trajectory.changeLane()` computed the Bézier curve's target-lane endpoint as an _absolute_ position (`currentPosition + 3 × carLength`). It validated this position against the _current_ lane's length but never against the _target_ lane's length. On diagonal roads, adjacent lanes have slightly different lengths due to the fan geometry of their midlines. When `targetLaneLength < nextPosition`, the endpoint's relative parameter exceeded 1.0, causing `Lane.getPoint()` to linearly extrapolate past the intersection boundary. After the crossing completed, `current.position` was set to the out-of-bounds value and the car was rendered beyond the road, floating in free space.

**Fix:** `changeLane()` now computes the offset as a **relative fraction** of the current lane:

```javascript
const nextRelPos = this.current.relativePosition + 3 * this.car.length / this.current._lane.length;
if (nextRelPos >= 1) throw new Error('too late to change lane');
this._startChangingLanes(nextLane, nextRelPos * nextLane.length);
```

This maps the same fractional offset onto the target lane's own length, guaranteeing the endpoint is always within `[0, targetLane.length)`.

---

## Limitations

### Positions Are Approximate Across Adjacent Lanes

MOBIL computes bumper-to-bumper gaps by comparing absolute positions on different lanes (e.g., the subject's position on lane 0 vs a leader's position on lane 1). Adjacent lanes on the same road share the same directional axis, so positions are approximately equivalent. On diagonal roads, the correspondence is not exact — there is a small offset proportional to `lane_width × sin(road_angle)`. At the simulation's default scale this is typically 1–5% of road length, which is acceptable for the purpose of acceleration estimation.

### No Merging onto a Different Road

MOBIL only considers adjacent lanes on the _same road_ (`nextLane.road === currentLane.road`). It does not model freeway on/off ramp merges or lane-level routing across roads. Those would require a fundamentally different approach (e.g., a look-ahead planning horizon that crosses intersections).

### MOBIL Is Suppressed When a Turn Is Queued

For simplicity and correctness, MOBIL is blocked for any car that has a left or right turn queued. This means cars making frequent turns (dense city driving, many short roads) will not benefit much from MOBIL. The more MOBIL matters, the longer and straighter the road segments.

### No Discretionary Right-of-Way Rules

The real MOBIL paper includes a "keep-right" obligation (in right-hand-drive countries, faster traffic keeps left, slower keeps right). This is not implemented. All lane evaluations are symmetric: a car will move either left or right purely on speed gain merit.

### Cooldown Is Global, Not Per-Direction

The `laneChangeCooldown` does not distinguish between directions. A car that just moved left (for a good reason) cannot move right for 4 simulation seconds, even if a strong incentive emerges. In practice this is rarely a problem, but it means MOBIL is somewhat conservative in rapidly evolving traffic patterns.

---

## Possible Future Enhancements

### Keep-Right Obligation

A "keep-right unless overtaking" bias can be added to the incentive criterion with a small negative bias applied to left-lane changes (or positive bias to right-lane changes). Example:

```javascript
const keepRightBias = candidate === currentLane.rightAdjacent ? 0.1 : 0.0;
return netGain + keepRightBias > Settings.mobil.threshold;
```

Tunable as a new `Settings.mobil.keepRightBias` parameter.

### Look-Ahead Horizon for Turn Lane Approach

Currently, turn-lane repositioning is suppressed from MOBIL for the entire duration `nextLane` is set (i.e., the whole length of the road). A finer approach would only suppress MOBIL within some distance of the stop line (e.g., 50 m), allowing MOBIL to operate freely on long roads even for turning cars.

```javascript
const nearIntersection = this.trajectory.distanceToStopLine < 50;
if (nearIntersection && (turnNumber === 0 || turnNumber === 2)) needsTurnLane = true;
```

### Per-Car Aggressiveness

`Settings.mobil.politeness` is currently a global constant. Making it a per-car property (randomly sampled at construction, with a bias toward `0.3` but ranging `0.1–0.8`) would produce more realistic individual driving styles. Fast cars (black, `maxSpeed = 60`) would naturally use a lower politeness value.

### Mandatory Lane Change (Freeway Weaving)

MOBIL in this application is purely voluntary / opportunistic. Real freeway weaving (a car _must_ exit in 500 m) can be modelled by adding a mandatory component that boosts `ourGain` proportionally to the urgency when a required exit is approaching. This would complement the existing turn-lane logic with a stronger pressure signal.

### Ramp Merge Model

The beltway in this simulation has a single south-side ramp connecting to the inner network. Vehicles merging from the ramp onto the beltway currently rely on IDM gap-following alone. A MOBIL-based cooperative merge model would allow beltway vehicles to yield by moving away from the merge point, improving throughput on the ramp.

### Asymmetric Cooldown

Replace the single `laneChangeCooldown` with separate left/right cooldowns. This would allow a car to quickly correct an undesirable move in one direction without waiting for the cooldown on the opposite direction to expire.

---

## Reference

Treiber, M., Kesting, A., & Helbing, D. (2007). _Congestion dynamics on motorways, arterials, and urban intersections: MOBIL lane-change model_. Physica A: Statistical Mechanics and its Applications.

The IDM and MOBIL source papers are available at: [http://www.traffic-simulation.de](http://www.traffic-simulation.de) (Martin Treiber's traffic simulation site).
