├── .clang-format ├── .editorconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── API.md ├── Dockerfile ├── LICENSE ├── README.md ├── binding.gyp ├── cloudformation ├── ci.template └── ci.template.js ├── common.gypi ├── example ├── README.md ├── index.js ├── package.json ├── solution.geojson └── solution.png ├── lib └── index.js ├── package-lock.json ├── package.json ├── scripts ├── install-deps.sh └── publish.sh ├── src ├── adaptors.h ├── main.cc ├── matrix.h ├── params.h ├── tsp.cc ├── tsp.h ├── tsp_params.h ├── tsp_worker.h ├── types.h ├── vector.h ├── vrp.cc ├── vrp.h ├── vrp_params.h └── vrp_worker.h └── test ├── tsp.js └── vrp.js /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | # BasedOnStyle: LLVM 4 | AccessModifierOffset: -2 5 | AlignAfterOpenBracket: Align 6 | AlignConsecutiveAssignments: false 7 | AlignConsecutiveDeclarations: false 8 | AlignEscapedNewlinesLeft: false 9 | AlignOperands: true 10 | AlignTrailingComments: true 11 | AllowAllParametersOfDeclarationOnNextLine: true 12 | AllowShortBlocksOnASingleLine: false 13 | AllowShortCaseLabelsOnASingleLine: false 14 | AllowShortFunctionsOnASingleLine: All 15 | AllowShortIfStatementsOnASingleLine: false 16 | AllowShortLoopsOnASingleLine: false 17 | AlwaysBreakAfterDefinitionReturnType: None 18 | AlwaysBreakAfterReturnType: None 19 | AlwaysBreakBeforeMultilineStrings: false 20 | AlwaysBreakTemplateDeclarations: false 21 | BinPackArguments: true 22 | BinPackParameters: true 23 | BraceWrapping: 24 | AfterClass: false 25 | AfterControlStatement: false 26 | AfterEnum: false 27 | AfterFunction: false 28 | AfterNamespace: false 29 | AfterObjCDeclaration: false 30 | AfterStruct: false 31 | AfterUnion: false 32 | BeforeCatch: false 33 | BeforeElse: false 34 | IndentBraces: false 35 | BreakBeforeBinaryOperators: None 36 | BreakBeforeBraces: Attach 37 | BreakBeforeTernaryOperators: true 38 | BreakConstructorInitializersBeforeComma: false 39 | ColumnLimit: 130 40 | CommentPragmas: '^ IWYU pragma:' 41 | ConstructorInitializerAllOnOneLineOrOnePerLine: false 42 | ConstructorInitializerIndentWidth: 4 43 | ContinuationIndentWidth: 4 44 | Cpp11BracedListStyle: true 45 | DerivePointerAlignment: false 46 | DisableFormat: false 47 | ExperimentalAutoDetectBinPacking: false 48 | ForEachMacros: [ foreach, Q_FOREACH, BOOST_FOREACH ] 49 | IncludeCategories: 50 | - Regex: '^"(llvm|llvm-c|clang|clang-c)/' 51 | Priority: 2 52 | - Regex: '^(<|"(gtest|isl|json)/)' 53 | Priority: 3 54 | - Regex: '.*' 55 | Priority: 1 56 | IndentCaseLabels: false 57 | IndentWidth: 2 58 | IndentWrappedFunctionNames: false 59 | KeepEmptyLinesAtTheStartOfBlocks: true 60 | MacroBlockBegin: '' 61 | MacroBlockEnd: '' 62 | MaxEmptyLinesToKeep: 1 63 | NamespaceIndentation: None 64 | ObjCBlockIndentWidth: 2 65 | ObjCSpaceAfterProperty: false 66 | ObjCSpaceBeforeProtocolList: true 67 | PenaltyBreakBeforeFirstCallParameter: 19 68 | PenaltyBreakComment: 300 69 | PenaltyBreakFirstLessLess: 120 70 | PenaltyBreakString: 1000 71 | PenaltyExcessCharacter: 1000000 72 | PenaltyReturnTypeOnItsOwnLine: 60 73 | PointerAlignment: Left 74 | ReflowComments: true 75 | SortIncludes: true 76 | SpaceAfterCStyleCast: false 77 | SpaceBeforeAssignmentOperators: true 78 | SpaceBeforeParens: ControlStatements 79 | SpaceInEmptyParentheses: false 80 | SpacesBeforeTrailingComments: 1 81 | SpacesInAngles: false 82 | SpacesInContainerLiterals: true 83 | SpacesInCStyleCastParentheses: false 84 | SpacesInParentheses: false 85 | SpacesInSquareBrackets: false 86 | Standard: Cpp11 87 | TabWidth: 8 88 | UseTab: Never 89 | ... 90 | 91 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | end_of_line = lf 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | charset = utf-8 9 | 10 | [*.{js}] 11 | indent_style = space 12 | indent_size = 2 13 | 14 | [*.{cc,h}] 15 | indent_style = space 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | mason_packages 2 | third_party 3 | build 4 | node_modules 5 | npm-debug.log 6 | .nyc* 7 | coverage 8 | lib/binding 9 | *.tgz 10 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !API.md 3 | !example/* 4 | !lib/*.js 5 | !LICENSE 6 | !README.md 7 | !package.json 8 | !package-lock.json 9 | !test/* 10 | !binding.gyp 11 | !common.gypi 12 | !src/* 13 | !scripts/install-deps.sh 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | addons: 4 | apt: 5 | sources: [ 'ubuntu-toolchain-r-test', 'llvm-toolchain-trusty-4.0' ] 6 | packages: [ 'libstdc++-4.9-dev', 'clang-4.0' ] 7 | 8 | install: 9 | - mkdir -p ./third_party/mason 10 | - curl -sSfL https://github.com/mapbox/mason/archive/v0.19.0.tar.gz | tar --gunzip --extract --strip-components=1 --directory=./third_party/mason 11 | - curl -Ls https://mapbox-release-engineering.s3.amazonaws.com/mbx-ci/latest/mbx-ci-linux-amd64 > mbx-ci && chmod 755 ./mbx-ci 12 | - npm install --build-from-source 13 | 14 | 15 | before_script: 16 | - ./mbx-ci aws setup 17 | - $(./mbx-ci aws credentials --export) 18 | 19 | script: 20 | - npm test 21 | - ./scripts/publish.sh 22 | 23 | matrix: 24 | include: 25 | - os: linux 26 | node_js: 6 27 | sudo: false 28 | env: CC='clang-4.0' CXX='clang++-4.0' 29 | - os: linux 30 | node_js: 8 31 | sudo: false 32 | env: CC='clang-4.0' CXX='clang++-4.0' 33 | - os: linux 34 | node_js: 10 35 | sudo: false 36 | env: CC='clang-4.0' CXX='clang++-4.0' 37 | - os: linux 38 | node_js: 12 39 | sudo: false 40 | env: CC='clang-4.0' CXX='clang++-4.0' 41 | - os: linux 42 | node_js: 14 43 | sudo: false 44 | env: CC='clang-4.0' CXX='clang++-4.0' 45 | 46 | - os: osx 47 | osx_image: xcode8.2 48 | node_js: 6 49 | - os: osx 50 | osx_image: xcode8.2 51 | node_js: 8 52 | - os: osx 53 | osx_image: xcode8.2 54 | node_js: 10 55 | - os: osx 56 | osx_image: xcode8.2 57 | node_js: 12 58 | - os: osx 59 | osx_image: xcode8.2 60 | node_js: 14 61 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | # Table of Contents 2 | - [Travelling Salesman Problem (TSP)](#tsp) 3 | - [Vehicle Routing Problem (VRP)](#vrp) 4 | 5 | 6 | # TSP 7 | 8 | Heuristically solves the Travelling Salesman Problem (TSP) with a single vehicle. 9 | For multiple vehicles or additional constraints see the [Vehicle Routing Problem (VRP)](#vrp). 10 | The TSP solver is split into two: the `TSP` constructor taking constructor-specific options and the asynchronous `Solve` function taking search-specific options. 11 | 12 | Note: even though the bindings take JavaScript **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)** types internally the solver works with integral types. 13 | Make sure to convert your floating point types into integral types, otherwise truncation will happen transparently. 14 | 15 | 16 | ## Constructor 17 | 18 | Constructs a TSP solver object. 19 | Initializes and caches user data internally for efficiency. 20 | 21 | 22 | **Parameters** 23 | 24 | **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** with solver-specific options: 25 | - `numNodes` **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)** Number of locations in the problem ("nodes"). 26 | - `costs` **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)** Cost array the solver minimizes in optimization. Can for example be duration, distance but does not have to be. Two-dimensional with `costs[from][to]` being a **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)** representing the cost for traversing the arc from `from` to `to`. 27 | 28 | 29 | **Examples** 30 | 31 | ```javascript 32 | var costs = [[0, 10, 10], 33 | [10, 0, 10], 34 | [10, 10, 0]]; 35 | 36 | var tspSolverOpts = { 37 | numNodes: 3, 38 | costs: costs 39 | }; 40 | 41 | var TSP = new node_or_tools.TSP(tspSolverOpts); 42 | ``` 43 | 44 | 45 | ## Solve 46 | 47 | Runs the TSP solver asynchronously to search for a solution. 48 | 49 | 50 | **Parameters** 51 | 52 | - `computeTimeLimit` **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)** Time limit in milliseconds for the solver. In general the longer you run the solver the better the solution (if there is any) will be. The solver will never run longer than this time limit but can finish earlier. 53 | - `depotNode` **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)** The depot node index in the range `[0, numNodes - 1]` where all vehicles start and end at. 54 | 55 | 56 | **Examples** 57 | 58 | ```javascript 59 | var tspSearchOpts = { 60 | computeTimeLimit: 1000, 61 | depotNode: depotNode 62 | }; 63 | 64 | TSP.Solve(tspSearchOpts, function (err, solution) { 65 | if (err) return console.log(err); 66 | console.log(util.inspect(solution, {showHidden: false, depth: null})); 67 | }); 68 | ``` 69 | 70 | **Result** 71 | 72 | **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)** with **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)** indices into the locations for the vehicle to visit in order. 73 | 74 | **Examples** 75 | 76 | ```javascript 77 | [ 4, 8, 12, 13, 14, 15, 11, 10, 9, 5, 6, 7, 3, 2, 1 ] 78 | ``` 79 | 80 | 81 | # VRP 82 | 83 | Heuristically solves the Vehicle Routing Problem (VRP) with multiple vehicles and constraints (time windows, capacities and more). 84 | The VRP solver is split into two: the `VRP` constructor taking constructor-specific options and the asynchronous `Solve` function taking search-specific options. 85 | 86 | Note: even though the bindings take JavaScript **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)** types internally the solver works with integral types. 87 | Make sure to convert your floating point types into integral types, otherwise truncation will happen transparently. 88 | 89 | ## Constructor 90 | 91 | Constructs a VRP solver object. 92 | Initializes and caches user data internally for efficiency. 93 | 94 | 95 | **Parameters** 96 | 97 | **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** with solver-specific options: 98 | - `numNodes` **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)** Number of locations in the problem ("nodes"). 99 | - `costs` **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)** Cost array the solver minimizes in optimization. Can for example be duration, distance but does not have to be. Two-dimensional with `costs[from][to]` being a **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)** representing the cost for traversing the arc from `from` to `to`. 100 | - `durations` **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)** Duration array the solver uses for time constraints. Two-dimensional with `durations[from][to]` being a **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)** representing the duration for servicing node `from` plus the time for traversing the arc from `from` to `to`. 101 | - `timeWindows` **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)** Time window array the solver uses for time constraints. Two-dimensional with `timeWindows[at]` being an **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)** of two **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)** representing the start and end time point of the time window when servicing the node `at` is allowed. The solver starts from time point `0` (you can think of this as the start of the work day) and the time points need to be positive offsets to this time point. 102 | - `demands` **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)** Demands array the solver uses for vehicle capacity constraints. Two-dimensional with `demands[from][to]` being a **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)** representing the demand at node `from`, for example number of packages to deliver to this location. The `to` node index is unused and reserved for future changes; set `demands[at]` to a constant array for now. The depot should have a demand of zero. 103 | 104 | 105 | **Examples** 106 | 107 | ```javascript 108 | var vrpSolverOpts = { 109 | numNodes: 3, 110 | costs: [[0, 10, 10], [10, 0, 10], [10, 10, 0]], 111 | durations: [[0, 2, 2], [2, 0, 2], [2, 2, 0]], 112 | timeWindows: [[0, 9], [2, 3], [2, 3]], 113 | demands: [[0, 0, 0], [1, 1, 1], [1, 1, 1]] 114 | }; 115 | 116 | var VRP = new node_or_tools.VRP(vrpSolverOpts); 117 | ``` 118 | 119 | 120 | ## Solve 121 | 122 | Runs the VRP solver asynchronously to search for a solution. 123 | 124 | 125 | **Parameters** 126 | 127 | - `computeTimeLimit` **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)** Time limit in milliseconds for the solver. In general the longer you run the solver the better the solution (if there is any) will be. The solver will never run longer than this time limit but can finish earlier. 128 | - `numVehicles` **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)** The number of vehicles for servicing nodes. 129 | - `depotNode` **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)** The depot node index in the range `[0, numNodes - 1]` where all vehicles start and end at. 130 | - `timeHorizon` **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)** The last time point the solver uses for time constraints. The solver starts from time point `0` (you can think of this as the start of the work day) and ends at `timeHorizon` (you can think of this as the end of the work day). 131 | - `vehicleCapacity` **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)** The maximum capacity for goods each vehicle can carry. Demand at nodes decrease the capacity. 132 | - `routeLocks` **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)** Route locks array the solver uses for locking (sub-) routes into place, per vehicle. Two-dimensional with `routeLocks[vehicle]` being an **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)** with node indices `vehicle` has to visit in order. Can be empty. Must not contain the depots. 133 | - `pickups` **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)** with node indices for picking up good. The corresponding delivery node index is in the `deliveries` **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)** at the same position (parallel arrays). For a pair of pickup and delivery indices: pickup location comes before the corresponding delivery location and is served by the same vehicle. 134 | - `deliveries` **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)** with node indices for delivering picked up goods. The corresponding pickup node index is in the `pickups` **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)** at the same position (parallel arrays). For a pair of pickup and delivery indices: pickup location comes before the corresponding delivery location and is served by the same vehicle. 135 | 136 | **Examples** 137 | 138 | ```javascript 139 | var vrpSearchOpts = { 140 | computeTimeLimit: 1000, 141 | numVehicles: 3, 142 | depotNode: depotNode, 143 | timeHorizon: 9 * 60 * 60, 144 | vehicleCapacity: 3, 145 | routeLocks: [[], [3, 4], []], 146 | pickups: [4, 9], 147 | deliveries: [12, 8] 148 | }; 149 | 150 | VRP.Solve(vrpSearchOpts, function (err, solution) { 151 | if (err) return console.log(err); 152 | console.log(util.inspect(solution, {showHidden: false, depth: null})); 153 | }); 154 | ``` 155 | 156 | **Result** 157 | 158 | **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** with: 159 | - `cost` **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)** internal objective to optimize for. 160 | - `routes` **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)** with **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)** indices into the locations for the vehicle to visit in order. Per vehicle. 161 | - `times` **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)** with **[Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)** `[earliest, latest]` service times at the locations for the vehicle to visit in order. Per vehicle. The solver starts from time point `0` (you can think of this as the start of the work day) and the time points are positive offsets to this time point. 162 | 163 | **Examples** 164 | 165 | ```javascript 166 | { cost: 90, 167 | routes: [ [ 4, 5, 9 ], [ 3, 7, 8 ], [ 1, 2, 6 ] ], 168 | times: 169 | [ [ [ 2700, 3600 ], [ 8400, 9300 ], [ 17100, 18000 ] ], 170 | [ [ 2100, 2400 ], [ 8400, 8700 ], [ 17700, 18000 ] ], 171 | [ [ 900, 10800 ], [ 3000, 12900 ], [ 8100, 18000 ] ] ]} 172 | ``` 173 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:16.04 2 | 3 | WORKDIR /usr/local/src/app 4 | COPY . /usr/local/src/app 5 | 6 | RUN apt-get update && \ 7 | apt-get install -y curl python make g++ && \ 8 | \ 9 | (curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.1/install.sh | bash) && \ 10 | export NVM_DIR="$HOME/.nvm" && \ 11 | . ${NVM_DIR}/nvm.sh && \ 12 | nvm install 4.8 && \ 13 | nvm use 4.8 && \ 14 | \ 15 | npm install --unsafe-perm && \ 16 | npm test && \ 17 | \ 18 | apt-get clean && \ 19 | rm -rf /var/lib/apt/lists/* 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Mapbox 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-or-tools 2 | 3 | [![Build Status](https://travis-ci.org/mapbox/node-or-tools.svg?branch=master)](https://travis-ci.org/mapbox/node-or-tools) 4 | 5 | NodeJS bindings for or-tools Travelling Salesman Problem (TSP) and Vehicle Routing Problem (VRP) solvers. 6 | 7 | See [API.md](API.md) for documentation. 8 | 9 | Solving TSP and VRP problems always starts out with having a `m x m` cost matrix for all pairwise routes between all locations. 10 | We recommend using the [Mapbox Directions Matrix](https://www.mapbox.com/api-documentation/navigation/#matrix) service when optimizing travel times. 11 | 12 | [![Example](https://raw.githubusercontent.com/mapbox/node-or-tools/master/example/solution.png?token=AAgLiX1m1BDa8ll0Lsk0xc6fz0RgQA1Lks5Y-VmAwA)](https://github.com/mapbox/node-or-tools/blob/master/example/solution.geojson) 13 | 14 | ### Quick Start 15 | 16 | npm install node_or_tools 17 | 18 | ```c++ 19 | var ortools = require('node_or_tools') 20 | 21 | var VRP = new ortools.VRP(solverOpts); 22 | 23 | VRP.Solve(searchOpts, function (err, solution) { 24 | // .. 25 | }); 26 | ``` 27 | 28 | See [API.md](API.md) for interface documentation and [the example](./example/README.md) for a small self-contained example. 29 | 30 | We ship pre-built native binaries (for Node.js LTS 4 and 6 on Linux and macOS). 31 | You will need a compatible C++ stdlib, see below if you encounter issues. 32 | Building from source is supported via the `--build-from-source` flag. 33 | 34 | #### Ubuntu 16.04 35 | 36 | You're fine. The system's stdlib is recent enough. 37 | 38 | #### Ubuntu 14.04 39 | 40 | ``` 41 | apt install software-properties-common 42 | add-apt-repository ppa:ubuntu-toolchain-r/test 43 | apt update 44 | apt install libstdc++-5-dev 45 | ``` 46 | 47 | ### Tests 48 | 49 | npm test 50 | 51 | 52 | ### Building - Undefined Symbols 53 | 54 | If your C++ compiler and stdlib are quite recent they will default to a new ABI. 55 | Mason packages are still built against an old ABI. 56 | If you see `undefined symbols` errors force the stdlib to use the old ABI by setting: 57 | 58 | export CXXFLAGS="-D_GLIBCXX_USE_CXX11_ABI=0" 59 | 60 | and re-build the project. 61 | 62 | ### Releases 63 | 64 | - Push commit with `[publish binary]` on master 65 | - Wait for Travis to build and publish binaries, check the AWS bucket if in doubt 66 | - Tag the release `git tag vx.y.z -a` on master, `git push origin vx.y.z` 67 | - Then `npm login`, `npm publish` to npm 68 | - Make Github Release for tag 69 | 70 | ### References 71 | 72 | Routing Interfaces 73 | - [RoutingModel](https://github.com/google/or-tools/blob/v5.1/src/constraint_solver/routing.h#L14) 74 | - [RoutingSearchParameters](https://github.com/google/or-tools/blob/v5.1/src/constraint_solver/routing_parameters.proto#L28-L29) 75 | - [RoutingModelParameters](https://github.com/google/or-tools/blob/v5.1/src/constraint_solver/routing_parameters.proto#L263-L264) 76 | 77 | More or-tools 78 | - Manual: [TSP](https://acrogenesis.com/or-tools/documentation/user_manual/manual/TSP.html) and [VRP](https://acrogenesis.com/or-tools/documentation/user_manual/manual/VRP.html) 79 | - [Examples](https://github.com/google/or-tools/tree/v5.1/examples/cpp) 80 | - [Reference](https://developers.google.com/optimization/reference/) 81 | 82 | Tests 83 | - [npmjs.com/package/tap](https://www.npmjs.com/package/tap) 84 | - [node-tap.org](http://www.node-tap.org) 85 | 86 | Node bindings 87 | - [NAN2](https://github.com/nodejs/nan#api) 88 | - [GYP](https://gyp.gsrc.io) 89 | 90 | ### License 91 | 92 | Copyright © 2017 Mapbox 93 | 94 | Distributed under the MIT License (MIT). 95 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | 'includes': [ 'common.gypi' ], 3 | 'variables': { 4 | # Includes we don't want warnings for. 5 | 'system_includes': [ 6 | '-isystem <(module_root_dir)/&2 echo "Usage" 59 | >&2 echo "" 60 | >&2 echo "$ ./scripts/publish.sh " 61 | >&2 echo "" 62 | >&2 echo "All args are forwarded to node-pre-gyp like --debug" 63 | >&2 echo "" 64 | exit 1 65 | } 66 | 67 | # https://stackoverflow.com/questions/192249/how-do-i-parse-command-line-arguments-in-bash 68 | for i in "$@" 69 | do 70 | case $i in 71 | -h | --help) 72 | usage 73 | shift 74 | ;; 75 | *) 76 | ;; 77 | esac 78 | done 79 | 80 | publish $@ 81 | -------------------------------------------------------------------------------- /src/adaptors.h: -------------------------------------------------------------------------------- 1 | #ifndef NODE_OR_TOOLS_ADAPTORS_F1FF74E9681C_H 2 | #define NODE_OR_TOOLS_ADAPTORS_F1FF74E9681C_H 3 | 4 | #include 5 | 6 | #include "types.h" 7 | 8 | // We adapt matrices and vectors for or-tools since it expects them to have specific signatures and types. 9 | 10 | // Matrix to operator()(NodeIndex, NodeIndex); 11 | template auto makeBinaryAdaptor(const T& m) { 12 | return [&](NodeIndex from, NodeIndex to) -> int64 { return m.at(from.value(), to.value()); }; 13 | } 14 | 15 | // Vector to operator()(NodeIndex); 16 | template auto makeUnaryAdaptor(const T& v) { 17 | return [&](NodeIndex idx) -> int64 { return v.at(idx.value()); }; 18 | } 19 | 20 | // Adaptors to callback. Note: ownership is bound to the underlying storage. 21 | template auto makeCallback(const Adaptor& adaptor) { 22 | return NewPermanentCallback(&adaptor, &Adaptor::operator()); 23 | } 24 | 25 | // Caches user provided Function(s, t) -> Number into Matrix 26 | template inline auto makeMatrixFromFunction(std::int32_t n, v8::Local fn) { 27 | if (n < 0) 28 | throw std::runtime_error{"Negative dimension"}; 29 | 30 | Nan::Callback callback{fn}; 31 | 32 | Matrix matrix{n}; 33 | 34 | for (std::int32_t fromIdx = 0; fromIdx < n; ++fromIdx) { 35 | for (std::int32_t toIdx = 0; toIdx < n; ++toIdx) { 36 | const auto argc = 2u; 37 | v8::Local argv[argc] = {Nan::New(fromIdx), Nan::New(toIdx)}; 38 | 39 | auto cost = callback.Call(argc, argv); 40 | 41 | if (!cost->IsNumber()) 42 | throw std::runtime_error{"Expected function signature: Number fn(Number from, Number to)"}; 43 | 44 | matrix.at(fromIdx, toIdx) = Nan::To(cost).FromJust(); 45 | } 46 | } 47 | 48 | return matrix; 49 | } 50 | 51 | // Caches user provided Function(node) -> [start, stop] into TimeWindows 52 | inline auto makeTimeWindowsFromFunction(std::int32_t n, v8::Local fn) { 53 | if (n < 0) 54 | throw std::runtime_error{"Negative size"}; 55 | 56 | Nan::Callback callback{fn}; 57 | 58 | TimeWindows timeWindows{n}; 59 | 60 | for (std::int32_t atIdx = 0; atIdx < n; ++atIdx) { 61 | const auto argc = 1u; 62 | v8::Local argv[argc] = {Nan::New(atIdx)}; 63 | 64 | auto interval = callback.Call(argc, argv); 65 | 66 | if (!interval->IsArray()) 67 | throw std::runtime_error{"Expected function signature: Array fn(Number at)"}; 68 | 69 | auto intervalArray = interval.As(); 70 | 71 | if (intervalArray->Length() != 2) 72 | throw std::runtime_error{"Expected interval array of shape [start, stop]"}; 73 | 74 | auto start = Nan::Get(intervalArray, 0).ToLocalChecked(); 75 | auto stop = Nan::Get(intervalArray, 1).ToLocalChecked(); 76 | 77 | if (!start->IsNumber() || !stop->IsNumber()) 78 | throw std::runtime_error{"Expected interval start and stop of type Number"}; 79 | 80 | Interval out{Nan::To(start).FromJust(), Nan::To(stop).FromJust()}; 81 | timeWindows.at(atIdx) = std::move(out); 82 | } 83 | 84 | return timeWindows; 85 | } 86 | 87 | // Caches user provided Function(vehicle) -> [node0, node1, ..] into RouteLocks 88 | inline auto makeRouteLocksFromFunction(std::int32_t n, v8::Local fn) { 89 | if (n < 0) 90 | throw std::runtime_error{"Negative size"}; 91 | 92 | Nan::Callback callback{fn}; 93 | 94 | // Note: use (n) for construction because RouteLocks is a weak alias to a std::vector. 95 | // Using vec(n) creates a vector of n items, using vec{n} creates a vector with a single element n. 96 | RouteLocks routeLocks(n); 97 | 98 | for (std::int32_t atIdx = 0; atIdx < n; ++atIdx) { 99 | const auto argc = 1u; 100 | v8::Local argv[argc] = {Nan::New(atIdx)}; 101 | 102 | auto locks = callback.Call(argc, argv); 103 | 104 | if (!locks->IsArray()) 105 | throw std::runtime_error{"Expected function signature: Array fn(Number vehicle)"}; 106 | 107 | auto locksArray = locks.As(); 108 | 109 | LockChain lockChain(locksArray->Length()); 110 | 111 | for (std::int32_t lockIdx = 0; lockIdx < (std::int32_t)locksArray->Length(); ++lockIdx) { 112 | auto node = Nan::Get(locksArray, lockIdx).ToLocalChecked(); 113 | 114 | if (!node->IsNumber()) 115 | throw std::runtime_error{"Expected lock node of type Number"}; 116 | 117 | lockChain.at(lockIdx) = Nan::To(node).FromJust(); 118 | } 119 | 120 | routeLocks.at(atIdx) = std::move(lockChain); 121 | } 122 | 123 | return routeLocks; 124 | } 125 | 126 | // Caches user provided Js Array into a Vector 127 | template inline auto makeVectorFromJsNumberArray(v8::Local array) { 128 | const std::int32_t len = array->Length(); 129 | 130 | Vector vec(len); 131 | 132 | for (std::int32_t atIdx = 0; atIdx < len; ++atIdx) { 133 | auto num = Nan::Get(array, atIdx).ToLocalChecked(); 134 | 135 | if (!num->IsNumber()) 136 | throw std::runtime_error{"Expected array element of types Number"}; 137 | 138 | vec.at(atIdx) = Nan::To(num).FromJust(); 139 | } 140 | 141 | return vec; 142 | } 143 | 144 | #endif 145 | -------------------------------------------------------------------------------- /src/main.cc: -------------------------------------------------------------------------------- 1 | #include "tsp.h" 2 | #include "vrp.h" 3 | 4 | NAN_MODULE_INIT(Init) { 5 | TSP::Init(target); 6 | VRP::Init(target); 7 | } 8 | 9 | NODE_MODULE(node_or_tools, Init) 10 | -------------------------------------------------------------------------------- /src/matrix.h: -------------------------------------------------------------------------------- 1 | #ifndef NODE_OR_TOOLS_MATRIX_F83F49233E85_H 2 | #define NODE_OR_TOOLS_MATRIX_F83F49233E85_H 3 | 4 | #include 5 | #include 6 | 7 | template class Matrix { 8 | static_assert(std::is_arithmetic::value, "Matrix requires T to be integral or floating point"); 9 | 10 | public: 11 | using Value = T; 12 | 13 | Matrix() = default; 14 | Matrix(std::int32_t n_) : n{n_} { 15 | if (n < 0) 16 | throw std::runtime_error{"Negative dimension"}; 17 | 18 | data.resize(n * n); 19 | } 20 | 21 | std::int32_t dim() const { return n; } 22 | std::int32_t size() const { return dim() * dim(); } 23 | 24 | T& at(std::int32_t x, std::int32_t y) { return data.at(y * n + x); } 25 | const T& at(std::int32_t x, std::int32_t y) const { return data.at(y * n + x); } 26 | 27 | private: 28 | std::int32_t n; 29 | std::vector data; 30 | }; 31 | 32 | #endif 33 | -------------------------------------------------------------------------------- /src/params.h: -------------------------------------------------------------------------------- 1 | #ifndef NODE_OR_TOOLS_PARAMS_90D22929381A_H 2 | #define NODE_OR_TOOLS_PARAMS_90D22929381A_H 3 | 4 | #include 5 | 6 | #include 7 | 8 | #include 9 | 10 | // Caches user provided 2d Array of Numbers into Matrix 11 | template inline auto makeMatrixFrom2dArray(std::int32_t n, v8::Local array) { 12 | if (n < 0) 13 | throw std::runtime_error{"Negative dimension"}; 14 | 15 | if (static_cast(array->Length()) != n) 16 | throw std::runtime_error{"Array dimension do not match size"}; 17 | 18 | Matrix matrix(n); 19 | 20 | for (std::int32_t fromIdx = 0; fromIdx < n; ++fromIdx) { 21 | auto inner = Nan::Get(array, fromIdx).ToLocalChecked(); 22 | 23 | if (!inner->IsArray()) 24 | throw std::runtime_error{"Expected Array of Arrays"}; 25 | 26 | auto innerArray = inner.As(); 27 | 28 | if (static_cast(array->Length()) != n) 29 | throw std::runtime_error{"Inner Array dimension do not match size"}; 30 | 31 | for (std::int32_t toIdx = 0; toIdx < n; ++toIdx) { 32 | auto num = Nan::Get(innerArray, toIdx).ToLocalChecked(); 33 | 34 | if (!num->IsNumber()) 35 | throw std::runtime_error{"Expected 2d Array of Numbers"}; 36 | 37 | auto value = Nan::To(num).FromJust(); 38 | 39 | matrix.at(fromIdx, toIdx) = value; 40 | } 41 | } 42 | 43 | return matrix; 44 | } 45 | 46 | #endif 47 | -------------------------------------------------------------------------------- /src/tsp.cc: -------------------------------------------------------------------------------- 1 | #include "tsp.h" 2 | #include "tsp_params.h" 3 | #include "tsp_worker.h" 4 | 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | TSP::TSP(CostMatrix costs_) : costs{std::make_shared(std::move(costs_))} {} 11 | 12 | NAN_MODULE_INIT(TSP::Init) { 13 | const auto whoami = Nan::New("TSP").ToLocalChecked(); 14 | 15 | auto fnTp = Nan::New(New); 16 | fnTp->SetClassName(whoami); 17 | fnTp->InstanceTemplate()->SetInternalFieldCount(1); 18 | 19 | SetPrototypeMethod(fnTp, "Solve", Solve); 20 | 21 | const auto fn = Nan::GetFunction(fnTp).ToLocalChecked(); 22 | constructor().Reset(fn); 23 | 24 | Nan::Set(target, whoami, fn); 25 | } 26 | 27 | NAN_METHOD(TSP::New) try { 28 | // Handle `new T()` as well as `T()` 29 | if (!info.IsConstructCall()) { 30 | auto init = Nan::New(constructor()); 31 | info.GetReturnValue().Set(Nan::NewInstance(init).ToLocalChecked()); 32 | return; 33 | } 34 | 35 | TSPSolverParams userParams{info}; 36 | 37 | const auto bytesChange = getBytes(userParams.costs); 38 | Nan::AdjustExternalMemory(bytesChange); 39 | 40 | auto* self = new TSP{std::move(userParams.costs)}; 41 | 42 | self->Wrap(info.This()); 43 | 44 | info.GetReturnValue().Set(info.This()); 45 | 46 | } catch (const std::exception& e) { 47 | return Nan::ThrowError(e.what()); 48 | } 49 | 50 | NAN_METHOD(TSP::Solve) try { 51 | auto* const self = Nan::ObjectWrap::Unwrap(info.Holder()); 52 | 53 | TSPSearchParams userParams{info}; 54 | 55 | // See routing_parameters.proto and routing_enums.proto 56 | auto modelParams = RoutingModel::DefaultModelParameters(); 57 | auto searchParams = RoutingModel::DefaultSearchParameters(); 58 | 59 | auto firstSolutionStrategy = FirstSolutionStrategy::AUTOMATIC; 60 | auto metaHeuristic = LocalSearchMetaheuristic::AUTOMATIC; 61 | 62 | searchParams.set_first_solution_strategy(firstSolutionStrategy); 63 | searchParams.set_local_search_metaheuristic(metaHeuristic); 64 | searchParams.set_time_limit_ms(userParams.computeTimeLimit); 65 | 66 | const std::int32_t numNodes = self->costs->dim(); 67 | const std::int32_t numVehicles = 1; // Always one for TSP 68 | 69 | auto* worker = new TSPWorker{self->costs, // 70 | new Nan::Callback{userParams.callback}, // 71 | modelParams, // 72 | searchParams, // 73 | numNodes, // 74 | numVehicles, // 75 | userParams.depotNode}; // 76 | Nan::AsyncQueueWorker(worker); 77 | 78 | } catch (const std::exception& e) { 79 | return Nan::ThrowError(e.what()); 80 | } 81 | 82 | Nan::Persistent& TSP::constructor() { 83 | static Nan::Persistent init; 84 | return init; 85 | } 86 | -------------------------------------------------------------------------------- /src/tsp.h: -------------------------------------------------------------------------------- 1 | #ifndef NODE_OR_TOOLS_TSP_17907AD93A08_H 2 | #define NODE_OR_TOOLS_TSP_17907AD93A08_H 3 | 4 | #include 5 | 6 | #include "adaptors.h" 7 | #include "types.h" 8 | 9 | #include 10 | 11 | class TSP : public Nan::ObjectWrap { 12 | public: 13 | static NAN_MODULE_INIT(Init); 14 | 15 | private: 16 | static NAN_METHOD(New); 17 | 18 | static NAN_METHOD(Solve); 19 | 20 | static Nan::Persistent& constructor(); 21 | 22 | // Wrapped Object 23 | 24 | TSP(CostMatrix costs); 25 | 26 | std::shared_ptr costs; 27 | }; 28 | 29 | #endif 30 | -------------------------------------------------------------------------------- /src/tsp_params.h: -------------------------------------------------------------------------------- 1 | #ifndef NODE_OR_TOOLS_TSP_PARAMS_D4D3AF3A298F_H 2 | #define NODE_OR_TOOLS_TSP_PARAMS_D4D3AF3A298F_H 3 | 4 | #include 5 | 6 | #include 7 | 8 | #include "params.h" 9 | 10 | struct TSPSolverParams { 11 | TSPSolverParams(const Nan::FunctionCallbackInfo& info); 12 | 13 | std::int32_t numNodes; 14 | CostMatrix costs; 15 | }; 16 | 17 | struct TSPSearchParams { 18 | TSPSearchParams(const Nan::FunctionCallbackInfo& info); 19 | 20 | std::int32_t computeTimeLimit; 21 | std::int32_t depotNode; 22 | 23 | v8::Local callback; 24 | }; 25 | 26 | // Impl. 27 | 28 | TSPSolverParams::TSPSolverParams(const Nan::FunctionCallbackInfo& info) { 29 | if (info.Length() != 1 || !info[0]->IsObject()) 30 | throw std::runtime_error{"Single object argument expected: SolverOptions"}; 31 | 32 | auto opts = info[0].As(); 33 | 34 | auto maybeNumNodes = Nan::Get(opts, Nan::New("numNodes").ToLocalChecked()); 35 | auto maybeCostMatrix = Nan::Get(opts, Nan::New("costs").ToLocalChecked()); 36 | 37 | auto numNodesOk = !maybeNumNodes.IsEmpty() && maybeNumNodes.ToLocalChecked()->IsNumber(); 38 | auto costMatrixOk = !maybeCostMatrix.IsEmpty() && maybeCostMatrix.ToLocalChecked()->IsArray(); 39 | 40 | if (!numNodesOk || !costMatrixOk) 41 | throw std::runtime_error{"SolverOptions expects 'numNodes' (Number), 'costs' (Array)"}; 42 | 43 | numNodes = Nan::To(maybeNumNodes.ToLocalChecked()).FromJust(); 44 | 45 | auto costMatrix = maybeCostMatrix.ToLocalChecked().As(); 46 | costs = makeMatrixFrom2dArray(numNodes, costMatrix); 47 | } 48 | 49 | TSPSearchParams::TSPSearchParams(const Nan::FunctionCallbackInfo& info) { 50 | if (info.Length() != 2 || !info[0]->IsObject() || !info[1]->IsFunction()) 51 | throw std::runtime_error{"Two arguments expected: SearchOptions (Object) and callback (Function)"}; 52 | 53 | auto opts = info[0].As(); 54 | 55 | auto maybeComputeTimeLimit = Nan::Get(opts, Nan::New("computeTimeLimit").ToLocalChecked()); 56 | auto maybeDepotNode = Nan::Get(opts, Nan::New("depotNode").ToLocalChecked()); 57 | 58 | auto computeTimeLimitOk = !maybeComputeTimeLimit.IsEmpty() && maybeComputeTimeLimit.ToLocalChecked()->IsNumber(); 59 | auto depotNodeOk = !maybeDepotNode.IsEmpty() && maybeDepotNode.ToLocalChecked()->IsNumber(); 60 | 61 | if (!computeTimeLimitOk || !depotNodeOk) 62 | throw std::runtime_error{"SearchOptions expects 'computeTimeLimit' (Number), 'depotNode' (Number)"}; 63 | 64 | computeTimeLimit = Nan::To(maybeComputeTimeLimit.ToLocalChecked()).FromJust(); 65 | depotNode = Nan::To(maybeDepotNode.ToLocalChecked()).FromJust(); 66 | callback = info[1].As(); 67 | } 68 | 69 | #endif 70 | -------------------------------------------------------------------------------- /src/tsp_worker.h: -------------------------------------------------------------------------------- 1 | #ifndef NODE_OR_TOOLS_TSP_WORKER_3C61C259EF2C_H 2 | #define NODE_OR_TOOLS_TSP_WORKER_3C61C259EF2C_H 3 | 4 | #include 5 | 6 | #include "adaptors.h" 7 | #include "types.h" 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | struct TSPWorker final : Nan::AsyncWorker { 14 | using Base = Nan::AsyncWorker; 15 | 16 | TSPWorker(std::shared_ptr costs_, Nan::Callback* callback, const RoutingModelParameters& modelParams_, 17 | const RoutingSearchParameters& searchParams_, std::int32_t numNodes, std::int32_t numVehicles, 18 | std::int32_t vehicleDepot) 19 | : Base(callback), costs{std::move(costs_)}, model{numNodes, numVehicles, NodeIndex{vehicleDepot}, modelParams_}, 20 | modelParams{modelParams_}, searchParams{searchParams_} {} 21 | 22 | void Execute() override { 23 | auto costAdaptor = makeBinaryAdaptor(*costs); 24 | auto costEvaluator = makeCallback(costAdaptor); 25 | 26 | model.SetArcCostEvaluatorOfAllVehicles(costEvaluator); 27 | 28 | const auto* assignment = model.SolveWithParameters(searchParams); 29 | 30 | if (!assignment || (model.status() != RoutingModel::Status::ROUTING_SUCCESS)) 31 | SetErrorMessage("Unable to find a solution"); 32 | 33 | model.AssignmentToRoutes(*assignment, &routes); 34 | 35 | if (routes.size() != 1) 36 | SetErrorMessage("Expected route for one vehicle"); 37 | } 38 | 39 | void HandleOKCallback() override { 40 | Nan::HandleScope scope; 41 | 42 | const auto& route = routes.front(); 43 | 44 | auto jsRoute = Nan::New(route.size()); 45 | 46 | for (std::size_t j = 0; j < route.size(); ++j) 47 | (void)Nan::Set(jsRoute, j, Nan::New(route[j].value())); 48 | 49 | const auto argc = 2u; 50 | v8::Local argv[argc] = {Nan::Null(), jsRoute}; 51 | 52 | callback->Call(argc, argv); 53 | } 54 | 55 | std::shared_ptr costs; // inc ref count to keep alive for async cb 56 | 57 | RoutingModel model; 58 | RoutingModelParameters modelParams; 59 | RoutingSearchParameters searchParams; 60 | 61 | // Stores solution until we can translate back to v8 objects 62 | std::vector> routes; 63 | }; 64 | 65 | #endif 66 | -------------------------------------------------------------------------------- /src/types.h: -------------------------------------------------------------------------------- 1 | #ifndef NODE_OR_TOOLS_TYPES_CD4622E664A7_H 2 | #define NODE_OR_TOOLS_TYPES_CD4622E664A7_H 3 | 4 | #include "ortools/constraint_solver/routing.h" 5 | 6 | #include 7 | 8 | #include "matrix.h" 9 | #include "vector.h" 10 | 11 | // Convenience types. Sshould be good enough for most of TSP / VRP related problems. 12 | 13 | // Newtype via phantom type scheme. Allows type system to distinguish between tagged types. 14 | // Note: NewType::Type != NewType::Type 15 | template struct NewType { 16 | struct Type : T { 17 | using T::T; 18 | }; 19 | }; 20 | 21 | using CostMatrix = NewType, struct CostMatrixTag>::Type; 22 | using DurationMatrix = NewType, struct DurationMatrixTag>::Type; 23 | using DemandMatrix = NewType, struct DemandMatrixTag>::Type; 24 | 25 | struct Interval { 26 | Interval() : start{0}, stop{0} {} 27 | 28 | Interval(std::int32_t start_, std::int32_t stop_) : start{start_}, stop{stop_} { 29 | if (start < 0 || stop < 0 || stop < start) 30 | throw std::runtime_error{"Negative intervals not supported"}; 31 | } 32 | 33 | std::int32_t start; 34 | std::int32_t stop; 35 | }; 36 | 37 | using TimeWindows = NewType, struct TimeWindowsTag>::Type; 38 | 39 | namespace ort = operations_research; 40 | 41 | // See routing.h 42 | using RoutingModel = ort::RoutingModel; 43 | using NodeIndex = ort::RoutingModel::NodeIndex; 44 | 45 | // See routing_parameters.proto 46 | using RoutingModelParameters = ort::RoutingModelParameters; 47 | using RoutingSearchParameters = ort::RoutingSearchParameters; 48 | 49 | // See routing_enums.proto 50 | using FirstSolutionStrategy = ort::FirstSolutionStrategy; 51 | using LocalSearchMetaheuristic = ort::LocalSearchMetaheuristic; 52 | 53 | // See search_limit.proto 54 | using SearchLimitParameters = ort::SearchLimitParameters; 55 | 56 | // See constraint_solver.h 57 | using Solver = ort::Solver; 58 | 59 | // Locks: for locking (sub-) routes into place: 60 | // - locks[i] holds the lock chain for vehicle i (can be empty) 61 | // - lock chain is ordered list of node indices for vehicle i (must not contain depots) 62 | using LockChain = std::vector; 63 | using RouteLocks = std::vector; 64 | 65 | // Pickup and Delivery constraints expressed as parallel arrays 66 | // e.g. pickups: [1, 2], deliveries: [5, 6] means 67 | // - pick up at node 1 and deliver to node 5 68 | // - pick up at node 2 and deliver to node 6 69 | using Pickups = NewType, struct PickupsTag>::Type; 70 | using Deliveries = NewType, struct DeliveriesTag>::Type; 71 | 72 | // Bytes in our type used for internal caching 73 | 74 | template struct Bytes; 75 | 76 | template <> struct Bytes { 77 | std::int32_t operator()(const CostMatrix& v) const { return v.size() * sizeof(CostMatrix::Value); } 78 | }; 79 | 80 | template <> struct Bytes { 81 | std::int32_t operator()(const DurationMatrix& v) const { return v.size() * sizeof(DurationMatrix::Value); } 82 | }; 83 | 84 | template <> struct Bytes { 85 | std::int32_t operator()(const DemandMatrix& v) const { return v.size() * sizeof(DemandMatrix::Value); } 86 | }; 87 | 88 | template <> struct Bytes { 89 | std::int32_t operator()(const TimeWindows& v) const { return v.size() * sizeof(TimeWindows::Value); } 90 | }; 91 | 92 | template <> struct Bytes { 93 | std::int32_t operator()(const RouteLocks& v) const { 94 | std::int32_t bytes = 0; 95 | 96 | for (const auto& lockChain : v) 97 | bytes += lockChain.size() * sizeof(LockChain::value_type); 98 | 99 | return bytes; 100 | } 101 | }; 102 | 103 | template <> struct Bytes { 104 | std::int32_t operator()(const Pickups& v) const { return v.size() * sizeof(Pickups::Value); } 105 | }; 106 | 107 | template <> struct Bytes { 108 | std::int32_t operator()(const Deliveries& v) const { return v.size() * sizeof(Deliveries::Value); } 109 | }; 110 | 111 | template std::int32_t getBytes(const T& v) { return Bytes{}(v); } 112 | 113 | #endif 114 | -------------------------------------------------------------------------------- /src/vector.h: -------------------------------------------------------------------------------- 1 | #ifndef NODE_OR_TOOLS_VECTOR_82AFACC9EA9A_H 2 | #define NODE_OR_TOOLS_VECTOR_82AFACC9EA9A_H 3 | 4 | #include 5 | #include 6 | 7 | template class Vector { 8 | public: 9 | using Value = T; 10 | 11 | Vector() = default; 12 | Vector(std::int32_t n) { data.resize(n); } 13 | 14 | std::int32_t size() const { return data.size(); } 15 | 16 | T& at(std::int32_t x) { return data.at(x); } 17 | const T& at(std::int32_t x) const { return data.at(x); } 18 | 19 | private: 20 | std::vector data; 21 | }; 22 | 23 | #endif 24 | -------------------------------------------------------------------------------- /src/vrp.cc: -------------------------------------------------------------------------------- 1 | #include "vrp.h" 2 | #include "vrp_params.h" 3 | #include "vrp_worker.h" 4 | 5 | VRP::VRP(CostMatrix costs_, DurationMatrix durations_, TimeWindows timeWindows_, DemandMatrix demands_) 6 | : costs{std::make_shared(std::move(costs_))}, 7 | durations{std::make_shared(std::move(durations_))}, 8 | timeWindows{std::make_shared(std::move(timeWindows_))}, 9 | demands{std::make_shared(std::move(demands_))} {} 10 | 11 | NAN_MODULE_INIT(VRP::Init) { 12 | const auto whoami = Nan::New("VRP").ToLocalChecked(); 13 | 14 | auto fnTp = Nan::New(New); 15 | fnTp->SetClassName(whoami); 16 | fnTp->InstanceTemplate()->SetInternalFieldCount(1); 17 | 18 | SetPrototypeMethod(fnTp, "Solve", Solve); 19 | 20 | const auto fn = Nan::GetFunction(fnTp).ToLocalChecked(); 21 | constructor().Reset(fn); 22 | 23 | Nan::Set(target, whoami, fn); 24 | } 25 | 26 | NAN_METHOD(VRP::New) try { 27 | // Handle `new T()` as well as `T()` 28 | if (!info.IsConstructCall()) { 29 | auto init = Nan::New(constructor()); 30 | info.GetReturnValue().Set(Nan::NewInstance(init).ToLocalChecked()); 31 | return; 32 | } 33 | 34 | VRPSolverParams userParams{info}; 35 | 36 | const auto bytesChange = getBytes(userParams.costs) // 37 | + getBytes(userParams.durations) // 38 | + getBytes(userParams.timeWindows) // 39 | + getBytes(userParams.demands); // 40 | 41 | Nan::AdjustExternalMemory(bytesChange); 42 | 43 | auto* self = new VRP{std::move(userParams.costs), // 44 | std::move(userParams.durations), // 45 | std::move(userParams.timeWindows), // 46 | std::move(userParams.demands)}; // 47 | 48 | self->Wrap(info.This()); 49 | 50 | info.GetReturnValue().Set(info.This()); 51 | 52 | } catch (const std::exception& e) { 53 | return Nan::ThrowError(e.what()); 54 | } 55 | 56 | NAN_METHOD(VRP::Solve) try { 57 | auto* const self = Nan::ObjectWrap::Unwrap(info.Holder()); 58 | 59 | VRPSearchParams userParams(info); 60 | 61 | const auto bytesChange = getBytes(userParams.routeLocks); 62 | Nan::AdjustExternalMemory(bytesChange); 63 | 64 | // See routing_parameters.proto and routing_enums.proto 65 | auto modelParams = RoutingModel::DefaultModelParameters(); 66 | auto searchParams = RoutingModel::DefaultSearchParameters(); 67 | 68 | auto firstSolutionStrategy = FirstSolutionStrategy::AUTOMATIC; 69 | auto metaHeuristic = LocalSearchMetaheuristic::AUTOMATIC; 70 | 71 | searchParams.set_first_solution_strategy(firstSolutionStrategy); 72 | searchParams.set_local_search_metaheuristic(metaHeuristic); 73 | searchParams.set_time_limit_ms(userParams.computeTimeLimit); 74 | 75 | // As long as we have a homogeneous fleet wrt. costs we can simplify the underlying model 76 | modelParams.set_reduce_vehicle_cost_model(true); 77 | 78 | // Do not cache callbacks internally, too: we already provide efficient matrix adaptors 79 | modelParams.set_max_callback_cache_size(0); 80 | 81 | const std::int32_t numNodes = self->costs->dim(); 82 | const std::int32_t numVehicles = userParams.numVehicles; 83 | 84 | // TODO: this is getting out of hand, clean up, e.g. split into data vs. config 85 | auto* worker = new VRPWorker{self->costs, // 86 | self->durations, // 87 | self->timeWindows, // 88 | self->demands, // 89 | new Nan::Callback{userParams.callback}, // 90 | modelParams, // 91 | searchParams, // 92 | numNodes, // 93 | numVehicles, // 94 | userParams.depotNode, // 95 | userParams.timeHorizon, // 96 | userParams.vehicleCapacity, // 97 | std::move(userParams.routeLocks), // 98 | std::move(userParams.pickups), // 99 | std::move(userParams.deliveries)}; // 100 | 101 | Nan::AsyncQueueWorker(worker); 102 | 103 | } catch (const std::exception& e) { 104 | return Nan::ThrowError(e.what()); 105 | } 106 | 107 | Nan::Persistent& VRP::constructor() { 108 | static Nan::Persistent init; 109 | return init; 110 | } 111 | -------------------------------------------------------------------------------- /src/vrp.h: -------------------------------------------------------------------------------- 1 | #ifndef NODE_OR_TOOLS_VRP_D6C3BEA8CF7E_H 2 | #define NODE_OR_TOOLS_VRP_D6C3BEA8CF7E_H 3 | 4 | #include 5 | 6 | #include "adaptors.h" 7 | #include "types.h" 8 | 9 | #include 10 | 11 | class VRP : public Nan::ObjectWrap { 12 | public: 13 | static NAN_MODULE_INIT(Init); 14 | 15 | private: 16 | static NAN_METHOD(New); 17 | 18 | static NAN_METHOD(Solve); 19 | 20 | static Nan::Persistent& constructor(); 21 | 22 | // Wrapped Object 23 | 24 | VRP(CostMatrix costs, DurationMatrix durations, TimeWindows timeWindows, DemandMatrix demands); 25 | 26 | // Non-Copyable 27 | VRP(const VRP&) = delete; 28 | VRP& operator=(const VRP&) = delete; 29 | 30 | // Non-Moveable 31 | VRP(VRP&&) = delete; 32 | VRP& operator=(VRP&&) = delete; 33 | 34 | // (s, t) arc costs we optimize, e.g. duration or distance. 35 | std::shared_ptr costs; 36 | // (s, t) arc travel durations: service time for s plus travel time from s to t. 37 | std::shared_ptr durations; 38 | // Time windows keyed by node for when deliveries are possible at node. 39 | std::shared_ptr timeWindows; 40 | // Demands at node s continuing to node t. 41 | std::shared_ptr demands; 42 | }; 43 | 44 | #endif 45 | -------------------------------------------------------------------------------- /src/vrp_params.h: -------------------------------------------------------------------------------- 1 | #ifndef NODE_OR_TOOLS_VRP_PARAMS_0BED4C140464_H 2 | #define NODE_OR_TOOLS_VRP_PARAMS_0BED4C140464_H 3 | 4 | #include 5 | 6 | #include 7 | 8 | #include "params.h" 9 | 10 | struct VRPSolverParams { 11 | VRPSolverParams(const Nan::FunctionCallbackInfo& info); 12 | 13 | std::int32_t numNodes; 14 | 15 | CostMatrix costs; 16 | DurationMatrix durations; 17 | TimeWindows timeWindows; 18 | DemandMatrix demands; 19 | }; 20 | 21 | struct VRPSearchParams { 22 | VRPSearchParams(const Nan::FunctionCallbackInfo& info); 23 | 24 | std::int32_t computeTimeLimit; 25 | std::int32_t numVehicles; 26 | std::int32_t depotNode; 27 | std::int32_t timeHorizon; 28 | std::int32_t vehicleCapacity; 29 | 30 | RouteLocks routeLocks; 31 | 32 | Pickups pickups; 33 | Deliveries deliveries; 34 | 35 | v8::Local callback; 36 | }; 37 | 38 | // Caches user provided 2d Array of [Number, Number] into Vectors of Intervals 39 | inline auto makeTimeWindowsFrom2dArray(std::int32_t n, v8::Local array) { 40 | if (n < 0) 41 | throw std::runtime_error{"Negative size"}; 42 | 43 | if (static_cast(array->Length()) != n) 44 | throw std::runtime_error{"Array dimension do not match size"}; 45 | 46 | TimeWindows timeWindows(n); 47 | 48 | for (std::int32_t atIdx = 0; atIdx < n; ++atIdx) { 49 | auto inner = Nan::Get(array, atIdx).ToLocalChecked(); 50 | 51 | if (!inner->IsArray()) 52 | throw std::runtime_error{"Expected Array of Arrays"}; 53 | 54 | auto innerArray = inner.As(); 55 | 56 | if (static_cast(innerArray->Length()) != 2) 57 | throw std::runtime_error{"Expected interval Array of shape [start, stop]"}; 58 | 59 | auto start = Nan::Get(innerArray, 0).ToLocalChecked(); 60 | auto stop = Nan::Get(innerArray, 1).ToLocalChecked(); 61 | 62 | if (!start->IsNumber() || !stop->IsNumber()) 63 | throw std::runtime_error{"Expected interval start and stop of type Number"}; 64 | 65 | auto startValue = Nan::To(start).FromJust(); 66 | auto stopValue = Nan::To(stop).FromJust(); 67 | 68 | Interval out{startValue, stopValue}; 69 | 70 | timeWindows.at(atIdx) = std::move(out); 71 | } 72 | 73 | return timeWindows; 74 | } 75 | 76 | // Caches user provided 2d Array of [Number, ..] into Vectors 77 | inline auto makeRouteLocksFrom2dArray(std::int32_t n, v8::Local array) { 78 | if (n < 0) 79 | throw std::runtime_error{"Negative size"}; 80 | 81 | if (static_cast(array->Length()) != n) 82 | throw std::runtime_error{"Array dimension do not match size"}; 83 | 84 | // Note: use (n) for construction because RouteLocks is a weak alias to a std::vector. 85 | // Using vec(n) creates a vector of n items, using vec{n} creates a vector with a single element n. 86 | RouteLocks routeLocks(n); 87 | 88 | for (std::int32_t atIdx = 0; atIdx < n; ++atIdx) { 89 | auto inner = Nan::Get(array, atIdx).ToLocalChecked(); 90 | 91 | if (!inner->IsArray()) 92 | throw std::runtime_error{"Expected Array of Arrays"}; 93 | 94 | auto innerArray = inner.As(); 95 | 96 | LockChain lockChain(innerArray->Length()); 97 | 98 | for (std::int32_t lockIdx = 0; lockIdx < static_cast(innerArray->Length()); ++lockIdx) { 99 | auto node = Nan::Get(innerArray, lockIdx).ToLocalChecked(); 100 | 101 | if (!node->IsNumber()) 102 | throw std::runtime_error{"Expected lock node of type Number"}; 103 | 104 | lockChain.at(lockIdx) = Nan::To(node).FromJust(); 105 | } 106 | 107 | routeLocks.at(atIdx) = std::move(lockChain); 108 | } 109 | 110 | return routeLocks; 111 | } 112 | 113 | // Impl. 114 | 115 | VRPSolverParams::VRPSolverParams(const Nan::FunctionCallbackInfo& info) { 116 | if (info.Length() != 1 || !info[0]->IsObject()) 117 | throw std::runtime_error{"Single object argument expected: SolverOptions"}; 118 | 119 | auto opts = info[0].As(); 120 | 121 | auto maybeNumNodes = Nan::Get(opts, Nan::New("numNodes").ToLocalChecked()); 122 | auto maybeCostMatrix = Nan::Get(opts, Nan::New("costs").ToLocalChecked()); 123 | auto maybeDurationMatrix = Nan::Get(opts, Nan::New("durations").ToLocalChecked()); 124 | auto maybeTimeWindowsVector = Nan::Get(opts, Nan::New("timeWindows").ToLocalChecked()); 125 | auto maybeDemandMatrix = Nan::Get(opts, Nan::New("demands").ToLocalChecked()); 126 | 127 | auto numNodesOk = !maybeNumNodes.IsEmpty() && maybeNumNodes.ToLocalChecked()->IsNumber(); 128 | auto costMatrixOk = !maybeCostMatrix.IsEmpty() && maybeCostMatrix.ToLocalChecked()->IsArray(); 129 | auto durationMatrixOk = !maybeDurationMatrix.IsEmpty() && maybeDurationMatrix.ToLocalChecked()->IsArray(); 130 | auto timeWindowsVectorOk = !maybeTimeWindowsVector.IsEmpty() && maybeTimeWindowsVector.ToLocalChecked()->IsArray(); 131 | auto demandMatrixOk = !maybeDemandMatrix.IsEmpty() && maybeDemandMatrix.ToLocalChecked()->IsArray(); 132 | 133 | if (!numNodesOk || !costMatrixOk || !durationMatrixOk || !timeWindowsVectorOk || !demandMatrixOk) 134 | throw std::runtime_error{"SolverOptions expects" 135 | " 'numNodes' (Number)," 136 | " 'costs' (Array)," 137 | " 'durations' (Array)," 138 | " 'timeWindows' (Array)," 139 | " 'demands' (Array)"}; 140 | 141 | numNodes = Nan::To(maybeNumNodes.ToLocalChecked()).FromJust(); 142 | 143 | auto costMatrix = maybeCostMatrix.ToLocalChecked().As(); 144 | auto durationMatrix = maybeDurationMatrix.ToLocalChecked().As(); 145 | auto timeWindowsVector = maybeTimeWindowsVector.ToLocalChecked().As(); 146 | auto demandMatrix = maybeDemandMatrix.ToLocalChecked().As(); 147 | 148 | costs = makeMatrixFrom2dArray(numNodes, costMatrix); 149 | durations = makeMatrixFrom2dArray(numNodes, durationMatrix); 150 | timeWindows = makeTimeWindowsFrom2dArray(numNodes, timeWindowsVector); 151 | demands = makeMatrixFrom2dArray(numNodes, demandMatrix); 152 | } 153 | 154 | VRPSearchParams::VRPSearchParams(const Nan::FunctionCallbackInfo& info) { 155 | if (info.Length() != 2 || !info[0]->IsObject() || !info[1]->IsFunction()) 156 | throw std::runtime_error{"Two arguments expected: SearchOptions (Object) and callback (Function)"}; 157 | 158 | auto opts = info[0].As(); 159 | 160 | auto maybeComputeTimeLimit = Nan::Get(opts, Nan::New("computeTimeLimit").ToLocalChecked()); 161 | auto maybeNumVehicles = Nan::Get(opts, Nan::New("numVehicles").ToLocalChecked()); 162 | auto maybeDepotNode = Nan::Get(opts, Nan::New("depotNode").ToLocalChecked()); 163 | auto maybeTimeHorizon = Nan::Get(opts, Nan::New("timeHorizon").ToLocalChecked()); 164 | auto maybeVehicleCapacity = Nan::Get(opts, Nan::New("vehicleCapacity").ToLocalChecked()); 165 | auto maybeRouteLocks = Nan::Get(opts, Nan::New("routeLocks").ToLocalChecked()); 166 | auto maybePickups = Nan::Get(opts, Nan::New("pickups").ToLocalChecked()); 167 | auto maybeDeliveries = Nan::Get(opts, Nan::New("deliveries").ToLocalChecked()); 168 | 169 | auto computeTimeLimitOk = !maybeComputeTimeLimit.IsEmpty() && maybeComputeTimeLimit.ToLocalChecked()->IsNumber(); 170 | auto numVehiclesOk = !maybeNumVehicles.IsEmpty() && maybeNumVehicles.ToLocalChecked()->IsNumber(); 171 | auto depotNodeOk = !maybeDepotNode.IsEmpty() && maybeDepotNode.ToLocalChecked()->IsNumber(); 172 | auto timeHorizonOk = !maybeTimeHorizon.IsEmpty() && maybeTimeHorizon.ToLocalChecked()->IsNumber(); 173 | auto vehicleCapacityOk = !maybeVehicleCapacity.IsEmpty() && maybeVehicleCapacity.ToLocalChecked()->IsNumber(); 174 | auto routeLocksOk = !maybeRouteLocks.IsEmpty() && maybeRouteLocks.ToLocalChecked()->IsArray(); 175 | auto pickupsOk = !maybePickups.IsEmpty() && maybePickups.ToLocalChecked()->IsArray(); 176 | auto deliveriesOk = !maybeDeliveries.IsEmpty() && maybeDeliveries.ToLocalChecked()->IsArray(); 177 | 178 | // TODO: this is getting out of hand, clean up, or better think about generic parameter parsing 179 | if (!computeTimeLimitOk || !numVehiclesOk || !depotNodeOk || !timeHorizonOk || !vehicleCapacityOk || !routeLocksOk || 180 | !pickupsOk || !deliveriesOk) 181 | throw std::runtime_error{"SearchOptions expects" 182 | " 'computeTimeLimit' (Number)," 183 | " 'numVehicles' (Number)," 184 | " 'depotNode' (Number)," 185 | " 'timeHorizon' (Number)," 186 | " 'vehicleCapacity' (Number)," 187 | " 'routeLocks' (Array)," 188 | " 'pickups' (Array)," 189 | " 'deliveries' (Array)"}; 190 | 191 | computeTimeLimit = Nan::To(maybeComputeTimeLimit.ToLocalChecked()).FromJust(); 192 | numVehicles = Nan::To(maybeNumVehicles.ToLocalChecked()).FromJust(); 193 | depotNode = Nan::To(maybeDepotNode.ToLocalChecked()).FromJust(); 194 | timeHorizon = Nan::To(maybeTimeHorizon.ToLocalChecked()).FromJust(); 195 | vehicleCapacity = Nan::To(maybeVehicleCapacity.ToLocalChecked()).FromJust(); 196 | 197 | auto routeLocksArray = maybeRouteLocks.ToLocalChecked().As(); 198 | routeLocks = makeRouteLocksFrom2dArray(numVehicles, routeLocksArray); 199 | 200 | auto pickupsArray = maybePickups.ToLocalChecked().As(); 201 | pickups = makeVectorFromJsNumberArray(pickupsArray); 202 | 203 | auto deliveriesArray = maybeDeliveries.ToLocalChecked().As(); 204 | deliveries = makeVectorFromJsNumberArray(deliveriesArray); 205 | 206 | callback = info[1].As(); 207 | } 208 | 209 | #endif 210 | -------------------------------------------------------------------------------- /src/vrp_worker.h: -------------------------------------------------------------------------------- 1 | #ifndef NODE_OR_TOOLS_VRP_WORKER_C6DF0F45B324_H 2 | #define NODE_OR_TOOLS_VRP_WORKER_C6DF0F45B324_H 3 | 4 | #include 5 | 6 | #include "adaptors.h" 7 | #include "types.h" 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | struct RoutingSolution { 16 | std::int64_t cost; 17 | std::vector> routes; 18 | std::vector> times; 19 | }; 20 | 21 | struct VRPWorker final : Nan::AsyncWorker { 22 | using Base = Nan::AsyncWorker; 23 | 24 | VRPWorker(std::shared_ptr costs_, // 25 | std::shared_ptr durations_, // 26 | std::shared_ptr timeWindows_, // 27 | std::shared_ptr demands_, // 28 | Nan::Callback* callback, // 29 | const RoutingModelParameters& modelParams_, // 30 | const RoutingSearchParameters& searchParams_, // 31 | std::int32_t numNodes_, // 32 | std::int32_t numVehicles_, // 33 | std::int32_t vehicleDepot_, // 34 | std::int32_t timeHorizon_, // 35 | std::int32_t vehicleCapacity_, // 36 | RouteLocks routeLocks_, // 37 | Pickups pickups_, // 38 | Deliveries deliveries_) // 39 | : Base(callback), 40 | // Cached vectors and matrices 41 | costs{std::move(costs_)}, 42 | durations{std::move(durations_)}, 43 | timeWindows{std::move(timeWindows_)}, 44 | demands{std::move(demands_)}, 45 | // Search settings 46 | numNodes{numNodes_}, 47 | numVehicles{numVehicles_}, 48 | vehicleDepot{vehicleDepot_}, 49 | timeHorizon{timeHorizon_}, 50 | vehicleCapacity{vehicleCapacity_}, 51 | routeLocks{std::move(routeLocks_)}, 52 | pickups{std::move(pickups_)}, 53 | deliveries{std::move(deliveries_)}, 54 | // Setup model 55 | model{numNodes, numVehicles, NodeIndex{vehicleDepot}, modelParams_}, 56 | modelParams{modelParams_}, 57 | searchParams{searchParams_} { 58 | 59 | const auto costsOk = costs->dim() == numNodes; 60 | const auto durationsOk = durations->dim() == numNodes; 61 | const auto timeWindowsOk = timeWindows->size() == numNodes; 62 | const auto demandsOk = demands->dim() == numNodes; 63 | 64 | if (!costsOk || !durationsOk || !timeWindowsOk || !demandsOk) 65 | throw std::runtime_error{"Expected costs, durations, timeWindow and demand sizes to match numNodes"}; 66 | 67 | const auto routeLocksOk = (std::int32_t)routeLocks.size() == numVehicles; 68 | 69 | if (!routeLocksOk) 70 | throw std::runtime_error{"Expected routeLocks size to match numVehicles"}; 71 | 72 | for (const auto& locks : routeLocks) { 73 | for (const auto& node : locks) { 74 | const auto nodeInBounds = node >= 0 && node < numNodes; 75 | 76 | if (!nodeInBounds) 77 | throw std::runtime_error{"Expected nodes in route locks to be in [0, numNodes - 1]"}; 78 | 79 | const auto nodeIsDepot = node == vehicleDepot; 80 | 81 | if (nodeIsDepot) 82 | throw std::runtime_error{"Expected depot not to be in route locks"}; 83 | } 84 | } 85 | 86 | const auto pickupsAndDeliveriesOk = pickups.size() == deliveries.size(); 87 | 88 | if (!pickupsAndDeliveriesOk) 89 | throw std::runtime_error{"Expected pickups and deliveries parallel array sizes to match"}; 90 | } 91 | 92 | void Execute() override { 93 | auto costAdaptor = makeBinaryAdaptor(*costs); 94 | auto costCallback = makeCallback(costAdaptor); 95 | 96 | model.SetArcCostEvaluatorOfAllVehicles(costCallback); 97 | 98 | // Time Dimension 99 | 100 | auto durationAdaptor = makeBinaryAdaptor(*durations); 101 | auto durationCallback = makeCallback(durationAdaptor); 102 | 103 | const static auto kDimensionTime = "time"; 104 | 105 | model.AddDimension(durationCallback, timeHorizon, timeHorizon, /*fix_start_cumul_to_zero=*/true, kDimensionTime); 106 | const auto& timeDimension = model.GetDimensionOrDie(kDimensionTime); 107 | 108 | for (std::int32_t node = 0; node < numNodes; ++node) { 109 | const auto interval = timeWindows->at(node); 110 | timeDimension.CumulVar(node)->SetRange(interval.start, interval.stop); 111 | // At the moment we only support a single interval for time windows. 112 | // We can support multiple intervals if we sort intervals by start then stop. 113 | // Then Cumulval(n)->SetRange(minStart, maxStop), then walk over intervals 114 | // removing intervals between active intervals: 115 | // CumulVar(n)->RemoveInterval(stop, start). 116 | } 117 | 118 | // Capacity Dimension 119 | 120 | auto demandAdaptor = makeBinaryAdaptor(*demands); 121 | auto demandCallback = makeCallback(demandAdaptor); 122 | 123 | const static auto kDimensionCapacity = "capacity"; 124 | 125 | model.AddDimension(demandCallback, /*slack=*/0, vehicleCapacity, /*fix_start_cumul_to_zero=*/true, kDimensionCapacity); 126 | // const auto& capacityDimension = model.GetDimensionOrDie(kDimensionCapacity); 127 | 128 | // Pickup and Deliveries 129 | 130 | auto* solver = model.solver(); 131 | 132 | for (std::int32_t atIdx = 0; atIdx < pickups.size(); ++atIdx) { 133 | const auto pickupIndex = model.NodeToIndex(pickups.at(atIdx)); 134 | const auto deliveryIndex = model.NodeToIndex(deliveries.at(atIdx)); 135 | 136 | auto* sameRouteCt = solver->MakeEquality(model.VehicleVar(pickupIndex), // 137 | model.VehicleVar(deliveryIndex)); // 138 | 139 | auto* pickupBeforeDeliveryCt = solver->MakeLessOrEqual(timeDimension.CumulVar(pickupIndex), // 140 | timeDimension.CumulVar(deliveryIndex)); // 141 | 142 | solver->AddConstraint(sameRouteCt); 143 | solver->AddConstraint(pickupBeforeDeliveryCt); 144 | 145 | model.AddPickupAndDelivery(pickups.at(atIdx), deliveries.at(atIdx)); 146 | } 147 | 148 | // Done with modifications to the routing model 149 | 150 | model.CloseModel(); 151 | 152 | // Locking routes into place needs to happen after the model is closed and the underlying vars are established 153 | const auto validLocks = model.ApplyLocksToAllVehicles(routeLocks, /*close_routes=*/false); 154 | 155 | if (!validLocks) 156 | return SetErrorMessage("Invalid locks"); 157 | 158 | const auto* assignment = model.SolveWithParameters(searchParams); 159 | 160 | if (!assignment || (model.status() != RoutingModel::Status::ROUTING_SUCCESS)) 161 | return SetErrorMessage("Unable to find a solution"); 162 | 163 | const auto cost = static_cast(assignment->ObjectiveValue()); 164 | 165 | std::vector> routes; 166 | model.AssignmentToRoutes(*assignment, &routes); 167 | 168 | std::vector> times; 169 | 170 | for (const auto& route : routes) { 171 | std::vector routeTimes; 172 | 173 | for (const auto& node : route) { 174 | const auto index = model.NodeToIndex(node); 175 | 176 | const auto* timeVar = timeDimension.CumulVar(index); 177 | 178 | const auto first = static_cast(assignment->Min(timeVar)); 179 | const auto last = static_cast(assignment->Max(timeVar)); 180 | 181 | routeTimes.push_back(Interval{first, last}); 182 | } 183 | 184 | times.push_back(std::move(routeTimes)); 185 | } 186 | 187 | solution = RoutingSolution{cost, std::move(routes), std::move(times)}; 188 | } 189 | 190 | void HandleOKCallback() override { 191 | Nan::HandleScope scope; 192 | 193 | auto jsSolution = Nan::New(); 194 | 195 | auto jsCost = Nan::New(solution.cost); 196 | auto jsRoutes = Nan::New(solution.routes.size()); 197 | auto jsTimes = Nan::New(solution.times.size()); 198 | 199 | for (std::size_t i = 0; i < solution.routes.size(); ++i) { 200 | const auto& route = solution.routes[i]; 201 | const auto& times = solution.times[i]; 202 | 203 | auto jsNodes = Nan::New(route.size()); 204 | auto jsNodeTimes = Nan::New(times.size()); 205 | 206 | for (std::size_t j = 0; j < route.size(); ++j) { 207 | Nan::Set(jsNodes, j, Nan::New(route[j].value())); 208 | 209 | auto jsInterval = Nan::New(2); 210 | 211 | Nan::Set(jsInterval, 0, Nan::New(times[j].start)); 212 | Nan::Set(jsInterval, 1, Nan::New(times[j].stop)); 213 | 214 | Nan::Set(jsNodeTimes, j, jsInterval); 215 | } 216 | 217 | Nan::Set(jsRoutes, i, jsNodes); 218 | Nan::Set(jsTimes, i, jsNodeTimes); 219 | } 220 | 221 | Nan::Set(jsSolution, Nan::New("cost").ToLocalChecked(), jsCost); 222 | Nan::Set(jsSolution, Nan::New("routes").ToLocalChecked(), jsRoutes); 223 | Nan::Set(jsSolution, Nan::New("times").ToLocalChecked(), jsTimes); 224 | 225 | const auto argc = 2u; 226 | v8::Local argv[argc] = {Nan::Null(), jsSolution}; 227 | 228 | callback->Call(argc, argv); 229 | } 230 | 231 | // Shared ownership: keeps objects alive until the last callback is done. 232 | std::shared_ptr costs; 233 | std::shared_ptr durations; 234 | std::shared_ptr timeWindows; 235 | std::shared_ptr demands; 236 | 237 | std::int32_t numNodes; 238 | std::int32_t numVehicles; 239 | std::int32_t vehicleDepot; 240 | std::int32_t timeHorizon; 241 | std::int32_t vehicleCapacity; 242 | 243 | const RouteLocks routeLocks; 244 | 245 | const Pickups pickups; 246 | const Deliveries deliveries; 247 | 248 | RoutingModel model; 249 | RoutingModelParameters modelParams; 250 | RoutingSearchParameters searchParams; 251 | 252 | // Stores solution until we can translate back to v8 objects 253 | RoutingSolution solution; 254 | }; 255 | 256 | #endif 257 | -------------------------------------------------------------------------------- /test/tsp.js: -------------------------------------------------------------------------------- 1 | var tap = require('tap'); 2 | var ortools = require('..') 3 | 4 | 5 | // Locations in a grid and Manhattan Distance for costs 6 | 7 | var locations = [[0, 0], [0, 1], [0, 2], [0, 3], 8 | [1, 0], [1, 1], [1, 2], [1, 3], 9 | [2, 0], [2, 1], [2, 2], [2, 3], 10 | [3, 0], [3, 1], [3, 2], [3, 3]]; 11 | 12 | var depot = 0; 13 | 14 | function manhattanDistance(lhs, rhs) { 15 | return Math.abs(lhs[0] - rhs[0]) + Math.abs(lhs[1] - rhs[1]); 16 | } 17 | 18 | var costMatrix = new Array(locations.length); 19 | 20 | for (var from = 0; from < locations.length; ++from) { 21 | costMatrix[from] = new Array(locations.length); 22 | 23 | for (var to = 0; to < locations.length; ++to) { 24 | costMatrix[from][to] = manhattanDistance(locations[from], locations[to]); 25 | } 26 | } 27 | 28 | 29 | tap.test('Test TSP', function(assert) { 30 | 31 | var solverOpts = { 32 | numNodes: locations.length, 33 | costs: costMatrix 34 | }; 35 | 36 | var TSP = new ortools.TSP(solverOpts); 37 | 38 | var searchOpts = { 39 | computeTimeLimit: 1000, 40 | depotNode: depot 41 | }; 42 | 43 | TSP.Solve(searchOpts, function (err, solution) { 44 | assert.ifError(err, 'Solution can be found'); 45 | 46 | assert.type(solution, Array, 'Solution is Array of locations'); 47 | assert.equal(solution.length, locations.length - 1, 'Number of locations in route is number of locations without depot'); 48 | 49 | assert.ok(!solution.find(function (v) { return v == depot; }), 'Depot is not in routes'); 50 | 51 | function adjacentCost(acc, v) { return { cost: acc.cost + costMatrix[acc.at][v], at: v }; } 52 | var route = solution.reduce(adjacentCost, { cost: 0, at: depot }); 53 | assert.equal(route.cost, locations.length - 1, 'Costs are minimum Manhattan Distance in location grid'); 54 | 55 | assert.end(); 56 | }); 57 | 58 | }); 59 | -------------------------------------------------------------------------------- /test/vrp.js: -------------------------------------------------------------------------------- 1 | var tap = require('tap'); 2 | var ortools = require('..') 3 | 4 | 5 | // Locations in a grid and Manhattan Distance for costs 6 | 7 | var locations = [[0, 0], [0, 1], [0, 2], [0, 3], 8 | [1, 0], [1, 1], [1, 2], [1, 3], 9 | [2, 0], [2, 1], [2, 2], [2, 3], 10 | [3, 0], [3, 1], [3, 2], [3, 3]]; 11 | 12 | var depot = 0; 13 | 14 | function manhattanDistance(lhs, rhs) { 15 | return Math.abs(lhs[0] - rhs[0]) + Math.abs(lhs[1] - rhs[1]); 16 | } 17 | 18 | var costMatrix = new Array(locations.length); 19 | 20 | for (var from = 0; from < locations.length; ++from) { 21 | costMatrix[from] = new Array(locations.length); 22 | 23 | for (var to = 0; to < locations.length; ++to) { 24 | costMatrix[from][to] = manhattanDistance(locations[from], locations[to]); 25 | } 26 | } 27 | 28 | var dayStarts = Hours(0); 29 | var dayEnds = Hours(3); 30 | 31 | 32 | var seed = 2147483650; 33 | 34 | function ParkMillerRNG(seed) { 35 | var modulus = 2147483647; 36 | var multiplier = 48271; 37 | var increment = 0; 38 | var state = seed; 39 | 40 | return function() { 41 | state = (multiplier * state + increment) % modulus; 42 | return state / modulus; 43 | }; 44 | } 45 | 46 | var rand = ParkMillerRNG(seed); 47 | 48 | 49 | function Seconds(v) { return v; }; 50 | function Minutes(v) { return Seconds(v * 60); } 51 | function Hours(v) { return Minutes(v * 60); } 52 | 53 | 54 | var durationMatrix = new Array(locations.length); 55 | 56 | for (var from = 0; from < locations.length; ++from) { 57 | durationMatrix[from] = new Array(locations.length); 58 | 59 | for (var to = 0; to < locations.length; ++to) { 60 | var serviceTime = Minutes(3); 61 | var travelTime = Minutes(costMatrix[from][to]); 62 | 63 | durationMatrix[from][to] = serviceTime + travelTime; 64 | } 65 | } 66 | 67 | 68 | var timeWindows = new Array(locations.length); 69 | 70 | for (var at = 0; at < locations.length; ++at) { 71 | if (at === depot) { 72 | timeWindows[at] = [dayStarts, dayEnds]; 73 | continue; 74 | } 75 | 76 | var earliest = dayStarts; 77 | var latest = dayEnds - Hours(1); 78 | 79 | var start = rand() * (latest - earliest) + earliest; 80 | var stop = rand() * (latest - start) + start; 81 | 82 | timeWindows[at] = [start, stop]; 83 | } 84 | 85 | 86 | var demandMatrix = new Array(locations.length); 87 | 88 | for (var from = 0; from < locations.length; ++from) { 89 | demandMatrix[from] = new Array(locations.length); 90 | 91 | for (var to = 0; to < locations.length; ++to) { 92 | if (from === depot) 93 | demandMatrix[from][to] = 0 94 | else 95 | demandMatrix[from][to] = 1 96 | } 97 | } 98 | 99 | 100 | tap.test('Test VRP', function(assert) { 101 | 102 | var solverOpts = { 103 | numNodes: locations.length, 104 | costs: costMatrix, 105 | durations: durationMatrix, 106 | timeWindows: timeWindows, 107 | demands: demandMatrix 108 | }; 109 | 110 | var VRP = new ortools.VRP(solverOpts); 111 | 112 | var numVehicles = 10; 113 | var timeHorizon = dayEnds - dayStarts; 114 | var vehicleCapacity = 10; 115 | 116 | // Dummy lock to let vehicle 0 go to location 2 and 3 first - to test route locks 117 | var routeLocks = new Array(numVehicles); 118 | 119 | for (var vehicle = 0; vehicle < numVehicles; ++vehicle) { 120 | if (vehicle === 0) 121 | routeLocks[vehicle] = [2, 3]; 122 | else 123 | routeLocks[vehicle] = []; 124 | } 125 | 126 | var searchOpts = { 127 | computeTimeLimit: 1000, 128 | numVehicles: numVehicles, 129 | depotNode: depot, 130 | timeHorizon: timeHorizon, 131 | vehicleCapacity: vehicleCapacity, 132 | routeLocks: routeLocks, 133 | pickups: [4, 12], 134 | deliveries: [9, 8] 135 | }; 136 | 137 | VRP.Solve(searchOpts, function (err, solution) { 138 | assert.ifError(err, 'Solution can be found'); 139 | 140 | assert.type(solution, Object, 'Solution is Object with properties'); 141 | assert.type(solution.routes, Array, 'Routes in solution is Array with routes per vehicle'); 142 | assert.type(solution.times, Array, 'Times in solution is Array with time windows per vehicle'); 143 | 144 | assert.equal(solution.routes.length, numVehicles, 'Number of routes is number of vehicles'); 145 | assert.equal(solution.times.length, numVehicles, 'Number of time windows in number of vehicles'); 146 | 147 | function used(v) { return v.length == 0 ? 0 : 1; } 148 | function addition(l, r) { return l + r; } 149 | 150 | var numVehiclesUsed = solution.routes.map(used).reduce(addition, 0); 151 | 152 | assert.ok(numVehiclesUsed > 0, 'Solution uses vehicles'); 153 | assert.ok(numVehiclesUsed <= numVehiclesUsed, 'Solution uses up to number of vehicles'); 154 | 155 | function checkRoute(v) { 156 | var depotInRoute = v.find(function (u) { return u == depot; }); 157 | assert.ok(!depotInRoute, 'Depot is not in route'); 158 | } 159 | 160 | function checkTimeWindows(v) { 161 | v.forEach(function (u) { 162 | assert.ok(u[0] < u[1], 'Valid Time Window'); 163 | }); 164 | } 165 | 166 | solution.routes.forEach(checkRoute); 167 | solution.times.forEach(checkTimeWindows); 168 | 169 | // We locked vehicle 0 to go to location 2 and 3 first 170 | assert.equal(solution.routes[0][0], 2); 171 | assert.equal(solution.routes[0][1], 3); 172 | 173 | assert.end(); 174 | }); 175 | }); 176 | --------------------------------------------------------------------------------