nextroute 1.11.1.dev0__cp38-cp38-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of nextroute might be problematic. Click here for more details.
- nextroute/__about__.py +3 -0
- nextroute/__init__.py +18 -0
- nextroute/base_model.py +24 -0
- nextroute/bin/nextroute.exe +0 -0
- nextroute/check/__init__.py +28 -0
- nextroute/check/schema.py +145 -0
- nextroute/options.py +225 -0
- nextroute/schema/__init__.py +29 -0
- nextroute/schema/input.py +87 -0
- nextroute/schema/location.py +16 -0
- nextroute/schema/output.py +140 -0
- nextroute/schema/statistics.py +149 -0
- nextroute/schema/stop.py +65 -0
- nextroute/schema/vehicle.py +72 -0
- nextroute/solve.py +145 -0
- nextroute/version.py +12 -0
- nextroute-1.11.1.dev0.dist-info/LICENSE +87 -0
- nextroute-1.11.1.dev0.dist-info/METADATA +281 -0
- nextroute-1.11.1.dev0.dist-info/RECORD +26 -0
- nextroute-1.11.1.dev0.dist-info/WHEEL +5 -0
- nextroute-1.11.1.dev0.dist-info/top_level.txt +2 -0
- tests/schema/__init__.py +1 -0
- tests/schema/test_input.py +59 -0
- tests/schema/test_output.py +318 -0
- tests/solve_golden/__init__.py +1 -0
- tests/solve_golden/main.py +50 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
# © 2019-present nextmv.io inc
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import math
|
|
5
|
+
import os
|
|
6
|
+
import unittest
|
|
7
|
+
|
|
8
|
+
from nextroute import check as nextrouteCheck
|
|
9
|
+
from nextroute.schema import (
|
|
10
|
+
Location,
|
|
11
|
+
ObjectiveOutput,
|
|
12
|
+
Output,
|
|
13
|
+
PlannedStopOutput,
|
|
14
|
+
ResultStatistics,
|
|
15
|
+
RunStatistics,
|
|
16
|
+
SeriesData,
|
|
17
|
+
Solution,
|
|
18
|
+
Statistics,
|
|
19
|
+
StopOutput,
|
|
20
|
+
VehicleOutput,
|
|
21
|
+
Version,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TestOutput(unittest.TestCase):
|
|
26
|
+
filepath = os.path.join(os.path.dirname(__file__), "output.json")
|
|
27
|
+
|
|
28
|
+
def test_from_json(self):
|
|
29
|
+
with open(self.filepath) as f:
|
|
30
|
+
json_data = json.load(f)
|
|
31
|
+
|
|
32
|
+
nextroute_output = Output.from_dict(json_data)
|
|
33
|
+
parsed = nextroute_output.to_dict()
|
|
34
|
+
solution = parsed["solutions"][0]
|
|
35
|
+
|
|
36
|
+
for s, stop in enumerate(solution["unplanned"]):
|
|
37
|
+
original_stop = json_data["solutions"][0]["unplanned"][s]
|
|
38
|
+
self.assertEqual(
|
|
39
|
+
stop,
|
|
40
|
+
original_stop,
|
|
41
|
+
f"stop: parsed({stop}) and original ({original_stop}) should be equal",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
for v, vehicle in enumerate(solution["vehicles"]):
|
|
45
|
+
original_vehicle = json_data["solutions"][0]["vehicles"][v]
|
|
46
|
+
self.assertEqual(
|
|
47
|
+
vehicle,
|
|
48
|
+
original_vehicle,
|
|
49
|
+
f"vehicle: parsed ({vehicle}) and original ({original_vehicle}) should be equal",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
self.assertEqual(
|
|
53
|
+
solution["objective"],
|
|
54
|
+
json_data["solutions"][0]["objective"],
|
|
55
|
+
f"objective: parsed ({solution['objective']}) and "
|
|
56
|
+
f"original ({json_data['solutions'][0]['objective']}) should be equal",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
statistics = parsed["statistics"]
|
|
60
|
+
self.assertEqual(
|
|
61
|
+
statistics["run"],
|
|
62
|
+
json_data["statistics"]["run"],
|
|
63
|
+
f"run: parsed ({statistics['run']}) and original ({json_data['statistics']['run']}) should be equal",
|
|
64
|
+
)
|
|
65
|
+
self.assertEqual(
|
|
66
|
+
statistics["result"],
|
|
67
|
+
json_data["statistics"]["result"],
|
|
68
|
+
f"result: parsed ({statistics['result']}) and "
|
|
69
|
+
f"original ({json_data['statistics']['result']}) should be equal",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def test_from_dict(self):
|
|
73
|
+
with open(self.filepath) as f:
|
|
74
|
+
json_data = json.load(f)
|
|
75
|
+
|
|
76
|
+
nextroute_output = Output.from_dict(json_data)
|
|
77
|
+
|
|
78
|
+
version = nextroute_output.version
|
|
79
|
+
self.assertTrue(isinstance(version, Version), "Version should be of type Version.")
|
|
80
|
+
|
|
81
|
+
solutions = nextroute_output.solutions
|
|
82
|
+
for solution in solutions:
|
|
83
|
+
self.assertTrue(
|
|
84
|
+
isinstance(solution, Solution),
|
|
85
|
+
f"Solution ({solution}) should be of type Solution.",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
unplanned = solution.unplanned
|
|
89
|
+
for stop in unplanned:
|
|
90
|
+
self.assertTrue(
|
|
91
|
+
isinstance(stop, StopOutput),
|
|
92
|
+
f"Stop ({stop}) should be of type StopOutput.",
|
|
93
|
+
)
|
|
94
|
+
self.assertTrue(
|
|
95
|
+
stop.id is not None,
|
|
96
|
+
f"Stop ({stop}) should have an id.",
|
|
97
|
+
)
|
|
98
|
+
self.assertNotEqual(
|
|
99
|
+
stop.id,
|
|
100
|
+
"",
|
|
101
|
+
f"Stop ({stop}) should have a valid id.",
|
|
102
|
+
)
|
|
103
|
+
self.assertTrue(
|
|
104
|
+
isinstance(stop.location, Location),
|
|
105
|
+
f"Stop ({stop}) should have a location.",
|
|
106
|
+
)
|
|
107
|
+
self.assertGreaterEqual(
|
|
108
|
+
stop.location.lat,
|
|
109
|
+
-90,
|
|
110
|
+
f"Stop ({stop}) should have a valid latitude.",
|
|
111
|
+
)
|
|
112
|
+
self.assertLessEqual(
|
|
113
|
+
stop.location.lat,
|
|
114
|
+
90,
|
|
115
|
+
f"Stop ({stop}) should have a valid latitude.",
|
|
116
|
+
)
|
|
117
|
+
self.assertGreaterEqual(
|
|
118
|
+
stop.location.lon,
|
|
119
|
+
-180,
|
|
120
|
+
f"Stop ({stop}) should have a valid longitude.",
|
|
121
|
+
)
|
|
122
|
+
self.assertLessEqual(
|
|
123
|
+
stop.location.lon,
|
|
124
|
+
180,
|
|
125
|
+
f"Stop ({stop}) should have a valid longitude.",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
vehicles = solution.vehicles
|
|
129
|
+
for vehicle in vehicles:
|
|
130
|
+
self.assertTrue(
|
|
131
|
+
isinstance(vehicle, VehicleOutput),
|
|
132
|
+
f"Vehicle ({vehicle}) should be of type VehicleOutput.",
|
|
133
|
+
)
|
|
134
|
+
self.assertTrue(
|
|
135
|
+
vehicle.id is not None,
|
|
136
|
+
f"Vehicle ({vehicle}) should have an id.",
|
|
137
|
+
)
|
|
138
|
+
self.assertNotEqual(
|
|
139
|
+
vehicle.id,
|
|
140
|
+
"",
|
|
141
|
+
f"Vehicle ({vehicle}) should have a valid id.",
|
|
142
|
+
)
|
|
143
|
+
self.assertGreaterEqual(
|
|
144
|
+
vehicle.route_duration,
|
|
145
|
+
0,
|
|
146
|
+
f"Vehicle ({vehicle}) should have a valid route duration.",
|
|
147
|
+
)
|
|
148
|
+
self.assertGreaterEqual(
|
|
149
|
+
vehicle.route_stops_duration,
|
|
150
|
+
0,
|
|
151
|
+
f"Vehicle ({vehicle}) should have a valid route stops duration.",
|
|
152
|
+
)
|
|
153
|
+
self.assertGreaterEqual(
|
|
154
|
+
vehicle.route_travel_distance,
|
|
155
|
+
0,
|
|
156
|
+
f"Vehicle ({vehicle}) should have a valid route travel distance.",
|
|
157
|
+
)
|
|
158
|
+
self.assertGreaterEqual(
|
|
159
|
+
vehicle.route_travel_duration,
|
|
160
|
+
0,
|
|
161
|
+
f"Vehicle ({vehicle}) should have a valid route travel duration.",
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
for stop in vehicle.route:
|
|
165
|
+
self.assertTrue(
|
|
166
|
+
isinstance(stop, PlannedStopOutput),
|
|
167
|
+
f"Stop ({stop}) should be of type PlannedStopOutput.",
|
|
168
|
+
)
|
|
169
|
+
self.assertTrue(
|
|
170
|
+
isinstance(stop.stop, StopOutput),
|
|
171
|
+
f"Stop ({stop}) should have a stop.",
|
|
172
|
+
)
|
|
173
|
+
self.assertGreaterEqual(
|
|
174
|
+
stop.travel_duration,
|
|
175
|
+
0,
|
|
176
|
+
f"Stop ({stop}) should have a valid travel duration.",
|
|
177
|
+
)
|
|
178
|
+
self.assertGreaterEqual(
|
|
179
|
+
stop.cumulative_travel_duration,
|
|
180
|
+
0,
|
|
181
|
+
f"Stop ({stop}) should have a valid cumulative travel duration.",
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
objective = solution.objective
|
|
185
|
+
self.assertTrue(
|
|
186
|
+
isinstance(objective, ObjectiveOutput),
|
|
187
|
+
f"Objective ({objective}) should be of type ObjectiveOutput.",
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
statistics = nextroute_output.statistics
|
|
191
|
+
self.assertTrue(
|
|
192
|
+
isinstance(statistics, Statistics),
|
|
193
|
+
f"Statistics ({statistics}) should be of type Statistics.",
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
run_statistics = statistics.run
|
|
197
|
+
self.assertTrue(
|
|
198
|
+
isinstance(run_statistics, RunStatistics),
|
|
199
|
+
f"Run statistics ({run_statistics}) should be of type RunStatistics.",
|
|
200
|
+
)
|
|
201
|
+
self.assertGreaterEqual(
|
|
202
|
+
run_statistics.duration,
|
|
203
|
+
0,
|
|
204
|
+
f"Run statistics ({run_statistics}) should have a valid duration.",
|
|
205
|
+
)
|
|
206
|
+
self.assertGreaterEqual(
|
|
207
|
+
run_statistics.iterations,
|
|
208
|
+
0,
|
|
209
|
+
f"Run statistics ({run_statistics}) should have a valid number of iterations.",
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
result_statistics = statistics.result
|
|
213
|
+
self.assertTrue(
|
|
214
|
+
isinstance(result_statistics, ResultStatistics),
|
|
215
|
+
f"Result statistics ({result_statistics}) should be of type ResultStatistics.",
|
|
216
|
+
)
|
|
217
|
+
self.assertGreaterEqual(
|
|
218
|
+
result_statistics.duration,
|
|
219
|
+
0,
|
|
220
|
+
f"Result statistics ({result_statistics}) should have a valid duration.",
|
|
221
|
+
)
|
|
222
|
+
self.assertGreaterEqual(
|
|
223
|
+
result_statistics.value,
|
|
224
|
+
0,
|
|
225
|
+
f"Result statistics ({result_statistics}) should have a valid value.",
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
series_data = statistics.series_data
|
|
229
|
+
self.assertTrue(
|
|
230
|
+
isinstance(series_data, SeriesData),
|
|
231
|
+
f"Series data ({series_data}) should be of type SeriesData.",
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
def test_with_check(self):
|
|
235
|
+
with open(os.path.join(os.path.dirname(__file__), "output_with_check.json")) as f:
|
|
236
|
+
json_data = json.load(f)
|
|
237
|
+
|
|
238
|
+
nextroute_output = Output.from_dict(json_data)
|
|
239
|
+
check = nextroute_output.solutions[0].check
|
|
240
|
+
self.assertTrue(
|
|
241
|
+
isinstance(check, nextrouteCheck.Output),
|
|
242
|
+
f"Check ({check}) should be of type nextrouteCheck.Output.",
|
|
243
|
+
)
|
|
244
|
+
self.assertTrue(
|
|
245
|
+
isinstance(check.solution, nextrouteCheck.Solution),
|
|
246
|
+
f"Solution ({check.solution}) should be of type nextrouteCheck.checkSolution.",
|
|
247
|
+
)
|
|
248
|
+
self.assertTrue(
|
|
249
|
+
isinstance(check.summary, nextrouteCheck.Summary),
|
|
250
|
+
f"Summary ({check.summary}) should be of type nextrouteCheck.Summary.",
|
|
251
|
+
)
|
|
252
|
+
for plan_unit in check.plan_units:
|
|
253
|
+
self.assertTrue(
|
|
254
|
+
isinstance(plan_unit, nextrouteCheck.PlanUnit),
|
|
255
|
+
f"Plan unit ({plan_unit}) should be of type nextrouteCheck.PlanUnit.",
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
for vehicle in check.vehicles:
|
|
259
|
+
self.assertTrue(
|
|
260
|
+
isinstance(vehicle, nextrouteCheck.Vehicle),
|
|
261
|
+
f"Vehicle ({vehicle}) should be of type nextrouteCheck.Vehicle",
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
def test_result_statistics_decoding(self):
|
|
265
|
+
test_cases = [
|
|
266
|
+
{
|
|
267
|
+
"name": "value is float",
|
|
268
|
+
"json_stats": '{"duration": 0.1, "value": 1.23}',
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
"name": "value is nan",
|
|
272
|
+
"json_stats": '{"duration": 0.1, "value": "nan"}',
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
"name": "value is infinity",
|
|
276
|
+
"json_stats": '{"duration": 0.1, "value": "inf"}',
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
"name": "value is infinity 2",
|
|
280
|
+
"json_stats": '{"duration": 0.1, "value": "+inf"}',
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
"name": "value is -infinity",
|
|
284
|
+
"json_stats": '{"duration": 0.1, "value": "-inf"}',
|
|
285
|
+
},
|
|
286
|
+
]
|
|
287
|
+
|
|
288
|
+
for test in test_cases:
|
|
289
|
+
dict_stats = json.loads(test["json_stats"])
|
|
290
|
+
stats = ResultStatistics.from_dict(dict_stats)
|
|
291
|
+
self.assertTrue(isinstance(stats, ResultStatistics))
|
|
292
|
+
self.assertTrue(isinstance(stats.value, float))
|
|
293
|
+
|
|
294
|
+
def test_result_statistics_encoding(self):
|
|
295
|
+
test_cases = [
|
|
296
|
+
{
|
|
297
|
+
"name": "value is float",
|
|
298
|
+
"stats": ResultStatistics(duration=0.1, value=1.23),
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
"name": "value is nan",
|
|
302
|
+
"stats": ResultStatistics(duration=0.1, value=math.nan),
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
"name": "value is infinity",
|
|
306
|
+
"stats": ResultStatistics(duration=0.1, value=math.inf),
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
"name": "value is -infinity",
|
|
310
|
+
"stats": ResultStatistics(duration=0.1, value=-1 * math.inf),
|
|
311
|
+
},
|
|
312
|
+
]
|
|
313
|
+
|
|
314
|
+
for test in test_cases:
|
|
315
|
+
stats = test["stats"]
|
|
316
|
+
dict_stats = stats.to_dict()
|
|
317
|
+
self.assertTrue(isinstance(dict_stats, dict))
|
|
318
|
+
self.assertTrue(isinstance(dict_stats["value"], float))
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# © 2019-present nextmv.io inc
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# This script is copied to the `src` root so that the `nextroute` import is
|
|
2
|
+
# resolved. It is fed an input via stdin and is meant to write the output to
|
|
3
|
+
# stdout.
|
|
4
|
+
import nextmv
|
|
5
|
+
|
|
6
|
+
import nextroute
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main() -> None:
|
|
10
|
+
"""Entry point for the program."""
|
|
11
|
+
|
|
12
|
+
parameters = [
|
|
13
|
+
nextmv.Parameter("input", str, "", "Path to input file. Default is stdin.", False),
|
|
14
|
+
nextmv.Parameter("output", str, "", "Path to output file. Default is stdout.", False),
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
default_options = nextroute.Options()
|
|
18
|
+
for name, default_value in default_options.to_dict().items():
|
|
19
|
+
parameters.append(nextmv.Parameter(name.lower(), type(default_value), default_value, name, False))
|
|
20
|
+
|
|
21
|
+
options = nextmv.Options(*parameters)
|
|
22
|
+
|
|
23
|
+
input = nextmv.load_local(options=options, path=options.input)
|
|
24
|
+
|
|
25
|
+
nextmv.log("Solving vehicle routing problem:")
|
|
26
|
+
nextmv.log(f" - stops: {len(input.data.get('stops', []))}")
|
|
27
|
+
nextmv.log(f" - vehicles: {len(input.data.get('vehicles', []))}")
|
|
28
|
+
|
|
29
|
+
model = DecisionModel()
|
|
30
|
+
output = model.solve(input)
|
|
31
|
+
nextmv.write_local(output, path=options.output)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class DecisionModel(nextmv.Model):
|
|
35
|
+
def solve(self, input: nextmv.Input) -> nextmv.Output:
|
|
36
|
+
"""Solves the given problem and returns the solution."""
|
|
37
|
+
|
|
38
|
+
nextroute_input = nextroute.schema.Input.from_dict(input.data)
|
|
39
|
+
nextroute_options = nextroute.Options.extract_from_dict(input.options.to_dict())
|
|
40
|
+
nextroute_output = nextroute.solve(nextroute_input, nextroute_options)
|
|
41
|
+
|
|
42
|
+
return nextmv.Output(
|
|
43
|
+
options=input.options,
|
|
44
|
+
solution=nextroute_output.solutions[0].to_dict(),
|
|
45
|
+
statistics=nextroute_output.statistics.to_dict(),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
if __name__ == "__main__":
|
|
50
|
+
main()
|