Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "micro_traffic_sim_core"
version = "0.1.9"
version = "0.1.10"
edition = "2024"
description = "Core library for microscopic traffic simulation via cellular automata."
license = "Apache-2.0"
Expand Down
27 changes: 27 additions & 0 deletions examples/merge-blocked/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# merge-blocked

Demonstrates the "keep rolling along a jammed lane" fallback in
`find_alternate_intention` and the confusion A*-skip, on the real stage network
(`network.json` - 676 cells exported from my local toy client application: forward/left/right
links, WGS84 coordinates, zones, speed limits).

Vehicle 1 drives 666 -> 667 -> 668 (no right neighbor) toward destination 621,
reachable only via the left merges 666->605, 667->606, 668->619. Past 668 lies
the point of no return (681 -> 683 death zone). The left lane is jammed with
parked vehicles.

```bash
cargo run --example merge-blocked
```

| Scenario | Parked | Expected outcome |
|---|---|---|
| A: gap at the last merge | 605, 606 | rolls along the jam, merges at 619 -> COMPLETED |
| B: fully jammed | 605, 606, 619 | rolls past the last merge, confusion drives it to the 683 death zone -> LOST, the lane stays free (accepted risk) |

A DEADLOCK verdict (standing next to the jam forever) is a regression: the
forward fallback in `find_alternate_intention` stopped working.

Once a vehicle is confused, its destination is unreachable from every cell it
can ever reach (reachability is monotone along directed edges), so per-tick A*
is skipped for it entirely - a failed full A* is the most expensive kind.
152 changes: 152 additions & 0 deletions examples/merge-blocked/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
//! Demonstration of the "keep rolling along a jammed lane" fallback in
//! find_alternate_intention, on the real "Pisareva" stage network.
//!
//! Setup: vehicle 1 drives in lane 666 -> 667 -> 668 (no right neighbor) with
//! destination 621, which is reachable ONLY through the left merges
//! 666->605, 667->606, 668->619 (after 668 comes the point of no return:
//! 681 -> 683 dead end). The left lane is jammed with parked vehicles.
//!
//! Scenario A - gap at the last merge (605, 606 parked, 619 free):
//! the vehicle rolls along the jam and merges into the gap -> COMPLETED.
//!
//! Scenario B - fully jammed (605, 606, 619 parked):
//! the vehicle rolls past the last merge, per-tick A* returns NoPathFound,
//! confusion drives it forward into the 683 death zone -> LOST.
//! The trip fails, but the road stays free - an accepted risk.
//!
//! A DEADLOCK verdict (standing next to the jam forever) is a regression:
//! it means the forward fallback in find_alternate_intention stopped working.
//!
//! Run: cargo run --example merge-blocked

use micro_traffic_sim_core::agents::Vehicle;
use micro_traffic_sim_core::behaviour::BehaviourType;
use micro_traffic_sim_core::geom::{SRID, new_point};
use micro_traffic_sim_core::grid::{cell::Cell, road_network::GridRoads, zones::ZoneType};
use micro_traffic_sim_core::simulation::grids_storage::GridsStorage;
use micro_traffic_sim_core::simulation::session::Session;
use micro_traffic_sim_core::verbose::VerboseLevel;

const NETWORK_JSON: &str = include_str!("network.json");

const START_CELL: i64 = 666;
const DESTINATION: i64 = 621;
const STEPS: usize = 80;
const DEADLOCK_THRESHOLD: usize = 15;

fn load_grid() -> GridRoads {
let cells: serde_json::Value =
serde_json::from_str(NETWORK_JSON).expect("network.json must be valid JSON");
let mut grid = GridRoads::new();
for c in cells.as_array().expect("array of cells") {
let zone = match c["zone"].as_i64().unwrap() {
1 => ZoneType::Birth,
2 => ZoneType::Death,
3 => ZoneType::Coordination,
4 => ZoneType::Common,
_ => ZoneType::Undefined,
};
let cell = Cell::new(c["id"].as_i64().unwrap())
.with_point(new_point(
c["lon"].as_f64().unwrap(),
c["lat"].as_f64().unwrap(),
Some(SRID::WGS84),
))
.with_forward_node(c["fwd"].as_i64().unwrap())
.with_left_node(c["left"].as_i64().unwrap())
.with_right_node(c["right"].as_i64().unwrap())
.with_zone_type(zone)
.with_speed_limit(c["sl"].as_i64().unwrap() as i32)
.build();
grid.add_cell(cell);
}
grid
}

fn run_scenario(name: &str, parked_cells: &[i64]) {
println!("Scenario {name}: parked at {parked_cells:?}");

let traveller = Vehicle::new(1)
.with_speed(1)
.with_speed_limit(4)
.with_cell(START_CELL)
.with_destination(DESTINATION)
.with_slowdown(0.0)
.build();
let mut vehicles = vec![traveller];
for (i, &cell) in parked_cells.iter().enumerate() {
vehicles.push(
Vehicle::new(100 + i as u64)
.with_cell(cell)
.with_destination(-1)
.with_behaviour(BehaviourType::Block)
.build(),
);
}

let grids_storage = GridsStorage::new().with_vehicles_net(load_grid()).build();
let mut session = Session::new(grids_storage, None);
session.set_verbose_level(VerboseLevel::None);
session.add_vehicles(vehicles);

let mut trajectory: Vec<i64> = vec![START_CELL];
let mut stuck_ticks = 0usize;
let mut completed = 0;
let mut lost = 0;

for _ in 0..STEPS {
let state = session.step().expect("simulation step failed");
completed = state.vehicles_completed;
lost = state.vehicles_lost;

let traveller = session
.get_vehicles()
.into_iter()
.find(|(_, v)| v.id == 1)
.map(|(_, v)| v);
match traveller {
Some(v) => {
if *trajectory.last().unwrap() == v.cell_id {
if v.speed == 0 {
stuck_ticks += 1;
}
} else {
trajectory.push(v.cell_id);
stuck_ticks = 0;
}
}
None => break, // removed: either completed or lost
}
if stuck_ticks >= DEADLOCK_THRESHOLD {
break;
}
}

println!("trajectory: {trajectory:?}");
if completed > 0 {
println!("VERDICT: COMPLETED - merged into the gap and reached {DESTINATION}\n");
} else if lost > 0 {
println!(
"VERDICT: LOST - rolled past the last merge, wandered to the death zone; \
the lane stays free (accepted risk)\n"
);
} else if stuck_ticks >= DEADLOCK_THRESHOLD {
println!(
"VERDICT: DEADLOCK - stood {stuck_ticks} ticks at cell {} waiting for \
the jammed lane; REGRESSION - the forward fallback did not kick in\n",
trajectory.last().unwrap()
);
} else {
println!("VERDICT: INCONCLUSIVE after {STEPS} steps\n");
}
}

fn main() {
let grid = load_grid();
println!(
"Loaded stage network: {} cells; route to {DESTINATION} exists only via left merges 605/606/619\n",
grid.get_cells_num()
);
run_scenario("A (gap at the last merge)", &[605, 606]);
run_scenario("B (fully jammed)", &[605, 606, 619]);
}
Loading