cpp-pd-code-simplify-interface 0.1.11__tar.gz → 0.1.12__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 (12) hide show
  1. {cpp_pd_code_simplify_interface-0.1.11 → cpp_pd_code_simplify_interface-0.1.12}/PKG-INFO +9 -3
  2. {cpp_pd_code_simplify_interface-0.1.11 → cpp_pd_code_simplify_interface-0.1.12}/README.md +8 -2
  3. cpp_pd_code_simplify_interface-0.1.12/cpp_pd_code_simplify_interface/_worker.py +47 -0
  4. {cpp_pd_code_simplify_interface-0.1.11 → cpp_pd_code_simplify_interface-0.1.12}/cpp_pd_code_simplify_interface/data/include/pdcode_simplify/pdcode_simplify.hpp +6 -0
  5. {cpp_pd_code_simplify_interface-0.1.11 → cpp_pd_code_simplify_interface-0.1.12}/cpp_pd_code_simplify_interface/data/src/native_interface.cpp +4 -1
  6. {cpp_pd_code_simplify_interface-0.1.11 → cpp_pd_code_simplify_interface-0.1.12}/cpp_pd_code_simplify_interface/data/src/pdcode_simplify.cpp +209 -110
  7. {cpp_pd_code_simplify_interface-0.1.11 → cpp_pd_code_simplify_interface-0.1.12}/cpp_pd_code_simplify_interface/main.py +130 -11
  8. {cpp_pd_code_simplify_interface-0.1.11 → cpp_pd_code_simplify_interface-0.1.12}/pyproject.toml +1 -1
  9. {cpp_pd_code_simplify_interface-0.1.11 → cpp_pd_code_simplify_interface-0.1.12}/build_backend/cpp_pd_code_simplify_interface_build_backend.py +0 -0
  10. {cpp_pd_code_simplify_interface-0.1.11 → cpp_pd_code_simplify_interface-0.1.12}/cpp_pd_code_simplify_interface/__init__.py +0 -0
  11. {cpp_pd_code_simplify_interface-0.1.11 → cpp_pd_code_simplify_interface-0.1.12}/cpp_pd_code_simplify_interface/__main__.py +0 -0
  12. {cpp_pd_code_simplify_interface-0.1.11 → cpp_pd_code_simplify_interface-0.1.12}/cpp_pd_code_simplify_interface/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cpp-pd-code-simplify-interface
3
- Version: 0.1.11
3
+ Version: 0.1.12
4
4
  Summary: Python interface for cpp-pd-code-simplify with runtime C++ compilation.
5
5
  License-Expression: MIT
6
6
  Author: GGN_2015
@@ -69,8 +69,14 @@ The default `max_paths=-1` uses deterministic heuristic green-path sampling in
69
69
  the C++ backend. Use `ban_heuristic=True` to request exhaustive green-path
70
70
  enumeration for a manageable input. Use `reduction_round=K` to cap applied
71
71
  mid-simplification rounds; the default `-1` runs until stable. Use
72
- `verbose=True` to forward timestamped C++ progress logs to stderr. Verbose log
73
- lines use local wall-clock time in `YYYY-MM-DD HH:MM:SS` format.
72
+ `timeout=K` to cap a call at `K` seconds; the default `-1` has no timeout. Use
73
+ `verbose=True` to forward timestamped C++ progress logs to stderr. If a call
74
+ times out, the returned dictionary still contains the best PD code found so far
75
+ and sets `timed_out` to `True`. Verbose log lines use local wall-clock time in
76
+ `YYYY-MM-DD HH:MM:SS` format. When `max_thread=-1` reaches a brute-force search
77
+ phase, verbose logs also include `actual_threads`, the worker count selected by
78
+ the C++ backend for that phase. The backend call runs in a helper process, so
79
+ `Ctrl+C` can terminate active C++ work and its worker threads cleanly.
74
80
 
75
81
  Batch use:
76
82
 
@@ -49,8 +49,14 @@ The default `max_paths=-1` uses deterministic heuristic green-path sampling in
49
49
  the C++ backend. Use `ban_heuristic=True` to request exhaustive green-path
50
50
  enumeration for a manageable input. Use `reduction_round=K` to cap applied
51
51
  mid-simplification rounds; the default `-1` runs until stable. Use
52
- `verbose=True` to forward timestamped C++ progress logs to stderr. Verbose log
53
- lines use local wall-clock time in `YYYY-MM-DD HH:MM:SS` format.
52
+ `timeout=K` to cap a call at `K` seconds; the default `-1` has no timeout. Use
53
+ `verbose=True` to forward timestamped C++ progress logs to stderr. If a call
54
+ times out, the returned dictionary still contains the best PD code found so far
55
+ and sets `timed_out` to `True`. Verbose log lines use local wall-clock time in
56
+ `YYYY-MM-DD HH:MM:SS` format. When `max_thread=-1` reaches a brute-force search
57
+ phase, verbose logs also include `actual_threads`, the worker count selected by
58
+ the C++ backend for that phase. The backend call runs in a helper process, so
59
+ `Ctrl+C` can terminate active C++ work and its worker threads cleanly.
54
60
 
55
61
  Batch use:
56
62
 
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import sys
5
+ from typing import Any
6
+
7
+ from .main import _run_one_direct
8
+
9
+
10
+ def main() -> int:
11
+ try:
12
+ request: dict[str, Any] = json.loads(sys.stdin.read())
13
+ result = _run_one_direct(
14
+ str(request.get("pd_text", "")),
15
+ max_paths=int(request.get("max_paths", -1)),
16
+ ban_heuristic=bool(request.get("ban_heuristic", False)),
17
+ reduction_round=int(request.get("reduction_round", -1)),
18
+ max_thread=int(request.get("max_thread", -1)),
19
+ timeout=int(request.get("timeout", -1)),
20
+ verbose=bool(request.get("verbose", False)),
21
+ known_crossingless_components=int(
22
+ request.get("known_crossingless_components", 0)
23
+ ),
24
+ remove_crossings=request.get("remove_crossings") or [],
25
+ )
26
+ print(json.dumps({"ok": True, "result": result}, separators=(",", ":")))
27
+ return 0
28
+ except KeyboardInterrupt:
29
+ print(
30
+ json.dumps(
31
+ {"ok": False, "error": "interrupted by Ctrl+C"},
32
+ separators=(",", ":"),
33
+ )
34
+ )
35
+ return 130
36
+ except BaseException as exc: # noqa: BLE001 - return errors through JSON.
37
+ print(
38
+ json.dumps(
39
+ {"ok": False, "error": str(exc)},
40
+ separators=(",", ":"),
41
+ )
42
+ )
43
+ return 0
44
+
45
+
46
+ if __name__ == "__main__":
47
+ raise SystemExit(main())
@@ -1,6 +1,7 @@
1
1
  #pragma once
2
2
 
3
3
  #include <array>
4
+ #include <chrono>
4
5
  #include <cstddef>
5
6
  #include <functional>
6
7
  #include <iosfwd>
@@ -51,10 +52,14 @@ struct GreenCrossing {
51
52
  struct SimplifierOptions {
52
53
  int max_paths = -1;
53
54
  int max_threads = -1;
55
+ int timeout_seconds = -1;
56
+ bool has_timeout_deadline = false;
57
+ std::chrono::steady_clock::time_point timeout_deadline{};
54
58
  bool ban_heuristic = false;
55
59
  bool require_applicable = false;
56
60
  bool verbose = false;
57
61
  std::function<void(const std::string&)> progress;
62
+ std::function<bool()> should_cancel;
58
63
  };
59
64
 
60
65
  struct LinkComponentSummary {
@@ -121,6 +126,7 @@ struct ReductionResult {
121
126
  std::size_t tested_green_paths = 0;
122
127
  std::string last_path_search_mode;
123
128
  bool stopped_by_round_limit = false;
129
+ bool timed_out = false;
124
130
  };
125
131
 
126
132
  PDCODE_SIMPLIFY_API PDCode parse_pd_code(const std::string& text);
@@ -106,7 +106,8 @@ std::string result_to_json(
106
106
  out << "\"tested_green_paths\":" << result.tested_green_paths << ",";
107
107
  out << "\"last_path_search_mode\":\"" << json_escape(result.last_path_search_mode) << "\",";
108
108
  out << "\"stopped_by_round_limit\":"
109
- << (result.stopped_by_round_limit ? "true" : "false");
109
+ << (result.stopped_by_round_limit ? "true" : "false") << ",";
110
+ out << "\"timed_out\":" << (result.timed_out ? "true" : "false");
110
111
  out << "}";
111
112
  return out.str();
112
113
  }
@@ -133,6 +134,7 @@ char* pdcode_simplify_run_json(
133
134
  int ban_heuristic,
134
135
  int reduction_round,
135
136
  int max_thread,
137
+ int timeout_seconds,
136
138
  int verbose,
137
139
  unsigned long long known_crossingless_components,
138
140
  const int* removed_crossings,
@@ -146,6 +148,7 @@ char* pdcode_simplify_run_json(
146
148
  pdcode_simplify::SimplifierOptions options;
147
149
  options.max_paths = max_paths;
148
150
  options.max_threads = max_thread;
151
+ options.timeout_seconds = timeout_seconds;
149
152
  options.ban_heuristic = ban_heuristic != 0;
150
153
  options.verbose = verbose != 0;
151
154
  options.progress = [](const std::string& message) {
@@ -1,6 +1,7 @@
1
1
  #include "pdcode_simplify/pdcode_simplify.hpp"
2
2
 
3
3
  #include <algorithm>
4
+ #include <chrono>
4
5
  #include <cctype>
5
6
  #include <cstdlib>
6
7
  #include <deque>
@@ -987,7 +988,10 @@ std::set<int> normalized_removed_crossings(const PDCode& code, const std::vector
987
988
  std::vector<int> heuristic_distances_to_target(
988
989
  const DualGraph& graph,
989
990
  int target,
990
- int cutoff);
991
+ int cutoff,
992
+ const SimplifierOptions& options);
993
+
994
+ void check_timeout(const SimplifierOptions& options);
991
995
 
992
996
  void collect_simple_paths_dfs(
993
997
  const DualGraph& graph,
@@ -999,7 +1003,9 @@ void collect_simple_paths_dfs(
999
1003
  const std::vector<int>& distance,
1000
1004
  std::vector<char>& visited,
1001
1005
  std::vector<int>& current_path,
1002
- std::vector<std::vector<int>>& paths) {
1006
+ std::vector<std::vector<int>>& paths,
1007
+ const SimplifierOptions& options) {
1008
+ check_timeout(options);
1003
1009
  const int infinity = std::numeric_limits<int>::max() / 4;
1004
1010
  if (static_cast<int>(current_path.size()) - 1 >= cutoff) {
1005
1011
  return;
@@ -1045,7 +1051,8 @@ void collect_simple_paths_dfs(
1045
1051
  distance,
1046
1052
  visited,
1047
1053
  current_path,
1048
- paths);
1054
+ paths,
1055
+ options);
1049
1056
  if (max_paths != -1 && static_cast<int>(paths.size()) > max_paths) {
1050
1057
  visited[next] = false;
1051
1058
  current_path.pop_back();
@@ -1063,7 +1070,9 @@ std::vector<std::vector<int>> collect_simple_paths(
1063
1070
  int source,
1064
1071
  int target,
1065
1072
  int cutoff,
1066
- int max_paths) {
1073
+ int max_paths,
1074
+ const SimplifierOptions& options) {
1075
+ check_timeout(options);
1067
1076
  std::vector<std::vector<int>> paths;
1068
1077
  if (source == target || source < 0 || target < 0 ||
1069
1078
  source >= static_cast<int>(graph.faces.size()) ||
@@ -1074,7 +1083,7 @@ std::vector<std::vector<int>> collect_simple_paths(
1074
1083
 
1075
1084
  std::vector<char> visited(graph.faces.size(), false);
1076
1085
  std::vector<int> current_path{source};
1077
- const std::vector<int> distance = heuristic_distances_to_target(graph, target, cutoff);
1086
+ const std::vector<int> distance = heuristic_distances_to_target(graph, target, cutoff, options);
1078
1087
  visited[source] = true;
1079
1088
  collect_simple_paths_dfs(
1080
1089
  graph,
@@ -1086,14 +1095,16 @@ std::vector<std::vector<int>> collect_simple_paths(
1086
1095
  distance,
1087
1096
  visited,
1088
1097
  current_path,
1089
- paths);
1098
+ paths,
1099
+ options);
1090
1100
  return paths;
1091
1101
  }
1092
1102
 
1093
1103
  std::vector<int> heuristic_distances_to_target(
1094
1104
  const DualGraph& graph,
1095
1105
  int target,
1096
- int cutoff) {
1106
+ int cutoff,
1107
+ const SimplifierOptions& options) {
1097
1108
  const int face_count = static_cast<int>(graph.faces.size());
1098
1109
  const int infinity = std::numeric_limits<int>::max() / 4;
1099
1110
  std::vector<int> distance(face_count, infinity);
@@ -1102,6 +1113,7 @@ std::vector<int> heuristic_distances_to_target(
1102
1113
  queue.push_back(target);
1103
1114
 
1104
1115
  while (!queue.empty()) {
1116
+ check_timeout(options);
1105
1117
  const int current = queue.front();
1106
1118
  queue.pop_front();
1107
1119
  for (int edge_index : graph.adjacency[current]) {
@@ -1179,7 +1191,9 @@ std::vector<std::vector<int>> collect_heuristic_paths(
1179
1191
  const DualGraph& graph,
1180
1192
  int source,
1181
1193
  int target,
1182
- int cutoff) {
1194
+ int cutoff,
1195
+ const SimplifierOptions& options) {
1196
+ check_timeout(options);
1183
1197
  std::vector<std::vector<int>> paths;
1184
1198
  const int face_count = static_cast<int>(graph.faces.size());
1185
1199
  if (source == target || source < 0 || target < 0 ||
@@ -1187,7 +1201,7 @@ std::vector<std::vector<int>> collect_heuristic_paths(
1187
1201
  return paths;
1188
1202
  }
1189
1203
 
1190
- const std::vector<int> distance = heuristic_distances_to_target(graph, target, cutoff);
1204
+ const std::vector<int> distance = heuristic_distances_to_target(graph, target, cutoff, options);
1191
1205
  const int infinity = std::numeric_limits<int>::max() / 4;
1192
1206
  if (distance[source] == infinity || distance[source] >= cutoff) {
1193
1207
  return paths;
@@ -1215,6 +1229,7 @@ std::vector<std::vector<int>> collect_heuristic_paths(
1215
1229
  int popped_states = 0;
1216
1230
  while (!queue.empty() && popped_states < state_budget &&
1217
1231
  static_cast<int>(paths.size()) < path_budget) {
1232
+ check_timeout(options);
1218
1233
  HeuristicState state = queue.top();
1219
1234
  queue.pop();
1220
1235
  ++popped_states;
@@ -1823,6 +1838,41 @@ std::string search_mode_for_options(const SimplifierOptions& options) {
1823
1838
  return "bounded";
1824
1839
  }
1825
1840
 
1841
+ void validate_timeout_options(const SimplifierOptions& options) {
1842
+ if (options.timeout_seconds < -1 || options.timeout_seconds == 0) {
1843
+ throw std::invalid_argument("timeout must be -1 or a positive integer");
1844
+ }
1845
+ }
1846
+
1847
+ class TimeoutError : public std::runtime_error {
1848
+ public:
1849
+ explicit TimeoutError(const std::string& message) : std::runtime_error(message) {}
1850
+ };
1851
+
1852
+ SimplifierOptions with_timeout_deadline(SimplifierOptions options) {
1853
+ validate_timeout_options(options);
1854
+ if (options.timeout_seconds > 0 && !options.has_timeout_deadline) {
1855
+ options.has_timeout_deadline = true;
1856
+ options.timeout_deadline =
1857
+ std::chrono::steady_clock::now() + std::chrono::seconds(options.timeout_seconds);
1858
+ }
1859
+ return options;
1860
+ }
1861
+
1862
+ void check_timeout(const SimplifierOptions& options) {
1863
+ if (options.should_cancel && options.should_cancel()) {
1864
+ throw std::runtime_error("interrupted by Ctrl+C");
1865
+ }
1866
+ if (!options.has_timeout_deadline) {
1867
+ return;
1868
+ }
1869
+ if (std::chrono::steady_clock::now() >= options.timeout_deadline) {
1870
+ std::ostringstream message;
1871
+ message << "timeout after " << options.timeout_seconds << " seconds";
1872
+ throw TimeoutError(message.str());
1873
+ }
1874
+ }
1875
+
1826
1876
  } // namespace
1827
1877
 
1828
1878
  PDCode parse_pd_code(const std::string& text) {
@@ -2213,6 +2263,7 @@ RedPathSearchOutcome search_single_red_path(
2213
2263
  const std::string& path_search_mode,
2214
2264
  int red_index,
2215
2265
  const std::atomic<int>* best_found_index) {
2266
+ check_timeout(options);
2216
2267
  RedPathSearchOutcome outcome;
2217
2268
  outcome.witness.path_search_mode = path_search_mode;
2218
2269
  if (should_skip_parallel_red_path(red_index, best_found_index)) {
@@ -2231,6 +2282,7 @@ RedPathSearchOutcome search_single_red_path(
2231
2282
  const std::array<int, 2> destinations{end_face, end_opposite_face};
2232
2283
 
2233
2284
  for (std::size_t i = 1; i + 1 < red_path.size(); ++i) {
2285
+ check_timeout(options);
2234
2286
  const Endpoint endpoint = red_path[i];
2235
2287
  const int right_region = graph.edge_to_face[endpoint_key(endpoint)];
2236
2288
  const int left_region = graph.edge_to_face[endpoint_key(diagram.opposite(endpoint))];
@@ -2243,15 +2295,17 @@ RedPathSearchOutcome search_single_red_path(
2243
2295
  const int cutoff = static_cast<int>(red_path.size()) - 1;
2244
2296
  for (int source : sources) {
2245
2297
  for (int destination : destinations) {
2298
+ check_timeout(options);
2246
2299
  if (should_skip_parallel_red_path(red_index, best_found_index)) {
2247
2300
  outcome.skipped = true;
2248
2301
  return outcome;
2249
2302
  }
2250
2303
  std::vector<std::vector<int>> found_paths;
2251
2304
  if (options.max_paths == -1 && !options.ban_heuristic) {
2252
- found_paths = collect_heuristic_paths(graph, source, destination, cutoff);
2305
+ found_paths = collect_heuristic_paths(graph, source, destination, cutoff, options);
2253
2306
  } else {
2254
- found_paths = collect_simple_paths(graph, source, destination, cutoff, options.max_paths);
2307
+ found_paths = collect_simple_paths(
2308
+ graph, source, destination, cutoff, options.max_paths, options);
2255
2309
  }
2256
2310
  paths.insert(paths.end(), found_paths.begin(), found_paths.end());
2257
2311
  if (options.max_paths != -1 && static_cast<int>(paths.size()) > options.max_paths) {
@@ -2261,6 +2315,7 @@ RedPathSearchOutcome search_single_red_path(
2261
2315
  }
2262
2316
 
2263
2317
  for (const auto& green_path : paths) {
2318
+ check_timeout(options);
2264
2319
  if (should_skip_parallel_red_path(red_index, best_found_index)) {
2265
2320
  outcome.skipped = true;
2266
2321
  return outcome;
@@ -2401,36 +2456,49 @@ SimplificationResult find_simplification_parallel_bruteforce(
2401
2456
  SimplificationResult find_simplification(
2402
2457
  const PDCode& code,
2403
2458
  const SimplifierOptions& options) {
2459
+ const SimplifierOptions run_options = with_timeout_deadline(options);
2460
+ check_timeout(run_options);
2404
2461
  SimplificationResult result;
2405
- result.path_search_mode = search_mode_for_options(options);
2462
+ result.path_search_mode = search_mode_for_options(run_options);
2406
2463
  Diagram diagram(code);
2464
+ check_timeout(run_options);
2407
2465
  DualGraph base_graph(diagram);
2466
+ check_timeout(run_options);
2408
2467
  const auto red_lines = possible_red_lines(diagram);
2409
- const bool brute_force_mode = options.max_paths == -1 && options.ban_heuristic;
2468
+ check_timeout(run_options);
2469
+ const bool brute_force_mode = run_options.max_paths == -1 && run_options.ban_heuristic;
2410
2470
  const int worker_count = brute_force_mode
2411
2471
  ? selected_bruteforce_worker_count(
2412
- options.max_threads,
2472
+ run_options.max_threads,
2413
2473
  static_cast<int>(red_lines.size()))
2414
2474
  : 1;
2475
+ if (brute_force_mode && run_options.max_threads == -1) {
2476
+ std::ostringstream message;
2477
+ message << "bruteforce_threads max_thread=-1"
2478
+ << " actual_threads=" << worker_count
2479
+ << " red_paths=" << red_lines.size();
2480
+ emit_progress(run_options, message.str());
2481
+ }
2415
2482
  if (worker_count > 1) {
2416
2483
  return find_simplification_parallel_bruteforce(
2417
2484
  code,
2418
2485
  diagram,
2419
2486
  base_graph,
2420
2487
  red_lines,
2421
- options,
2488
+ run_options,
2422
2489
  result.path_search_mode,
2423
2490
  worker_count);
2424
2491
  }
2425
2492
 
2426
2493
  for (const auto& red_path : red_lines) {
2494
+ check_timeout(run_options);
2427
2495
  ++result.tested_red_paths;
2428
2496
  const RedPathSearchOutcome outcome = search_single_red_path(
2429
2497
  code,
2430
2498
  diagram,
2431
2499
  base_graph,
2432
2500
  red_path,
2433
- options,
2501
+ run_options,
2434
2502
  result.path_search_mode,
2435
2503
  -1,
2436
2504
  nullptr);
@@ -2452,125 +2520,155 @@ ReductionResult reduce_pd_code(
2452
2520
  std::size_t known_crossingless_components,
2453
2521
  const SimplifierOptions& options,
2454
2522
  int reduction_round) {
2455
- {
2456
- std::ostringstream message;
2457
- message << "start input_crossings=" << code.size()
2458
- << " known_crossingless_components=" << known_crossingless_components
2459
- << " reduction_round=" << reduction_round
2460
- << " max_paths=" << options.max_paths
2461
- << " max_thread=" << options.max_threads
2462
- << " heuristic=" << (options.ban_heuristic ? "off" : "on");
2463
- emit_progress(options, message.str());
2464
- }
2465
-
2466
- const PDSimplificationResult prepared = simplify_pd_code(code, known_crossingless_components);
2523
+ const SimplifierOptions run_options = with_timeout_deadline(options);
2467
2524
  ReductionResult output;
2468
- output.code = prepared.code;
2469
- output.crossingless_components = prepared.crossingless_components;
2470
- output.reidemeister_i_moves = prepared.reidemeister_i_moves;
2471
- output.nugatory_crossing_moves = prepared.nugatory_crossing_moves;
2472
- {
2473
- std::ostringstream message;
2474
- message << "pre_simplify input_crossings=" << code.size()
2475
- << " output_crossings=" << output.code.size()
2476
- << " crossingless_components=" << output.crossingless_components
2477
- << " r1_moves=" << prepared.reidemeister_i_moves
2478
- << " nugatory_moves=" << prepared.nugatory_crossing_moves;
2479
- emit_progress(options, message.str());
2480
- }
2525
+ output.code = code;
2526
+ output.crossingless_components = known_crossingless_components;
2481
2527
 
2482
- while (reduction_round < 0 || output.mid_simplification_rounds < reduction_round) {
2483
- SimplifierOptions search_options = options;
2484
- search_options.require_applicable = true;
2485
- const int round = output.mid_simplification_rounds + 1;
2528
+ try {
2529
+ check_timeout(run_options);
2486
2530
  {
2487
2531
  std::ostringstream message;
2488
- message << "round " << round
2489
- << " search_start crossings=" << output.code.size()
2490
- << " mode=" << search_mode_for_options(search_options)
2491
- << " max_thread=" << search_options.max_threads;
2492
- emit_progress(options, message.str());
2493
- }
2494
- SimplificationResult search = find_simplification(output.code, search_options);
2495
- output.tested_red_paths += search.tested_red_paths;
2496
- output.tested_green_paths += search.tested_green_paths;
2497
- output.last_path_search_mode = search.path_search_mode;
2532
+ message << "start input_crossings=" << code.size()
2533
+ << " known_crossingless_components=" << known_crossingless_components
2534
+ << " reduction_round=" << reduction_round
2535
+ << " max_paths=" << run_options.max_paths
2536
+ << " max_thread=" << run_options.max_threads
2537
+ << " timeout=" << run_options.timeout_seconds
2538
+ << " heuristic=" << (run_options.ban_heuristic ? "off" : "on");
2539
+ emit_progress(run_options, message.str());
2540
+ }
2541
+
2542
+ const PDSimplificationResult prepared =
2543
+ simplify_pd_code(code, known_crossingless_components);
2544
+ output.code = prepared.code;
2545
+ output.crossingless_components = prepared.crossingless_components;
2546
+ output.reidemeister_i_moves = prepared.reidemeister_i_moves;
2547
+ output.nugatory_crossing_moves = prepared.nugatory_crossing_moves;
2548
+ check_timeout(run_options);
2498
2549
  {
2499
2550
  std::ostringstream message;
2500
- message << "round " << round
2501
- << " search_done found=" << (search.found ? "yes" : "no")
2502
- << " mode=" << search.path_search_mode
2503
- << " tested_red=" << search.tested_red_paths
2504
- << " tested_green=" << search.tested_green_paths;
2505
- emit_progress(options, message.str());
2506
- }
2507
-
2508
- if (!search.found && reduction_round < 0 &&
2509
- options.max_paths == -1 && !options.ban_heuristic) {
2510
- SimplifierOptions brute_options = options;
2511
- brute_options.max_paths = -1;
2512
- brute_options.ban_heuristic = true;
2513
- brute_options.require_applicable = true;
2551
+ message << "pre_simplify input_crossings=" << code.size()
2552
+ << " output_crossings=" << output.code.size()
2553
+ << " crossingless_components=" << output.crossingless_components
2554
+ << " r1_moves=" << prepared.reidemeister_i_moves
2555
+ << " nugatory_moves=" << prepared.nugatory_crossing_moves;
2556
+ emit_progress(run_options, message.str());
2557
+ }
2558
+
2559
+ while (reduction_round < 0 || output.mid_simplification_rounds < reduction_round) {
2560
+ check_timeout(run_options);
2561
+ SimplifierOptions search_options = run_options;
2562
+ search_options.require_applicable = true;
2563
+ output.last_path_search_mode = search_mode_for_options(search_options);
2564
+ const int round = output.mid_simplification_rounds + 1;
2514
2565
  {
2515
2566
  std::ostringstream message;
2516
2567
  message << "round " << round
2517
- << " brute_fallback_start crossings=" << output.code.size()
2518
- << " max_thread=" << brute_options.max_threads;
2519
- emit_progress(options, message.str());
2520
- }
2521
- SimplificationResult brute = find_simplification(output.code, brute_options);
2522
- output.tested_red_paths += brute.tested_red_paths;
2523
- output.tested_green_paths += brute.tested_green_paths;
2524
- output.last_path_search_mode = brute.path_search_mode;
2568
+ << " search_start crossings=" << output.code.size()
2569
+ << " mode=" << output.last_path_search_mode
2570
+ << " max_thread=" << search_options.max_threads;
2571
+ emit_progress(run_options, message.str());
2572
+ }
2573
+ SimplificationResult search = find_simplification(output.code, search_options);
2574
+ output.tested_red_paths += search.tested_red_paths;
2575
+ output.tested_green_paths += search.tested_green_paths;
2576
+ output.last_path_search_mode = search.path_search_mode;
2525
2577
  {
2526
2578
  std::ostringstream message;
2527
2579
  message << "round " << round
2528
- << " brute_fallback_done found=" << (brute.found ? "yes" : "no")
2529
- << " tested_red=" << brute.tested_red_paths
2530
- << " tested_green=" << brute.tested_green_paths;
2531
- emit_progress(options, message.str());
2580
+ << " search_done found=" << (search.found ? "yes" : "no")
2581
+ << " mode=" << search.path_search_mode
2582
+ << " tested_red=" << search.tested_red_paths
2583
+ << " tested_green=" << search.tested_green_paths;
2584
+ emit_progress(run_options, message.str());
2585
+ }
2586
+
2587
+ if (!search.found && reduction_round < 0 &&
2588
+ run_options.max_paths == -1 && !run_options.ban_heuristic) {
2589
+ SimplifierOptions brute_options = run_options;
2590
+ brute_options.max_paths = -1;
2591
+ brute_options.ban_heuristic = true;
2592
+ brute_options.require_applicable = true;
2593
+ output.last_path_search_mode = search_mode_for_options(brute_options);
2594
+ {
2595
+ std::ostringstream message;
2596
+ message << "round " << round
2597
+ << " brute_fallback_start crossings=" << output.code.size()
2598
+ << " max_thread=" << brute_options.max_threads;
2599
+ emit_progress(run_options, message.str());
2600
+ }
2601
+ SimplificationResult brute = find_simplification(output.code, brute_options);
2602
+ output.tested_red_paths += brute.tested_red_paths;
2603
+ output.tested_green_paths += brute.tested_green_paths;
2604
+ output.last_path_search_mode = brute.path_search_mode;
2605
+ {
2606
+ std::ostringstream message;
2607
+ message << "round " << round
2608
+ << " brute_fallback_done found=" << (brute.found ? "yes" : "no")
2609
+ << " tested_red=" << brute.tested_red_paths
2610
+ << " tested_green=" << brute.tested_green_paths;
2611
+ emit_progress(run_options, message.str());
2612
+ }
2613
+ if (brute.found) {
2614
+ ++output.heuristic_failover_rounds;
2615
+ search = std::move(brute);
2616
+ }
2532
2617
  }
2533
- if (brute.found) {
2534
- ++output.heuristic_failover_rounds;
2535
- search = std::move(brute);
2618
+
2619
+ if (!search.found) {
2620
+ {
2621
+ std::ostringstream message;
2622
+ message << "round " << round
2623
+ << " stop_no_path crossings=" << output.code.size();
2624
+ emit_progress(run_options, message.str());
2625
+ }
2626
+ break;
2536
2627
  }
2537
- }
2538
2628
 
2539
- if (!search.found) {
2629
+ const std::size_t before_apply_crossings = output.code.size();
2630
+ check_timeout(run_options);
2631
+ const MidSimplificationApplyResult applied =
2632
+ apply_simplification_witness(output.code, search, output.crossingless_components);
2633
+ ++output.mid_simplification_rounds;
2634
+ output.code = applied.code;
2635
+ output.crossingless_components = applied.crossingless_components;
2636
+ check_timeout(run_options);
2637
+ const PDSimplificationResult simplified =
2638
+ simplify_pd_code(output.code, output.crossingless_components);
2639
+ output.code = simplified.code;
2640
+ output.crossingless_components = simplified.crossingless_components;
2641
+ output.reidemeister_i_moves += simplified.reidemeister_i_moves;
2642
+ output.nugatory_crossing_moves += simplified.nugatory_crossing_moves;
2643
+ check_timeout(run_options);
2540
2644
  {
2541
2645
  std::ostringstream message;
2542
2646
  message << "round " << round
2543
- << " stop_no_path crossings=" << output.code.size();
2544
- emit_progress(options, message.str());
2647
+ << " applied crossings=" << before_apply_crossings
2648
+ << " -> " << applied.code.size()
2649
+ << " -> " << output.code.size()
2650
+ << " crossingless_components=" << output.crossingless_components
2651
+ << " r1_moves=" << simplified.reidemeister_i_moves
2652
+ << " nugatory_moves=" << simplified.nugatory_crossing_moves;
2653
+ emit_progress(run_options, message.str());
2545
2654
  }
2546
- break;
2547
2655
  }
2548
-
2549
- const std::size_t before_apply_crossings = output.code.size();
2550
- const MidSimplificationApplyResult applied =
2551
- apply_simplification_witness(output.code, search, output.crossingless_components);
2552
- ++output.mid_simplification_rounds;
2553
- const PDSimplificationResult simplified =
2554
- simplify_pd_code(applied.code, applied.crossingless_components);
2555
- output.code = simplified.code;
2556
- output.crossingless_components = simplified.crossingless_components;
2557
- output.reidemeister_i_moves += simplified.reidemeister_i_moves;
2558
- output.nugatory_crossing_moves += simplified.nugatory_crossing_moves;
2656
+ } catch (const TimeoutError& error) {
2657
+ output.timed_out = true;
2559
2658
  {
2560
2659
  std::ostringstream message;
2561
- message << "round " << round
2562
- << " applied crossings=" << before_apply_crossings
2563
- << " -> " << applied.code.size()
2564
- << " -> " << output.code.size()
2660
+ message << error.what()
2661
+ << "; returning_current_best crossings=" << output.code.size()
2565
2662
  << " crossingless_components=" << output.crossingless_components
2566
- << " r1_moves=" << simplified.reidemeister_i_moves
2567
- << " nugatory_moves=" << simplified.nugatory_crossing_moves;
2568
- emit_progress(options, message.str());
2663
+ << " mid_rounds=" << output.mid_simplification_rounds;
2664
+ emit_progress(run_options, message.str());
2569
2665
  }
2570
2666
  }
2571
2667
 
2572
2668
  output.stopped_by_round_limit =
2573
- reduction_round >= 0 && output.mid_simplification_rounds >= reduction_round;
2669
+ !output.timed_out &&
2670
+ reduction_round >= 0 &&
2671
+ output.mid_simplification_rounds >= reduction_round;
2574
2672
  {
2575
2673
  std::ostringstream message;
2576
2674
  message << "done final_crossings=" << output.code.size()
@@ -2578,8 +2676,9 @@ ReductionResult reduce_pd_code(
2578
2676
  << " mid_rounds=" << output.mid_simplification_rounds
2579
2677
  << " heuristic_failover_rounds=" << output.heuristic_failover_rounds
2580
2678
  << " stopped_by_round_limit="
2581
- << (output.stopped_by_round_limit ? "yes" : "no");
2582
- emit_progress(options, message.str());
2679
+ << (output.stopped_by_round_limit ? "yes" : "no")
2680
+ << " timed_out=" << (output.timed_out ? "yes" : "no");
2681
+ emit_progress(run_options, message.str());
2583
2682
  }
2584
2683
  return output;
2585
2684
  }
@@ -762,6 +762,7 @@ def _load_library() -> ctypes.CDLL:
762
762
  ctypes.c_int,
763
763
  ctypes.c_int,
764
764
  ctypes.c_int,
765
+ ctypes.c_int,
765
766
  ctypes.c_ulonglong,
766
767
  ctypes.POINTER(ctypes.c_int),
767
768
  ctypes.c_ulonglong,
@@ -774,13 +775,14 @@ def _load_library() -> ctypes.CDLL:
774
775
  return library
775
776
 
776
777
 
777
- def _run_one(
778
+ def _run_one_direct(
778
779
  pd_text: str,
779
780
  *,
780
781
  max_paths: int = -1,
781
782
  ban_heuristic: bool = False,
782
783
  reduction_round: int = -1,
783
784
  max_thread: int = -1,
785
+ timeout: int = -1,
784
786
  verbose: bool = False,
785
787
  known_crossingless_components: int = 0,
786
788
  remove_crossings: Optional[Sequence[int]] = None,
@@ -789,6 +791,8 @@ def _run_one(
789
791
  raise ValueError("reduction_round must be -1 or a non-negative integer")
790
792
  if max_thread < -1 or max_thread == 0:
791
793
  raise ValueError("max_thread must be -1 or a positive integer")
794
+ if timeout < -1 or timeout == 0:
795
+ raise ValueError("timeout must be -1 or a positive integer")
792
796
  library = _load_library()
793
797
  removed_count = 0 if remove_crossings is None else len(remove_crossings)
794
798
  removed_array = None
@@ -801,6 +805,7 @@ def _run_one(
801
805
  1 if ban_heuristic else 0,
802
806
  int(reduction_round),
803
807
  int(max_thread),
808
+ int(timeout),
804
809
  1 if verbose else 0,
805
810
  int(known_crossingless_components),
806
811
  removed_array,
@@ -822,6 +827,93 @@ def _run_one(
822
827
  return result
823
828
 
824
829
 
830
+ def _terminate_process(proc: subprocess.Popen[str]) -> None:
831
+ if proc.poll() is not None:
832
+ return
833
+ proc.terminate()
834
+ try:
835
+ proc.wait(timeout=2.0)
836
+ except subprocess.TimeoutExpired:
837
+ proc.kill()
838
+ proc.wait()
839
+
840
+
841
+ def _run_one(
842
+ pd_text: str,
843
+ *,
844
+ max_paths: int = -1,
845
+ ban_heuristic: bool = False,
846
+ reduction_round: int = -1,
847
+ max_thread: int = -1,
848
+ timeout: int = -1,
849
+ verbose: bool = False,
850
+ known_crossingless_components: int = 0,
851
+ remove_crossings: Optional[Sequence[int]] = None,
852
+ ) -> dict[str, Any]:
853
+ if reduction_round < -1:
854
+ raise ValueError("reduction_round must be -1 or a non-negative integer")
855
+ if max_thread < -1 or max_thread == 0:
856
+ raise ValueError("max_thread must be -1 or a positive integer")
857
+ if timeout < -1 or timeout == 0:
858
+ raise ValueError("timeout must be -1 or a positive integer")
859
+
860
+ request = {
861
+ "pd_text": pd_text,
862
+ "max_paths": int(max_paths),
863
+ "ban_heuristic": bool(ban_heuristic),
864
+ "reduction_round": int(reduction_round),
865
+ "max_thread": int(max_thread),
866
+ "timeout": int(timeout),
867
+ "verbose": bool(verbose),
868
+ "known_crossingless_components": int(known_crossingless_components),
869
+ "remove_crossings": [int(value) for value in remove_crossings or []],
870
+ }
871
+ env = os.environ.copy()
872
+ project_root = str(pathlib.Path(__file__).resolve().parents[1])
873
+ env["PYTHONPATH"] = project_root + os.pathsep + env.get("PYTHONPATH", "")
874
+ proc = subprocess.Popen(
875
+ [
876
+ sys.executable,
877
+ "-m",
878
+ "cpp_pd_code_simplify_interface._worker",
879
+ ],
880
+ cwd=str(pathlib.Path.cwd()),
881
+ text=True,
882
+ stdin=subprocess.PIPE,
883
+ stdout=subprocess.PIPE,
884
+ stderr=None if verbose else subprocess.PIPE,
885
+ env=env,
886
+ )
887
+ try:
888
+ stdout, stderr = proc.communicate(json.dumps(request))
889
+ except KeyboardInterrupt:
890
+ _terminate_process(proc)
891
+ raise
892
+
893
+ if proc.returncode != 0:
894
+ detail = (stderr or "").strip()
895
+ raise PdCodeSimplifyInterfaceError(
896
+ f"C++ interface worker failed with exit code {proc.returncode}"
897
+ + (f": {detail}" if detail else "")
898
+ )
899
+ try:
900
+ envelope = json.loads(stdout)
901
+ except json.JSONDecodeError as exc:
902
+ raise PdCodeSimplifyInterfaceError(
903
+ f"invalid interface worker JSON output: {stdout!r}"
904
+ ) from exc
905
+ if not isinstance(envelope, dict):
906
+ raise PdCodeSimplifyInterfaceError(
907
+ f"invalid interface worker response type: {type(envelope)!r}"
908
+ )
909
+ if not envelope.get("ok", False):
910
+ raise PdCodeSimplifyInterfaceError(str(envelope.get("error", "unknown worker error")))
911
+ result = envelope.get("result")
912
+ if not isinstance(result, dict):
913
+ raise PdCodeSimplifyInterfaceError("interface worker did not return a JSON object result")
914
+ return result
915
+
916
+
825
917
  def simplify(
826
918
  pd_code: PdInput,
827
919
  *,
@@ -829,6 +921,7 @@ def simplify(
829
921
  ban_heuristic: bool = False,
830
922
  reduction_round: int = -1,
831
923
  max_thread: int = -1,
924
+ timeout: int = -1,
832
925
  verbose: bool = False,
833
926
  known_crossingless_components: int = 0,
834
927
  remove_crossings: Optional[Sequence[int]] = None,
@@ -841,6 +934,7 @@ def simplify(
841
934
  ban_heuristic=ban_heuristic,
842
935
  reduction_round=reduction_round,
843
936
  max_thread=max_thread,
937
+ timeout=timeout,
844
938
  verbose=verbose,
845
939
  known_crossingless_components=known_crossingless_components,
846
940
  remove_crossings=remove_crossings,
@@ -854,6 +948,7 @@ def simplify_many(
854
948
  ban_heuristic: bool = False,
855
949
  reduction_round: int = -1,
856
950
  max_thread: int = -1,
951
+ timeout: int = -1,
857
952
  verbose: bool = False,
858
953
  known_crossingless_components: int = 0,
859
954
  remove_crossings: Optional[Sequence[int]] = None,
@@ -867,6 +962,7 @@ def simplify_many(
867
962
  ban_heuristic=ban_heuristic,
868
963
  reduction_round=reduction_round,
869
964
  max_thread=max_thread,
965
+ timeout=timeout,
870
966
  verbose=verbose,
871
967
  known_crossingless_components=known_crossingless_components,
872
968
  remove_crossings=remove_crossings,
@@ -884,6 +980,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
884
980
  parser.add_argument("--ban-heuristic", action="store_true")
885
981
  parser.add_argument("--reduction-round", type=int, default=-1)
886
982
  parser.add_argument("--max-thread", type=int, default=-1)
983
+ parser.add_argument("--timeout", type=int, default=-1)
887
984
  parser.add_argument("--verbose", action="store_true")
888
985
  parser.add_argument("--known-crossingless-components", type=int, default=0)
889
986
  parser.add_argument("--remove-crossings", help="comma-separated zero-based crossing indices")
@@ -892,6 +989,8 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
892
989
  parser.error("--reduction-round must be -1 or a non-negative integer")
893
990
  if args.max_thread < -1 or args.max_thread == 0:
894
991
  parser.error("--max-thread must be -1 or a positive integer")
992
+ if args.timeout < -1 or args.timeout == 0:
993
+ parser.error("--timeout must be -1 or a positive integer")
895
994
  if args.pd_code and args.pd_code_option:
896
995
  parser.error("pass either a positional PD code or --pd-code, not both")
897
996
  pd_code_text = args.pd_code_option or args.pd_code
@@ -916,6 +1015,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
916
1015
  ban_heuristic=args.ban_heuristic,
917
1016
  reduction_round=args.reduction_round,
918
1017
  max_thread=args.max_thread,
1018
+ timeout=args.timeout,
919
1019
  verbose=args.verbose,
920
1020
  known_crossingless_components=args.known_crossingless_components,
921
1021
  remove_crossings=remove_crossings,
@@ -923,6 +1023,15 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
923
1023
  if show_labels:
924
1024
  item = {"label": label, **item}
925
1025
  batch_payload.append(item)
1026
+ if item.get("timed_out"):
1027
+ exit_code = 2
1028
+ except KeyboardInterrupt:
1029
+ exit_code = 130
1030
+ item = {"error": "interrupted by Ctrl+C"}
1031
+ if show_labels:
1032
+ item = {"label": label, **item}
1033
+ batch_payload.append(item)
1034
+ break
926
1035
  except Exception as exc:
927
1036
  exit_code = 2
928
1037
  item = {"error": str(exc)}
@@ -931,16 +1040,26 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
931
1040
  batch_payload.append(item)
932
1041
  payload: Any = batch_payload
933
1042
  else:
934
- payload = simplify(
935
- pd_code_text or "",
936
- max_paths=args.max_paths,
937
- ban_heuristic=args.ban_heuristic,
938
- reduction_round=args.reduction_round,
939
- max_thread=args.max_thread,
940
- verbose=args.verbose,
941
- known_crossingless_components=args.known_crossingless_components,
942
- remove_crossings=remove_crossings,
943
- )
1043
+ try:
1044
+ payload = simplify(
1045
+ pd_code_text or "",
1046
+ max_paths=args.max_paths,
1047
+ ban_heuristic=args.ban_heuristic,
1048
+ reduction_round=args.reduction_round,
1049
+ max_thread=args.max_thread,
1050
+ timeout=args.timeout,
1051
+ verbose=args.verbose,
1052
+ known_crossingless_components=args.known_crossingless_components,
1053
+ remove_crossings=remove_crossings,
1054
+ )
1055
+ if isinstance(payload, dict) and payload.get("timed_out"):
1056
+ exit_code = 2
1057
+ except KeyboardInterrupt:
1058
+ exit_code = 130
1059
+ payload = {"error": "interrupted by Ctrl+C"}
1060
+ except Exception as exc:
1061
+ exit_code = 2
1062
+ payload = {"error": str(exc)}
944
1063
 
945
1064
  print(json.dumps(payload, indent=2))
946
1065
  return exit_code
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cpp-pd-code-simplify-interface"
3
- version = "0.1.11"
3
+ version = "0.1.12"
4
4
  description = "Python interface for cpp-pd-code-simplify with runtime C++ compilation."
5
5
  authors = [
6
6
  {name = "GGN_2015", email = "neko@jlulug.org"}