tourforge 0.1.0__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.
- tourforge-0.1.0/CHANGELOG.md +34 -0
- tourforge-0.1.0/CONTRIBUTING.md +62 -0
- tourforge-0.1.0/LICENSE +21 -0
- tourforge-0.1.0/MANIFEST.in +21 -0
- tourforge-0.1.0/PKG-INFO +340 -0
- tourforge-0.1.0/README.md +306 -0
- tourforge-0.1.0/benchmark-results/metadata.json +54 -0
- tourforge-0.1.0/benchmark-results/raw-measurements.csv +31 -0
- tourforge-0.1.0/benchmark-results/summary.json +128 -0
- tourforge-0.1.0/benchmark-results/summary.md +26 -0
- tourforge-0.1.0/benchmark.py +471 -0
- tourforge-0.1.0/cpp/tsp.cpp +70 -0
- tourforge-0.1.0/cpp/tsp.h +18 -0
- tourforge-0.1.0/docs/benchmark.md +194 -0
- tourforge-0.1.0/docs/images/benchmark-execution-time.png +0 -0
- tourforge-0.1.0/docs/images/benchmark-speedup.png +0 -0
- tourforge-0.1.0/docs/images/benchmark-variability.png +0 -0
- tourforge-0.1.0/docs/linkedin.md +182 -0
- tourforge-0.1.0/docs/release.md +155 -0
- tourforge-0.1.0/docs/requirements-matrix.md +45 -0
- tourforge-0.1.0/pyproject.toml +86 -0
- tourforge-0.1.0/scripts/plot_benchmarks.py +241 -0
- tourforge-0.1.0/scripts/validate_benchmark_data.py +92 -0
- tourforge-0.1.0/setup.cfg +4 -0
- tourforge-0.1.0/setup.py +120 -0
- tourforge-0.1.0/src/tourforge/__init__.py +52 -0
- tourforge-0.1.0/src/tourforge/_native.pyi +3 -0
- tourforge-0.1.0/src/tourforge/_native.pyx +51 -0
- tourforge-0.1.0/src/tourforge/_python.py +37 -0
- tourforge-0.1.0/src/tourforge/_tsp.pxd +6 -0
- tourforge-0.1.0/src/tourforge/_validation.py +134 -0
- tourforge-0.1.0/src/tourforge/py.typed +0 -0
- tourforge-0.1.0/tests/__init__.py +0 -0
- tourforge-0.1.0/tests/conftest.py +32 -0
- tourforge-0.1.0/tests/test_benchmark_fixture.py +78 -0
- tourforge-0.1.0/tests/test_parity.py +56 -0
- tourforge-0.1.0/tests/test_solvers.py +112 -0
- tourforge-0.1.0/tests/test_validation.py +119 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes are documented here. The project follows semantic
|
|
4
|
+
versioning once a public release exists.
|
|
5
|
+
|
|
6
|
+
## 0.1.0 - unreleased
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- Pure-Python exact TSP solver using `itertools.permutations`.
|
|
11
|
+
- C++17 exact TSP solver using `std::next_permutation`.
|
|
12
|
+
- Cython bridge with validation, flattening, GIL release, and 64-bit cost
|
|
13
|
+
accumulation.
|
|
14
|
+
- Public `solve`, `tsp_python`, `tsp_native`, and `validate_distance_matrix`
|
|
15
|
+
API with type hints and `py.typed`.
|
|
16
|
+
- Deterministic benchmark for N=8, N=10, and N=11 with raw CSV, metadata,
|
|
17
|
+
summary, charts, and data-validation script.
|
|
18
|
+
- Pytest suite covering correctness, parity, invalid input, overflow boundaries,
|
|
19
|
+
repeated calls, and benchmark fixtures.
|
|
20
|
+
- Source distribution and Windows wheel build configuration using setuptools,
|
|
21
|
+
Cython, and an MSVC/MinGW-compatible build extension.
|
|
22
|
+
- CI and release-only trusted-publishing workflow definitions.
|
|
23
|
+
- Release gate that runs the full CI matrix, checks metadata, and smoke-tests the
|
|
24
|
+
built wheel before upload.
|
|
25
|
+
- Immutable commit-SHA pins for all third-party GitHub Actions.
|
|
26
|
+
- Portable benchmark metadata paths and explicit pre-commit provenance.
|
|
27
|
+
- Package metadata limited to the locally verified Python and OS support.
|
|
28
|
+
|
|
29
|
+
### Known limitations
|
|
30
|
+
|
|
31
|
+
- Exact search remains factorial and is not suitable for large instances.
|
|
32
|
+
- No public PyPI or GitHub release has been completed from this workspace.
|
|
33
|
+
- Cross-platform builds are configured but only Windows/CPython 3.11 has been
|
|
34
|
+
built and measured locally.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
## Development setup
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
python -m venv .venv
|
|
7
|
+
.venv\Scripts\activate
|
|
8
|
+
python -m pip install -e ".[dev]"
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
On Linux or macOS, activate with `source .venv/bin/activate`. A C++17 compiler is
|
|
12
|
+
required. On Windows, Visual C++ Build Tools is preferred; the build script can
|
|
13
|
+
fall back to MinGW-w64 when `g++` is on `PATH`.
|
|
14
|
+
|
|
15
|
+
## Checks
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
python -m pytest -q
|
|
19
|
+
python -m ruff check .
|
|
20
|
+
python -m mypy
|
|
21
|
+
python -m build
|
|
22
|
+
python -m twine check dist/*
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Do not treat a successful compile as correctness evidence. Any change to either
|
|
26
|
+
solver should keep the parity and independent-reference tests passing.
|
|
27
|
+
|
|
28
|
+
## Benchmark changes
|
|
29
|
+
|
|
30
|
+
If you change the algorithm, the C++ implementation, compiler flags, or the
|
|
31
|
+
public conversion path, rerun the deterministic benchmark and update the
|
|
32
|
+
committed results only from the new raw data:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
python benchmark.py
|
|
36
|
+
python scripts/plot_benchmarks.py
|
|
37
|
+
python scripts/validate_benchmark_data.py
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Do not hardcode expected timings. Do not remove inconvenient samples without
|
|
41
|
+
documenting why. Report the machine, compiler, Python version, seed, aggregation
|
|
42
|
+
method, and measurement scope.
|
|
43
|
+
|
|
44
|
+
## Pull requests
|
|
45
|
+
|
|
46
|
+
- Keep the Python and native algorithms equivalent.
|
|
47
|
+
- Add a regression test for every defect fixed.
|
|
48
|
+
- Update the README or docs when the public API or input contract changes.
|
|
49
|
+
- Do not include build artifacts, virtual environments, credentials, or local
|
|
50
|
+
benchmark scratch files.
|
|
51
|
+
- Describe benchmark claims as observations from a specific environment, not as
|
|
52
|
+
universal properties of Cython or C++.
|
|
53
|
+
|
|
54
|
+
## Reporting bugs
|
|
55
|
+
|
|
56
|
+
Include:
|
|
57
|
+
|
|
58
|
+
- Python version and platform.
|
|
59
|
+
- Compiler and Cython version.
|
|
60
|
+
- The exact matrix (or a minimal reproduction).
|
|
61
|
+
- Expected and actual results.
|
|
62
|
+
- Whether the failure occurs in `tsp_python`, `tsp_native`, or both.
|
tourforge-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Garvit Choudhary
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
include LICENSE
|
|
2
|
+
include README.md
|
|
3
|
+
include CHANGELOG.md
|
|
4
|
+
include CONTRIBUTING.md
|
|
5
|
+
include pyproject.toml
|
|
6
|
+
include setup.py
|
|
7
|
+
include MANIFEST.in
|
|
8
|
+
include benchmark.py
|
|
9
|
+
|
|
10
|
+
recursive-include src/tourforge *.py *.pyi *.pyx *.pxd py.typed
|
|
11
|
+
recursive-include cpp *.h *.cpp
|
|
12
|
+
recursive-include tests *.py
|
|
13
|
+
recursive-include scripts *.py
|
|
14
|
+
recursive-include docs *.md *.png
|
|
15
|
+
recursive-include benchmark-results *.csv *.json *.md
|
|
16
|
+
|
|
17
|
+
exclude src/tourforge/_native.cpp
|
|
18
|
+
prune src/tourforge.egg-info
|
|
19
|
+
prune build
|
|
20
|
+
prune dist
|
|
21
|
+
global-exclude *.py[cod] __pycache__ *.pyd *.so *.dll *.lib *.exp *.o *.obj
|
tourforge-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tourforge
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Exact brute-force traveling salesperson solvers in pure Python and Cython/C++
|
|
5
|
+
Author: Garvit Choudhary
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: traveling-salesperson,tsp,cython,cpp,benchmark,combinatorial-optimization
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Intended Audience :: Education
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
13
|
+
Classifier: Programming Language :: C++
|
|
14
|
+
Classifier: Programming Language :: Cython
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Topic :: Education
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Provides-Extra: benchmark
|
|
24
|
+
Requires-Dist: matplotlib>=3.7; extra == "benchmark"
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
27
|
+
Requires-Dist: Cython>=3.0.10; extra == "dev"
|
|
28
|
+
Requires-Dist: matplotlib>=3.7; extra == "dev"
|
|
29
|
+
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
31
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
32
|
+
Requires-Dist: twine>=5; extra == "dev"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# TourForge
|
|
36
|
+
|
|
37
|
+
TourForge is a small, exact Traveling Salesperson Problem (TSP) library with two
|
|
38
|
+
algorithmically identical solvers: one written in pure Python and one written in
|
|
39
|
+
C++ and exposed through Cython. It exists to make the boundary between readable
|
|
40
|
+
Python and native code measurable, reproducible, and easy to inspect.
|
|
41
|
+
|
|
42
|
+
The project is deliberately narrow. It does not pretend that brute force is a
|
|
43
|
+
production routing algorithm, and it does not hide validation or conversion
|
|
44
|
+
costs inside a benchmark that only measures a C++ loop.
|
|
45
|
+
|
|
46
|
+
> **Release status:** not yet published to PyPI, and no public GitHub repository
|
|
47
|
+
> has been created from this workspace. Repository and package links are omitted
|
|
48
|
+
> until they can be verified. See [docs/release.md](docs/release.md) for the
|
|
49
|
+
> release checklist.
|
|
50
|
+
|
|
51
|
+
## What is included
|
|
52
|
+
|
|
53
|
+
- A pure-Python solver using `itertools.permutations`.
|
|
54
|
+
- A C++ solver using `std::next_permutation`.
|
|
55
|
+
- A Cython bridge that validates and flattens Python input into
|
|
56
|
+
`std::vector<int>`, releases the GIL for the search, and returns a Python
|
|
57
|
+
integer.
|
|
58
|
+
- A stable public API: `solve`, `tsp_python`, `tsp_native`, and
|
|
59
|
+
`validate_distance_matrix`.
|
|
60
|
+
- Deterministic benchmarks for N=8, N=10, and N=11 with raw CSV data, metadata,
|
|
61
|
+
charts, and tests that verify both solvers agree before timing.
|
|
62
|
+
- A source distribution and a locally built Windows wheel, both checked with
|
|
63
|
+
Twine.
|
|
64
|
+
- CI and release-only trusted-publishing workflow definitions. They are
|
|
65
|
+
statically reviewed but have not been executed from this workspace.
|
|
66
|
+
|
|
67
|
+
## Installation
|
|
68
|
+
|
|
69
|
+
From a source checkout:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
python -m pip install .
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The package has no runtime dependencies. Building from source requires CPython
|
|
76
|
+
3.11 or newer and a C++17 compiler:
|
|
77
|
+
|
|
78
|
+
- Windows: Visual C++ Build Tools is the recommended toolchain. MinGW-w64 with
|
|
79
|
+
`g++` on `PATH` is also supported by the build script.
|
|
80
|
+
- Linux/macOS: GCC, Clang, or the Xcode command-line tools. These builds are not
|
|
81
|
+
locally verified; the release workflow's CI matrix is the gate.
|
|
82
|
+
|
|
83
|
+
For development:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
python -m pip install -e ".[dev]"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
A binary wheel contains the compiled extension. A source distribution does not:
|
|
90
|
+
`pip install` on a machine without a wheel must compile the extension locally.
|
|
91
|
+
|
|
92
|
+
## Quick start
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from tourforge import solve
|
|
96
|
+
|
|
97
|
+
distance_matrix = [
|
|
98
|
+
[0, 10, 15, 20],
|
|
99
|
+
[10, 0, 35, 25],
|
|
100
|
+
[15, 35, 0, 30],
|
|
101
|
+
[20, 25, 30, 0],
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
print(solve(distance_matrix)) # 80, native backend
|
|
105
|
+
print(solve(distance_matrix, backend="python")) # 80, pure Python
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The optimal tour is `0 -> 1 -> 3 -> 2 -> 0` with cost
|
|
109
|
+
`10 + 25 + 30 + 15 = 80`.
|
|
110
|
+
|
|
111
|
+
## Input and output contract
|
|
112
|
+
|
|
113
|
+
- City `0` is the fixed start and end of the tour.
|
|
114
|
+
- Every other city is visited exactly once.
|
|
115
|
+
- The matrix must be non-empty, square, and contain only integers.
|
|
116
|
+
- Supported distances are `0 <= d <= 2_147_483_647` (`INT32_MAX`). The same
|
|
117
|
+
limit applies to both backends so their behaviour is equivalent.
|
|
118
|
+
- Negative distances, booleans, floats, strings, `None`, jagged rows, and empty
|
|
119
|
+
matrices are rejected with `TypeError` or `ValueError`.
|
|
120
|
+
- A one-city matrix returns `0`.
|
|
121
|
+
- The diagonal is ignored by the objective. A zero diagonal is conventional,
|
|
122
|
+
but a non-zero non-negative diagonal is accepted and does not affect the
|
|
123
|
+
result.
|
|
124
|
+
- Rows must be Python sequences. A NumPy array is not a
|
|
125
|
+
`collections.abc.Sequence`; convert it with `.tolist()` first. NumPy integer
|
|
126
|
+
scalars inside Python lists are accepted through `operator.index`.
|
|
127
|
+
- The return value is a Python `int`. Native tour costs accumulate in
|
|
128
|
+
`std::int64_t`; a guard rejects inputs whose worst-case cost cannot fit in
|
|
129
|
+
signed 64 bits, although this is unreachable for any matrix small enough to
|
|
130
|
+
enumerate with distances capped at `INT32_MAX`.
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from tourforge import tsp_native, tsp_python
|
|
134
|
+
|
|
135
|
+
matrix = ((0, 3, 4), (3, 0, 5), (4, 5, 0))
|
|
136
|
+
assert tsp_python(matrix) == tsp_native(matrix) == 12
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Algorithm
|
|
140
|
+
|
|
141
|
+
Fixing the start city removes rotationally equivalent tours. For `N` cities,
|
|
142
|
+
the remaining `N - 1` cities can be arranged in `(N - 1)!` ways. Each candidate
|
|
143
|
+
is a complete cycle, so the objective is:
|
|
144
|
+
|
|
145
|
+
```text
|
|
146
|
+
cost(tour) = d[0][p1] + d[p1][p2] + ... + d[pN-1][0]
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
The Python solver enumerates `itertools.permutations(range(1, N))`. The C++
|
|
150
|
+
solver keeps a sorted `std::vector<int>` of the same non-starting indices and
|
|
151
|
+
walks it with `std::next_permutation`. Both evaluate exactly the same set of
|
|
152
|
+
candidate tours and return the minimum cost.
|
|
153
|
+
|
|
154
|
+
The total running time is `O((N - 1)! * N)`, which is factorial. Native code
|
|
155
|
+
reduces interpreter and object overhead; it does not change the complexity
|
|
156
|
+
class. This is an implementation optimisation, not an algorithmic one.
|
|
157
|
+
|
|
158
|
+
## Architecture
|
|
159
|
+
|
|
160
|
+
```mermaid
|
|
161
|
+
flowchart LR
|
|
162
|
+
A["Python list of lists"] --> B["validate_distance_matrix"]
|
|
163
|
+
B --> C["immutable tuple of tuples"]
|
|
164
|
+
C --> D["tsp_python"]
|
|
165
|
+
C --> E["Cython tsp_native"]
|
|
166
|
+
E --> F["flatten to std::vector<int>"]
|
|
167
|
+
F --> G["tourforge::tsp_solve<br/>nogil, std::next_permutation"]
|
|
168
|
+
G --> H["std::int64_t optimum"]
|
|
169
|
+
H --> I["Python int"]
|
|
170
|
+
D --> I
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The native call path has four distinct costs:
|
|
174
|
+
|
|
175
|
+
1. Python-level validation and normalisation.
|
|
176
|
+
2. Flattening the tuple rows into a contiguous `std::vector<int>`.
|
|
177
|
+
3. The C++ permutation search, which runs without the GIL.
|
|
178
|
+
4. Conversion of the `int64_t` result to a Python `int`.
|
|
179
|
+
|
|
180
|
+
The benchmark measures the complete public call, so all four costs are included.
|
|
181
|
+
No isolated native-loop timing is presented alongside it, because comparing that
|
|
182
|
+
number directly with the end-to-end Python number would overstate the benefit of
|
|
183
|
+
the wrapper.
|
|
184
|
+
|
|
185
|
+
### Files
|
|
186
|
+
|
|
187
|
+
| Path | Purpose |
|
|
188
|
+
| --- | --- |
|
|
189
|
+
| `cpp/tsp.h`, `cpp/tsp.cpp` | Native solver declaration and implementation |
|
|
190
|
+
| `src/tourforge/_python.py` | Pure-Python solver |
|
|
191
|
+
| `src/tourforge/_validation.py` | Shared input validation and cost-bound guard |
|
|
192
|
+
| `src/tourforge/_tsp.pxd` | C++ declaration for Cython |
|
|
193
|
+
| `src/tourforge/_native.pyx` | Python/Cython/C++ bridge |
|
|
194
|
+
| `src/tourforge/__init__.py` | Public API |
|
|
195
|
+
| `benchmark.py` | Deterministic benchmark runner |
|
|
196
|
+
| `scripts/plot_benchmarks.py` | Charts generated from saved results |
|
|
197
|
+
| `scripts/validate_benchmark_data.py` | Cross-checks charts/report data against raw CSV |
|
|
198
|
+
| `tests/` | Validation, correctness, parity, overflow, and benchmark tests |
|
|
199
|
+
|
|
200
|
+
## Build from source
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
python -m pip install --upgrade build
|
|
204
|
+
python -m build
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
This produces an sdist and a platform wheel in `dist/`. The sdist contains the
|
|
208
|
+
hand-written C++, header, Cython, and build files. The generated
|
|
209
|
+
`src/tourforge/_native.cpp` is intentionally excluded from version control and
|
|
210
|
+
from the sdist because it is rebuilt from `_native.pyx` during installation.
|
|
211
|
+
|
|
212
|
+
The Windows build script selects MSVC when it is available. If MSVC is absent
|
|
213
|
+
and `g++` is on `PATH`, it falls back to MinGW-w64 and statically links the
|
|
214
|
+
MinGW runtime so the resulting wheel does not require `libstdc++-6.dll` or
|
|
215
|
+
`libwinpthread-1.dll` at import time. `TOURFORGE_FORCE_MINGW=1` forces the
|
|
216
|
+
fallback.
|
|
217
|
+
|
|
218
|
+
## Tests
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
python -m pytest -q
|
|
222
|
+
python -m ruff check .
|
|
223
|
+
python -m mypy
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
The suite covers one-city and two-city inputs, a manually verified four-city
|
|
227
|
+
optimum, assorted matrices, asymmetric matrices, parity between both solvers,
|
|
228
|
+
random instances checked against an independent brute-force reference, invalid
|
|
229
|
+
dimensions and values, booleans and floats, the 32-bit distance limit, 64-bit
|
|
230
|
+
accumulation, repeated calls, and deterministic benchmark fixtures.
|
|
231
|
+
|
|
232
|
+
## Benchmarks
|
|
233
|
+
|
|
234
|
+
Run from a source checkout:
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
python -m pip install -e ".[benchmark]"
|
|
238
|
+
python benchmark.py
|
|
239
|
+
python scripts/plot_benchmarks.py
|
|
240
|
+
python scripts/validate_benchmark_data.py
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
`benchmark.py` uses a documented seed (`20261029`), builds a symmetric matrix
|
|
244
|
+
with a zero diagonal and distances in `[1, 100]`, and gives the identical matrix
|
|
245
|
+
to both solvers. It verifies that the two optima agree before accepting any
|
|
246
|
+
timing. It performs one untimed warm-up call per solver and size, then selects a
|
|
247
|
+
repeat count targeting roughly 0.25 seconds per independent sample. Each
|
|
248
|
+
reported per-call time is the elapsed time divided by the number of repeats.
|
|
249
|
+
Five independent runs are used; the minimum, median, maximum, standard
|
|
250
|
+
deviation, and relative spread are recorded.
|
|
251
|
+
|
|
252
|
+
Random generation, warm-up, printing, compilation, and plotting are outside the
|
|
253
|
+
timed interval. The timer is `time.perf_counter`, and the measured scope is the
|
|
254
|
+
public API end to end.
|
|
255
|
+
|
|
256
|
+
### Measured results
|
|
257
|
+
|
|
258
|
+
Measured on 2026-10-29 on the machine listed below. These are actual results from
|
|
259
|
+
this workspace, not values copied from the assignment sheet.
|
|
260
|
+
|
|
261
|
+
| N | Candidate tours | Pure Python median | Cython/C++ median | Median speedup |
|
|
262
|
+
|---:|---:|---:|---:|---:|
|
|
263
|
+
| 8 | 5,040 | 1.953 ms | 0.033 ms | 58.90x |
|
|
264
|
+
| 10 | 362,880 | 168.707 ms | 1.889 ms | 89.32x |
|
|
265
|
+
| 11 | 3,628,800 | 1,832.206 ms | 19.905 ms | 92.05x |
|
|
266
|
+
|
|
267
|
+
Environment:
|
|
268
|
+
|
|
269
|
+
- OS: Windows 11 (build 26300), AMD64.
|
|
270
|
+
- CPU: Intel Core 5 210H, 8 cores / 12 logical processors.
|
|
271
|
+
- Python: CPython 3.11.9.
|
|
272
|
+
- Cython: 3.3.0.
|
|
273
|
+
- C++ compiler: MinGW-w64 GCC 16.2.0 (`-O3 -std=c++17`, static MinGW runtime).
|
|
274
|
+
- Timer: `time.perf_counter`; 5 independent runs; seed 20261029.
|
|
275
|
+
- Raw data: `benchmark-results/raw-measurements.csv`.
|
|
276
|
+
- Metadata: `benchmark-results/metadata.json`.
|
|
277
|
+
|
|
278
|
+

|
|
279
|
+
|
|
280
|
+

|
|
281
|
+
|
|
282
|
+

|
|
283
|
+
|
|
284
|
+
The speedup grows with N because the native search does more work per Python
|
|
285
|
+
call. At N=8, validation, conversion, and call overhead are a larger share of the
|
|
286
|
+
native time. At N=11, the factorial search dominates and the native median is
|
|
287
|
+
about 92 times lower than the Python median. The result is specific to this
|
|
288
|
+
machine, this compiler, this input generation method, and this implementation.
|
|
289
|
+
|
|
290
|
+
See [docs/benchmark.md](docs/benchmark.md) for the full protocol and analysis.
|
|
291
|
+
|
|
292
|
+
## Complexity and limitations
|
|
293
|
+
|
|
294
|
+
- Brute force remains `O((N - 1)! * N)`. N=12 means 39,916,800 candidate tours;
|
|
295
|
+
N=15 means over 87 billion. Exact enumeration is not a practical routing
|
|
296
|
+
solution beyond small instances.
|
|
297
|
+
- The library uses an adjacency matrix and an exact search. It does not implement
|
|
298
|
+
Held-Karp dynamic programming, branch-and-bound, 2-opt, Christofides, or any
|
|
299
|
+
heuristic. Those are different algorithms with different trade-offs.
|
|
300
|
+
- Distances are limited to signed 32-bit integers at the boundary. The native
|
|
301
|
+
accumulation uses signed 64-bit integers.
|
|
302
|
+
- The native path includes Python validation and flattening. For trivially small
|
|
303
|
+
inputs, those costs can be a meaningful fraction of total latency.
|
|
304
|
+
- A source distribution requires a compiler. Only the local Windows/CPython 3.11
|
|
305
|
+
artifact has been built and tested in this workspace; the CI definition is
|
|
306
|
+
prepared but has not been executed here.
|
|
307
|
+
|
|
308
|
+
## When native code is useful
|
|
309
|
+
|
|
310
|
+
Native code is a good fit when a CPU-bound loop has a stable interface, a large
|
|
311
|
+
amount of work per call, and a small amount of data crossing the boundary. This
|
|
312
|
+
TSP exercise is an intentionally extreme example: the search is factorial, so the
|
|
313
|
+
conversion cost is quickly amortised.
|
|
314
|
+
|
|
315
|
+
It is often the wrong first move. Normal Python may be fast enough; NumPy may
|
|
316
|
+
express array-heavy work better; a better algorithm can dominate constant-factor
|
|
317
|
+
tuning; and I/O, database access, or network latency may be the real bottleneck.
|
|
318
|
+
Cython does not make arbitrary Python programs faster, and a native extension
|
|
319
|
+
adds build, packaging, and platform support costs.
|
|
320
|
+
|
|
321
|
+
## Platform and Python support
|
|
322
|
+
|
|
323
|
+
Evidence in this workspace:
|
|
324
|
+
|
|
325
|
+
- Built and tested: CPython 3.11.9, Windows 11 build 26300, AMD64, MinGW-w64 GCC
|
|
326
|
+
16.2.0, Cython 3.3.0.
|
|
327
|
+
- Locally built wheel: `cp311-cp311-win_amd64`.
|
|
328
|
+
- Package metadata declares Python `>=3.11` and only the Windows OS classifier.
|
|
329
|
+
The CI and release workflows target CPython 3.11-3.13 on Ubuntu, Windows, and
|
|
330
|
+
macOS, but those jobs have not been run in this workspace. Broaden the
|
|
331
|
+
classifiers only after the matrix has passed.
|
|
332
|
+
|
|
333
|
+
## Contributing
|
|
334
|
+
|
|
335
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md). Bug reports should include the Python
|
|
336
|
+
version, compiler, platform, matrix, and the expected versus actual result.
|
|
337
|
+
|
|
338
|
+
## License
|
|
339
|
+
|
|
340
|
+
MIT. See [LICENSE](LICENSE).
|