mirror of
https://github.com/a-b-street/abstreet.git
synced 2024-12-28 08:53:26 +03:00
451 lines
23 KiB
Markdown
451 lines
23 KiB
Markdown
# Traffic Simulation from scratch
|
|
|
|
The goal of this article is to explain the traffic simulation model that A/B
|
|
Street uses. There's a large amount of traffic simulation research in academia,
|
|
but the papers are often paywalled or require background knowledge. This article
|
|
is meant to be accessible to anybody with a basic background in software
|
|
engineering and high-school kinematics.
|
|
|
|
Disclaimers... my background is in software engineering, not civil engineering.
|
|
The design space that A/B Street explores is absolutely massive; there are so
|
|
many alternate ways of doing everything from modeling the map, to representing
|
|
agents and their movement and conflict... Please send any critique/feedback/etc
|
|
|
|
## Introduction
|
|
|
|
My other goal with this article is to explain what I work on to my friends and
|
|
family. It's very easy to say "I'm not a computer programmer or a math expert,
|
|
there's no way I could understand this," and despite how frustrating this is,
|
|
I've previously accepted this. But today I want more. Driving, moving around a
|
|
city, and getting stuck in traffic are common experiences, and I think they can
|
|
help...
|
|
|
|
Imagine there's an incredibly rainy afternoon and we've got lots of paper. I
|
|
draw a to-scale map of your hometown, showing details like how many lanes are on
|
|
every road, noting the speed limits, and marking all the stop signs and traffic
|
|
lights. Then I place a bunch of color-coded Hot Wheels and, I don't know, bits
|
|
of paper around the map. Each of the cars will start somewhere and wants to go
|
|
to their colored square. To make it easy, let's pretend they all start driving
|
|
at the same time. My challenge for you is to show me exactly where the cars are
|
|
30 seconds after starting to drive, then 5 minutes in, and then an hour later.
|
|
Maybe I'm interested in figuring out where the traffic jams happen. Or maybe we
|
|
throw in some buses and little toy soldiers, and I want to know how long people
|
|
after waiting for their bus because it's delayed in traffic. Or maybe I'm just
|
|
sadistic and want to watch you squirm.
|
|
|
|
How would you figure out what happens to all of the cars after some amount of
|
|
time? You'll probably start by figuring out the route each of them will take to
|
|
their destination -- probably some approximation of the shortest path (by pure
|
|
distance) or fastest (a longer route on a highway might be faster than a short
|
|
one through residential roads). You'll inch the cars forward on their lane, not
|
|
moving them (too much) faster than the speed limit. When two cars are near each
|
|
other, you'll make one follow the other at a reasonable distance, or maybe
|
|
change lanes and try to overtake them if there's room. You'll make the cars stop
|
|
at stop signs and for red lights. When you accidentally push too many cars
|
|
through a green light turned feisty yellow then somber red, you'll make the
|
|
opposing lane's cars angrily honk at the jerk blocking the box by making odd
|
|
little squeaks out of the corner of your mouth. (And I will laugh at you, of
|
|
course.)
|
|
|
|
Of course, you won't be able to tell me with perfect accuracy where all the cars
|
|
are 45.2 seconds into our little game. There are potholes that'll slow some cars
|
|
down a bit that aren't marked on the map, and some drivers that take a little
|
|
longer to accelerate or notice the light's green. That's fine -- complete
|
|
realism isn't so important, as long as things look reasonable.
|
|
|
|
For this to be interesting for me to watch, there have to be a realistic number
|
|
of cars -- 10 little Hot Wheels squeaking around all of Seattle won't tell me
|
|
anything interesting about how the city flows. By now, you might be thinking
|
|
this is going to be slightly tedious. Your fingers are going to get a bit
|
|
cramped from budging 500,000 cars around a bit at a time. So I'll cut you a deal
|
|
-- if you'll describe rules for how to move each of the cars forward a bit in
|
|
sufficient detail, then I'll make a computer do all of the tedious bits.
|
|
|
|
And that's all programming a traffic simulator is. You don't need to know what
|
|
arrays and entity-component systems and trans-finite agent-based cellular RAM
|
|
drives are (I made up that last one maybe). Let's get started!
|
|
|
|
## The map
|
|
|
|
Let's start with deciding exactly what our map of Seattle looks like. One of the
|
|
trickiest and most satisfying parts about computer programming is figuring out
|
|
what parts of the world to represent. Too much irrelevant detail makes it harder
|
|
to... Yes, a tree partly blocking a tight corner might make people slow down,
|
|
but it's probably a bit too much detail to worry about. Your choice of
|
|
abstraction should, it turns out, depend on what you're actually trying to do.
|
|
In this case, I'll cheat momentarily and describe how we should model the map.
|
|
Later, I'll explain what I want A/B Street to be and how that led to including
|
|
some things while omitting others.
|
|
|
|
Let's also clear up terminology. Diagram goes here...
|
|
|
|
Let's start with **roads**. A road goes between exactly two **intersections**.
|
|
You might think of 2nd Ave as a long road through all of downtown, but we'll
|
|
chop it up as 2nd Ave between Bell St and Lenora, 2nd Ave from Lenora to Seneca,
|
|
etc. Most intersections have two or more roads connected to them, but of course,
|
|
we might also have dead-ends and cul-de-sacs. Each road has individual **lanes**
|
|
going in some direction. Most roads have lanes going both directions, but a few
|
|
one-ways only have lanes going in one direction. Cars will travel along a lane
|
|
in single file and, to keep things simple, never change lanes in the middle of a
|
|
road. When the car reaches the end of a lane, it can perform one of several
|
|
**turns** through the intersection. After finishing the turn, the car will be at
|
|
the beginning of a lane in another road. Some turns conflict, meaning it's not
|
|
safe for two cars to do them simultaneously, while others don't conflict.
|
|
|
|
If cars can't ever change lanes, couldn't they get stuck? Maybe a car starts on
|
|
the rightmost lane and is only allowed to turn right, but actually needs to be
|
|
in a middle lane to go straight through the intersection. Don't worry -- you can
|
|
assume that there's a path between any two lanes. Instead of changing lanes in
|
|
the middle of a road, cars in our game will change lanes when they turn. EXAMPLE
|
|
PIC. I'll describe later why this is a good idea.
|
|
|
|
For now, let's assume cars start on some lane. When their front bumper hits
|
|
their colored square on their destination, they just immediately vanish. The
|
|
colored square could be at the end of their destination lane, or somewhere in
|
|
the middle.
|
|
|
|
(could mention borders or not, maybe footnote)
|
|
|
|
Don't worry about parking, pedestrians, bicycles, or buses. These things are all
|
|
important to A/B Street, but we'll add them in later.
|
|
|
|
## Disrete-time model
|
|
|
|
Whoa, fancy name! Ignore it for a moment.
|
|
|
|
How do people drive? Very roughly, they look at things around them, take an
|
|
action (press the gas some amount, press the brake some amount, turn the wheel a
|
|
bit), and then do the same thing a half-second (or so) later. That's the essence
|
|
of agent-based modeling -- sense the environment, plan what to do next, do it,
|
|
then repeat some time later. We'll call the amount of time between each choice
|
|
the **timestep** and say it's about 0.1 seconds. Let's try simulating traffic
|
|
roughly this way -- every single car will take an action every 0.1 seconds that
|
|
advances them through the world. Breaking up time in these regular 0.1s
|
|
intervals is how we get the term "discrete-time model."
|
|
|
|
What kind of controls do we want to give each driver? If we let them turn the
|
|
steering wheel a few degrees left or right and apply some pressure to the gas
|
|
pedal, then we have to figure out how this affects the position of the car and
|
|
worry about how to make sure cars stay in their lane. That's way too
|
|
complicated, and not interesting for our purposes. So let's say that for every
|
|
car, we keep track of a few details:
|
|
|
|
- current lane or turn
|
|
- **dist_along**: distance of the front bumper along that lane or turn (starting
|
|
at 0 for the beginning of the lane)
|
|
- current speed
|
|
- the remaining path to the goal (lane1, turn2, lane3, turn5, ..., lane10)
|
|
- vehicle length
|
|
- maximum acceleration (some cars can start more quickly)
|
|
- maximum deceleration (some cars can slam on their brakes and stop faster)
|
|
|
|
The first four will change every 0.1s, while the last three don't ever change.
|
|
|
|
So what controls can a driver do? Accelerate (or decelerate) some amount. That's
|
|
all. When the dist_along is bigger than the current lane/turn's length, we make
|
|
the new current lane be the first thing from the remaining path, discard the
|
|
path, and reset the dist_along to 0.
|
|
|
|
(this section got weird -- talk about controls first, brief bit of kinematics,
|
|
then state. follow along curve of lanes automatically.)
|
|
|
|
### Constraints
|
|
|
|
What kind of things influence a driver's decision each timestep, and what do
|
|
they need to be able to sense about their environment to use the rule? I can
|
|
think of three:
|
|
|
|
1. Don't exceed the speed limit of the current road
|
|
|
|
- so the driver needs to be able to look at the speed limit of the current road
|
|
|
|
2. Don't hit the car in front of me
|
|
|
|
- need to see the current dist_along, speed, length, accel and deaccel of the
|
|
next car in the queue
|
|
- actually, humans can't eyeball another car and know how quickly it can speed
|
|
up or slow down. maybe they just assume some reasonable safe estimate.
|
|
|
|
3. Maybe stop at the end of the lane
|
|
|
|
- for stop signs or red/yellow lights
|
|
|
|
And of course, whatever acceleration the driver picks gets clamped by their
|
|
physical limits. Other than these constraints, let's assume every driver will
|
|
want to go as fast as possible and isn't trying to drive smoothly or in a
|
|
fuel-efficient manner. Not realistic, but makes our lives easier. So if each of
|
|
these three constraints gives an acceleration, we have to pick the smallest one
|
|
to be safe. Rule 1 says hit the gas and go 5m/s^2, but rule 2 says we can safely
|
|
go 2m/s^2 and not hit the next car and rule 3 actually says wait, we need to hit
|
|
the brakes and go -1m/s^2 right now. Have to go with rule 3.
|
|
|
|
### Some math
|
|
|
|
Skip this section freely. The takeaways are:
|
|
|
|
- there's a way to figure out the acceleration to obey the 3 constraints
|
|
- the math gets tricky because (1) the car will only be doing that acceleration
|
|
for 0.1s and then getting to act again, and (2) floating point math is tricky
|
|
|
|
### Lookahead
|
|
|
|
lookahead (worst case analyses), cover multiple lane->turn->lanes maybe
|
|
|
|
### Retrospective
|
|
|
|
1. It's fundamentally slow; there's lots of busy work. Cars in freeflow with
|
|
nothing blocking them yet, or cars
|
|
|
|
2. Figuring out acceleration in order to do something for the next tick is
|
|
complicated.
|
|
|
|
- floating pt bugs, apply accel but make sure speed doesnt go negative or dist
|
|
doesnt exceed end of lane if they were supposed to stop... wind up storing an
|
|
intent of what they wanted to do, make corrections based on that. hacky.
|
|
|
|
3. The realism of having cars accel and deaccel doesnt really add much, and
|
|
since the approach has silly assumptions anyway (slam on brakes and
|
|
accelerator as much as possible), unrealistic
|
|
|
|
## Discrete-event model take 1: time-space intervals
|
|
|
|
things that were not finished / still hard:
|
|
|
|
- cover a short lane
|
|
- real quadratic distance over time was breaking stuff
|
|
- impossible accel/deaccel happened
|
|
- faster lead car made adjusting follower very hard
|
|
|
|
## Discrete-event model take 2
|
|
|
|
Let's try again, but even simpler and more incremental this time.
|
|
|
|
(make sure to explain the premise of parametric time and events)
|
|
|
|
### v0: one car
|
|
|
|
Forget about speed, acceleration, and even multiple cars momentarily. What is a
|
|
car's basic state machine? They enter a lane, travel across it, maybe stop and
|
|
wait for the intersection, execute a turn through the intersection, and then
|
|
enter the next lane. We could assign reasonable times for each of these --
|
|
crossing a lane takes lane_distance / min(road's speed limit, car's max speed)
|
|
at minimum. Intersections could become responsible for telling cars when to move
|
|
-- stop signs would keep a FIFO queue of waiting cars and wake up each car as
|
|
the last one completes their turn, while traffic signals could wake up all
|
|
relevant cars when the cycle changes.
|
|
|
|
### v1: queueing
|
|
|
|
Alright, but what about multiple cars? In one lane, they form a queue -- no
|
|
over-taking or lane-changing. The FSM doesn't get much more complicated: a car
|
|
enters a lane, spends at least the freeflow_time to cross it, and then either
|
|
winds up front of the queue or behind somebody else. If they're in the front,
|
|
similar logic from before applies -- except they first need to make sure the
|
|
target lane they want to turn to has room. Maybe cars are already backed up all
|
|
the way there. If so, they could just wait until that target lane has at least
|
|
one car leave. When the car is queued behind another, they don't have anything
|
|
interesting to do until they become the queue's lead vehicle.
|
|
|
|
Another way to understand this system is to picture every lane as having two
|
|
parts -- the empty portion where cars cross in freeflow and a queue at the end.
|
|
Cars have to pay a minimum amount of time to cross the lane, and then they wind
|
|
up in the queue. The time to go from the end of the queue to the front requires
|
|
crunching through the queue front-to-back, figuring out when each successive
|
|
lead vehicle can start their turn and get out of the way.
|
|
|
|
### Drawing
|
|
|
|
An intermission -- we haven't pinned down exactly where cars are at some point
|
|
in time, so how do we draw them? The idea for this DES model began without
|
|
worrying about this too much -- when the map is zoomed out, individual cars are
|
|
hard to see anyway; the player probably just wants to know roughly where lots of
|
|
cars are moving and stuck waiting. This can be calculated easily at any time --
|
|
just count the number of cars in each queue and see if they're in the Crossing
|
|
or Queued state.
|
|
|
|
But when zoomed in, we do want to draw individual cars at exact positions
|
|
through time! Luckily, this isn't hard at all. The only change from the timestep
|
|
model is that we have to process a queue at a time; we can't randomly query an
|
|
individual car. This is fine from a performance perspective -- we almost always
|
|
want to draw all cars on lanes visible on-screen anyway.
|
|
|
|
So how does it work? First consider the queue's lead vehicle. If they're Queued,
|
|
then the front of the car must be at the end of the lane waiting to turn. If
|
|
they're Crossing, then we can just linearly interpolate their front position
|
|
from (0, lane_length) using the time-interval of their crossing and the current
|
|
time. Then we consider the queue's second car. In an ideal world where they're
|
|
the lead car, we do the same calculation based on Queued or Crossing state. But
|
|
the second car is limited by the first. So as we process the queue, we track the
|
|
bound -- a car's front position + the car's length + a fixed following distance
|
|
of 1m. The second car might be farther back, or directly following the first and
|
|
blocked by them. We just take min(ideal distance, bound), and repeat for the
|
|
third car.
|
|
|
|
### v2: preventing discontinuities
|
|
|
|
There's an obvious problem happening when the lead vehicle of a queue leaves the
|
|
queue -- everybody queued behind them suddenly jump forward. Discontinuities
|
|
like this are of course unrealistic, but more importantly for A/B St's purpose,
|
|
looks confusing to watch. So let's try a simple fix: when a lead car exits a
|
|
queue, update its follower to know to cross the remaining distance properly. The
|
|
follower might be Queued right behind the lead, or they might still be Crossing.
|
|
If they're still Crossing, no worries -- they'll continue to smoothly Cross to
|
|
the end of the lane. But if they're Queued, then reset the follower to Crossing,
|
|
but instead make them cover a smaller distance -- (lane_length - lead car's
|
|
length - FOLLOWING_DISTANCE, lane_length), using the usual min(lane speed limit,
|
|
car's max speed). Since they're Queued, we know that's exactly where the
|
|
follower must be.
|
|
|
|
This smoothness comes at a price -- instead of a car taking one event to cross a
|
|
lane, it now might go through a bunch of Crossing states -- the lane's max
|
|
capacity for vehicles, at worst. That's not so bad, and it's worth it though.
|
|
|
|
### v3: starting and stopping early
|
|
|
|
The basic traffic model is now in-place. As we add more elements of A/B Street
|
|
in, we need one last tweak to the driving model. Cars don't always enter a lane
|
|
at the beginning or exit at the very end. Cars sometimes start in the middle of
|
|
a lane by exiting an adjacent parking spot. They sometimes end in the middle of
|
|
a lane, by parking somewhere. And buses will idle in the middle of a lane,
|
|
waiting at a bus stop for some amount of time.
|
|
|
|
When we update a car, so far we've never needed to calculate exact distances of
|
|
everybody on the queue at that time. That's just for drawing. But now, we'll
|
|
actually need those distances in two cases: when a car is finished parking or
|
|
when a car is somewhere along their last lane. (Note that buses idling at a stop
|
|
satisfy this second case -- when they leave the stop, they start following a new
|
|
path to the next stop.) When the lead car vanishes from the driving lane (by
|
|
shifting into the adjacent parking spot, for example), we simply update the
|
|
follower to the Crossing state, starting at the exact position they are at that
|
|
time (because we calculated it). If they were Queued behind the vanishing car,
|
|
we know their exact position without having to calculate all of them. But
|
|
Crossing cars still paying the minimum time to cross the lane might jump forward
|
|
when the car in front vanishes. To prevent this, we refresh the Crossing state
|
|
to explicitly start from where they became unblocked.
|
|
|
|
### Remaining work
|
|
|
|
There's one more discontinuity remaining. Since cars have length, they can
|
|
occupy more than one lane at a time. When the lead car leaves a queue, the
|
|
follower is updated to cross the remaining distance to the end. But if the
|
|
leader is moving slowly through their turn, then the follower will actually hit
|
|
the back end of the lead vehicle! We need a way to mark that the back of a
|
|
vehicle is still in the queue. Maybe just tracking the back of cars would make
|
|
more sense? But intersections need to know when a car has started a turn, and
|
|
cars spawning on the next lane might care when the front (but not back) of a car
|
|
is on the new lane. So maybe we just need to explicitly stick a car in multiple
|
|
queues at a time and know when to update the follower on the old lane. Except
|
|
knowing when the lead car has advanced some minimum distance into the new lane
|
|
seemingly requires calculating exact distances frequently!
|
|
|
|
This jump bug also happens when a lead car vanishes at a border node. They
|
|
vanish when their front hits the border, even though they should really only
|
|
vanish when their back makes it through.
|
|
|
|
The other big thing to fix is blind retries. In almost all cases, we can
|
|
calculate exactly when to update a car. Except for three:
|
|
|
|
1. Car initially spawning, but maybe not room to start. Describe the rules for
|
|
that anyway.
|
|
|
|
2. Car on the last lane, but haven't reached end_distance yet. Tried a more
|
|
accurate prediction thing, but it caused more events than a blind retry of
|
|
5s.
|
|
|
|
3. Cars waiting to turn, but not starting because target lane is full. Could
|
|
register a dependency and get waked up when that queue's size falls below its
|
|
max capacity. Could use this also for gridlock detection. Oops, but can't
|
|
start because of no room_at_end. That's different than target lane being
|
|
full, and it's hard to predict when there'll be room to inch forward at the
|
|
end.
|
|
|
|
#### Notes on fixing the jump bug
|
|
|
|
- can track a laggy leader per queue. there's 0 or 1 of these, impossible to be
|
|
more.
|
|
- a car can be a laggy leader in several queues, like long bus on short lanes.
|
|
last_steps is kinda equivalent to this.
|
|
|
|
is it possible for get_car_positions to think something should still be blocked
|
|
but the rest of the state machine to not? - dont change somebody to
|
|
WaitingToAdvance until theres no laggy leader.
|
|
|
|
TODO impl:
|
|
|
|
- get_car_positions needs to recurse
|
|
- trim_last_steps needs to do something different
|
|
|
|
the eager impl:
|
|
|
|
- anytime we update a Car with last_steps, try to go erase those. need full
|
|
distances. when we successfully erase, update followers that are Queued. - -
|
|
follower only starts moving once the laggy lead is totally out. wrong. they
|
|
were Queued, so immediately promote them to WaitingToAdvance. smoothly draw in
|
|
the meantime by recursing
|
|
- optimistic check in the middle of Crossing, but use a different event for this
|
|
to be clear. the retry should only be expensive in gridlocky cases. -
|
|
BLIND_RETRY after... similar to end_dist checking. - note other routines dont
|
|
even do checks that could hit numerical precision, we just assume events are
|
|
scheduled for correct time.
|
|
- maybe v1: dont recurse in get_car_positions, just block off entire laggy
|
|
leader length until they're totally out.
|
|
- v2: do have to recurse. :(
|
|
|
|
the lazy impl:
|
|
|
|
- when we become WaitingToAdvance on a queue of length > vehicle len, clean up
|
|
last_steps if needed
|
|
- when we vanish, clean up all last_steps
|
|
- when a follower gets to ithin laggy leader length of the queue end, need to
|
|
resolve earlier - resolving early needs to get exact distances. expensive, but
|
|
common case is theyre clear. if somebody gets stuck fully leaving a queue,
|
|
then this will spam, but thats also gridlocky and we want to avoid that anyway
|
|
|
|
other ideas:
|
|
|
|
- can we cheat and ensure that we only clip into laggy leaders briefly? - if
|
|
laggy leaders never get stuck at end of their next queue...
|
|
- alt: store front and back of each car in queues separately - compute crossing
|
|
state and stuff for those individually? double the work?
|
|
|
|
## A/B Street's full simulation architecture
|
|
|
|
start from step(), the master event queue, how each event is dispatched, each
|
|
agent's states
|
|
|
|
FSM for intersections, cars, peds (need to represent stuff like updating a
|
|
follower, or being updated by a leader)
|
|
|
|
## Appendix
|
|
|
|
pedestrian, transit, bikes, buses, etc limits... overtaking (especially cars and
|
|
bikes on roads)
|
|
|
|
Traffic modeling is a complex space, but for the purposes of this article, a
|
|
traffic simulation is a computer program that takes a map with roads and
|
|
intersections and a list of trips (depart from here at this time, go there using
|
|
a car or bus or by foot) and shows where all of the moving agents wind up over
|
|
time. I'm sure you can imagine a great many uses for them both professional and
|
|
nefarious, but our mission today is to understand one particular traffic
|
|
simulation works. A/B Street is a computer game I've been building to experiment
|
|
with the traffic in Seattle. My goal for A/B Street is to make it easy for
|
|
anybody to ask what-if questions.
|
|
|
|
a/b st is a game, needs performance (city scale -- X road segments, X
|
|
intersections, X agents total), determinism, not complete realism agent-based,
|
|
rule out others complex maps
|
|
|
|
cars, buses, bikes only (things on the road in a queue)
|
|
|
|
abst is a game, what's the essence of scarcity? contention at intersections,
|
|
lanes restricting usage, parking. NOT modeling pedestrians queueing on a
|
|
sidewalk, bc in practice, doesnt happen (except maybe around pike place or at
|
|
festivals). not modeling bike racks at all -- in practice, can lock up within a
|
|
block of destination without effort.
|
|
|
|
if modeling big highways, this wouldnt be great. but we're focused on intra-city
|
|
seattle -- modeling phenomena like jam waves not so important. if the player
|
|
does parking->bus lane and the bus moves faster, but more cars circle around
|
|
looking for parking, then the model is sufficiently interesting to answer the
|
|
questions i want. dont need to model stopping distance for that.
|