cpp-pd-code-simplify-interface 0.1.11__py3-none-any.whl → 0.1.13__py3-none-any.whl

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.
@@ -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,18 +1070,24 @@ 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
- if (source == target || source < 0 || target < 0 ||
1077
+ if (source < 0 || target < 0 ||
1069
1078
  source >= static_cast<int>(graph.faces.size()) ||
1070
1079
  target >= static_cast<int>(graph.faces.size()) ||
1071
1080
  cutoff <= 0) {
1072
1081
  return paths;
1073
1082
  }
1083
+ if (source == target) {
1084
+ paths.push_back(std::vector<int>{source});
1085
+ return paths;
1086
+ }
1074
1087
 
1075
1088
  std::vector<char> visited(graph.faces.size(), false);
1076
1089
  std::vector<int> current_path{source};
1077
- const std::vector<int> distance = heuristic_distances_to_target(graph, target, cutoff);
1090
+ const std::vector<int> distance = heuristic_distances_to_target(graph, target, cutoff, options);
1078
1091
  visited[source] = true;
1079
1092
  collect_simple_paths_dfs(
1080
1093
  graph,
@@ -1086,14 +1099,16 @@ std::vector<std::vector<int>> collect_simple_paths(
1086
1099
  distance,
1087
1100
  visited,
1088
1101
  current_path,
1089
- paths);
1102
+ paths,
1103
+ options);
1090
1104
  return paths;
1091
1105
  }
1092
1106
 
1093
1107
  std::vector<int> heuristic_distances_to_target(
1094
1108
  const DualGraph& graph,
1095
1109
  int target,
1096
- int cutoff) {
1110
+ int cutoff,
1111
+ const SimplifierOptions& options) {
1097
1112
  const int face_count = static_cast<int>(graph.faces.size());
1098
1113
  const int infinity = std::numeric_limits<int>::max() / 4;
1099
1114
  std::vector<int> distance(face_count, infinity);
@@ -1102,6 +1117,7 @@ std::vector<int> heuristic_distances_to_target(
1102
1117
  queue.push_back(target);
1103
1118
 
1104
1119
  while (!queue.empty()) {
1120
+ check_timeout(options);
1105
1121
  const int current = queue.front();
1106
1122
  queue.pop_front();
1107
1123
  for (int edge_index : graph.adjacency[current]) {
@@ -1179,15 +1195,21 @@ std::vector<std::vector<int>> collect_heuristic_paths(
1179
1195
  const DualGraph& graph,
1180
1196
  int source,
1181
1197
  int target,
1182
- int cutoff) {
1198
+ int cutoff,
1199
+ const SimplifierOptions& options) {
1200
+ check_timeout(options);
1183
1201
  std::vector<std::vector<int>> paths;
1184
1202
  const int face_count = static_cast<int>(graph.faces.size());
1185
- if (source == target || source < 0 || target < 0 ||
1203
+ if (source < 0 || target < 0 ||
1186
1204
  source >= face_count || target >= face_count || cutoff <= 0) {
1187
1205
  return paths;
1188
1206
  }
1207
+ if (source == target) {
1208
+ paths.push_back(std::vector<int>{source});
1209
+ return paths;
1210
+ }
1189
1211
 
1190
- const std::vector<int> distance = heuristic_distances_to_target(graph, target, cutoff);
1212
+ const std::vector<int> distance = heuristic_distances_to_target(graph, target, cutoff, options);
1191
1213
  const int infinity = std::numeric_limits<int>::max() / 4;
1192
1214
  if (distance[source] == infinity || distance[source] >= cutoff) {
1193
1215
  return paths;
@@ -1215,6 +1237,7 @@ std::vector<std::vector<int>> collect_heuristic_paths(
1215
1237
  int popped_states = 0;
1216
1238
  while (!queue.empty() && popped_states < state_budget &&
1217
1239
  static_cast<int>(paths.size()) < path_budget) {
1240
+ check_timeout(options);
1218
1241
  HeuristicState state = queue.top();
1219
1242
  queue.pop();
1220
1243
  ++popped_states;
@@ -1823,6 +1846,41 @@ std::string search_mode_for_options(const SimplifierOptions& options) {
1823
1846
  return "bounded";
1824
1847
  }
1825
1848
 
1849
+ void validate_timeout_options(const SimplifierOptions& options) {
1850
+ if (options.timeout_seconds < -1 || options.timeout_seconds == 0) {
1851
+ throw std::invalid_argument("timeout must be -1 or a positive integer");
1852
+ }
1853
+ }
1854
+
1855
+ class TimeoutError : public std::runtime_error {
1856
+ public:
1857
+ explicit TimeoutError(const std::string& message) : std::runtime_error(message) {}
1858
+ };
1859
+
1860
+ SimplifierOptions with_timeout_deadline(SimplifierOptions options) {
1861
+ validate_timeout_options(options);
1862
+ if (options.timeout_seconds > 0 && !options.has_timeout_deadline) {
1863
+ options.has_timeout_deadline = true;
1864
+ options.timeout_deadline =
1865
+ std::chrono::steady_clock::now() + std::chrono::seconds(options.timeout_seconds);
1866
+ }
1867
+ return options;
1868
+ }
1869
+
1870
+ void check_timeout(const SimplifierOptions& options) {
1871
+ if (options.should_cancel && options.should_cancel()) {
1872
+ throw std::runtime_error("interrupted by Ctrl+C");
1873
+ }
1874
+ if (!options.has_timeout_deadline) {
1875
+ return;
1876
+ }
1877
+ if (std::chrono::steady_clock::now() >= options.timeout_deadline) {
1878
+ std::ostringstream message;
1879
+ message << "timeout after " << options.timeout_seconds << " seconds";
1880
+ throw TimeoutError(message.str());
1881
+ }
1882
+ }
1883
+
1826
1884
  } // namespace
1827
1885
 
1828
1886
  PDCode parse_pd_code(const std::string& text) {
@@ -2213,6 +2271,7 @@ RedPathSearchOutcome search_single_red_path(
2213
2271
  const std::string& path_search_mode,
2214
2272
  int red_index,
2215
2273
  const std::atomic<int>* best_found_index) {
2274
+ check_timeout(options);
2216
2275
  RedPathSearchOutcome outcome;
2217
2276
  outcome.witness.path_search_mode = path_search_mode;
2218
2277
  if (should_skip_parallel_red_path(red_index, best_found_index)) {
@@ -2231,6 +2290,7 @@ RedPathSearchOutcome search_single_red_path(
2231
2290
  const std::array<int, 2> destinations{end_face, end_opposite_face};
2232
2291
 
2233
2292
  for (std::size_t i = 1; i + 1 < red_path.size(); ++i) {
2293
+ check_timeout(options);
2234
2294
  const Endpoint endpoint = red_path[i];
2235
2295
  const int right_region = graph.edge_to_face[endpoint_key(endpoint)];
2236
2296
  const int left_region = graph.edge_to_face[endpoint_key(diagram.opposite(endpoint))];
@@ -2243,15 +2303,17 @@ RedPathSearchOutcome search_single_red_path(
2243
2303
  const int cutoff = static_cast<int>(red_path.size()) - 1;
2244
2304
  for (int source : sources) {
2245
2305
  for (int destination : destinations) {
2306
+ check_timeout(options);
2246
2307
  if (should_skip_parallel_red_path(red_index, best_found_index)) {
2247
2308
  outcome.skipped = true;
2248
2309
  return outcome;
2249
2310
  }
2250
2311
  std::vector<std::vector<int>> found_paths;
2251
2312
  if (options.max_paths == -1 && !options.ban_heuristic) {
2252
- found_paths = collect_heuristic_paths(graph, source, destination, cutoff);
2313
+ found_paths = collect_heuristic_paths(graph, source, destination, cutoff, options);
2253
2314
  } else {
2254
- found_paths = collect_simple_paths(graph, source, destination, cutoff, options.max_paths);
2315
+ found_paths = collect_simple_paths(
2316
+ graph, source, destination, cutoff, options.max_paths, options);
2255
2317
  }
2256
2318
  paths.insert(paths.end(), found_paths.begin(), found_paths.end());
2257
2319
  if (options.max_paths != -1 && static_cast<int>(paths.size()) > options.max_paths) {
@@ -2261,6 +2323,7 @@ RedPathSearchOutcome search_single_red_path(
2261
2323
  }
2262
2324
 
2263
2325
  for (const auto& green_path : paths) {
2326
+ check_timeout(options);
2264
2327
  if (should_skip_parallel_red_path(red_index, best_found_index)) {
2265
2328
  outcome.skipped = true;
2266
2329
  return outcome;
@@ -2401,36 +2464,49 @@ SimplificationResult find_simplification_parallel_bruteforce(
2401
2464
  SimplificationResult find_simplification(
2402
2465
  const PDCode& code,
2403
2466
  const SimplifierOptions& options) {
2467
+ const SimplifierOptions run_options = with_timeout_deadline(options);
2468
+ check_timeout(run_options);
2404
2469
  SimplificationResult result;
2405
- result.path_search_mode = search_mode_for_options(options);
2470
+ result.path_search_mode = search_mode_for_options(run_options);
2406
2471
  Diagram diagram(code);
2472
+ check_timeout(run_options);
2407
2473
  DualGraph base_graph(diagram);
2474
+ check_timeout(run_options);
2408
2475
  const auto red_lines = possible_red_lines(diagram);
2409
- const bool brute_force_mode = options.max_paths == -1 && options.ban_heuristic;
2476
+ check_timeout(run_options);
2477
+ const bool brute_force_mode = run_options.max_paths == -1 && run_options.ban_heuristic;
2410
2478
  const int worker_count = brute_force_mode
2411
2479
  ? selected_bruteforce_worker_count(
2412
- options.max_threads,
2480
+ run_options.max_threads,
2413
2481
  static_cast<int>(red_lines.size()))
2414
2482
  : 1;
2483
+ if (brute_force_mode && run_options.max_threads == -1) {
2484
+ std::ostringstream message;
2485
+ message << "bruteforce_threads max_thread=-1"
2486
+ << " actual_threads=" << worker_count
2487
+ << " red_paths=" << red_lines.size();
2488
+ emit_progress(run_options, message.str());
2489
+ }
2415
2490
  if (worker_count > 1) {
2416
2491
  return find_simplification_parallel_bruteforce(
2417
2492
  code,
2418
2493
  diagram,
2419
2494
  base_graph,
2420
2495
  red_lines,
2421
- options,
2496
+ run_options,
2422
2497
  result.path_search_mode,
2423
2498
  worker_count);
2424
2499
  }
2425
2500
 
2426
2501
  for (const auto& red_path : red_lines) {
2502
+ check_timeout(run_options);
2427
2503
  ++result.tested_red_paths;
2428
2504
  const RedPathSearchOutcome outcome = search_single_red_path(
2429
2505
  code,
2430
2506
  diagram,
2431
2507
  base_graph,
2432
2508
  red_path,
2433
- options,
2509
+ run_options,
2434
2510
  result.path_search_mode,
2435
2511
  -1,
2436
2512
  nullptr);
@@ -2452,125 +2528,155 @@ ReductionResult reduce_pd_code(
2452
2528
  std::size_t known_crossingless_components,
2453
2529
  const SimplifierOptions& options,
2454
2530
  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);
2531
+ const SimplifierOptions run_options = with_timeout_deadline(options);
2467
2532
  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
- }
2533
+ output.code = code;
2534
+ output.crossingless_components = known_crossingless_components;
2481
2535
 
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;
2536
+ try {
2537
+ check_timeout(run_options);
2486
2538
  {
2487
2539
  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;
2540
+ message << "start input_crossings=" << code.size()
2541
+ << " known_crossingless_components=" << known_crossingless_components
2542
+ << " reduction_round=" << reduction_round
2543
+ << " max_paths=" << run_options.max_paths
2544
+ << " max_thread=" << run_options.max_threads
2545
+ << " timeout=" << run_options.timeout_seconds
2546
+ << " heuristic=" << (run_options.ban_heuristic ? "off" : "on");
2547
+ emit_progress(run_options, message.str());
2548
+ }
2549
+
2550
+ const PDSimplificationResult prepared =
2551
+ simplify_pd_code(code, known_crossingless_components);
2552
+ output.code = prepared.code;
2553
+ output.crossingless_components = prepared.crossingless_components;
2554
+ output.reidemeister_i_moves = prepared.reidemeister_i_moves;
2555
+ output.nugatory_crossing_moves = prepared.nugatory_crossing_moves;
2556
+ check_timeout(run_options);
2498
2557
  {
2499
2558
  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;
2559
+ message << "pre_simplify input_crossings=" << code.size()
2560
+ << " output_crossings=" << output.code.size()
2561
+ << " crossingless_components=" << output.crossingless_components
2562
+ << " r1_moves=" << prepared.reidemeister_i_moves
2563
+ << " nugatory_moves=" << prepared.nugatory_crossing_moves;
2564
+ emit_progress(run_options, message.str());
2565
+ }
2566
+
2567
+ while (reduction_round < 0 || output.mid_simplification_rounds < reduction_round) {
2568
+ check_timeout(run_options);
2569
+ SimplifierOptions search_options = run_options;
2570
+ search_options.require_applicable = true;
2571
+ output.last_path_search_mode = search_mode_for_options(search_options);
2572
+ const int round = output.mid_simplification_rounds + 1;
2514
2573
  {
2515
2574
  std::ostringstream message;
2516
2575
  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;
2576
+ << " search_start crossings=" << output.code.size()
2577
+ << " mode=" << output.last_path_search_mode
2578
+ << " max_thread=" << search_options.max_threads;
2579
+ emit_progress(run_options, message.str());
2580
+ }
2581
+ SimplificationResult search = find_simplification(output.code, search_options);
2582
+ output.tested_red_paths += search.tested_red_paths;
2583
+ output.tested_green_paths += search.tested_green_paths;
2584
+ output.last_path_search_mode = search.path_search_mode;
2525
2585
  {
2526
2586
  std::ostringstream message;
2527
2587
  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());
2588
+ << " search_done found=" << (search.found ? "yes" : "no")
2589
+ << " mode=" << search.path_search_mode
2590
+ << " tested_red=" << search.tested_red_paths
2591
+ << " tested_green=" << search.tested_green_paths;
2592
+ emit_progress(run_options, message.str());
2593
+ }
2594
+
2595
+ if (!search.found && reduction_round < 0 &&
2596
+ run_options.max_paths == -1 && !run_options.ban_heuristic) {
2597
+ SimplifierOptions brute_options = run_options;
2598
+ brute_options.max_paths = -1;
2599
+ brute_options.ban_heuristic = true;
2600
+ brute_options.require_applicable = true;
2601
+ output.last_path_search_mode = search_mode_for_options(brute_options);
2602
+ {
2603
+ std::ostringstream message;
2604
+ message << "round " << round
2605
+ << " brute_fallback_start crossings=" << output.code.size()
2606
+ << " max_thread=" << brute_options.max_threads;
2607
+ emit_progress(run_options, message.str());
2608
+ }
2609
+ SimplificationResult brute = find_simplification(output.code, brute_options);
2610
+ output.tested_red_paths += brute.tested_red_paths;
2611
+ output.tested_green_paths += brute.tested_green_paths;
2612
+ output.last_path_search_mode = brute.path_search_mode;
2613
+ {
2614
+ std::ostringstream message;
2615
+ message << "round " << round
2616
+ << " brute_fallback_done found=" << (brute.found ? "yes" : "no")
2617
+ << " tested_red=" << brute.tested_red_paths
2618
+ << " tested_green=" << brute.tested_green_paths;
2619
+ emit_progress(run_options, message.str());
2620
+ }
2621
+ if (brute.found) {
2622
+ ++output.heuristic_failover_rounds;
2623
+ search = std::move(brute);
2624
+ }
2532
2625
  }
2533
- if (brute.found) {
2534
- ++output.heuristic_failover_rounds;
2535
- search = std::move(brute);
2626
+
2627
+ if (!search.found) {
2628
+ {
2629
+ std::ostringstream message;
2630
+ message << "round " << round
2631
+ << " stop_no_path crossings=" << output.code.size();
2632
+ emit_progress(run_options, message.str());
2633
+ }
2634
+ break;
2536
2635
  }
2537
- }
2538
2636
 
2539
- if (!search.found) {
2637
+ const std::size_t before_apply_crossings = output.code.size();
2638
+ check_timeout(run_options);
2639
+ const MidSimplificationApplyResult applied =
2640
+ apply_simplification_witness(output.code, search, output.crossingless_components);
2641
+ ++output.mid_simplification_rounds;
2642
+ output.code = applied.code;
2643
+ output.crossingless_components = applied.crossingless_components;
2644
+ check_timeout(run_options);
2645
+ const PDSimplificationResult simplified =
2646
+ simplify_pd_code(output.code, output.crossingless_components);
2647
+ output.code = simplified.code;
2648
+ output.crossingless_components = simplified.crossingless_components;
2649
+ output.reidemeister_i_moves += simplified.reidemeister_i_moves;
2650
+ output.nugatory_crossing_moves += simplified.nugatory_crossing_moves;
2651
+ check_timeout(run_options);
2540
2652
  {
2541
2653
  std::ostringstream message;
2542
2654
  message << "round " << round
2543
- << " stop_no_path crossings=" << output.code.size();
2544
- emit_progress(options, message.str());
2655
+ << " applied crossings=" << before_apply_crossings
2656
+ << " -> " << applied.code.size()
2657
+ << " -> " << output.code.size()
2658
+ << " crossingless_components=" << output.crossingless_components
2659
+ << " r1_moves=" << simplified.reidemeister_i_moves
2660
+ << " nugatory_moves=" << simplified.nugatory_crossing_moves;
2661
+ emit_progress(run_options, message.str());
2545
2662
  }
2546
- break;
2547
2663
  }
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;
2664
+ } catch (const TimeoutError& error) {
2665
+ output.timed_out = true;
2559
2666
  {
2560
2667
  std::ostringstream message;
2561
- message << "round " << round
2562
- << " applied crossings=" << before_apply_crossings
2563
- << " -> " << applied.code.size()
2564
- << " -> " << output.code.size()
2668
+ message << error.what()
2669
+ << "; returning_current_best crossings=" << output.code.size()
2565
2670
  << " 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());
2671
+ << " mid_rounds=" << output.mid_simplification_rounds;
2672
+ emit_progress(run_options, message.str());
2569
2673
  }
2570
2674
  }
2571
2675
 
2572
2676
  output.stopped_by_round_limit =
2573
- reduction_round >= 0 && output.mid_simplification_rounds >= reduction_round;
2677
+ !output.timed_out &&
2678
+ reduction_round >= 0 &&
2679
+ output.mid_simplification_rounds >= reduction_round;
2574
2680
  {
2575
2681
  std::ostringstream message;
2576
2682
  message << "done final_crossings=" << output.code.size()
@@ -2578,8 +2684,9 @@ ReductionResult reduce_pd_code(
2578
2684
  << " mid_rounds=" << output.mid_simplification_rounds
2579
2685
  << " heuristic_failover_rounds=" << output.heuristic_failover_rounds
2580
2686
  << " stopped_by_round_limit="
2581
- << (output.stopped_by_round_limit ? "yes" : "no");
2582
- emit_progress(options, message.str());
2687
+ << (output.stopped_by_round_limit ? "yes" : "no")
2688
+ << " timed_out=" << (output.timed_out ? "yes" : "no");
2689
+ emit_progress(run_options, message.str());
2583
2690
  }
2584
2691
  return output;
2585
2692
  }
@@ -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
  Metadata-Version: 2.4
2
2
  Name: cpp-pd-code-simplify-interface
3
- Version: 0.1.11
3
+ Version: 0.1.13
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
 
@@ -0,0 +1,12 @@
1
+ cpp_pd_code_simplify_interface/__init__.py,sha256=aaNwD--zgQX13xiU1hpuvgXy8vVTFGRk-r5zLm8zIRY,447
2
+ cpp_pd_code_simplify_interface/__main__.py,sha256=fdA1QWBe5LNX0fbtaJXa1-dwEhrg_4sS_5UnjaajcMo,80
3
+ cpp_pd_code_simplify_interface/_worker.py,sha256=pgvL3zlAZkUVM033YKAT1geXxcI8TxTUXLp83sgrZgw,1477
4
+ cpp_pd_code_simplify_interface/main.py,sha256=QkhR_2Cq9Ep_8hT5qpFKML7bhq99SuQSKO0jePJHp3U,37007
5
+ cpp_pd_code_simplify_interface/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
6
+ cpp_pd_code_simplify_interface-0.1.13.dist-info/entry_points.txt,sha256=ThorC4Enxp317TKITsXm9pE0gUp7pt-pP6Dl5mEPUR8,91
7
+ cpp_pd_code_simplify_interface-0.1.13.dist-info/METADATA,sha256=-uQyhNQjhrKxBj2_HaRqkrWhF50ojY878shH3kafxaE,4553
8
+ cpp_pd_code_simplify_interface-0.1.13.dist-info/WHEEL,sha256=eY7nduwzv-ldUxpzbRlxwvC693Hg6PX8bWDjEHjZ_dk,88
9
+ cpp_pd_code_simplify_interface/data/include/pdcode_simplify/pdcode_simplify.hpp,sha256=ZWZVWuSXN9HgCYd88pfURgLqs-3_0xGDi_ZEPZrYUyY,5135
10
+ cpp_pd_code_simplify_interface/data/src/native_interface.cpp,sha256=-X9ztIyCl5y27Yhj2vCyC4bHCeDpFlcKm6ACPMDoxHs,6708
11
+ cpp_pd_code_simplify_interface/data/src/pdcode_simplify.cpp,sha256=xQd5-tirH-Mk4dLZpXAAZo6qHP4mpAFO94Z8GFCPJbg,95788
12
+ cpp_pd_code_simplify_interface-0.1.13.dist-info/RECORD,,
@@ -1,11 +0,0 @@
1
- cpp_pd_code_simplify_interface/__init__.py,sha256=aaNwD--zgQX13xiU1hpuvgXy8vVTFGRk-r5zLm8zIRY,447
2
- cpp_pd_code_simplify_interface/__main__.py,sha256=fdA1QWBe5LNX0fbtaJXa1-dwEhrg_4sS_5UnjaajcMo,80
3
- cpp_pd_code_simplify_interface/main.py,sha256=4t-6xvjQjsuEWP_oiwedGtgrLYu3316uATrl9t4Q8zs,32778
4
- cpp_pd_code_simplify_interface/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
5
- cpp_pd_code_simplify_interface-0.1.11.dist-info/entry_points.txt,sha256=ThorC4Enxp317TKITsXm9pE0gUp7pt-pP6Dl5mEPUR8,91
6
- cpp_pd_code_simplify_interface-0.1.11.dist-info/METADATA,sha256=XtZebJNhEOhA9lHFiGYYHFQJUF5DXS7ItsG0AhOALv8,4073
7
- cpp_pd_code_simplify_interface-0.1.11.dist-info/WHEEL,sha256=eY7nduwzv-ldUxpzbRlxwvC693Hg6PX8bWDjEHjZ_dk,88
8
- cpp_pd_code_simplify_interface/data/include/pdcode_simplify/pdcode_simplify.hpp,sha256=0WflWW2MB2EAK8GbcKKr9SbnoByMV3hFxt4m4mAerzQ,4917
9
- cpp_pd_code_simplify_interface/data/src/native_interface.cpp,sha256=16HbKJBPA_EGdbyom4sSoQ7lisDBwaKrl7NsphmlA-k,6555
10
- cpp_pd_code_simplify_interface/data/src/pdcode_simplify.cpp,sha256=n5rvqpHcUqS87O2AV9duSMAmrh7KUI0HwGHYd5UkMFY,91309
11
- cpp_pd_code_simplify_interface-0.1.11.dist-info/RECORD,,