cpp-pd-code-simplify-interface 0.1.1__tar.gz → 0.1.3__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cpp-pd-code-simplify-interface
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: Python interface for cpp-pd-code-simplify with runtime C++ compilation.
5
5
  License-Expression: MIT
6
6
  Author: GGN_2015
@@ -35,7 +35,7 @@ use, the package compiles a cached local dynamic library through
35
35
  compiler compatible with `g++` must be available at runtime.
36
36
 
37
37
  Calls use the C++ library's default preprocessing pipeline: R1-move removal
38
- followed by nugatory-crossing removal.
38
+ followed by nugatory-crossing removal, then iterative mid-simplification.
39
39
 
40
40
  ## Example
41
41
 
@@ -44,9 +44,15 @@ import cpp_pd_code_simplify_interface as simplify
44
44
 
45
45
  pd_code = "PD[X[1,5,2,4],X[3,1,4,6],X[5,3,6,2]]"
46
46
  result = simplify.simplify(pd_code)
47
- print(result["simplification_found"])
47
+ print(result["final_pd_code"])
48
48
  ```
49
49
 
50
+ The default `max_paths=-1` uses deterministic heuristic green-path sampling in
51
+ the C++ backend. Use `ban_heuristic=True` to request exhaustive green-path
52
+ enumeration for a manageable input. Use `reduction_round=K` to cap applied
53
+ mid-simplification rounds; the default `-1` runs until stable. Use
54
+ `verbose=True` to forward C++ progress logs to stderr.
55
+
50
56
  Batch use:
51
57
 
52
58
  ```python
@@ -72,7 +78,7 @@ Python process needs a 64-bit compiler target.
72
78
  Command-line use also supports multi-line PD-code files:
73
79
 
74
80
  ```sh
75
- python -m cpp_pd_code_simplify_interface --pd-file inputs.pd --max-paths 100
81
+ python -m cpp_pd_code_simplify_interface --pd-file inputs.pd --max-paths -1 --verbose
76
82
  ```
77
83
 
78
84
  ## Build And Publish
@@ -15,7 +15,7 @@ use, the package compiles a cached local dynamic library through
15
15
  compiler compatible with `g++` must be available at runtime.
16
16
 
17
17
  Calls use the C++ library's default preprocessing pipeline: R1-move removal
18
- followed by nugatory-crossing removal.
18
+ followed by nugatory-crossing removal, then iterative mid-simplification.
19
19
 
20
20
  ## Example
21
21
 
@@ -24,9 +24,15 @@ import cpp_pd_code_simplify_interface as simplify
24
24
 
25
25
  pd_code = "PD[X[1,5,2,4],X[3,1,4,6],X[5,3,6,2]]"
26
26
  result = simplify.simplify(pd_code)
27
- print(result["simplification_found"])
27
+ print(result["final_pd_code"])
28
28
  ```
29
29
 
30
+ The default `max_paths=-1` uses deterministic heuristic green-path sampling in
31
+ the C++ backend. Use `ban_heuristic=True` to request exhaustive green-path
32
+ enumeration for a manageable input. Use `reduction_round=K` to cap applied
33
+ mid-simplification rounds; the default `-1` runs until stable. Use
34
+ `verbose=True` to forward C++ progress logs to stderr.
35
+
30
36
  Batch use:
31
37
 
32
38
  ```python
@@ -52,7 +58,7 @@ Python process needs a 64-bit compiler target.
52
58
  Command-line use also supports multi-line PD-code files:
53
59
 
54
60
  ```sh
55
- python -m cpp_pd_code_simplify_interface --pd-file inputs.pd --max-paths 100
61
+ python -m cpp_pd_code_simplify_interface --pd-file inputs.pd --max-paths -1 --verbose
56
62
  ```
57
63
 
58
64
  ## Build And Publish
@@ -2,6 +2,7 @@
2
2
 
3
3
  #include <array>
4
4
  #include <cstddef>
5
+ #include <functional>
5
6
  #include <iosfwd>
6
7
  #include <string>
7
8
  #include <vector>
@@ -48,7 +49,11 @@ struct GreenCrossing {
48
49
  };
49
50
 
50
51
  struct SimplifierOptions {
51
- int max_paths = 100;
52
+ int max_paths = -1;
53
+ bool ban_heuristic = false;
54
+ bool require_applicable = false;
55
+ bool verbose = false;
56
+ std::function<void(const std::string&)> progress;
52
57
  };
53
58
 
54
59
  struct LinkComponentSummary {
@@ -98,6 +103,7 @@ struct PDSimplificationResult {
98
103
  struct SimplificationResult {
99
104
  bool found = false;
100
105
  Direction direction = Direction::Left;
106
+ std::string path_search_mode;
101
107
  std::vector<Endpoint> red_path;
102
108
  std::vector<int> green_path;
103
109
  std::vector<GreenCrossing> green_crossings;
@@ -105,6 +111,24 @@ struct SimplificationResult {
105
111
  std::size_t tested_green_paths = 0;
106
112
  };
107
113
 
114
+ struct MidSimplificationApplyResult {
115
+ PDCode code;
116
+ std::size_t crossingless_components = 0;
117
+ };
118
+
119
+ struct ReductionResult {
120
+ PDCode code;
121
+ std::size_t crossingless_components = 0;
122
+ int mid_simplification_rounds = 0;
123
+ int heuristic_failover_rounds = 0;
124
+ int reidemeister_i_moves = 0;
125
+ int nugatory_crossing_moves = 0;
126
+ std::size_t tested_red_paths = 0;
127
+ std::size_t tested_green_paths = 0;
128
+ std::string last_path_search_mode;
129
+ bool stopped_by_round_limit = false;
130
+ };
131
+
108
132
  PDCODE_SIMPLIFY_API PDCode parse_pd_code(const std::string& text);
109
133
  PDCODE_SIMPLIFY_API std::string format_pd_code(const PDCode& code);
110
134
  PDCODE_SIMPLIFY_API std::string format_endpoint(const Endpoint& endpoint);
@@ -140,6 +164,17 @@ PDCODE_SIMPLIFY_API SimplificationResult find_simplification(
140
164
  const PDCode& code,
141
165
  const SimplifierOptions& options = SimplifierOptions{});
142
166
 
167
+ PDCODE_SIMPLIFY_API MidSimplificationApplyResult apply_simplification_witness(
168
+ const PDCode& code,
169
+ const SimplificationResult& result,
170
+ std::size_t known_crossingless_components = 0);
171
+
172
+ PDCODE_SIMPLIFY_API ReductionResult reduce_pd_code(
173
+ const PDCode& code,
174
+ std::size_t known_crossingless_components = 0,
175
+ const SimplifierOptions& options = SimplifierOptions{},
176
+ int reduction_round = -1);
177
+
143
178
  PDCODE_SIMPLIFY_API std::ostream& operator<<(std::ostream& out, const Endpoint& endpoint);
144
179
 
145
180
  } // namespace pdcode_simplify
@@ -3,6 +3,7 @@
3
3
  #include <cstdlib>
4
4
  #include <cstring>
5
5
  #include <exception>
6
+ #include <iostream>
6
7
  #include <sstream>
7
8
  #include <string>
8
9
  #include <vector>
@@ -55,14 +56,14 @@ void append_component_counts(
55
56
  }
56
57
 
57
58
  std::string result_to_json(
58
- const pdcode_simplify::SimplificationResult& result,
59
+ const pdcode_simplify::ReductionResult& result,
59
60
  const pdcode_simplify::ComponentAnalysis& input_components,
60
- const pdcode_simplify::ComponentAnalysis* after_removal_components,
61
- const pdcode_simplify::PDSimplificationResult& pd_simplification,
62
- const pdcode_simplify::ComponentAnalysis& search_components) {
61
+ const pdcode_simplify::ComponentAnalysis& final_components,
62
+ const pdcode_simplify::ComponentAnalysis* after_removal_components) {
63
63
  std::ostringstream out;
64
64
  out << "{";
65
- out << "\"simplification_found\":" << (result.found ? "true" : "false") << ",";
65
+ out << "\"simplification_found\":"
66
+ << (result.mid_simplification_rounds > 0 ? "true" : "false") << ",";
66
67
  out << "\"input_components\":{";
67
68
  append_component_counts(out, input_components);
68
69
  out << "},";
@@ -71,49 +72,21 @@ std::string result_to_json(
71
72
  append_component_counts(out, *after_removal_components);
72
73
  out << "},";
73
74
  }
74
- out << "\"pd_simplification\":{"
75
- << "\"enabled\":true,"
76
- << "\"reidemeister_i_moves\":" << pd_simplification.reidemeister_i_moves << ","
77
- << "\"nugatory_crossing_moves\":" << pd_simplification.nugatory_crossing_moves << ","
78
- << "\"output_crossings\":" << pd_simplification.code.size()
79
- << "},";
80
- out << "\"search_components\":{";
81
- append_component_counts(out, search_components);
75
+ out << "\"final_pd_code\":\"" << json_escape(pdcode_simplify::format_pd_code(result.code))
76
+ << "\",";
77
+ out << "\"final_crossings\":" << result.code.size() << ",";
78
+ out << "\"final_components\":{";
79
+ append_component_counts(out, final_components);
82
80
  out << "},";
81
+ out << "\"mid_simplification_rounds\":" << result.mid_simplification_rounds << ",";
82
+ out << "\"heuristic_failover_rounds\":" << result.heuristic_failover_rounds << ",";
83
+ out << "\"reidemeister_i_moves\":" << result.reidemeister_i_moves << ",";
84
+ out << "\"nugatory_crossing_moves\":" << result.nugatory_crossing_moves << ",";
83
85
  out << "\"tested_red_paths\":" << result.tested_red_paths << ",";
84
- out << "\"tested_green_paths\":" << result.tested_green_paths;
85
- if (result.found) {
86
- out << ",";
87
- out << "\"direction\":\"" << pdcode_simplify::format_direction(result.direction) << "\",";
88
- out << "\"red_path\":[";
89
- for (std::size_t i = 0; i < result.red_path.size(); ++i) {
90
- if (i != 0) {
91
- out << ",";
92
- }
93
- out << "{\"crossing\":" << result.red_path[i].crossing
94
- << ",\"strand\":" << result.red_path[i].strand << "}";
95
- }
96
- out << "],";
97
- out << "\"green_path\":[";
98
- for (std::size_t i = 0; i < result.green_path.size(); ++i) {
99
- if (i != 0) {
100
- out << ",";
101
- }
102
- out << result.green_path[i];
103
- }
104
- out << "],";
105
- out << "\"green_crossings\":[";
106
- for (std::size_t i = 0; i < result.green_crossings.size(); ++i) {
107
- if (i != 0) {
108
- out << ",";
109
- }
110
- const auto& crossing = result.green_crossings[i];
111
- out << "{\"from_face\":" << crossing.from_face
112
- << ",\"to_face\":" << crossing.to_face
113
- << ",\"strand_level\":\"" << json_escape(crossing.strand_level) << "\"}";
114
- }
115
- out << "]";
116
- }
86
+ out << "\"tested_green_paths\":" << result.tested_green_paths << ",";
87
+ out << "\"last_path_search_mode\":\"" << json_escape(result.last_path_search_mode) << "\",";
88
+ out << "\"stopped_by_round_limit\":"
89
+ << (result.stopped_by_round_limit ? "true" : "false");
117
90
  out << "}";
118
91
  return out.str();
119
92
  }
@@ -137,6 +110,9 @@ __declspec(dllexport)
137
110
  char* pdcode_simplify_run_json(
138
111
  const char* pd_text,
139
112
  int max_paths,
113
+ int ban_heuristic,
114
+ int reduction_round,
115
+ int verbose,
140
116
  unsigned long long known_crossingless_components,
141
117
  const int* removed_crossings,
142
118
  unsigned long long removed_crossing_count) {
@@ -148,6 +124,11 @@ char* pdcode_simplify_run_json(
148
124
  const std::string text(pd_text);
149
125
  pdcode_simplify::SimplifierOptions options;
150
126
  options.max_paths = max_paths;
127
+ options.ban_heuristic = ban_heuristic != 0;
128
+ options.verbose = verbose != 0;
129
+ options.progress = [](const std::string& message) {
130
+ std::cerr << "[pdcode-simplify] " << message << '\n';
131
+ };
151
132
 
152
133
  const pdcode_simplify::PDCode code = pdcode_simplify::parse_pd_code(text);
153
134
  std::size_t crossingless = static_cast<std::size_t>(known_crossingless_components);
@@ -167,17 +148,15 @@ char* pdcode_simplify_run_json(
167
148
  code, removed, crossingless);
168
149
  }
169
150
 
170
- const auto pd_simplification = pdcode_simplify::simplify_pd_code(code, crossingless);
171
- const auto search_components = pdcode_simplify::analyze_components(
172
- pd_simplification.code,
173
- pd_simplification.crossingless_components);
174
- const auto result = pdcode_simplify::find_simplification(pd_simplification.code, options);
151
+ const auto result =
152
+ pdcode_simplify::reduce_pd_code(code, crossingless, options, reduction_round);
153
+ const auto final_components =
154
+ pdcode_simplify::analyze_components(result.code, result.crossingless_components);
175
155
  return copy_string(result_to_json(
176
156
  result,
177
157
  input_components,
178
- has_removal ? &after_removal_components : nullptr,
179
- pd_simplification,
180
- search_components));
158
+ final_components,
159
+ has_removal ? &after_removal_components : nullptr));
181
160
  } catch (const std::exception& error) {
182
161
  return copy_string(std::string("{\"error\":\"") + json_escape(error.what()) + "\"}");
183
162
  } catch (...) {
@@ -7,6 +7,7 @@
7
7
  #include <limits>
8
8
  #include <map>
9
9
  #include <numeric>
10
+ #include <queue>
10
11
  #include <random>
11
12
  #include <set>
12
13
  #include <sstream>
@@ -18,6 +19,11 @@ namespace pdcode_simplify {
18
19
  namespace {
19
20
 
20
21
  constexpr int kBlockedWeight = 10000;
22
+ constexpr int kHeuristicBeamWidth = 8;
23
+ constexpr int kHeuristicMinStateBudget = 128;
24
+ constexpr int kHeuristicMaxStateBudget = 4096;
25
+ constexpr int kHeuristicMinPathBudget = 24;
26
+ constexpr int kHeuristicMaxPathBudget = 384;
21
27
 
22
28
  int positive_mod(int value, int modulus) {
23
29
  int result = value % modulus;
@@ -998,18 +1004,30 @@ void reset_weights(DualGraph& graph) {
998
1004
  }
999
1005
  }
1000
1006
 
1007
+ std::vector<int> heuristic_distances_to_target(
1008
+ const DualGraph& graph,
1009
+ int target,
1010
+ int cutoff);
1011
+
1001
1012
  void collect_simple_paths_dfs(
1002
1013
  const DualGraph& graph,
1003
1014
  int current,
1004
1015
  int target,
1005
1016
  int cutoff,
1006
1017
  int max_paths,
1018
+ int current_weight,
1019
+ const std::vector<int>& distance,
1007
1020
  std::vector<char>& visited,
1008
1021
  std::vector<int>& current_path,
1009
1022
  std::vector<std::vector<int>>& paths) {
1023
+ const int infinity = std::numeric_limits<int>::max() / 4;
1010
1024
  if (static_cast<int>(current_path.size()) - 1 >= cutoff) {
1011
1025
  return;
1012
1026
  }
1027
+ if (current < 0 || current >= static_cast<int>(distance.size()) ||
1028
+ distance[current] == infinity || current_weight + distance[current] >= cutoff) {
1029
+ return;
1030
+ }
1013
1031
 
1014
1032
  for (int edge_index : graph.adjacency[current]) {
1015
1033
  const GraphEdge& edge = graph.edges[edge_index];
@@ -1017,32 +1035,37 @@ void collect_simple_paths_dfs(
1017
1035
  if (visited[next]) {
1018
1036
  continue;
1019
1037
  }
1038
+ const int next_weight = current_weight + edge.weight;
1039
+ if (next_weight >= cutoff) {
1040
+ continue;
1041
+ }
1042
+ if (next < 0 || next >= static_cast<int>(distance.size()) ||
1043
+ distance[next] == infinity || next_weight + distance[next] >= cutoff) {
1044
+ continue;
1045
+ }
1020
1046
 
1021
1047
  current_path.push_back(next);
1022
1048
  visited[next] = true;
1023
1049
 
1024
1050
  if (next == target) {
1025
- int path_weight = 0;
1026
- for (std::size_t i = 0; i + 1 < current_path.size(); ++i) {
1027
- const GraphEdge* path_edge = graph.edge(current_path[i], current_path[i + 1]);
1028
- if (path_edge == nullptr) {
1029
- throw std::logic_error("Missing dual edge while weighing a path");
1030
- }
1031
- path_weight += path_edge->weight;
1032
- if (path_weight >= cutoff) {
1033
- break;
1034
- }
1035
- }
1036
- if (path_weight < cutoff) {
1037
- paths.push_back(current_path);
1038
- }
1051
+ paths.push_back(current_path);
1039
1052
  if (max_paths != -1 && static_cast<int>(paths.size()) > max_paths) {
1040
1053
  visited[next] = false;
1041
1054
  current_path.pop_back();
1042
1055
  return;
1043
1056
  }
1044
1057
  } else {
1045
- collect_simple_paths_dfs(graph, next, target, cutoff, max_paths, visited, current_path, paths);
1058
+ collect_simple_paths_dfs(
1059
+ graph,
1060
+ next,
1061
+ target,
1062
+ cutoff,
1063
+ max_paths,
1064
+ next_weight,
1065
+ distance,
1066
+ visited,
1067
+ current_path,
1068
+ paths);
1046
1069
  if (max_paths != -1 && static_cast<int>(paths.size()) > max_paths) {
1047
1070
  visited[next] = false;
1048
1071
  current_path.pop_back();
@@ -1071,8 +1094,216 @@ std::vector<std::vector<int>> collect_simple_paths(
1071
1094
 
1072
1095
  std::vector<char> visited(graph.faces.size(), false);
1073
1096
  std::vector<int> current_path{source};
1097
+ const std::vector<int> distance = heuristic_distances_to_target(graph, target, cutoff);
1074
1098
  visited[source] = true;
1075
- collect_simple_paths_dfs(graph, source, target, cutoff, max_paths, visited, current_path, paths);
1099
+ collect_simple_paths_dfs(
1100
+ graph,
1101
+ source,
1102
+ target,
1103
+ cutoff,
1104
+ max_paths,
1105
+ 0,
1106
+ distance,
1107
+ visited,
1108
+ current_path,
1109
+ paths);
1110
+ return paths;
1111
+ }
1112
+
1113
+ std::vector<int> heuristic_distances_to_target(
1114
+ const DualGraph& graph,
1115
+ int target,
1116
+ int cutoff) {
1117
+ const int face_count = static_cast<int>(graph.faces.size());
1118
+ const int infinity = std::numeric_limits<int>::max() / 4;
1119
+ std::vector<int> distance(face_count, infinity);
1120
+ std::deque<int> queue;
1121
+ distance[target] = 0;
1122
+ queue.push_back(target);
1123
+
1124
+ while (!queue.empty()) {
1125
+ const int current = queue.front();
1126
+ queue.pop_front();
1127
+ for (int edge_index : graph.adjacency[current]) {
1128
+ const GraphEdge& edge = graph.edges[edge_index];
1129
+ if (edge.weight >= cutoff) {
1130
+ continue;
1131
+ }
1132
+ const int next = edge.u == current ? edge.v : edge.u;
1133
+ if (distance[next] != infinity) {
1134
+ continue;
1135
+ }
1136
+ distance[next] = distance[current] + 1;
1137
+ queue.push_back(next);
1138
+ }
1139
+ }
1140
+ return distance;
1141
+ }
1142
+
1143
+ struct HeuristicState {
1144
+ std::vector<int> path;
1145
+ std::vector<char> visited;
1146
+ int weight = 0;
1147
+ int branch_penalty = 0;
1148
+ int estimated_weight = 0;
1149
+ int estimated_length = 0;
1150
+ int serial = 0;
1151
+ };
1152
+
1153
+ struct HeuristicStateWorse {
1154
+ bool operator()(const HeuristicState& lhs, const HeuristicState& rhs) const {
1155
+ if (lhs.estimated_weight != rhs.estimated_weight) {
1156
+ return lhs.estimated_weight > rhs.estimated_weight;
1157
+ }
1158
+ if (lhs.estimated_length != rhs.estimated_length) {
1159
+ return lhs.estimated_length > rhs.estimated_length;
1160
+ }
1161
+ if (lhs.branch_penalty != rhs.branch_penalty) {
1162
+ return lhs.branch_penalty > rhs.branch_penalty;
1163
+ }
1164
+ if (lhs.weight != rhs.weight) {
1165
+ return lhs.weight > rhs.weight;
1166
+ }
1167
+ if (lhs.path.size() != rhs.path.size()) {
1168
+ return lhs.path.size() > rhs.path.size();
1169
+ }
1170
+ return lhs.serial > rhs.serial;
1171
+ }
1172
+ };
1173
+
1174
+ struct HeuristicStep {
1175
+ int next = -1;
1176
+ int edge_index = -1;
1177
+ int edge_weight = 0;
1178
+ int distance = 0;
1179
+ int degree_penalty = 0;
1180
+ };
1181
+
1182
+ bool heuristic_step_less(const HeuristicStep& lhs, const HeuristicStep& rhs) {
1183
+ if (lhs.edge_weight != rhs.edge_weight) {
1184
+ return lhs.edge_weight < rhs.edge_weight;
1185
+ }
1186
+ if (lhs.distance != rhs.distance) {
1187
+ return lhs.distance < rhs.distance;
1188
+ }
1189
+ if (lhs.degree_penalty != rhs.degree_penalty) {
1190
+ return lhs.degree_penalty < rhs.degree_penalty;
1191
+ }
1192
+ if (lhs.next != rhs.next) {
1193
+ return lhs.next < rhs.next;
1194
+ }
1195
+ return lhs.edge_index < rhs.edge_index;
1196
+ }
1197
+
1198
+ std::vector<std::vector<int>> collect_heuristic_paths(
1199
+ const DualGraph& graph,
1200
+ int source,
1201
+ int target,
1202
+ int cutoff) {
1203
+ std::vector<std::vector<int>> paths;
1204
+ const int face_count = static_cast<int>(graph.faces.size());
1205
+ if (source == target || source < 0 || target < 0 ||
1206
+ source >= face_count || target >= face_count || cutoff <= 0) {
1207
+ return paths;
1208
+ }
1209
+
1210
+ const std::vector<int> distance = heuristic_distances_to_target(graph, target, cutoff);
1211
+ const int infinity = std::numeric_limits<int>::max() / 4;
1212
+ if (distance[source] == infinity || distance[source] >= cutoff) {
1213
+ return paths;
1214
+ }
1215
+
1216
+ const int state_budget = std::max(
1217
+ kHeuristicMinStateBudget,
1218
+ std::min(kHeuristicMaxStateBudget, face_count * std::max(1, cutoff) * 8));
1219
+ const int path_budget = std::max(
1220
+ kHeuristicMinPathBudget,
1221
+ std::min(kHeuristicMaxPathBudget, face_count * 2 + cutoff * 8));
1222
+
1223
+ std::priority_queue<HeuristicState, std::vector<HeuristicState>, HeuristicStateWorse> queue;
1224
+ int serial = 0;
1225
+ HeuristicState initial;
1226
+ initial.path.push_back(source);
1227
+ initial.visited.assign(face_count, false);
1228
+ initial.visited[source] = true;
1229
+ initial.estimated_weight = distance[source];
1230
+ initial.estimated_length = distance[source];
1231
+ initial.serial = serial++;
1232
+ queue.push(initial);
1233
+
1234
+ std::map<long long, int> popped_by_depth_face;
1235
+ int popped_states = 0;
1236
+ while (!queue.empty() && popped_states < state_budget &&
1237
+ static_cast<int>(paths.size()) < path_budget) {
1238
+ HeuristicState state = queue.top();
1239
+ queue.pop();
1240
+ ++popped_states;
1241
+
1242
+ const int current = state.path.back();
1243
+ const int depth = static_cast<int>(state.path.size()) - 1;
1244
+ if (current == target) {
1245
+ if (state.weight < cutoff) {
1246
+ paths.push_back(state.path);
1247
+ }
1248
+ continue;
1249
+ }
1250
+ if (depth >= cutoff - 1) {
1251
+ continue;
1252
+ }
1253
+
1254
+ const long long beam_key =
1255
+ static_cast<long long>(depth) * static_cast<long long>(face_count) + current;
1256
+ int& beam_count = popped_by_depth_face[beam_key];
1257
+ if (beam_count >= kHeuristicBeamWidth) {
1258
+ continue;
1259
+ }
1260
+ ++beam_count;
1261
+
1262
+ std::vector<HeuristicStep> steps;
1263
+ for (int edge_index : graph.adjacency[current]) {
1264
+ const GraphEdge& edge = graph.edges[edge_index];
1265
+ const int next = edge.u == current ? edge.v : edge.u;
1266
+ if (state.visited[next]) {
1267
+ continue;
1268
+ }
1269
+ if (distance[next] == infinity) {
1270
+ continue;
1271
+ }
1272
+ const int new_weight = state.weight + edge.weight;
1273
+ if (new_weight >= cutoff) {
1274
+ continue;
1275
+ }
1276
+ const int new_depth = depth + 1;
1277
+ if (new_depth + distance[next] >= cutoff) {
1278
+ continue;
1279
+ }
1280
+ HeuristicStep step;
1281
+ step.next = next;
1282
+ step.edge_index = edge_index;
1283
+ step.edge_weight = edge.weight;
1284
+ step.distance = distance[next];
1285
+ step.degree_penalty = std::max(0, static_cast<int>(graph.adjacency[next].size()) - 2);
1286
+ steps.push_back(step);
1287
+ }
1288
+ std::sort(steps.begin(), steps.end(), heuristic_step_less);
1289
+
1290
+ for (const HeuristicStep& step : steps) {
1291
+ const GraphEdge& edge = graph.edges[step.edge_index];
1292
+ HeuristicState next_state;
1293
+ next_state.path = state.path;
1294
+ next_state.path.push_back(step.next);
1295
+ next_state.visited = state.visited;
1296
+ next_state.visited[step.next] = true;
1297
+ next_state.weight = state.weight + edge.weight;
1298
+ next_state.branch_penalty = state.branch_penalty + step.degree_penalty;
1299
+ next_state.estimated_weight = next_state.weight + distance[step.next];
1300
+ next_state.estimated_length =
1301
+ static_cast<int>(next_state.path.size()) - 1 + distance[step.next];
1302
+ next_state.serial = serial++;
1303
+ queue.push(std::move(next_state));
1304
+ }
1305
+ }
1306
+
1076
1307
  return paths;
1077
1308
  }
1078
1309
 
@@ -1243,6 +1474,292 @@ bool do_check(
1243
1474
  return true;
1244
1475
  }
1245
1476
 
1477
+ class DisjointSet {
1478
+ public:
1479
+ int find(int value) {
1480
+ auto inserted = parent_.insert(std::make_pair(value, value));
1481
+ int parent = inserted.first->second;
1482
+ if (parent != value) {
1483
+ parent = find(parent);
1484
+ parent_[value] = parent;
1485
+ }
1486
+ return parent;
1487
+ }
1488
+
1489
+ void unite(int first, int second) {
1490
+ int first_root = find(first);
1491
+ int second_root = find(second);
1492
+ if (first_root == second_root) {
1493
+ return;
1494
+ }
1495
+ if (second_root < first_root) {
1496
+ std::swap(first_root, second_root);
1497
+ }
1498
+ parent_[second_root] = first_root;
1499
+ }
1500
+
1501
+ private:
1502
+ std::map<int, int> parent_;
1503
+ };
1504
+
1505
+ std::map<std::pair<int, int>, std::string> green_crossing_levels(
1506
+ const SimplificationResult& result) {
1507
+ std::map<std::pair<int, int>, std::string> levels;
1508
+ for (const GreenCrossing& crossing : result.green_crossings) {
1509
+ levels[std::make_pair(crossing.from_face, crossing.to_face)] = crossing.strand_level;
1510
+ }
1511
+ return levels;
1512
+ }
1513
+
1514
+ void clear_witness(SimplificationResult& result) {
1515
+ result.found = false;
1516
+ result.red_path.clear();
1517
+ result.green_path.clear();
1518
+ result.green_crossings.clear();
1519
+ }
1520
+
1521
+ } // namespace
1522
+
1523
+ MidSimplificationApplyResult apply_simplification_witness(
1524
+ const PDCode& code,
1525
+ const SimplificationResult& result,
1526
+ std::size_t known_crossingless_components) {
1527
+ if (!result.found) {
1528
+ throw std::invalid_argument("Cannot apply a missing simplification witness");
1529
+ }
1530
+ if (result.red_path.size() < 2) {
1531
+ throw std::invalid_argument("Simplification witness red path is too short");
1532
+ }
1533
+
1534
+ Diagram diagram(code);
1535
+ DualGraph graph(diagram);
1536
+ std::set<int> removed_crossings;
1537
+ std::map<int, int> red_entry_by_crossing;
1538
+ for (std::size_t i = 0; i + 1 < result.red_path.size(); ++i) {
1539
+ removed_crossings.insert(result.red_path[i].crossing);
1540
+ red_entry_by_crossing[result.red_path[i].crossing] = result.red_path[i].strand;
1541
+ }
1542
+ if (removed_crossings.size() != result.red_path.size() - 1) {
1543
+ throw std::invalid_argument("Simplification witness repeats a removed red crossing");
1544
+ }
1545
+ if (removed_crossings.count(result.red_path.back().crossing) != 0) {
1546
+ throw std::invalid_argument("Simplification witness ends inside the removed red arc");
1547
+ }
1548
+
1549
+ const std::map<std::pair<int, int>, std::string> levels = green_crossing_levels(result);
1550
+ DisjointSet dsu;
1551
+ const int endpoint_count = static_cast<int>(code.size() * 4);
1552
+ const int new_crossing_count =
1553
+ result.green_path.empty() ? 0 : static_cast<int>(result.green_path.size()) - 1;
1554
+ const int new_base = endpoint_count;
1555
+
1556
+ auto new_node = [&](int crossing_index, int strand) {
1557
+ return new_base + crossing_index * 4 + strand;
1558
+ };
1559
+ auto is_removed_node = [&](int node) {
1560
+ return node < endpoint_count && removed_crossings.count(node / 4) != 0;
1561
+ };
1562
+ auto is_removed_red_node = [&](int node) {
1563
+ if (!is_removed_node(node)) {
1564
+ return false;
1565
+ }
1566
+ const int crossing = node / 4;
1567
+ const int strand = node % 4;
1568
+ const int red_strand = red_entry_by_crossing.at(crossing);
1569
+ return strand == red_strand || strand == positive_mod(red_strand + 2, 4);
1570
+ };
1571
+
1572
+ std::set<int> crossed_labels;
1573
+ struct CrossedEdge {
1574
+ int interface_from = -1;
1575
+ int interface_to = -1;
1576
+ std::string level;
1577
+ };
1578
+ std::vector<CrossedEdge> crossed_edges;
1579
+ crossed_edges.reserve(new_crossing_count);
1580
+ for (int i = 0; i < new_crossing_count; ++i) {
1581
+ const int from_face = result.green_path[i];
1582
+ const int to_face = result.green_path[i + 1];
1583
+ const GraphEdge* edge = graph.edge(from_face, to_face);
1584
+ if (edge == nullptr) {
1585
+ throw std::invalid_argument("Simplification witness green path crosses a missing dual edge");
1586
+ }
1587
+ const int interface_from = graph.interface_for_face(*edge, from_face);
1588
+ const int interface_to = graph.interface_for_face(*edge, to_face);
1589
+ if (is_removed_red_node(interface_from) || is_removed_red_node(interface_to)) {
1590
+ throw std::invalid_argument("Simplification witness crosses an edge removed with the red arc");
1591
+ }
1592
+ const int label = code[interface_from / 4][interface_from % 4];
1593
+ if (!crossed_labels.insert(label).second) {
1594
+ throw std::invalid_argument("Simplification witness crosses the same PD edge more than once");
1595
+ }
1596
+ const auto level = levels.find(std::make_pair(from_face, to_face));
1597
+ if (level == levels.end()) {
1598
+ throw std::invalid_argument("Simplification witness is missing a green crossing level");
1599
+ }
1600
+ CrossedEdge crossed;
1601
+ crossed.interface_from = interface_from;
1602
+ crossed.interface_to = interface_to;
1603
+ crossed.level = level->second;
1604
+ crossed_edges.push_back(std::move(crossed));
1605
+ }
1606
+
1607
+ std::map<int, std::vector<int>> label_endpoints;
1608
+ for (int crossing_index = 0; crossing_index < static_cast<int>(code.size()); ++crossing_index) {
1609
+ for (int strand = 0; strand < 4; ++strand) {
1610
+ label_endpoints[code[crossing_index][strand]].push_back(crossing_index * 4 + strand);
1611
+ }
1612
+ }
1613
+ for (const auto& item : label_endpoints) {
1614
+ if (item.second.size() != 2) {
1615
+ std::ostringstream message;
1616
+ message << "PD label " << item.first << " appears " << item.second.size() << " times";
1617
+ throw std::invalid_argument(message.str());
1618
+ }
1619
+ if (crossed_labels.count(item.first) == 0) {
1620
+ dsu.unite(item.second[0], item.second[1]);
1621
+ }
1622
+ }
1623
+
1624
+ for (const auto& item : red_entry_by_crossing) {
1625
+ const int crossing = item.first;
1626
+ const int strand = item.second;
1627
+ dsu.unite(crossing * 4 + positive_mod(strand + 1, 4),
1628
+ crossing * 4 + positive_mod(strand + 3, 4));
1629
+ }
1630
+
1631
+ int green_anchor = endpoint_key(result.red_path.front());
1632
+ for (int i = 0; i < static_cast<int>(crossed_edges.size()); ++i) {
1633
+ const CrossedEdge& crossed = crossed_edges[i];
1634
+ int existing_from_pos = -1;
1635
+ int existing_to_pos = -1;
1636
+ int green_in_pos = -1;
1637
+ int green_out_pos = -1;
1638
+ if (crossed.level == "over") {
1639
+ existing_from_pos = 0;
1640
+ green_in_pos = 1;
1641
+ existing_to_pos = 2;
1642
+ green_out_pos = 3;
1643
+ } else if (crossed.level == "under") {
1644
+ green_in_pos = 0;
1645
+ existing_to_pos = 1;
1646
+ green_out_pos = 2;
1647
+ existing_from_pos = 3;
1648
+ } else {
1649
+ throw std::invalid_argument("Unknown green crossing strand level: " + crossed.level);
1650
+ }
1651
+
1652
+ dsu.unite(crossed.interface_from, new_node(i, existing_from_pos));
1653
+ dsu.unite(crossed.interface_to, new_node(i, existing_to_pos));
1654
+ dsu.unite(green_anchor, new_node(i, green_in_pos));
1655
+ green_anchor = new_node(i, green_out_pos);
1656
+ }
1657
+ dsu.unite(green_anchor, endpoint_key(result.red_path.back()));
1658
+
1659
+ std::vector<int> active_nodes;
1660
+ active_nodes.reserve(endpoint_count + new_crossing_count * 4);
1661
+ for (int node = 0; node < endpoint_count; ++node) {
1662
+ if (!is_removed_node(node)) {
1663
+ active_nodes.push_back(node);
1664
+ }
1665
+ }
1666
+ for (int crossing = 0; crossing < new_crossing_count; ++crossing) {
1667
+ for (int strand = 0; strand < 4; ++strand) {
1668
+ active_nodes.push_back(new_node(crossing, strand));
1669
+ }
1670
+ }
1671
+
1672
+ std::map<int, std::vector<int>> grouped;
1673
+ for (int node : active_nodes) {
1674
+ grouped[dsu.find(node)].push_back(node);
1675
+ }
1676
+
1677
+ std::vector<std::vector<int>> groups;
1678
+ for (auto& item : grouped) {
1679
+ std::sort(item.second.begin(), item.second.end());
1680
+ groups.push_back(item.second);
1681
+ }
1682
+ std::sort(groups.begin(), groups.end(), [](const std::vector<int>& lhs, const std::vector<int>& rhs) {
1683
+ return lhs.front() < rhs.front();
1684
+ });
1685
+
1686
+ std::map<int, int> label_by_node;
1687
+ int next_label = 0;
1688
+ for (const std::vector<int>& nodes : groups) {
1689
+ if (nodes.size() != 2) {
1690
+ std::ostringstream message;
1691
+ message << "Applied simplification produced a non-PD edge with "
1692
+ << nodes.size() << " active endpoints";
1693
+ throw std::runtime_error(message.str());
1694
+ }
1695
+ for (int node : nodes) {
1696
+ label_by_node[node] = next_label;
1697
+ }
1698
+ ++next_label;
1699
+ }
1700
+
1701
+ PDCode output;
1702
+ output.reserve(code.size() - removed_crossings.size() + static_cast<std::size_t>(new_crossing_count));
1703
+ for (int crossing_index = 0; crossing_index < static_cast<int>(code.size()); ++crossing_index) {
1704
+ if (removed_crossings.count(crossing_index) != 0) {
1705
+ continue;
1706
+ }
1707
+ Crossing crossing{};
1708
+ for (int strand = 0; strand < 4; ++strand) {
1709
+ crossing[strand] = label_by_node.at(crossing_index * 4 + strand);
1710
+ }
1711
+ output.push_back(crossing);
1712
+ }
1713
+ for (int crossing_index = 0; crossing_index < new_crossing_count; ++crossing_index) {
1714
+ Crossing crossing{};
1715
+ for (int strand = 0; strand < 4; ++strand) {
1716
+ crossing[strand] = label_by_node.at(new_node(crossing_index, strand));
1717
+ }
1718
+ output.push_back(crossing);
1719
+ }
1720
+
1721
+ const std::size_t total_components =
1722
+ analyze_components(code, known_crossingless_components).total_components();
1723
+ output = renumber_full_dfs(output);
1724
+ const std::size_t crossing_components = analyze_components(output).components_with_crossings();
1725
+
1726
+ MidSimplificationApplyResult applied;
1727
+ applied.code = std::move(output);
1728
+ applied.crossingless_components =
1729
+ total_components > crossing_components ? total_components - crossing_components : 0;
1730
+ return applied;
1731
+ }
1732
+
1733
+ namespace {
1734
+
1735
+ bool witness_has_applicable_surgery(const PDCode& code, const SimplificationResult& result) {
1736
+ try {
1737
+ (void)apply_simplification_witness(code, result, 0);
1738
+ return true;
1739
+ } catch (const std::exception&) {
1740
+ return false;
1741
+ }
1742
+ }
1743
+
1744
+ void emit_progress(const SimplifierOptions& options, const std::string& message) {
1745
+ if (!options.verbose) {
1746
+ return;
1747
+ }
1748
+ if (options.progress) {
1749
+ options.progress(message);
1750
+ }
1751
+ }
1752
+
1753
+ std::string search_mode_for_options(const SimplifierOptions& options) {
1754
+ if (options.max_paths == -1 && !options.ban_heuristic) {
1755
+ return "heuristic";
1756
+ }
1757
+ if (options.max_paths == -1) {
1758
+ return "bruteforce";
1759
+ }
1760
+ return "bounded";
1761
+ }
1762
+
1246
1763
  } // namespace
1247
1764
 
1248
1765
  PDCode parse_pd_code(const std::string& text) {
@@ -1283,13 +1800,13 @@ PDCode parse_pd_code(const std::string& text) {
1283
1800
 
1284
1801
  std::string format_pd_code(const PDCode& code) {
1285
1802
  std::ostringstream out;
1286
- out << '[';
1803
+ out << "PD[";
1287
1804
  for (std::size_t i = 0; i < code.size(); ++i) {
1288
1805
  if (i != 0) {
1289
- out << ", ";
1806
+ out << ',';
1290
1807
  }
1291
- out << '(' << code[i][0] << ", " << code[i][1] << ", "
1292
- << code[i][2] << ", " << code[i][3] << ')';
1808
+ out << "X[" << code[i][0] << ',' << code[i][1] << ','
1809
+ << code[i][2] << ',' << code[i][3] << ']';
1293
1810
  }
1294
1811
  out << ']';
1295
1812
  return out.str();
@@ -1627,6 +2144,13 @@ SimplificationResult find_simplification(
1627
2144
  const PDCode& code,
1628
2145
  const SimplifierOptions& options) {
1629
2146
  SimplificationResult result;
2147
+ if (options.max_paths == -1 && !options.ban_heuristic) {
2148
+ result.path_search_mode = "heuristic";
2149
+ } else if (options.max_paths == -1) {
2150
+ result.path_search_mode = "bruteforce";
2151
+ } else {
2152
+ result.path_search_mode = "bounded";
2153
+ }
1630
2154
  Diagram diagram(code);
1631
2155
  DualGraph graph(diagram);
1632
2156
  const auto red_lines = possible_red_lines(diagram);
@@ -1657,7 +2181,12 @@ SimplificationResult find_simplification(
1657
2181
  const int cutoff = static_cast<int>(red_path.size()) - 1;
1658
2182
  for (int source : sources) {
1659
2183
  for (int destination : destinations) {
1660
- auto found_paths = collect_simple_paths(graph, source, destination, cutoff, options.max_paths);
2184
+ std::vector<std::vector<int>> found_paths;
2185
+ if (options.max_paths == -1 && !options.ban_heuristic) {
2186
+ found_paths = collect_heuristic_paths(graph, source, destination, cutoff);
2187
+ } else {
2188
+ found_paths = collect_simple_paths(graph, source, destination, cutoff, options.max_paths);
2189
+ }
1661
2190
  paths.insert(paths.end(), found_paths.begin(), found_paths.end());
1662
2191
  if (options.max_paths != -1 && static_cast<int>(paths.size()) > options.max_paths) {
1663
2192
  break;
@@ -1671,10 +2200,16 @@ SimplificationResult find_simplification(
1671
2200
  continue;
1672
2201
  }
1673
2202
  if (do_check(diagram, graph, red_path, green_path, Direction::Left, result)) {
1674
- return result;
2203
+ if (!options.require_applicable || witness_has_applicable_surgery(code, result)) {
2204
+ return result;
2205
+ }
2206
+ clear_witness(result);
1675
2207
  }
1676
2208
  if (do_check(diagram, graph, red_path, green_path, Direction::Right, result)) {
1677
- return result;
2209
+ if (!options.require_applicable || witness_has_applicable_surgery(code, result)) {
2210
+ return result;
2211
+ }
2212
+ clear_witness(result);
1678
2213
  }
1679
2214
  }
1680
2215
  }
@@ -1682,6 +2217,140 @@ SimplificationResult find_simplification(
1682
2217
  return result;
1683
2218
  }
1684
2219
 
2220
+ ReductionResult reduce_pd_code(
2221
+ const PDCode& code,
2222
+ std::size_t known_crossingless_components,
2223
+ const SimplifierOptions& options,
2224
+ int reduction_round) {
2225
+ {
2226
+ std::ostringstream message;
2227
+ message << "start input_crossings=" << code.size()
2228
+ << " known_crossingless_components=" << known_crossingless_components
2229
+ << " reduction_round=" << reduction_round
2230
+ << " max_paths=" << options.max_paths
2231
+ << " heuristic=" << (options.ban_heuristic ? "off" : "on");
2232
+ emit_progress(options, message.str());
2233
+ }
2234
+
2235
+ const PDSimplificationResult prepared = simplify_pd_code(code, known_crossingless_components);
2236
+ ReductionResult output;
2237
+ output.code = prepared.code;
2238
+ output.crossingless_components = prepared.crossingless_components;
2239
+ output.reidemeister_i_moves = prepared.reidemeister_i_moves;
2240
+ output.nugatory_crossing_moves = prepared.nugatory_crossing_moves;
2241
+ {
2242
+ std::ostringstream message;
2243
+ message << "pre_simplify input_crossings=" << code.size()
2244
+ << " output_crossings=" << output.code.size()
2245
+ << " crossingless_components=" << output.crossingless_components
2246
+ << " r1_moves=" << prepared.reidemeister_i_moves
2247
+ << " nugatory_moves=" << prepared.nugatory_crossing_moves;
2248
+ emit_progress(options, message.str());
2249
+ }
2250
+
2251
+ while (reduction_round < 0 || output.mid_simplification_rounds < reduction_round) {
2252
+ SimplifierOptions search_options = options;
2253
+ search_options.require_applicable = true;
2254
+ const int round = output.mid_simplification_rounds + 1;
2255
+ {
2256
+ std::ostringstream message;
2257
+ message << "round " << round
2258
+ << " search_start crossings=" << output.code.size()
2259
+ << " mode=" << search_mode_for_options(search_options);
2260
+ emit_progress(options, message.str());
2261
+ }
2262
+ SimplificationResult search = find_simplification(output.code, search_options);
2263
+ output.tested_red_paths += search.tested_red_paths;
2264
+ output.tested_green_paths += search.tested_green_paths;
2265
+ output.last_path_search_mode = search.path_search_mode;
2266
+ {
2267
+ std::ostringstream message;
2268
+ message << "round " << round
2269
+ << " search_done found=" << (search.found ? "yes" : "no")
2270
+ << " mode=" << search.path_search_mode
2271
+ << " tested_red=" << search.tested_red_paths
2272
+ << " tested_green=" << search.tested_green_paths;
2273
+ emit_progress(options, message.str());
2274
+ }
2275
+
2276
+ if (!search.found && reduction_round < 0 &&
2277
+ options.max_paths == -1 && !options.ban_heuristic) {
2278
+ SimplifierOptions brute_options = options;
2279
+ brute_options.max_paths = -1;
2280
+ brute_options.ban_heuristic = true;
2281
+ brute_options.require_applicable = true;
2282
+ {
2283
+ std::ostringstream message;
2284
+ message << "round " << round
2285
+ << " brute_fallback_start crossings=" << output.code.size();
2286
+ emit_progress(options, message.str());
2287
+ }
2288
+ SimplificationResult brute = find_simplification(output.code, brute_options);
2289
+ output.tested_red_paths += brute.tested_red_paths;
2290
+ output.tested_green_paths += brute.tested_green_paths;
2291
+ output.last_path_search_mode = brute.path_search_mode;
2292
+ {
2293
+ std::ostringstream message;
2294
+ message << "round " << round
2295
+ << " brute_fallback_done found=" << (brute.found ? "yes" : "no")
2296
+ << " tested_red=" << brute.tested_red_paths
2297
+ << " tested_green=" << brute.tested_green_paths;
2298
+ emit_progress(options, message.str());
2299
+ }
2300
+ if (brute.found) {
2301
+ ++output.heuristic_failover_rounds;
2302
+ search = std::move(brute);
2303
+ }
2304
+ }
2305
+
2306
+ if (!search.found) {
2307
+ {
2308
+ std::ostringstream message;
2309
+ message << "round " << round
2310
+ << " stop_no_path crossings=" << output.code.size();
2311
+ emit_progress(options, message.str());
2312
+ }
2313
+ break;
2314
+ }
2315
+
2316
+ const std::size_t before_apply_crossings = output.code.size();
2317
+ const MidSimplificationApplyResult applied =
2318
+ apply_simplification_witness(output.code, search, output.crossingless_components);
2319
+ ++output.mid_simplification_rounds;
2320
+ const PDSimplificationResult simplified =
2321
+ simplify_pd_code(applied.code, applied.crossingless_components);
2322
+ output.code = simplified.code;
2323
+ output.crossingless_components = simplified.crossingless_components;
2324
+ output.reidemeister_i_moves += simplified.reidemeister_i_moves;
2325
+ output.nugatory_crossing_moves += simplified.nugatory_crossing_moves;
2326
+ {
2327
+ std::ostringstream message;
2328
+ message << "round " << round
2329
+ << " applied crossings=" << before_apply_crossings
2330
+ << " -> " << applied.code.size()
2331
+ << " -> " << output.code.size()
2332
+ << " crossingless_components=" << output.crossingless_components
2333
+ << " r1_moves=" << simplified.reidemeister_i_moves
2334
+ << " nugatory_moves=" << simplified.nugatory_crossing_moves;
2335
+ emit_progress(options, message.str());
2336
+ }
2337
+ }
2338
+
2339
+ output.stopped_by_round_limit =
2340
+ reduction_round >= 0 && output.mid_simplification_rounds >= reduction_round;
2341
+ {
2342
+ std::ostringstream message;
2343
+ message << "done final_crossings=" << output.code.size()
2344
+ << " crossingless_components=" << output.crossingless_components
2345
+ << " mid_rounds=" << output.mid_simplification_rounds
2346
+ << " heuristic_failover_rounds=" << output.heuristic_failover_rounds
2347
+ << " stopped_by_round_limit="
2348
+ << (output.stopped_by_round_limit ? "yes" : "no");
2349
+ emit_progress(options, message.str());
2350
+ }
2351
+ return output;
2352
+ }
2353
+
1685
2354
  std::ostream& operator<<(std::ostream& out, const Endpoint& endpoint) {
1686
2355
  out << format_endpoint(endpoint);
1687
2356
  return out;
@@ -87,6 +87,72 @@ def normalize_pd_codes(pd_codes: PdManyInput) -> list[str]:
87
87
  return [normalize_pd_code(pd_code) for pd_code in pd_codes]
88
88
 
89
89
 
90
+ def _label_for_block(text: str, block_start: int, label_prefix: str, index: int) -> str:
91
+ line_start = text.rfind("\n", 0, block_start)
92
+ line_start = 0 if line_start == -1 else line_start + 1
93
+ before_block = text[line_start:block_start]
94
+ if ":" in before_block:
95
+ line_label = before_block.split(":", 1)[0].strip()
96
+ if line_label:
97
+ return f"{label_prefix}:{line_label}"
98
+ if index == 0:
99
+ return label_prefix
100
+ return f"{label_prefix}#{index + 1}"
101
+
102
+
103
+ def _pd_file_jobs(path: str) -> list[tuple[str, str]]:
104
+ text = pathlib.Path(path).read_text(encoding="utf-8")
105
+ jobs: list[tuple[str, str]] = []
106
+ position = 0
107
+ index = 0
108
+
109
+ while True:
110
+ start = text.find("PD[", position)
111
+ if start == -1:
112
+ break
113
+ depth = 0
114
+ end = -1
115
+ for cursor in range(start + 2, len(text)):
116
+ if text[cursor] == "[":
117
+ depth += 1
118
+ elif text[cursor] == "]":
119
+ depth -= 1
120
+ if depth == 0:
121
+ end = cursor
122
+ break
123
+ if end == -1:
124
+ jobs.append((f"{path}#{index + 1}", text[start:].strip()))
125
+ break
126
+ jobs.append((
127
+ _label_for_block(text, start, path, index),
128
+ text[start : end + 1],
129
+ ))
130
+ index += 1
131
+ position = end + 1
132
+
133
+ if jobs:
134
+ if len(jobs) == 1:
135
+ jobs[0] = (path, jobs[0][1])
136
+ return jobs
137
+
138
+ for line in text.splitlines():
139
+ cleaned = line.strip()
140
+ if not cleaned or cleaned.startswith("#"):
141
+ continue
142
+ label = path
143
+ payload = cleaned
144
+ if ":" in cleaned:
145
+ line_label, payload = cleaned.split(":", 1)
146
+ line_label = line_label.strip()
147
+ payload = payload.strip()
148
+ if line_label:
149
+ label = f"{path}:{line_label}"
150
+ elif jobs:
151
+ label = f"{path}#{len(jobs) + 1}"
152
+ jobs.append((label, payload))
153
+ return jobs
154
+
155
+
90
156
  @contextlib.contextmanager
91
157
  def _resource_paths():
92
158
  package = "cpp_pd_code_simplify_interface"
@@ -335,6 +401,9 @@ def _load_library() -> ctypes.CDLL:
335
401
  library.pdcode_simplify_run_json.argtypes = [
336
402
  ctypes.c_char_p,
337
403
  ctypes.c_int,
404
+ ctypes.c_int,
405
+ ctypes.c_int,
406
+ ctypes.c_int,
338
407
  ctypes.c_ulonglong,
339
408
  ctypes.POINTER(ctypes.c_int),
340
409
  ctypes.c_ulonglong,
@@ -350,10 +419,15 @@ def _load_library() -> ctypes.CDLL:
350
419
  def _run_one(
351
420
  pd_text: str,
352
421
  *,
353
- max_paths: int = 100,
422
+ max_paths: int = -1,
423
+ ban_heuristic: bool = False,
424
+ reduction_round: int = -1,
425
+ verbose: bool = False,
354
426
  known_crossingless_components: int = 0,
355
427
  remove_crossings: Optional[Sequence[int]] = None,
356
428
  ) -> dict[str, Any]:
429
+ if reduction_round < -1:
430
+ raise ValueError("reduction_round must be -1 or a non-negative integer")
357
431
  library = _load_library()
358
432
  removed_count = 0 if remove_crossings is None else len(remove_crossings)
359
433
  removed_array = None
@@ -363,6 +437,9 @@ def _run_one(
363
437
  pointer = library.pdcode_simplify_run_json(
364
438
  pd_text.encode("utf-8"),
365
439
  int(max_paths),
440
+ 1 if ban_heuristic else 0,
441
+ int(reduction_round),
442
+ 1 if verbose else 0,
366
443
  int(known_crossingless_components),
367
444
  removed_array,
368
445
  int(removed_count),
@@ -386,7 +463,10 @@ def _run_one(
386
463
  def simplify(
387
464
  pd_code: PdInput,
388
465
  *,
389
- max_paths: int = 100,
466
+ max_paths: int = -1,
467
+ ban_heuristic: bool = False,
468
+ reduction_round: int = -1,
469
+ verbose: bool = False,
390
470
  known_crossingless_components: int = 0,
391
471
  remove_crossings: Optional[Sequence[int]] = None,
392
472
  ) -> dict[str, Any]:
@@ -395,6 +475,9 @@ def simplify(
395
475
  return _run_one(
396
476
  normalize_pd_code(pd_code),
397
477
  max_paths=max_paths,
478
+ ban_heuristic=ban_heuristic,
479
+ reduction_round=reduction_round,
480
+ verbose=verbose,
398
481
  known_crossingless_components=known_crossingless_components,
399
482
  remove_crossings=remove_crossings,
400
483
  )
@@ -403,7 +486,10 @@ def simplify(
403
486
  def simplify_many(
404
487
  pd_codes: PdManyInput,
405
488
  *,
406
- max_paths: int = 100,
489
+ max_paths: int = -1,
490
+ ban_heuristic: bool = False,
491
+ reduction_round: int = -1,
492
+ verbose: bool = False,
407
493
  known_crossingless_components: int = 0,
408
494
  remove_crossings: Optional[Sequence[int]] = None,
409
495
  ) -> list[dict[str, Any]]:
@@ -413,6 +499,9 @@ def simplify_many(
413
499
  _run_one(
414
500
  pd_text,
415
501
  max_paths=max_paths,
502
+ ban_heuristic=ban_heuristic,
503
+ reduction_round=reduction_round,
504
+ verbose=verbose,
416
505
  known_crossingless_components=known_crossingless_components,
417
506
  remove_crossings=remove_crossings,
418
507
  )
@@ -423,47 +512,61 @@ def simplify_many(
423
512
  def main(argv: Optional[Sequence[str]] = None) -> int:
424
513
  parser = argparse.ArgumentParser(description="Run cpp-pd-code-simplify through the Python interface.")
425
514
  parser.add_argument("pd_code", nargs="?", help="PD code as PD[...] text or a Python-style list of crossings.")
515
+ parser.add_argument("--pd-code", "-c", dest="pd_code_option", help="literal PD[...] string")
426
516
  parser.add_argument("--pd-file", "-f", help="read one file containing one or more labelled PD-code lines")
427
- parser.add_argument("--max-paths", type=int, default=100)
517
+ parser.add_argument("--max-paths", type=int, default=-1)
518
+ parser.add_argument("--ban-heuristic", action="store_true")
519
+ parser.add_argument("--reduction-round", type=int, default=-1)
520
+ parser.add_argument("--verbose", action="store_true")
428
521
  parser.add_argument("--known-crossingless-components", type=int, default=0)
429
522
  parser.add_argument("--remove-crossings", help="comma-separated zero-based crossing indices")
430
523
  args = parser.parse_args(argv)
431
- if args.pd_file and args.pd_code:
432
- parser.error("pass either a positional PD code or --pd-file, not both")
433
- if not args.pd_file and not args.pd_code:
434
- parser.error("a positional PD code or --pd-file is required")
524
+ if args.reduction_round < -1:
525
+ parser.error("--reduction-round must be -1 or a non-negative integer")
526
+ if args.pd_code and args.pd_code_option:
527
+ parser.error("pass either a positional PD code or --pd-code, not both")
528
+ pd_code_text = args.pd_code_option or args.pd_code
529
+ if args.pd_file and pd_code_text:
530
+ parser.error("pass either a PD code or --pd-file, not both")
531
+ if not args.pd_file and not pd_code_text:
532
+ parser.error("a PD code or --pd-file is required")
435
533
  remove_crossings = None
436
534
  if args.remove_crossings:
437
535
  remove_crossings = [int(token) for token in re.findall(r"-?\d+", args.remove_crossings)]
438
536
 
439
537
  exit_code = 0
440
538
  if args.pd_file:
441
- lines = []
442
- for line in pathlib.Path(args.pd_file).read_text(encoding="utf-8").splitlines():
443
- cleaned = line.strip()
444
- if not cleaned or cleaned.startswith("#"):
445
- continue
446
- payload = cleaned.split(":", 1)[1].strip() if ":" in cleaned else cleaned
447
- lines.append(payload)
539
+ jobs = _pd_file_jobs(args.pd_file)
448
540
  batch_payload = []
449
- for line in lines:
541
+ show_labels = len(jobs) > 1
542
+ for label, line in jobs:
450
543
  try:
451
- batch_payload.append(
452
- simplify(
453
- line,
454
- max_paths=args.max_paths,
455
- known_crossingless_components=args.known_crossingless_components,
456
- remove_crossings=remove_crossings,
457
- )
544
+ item = simplify(
545
+ line,
546
+ max_paths=args.max_paths,
547
+ ban_heuristic=args.ban_heuristic,
548
+ reduction_round=args.reduction_round,
549
+ verbose=args.verbose,
550
+ known_crossingless_components=args.known_crossingless_components,
551
+ remove_crossings=remove_crossings,
458
552
  )
553
+ if show_labels:
554
+ item = {"label": label, **item}
555
+ batch_payload.append(item)
459
556
  except Exception as exc:
460
557
  exit_code = 2
461
- batch_payload.append({"error": str(exc)})
558
+ item = {"error": str(exc)}
559
+ if show_labels:
560
+ item = {"label": label, **item}
561
+ batch_payload.append(item)
462
562
  payload: Any = batch_payload
463
563
  else:
464
564
  payload = simplify(
465
- args.pd_code or "",
565
+ pd_code_text or "",
466
566
  max_paths=args.max_paths,
567
+ ban_heuristic=args.ban_heuristic,
568
+ reduction_round=args.reduction_round,
569
+ verbose=args.verbose,
467
570
  known_crossingless_components=args.known_crossingless_components,
468
571
  remove_crossings=remove_crossings,
469
572
  )
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cpp-pd-code-simplify-interface"
3
- version = "0.1.1"
3
+ version = "0.1.3"
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"}