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.
Files changed (38) hide show
  1. tourforge-0.1.0/CHANGELOG.md +34 -0
  2. tourforge-0.1.0/CONTRIBUTING.md +62 -0
  3. tourforge-0.1.0/LICENSE +21 -0
  4. tourforge-0.1.0/MANIFEST.in +21 -0
  5. tourforge-0.1.0/PKG-INFO +340 -0
  6. tourforge-0.1.0/README.md +306 -0
  7. tourforge-0.1.0/benchmark-results/metadata.json +54 -0
  8. tourforge-0.1.0/benchmark-results/raw-measurements.csv +31 -0
  9. tourforge-0.1.0/benchmark-results/summary.json +128 -0
  10. tourforge-0.1.0/benchmark-results/summary.md +26 -0
  11. tourforge-0.1.0/benchmark.py +471 -0
  12. tourforge-0.1.0/cpp/tsp.cpp +70 -0
  13. tourforge-0.1.0/cpp/tsp.h +18 -0
  14. tourforge-0.1.0/docs/benchmark.md +194 -0
  15. tourforge-0.1.0/docs/images/benchmark-execution-time.png +0 -0
  16. tourforge-0.1.0/docs/images/benchmark-speedup.png +0 -0
  17. tourforge-0.1.0/docs/images/benchmark-variability.png +0 -0
  18. tourforge-0.1.0/docs/linkedin.md +182 -0
  19. tourforge-0.1.0/docs/release.md +155 -0
  20. tourforge-0.1.0/docs/requirements-matrix.md +45 -0
  21. tourforge-0.1.0/pyproject.toml +86 -0
  22. tourforge-0.1.0/scripts/plot_benchmarks.py +241 -0
  23. tourforge-0.1.0/scripts/validate_benchmark_data.py +92 -0
  24. tourforge-0.1.0/setup.cfg +4 -0
  25. tourforge-0.1.0/setup.py +120 -0
  26. tourforge-0.1.0/src/tourforge/__init__.py +52 -0
  27. tourforge-0.1.0/src/tourforge/_native.pyi +3 -0
  28. tourforge-0.1.0/src/tourforge/_native.pyx +51 -0
  29. tourforge-0.1.0/src/tourforge/_python.py +37 -0
  30. tourforge-0.1.0/src/tourforge/_tsp.pxd +6 -0
  31. tourforge-0.1.0/src/tourforge/_validation.py +134 -0
  32. tourforge-0.1.0/src/tourforge/py.typed +0 -0
  33. tourforge-0.1.0/tests/__init__.py +0 -0
  34. tourforge-0.1.0/tests/conftest.py +32 -0
  35. tourforge-0.1.0/tests/test_benchmark_fixture.py +78 -0
  36. tourforge-0.1.0/tests/test_parity.py +56 -0
  37. tourforge-0.1.0/tests/test_solvers.py +112 -0
  38. 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.
@@ -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
@@ -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&lt;int&gt;"]
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
+ ![Median end-to-end execution time for both solvers](docs/images/benchmark-execution-time.png)
279
+
280
+ ![Median speedup of the native solver](docs/images/benchmark-speedup.png)
281
+
282
+ ![Run-to-run variability with individual samples and median markers](docs/images/benchmark-variability.png)
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).