cndetector 0.2.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 (43) hide show
  1. cndetector-0.2.0/LICENSE +21 -0
  2. cndetector-0.2.0/PKG-INFO +241 -0
  3. cndetector-0.2.0/README.md +204 -0
  4. cndetector-0.2.0/buildtools/__init__.py +0 -0
  5. cndetector-0.2.0/buildtools/build_extensions.py +79 -0
  6. cndetector-0.2.0/buildtools/build_wrapper.py +23 -0
  7. cndetector-0.2.0/buildtools/get_version.py +70 -0
  8. cndetector-0.2.0/cndetector/Model.py +645 -0
  9. cndetector-0.2.0/cndetector/ProgressPrinter.py +103 -0
  10. cndetector-0.2.0/cndetector/Result.py +21 -0
  11. cndetector-0.2.0/cndetector/__init__.py +57 -0
  12. cndetector-0.2.0/cndetector/_cndetector.pyi +160 -0
  13. cndetector-0.2.0/cndetector/constants.py +17 -0
  14. cndetector-0.2.0/cndetector/cpp/Graph/CNP_Graph.cpp +948 -0
  15. cndetector-0.2.0/cndetector/cpp/Graph/CNP_Graph.h +270 -0
  16. cndetector-0.2.0/cndetector/cpp/Graph/DCNP_Graph.cpp +490 -0
  17. cndetector-0.2.0/cndetector/cpp/Graph/DCNP_Graph.h +278 -0
  18. cndetector-0.2.0/cndetector/cpp/Graph/Types.h +26 -0
  19. cndetector-0.2.0/cndetector/cpp/ParallelFor.h +152 -0
  20. cndetector-0.2.0/cndetector/cpp/Population.cpp +589 -0
  21. cndetector-0.2.0/cndetector/cpp/Population.h +158 -0
  22. cndetector-0.2.0/cndetector/cpp/ProblemData.cpp +185 -0
  23. cndetector-0.2.0/cndetector/cpp/ProblemData.h +38 -0
  24. cndetector-0.2.0/cndetector/cpp/RandomNumberGenerator.h +129 -0
  25. cndetector-0.2.0/cndetector/cpp/crossover/reduceSolveCombine.cpp +144 -0
  26. cndetector-0.2.0/cndetector/cpp/crossover/reduceSolveCombine.h +56 -0
  27. cndetector-0.2.0/cndetector/cpp/pybind.cpp +268 -0
  28. cndetector-0.2.0/cndetector/cpp/search/DCNPSearch.cpp +95 -0
  29. cndetector-0.2.0/cndetector/cpp/search/DCNPSearch.h +11 -0
  30. cndetector-0.2.0/cndetector/cpp/search/L2NSSearch.cpp +98 -0
  31. cndetector-0.2.0/cndetector/cpp/search/L2NSSearch.h +44 -0
  32. cndetector-0.2.0/cndetector/cpp/search/LocalSearch.h +24 -0
  33. cndetector-0.2.0/cndetector/params.py +88 -0
  34. cndetector-0.2.0/cndetector/py.typed +0 -0
  35. cndetector-0.2.0/cndetector/read.py +40 -0
  36. cndetector-0.2.0/cndetector/stop/MaxIterations.py +20 -0
  37. cndetector-0.2.0/cndetector/stop/MaxRuntime.py +19 -0
  38. cndetector-0.2.0/cndetector/stop/NoImprovement.py +23 -0
  39. cndetector-0.2.0/cndetector/stop/StoppingCriterion.py +11 -0
  40. cndetector-0.2.0/cndetector/stop/__init__.py +13 -0
  41. cndetector-0.2.0/meson.build +96 -0
  42. cndetector-0.2.0/pyproject.toml +130 -0
  43. cndetector-0.2.0/tests/test_basic.py +351 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Xuebo
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,241 @@
1
+ Metadata-Version: 2.4
2
+ Name: cndetector
3
+ Version: 0.2.0
4
+ Summary: A high-performance Python solver for Critical Node Problems using Population-based Dual Memetic Search
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Keywords: graph,optimization,critical-node-problem,memetic-search,metaheuristic
8
+ Author: xuebo100
9
+ Author-email: x3219658574@126.com
10
+ Maintainer: xuebo100
11
+ Maintainer-email: x3219658574@126.com
12
+ Requires-Python: >=3.9
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Development Status :: 3 - Alpha
16
+ Classifier: Topic :: Software Development
17
+ Classifier: Topic :: Scientific/Engineering
18
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.9
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Programming Language :: Python :: 3.13
25
+ Classifier: Programming Language :: C++
26
+ Provides-Extra: dev
27
+ Requires-Dist: mypy (>=1.0.0) ; extra == "dev"
28
+ Requires-Dist: pre-commit (>=3.0.0) ; extra == "dev"
29
+ Requires-Dist: pytest (>=7.0.0) ; extra == "dev"
30
+ Requires-Dist: pytest-cov (>=4.0.0) ; extra == "dev"
31
+ Requires-Dist: ruff (>=0.1.0) ; extra == "dev"
32
+ Project-URL: Homepage, https://github.com/xuebo100/PCMS
33
+ Project-URL: Issues, https://github.com/xuebo100/PCMS/issues
34
+ Project-URL: Repository, https://github.com/xuebo100/PCMS
35
+ Description-Content-Type: text/markdown
36
+
37
+ <div align="center">
38
+
39
+ # CNDetector
40
+
41
+ **A high-performance solver for Critical Node Problems, with a C++ core and a clean Python API.**
42
+
43
+ [![PyPI](https://img.shields.io/pypi/v/cndetector.svg)](https://pypi.org/project/cndetector/)
44
+ [![Python](https://img.shields.io/pypi/pyversions/cndetector.svg)](https://pypi.org/project/cndetector/)
45
+ [![CI](https://github.com/xuebo100/CNDetector/actions/workflows/ci.yml/badge.svg)](https://github.com/xuebo100/CNDetector/actions/workflows/ci.yml)
46
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
47
+
48
+ </div>
49
+
50
+ CNDetector finds the set of vertices whose removal most fragments a graph, using
51
+ **IRMS** (*Iterative Ruin and Memetic Search*) — a population-based memetic
52
+ metaheuristic implemented in C++ and exposed through pybind11.
53
+
54
+ Given a graph and a budget `k`, the **Critical Node Problem (CNP)** asks which
55
+ `k` vertices to remove to minimize the residual pairwise connectivity — the sum
56
+ of `|C|·(|C|−1)/2` over every remaining connected component `C`. The
57
+ **distance-based variant (DCNP)** instead minimizes the number of node pairs
58
+ that stay within `D` hops of each other.
59
+
60
+ ## Installation
61
+
62
+ ```bash
63
+ pip install cndetector
64
+ ```
65
+
66
+ <details>
67
+ <summary>Build from source</summary>
68
+
69
+ CNDetector builds from source with Meson + Ninja + pybind11:
70
+
71
+ ```bash
72
+ pip install -e . --no-build-isolation
73
+ ```
74
+
75
+ Requirements: Python ≥ 3.9 and a C++20-capable compiler (clang ≥ 17, gcc ≥ 11,
76
+ or MSVC 2022). `meson`, `ninja` and `pybind11` are pulled in automatically.
77
+
78
+ </details>
79
+
80
+ ## Quick start — CNP
81
+
82
+ ```python
83
+ from cndetector import Model, MaxIterations
84
+
85
+ model = Model()
86
+ for u, v in [
87
+ (0, 1), (1, 2), (2, 3), (3, 4), (4, 5),
88
+ (5, 6), (6, 7), (7, 8), (8, 9), (9, 0),
89
+ (0, 5), (1, 6), (2, 7), (3, 8), (4, 9),
90
+ ]:
91
+ model.add_edge(u, v)
92
+
93
+ result = model.solve(
94
+ budget=3, # remove 3 nodes
95
+ stopping_criterion=MaxIterations(50),
96
+ seed=42,
97
+ )
98
+
99
+ print(f"Best objective: {result.best_obj_value}")
100
+ print(f"Removed nodes: {sorted(result.best_solution)}")
101
+ ```
102
+
103
+ Load a graph from a file instead of building it by hand:
104
+
105
+ ```python
106
+ import cndetector
107
+
108
+ model = cndetector.Model.from_data(cndetector.read("path/to/graph.adj"))
109
+ ```
110
+
111
+ `cndetector.read()` handles adjacency-list files and auto-detects DIMACS edge-list
112
+ files (a `p edge n m` line followed by `e u v` lines).
113
+
114
+ ## Distance-based CNP (DCNP)
115
+
116
+ Select DCNP with `problem="DCNP"` and a `distance`:
117
+
118
+ ```python
119
+ import cndetector
120
+ from cndetector import MaxRuntime
121
+
122
+ model = cndetector.Model.from_data(cndetector.read("Instances/DCNP/R1/karate.txt"))
123
+
124
+ result = model.solve(
125
+ problem="DCNP",
126
+ budget=3,
127
+ distance=2, # count pairs within 2 hops
128
+ stopping_criterion=MaxRuntime(10),
129
+ seed=1,
130
+ )
131
+
132
+ print(f"Best D-hop pairs: {result.best_obj_value}")
133
+ print(f"Removed nodes: {sorted(result.best_solution)}")
134
+ ```
135
+
136
+ DCNP runs the same dual-population search as CNP. Its objective rebuilds a
137
+ K-hop tree on every step, so a single local search is far more expensive, and
138
+ CNDetector therefore applies a lighter parameter set automatically — see
139
+ [Tuning DCNP](#tuning-dcnp).
140
+
141
+ ## API reference
142
+
143
+ ### `Model.solve(...)`
144
+
145
+ | Argument | Default | Description |
146
+ |----------|---------|-------------|
147
+ | `budget` | — | Number of nodes to remove (`1 ≤ budget < \|V\|`). Required. |
148
+ | `stopping_criterion` | — | Callable stopping the solver when it returns `True`. Required. |
149
+ | `problem` | `"CNP"` | `"CNP"` or `"DCNP"`. |
150
+ | `distance` | `None` | DCNP distance threshold `D` (required for DCNP, `D ≥ 1`). |
151
+ | `seed` | `0` | RNG seed (`0` is valid). |
152
+ | `params` | `SolverParams()` | Tunable solver parameters (see below). |
153
+ | `display` | `True` | Log progress to the package logger. |
154
+ | `collect_stats` | `True` | Record a per-iteration trace in `Result.stats`. |
155
+
156
+ ### Stopping criteria
157
+
158
+ | Criterion | Stops when |
159
+ |-----------|------------|
160
+ | `MaxIterations(n)` | `n` solver iterations have run |
161
+ | `MaxRuntime(s)` | `s` seconds of wall-clock time have elapsed |
162
+ | `NoImprovement(n)` | the best objective hasn't improved for `n` iterations |
163
+
164
+ Any `Callable[[float], bool]` works, so you can supply your own.
165
+
166
+ ### `SolverParams`
167
+
168
+ ```python
169
+ from cndetector import SolverParams
170
+
171
+ params = SolverParams(
172
+ population_size=10, # theta: size of each population
173
+ thread_count=2, # kappa: threads, and offspring per generation
174
+ transfer_interval=20, # beta: generations between population exchanges
175
+ partial_ratio=0.95, # 1 - alpha, the relaxation coefficient
176
+ stagnation_threshold=500, # delta: idle generations before reconstruction
177
+ search="L2NS", # local-search strategy
178
+ )
179
+ ```
180
+
181
+ The defaults are the tuned values used in the paper: `theta = 10`,
182
+ `kappa = 2`, `beta = 20`, `alpha = 0.05`, `xi = 1000`, `delta = 500`. The same
183
+ set applies to both CNP and DCNP.
184
+
185
+ `search` accepts `"L2NS"` (the default), `"L2NS-ADAPT"`, or `"L2NS<N>"` for a
186
+ fixed destroy size (e.g. `"L2NS5"`). The `l2ns_*` fields override the
187
+ local-search budget: `l2ns_random_idle_product` is the allowable idle iteration
188
+ count `xi`, and each L2NS run draws a destroy size `lambda` from `[1, 50]` and
189
+ stops after `min(500, xi / lambda)` idle iterations.
190
+
191
+ ### Parallelism
192
+
193
+ `thread_count` (`kappa`) sets the number of worker threads, and Algorithm 2
194
+ generates one offspring per thread. Parallelism affects wall-clock time only:
195
+ for a fixed iteration budget the solver returns bit-identical solutions for any
196
+ thread count. `set_max_threads` / `get_max_threads` expose the cap directly.
197
+
198
+ ### `Result`
199
+
200
+ `Model.solve` returns a `Result` with `best_solution`, `best_obj_value`,
201
+ `num_iterations`, `runtime`, `best_found_at_time`, an optional per-iteration
202
+ `stats` list, and the final `feasible_population`.
203
+
204
+ ## Tuning DCNP
205
+
206
+ Evaluating the DCNP objective rebuilds the b-hop trees on every step, which
207
+ makes one local search one to two orders of magnitude more expensive than for
208
+ CNP. DCNP therefore uses a lighter parameter set, applied automatically **only
209
+ when you leave these knobs at the library defaults**:
210
+
211
+ | Knob | CNP default | DCNP default |
212
+ |------|-------------|--------------|
213
+ | `population_size` (theta) | 10 | **4** |
214
+ | `transfer_interval` (beta) | 20 | **5** |
215
+ | `l2ns_random_idle_product` (xi) | 1000 | **100** |
216
+ | `l2ns_random_min_idle_steps` | 1 | **20** |
217
+ | `l2ns_random_max_idle_steps` | 500 | **80** |
218
+ | `l2ns_random_batch_max` | 50 | **15** |
219
+ | `l2ns_theta` | 0.3 | **0.3** |
220
+
221
+ Passing an explicit value (or any `l2ns_*` field) overrides it. These values
222
+ were tuned on the 100–500 node instances USAir97, Circuit and Ecoli.
223
+
224
+ ## Development
225
+
226
+ ```bash
227
+ pip install -e ".[dev]" --no-build-isolation # dev dependencies
228
+ pytest tests/ # run the tests
229
+ python buildtools/build_extensions.py --build_type release # recompile C++
230
+ ```
231
+
232
+ > **macOS note.** On Apple Silicon, after recompiling and copying the native
233
+ > extension (`_cndetector.*.so`), `import` may be killed by AMFI with SIGKILL (exit
234
+ > code 137) because the copied Mach-O's signature is invalidated. Re-sign it
235
+ > ad-hoc to fix it:
236
+ > `codesign -f -s - cndetector/_cndetector.cpython-*-darwin.so`.
237
+
238
+ ## License
239
+
240
+ Released under the MIT License — see [`LICENSE`](LICENSE).
241
+
@@ -0,0 +1,204 @@
1
+ <div align="center">
2
+
3
+ # CNDetector
4
+
5
+ **A high-performance solver for Critical Node Problems, with a C++ core and a clean Python API.**
6
+
7
+ [![PyPI](https://img.shields.io/pypi/v/cndetector.svg)](https://pypi.org/project/cndetector/)
8
+ [![Python](https://img.shields.io/pypi/pyversions/cndetector.svg)](https://pypi.org/project/cndetector/)
9
+ [![CI](https://github.com/xuebo100/CNDetector/actions/workflows/ci.yml/badge.svg)](https://github.com/xuebo100/CNDetector/actions/workflows/ci.yml)
10
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
11
+
12
+ </div>
13
+
14
+ CNDetector finds the set of vertices whose removal most fragments a graph, using
15
+ **IRMS** (*Iterative Ruin and Memetic Search*) — a population-based memetic
16
+ metaheuristic implemented in C++ and exposed through pybind11.
17
+
18
+ Given a graph and a budget `k`, the **Critical Node Problem (CNP)** asks which
19
+ `k` vertices to remove to minimize the residual pairwise connectivity — the sum
20
+ of `|C|·(|C|−1)/2` over every remaining connected component `C`. The
21
+ **distance-based variant (DCNP)** instead minimizes the number of node pairs
22
+ that stay within `D` hops of each other.
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ pip install cndetector
28
+ ```
29
+
30
+ <details>
31
+ <summary>Build from source</summary>
32
+
33
+ CNDetector builds from source with Meson + Ninja + pybind11:
34
+
35
+ ```bash
36
+ pip install -e . --no-build-isolation
37
+ ```
38
+
39
+ Requirements: Python ≥ 3.9 and a C++20-capable compiler (clang ≥ 17, gcc ≥ 11,
40
+ or MSVC 2022). `meson`, `ninja` and `pybind11` are pulled in automatically.
41
+
42
+ </details>
43
+
44
+ ## Quick start — CNP
45
+
46
+ ```python
47
+ from cndetector import Model, MaxIterations
48
+
49
+ model = Model()
50
+ for u, v in [
51
+ (0, 1), (1, 2), (2, 3), (3, 4), (4, 5),
52
+ (5, 6), (6, 7), (7, 8), (8, 9), (9, 0),
53
+ (0, 5), (1, 6), (2, 7), (3, 8), (4, 9),
54
+ ]:
55
+ model.add_edge(u, v)
56
+
57
+ result = model.solve(
58
+ budget=3, # remove 3 nodes
59
+ stopping_criterion=MaxIterations(50),
60
+ seed=42,
61
+ )
62
+
63
+ print(f"Best objective: {result.best_obj_value}")
64
+ print(f"Removed nodes: {sorted(result.best_solution)}")
65
+ ```
66
+
67
+ Load a graph from a file instead of building it by hand:
68
+
69
+ ```python
70
+ import cndetector
71
+
72
+ model = cndetector.Model.from_data(cndetector.read("path/to/graph.adj"))
73
+ ```
74
+
75
+ `cndetector.read()` handles adjacency-list files and auto-detects DIMACS edge-list
76
+ files (a `p edge n m` line followed by `e u v` lines).
77
+
78
+ ## Distance-based CNP (DCNP)
79
+
80
+ Select DCNP with `problem="DCNP"` and a `distance`:
81
+
82
+ ```python
83
+ import cndetector
84
+ from cndetector import MaxRuntime
85
+
86
+ model = cndetector.Model.from_data(cndetector.read("Instances/DCNP/R1/karate.txt"))
87
+
88
+ result = model.solve(
89
+ problem="DCNP",
90
+ budget=3,
91
+ distance=2, # count pairs within 2 hops
92
+ stopping_criterion=MaxRuntime(10),
93
+ seed=1,
94
+ )
95
+
96
+ print(f"Best D-hop pairs: {result.best_obj_value}")
97
+ print(f"Removed nodes: {sorted(result.best_solution)}")
98
+ ```
99
+
100
+ DCNP runs the same dual-population search as CNP. Its objective rebuilds a
101
+ K-hop tree on every step, so a single local search is far more expensive, and
102
+ CNDetector therefore applies a lighter parameter set automatically — see
103
+ [Tuning DCNP](#tuning-dcnp).
104
+
105
+ ## API reference
106
+
107
+ ### `Model.solve(...)`
108
+
109
+ | Argument | Default | Description |
110
+ |----------|---------|-------------|
111
+ | `budget` | — | Number of nodes to remove (`1 ≤ budget < \|V\|`). Required. |
112
+ | `stopping_criterion` | — | Callable stopping the solver when it returns `True`. Required. |
113
+ | `problem` | `"CNP"` | `"CNP"` or `"DCNP"`. |
114
+ | `distance` | `None` | DCNP distance threshold `D` (required for DCNP, `D ≥ 1`). |
115
+ | `seed` | `0` | RNG seed (`0` is valid). |
116
+ | `params` | `SolverParams()` | Tunable solver parameters (see below). |
117
+ | `display` | `True` | Log progress to the package logger. |
118
+ | `collect_stats` | `True` | Record a per-iteration trace in `Result.stats`. |
119
+
120
+ ### Stopping criteria
121
+
122
+ | Criterion | Stops when |
123
+ |-----------|------------|
124
+ | `MaxIterations(n)` | `n` solver iterations have run |
125
+ | `MaxRuntime(s)` | `s` seconds of wall-clock time have elapsed |
126
+ | `NoImprovement(n)` | the best objective hasn't improved for `n` iterations |
127
+
128
+ Any `Callable[[float], bool]` works, so you can supply your own.
129
+
130
+ ### `SolverParams`
131
+
132
+ ```python
133
+ from cndetector import SolverParams
134
+
135
+ params = SolverParams(
136
+ population_size=10, # theta: size of each population
137
+ thread_count=2, # kappa: threads, and offspring per generation
138
+ transfer_interval=20, # beta: generations between population exchanges
139
+ partial_ratio=0.95, # 1 - alpha, the relaxation coefficient
140
+ stagnation_threshold=500, # delta: idle generations before reconstruction
141
+ search="L2NS", # local-search strategy
142
+ )
143
+ ```
144
+
145
+ The defaults are the tuned values used in the paper: `theta = 10`,
146
+ `kappa = 2`, `beta = 20`, `alpha = 0.05`, `xi = 1000`, `delta = 500`. The same
147
+ set applies to both CNP and DCNP.
148
+
149
+ `search` accepts `"L2NS"` (the default), `"L2NS-ADAPT"`, or `"L2NS<N>"` for a
150
+ fixed destroy size (e.g. `"L2NS5"`). The `l2ns_*` fields override the
151
+ local-search budget: `l2ns_random_idle_product` is the allowable idle iteration
152
+ count `xi`, and each L2NS run draws a destroy size `lambda` from `[1, 50]` and
153
+ stops after `min(500, xi / lambda)` idle iterations.
154
+
155
+ ### Parallelism
156
+
157
+ `thread_count` (`kappa`) sets the number of worker threads, and Algorithm 2
158
+ generates one offspring per thread. Parallelism affects wall-clock time only:
159
+ for a fixed iteration budget the solver returns bit-identical solutions for any
160
+ thread count. `set_max_threads` / `get_max_threads` expose the cap directly.
161
+
162
+ ### `Result`
163
+
164
+ `Model.solve` returns a `Result` with `best_solution`, `best_obj_value`,
165
+ `num_iterations`, `runtime`, `best_found_at_time`, an optional per-iteration
166
+ `stats` list, and the final `feasible_population`.
167
+
168
+ ## Tuning DCNP
169
+
170
+ Evaluating the DCNP objective rebuilds the b-hop trees on every step, which
171
+ makes one local search one to two orders of magnitude more expensive than for
172
+ CNP. DCNP therefore uses a lighter parameter set, applied automatically **only
173
+ when you leave these knobs at the library defaults**:
174
+
175
+ | Knob | CNP default | DCNP default |
176
+ |------|-------------|--------------|
177
+ | `population_size` (theta) | 10 | **4** |
178
+ | `transfer_interval` (beta) | 20 | **5** |
179
+ | `l2ns_random_idle_product` (xi) | 1000 | **100** |
180
+ | `l2ns_random_min_idle_steps` | 1 | **20** |
181
+ | `l2ns_random_max_idle_steps` | 500 | **80** |
182
+ | `l2ns_random_batch_max` | 50 | **15** |
183
+ | `l2ns_theta` | 0.3 | **0.3** |
184
+
185
+ Passing an explicit value (or any `l2ns_*` field) overrides it. These values
186
+ were tuned on the 100–500 node instances USAir97, Circuit and Ecoli.
187
+
188
+ ## Development
189
+
190
+ ```bash
191
+ pip install -e ".[dev]" --no-build-isolation # dev dependencies
192
+ pytest tests/ # run the tests
193
+ python buildtools/build_extensions.py --build_type release # recompile C++
194
+ ```
195
+
196
+ > **macOS note.** On Apple Silicon, after recompiling and copying the native
197
+ > extension (`_cndetector.*.so`), `import` may be killed by AMFI with SIGKILL (exit
198
+ > code 137) because the copied Mach-O's signature is invalidated. Re-sign it
199
+ > ad-hoc to fix it:
200
+ > `codesign -f -s - cndetector/_cndetector.cpython-*-darwin.so`.
201
+
202
+ ## License
203
+
204
+ Released under the MIT License — see [`LICENSE`](LICENSE).
File without changes
@@ -0,0 +1,79 @@
1
+ """Builds the native extensions for CNDetector."""
2
+
3
+ import argparse
4
+ import pathlib
5
+ import shutil
6
+ import sys
7
+ from subprocess import check_call
8
+
9
+
10
+ def parse_args():
11
+ parser = argparse.ArgumentParser(prog="build_extensions")
12
+ parser.add_argument("--build_dir", default="build")
13
+ parser.add_argument("--build_type", default="release",
14
+ choices=["debug", "debugoptimized", "release"])
15
+ parser.add_argument("--clean", action="store_true")
16
+ parser.add_argument("--verbose", action="store_true")
17
+ parser.add_argument("--additional", nargs=argparse.REMAINDER, default=[])
18
+ return parser.parse_args()
19
+
20
+
21
+ def clean(build_dir: pathlib.Path, install_dir: pathlib.Path):
22
+ if build_dir.exists():
23
+ shutil.rmtree(build_dir)
24
+ for extension in install_dir.rglob("*.so"):
25
+ extension.unlink()
26
+ for extension in install_dir.rglob("*.pyd"):
27
+ extension.unlink()
28
+
29
+
30
+ def configure(build_dir: pathlib.Path, build_type: str, *additional: list[str]):
31
+ cwd = pathlib.Path.cwd()
32
+ is_windows = sys.platform.startswith("win")
33
+ args = [
34
+ build_dir,
35
+ "--buildtype", build_type,
36
+ f"-Dpython.platlibdir={cwd.absolute()}",
37
+ f"-Dstrip={'true' if build_type == 'release' else 'false'}",
38
+ f"-Db_coverage={'true' if build_type != 'release' else 'false'}",
39
+ *(("-Db_vscrt=mt",) if is_windows else ()),
40
+ *additional,
41
+ ]
42
+ cmd = "configure" if build_dir.exists() else "setup"
43
+ check_call(["meson", cmd, *args])
44
+
45
+
46
+ def compile(build_dir: pathlib.Path, verbose: bool):
47
+ args = ["-C", build_dir] + (["--verbose"] if verbose else [])
48
+ check_call(["meson", "compile", *args])
49
+
50
+
51
+ def install(build_dir: pathlib.Path):
52
+ check_call(["meson", "install", "-C", build_dir])
53
+
54
+
55
+ def build(
56
+ build_dir: pathlib.Path,
57
+ build_type: str,
58
+ verbose: bool,
59
+ *additional: list[str],
60
+ ):
61
+ configure(build_dir, build_type, *additional)
62
+ compile(build_dir, verbose)
63
+ install(build_dir)
64
+
65
+
66
+ def main():
67
+ args = parse_args()
68
+ cwd = pathlib.Path.cwd()
69
+ build_dir = cwd / args.build_dir
70
+
71
+ if args.clean:
72
+ install_dir = cwd / "cndetector"
73
+ clean(build_dir, install_dir)
74
+
75
+ build(build_dir, args.build_type, args.verbose, *args.additional)
76
+
77
+
78
+ if __name__ == "__main__":
79
+ main()
@@ -0,0 +1,23 @@
1
+ """
2
+ Wrapper around the build_extensions script for poetry build backend.
3
+ """
4
+
5
+ import pathlib
6
+ import tempfile
7
+
8
+ from build_extensions import build, clean
9
+
10
+
11
+ def main():
12
+ cwd = pathlib.Path.cwd()
13
+
14
+ with tempfile.TemporaryDirectory() as tmpdir:
15
+ build_dir = pathlib.Path(tmpdir)
16
+ install_dir = cwd / "cndetector"
17
+
18
+ clean(build_dir, install_dir)
19
+ build(build_dir, build_type="release", verbose=False)
20
+
21
+
22
+ if __name__ == "__main__":
23
+ main()
@@ -0,0 +1,70 @@
1
+ """Extract the project version from pyproject.toml.
2
+
3
+ This is the single source of truth for the version: ``[project].version`` in
4
+ pyproject.toml. meson.build calls this script so the native extension and the
5
+ Python metadata never drift apart.
6
+
7
+ meson runs this with whatever ``python3`` it finds first (often the system
8
+ interpreter, e.g. 3.9), so it must stay compatible with older Pythons: the
9
+ ``from __future__`` import keeps the ``X | None`` annotations from being
10
+ evaluated at definition time on 3.9.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import re
16
+ import sys
17
+ from pathlib import Path
18
+
19
+ try:
20
+ import tomllib # Python 3.11+
21
+ except ModuleNotFoundError: # pragma: no cover - older build interpreters
22
+ try:
23
+ import tomli as tomllib # type: ignore[no-redef]
24
+ except ModuleNotFoundError:
25
+ tomllib = None # type: ignore[assignment]
26
+
27
+
28
+ def _find_pyproject() -> Path | None:
29
+ script_dir = Path(__file__).resolve().parent
30
+ candidates = [script_dir.parent / "pyproject.toml", Path.cwd() / "pyproject.toml"]
31
+ for candidate in candidates:
32
+ if candidate.exists():
33
+ return candidate
34
+ return None
35
+
36
+
37
+ def _version_from_toml(text: str) -> str | None:
38
+ if tomllib is not None:
39
+ try:
40
+ data = tomllib.loads(text)
41
+ except Exception:
42
+ data = {}
43
+ version = data.get("project", {}).get("version")
44
+ if isinstance(version, str):
45
+ return version
46
+ # Fallback: match version only inside the [project] table, so unrelated
47
+ # version = "..." lines (e.g. dependency pins) are never picked up.
48
+ project_section = re.search(
49
+ r"^\[project\]\s*$(.*?)(?=^\[|\Z)", text, re.MULTILINE | re.DOTALL
50
+ )
51
+ scope = project_section.group(1) if project_section else text
52
+ match = re.search(r'^\s*version\s*=\s*"([^"]+)"', scope, re.MULTILINE)
53
+ return match.group(1) if match else None
54
+
55
+
56
+ def main() -> None:
57
+ pyproject = _find_pyproject()
58
+ if pyproject is None:
59
+ print("0.0.0")
60
+ return
61
+ version = _version_from_toml(pyproject.read_text(encoding="utf-8"))
62
+ print(version if version else "0.0.0")
63
+
64
+
65
+ if __name__ == "__main__":
66
+ try:
67
+ main()
68
+ except Exception as exc: # pragma: no cover
69
+ print(f"Error: {exc}", file=sys.stderr)
70
+ sys.exit(1)