├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .nvmrc ├── .travis.yml ├── LICENSE ├── README.md ├── config └── common.sample.json ├── docs ├── ARTICLE.md ├── NOTES.md └── traffic-modelling-running-simulation.gif ├── lib ├── CommonConfigInitializer.js ├── ConfigValidator.js ├── Simulation.js ├── SimulationRunner.js ├── SimulationsConfigurator.js ├── TableGenerator.js ├── traffic-model │ ├── ArrivalSequence.js │ ├── Car.js │ ├── LaneAssigner.js │ ├── LaneChangeDecider.js │ ├── MetricsCalculator.js │ ├── Road.js │ ├── TrafficModel.js │ └── independentVariables.json └── utils │ ├── fileCreators.js │ └── index.js ├── package-lock.json ├── package.json ├── results └── .gitignore ├── scripts ├── configure.js └── simulate.js └── tests └── lib ├── traffic-model ├── LaneAssigner.test.js └── LaneChangeDecider.test.js └── utils.test.js /.eslintignore: -------------------------------------------------------------------------------- 1 | config 2 | docs 3 | node_modules 4 | results 5 | tests 6 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "airbnb-base" 9 | ], 10 | "globals": { 11 | "Atomics": "readonly", 12 | "SharedArrayBuffer": "readonly" 13 | }, 14 | "parserOptions": { 15 | "ecmaVersion": 2018 16 | }, 17 | "rules": { 18 | } 19 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | node_modules 4 | config/common.json 5 | config/simulations.json 6 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v12.4.0 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | before_script: 3 | - cp config/common.sample.json config/common.json 4 | - echo '[]' > config/simulations.json 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Paul Sobocinski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.com/psobocinski/traffic-modelling.svg?branch=master)](https://travis-ci.com/psobocinski/traffic-modelling) 2 | 3 | # Traffic Modelling 4 | 5 | A simple computer model for simulating traffic congestion. 6 | 7 | ## Quickstart 8 | 9 | 1. Install dependencies: `npm install` 10 | 2. Generate configuration files: `npm run configure` 11 | 3. Run simulation: `npm run simulate` 12 | 13 | ## Overview 14 | 15 | For an overview of the computer model and how it can be used, please see this article [investigating whether lane changing in traffic is a zero-sum game](https://www.connected.io/post/traffic-modelling-is-beating-traffic-congestion-a-zero-sum-game). 16 | 17 | ## Configuring the model 18 | 19 | `npm run configure` generates two configuration files: 20 | 21 | ### 1. Common Configuration: `config/common.json` 22 | 23 | Copied from the sample file: `config/common.sample.json`. Contains the following fields: 24 | 25 | 1. `arrivalProbability`: Likelihood a car arrives on the next tick 26 | 1. `durationInTicks`: How long each simulation runs for 27 | 1. `numberOfIterations`: The number of times (iterations) each configured simulation is run. The result takes an average of all iterations 28 | 1. `simulationDefaults`: The values of the Independent Variables used in all simulations, unless they are overridden in the simulation configuration 29 | 1. `laneChangeDelayTicks`: The time added to the Total Time in Lane upon a lane change 30 | 1. `lanePlacement`: How each Car is assigned to a Lane (`levelled` or `random`) 31 | 1. `minTicksInLane`: The minimum time in Lane until Car considers switching Lanes 32 | 1. `minTickSpeedupPercent`: The minimum time saved by switching Lanes for Car to consider switching Lanes 33 | 1. `numberOfLanes`: The number of lanes (minimum 2) 34 | 1. `timeOnRoadFunction`: The function `t = f(n)` used to calculate total time on road `t`. Based on `n`, the number of Cars in lane. 35 | 1. `output`: Configuration for how simulation output is written 36 | 1. `format`: File format (`markdown` or `csv`). 37 | 1. `path`: File path (filename will be `results`). 38 | 1. `columns`: Columns to include in results. Array of tuples. First entry is the variable name, the second is the column header used. Examples: 39 | - `[ "minTicksInLane", "Min Time in Lane" ]` 40 | - `[ "overallThroughput", "Aggregate Throughput (cars / 100 ticks)" ]` 41 | 42 | ### 2. Simulation-specific Configuration: `config/simulations.json` 43 | 44 | Contains configurations for every simulation, where one or more Independent Variables can be overridden. 45 | This file is auto-generated by prompting the user, varying one Independent Variable at a time. 46 | Additional simulations can be added by editing this file directly (prior to running `npm run simulate`). 47 | 48 | ## Running the simulation 49 | 50 | `npm run simulate` runs the simulation and outputs debug data to the console. 51 | It also generates the results and writes them to the path and format as specified in the common configuration. 52 | 53 | ### Traffic Performance Metrics 54 | 55 | The following can be included in the results file by adding them to the common configuration (under `output.columns`): 56 | 57 | - `simulationTime`: Elapsed time of simulation 58 | - `carsEntered`: Total number of cars entered over the course of the simulation 59 | - `carsRemaining`: Cars remaining on the road at the end of the simulation 60 | - `averageTimeOnRoad`: Average time spent on the road by all cars 61 | - `carsOnRoadAggregateTime`: Total of all times spent by all cars on the road 62 | - `totalLaneChanges`: Total number of lane changes that occurred 63 | - `overallThroughput`: Measures traffic performance. 64 | - `Aggregate Throughput (cars / 100 ticks) = Total Cars Exited / Aggregate Time * 100` 65 | - `averageThroughput`: Same as above, except that it is calculated per tick, then averaged 66 | 67 | ## Development 68 | 69 | - Run tests while you code: `npm run specs -- --watch` 70 | - Run ESLint and all tests: `npm test` 71 | - NOTE: also runs in Travis CI 72 | - Run ESLint on its own: `npm run lint` 73 | - NOTE: not recommended, instead configure your IDE to use the project's ESLint configuration 74 | -------------------------------------------------------------------------------- /config/common.sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrivalProbability": 0.9, 3 | "durationInTicks": 10000, 4 | "numberOfIterations": 10, 5 | "simulationDefaults": { 6 | "laneChangeDelayTicks": 5, 7 | "lanePlacement": "random", 8 | "minTicksInLane": 2, 9 | "minTickSpeedupPercent": 60, 10 | "numberOfLanes": 3, 11 | "timeOnRoadFunction": "2 * Math.max(0, n - 5) + 20" 12 | }, 13 | "output": { 14 | "format": "csv", 15 | "path": "./results", 16 | "columns": [ 17 | [ "simulationTime", "Elapsed Time of Simulation" ], 18 | [ "carsEntered", "Number of Cars entered" ], 19 | [ "laneChangeDelayTicks", "Lane Change Delay" ], 20 | [ "minTicksInLane", "Min Time in Lane" ], 21 | [ "minTickSpeedupPercent", "Time Saved %" ], 22 | [ "numberOfLanes", "Number of Lanes" ], 23 | [ "averageTimeOnRoad", "Avg Time" ], 24 | [ "carsRemaining", "Cars Remain" ], 25 | [ "carsOnRoadAggregateTime", "Aggregate Time" ], 26 | [ "totalLaneChanges", "Number of Lane Changes" ], 27 | [ "overallThroughput", "Aggregate Throughput (cars / 100 ticks)" ], 28 | [ "averageThroughput", "Average Throughput per tick (cars / 100 ticks)" ] 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /docs/ARTICLE.md: -------------------------------------------------------------------------------- 1 | # Applying Game Theory to Traffic Modelling 2 | 3 | Traffic congestion is a problem. 4 | 5 | A study from last year found my home town Toronto to be the [20th most congested city in the world](http://inrix.com/scorecard-city/?city=Toronto%2C%20ON&index=20), with an average of 164 hours spent in congestion in 2018. 6 | 7 | I am not surprised. 8 | 9 | Early in my career, I commuted over an hour twice a day. In a year, that works out to roughly 21 days sitting in traffic. It feels particularly wasteful when I consider that I spent it driving. With transit, I can a least spend the time reading a book or catching up on news. 10 | 11 | I have it a bit better these days (it takes me 10 minutes to walk to work). I still can't help but wonder how much time commuters could save if we reduced traffic congestion. In my case, even a 10% improvement could saved me a couple of days every year. 12 | 13 | ## How much time would we save if cars drove themselves? 14 | 15 | It's easy to imagine how self-driving cars could save us time. As our car drives us to work, we'd be able to at least read that book we've been meaning to finish. 16 | 17 | That benefit aside, however, let's consider actual time on the road. If we relinquished control to our artifically-intelligent automobiles, would we really end up spending less time in traffic? 18 | 19 | More specifically, what if we rely on self-driving cars to place us in a lane of traffic (and keep us there), instead of using our own judgement to minimize our time in traffic, by changing lanes multiple times over the course of our commute? 20 | 21 | The goal of this article is introduce an approach to answering this question quantitatively through the use of a simplified traffic model. In particular, the model aims to programmatically simulate traffic conditions and human behaviour. 22 | 23 | Before exploring how self-driving cars can manage traffic better than we can, there is a more fundamental question we should answer. 24 | 25 | ## Is Opportunistic Lane-Changing a Zero-sum game? 26 | 27 | It's a familiar struggle for regular commuters. The work meeting starts in half an hour, but the GPS is telling you that the ETA is in 36 minutes. Can you shave off those minutes with some clever, well-timed lane-changes? 28 | 29 | If we haven't done it ourselves, we've been a witness to (and victim of) other drivers "weave" through traffic. Some of us feel that these drivers are only taking time off their commute and adding it onto us. In other words, such opportunistic lane-changing behaviour is a "zero-sum game". 30 | 31 | But is it? 32 | 33 | Wikipedia defines a zero-sum game as follows: 34 | 35 | > In game theory and economic theory, a zero-sum game is a mathematical representation of a situation in which each participant's gain or loss of utility is exactly balanced by the losses or gains of the utility of the other participants. 36 | 37 | In our case, we say that opportunistic lane-changing is a zero-sum game when such behaviour does not result in a better outcome for all commuters on the road (over a given time period). 38 | 39 | If we can conclude a zero-sum game, we can also conclude that self-driving cars can manage traffic better than humans simply by _not changing lanes at all_. 40 | 41 | ## Assumptions and Definitions 42 | 43 | In order to explore the "zero-sum game" hypothesis, we'll clarify some terms and define what to measure. 44 | 45 | ### Lane Change Conditions 46 | 47 | Let's start with the notion of "opportunistic lane-changing". The term "opportunistic" is used to indicate that the driver will act opportunistically for their own self interest, i.e. they will not consider the effects of their actions on other drivers. 48 | 49 | In light of this, here are two lane change conditions that seem reasonable: 50 | 51 | "As a driver, I will change lanes if all of the following conditions are met:" 52 | 53 | 1. Changing to the other lane drops my remaining time in traffic by a certain percentage 54 | 2. I have spent a certain minimum length of time in my current lane 55 | 56 | ### Traffic Performance Metrics 57 | 58 | Second, we define what a "better outcome" means in our model. In particular, we define quantitative metrics such that lower values indicate the better outcome. 59 | 60 | #### Aggregate and Average Time 61 | 62 | The ideal length of time spent on the road is zero. Therefore, we define two metrics: 63 | 64 | - **Average Time:** The average time spent by drivers on the road 65 | - **Aggregate Time:** A total of every driver's time spent on the road 66 | 67 | ### Cars Remaining 68 | 69 | Given that the following conditions remain the same for all simulation runs: 70 | 71 | - number of cars entering the road 72 | - simulation duration 73 | 74 | We define an additional metric: 75 | 76 | - **Cars Remaining:** The total number of cars remaining on the road at the end of the simulation 77 | 78 | #### Aggregate Throughout 79 | 80 | What if a simulation run results in a lower Aggregate Time, but a higher number of Cars Remaining? We define the following metric to account for this: 81 | 82 | - **Aggregate Throughput:** `Aggregate Time / Cars Exited` 83 | 84 | Unlike the prior metics, a higher value for this metric indicates the better outcome. 85 | 86 | ## Defining the Model 87 | 88 | We model the total time on road for a given car to be solely dependent on the number of other cars in its lane: 89 | 90 | `t[total] = f(n)` 91 | 92 | - `t[total]`: total time on road 93 | - `n`: number of cars in the given lane 94 | 95 | Subsequently, a given car's remaining time on road `t[remaining]` is calculated as follows: 96 | 97 | `t[remaining] = t[total] - t[elapsed]` 98 | 99 | The elapsed time on road `t[elapsed]` is incremented as the simulation runs (more on this later). A car exits the road once `t[remaining]` reaches 0. 100 | 101 | This approach avoids the need to track a car's position on the road, it simplifies the implementation of the model. 102 | 103 | This assumption allows us to keep the model simple, as we avoid having to consider the cars' positions on the road. 104 | 105 | ### Time on Road Function 106 | 107 | **TODO: DIAGRAM GOES HERE** 108 | 109 | `t[total] = A * max(0, n - N) + M` 110 | 111 | The graph above portrays a simplified yet reasonably possible time on road function. The time **M** represents the minimum length of time spent on road, regardless of the number of cars in a given lane. Similarly, **N** represents the "congestion-free" capacity of a lane of traffic. Once the number of cars grows beyond this number, the time on road increases proportionally to the number of cars in the lane. The number **A** represents the factor by which the time on road increases in response to one additional car on the road. 112 | 113 | ### Time Unit (tick) 114 | 115 | Due to this being a programmatic model, we introduce the notion of a **tick**. This serves two main purposes: 116 | 117 | - An arbitrary unit of time used in model 118 | - The smallest progression step (or increment) in the simulation where a change can occur. 119 | 120 | ### Simluation Overview 121 | 122 | **TODO: DIAGRAM GOES HERE** 123 | 124 | The diagram above represents the general design of the simluation. The key concepts are: 125 | 126 | - **Arrival Sequence:** A pre-generated sequence of boolean values based on an arrival probability 127 | - **Lane Assigner:** If a car arrives, it is assigned to a lane at random 128 | - **Lanes:** A number of **L** lanes that "contain" the cars. While there is no limit to the number of cars that can be placed in the lane, the total length of each car in the lane is subject to the *Time on Road function* 129 | - **Car:** Decides whether or not to change lanes based on the *Lane Change Conditions* 130 | - **Lane Exit:** Occurs for any car in any lane once **t[elapsed]** reaches **t[total]** 131 | - **Metrics Tracker:** Tracks the metrics, as defined earlier 132 | 133 | #### Behaviour on Tick 134 | 135 | Here is what happens on every tick: 136 | 137 | 1. An value is popped off the Arrival Sequence 138 | 2. If there is an arrival, the Lane Assigner assigns it to a Lane 139 | 3. Cars who have met their Lane Change Conditions change Lanes 140 | 4. The time remaining is re-calculated for all Cars in all Lanes 141 | 5. Cars who have reached zero time remaining exit the road 142 | 6. Steps 4 and 5 are repeated until no more Cars exit the road 143 | 144 | #### Input Conditions 145 | 146 | Here are the input conditions that are common to all simulation runs (i.e. our controlled variables): 147 | 148 | - **Arrival Probability:** For every tick, the likelihood of a car arriving. This value is close to 100% to simulate heavy traffic. 149 | - **Duration:** How long to run the simulation for 150 | - **Lane Change Delay:** If a car changes lanes, how much time is added to their total time in lane (in ticks). In other words, this is the time penalty for changing lanes 151 | - **Time on Road Function:** As described earlier. Defined as an input condition so that we can improve it in the future 152 | - **Lanes:** The number of lanes that the road is comprised of 153 | - **Iterations:** The number of iterations to run the simulation for 154 | 155 | Note that times are in ticks (as covered earlier). 156 | 157 | Here are the input conditions that can be different for every simulation run (i.e. independent variables): 158 | 159 | - **Lane Change Conditions:** if this is undefined, then no lane changes occur. If it is defined, the following values are set (as covered earlier): 160 | - **Minimum Time in Lane:** The minimum time spent in lane before a lane change is considered 161 | - **Minimum Time Saved Percentage:** The minimum time saved by changing lanes (as a percentage) 162 | 163 | ## Running the Simulations 164 | 165 | Through trial and error, we determine an appropriate set of conditions. The ideal parameters should result in: 166 | 167 | 1. The number of cars in lane exceeds the congestion-free capacity most of the time: This ensures that actual traffic congestion is 168 | 2. Most of the the cars that entered the road have exited it by the end of the simulation 169 | 170 | As we are interested in observing the effects of traffic congestion, #1 is necessary. On the other hand, #2 ensures we are modelling a functional road that is serves its purpose of "carrying" traffic through it. 171 | 172 | Based on the above, here are the Input Conditions that we have settled on: 173 | 174 | ```json 175 | { 176 | "arrivalProbability": 0.9, 177 | "durationInTicks": 10000, 178 | "laneChangeDelayTicks": 5, 179 | "numberOfIterations": 5, 180 | "numberOfLanes": 3, 181 | "timeOnRoadFunction": "2 * Math.max(0, n - 5) + 20" 182 | } 183 | ``` 184 | 185 | ### Results of Varying Lane Change Conditions on Traffic Performance 186 | 187 | We change the two Lane Change Conditions independently, starting with Minimum Time in Lane. 188 | 189 | #### Part A: Effect of changing Minimum Time in Lane 190 | 191 | Keeping the Time Saved Percentage constant, we change the Minimum Time in Lane. Note that the first row represents the case where no lane change conditions occur. 192 | 193 | | Min Time in Lane | Time Saved % | Avg Time | Cars Remain | Aggregate Time | Aggregate Throughput ( cars / 100 ticks) | 194 | | ---------------- | ------------ | -------- | ----------- | -------------- | ---------------------------------------- | 195 | | n/a | n/a | 26.5 | 19 | 238586 | 3.77 | 196 | | 0 | 60 | 51.9 | 74 | 467290 | 1.93 | 197 | | 5 | 60 | 36.8 | 23 | 330752 | 2.72 | 198 | | 10 | 60 | 27.2 | 45 | 244972 | 3.68 | 199 | | 15 | 60 | 25.1 | 19 | 225633 | 3.99 | 200 | | 20 | 60 | 24.6 | 19 | 221734 | 4.06 | 201 | | 25 | 60 | 24.2 | 21 | 218049 | 4.13 | 202 | | 30 | 60 | 24.7 | 22 | 222337 | 4.05 | 203 | | 99999999 | 60 | 26.5 | 19 | 238777 | 3.77 | 204 | 205 | **TODO: REPLACE TABLE WITH LINE CHART** 206 | 207 | Given that we set the "Time Saved %" lane change condition constant at 60%, the model yields an optimum result at a Min Time in Lane of around 25 ticks. Also note that at lower values, it performs worse than the no-lane-changes case. 208 | 209 | #### Part B: Effect of changing Time Saved Percentage 210 | 211 | Now, we change the Mininum Time in Lane, while keeping Time Saved Percentage constant. 212 | 213 | | Min Time in Lane | Time Saved % | Avg Time | Cars Remain | Aggregate Time | Aggregate Throughput ( cars / 100 ticks) | 214 | | ---------------- | ------------ | -------- | ----------- | -------------- | ---------------------------------------- | 215 | | n/a | n/a | 26.6 | 24 | 240121 | 3.76 | 216 | | 2 | 30 | 56.2 | 83 | 507291 | 1.78 | 217 | | 2 | 60 | 35.2 | 55 | 317806 | 2.84 | 218 | | 2 | 90 | 25.1 | 26 | 226751 | 3.98 | 219 | | 2 | 120 | 24.5 | 29 | 221222 | 4.08 | 220 | | 2 | 150 | 24.3 | 25 | 219458 | 4.12 | 221 | | 2 | 180 | 24.3 | 28 | 219550 | 4.11 | 222 | | 2 | 210 | 24.2 | 21 | 218634 | 4.13 | 223 | | 2 | 99999999 | 24.2 | 21 | 218670 | 4.13 | 224 | 225 | **TODO: REPLACE TABLE WITH LINE CHART** 226 | 227 | Similar to Part A, the simulation performs worse than the no-ane-changes case at lower values of "Time Saved %". Also, the simulation yields a better outcome as the "Time Saved Percentage" lane change condition is increased. The gains begin to taper off at 210% and a throughput of 4.15. (*) 228 | 229 | (*) Lane changes occur even at a very high "Time Saved %" threshold, because a lane change will always occur when the time remaining in the new lane is zero. 230 | 231 | ### Conclusion 232 | 233 | The results yield three key conclusions: 234 | 235 | 1. Depending on input or lane change conditions, opportunistic lane changing can yield either a better or worse outcome than no lane changing 236 | 2. There is an optimal combination of opportunistic lane change conditions that yield the best outcome possible 237 | 3. Opportunistic lane changing can yield a significantly better outcome than no lane changing (~10%). Further simulation runs can determine a combined optimum for both lane change conditions that potentially yields an even better improvement than identified here. 238 | 239 | If opportunistic lane changes in congested traffic were a zero-sum game, we would yield the same set of metrics for all simulation runs. Instead, we discovered that opportunistic lane changing to be both less performant and more performant than no lane changing. 240 | 241 | Furthermore, a net-positive result for all drivers seems reasonably achievable, even while pursuing an opportunistic approach (specifically, the one we described here). 242 | 243 | Therefore, we can conclude that generally speaking, **opportunistic lane changing is not a zero-sum gain**, and can benefit all drivers on the road if exercised judiciously. This means not changing lanes too frequently (i.e. adhering to a reasonable minimum time in lane), and only changing lanes if it saves a signicant amount of time (i.e. the time saved is 90% or higher). 244 | 245 | #### Next Steps 246 | 247 | "Be patient. Change lanes, but not frivolously. Everybody wins." 248 | 249 | This is essentially what the results tell us. Experienced drivers will likely not find this conclusion surprising. 250 | 251 | Perhaps the model could unlock more benefit by having actual traffic data mapped to it. Doing so could yield a more concrete optimum range given varying conditions that we had held constant in our investigation. For example, modelling various traffic conditions by defining a different **Time on Road Function**. 252 | 253 | Moreover, we have only begun to explore the potential of self-driving cars, in the context of the model. A random lane assignment with no lane changes will inevitably lead to imbalanced lanes. This strongly suggests that a lane-levelling algorithm can be a significant performance improvement over the no-lane-changes case. 254 | 255 | Therefore, it would be potentially valuable to investigate how such an algorithm compares to an opportunistic lane changing strategy. This is what we aim to pursue in the second part of this article. Stay tuned! 256 | 257 | #### Source Code 258 | 259 | The model is written in Node JS and is available under an open-source MIT license: 260 | 261 | https://github.com/connected-psobocinski/game-theory-traffic-modelling 262 | 263 | **TODO: MOVE TO PUBLIC REPO** 264 | -------------------------------------------------------------------------------- /docs/NOTES.md: -------------------------------------------------------------------------------- 1 | 2 | ## I. Calculating time on road 3 | 4 | number of lanes = 2 5 | 6 | time on road: `t = f(n)`, where: 7 | - `n`: number of vehicles in current lane (variable, used by simulation) 8 | 9 | Possible time on road functions: 10 | - Linear: `A * Math.max(0, n - N) + M` 11 | - Quadratic: `A * Math.pow(Math.max(0, n - N), 2) + M` 12 | 13 | Note that the constants `A, M, and N` must be replaced with non-zero numeric values prior to running the simulation. 14 | The constant `M` and `N` have special meaning: 15 | - `M` is the minimum amount of time in the lane; 16 | - `N` is the max number of cars where a constant time `M` is spent in the lane 17 | represents the minimum amount of time in lane (even when empty). 18 | 19 | Example function definitions: 20 | - Linear: `2 * Math.max(0, n - 5) + 20` 21 | - Quadratic: `0.5 * Math.pow(Math.max(0, n - 5), 2) + 20` 22 | 23 | ## II. Behaviour Modelling 24 | 25 | ### 1. Random 26 | 27 | - Car is placed in a lane at random 28 | - No lane changes occur. 29 | 30 | ### 2. Auto-levelling 31 | 32 | - Car is placed in whichever lane results in the smallest difference in cars between lanes 33 | - No lane changes occur. 34 | 35 | ### 3. Greedy drivers 36 | 37 | 1. Lane is changed when: 38 | - Minimum time t(ticks) spent in lane, AND 39 | - Remaining time in lane has increased beyond a minimum percentage 40 | 1. New car then enters lane with lowest number of cars ("levelled" approach) 41 | 42 | TODO: allow switching between levelled and random approach for car arrival via config. 43 | 44 | ## Recalculating remaining time on road 45 | 46 | Whenever number of cars in lane changes, the time remaining for every car is recalculated as follows: 47 | 48 | t(remaining) = Min\[t(recalculated) - t(elapsed), 0\] 49 | 50 | where t(recalculated) uses the original formula in section I. 51 | 52 | ## On tick 53 | 54 | The 'tick' represents the smallest modelled unit of time. On every tick, the following occurs: 55 | 56 | 1. The length of elapsed time for all cars on the road is incremented by 1 57 | 2. The remaining time is recalculated. If equal to or less than 0, those cars are removed from the road 58 | 3. Repeat step 2 until there are no cars removed from the road 59 | 4. Calculate throughput metric for steps 2 to 3 60 | 61 | ## Output metrics 62 | 63 | 1. Throughput per tick = N / T 64 | - N is total number of vehicles exited on that tick. 65 | - T is total time spent on road by all vehicles that exited on that tick. 66 | 2. Average throughput per tick = average of all throughputs per tick for the length of the simulation 67 | 3. Total throughput = Σ(N) / Σ(T) for the length of the simulation 68 | -------------------------------------------------------------------------------- /docs/traffic-modelling-running-simulation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psobocinski/traffic-modelling/495eb49dec526faf931387bc4334b240a10c698c/docs/traffic-modelling-running-simulation.gif -------------------------------------------------------------------------------- /lib/CommonConfigInitializer.js: -------------------------------------------------------------------------------- 1 | 2 | const fs = require('fs'); 3 | const { log } = require('./utils'); 4 | 5 | class CommonConfigInitializer { 6 | static initialize() { 7 | const templateFileName = './config/common.sample.json'; 8 | const destinationFileName = './config/common.json'; 9 | 10 | if (!fs.existsSync(destinationFileName)) fs.copyFileSync(templateFileName, destinationFileName); 11 | 12 | log(`Common config initialized in ${destinationFileName}:`); 13 | log(`${fs.readFileSync(destinationFileName)}`); 14 | } 15 | } 16 | 17 | module.exports = CommonConfigInitializer; 18 | -------------------------------------------------------------------------------- /lib/ConfigValidator.js: -------------------------------------------------------------------------------- 1 | 2 | const validators = { 3 | integer: input => (Number.isInteger(parseFloat(input)) ? true : 'Must be an integer'), 4 | number: input => (Number.isNaN(parseFloat(input)) ? 'Must be a number' : true), 5 | }; 6 | 7 | function validateGreaterThanOrEqualTo(minimum, number) { 8 | return parseFloat(number) < minimum ? `${number} must be greater than or equal to ${minimum}` : null; 9 | } 10 | 11 | function validateGreaterThan(minimum, number) { 12 | return parseFloat(number) <= minimum ? `${number} must be greater than ${minimum}` : null; 13 | } 14 | 15 | class ConfigValidator { 16 | constructor(variablesConfig) { 17 | this.variablesConfig = variablesConfig; 18 | } 19 | 20 | validateSmallestValue(input, { independentVariable }) { 21 | const variableConfig = this.validationConfigFor(independentVariable); 22 | 23 | return validateGreaterThanOrEqualTo(variableConfig.minimum, input) 24 | || ConfigValidator.validatorFor(variableConfig)(input); 25 | } 26 | 27 | validateIncrement(input, { independentVariable }) { 28 | const variableConfig = this.validationConfigFor(independentVariable); 29 | 30 | return validateGreaterThan(0, input) 31 | || ConfigValidator.validatorFor(variableConfig)(input); 32 | } 33 | 34 | validationConfigFor(variableName) { 35 | return this.variablesConfig.find(({ name }) => name === variableName); 36 | } 37 | 38 | static validateNumberOfSimulations(input) { 39 | return validateGreaterThan(0, input) 40 | || validators.integer(input); 41 | } 42 | 43 | static validatorFor({ type }) { 44 | return validators[type]; 45 | } 46 | } 47 | 48 | module.exports = ConfigValidator; 49 | -------------------------------------------------------------------------------- /lib/Simulation.js: -------------------------------------------------------------------------------- 1 | 2 | const { calculateAverageResult } = require('./utils'); 3 | 4 | class Simulation { 5 | constructor({ numberOfIterations }, config, executionModel) { 6 | this.config = config; 7 | this.executionModel = executionModel; 8 | this.numberOfIterations = numberOfIterations; 9 | } 10 | 11 | run() { 12 | const iterationResults = Array(this.numberOfIterations).fill().map(() => this.runIteration()); 13 | 14 | return calculateAverageResult(iterationResults); 15 | } 16 | 17 | runIteration() { 18 | return this.executionModel.run(this.config); 19 | } 20 | } 21 | 22 | module.exports = Simulation; 23 | -------------------------------------------------------------------------------- /lib/SimulationRunner.js: -------------------------------------------------------------------------------- 1 | 2 | const { createResultsFile } = require('./utils/fileCreators'); 3 | const Simulation = require('./Simulation'); 4 | const TableGenerator = require('./TableGenerator'); 5 | 6 | class SimulationRunner { 7 | constructor(commonConfig, simulationsConfig, executionModel) { 8 | this.commonConfig = commonConfig; 9 | this.simulationsConfig = simulationsConfig; 10 | this.executionModel = executionModel; 11 | } 12 | 13 | run() { 14 | let config; 15 | const simulations = []; 16 | 17 | this.simulationsConfig.forEach((simulationConfig) => { 18 | config = Object.assign({}, this.commonConfig.simulationDefaults, simulationConfig); 19 | simulations.push( 20 | new Simulation(this.commonConfig, config, this.executionModel), 21 | ); 22 | }); 23 | 24 | const simulationResults = simulations.map(simulation => simulation.run()); 25 | const table = (new TableGenerator(this.commonConfig.output)).generate(simulationResults); 26 | 27 | createResultsFile(this.commonConfig.output, table); 28 | } 29 | } 30 | 31 | module.exports = SimulationRunner; 32 | -------------------------------------------------------------------------------- /lib/SimulationsConfigurator.js: -------------------------------------------------------------------------------- 1 | 2 | const inquirer = require('inquirer'); 3 | const ConfigValidator = require('./ConfigValidator'); 4 | const { createConfigFile } = require('./utils/fileCreators'); 5 | 6 | function buildSimulationConfig(independentVariable, value) { 7 | return { [independentVariable]: value }; 8 | } 9 | 10 | function buildSimulationsConfig( 11 | independentVariable, 12 | smallestValue, 13 | increment, 14 | numberOfSimulations, 15 | ) { 16 | return Array(numberOfSimulations) 17 | .fill() 18 | .map( 19 | (_, index) => buildSimulationConfig( 20 | independentVariable, 21 | smallestValue + increment * index, 22 | ), 23 | ); 24 | } 25 | 26 | function processAnswers({ 27 | independentVariable, smallestValue, increment, numberOfSimulations, 28 | }) { 29 | const simulations = buildSimulationsConfig( 30 | independentVariable, 31 | parseFloat(smallestValue), 32 | parseFloat(increment), 33 | parseInt(numberOfSimulations, 10), 34 | ); 35 | 36 | createConfigFile('simulations.json', simulations); 37 | } 38 | 39 | class SimulationsConfigurator { 40 | constructor(independentVariables) { 41 | this.independentVariables = independentVariables; 42 | this.validator = new ConfigValidator(independentVariables); 43 | this.prompt = inquirer.createPromptModule(); 44 | } 45 | 46 | configure() { 47 | this.prompt(this.buildQuestions()).then(processAnswers); 48 | } 49 | 50 | buildQuestions() { 51 | return [ 52 | { 53 | type: 'list', 54 | name: 'independentVariable', 55 | message: 'Select the independent variable', 56 | choices: this.independentVariables.map(({ name }) => name), 57 | }, 58 | { 59 | name: 'smallestValue', 60 | message: 'Set variable\'s smallest value', 61 | validate: this.validator.validateSmallestValue.bind(this.validator), 62 | }, 63 | { 64 | name: 'increment', 65 | message: 'Set variable\'s increment', 66 | validate: this.validator.validateIncrement.bind(this.validator), 67 | }, 68 | { 69 | name: 'numberOfSimulations', 70 | message: 'Set number of simulations', 71 | validate: this.validator.constructor.validateNumberOfSimulations, 72 | }, 73 | ]; 74 | } 75 | } 76 | 77 | module.exports = SimulationsConfigurator; 78 | -------------------------------------------------------------------------------- /lib/TableGenerator.js: -------------------------------------------------------------------------------- 1 | 2 | const formatters = { 3 | header: { 4 | csv: row => row.join(','), 5 | markdown: row => `| ${row.join(' | ')} |\n| ${Array(row.length).fill('---').join(' | ')} |`, 6 | }, 7 | row: { 8 | csv: row => row.join(','), 9 | markdown: row => `| ${row.join(' | ')} |`, 10 | 11 | }, 12 | }; 13 | 14 | class TableGenerator { 15 | constructor({ columns, format }) { 16 | this.headerFormatter = formatters.header[format]; 17 | this.rowFormatter = formatters.row[format]; 18 | this.rowHeaderLabels = columns.map(([, label]) => label); 19 | } 20 | 21 | generate(results) { 22 | const rows = results.map(result => this.rowFormatter(result)); 23 | const rowHeaders = [this.headerFormatter(this.rowHeaderLabels)]; 24 | 25 | return rowHeaders.concat(rows).join('\n'); 26 | } 27 | } 28 | 29 | module.exports = TableGenerator; 30 | -------------------------------------------------------------------------------- /lib/traffic-model/ArrivalSequence.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | class ArrivalSequence { 4 | constructor({ durationInTicks, arrivalProbability }) { 5 | this.durationInTicks = durationInTicks; 6 | this.arrivalProbability = arrivalProbability; 7 | } 8 | 9 | isArrival() { 10 | return Math.random() < this.arrivalProbability; 11 | } 12 | 13 | generate() { 14 | const arrivalSequence = []; 15 | 16 | for (let i = 0; i < this.durationInTicks; i += 1) { 17 | if (this.isArrival()) arrivalSequence.push(true); 18 | else arrivalSequence.push(false); 19 | } 20 | 21 | return arrivalSequence; 22 | } 23 | } 24 | 25 | module.exports = ArrivalSequence; 26 | -------------------------------------------------------------------------------- /lib/traffic-model/Car.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | class Car { 4 | constructor(timeOnRoadCalculator, totalCarsInLane) { 5 | this.timeOnRoadCalculator = timeOnRoadCalculator; 6 | this.elapsedTimeInLane = 0; 7 | this.elapsedTime = 0; 8 | this.timeSpentOnLaneChanges = 0; 9 | this.totalTime = this.timeOnRoadCalculator(totalCarsInLane); 10 | this.priorRemainingTime = this.remainingTime(); 11 | } 12 | 13 | reCalculateTotalTime(totalCarsInLane) { 14 | this.priorRemainingTime = this.remainingTime(); 15 | this.totalTime = this.calculateTotalTimeFor(totalCarsInLane); 16 | } 17 | 18 | adjustTimesForLaneChange(laneChangeDelayTicks) { 19 | this.elapsedTimeInLane = 0; 20 | this.timeSpentOnLaneChanges += laneChangeDelayTicks; 21 | } 22 | 23 | remainingTime() { 24 | return Math.max(this.totalTime - this.elapsedTime, 0); 25 | } 26 | 27 | timeDelayPercent() { 28 | return (this.remainingTime() - this.priorRemainingTime) / this.priorRemainingTime * 100; 29 | } 30 | 31 | timeSpeedupPercent(shorterLaneLength, laneChangeDelayTicks) { 32 | const totalTimeInShorterLane = this.calculateTotalTimeFor(shorterLaneLength) 33 | + laneChangeDelayTicks; 34 | const remainingTimeInShorterLane = Math.max(totalTimeInShorterLane - this.elapsedTime, 0); 35 | 36 | return (this.remainingTime() - remainingTimeInShorterLane) / remainingTimeInShorterLane * 100; 37 | } 38 | 39 | calculateTotalTimeFor(laneLength) { 40 | return this.timeOnRoadCalculator(laneLength) + this.timeSpentOnLaneChanges; 41 | } 42 | 43 | tick() { 44 | this.elapsedTime += 1; 45 | this.elapsedTimeInLane += 1; 46 | } 47 | } 48 | 49 | module.exports = Car; 50 | -------------------------------------------------------------------------------- /lib/traffic-model/LaneAssigner.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | class LaneAssigner { 4 | constructor({ lanePlacement }) { 5 | const lanePlacementBehaviours = { 6 | levelled: LaneAssigner.indexOfShortestLane, 7 | random: LaneAssigner.indexOfRandomLane, 8 | }; 9 | 10 | this.lanePlacementBehaviour = lanePlacementBehaviours[lanePlacement]; 11 | } 12 | 13 | chooseLaneIndex(lanes) { 14 | return this.lanePlacementBehaviour(lanes); 15 | } 16 | 17 | static indexOfRandomLane(lanes) { 18 | return Math.floor(Math.random() * lanes.length); 19 | } 20 | 21 | static indexOfShortestLane(lanes) { 22 | return lanes.reduce(({ shortestIndex, shortestLength }, cars, index) => { 23 | let currentShortestIndex = shortestIndex; 24 | let currentShortestLength = shortestLength; 25 | 26 | if ( 27 | shortestIndex === undefined || cars.length < shortestLength 28 | ) [currentShortestIndex, currentShortestLength] = [index, cars.length]; 29 | 30 | return { shortestIndex: currentShortestIndex, shortestLength: currentShortestLength }; 31 | }, {}).shortestIndex; 32 | } 33 | } 34 | 35 | module.exports = LaneAssigner; 36 | -------------------------------------------------------------------------------- /lib/traffic-model/LaneChangeDecider.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const { isSet } = require('../utils'); 4 | 5 | class LaneChangeDecider { 6 | constructor({ 7 | laneChangeDelayTicks = 0, minTicksInLane, minTickDelayPercent, minTickSpeedupPercent, 8 | }) { 9 | this.laneChangeDelayTicks = laneChangeDelayTicks; 10 | this.minTicksInLane = minTicksInLane; 11 | this.minTickDelayPercent = minTickDelayPercent; 12 | this.minTickSpeedupPercent = minTickSpeedupPercent; 13 | } 14 | 15 | hasLaneChangeConditions() { 16 | return isSet(this.minTicksInLane) 17 | || isSet(this.minTickDelayPercent) 18 | || isSet(this.minTickSpeedupPercent); 19 | } 20 | 21 | isChangingLanes(car, shorterLaneLength) { 22 | return this.isExceededMinTimeInLane(car) 23 | && this.isExceededMinLaneDelay(car) 24 | && this.isExceededMinTimeSaved(car, shorterLaneLength); 25 | } 26 | 27 | isExceededMinTimeSaved(car, shorterLaneLength) { 28 | if (!isSet(this.minTickSpeedupPercent)) return true; 29 | 30 | return car.timeSpeedupPercent(shorterLaneLength, this.laneChangeDelayTicks) 31 | >= this.minTickSpeedupPercent; 32 | } 33 | 34 | isExceededMinLaneDelay(car) { 35 | if (!isSet(this.minTickDelayPercent)) return true; 36 | 37 | return car.timeDelayPercent() >= this.minTickDelayPercent; 38 | } 39 | 40 | isExceededMinTimeInLane(car) { 41 | if (!isSet(this.minTicksInLane)) return true; 42 | 43 | return car.elapsedTimeInLane >= this.minTicksInLane; 44 | } 45 | } 46 | 47 | module.exports = LaneChangeDecider; 48 | -------------------------------------------------------------------------------- /lib/traffic-model/MetricsCalculator.js: -------------------------------------------------------------------------------- 1 | 2 | const { isSet, log } = require('../utils'); 3 | 4 | class MetricsCalculator { 5 | constructor( 6 | { output }, 7 | { 8 | laneChangeDelayTicks, 9 | lanePlacement, 10 | minTickDelayPercent, 11 | minTicksInLane, 12 | minTickSpeedupPercent, 13 | numberOfLanes, 14 | }, 15 | arrivalSequence, 16 | ) { 17 | this.columnMetrics = output.columns.map(([metric]) => metric); 18 | 19 | // SIMULATION METRICS 20 | this.simulationTime = () => arrivalSequence.length; 21 | this.carsEntered = () => arrivalSequence.filter(arrival => arrival).length; 22 | 23 | // INDEPENDENT VARIABLES 24 | this.laneChangeDelayTicks = () => laneChangeDelayTicks; 25 | this.lanePlacement = () => lanePlacement; 26 | this.minTickDelayPercent = () => minTickDelayPercent; 27 | this.minTicksInLane = () => minTicksInLane; 28 | this.minTickSpeedupPercent = () => minTickSpeedupPercent; 29 | this.numberOfLanes = () => numberOfLanes; 30 | 31 | this.carsExited = 0; 32 | this.carsExitedAggregateTime = 0; 33 | this.carsRemainingAggregateTime = null; 34 | this.throughputsPerTick = []; 35 | this.laneChanges = 0; 36 | } 37 | 38 | // DEPENDENT VARIABLES 39 | // - averageTimeOnRoad() 40 | // - carsRemaining() 41 | // - carsOnRoadAggregateTime() 42 | // - totalLaneChanges() 43 | // - overallThroughput() 44 | // - averageThroughput() 45 | 46 | averageTimeOnRoad() { 47 | const avgTime = this.carsExitedAggregateTime / this.carsExited; 48 | 49 | return Math.round(avgTime * 10) / 10; 50 | } 51 | 52 | carsRemaining() { 53 | return this.carsEntered() - this.carsExited; 54 | } 55 | 56 | carsOnRoadAggregateTime() { 57 | return this.carsExitedAggregateTime + this.carsRemainingAggregateTime; 58 | } 59 | 60 | totalLaneChanges() { 61 | return this.laneChanges; 62 | } 63 | 64 | overallThroughput() { 65 | const throughput = this.carsExited / this.carsExitedAggregateTime * 100; 66 | 67 | return Math.round(throughput * 100) / 100; 68 | } 69 | 70 | averageThroughput() { 71 | if (this.throughputsPerTick.length === 0) return 0; 72 | 73 | const avgThroughput = this.throughputsPerTick.reduce((sum, throughput) => sum + throughput) 74 | / this.throughputsPerTick.length 75 | * 100; 76 | 77 | return Math.round(avgThroughput * 100) / 100; 78 | } 79 | 80 | tick(carsExited) { 81 | const carsExitedForTick = carsExited.length; 82 | const carsExitedTotalTime = carsExited.reduce((sum, car) => sum + car.totalTime, 0); 83 | const throughputForTick = carsExitedTotalTime ? carsExitedForTick / carsExitedTotalTime : 0; 84 | 85 | this.carsExited += carsExitedForTick; 86 | this.carsExitedAggregateTime += carsExitedTotalTime; 87 | this.throughputsPerTick.push(throughputForTick); 88 | } 89 | 90 | complete(road) { 91 | this.carsRemainingAggregateTime = road.lanes.reduce( 92 | (laneSum, cars) => laneSum + cars.reduce((carSum, car) => carSum + car.elapsedTime, 0), 0, 93 | ); 94 | 95 | this.printSimulationInfo(); 96 | this.printMetrics(); 97 | this.printDebugMetrics(road); 98 | 99 | return this.results(); 100 | } 101 | 102 | results() { 103 | return this.columnMetrics.map(metric => this[metric]()); 104 | } 105 | 106 | printSimulationInfo() { 107 | const laneChangeProperties = ['minTickDelayPercent', 'minTicksInLane', 'minTickSpeedupPercent']; 108 | const laneChangeConditions = laneChangeProperties.reduce((acc, laneChangeProperty) => { 109 | const propValue = this[laneChangeProperty](); 110 | if (isSet(propValue)) acc[laneChangeProperty] = propValue; 111 | return acc; 112 | }, {}); 113 | 114 | log('\nLane placement', this.lanePlacement()); 115 | log('Lane Change Delay', this.laneChangeDelayTicks()); 116 | log('Lane Change Conditions', laneChangeConditions); 117 | } 118 | 119 | printMetrics() { 120 | log('Average Time', this.averageTimeOnRoad()); 121 | log('Cars Remaining', this.carsRemaining()); 122 | log('Aggregate Time', this.carsOnRoadAggregateTime()); 123 | log('Aggregate Throughput', this.overallThroughput()); 124 | } 125 | 126 | printDebugMetrics(road) { 127 | const remainingCarCounts = road.lanes.map(cars => cars.length); 128 | 129 | log('Average Throughput per tick', this.averageThroughput()); 130 | log('Number of Lane changes', this.totalLaneChanges()); 131 | log('Remaining cars in lanes:', remainingCarCounts); 132 | } 133 | } 134 | 135 | module.exports = MetricsCalculator; 136 | -------------------------------------------------------------------------------- /lib/traffic-model/Road.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const Car = require('./Car'); 4 | const { arrayDifference, defineFunction, newArray2D } = require('../utils'); 5 | 6 | class Road { 7 | constructor( 8 | { laneChangeDelayTicks, timeOnRoadFunction, numberOfLanes }, 9 | laneChangeDecider, 10 | metricsCalculator, 11 | ) { 12 | this.laneChangeDelayTicks = laneChangeDelayTicks; 13 | this.timeOnRoadCalculator = defineFunction('n', timeOnRoadFunction); 14 | this.laneChangeDecider = laneChangeDecider; 15 | this.metricsCalculator = metricsCalculator; 16 | this.lanes = newArray2D(numberOfLanes); 17 | } 18 | 19 | addCar(laneIndex) { 20 | const addToLane = this.lanes[laneIndex]; 21 | const carToAdd = new Car(this.timeOnRoadCalculator, addToLane.length); 22 | 23 | addToLane.push(carToAdd); 24 | } 25 | 26 | tick() { 27 | const reCalculateTotalTimeForCar = (car, _, cars) => car.reCalculateTotalTime(cars.length); 28 | let exitingCars = []; 29 | let newlyExitingCars; 30 | let isDoneAdjustingLanes; 31 | 32 | // 1. Increment the elapsedTime for all cars on the road by 1 33 | this.lanes.forEach(cars => cars.forEach(car => car.tick())); 34 | 35 | for (let laneIndex = 0; laneIndex < this.lanes.length; laneIndex += 1) { 36 | let cars = this.lanes[laneIndex]; 37 | 38 | isDoneAdjustingLanes = false; 39 | 40 | while (!isDoneAdjustingLanes) { 41 | // 2. If remainingTime equal to 0, those cars exit the road 42 | newlyExitingCars = cars.filter(car => car.remainingTime() === 0); 43 | 44 | // 3. If any cars founding exiting the lane in step 3: 45 | if (newlyExitingCars.length > 0) { 46 | // a: Track exiting cars 47 | exitingCars = exitingCars.concat(newlyExitingCars); 48 | 49 | // b: Remove exited cars from the lane 50 | cars = arrayDifference(cars, newlyExitingCars); 51 | 52 | // c: re-calculate totalTime for all cars in that lane 53 | cars.forEach(reCalculateTotalTimeForCar); 54 | } else { 55 | // Otherwise (no more cars exited), exit the loop 56 | isDoneAdjustingLanes = true; 57 | } 58 | } 59 | 60 | this.lanes[laneIndex] = cars; 61 | } 62 | 63 | // 5. Record metrics 64 | this.metricsCalculator.tick(exitingCars); 65 | } 66 | 67 | conditionallyChangeLanes() { 68 | if (!this.laneChangeDecider.hasLaneChangeConditions()) return; 69 | 70 | const carsToSwitch = newArray2D(this.lanes.length); let 71 | shorterLaneIndex; 72 | 73 | this.lanes.forEach( 74 | (cars, laneIndex) => { 75 | carsToSwitch[laneIndex] = cars.filter( 76 | (car) => { 77 | shorterLaneIndex = this.shortestAdjacentLaneIndexFor(laneIndex); 78 | 79 | return this.laneChangeDecider.isChangingLanes(car, this.lanes[shorterLaneIndex].length); 80 | }, 81 | ); 82 | }, 83 | ); 84 | 85 | this.changeLanes(carsToSwitch); 86 | } 87 | 88 | changeLanes(carsToSwitch) { 89 | let carIndex; 90 | let newLaneIndex; 91 | 92 | carsToSwitch.forEach((cars, laneIndex) => { 93 | cars.forEach((car) => { 94 | carIndex = this.lanes[laneIndex].indexOf(car); 95 | newLaneIndex = this.shortestAdjacentLaneIndexFor(laneIndex); 96 | 97 | // 1. remove from original lane 98 | this.lanes[laneIndex].splice(carIndex, 1); 99 | 100 | // 2. add to new lane 101 | this.lanes[newLaneIndex].push(car); 102 | 103 | // 3. reset elapsed time in lane and add delay for lane change 104 | car.adjustTimesForLaneChange(this.laneChangeDelayTicks); 105 | 106 | // 4. re-calculate total time 107 | car.reCalculateTotalTime(this.lanes[newLaneIndex].length); 108 | 109 | // 5. track lane change 110 | this.metricsCalculator.laneChanges += 1; 111 | }); 112 | }); 113 | } 114 | 115 | shortestAdjacentLaneIndexFor(laneIndex) { 116 | const adjacentLaneIndexes = this.adjacentLaneIndexesFor(laneIndex); 117 | 118 | if (adjacentLaneIndexes.length === 1) return adjacentLaneIndexes[0]; 119 | 120 | const [leftLaneIndex, rightLaneIndex] = adjacentLaneIndexes; 121 | const leftLaneLength = this.lanes[leftLaneIndex].length; 122 | const rightLaneLength = this.lanes[rightLaneIndex].length; 123 | 124 | return rightLaneLength < leftLaneLength ? rightLaneIndex : leftLaneIndex; 125 | } 126 | 127 | adjacentLaneIndexesFor(laneIndex) { 128 | const leftLaneIndex = laneIndex - 1; 129 | const rightLaneIndex = laneIndex + 1; 130 | 131 | if (laneIndex === 0) return [rightLaneIndex]; 132 | if (laneIndex === this.lanes.length - 1) return [leftLaneIndex]; 133 | return [leftLaneIndex, rightLaneIndex]; 134 | } 135 | } 136 | 137 | module.exports = Road; 138 | -------------------------------------------------------------------------------- /lib/traffic-model/TrafficModel.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const Road = require('./Road'); 4 | const MetricsCalculator = require('./MetricsCalculator'); 5 | const ArrivalSequence = require('./ArrivalSequence'); 6 | const LaneAssigner = require('./LaneAssigner'); 7 | const LaneChangeDecider = require('./LaneChangeDecider'); 8 | 9 | class TrafficModel { 10 | constructor(commonConfig) { 11 | this.arrivalSequence = (new ArrivalSequence(commonConfig)).generate(); 12 | this.commonConfig = commonConfig; 13 | } 14 | 15 | run(config) { 16 | const metricsCalculator = new MetricsCalculator( 17 | this.commonConfig, 18 | config, this.arrivalSequence, 19 | ); 20 | const laneAssigner = new LaneAssigner(config); 21 | const laneChangeDecider = new LaneChangeDecider(config); 22 | const road = new Road(config, laneChangeDecider, metricsCalculator); 23 | 24 | this.arrivalSequence.forEach(isArrival => TrafficModel.tick(isArrival, laneAssigner, road)); 25 | 26 | return metricsCalculator.complete(road); 27 | } 28 | 29 | static tick(isArrival, laneAssigner, road) { 30 | if (isArrival) road.addCar(laneAssigner.chooseLaneIndex(road.lanes)); 31 | 32 | road.conditionallyChangeLanes(); 33 | road.tick(); 34 | } 35 | } 36 | 37 | module.exports = TrafficModel; 38 | -------------------------------------------------------------------------------- /lib/traffic-model/independentVariables.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "laneChangeDelayTicks", 4 | "type": "number" 5 | }, 6 | { 7 | "name": "minTicksInLane", 8 | "type": "integer", 9 | "minimum": 0 10 | }, 11 | { 12 | "name": "minTickSpeedupPercent", 13 | "type": "number" 14 | }, 15 | { 16 | "name": "numberOfLanes", 17 | "type": "integer", 18 | "minimum": 2 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /lib/utils/fileCreators.js: -------------------------------------------------------------------------------- 1 | 2 | const fs = require('fs'); 3 | const { log } = require('./index'); 4 | 5 | function writeFile(pathAndFileName, fileContent) { 6 | fs.writeFile(pathAndFileName, `${fileContent}\n`, (error) => { 7 | if (error) log(error); 8 | 9 | log(`Created file: ${pathAndFileName}`); 10 | }); 11 | } 12 | 13 | function createConfigFile(filename, config) { 14 | const pathAndFileName = `./config/${filename}`; 15 | const fileContent = JSON.stringify(config, null, 2); 16 | 17 | writeFile(pathAndFileName, fileContent); 18 | } 19 | 20 | function createResultsFile({ path, format }, results) { 21 | const pathAndFileName = `${path}/results.${format}`; 22 | 23 | writeFile(pathAndFileName, results); 24 | } 25 | 26 | module.exports = { createConfigFile, createResultsFile }; 27 | -------------------------------------------------------------------------------- /lib/utils/index.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | function arrayDifference(arr1, arr2) { 4 | return (arr1.filter(x => !arr2.includes(x))); 5 | } 6 | 7 | function calculateAverageResult(results) { 8 | const resultSums = (new Array(results[0].length)).fill(null); 9 | 10 | results.forEach(resultRow => resultRow.forEach((resultCellValue, index) => { 11 | if (typeof resultCellValue === 'number') resultSums[index] += resultCellValue; 12 | })); 13 | 14 | return resultSums.map(resultSum => (typeof resultSum === 'number' 15 | ? Math.round(resultSum / results.length * 100) / 100 16 | : undefined)); 17 | } 18 | 19 | function isSet(value) { 20 | return ![null, undefined].includes(value); 21 | } 22 | 23 | function log(...args) { 24 | // eslint-disable-next-line no-console 25 | console.log(...args); 26 | } 27 | 28 | function defineFunction(parameter, functionString) { 29 | // eslint-disable-next-line no-new-func 30 | return new Function(parameter, `return ${functionString}`); 31 | } 32 | 33 | function newArray2D(arrayLength) { 34 | const newArray = []; 35 | 36 | for (let i = 0; i < arrayLength; i += 1) newArray[i] = []; 37 | 38 | return newArray; 39 | } 40 | 41 | module.exports = { 42 | arrayDifference, 43 | calculateAverageResult, 44 | defineFunction, 45 | isSet, 46 | log, 47 | newArray2D, 48 | }; 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "traffic-modelling", 3 | "version": "1.0.0", 4 | "description": "A simple computer model for simulating traffic congestion.", 5 | "scripts": { 6 | "configure": "node scripts/configure.js", 7 | "simulate": "node scripts/simulate.js", 8 | "specs": "jest", 9 | "lint": "eslint .", 10 | "start": "npm run configure ; npm run simulate", 11 | "test": "npm run specs && npm run lint" 12 | }, 13 | "author": "Paul Sobocinski", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "eslint": "^5.16.0", 17 | "eslint-config-airbnb-base": "^13.2.0", 18 | "eslint-plugin-import": "^2.18.2", 19 | "jest": "^24.8.0" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/psobocinski/traffic-modelling" 24 | }, 25 | "dependencies": { 26 | "inquirer": "^6.5.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /results/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore 5 | -------------------------------------------------------------------------------- /scripts/configure.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const independentVariables = require('../lib/traffic-model/independentVariables'); 4 | const CommonConfigInitializer = require('../lib/CommonConfigInitializer'); 5 | const SimulationsConfigurator = require('../lib/SimulationsConfigurator'); 6 | 7 | const configurator = new SimulationsConfigurator(independentVariables); 8 | 9 | CommonConfigInitializer.initialize(); 10 | configurator.configure(); 11 | -------------------------------------------------------------------------------- /scripts/simulate.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const commonConfig = require('../config/common'); 4 | const simulationsConfig = require('../config/simulations'); 5 | const SimulationRunner = require('../lib/SimulationRunner'); 6 | const TrafficModel = require('../lib/traffic-model/TrafficModel'); 7 | 8 | const trafficModel = new TrafficModel(commonConfig); 9 | const runner = new SimulationRunner(commonConfig, simulationsConfig, trafficModel); 10 | 11 | runner.run(); 12 | -------------------------------------------------------------------------------- /tests/lib/traffic-model/LaneAssigner.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const LaneAssigner = require('../../../lib/traffic-model/LaneAssigner'); 4 | 5 | describe('LaneAssigner', () => { 6 | describe('indexOfShortestLane', () => { 7 | let lanes, index, c; 8 | 9 | it('returns the index of shortest lane', () => { 10 | lanes = [ [c], [] ]; 11 | 12 | index = LaneAssigner.indexOfShortestLane(lanes); 13 | 14 | expect(index).toEqual(1); 15 | }); 16 | 17 | it('returns the lowest index when lanes have same length', () => { 18 | lanes = [ [c, c], [c, c] ]; 19 | 20 | index = LaneAssigner.indexOfShortestLane(lanes); 21 | 22 | expect(index).toEqual(0); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /tests/lib/traffic-model/LaneChangeDecider.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const LaneChangeDecider = require('../../../lib/traffic-model/LaneChangeDecider'), 4 | noop = () => null; 5 | 6 | describe('LaneChangeDecider', () => { 7 | const newDecider = config => new LaneChangeDecider(config); 8 | 9 | describe('constructor', () => { 10 | it('defaults laneChangeDelayTicks property to zero', () => { 11 | expect(newDecider({}).laneChangeDelayTicks).toBe(0); 12 | }); 13 | }); 14 | 15 | describe('isChangingLanes', () => { 16 | let car = {timeDelayPercent: noop, timeSpeedupPercent: noop}, 17 | model = newDecider({}); 18 | 19 | describe('minTicksInLane condition', () => { 20 | it('returns true when elapsedTimeInLane equal to minTicksInLane', () => { 21 | model.minTicksInLane = 2; 22 | car.elapsedTimeInLane = 2; 23 | 24 | expect(model.isChangingLanes(car)).toBe(true); 25 | }); 26 | 27 | it('returns false when elapsedTimeInLane less than minTicksInLane', () => { 28 | model.minTicksInLane = 3; 29 | car.elapsedTimeInLane = 2; 30 | 31 | expect(model.isChangingLanes(car)).toBe(false); 32 | }); 33 | }); 34 | 35 | describe('multiple conditions', () => { 36 | it('ignores unset conditions', () => { 37 | model = newDecider({ 38 | minTickDelayPercent: 50, 39 | minTickSpeedupPercent: 20 40 | }); 41 | car = { 42 | timeDelayPercent: () => 49, 43 | timeSpeedupPercent: () => 19, 44 | elapsedTimeInLane: 100 45 | }; 46 | 47 | expect(model.isChangingLanes(car)).toBe(false); 48 | }); 49 | 50 | it('returns false when one condition fails', () => { 51 | model = newDecider({ 52 | minTickDelayPercent: 50, 53 | minTickSpeedupPercent: 20, 54 | minTicksInLane: 10 55 | }); 56 | car = { 57 | timeDelayPercent: () => 50, 58 | timeSpeedupPercent: () => 20, 59 | elapsedTimeInLane: 9 60 | }; 61 | 62 | expect(model.isChangingLanes(car)).toBe(false); 63 | }); 64 | 65 | it('returns true when all conditions are met', () =>{ 66 | model = newDecider({ 67 | minTickDelayPercent: 50, 68 | minTickSpeedupPercent: 20, 69 | minTicksInLane: 10 70 | }); 71 | car = { 72 | timeDelayPercent: () => 50, 73 | timeSpeedupPercent: () => 20, 74 | elapsedTimeInLane: 10 75 | }; 76 | 77 | expect(model.isChangingLanes(car)).toBe(true); 78 | }); 79 | }); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /tests/lib/utils.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const utils = require('../../lib//utils'); 4 | 5 | describe('utils', () => { 6 | describe('arrayDifference', () => { 7 | it('returns the difference of two arrays', () => { 8 | const a = [1, 2, 3], 9 | b = [3, 2], 10 | difference = utils.arrayDifference(a, b); 11 | 12 | expect(difference).toEqual([1]); 13 | }) 14 | }); 15 | 16 | describe('calculateAverageResult', () => { 17 | it('returns a row containing the average of all table rows', () => { 18 | const results = [ 19 | [1, 2], 20 | [3, 4] 21 | ], 22 | avg = utils.calculateAverageResult(results); 23 | 24 | expect(avg).toEqual([2, 3]); 25 | }); 26 | 27 | it('rounds to two decimal points', () => { 28 | const results = [ 29 | [1.11], 30 | [2] 31 | ], 32 | avg = utils.calculateAverageResult(results); 33 | 34 | expect(avg).toEqual([1.56]); 35 | }); 36 | 37 | it('ignores non-numeric values', () => { 38 | const results = [ 39 | [null, undefined, 5, 'x', {}], 40 | ['--', '--', 10, '--', '--'] 41 | ], 42 | avg = utils.calculateAverageResult(results); 43 | 44 | expect(avg).toEqual( 45 | [undefined, undefined, 7.5, undefined, undefined] 46 | ); 47 | }); 48 | }); 49 | 50 | describe('isSet', () => { 51 | describe('true for', () => { 52 | it('0', () => expect(utils.isSet(0)).toBe(true)); 53 | it("empty string", () => expect(utils.isSet('')).toBe(true)); 54 | it("empty object", () => expect(utils.isSet({})).toBe(true)); 55 | }); 56 | 57 | describe('false for', () => { 58 | it('null', () => expect(utils.isSet(null)).toBe(false)); 59 | it('undefined', () => expect(utils.isSet(undefined)).toBe(false)); 60 | }); 61 | }); 62 | 63 | describe('newArray2D', () => { 64 | let newArray; 65 | 66 | it('creates an empty 2D array of given length', () => { 67 | newArray = utils.newArray2D(2); 68 | 69 | expect(newArray).toEqual([[], []]); 70 | }); 71 | 72 | // test to ensure (new Array(arrayLength)).fill([]) is not used (as it creates empty arrays by reference) 73 | it('creates empty arrays by value (i.e. distinct copies)', () => { 74 | newArray = utils.newArray2D(2); 75 | 76 | newArray[1].push('x'); 77 | 78 | expect(newArray).toEqual([[], ['x']]); 79 | }); 80 | }); 81 | }); 82 | --------------------------------------------------------------------------------