PyRCH 0.0.1__tar.gz

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.
Files changed (90) hide show
  1. pyrch-0.0.1/.github/workflows/publish-pypi.yml +84 -0
  2. pyrch-0.0.1/.gitignore +54 -0
  3. pyrch-0.0.1/PKG-INFO +444 -0
  4. pyrch-0.0.1/README.md +421 -0
  5. pyrch-0.0.1/cpp/CMakeLists.txt +20 -0
  6. pyrch-0.0.1/cpp/README.md +41 -0
  7. pyrch-0.0.1/cpp/prog/json_solver.cpp +256 -0
  8. pyrch-0.0.1/cpp/src/binary_set.hpp +202 -0
  9. pyrch-0.0.1/cpp/src/debug.hpp +36 -0
  10. pyrch-0.0.1/cpp/src/graph.hpp +608 -0
  11. pyrch-0.0.1/cpp/src/include/experiment_map.hpp +162 -0
  12. pyrch-0.0.1/cpp/src/include/experiment_runner.hpp +152 -0
  13. pyrch-0.0.1/cpp/src/include/search_LKH_mtsp.hpp +178 -0
  14. pyrch-0.0.1/cpp/src/include/search_greedy_mtsp.hpp +153 -0
  15. pyrch-0.0.1/cpp/src/include/solver_types.hpp +69 -0
  16. pyrch-0.0.1/cpp/src/mtsp/constraint.hpp +99 -0
  17. pyrch-0.0.1/cpp/src/mtsp/options.hpp +27 -0
  18. pyrch-0.0.1/cpp/src/mtsp/plugins/constraint_checker/composite_constraint_checker.cpp +80 -0
  19. pyrch-0.0.1/cpp/src/mtsp/plugins/constraint_checker/composite_constraint_checker.hpp +63 -0
  20. pyrch-0.0.1/cpp/src/mtsp/plugins/constraint_checker/default_constraint_checker.cpp +42 -0
  21. pyrch-0.0.1/cpp/src/mtsp/plugins/constraint_checker/default_constraint_checker.hpp +66 -0
  22. pyrch-0.0.1/cpp/src/mtsp/plugins/constraint_checker/time_limit_constraint_checker.cpp +17 -0
  23. pyrch-0.0.1/cpp/src/mtsp/plugins/constraint_checker/time_limit_constraint_checker.hpp +38 -0
  24. pyrch-0.0.1/cpp/src/mtsp/plugins/constraint_checker/time_window_constraint_checker.cpp +209 -0
  25. pyrch-0.0.1/cpp/src/mtsp/plugins/constraint_checker/time_window_constraint_checker.hpp +104 -0
  26. pyrch-0.0.1/cpp/src/mtsp/plugins/expansion/default_expansion.cpp +24 -0
  27. pyrch-0.0.1/cpp/src/mtsp/plugins/expansion/default_expansion.hpp +42 -0
  28. pyrch-0.0.1/cpp/src/mtsp/plugins/focal_comparator/default_focal_comparator.cpp +23 -0
  29. pyrch-0.0.1/cpp/src/mtsp/plugins/focal_comparator/default_focal_comparator.hpp +43 -0
  30. pyrch-0.0.1/cpp/src/mtsp/plugins/focal_comparator/fvalue_focal_comparator.cpp +23 -0
  31. pyrch-0.0.1/cpp/src/mtsp/plugins/focal_comparator/fvalue_focal_comparator.hpp +43 -0
  32. pyrch-0.0.1/cpp/src/mtsp/plugins/heuristic/default_heuristic.cpp +81 -0
  33. pyrch-0.0.1/cpp/src/mtsp/plugins/heuristic/default_heuristic.hpp +73 -0
  34. pyrch-0.0.1/cpp/src/mtsp/plugins/heuristic/zero_heuristic.hpp +42 -0
  35. pyrch-0.0.1/cpp/src/mtsp/plugins/objective/default_objective.cpp +33 -0
  36. pyrch-0.0.1/cpp/src/mtsp/plugins/objective/default_objective.hpp +42 -0
  37. pyrch-0.0.1/cpp/src/mtsp/plugins/objective/min_sum_objective.cpp +23 -0
  38. pyrch-0.0.1/cpp/src/mtsp/plugins/objective/min_sum_objective.hpp +40 -0
  39. pyrch-0.0.1/cpp/src/mtsp/plugins/plugin.hpp +291 -0
  40. pyrch-0.0.1/cpp/src/mtsp/plugins/post_optimizer/composite_post_optimizer.hpp +43 -0
  41. pyrch-0.0.1/cpp/src/mtsp/plugins/post_optimizer/default_post_optimizer.cpp +586 -0
  42. pyrch-0.0.1/cpp/src/mtsp/plugins/post_optimizer/default_post_optimizer.hpp +252 -0
  43. pyrch-0.0.1/cpp/src/mtsp/plugins/post_optimizer/no_op_post_optimizer.hpp +35 -0
  44. pyrch-0.0.1/cpp/src/mtsp/plugins/post_optimizer/route_merging_post_optimizer.cpp +318 -0
  45. pyrch-0.0.1/cpp/src/mtsp/plugins/post_optimizer/route_merging_post_optimizer.hpp +96 -0
  46. pyrch-0.0.1/cpp/src/mtsp/plugins.hpp +51 -0
  47. pyrch-0.0.1/cpp/src/mtsp/problem.cpp +193 -0
  48. pyrch-0.0.1/cpp/src/mtsp/problem.hpp +79 -0
  49. pyrch-0.0.1/cpp/src/mtsp/solution.hpp +22 -0
  50. pyrch-0.0.1/cpp/src/mtsp/solver.hpp +533 -0
  51. pyrch-0.0.1/cpp/src/mtsp/types.hpp +50 -0
  52. pyrch-0.0.1/cpp/src/source/binary_set.cpp +15 -0
  53. pyrch-0.0.1/cpp/src/source/experiment_map.cpp +272 -0
  54. pyrch-0.0.1/cpp/src/source/experiment_runner.cpp +726 -0
  55. pyrch-0.0.1/cpp/src/source/graph.cpp +1063 -0
  56. pyrch-0.0.1/cpp/src/source/graph_io.cpp +197 -0
  57. pyrch-0.0.1/cpp/src/source/search_LKH_mtsp.cpp +665 -0
  58. pyrch-0.0.1/cpp/src/source/search_greedy_mtsp.cpp +315 -0
  59. pyrch-0.0.1/cpp/src/source/solver.cpp +1297 -0
  60. pyrch-0.0.1/cpp/src/source/solver_types.cpp +50 -0
  61. pyrch-0.0.1/cpp/src/type_def.hpp +20 -0
  62. pyrch-0.0.1/cpp/src/utils/bitsets.hpp +210 -0
  63. pyrch-0.0.1/cpp/src/utils/path.hpp +31 -0
  64. pyrch-0.0.1/cpp/src/utils/statis.hpp +53 -0
  65. pyrch-0.0.1/cpp/src/utils/timer.hpp +39 -0
  66. pyrch-0.0.1/cpp/src/vec_type.hpp +299 -0
  67. pyrch-0.0.1/cpp/third_party/CLI11.hpp +11527 -0
  68. pyrch-0.0.1/cpp/third_party/catch.hpp +17976 -0
  69. pyrch-0.0.1/cpp/third_party/json.hpp +25540 -0
  70. pyrch-0.0.1/docs/architecture.md +167 -0
  71. pyrch-0.0.1/docs/tools.md +87 -0
  72. pyrch-0.0.1/example/01_solve_from_dict.py +20 -0
  73. pyrch-0.0.1/example/02_solve_from_json_inputs.py +31 -0
  74. pyrch-0.0.1/example/03_planner_api.py +18 -0
  75. pyrch-0.0.1/example/04_low_level_api.py +90 -0
  76. pyrch-0.0.1/example/05_visualization.py +45 -0
  77. pyrch-0.0.1/example/README.md +61 -0
  78. pyrch-0.0.1/example/common.py +120 -0
  79. pyrch-0.0.1/example/data/problem.json +115 -0
  80. pyrch-0.0.1/example/data/verify_problem.json +40 -0
  81. pyrch-0.0.1/example/output/planner_map.png +0 -0
  82. pyrch-0.0.1/example/output/planner_result.png +0 -0
  83. pyrch-0.0.1/example/verify_examples.py +160 -0
  84. pyrch-0.0.1/pyproject.toml +38 -0
  85. pyrch-0.0.1/python/CMakeLists.txt +33 -0
  86. pyrch-0.0.1/python/rch_bind.cpp +244 -0
  87. pyrch-0.0.1/rch/__init__.py +77 -0
  88. pyrch-0.0.1/rch/_api.py +286 -0
  89. pyrch-0.0.1/rch/_viz.py +243 -0
  90. pyrch-0.0.1/tools/compile +8 -0
@@ -0,0 +1,84 @@
1
+ name: Publish PyPI
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ push:
6
+ tags:
7
+ - "v*"
8
+
9
+ jobs:
10
+ build_wheels:
11
+ name: Build wheel on ${{ matrix.os }}
12
+ runs-on: ${{ matrix.os }}
13
+ strategy:
14
+ fail-fast: false
15
+ matrix:
16
+ os: [ubuntu-latest, macos-latest, windows-latest]
17
+
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+
21
+ - name: Set up QEMU
22
+ if: runner.os == 'Linux' && runner.arch == 'X64'
23
+ uses: docker/setup-qemu-action@v3
24
+ with:
25
+ platforms: arm64
26
+
27
+ - uses: pypa/cibuildwheel@v2.20.0
28
+ env:
29
+ CIBW_BUILD: cp38-* cp39-* cp310-* cp311-* cp312-*
30
+ CIBW_SKIP: "*-musllinux_* pp* *-win32 *-manylinux_i686"
31
+ CIBW_ARCHS_LINUX: "auto aarch64"
32
+ CIBW_TEST_COMMAND: python -c "import rch; print(rch.__version__)"
33
+ CIBW_TEST_SKIP: "*-*linux_aarch64"
34
+
35
+ - uses: actions/upload-artifact@v4
36
+ with:
37
+ name: wheels-${{ matrix.os }}
38
+ path: wheelhouse/*.whl
39
+
40
+ build_sdist:
41
+ name: Build sdist
42
+ runs-on: ubuntu-latest
43
+
44
+ steps:
45
+ - uses: actions/checkout@v4
46
+
47
+ - uses: actions/setup-python@v5
48
+ with:
49
+ python-version: "3.12"
50
+
51
+ - name: Install build frontend
52
+ run: python -m pip install --upgrade build
53
+
54
+ - name: Build sdist
55
+ run: python -m build --sdist
56
+
57
+ - uses: actions/upload-artifact@v4
58
+ with:
59
+ name: sdist
60
+ path: dist/*.tar.gz
61
+
62
+ publish:
63
+ name: Publish to PyPI
64
+ needs: [build_wheels, build_sdist]
65
+ runs-on: ubuntu-latest
66
+ if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch'
67
+ permissions:
68
+ id-token: write
69
+
70
+ steps:
71
+ - uses: actions/download-artifact@v4
72
+ with:
73
+ path: dist
74
+
75
+ - name: Flatten artifacts
76
+ shell: bash
77
+ run: |
78
+ mkdir -p publish-dist
79
+ find dist -type f \( -name "*.whl" -o -name "*.tar.gz" \) -exec cp {} publish-dist/ \;
80
+ ls -lah publish-dist
81
+
82
+ - uses: pypa/gh-action-pypi-publish@release/v1
83
+ with:
84
+ packages-dir: publish-dist
pyrch-0.0.1/.gitignore ADDED
@@ -0,0 +1,54 @@
1
+ # Prerequisites
2
+ *.d
3
+
4
+ # Compiled Object files
5
+ *.slo
6
+ *.lo
7
+ *.o
8
+ *.obj
9
+
10
+ # Precompiled Headers
11
+ *.gch
12
+ *.pch
13
+
14
+ # Linker files
15
+ *.ilk
16
+
17
+ # Debugger Files
18
+ *.pdb
19
+
20
+ # Compiled Dynamic libraries
21
+ *.so
22
+ *.dylib
23
+ *.dll
24
+
25
+ # Fortran module files
26
+ *.mod
27
+ *.smod
28
+
29
+ # Compiled Static libraries
30
+ *.lai
31
+ *.la
32
+ *.a
33
+ *.lib
34
+
35
+ # Executables
36
+ *.exe
37
+ *.out
38
+ *.app
39
+
40
+ # debug information files
41
+ *.dwo
42
+
43
+ .vscode
44
+
45
+ __pycache__
46
+
47
+ heha_tw_subset_ablation/
48
+ pure_tw_ablation/
49
+ heha_tw_*/
50
+ heha_dtw_*/
51
+
52
+ .venv/
53
+ build/
54
+ dist/
pyrch-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,444 @@
1
+ Metadata-Version: 2.2
2
+ Name: PyRCH
3
+ Version: 0.0.1
4
+ Summary: RCH: Partial-Expansion Anytime Focal Search solver for heterogeneous multi-agent TSP
5
+ Keywords: mtsp,tsp,multi-agent,path-planning,routing,heterogeneous-agents
6
+ Author: MTSP-Solver Team
7
+ License: MIT
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3 :: Only
11
+ Classifier: Programming Language :: C++
12
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
13
+ Classifier: Operating System :: POSIX :: Linux
14
+ Project-URL: Homepage, https://github.com/rap-lab-org/dev_peaf
15
+ Project-URL: Repository, https://github.com/rap-lab-org/dev_peaf
16
+ Project-URL: Issues, https://github.com/rap-lab-org/dev_peaf/issues
17
+ Requires-Python: >=3.8
18
+ Provides-Extra: dev
19
+ Requires-Dist: pytest; extra == "dev"
20
+ Provides-Extra: viz
21
+ Requires-Dist: matplotlib>=3.8; extra == "viz"
22
+ Description-Content-Type: text/markdown
23
+
24
+ # RCH Solver
25
+
26
+ **RCH** — a solver for the **Heterogeneous Multi-Agent Travelling Salesman Problem (MTSP)**.
27
+
28
+ This package wraps the high-performance C++ solver core via [pybind11](https://github.com/pybind/pybind11) and makes it available as a regular Python library.
29
+
30
+ ---
31
+
32
+ ## Installation
33
+
34
+ ### Prerequisites
35
+
36
+ - Python >= 3.8
37
+ - A C++17 compiler (`g++ >= 7`, `clang++ >= 5`)
38
+ - CMake >= 3.15
39
+ - pip
40
+ - matplotlib (only needed for visualization helpers)
41
+
42
+ ### Install from PyPI
43
+
44
+ ```bash
45
+ pip install PyRCH
46
+ ```
47
+
48
+ If you also want plotting helpers:
49
+
50
+ ```bash
51
+ pip install "PyRCH[viz]"
52
+ ```
53
+
54
+ ### Install from source
55
+
56
+ ```bash
57
+ cd MTSP-Solver
58
+
59
+ # Install (builds the C++ extension automatically)
60
+ pip install .
61
+
62
+ # Or, for development (editable install):
63
+ pip install -e .
64
+ ```
65
+
66
+ > **Note**: On Ubuntu 24+ managed Python installs, you may need to add `--break-system-packages` or use a virtual environment.
67
+
68
+ ## Release To PyPI
69
+
70
+ If you are publishing manually from a Linux development machine, upload the **sdist** only:
71
+
72
+ ```bash
73
+ python -m build --sdist
74
+ twine check dist/*
75
+ twine upload dist/pyrch-*.tar.gz
76
+ ```
77
+
78
+ > **Why not upload the local wheel?** A wheel built locally on Linux is typically tagged like `linux_x86_64` or `linux_aarch64`. PyPI rejects those platform tags. Linux wheels uploaded to PyPI should be built as compliant `manylinux` wheels, which this repository produces through GitHub Actions.
79
+
80
+ To publish wheels:
81
+
82
+ 1. Configure PyPI Trusted Publishing for this repository.
83
+ 2. Push a version tag such as `v0.0.1`.
84
+ 3. Let `.github/workflows/publish-pypi.yml` build and upload the wheels plus sdist.
85
+
86
+ ---
87
+
88
+ ## Quick Start
89
+
90
+ ### 1. Solve a JSON problem file
91
+
92
+ ```python
93
+ import rch
94
+
95
+ result = rch.solve("path/to/problem.json", time_limit=10)
96
+
97
+ print(result["status"]) # "success" or "failed"
98
+ print(result["timeout"]) # True if time-limit was reached
99
+ print(result["statistics"]) # {"max_cost": ..., "sum_cost": ..., "solve_time": ..., ...}
100
+
101
+ for route in result["routes"]:
102
+ print(f"Agent {route['agent_id']}: path={route['path']}, cost={route['cost']}")
103
+
104
+ # Anytime improvement history
105
+ for snapshot in result["anytime"]:
106
+ print(f" t={snapshot['time']:.3f}s max_cost={snapshot['max_cost']:.2f}")
107
+ ```
108
+
109
+ ### 2. Solve from a Python dict
110
+
111
+ ```python
112
+ import rch
113
+
114
+ problem_data = {
115
+ "nodes": [
116
+ {"id": 0, "x": 0.0, "y": 0.0, "type": "depot"},
117
+ {"id": 1, "x": 1.0, "y": 2.0, "type": "target"},
118
+ {"id": 2, "x": 3.0, "y": 1.0, "type": "target"},
119
+ ],
120
+ "agents": [
121
+ {"id": 0, "type": "UAV", "start_node": 0, "end_node": 0},
122
+ {"id": 1, "type": "UAV", "start_node": 0, "end_node": 0},
123
+ ],
124
+ "costs": {
125
+ "UAV": [
126
+ [0.0, 2.24, 3.16],
127
+ [2.24, 0.0, 2.24],
128
+ [3.16, 2.24, 0.0],
129
+ ]
130
+ },
131
+ "options": {
132
+ "return_to_end": True,
133
+ "objective": "min_max"
134
+ }
135
+ }
136
+
137
+ result = rch.solve(problem_data, time_limit=5)
138
+ print(result)
139
+ ```
140
+
141
+ ### 3. Planner API (recommended)
142
+
143
+ ```python
144
+ import rch
145
+
146
+ planner = rch.Planner()
147
+
148
+ # Add nodes
149
+ planner.add_depot(0, x=0.0, y=0.0)
150
+ planner.add_target(1, x=1.0, y=2.0)
151
+ planner.add_target(2, x=3.0, y=1.0)
152
+
153
+ # Add agents
154
+ planner.add_agent(agent_id=0, agent_type="UAV", start_node=0, end_node=0, time_limit=6.0)
155
+ planner.add_agent(agent_id=1, agent_type="UGV", start_node=0, end_node=0, time_limit=9.0)
156
+
157
+ # Set cost matrices (one per agent type)
158
+ planner.set_cost_matrix("UAV", [
159
+ [0.0, 2.24, 3.16],
160
+ [2.24, 0.0, 2.24],
161
+ [3.16, 2.24, 0.0],
162
+ ])
163
+ planner.set_cost_matrix("UGV", [
164
+ [0.0, 1.80, 4.20],
165
+ [1.80, 0.0, 2.90],
166
+ [4.20, 2.90, 0.0],
167
+ ])
168
+
169
+ # Add constraints (optional)
170
+ planner.add_assignment(1, ["UAV"])
171
+ planner.add_assignment(2, ["UGV"])
172
+ planner.add_time_window(1, start=0.0, end=3.0)
173
+ planner.add_time_window(2, start=0.0, end=6.0)
174
+
175
+ # Set solver options
176
+ planner.set_options(return_to_end=True, objective="min_max", time_limit=5)
177
+
178
+ # Visualize the problem map
179
+ planner.show_map()
180
+
181
+ # Solve
182
+ result = planner.solve()
183
+ print(result["status"])
184
+ for route in result["routes"]:
185
+ print(f" Agent {route['agent_id']}: path={route['path']}, cost={route['cost']:.3f}")
186
+
187
+ # Visualize the result
188
+ planner.show_result(result)
189
+ ```
190
+
191
+ ### 4. Visualization
192
+
193
+ #### `show_map` — view the problem before solving
194
+
195
+ ```python
196
+ # Via Planner method:
197
+ planner.show_map()
198
+
199
+ # Or via module-level function:
200
+ rch.show_map(planner)
201
+
202
+ # Draw on an existing axes (e.g. for subplots):
203
+ import matplotlib.pyplot as plt
204
+ fig, ax = plt.subplots()
205
+ rch.show_map(planner, ax=ax, show=False)
206
+ plt.savefig("map.png")
207
+ ```
208
+
209
+ Depots are drawn as black stars (★), targets as grey dots, and each agent's start position as a coloured triangle.
210
+
211
+ #### `show_result` — view routes after solving
212
+
213
+ ```python
214
+ result = planner.solve()
215
+
216
+ # Via Planner method:
217
+ planner.show_result(result)
218
+
219
+ # Or via module-level function:
220
+ rch.show_result(planner, result)
221
+ ```
222
+
223
+ Each agent's route is drawn with a distinct colour and directional arrows.
224
+
225
+ **`return_to_end=False` handling**: when the solver option `return_to_end` is `False`, the solver internally appends the depot as the last node in each route. `show_result` automatically detects this and removes the trailing depot from the visualization — the route will end at the last target visited, without drawing an edge back to the depot.
226
+
227
+ ### 5. Low-level programmatic API
228
+
229
+ ```python
230
+ from rch import Problem, Solver, Options, ObjectiveType, NodeType
231
+ from rch import Node, Agent, AssignmentConstraint, TimeWindowConstraint
232
+
233
+ # Build a problem instance in code
234
+ problem = Problem()
235
+
236
+ # Add nodes
237
+ for nid, (x, y), ntype in [(0, (0, 0), NodeType.DEPOT),
238
+ (1, (1, 2), NodeType.TARGET),
239
+ (2, (3, 1), NodeType.TARGET)]:
240
+ n = Node()
241
+ n.id = nid
242
+ n.position.x = x
243
+ n.position.y = y
244
+ n.type = ntype
245
+ problem.add_node(n)
246
+
247
+ # Add agents
248
+ a = Agent()
249
+ a.id = 0
250
+ a.type = "UAV"
251
+ a.start_node = 0
252
+ a.end_node = 0
253
+ problem.add_agent(a, 0)
254
+
255
+ a2 = Agent()
256
+ a2.id = 1
257
+ a2.type = "UAV"
258
+ a2.start_node = 0
259
+ a2.end_node = 0
260
+ problem.add_agent(a2, 1)
261
+
262
+ # Set cost matrix (one per agent type)
263
+ import math
264
+ nodes_xy = [(0,0), (1,2), (3,1)]
265
+ n = len(nodes_xy)
266
+ cost = [[0.0]*n for _ in range(n)]
267
+ for i in range(n):
268
+ for j in range(n):
269
+ dx = nodes_xy[i][0] - nodes_xy[j][0]
270
+ dy = nodes_xy[i][1] - nodes_xy[j][1]
271
+ cost[i][j] = math.sqrt(dx*dx + dy*dy)
272
+ problem.set_cost_matrix("UAV", cost)
273
+
274
+ # Configure options
275
+ problem.options().objective = ObjectiveType.MinMax
276
+ problem.options().return_to_end = True
277
+ problem.options().time_limit = 5.0
278
+
279
+ # Solve
280
+ opts = problem.options()
281
+ solver = Solver(problem, opts)
282
+ ret = solver.solve()
283
+ result = solver.get_result()
284
+
285
+ # Note: this low-level API follows the original C++ convention:
286
+ # ret == 1 means success, ret == 0 means failure.
287
+ print(f"Return code: {ret}")
288
+ print(f"Paths: {result.paths}")
289
+ print(f"Costs: {result.times}")
290
+ ```
291
+
292
+ ---
293
+
294
+ ## JSON Input Format
295
+
296
+ ```json
297
+ {
298
+ "nodes": [
299
+ {"id": 0, "x": 0.0, "y": 0.0, "z": 0.0, "type": "depot"},
300
+ {"id": 1, "x": 1.5, "y": 2.3, "z": 0.0, "type": "target"}
301
+ ],
302
+ "agents": [
303
+ {
304
+ "id": 0,
305
+ "type": "TypeA",
306
+ "start_node": 0,
307
+ "end_node": 0,
308
+ "max_length": 100.0,
309
+ "capacity": 10.0
310
+ }
311
+ ],
312
+ "costs": {
313
+ "TypeA": [[0.0, 1.5], [1.5, 0.0]]
314
+ },
315
+ "constraints": [
316
+ {
317
+ "kind": "assignment",
318
+ "items": [
319
+ {"node": 1, "types": ["TypeA"]}
320
+ ]
321
+ },
322
+ {
323
+ "kind": "timewindow",
324
+ "items": [
325
+ {"node": 1, "start": 0.0, "end": 50.0}
326
+ ]
327
+ }
328
+ ],
329
+ "options": {
330
+ "return_to_end": true,
331
+ "objective": "min_max",
332
+ "time_limit": 60
333
+ }
334
+ }
335
+ ```
336
+
337
+ ### Fields
338
+
339
+ | Field | Description |
340
+ |---|---|
341
+ | `nodes` | List of nodes. Each has `id`, `x`, `y`, optional `z`, `type` (`"depot"` or `"target"`). |
342
+ | `agents` | List of agents. Each has `id`, `type`, `start_node`, `end_node`, optional `max_length`, `capacity`. |
343
+ | `costs` | Dict mapping agent type name → cost matrix (2D array, row = from node id, col = to node id). |
344
+ | `constraints` | Optional. List of constraint blocks. `kind` = `"assignment"` or `"timewindow"`. |
345
+ | `options` | Optional. `return_to_end` (bool), `objective` (`"min_max"` or `"min_sum"`), `time_limit` (seconds). |
346
+
347
+ ---
348
+
349
+ ## Result Format
350
+
351
+ ```python
352
+ {
353
+ "status": "success", # "success" or "failed"
354
+ "timeout": False, # True if solver hit time limit
355
+ "routes": [
356
+ {
357
+ "agent_id": 0,
358
+ "path": [0, 2, 0], # ordered node IDs
359
+ "cost": 6.32 # route cost
360
+ },
361
+ ...
362
+ ],
363
+ "statistics": {
364
+ "solve_time": 0.123, # wall-clock time (s)
365
+ "max_cost": 6.32, # maximum route cost
366
+ "sum_cost": 10.56, # total cost of all routes
367
+ "n_generated": 1500, # labels generated
368
+ "n_expanded": 800, # labels expanded
369
+ "last_update_time": 0.08 # time of last solution improvement
370
+ },
371
+ "anytime": [
372
+ {"time": 0.01, "max_cost": 9.5, "sum_cost": 15.2},
373
+ {"time": 0.05, "max_cost": 7.1, "sum_cost": 12.0},
374
+ ...
375
+ ]
376
+ }
377
+ ```
378
+
379
+ ---
380
+
381
+ ## API Reference
382
+
383
+ ### `rch.solve(source, *, time_limit=-1) → dict`
384
+
385
+ Solve an MTSP instance.
386
+
387
+ - `source` — file path (`str` / `Path`), raw JSON string, or Python `dict`.
388
+ - `time_limit` — override the time limit in seconds (default: use the value in JSON).
389
+
390
+ ### `rch.Planner` (recommended)
391
+
392
+ High-level builder API. All mutating methods return `self` for chaining.
393
+
394
+ - `add_depot(node_id, *, x, y, z=0)` — add a depot node.
395
+ - `add_target(node_id, *, x, y, z=0, demand=0)` — add a target node.
396
+ - `add_agent(*, agent_id, agent_type, start_node, end_node, order=None, capacity_limit=-1, time_limit=-1)` — add an agent. `time_limit` is the max travel distance/time (≤0 means no limit).
397
+ - `set_cost_matrix(agent_type, matrix)` — set cost matrix for an agent type.
398
+ - `add_assignment(node_id, types)` — assign a node to a list of allowed agent types.
399
+ - `add_time_window(node_id, start, end)` — add a time window constraint for a node.
400
+ - `set_options(*, return_to_end=None, objective=None, time_limit=None)` — set solver options. `objective` accepts `"min_max"` or `"min_sum"`.
401
+ - `solve() → dict` — run the solver and return a result dict.
402
+ - `show_map(**kwargs) → Axes` — visualize the problem map (depots, targets, agent starts).
403
+ - `show_result(result, **kwargs) → Axes` — visualize solved routes on the map.
404
+
405
+ ### `rch.show_map(planner, *, ax=None, figsize=(8,6), show=True) → Axes`
406
+
407
+ Plot the problem map: depots (★), targets (●), and agent start positions (▲).
408
+
409
+ ### `rch.show_result(planner, result, *, ax=None, figsize=(8,6), show=True) → Axes`
410
+
411
+ Plot solved routes on the map. Each agent's path is drawn with a distinct colour and directional arrows. When `return_to_end=False`, the trailing depot is automatically stripped from the visualization.
412
+
413
+ ### `rch.Problem`
414
+
415
+ Low-level programmatic problem builder. Methods:
416
+
417
+ - `add_node(node: Node)` — add a node.
418
+ - `add_agent(agent: Agent, id: int)` — add an agent.
419
+ - `set_cost_matrix(agent_type: str, matrix: List[List[float]])` — set cost matrix.
420
+ - `set_assignment_constraint(c: AssignmentConstraint)` — set assignment constraint.
421
+ - `set_timewindow_constraint(c: TimeWindowConstraint)` — set time window constraint.
422
+ - `options() → Options` — access/modify solver options.
423
+
424
+ ### `rch.Solver`
425
+
426
+ Low-level solver. Construct with `Solver(problem, options)`.
427
+
428
+ - `solve() → int` — run the solver (1 = success, 0 = failure).
429
+ - `get_result() → Result` — get the final result.
430
+ - `get_result_process() → List[Tuple[float, Result]]` — get full anytime history.
431
+
432
+ ### `rch.ObjectiveType`
433
+
434
+ Enum: `ObjectiveType.MinMax`, `ObjectiveType.MinSum`.
435
+
436
+ ### `rch.NodeType`
437
+
438
+ Enum: `NodeType.DEPOT`, `NodeType.TARGET`.
439
+
440
+ ---
441
+
442
+ ## License
443
+
444
+ MIT