cpp-pd-code-simplify-interface 0.1.2__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.2
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,12 +44,14 @@ 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
50
  The default `max_paths=-1` uses deterministic heuristic green-path sampling in
51
51
  the C++ backend. Use `ban_heuristic=True` to request exhaustive green-path
52
- enumeration for a manageable input.
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.
53
55
 
54
56
  Batch use:
55
57
 
@@ -76,7 +78,7 @@ Python process needs a 64-bit compiler target.
76
78
  Command-line use also supports multi-line PD-code files:
77
79
 
78
80
  ```sh
79
- python -m cpp_pd_code_simplify_interface --pd-file inputs.pd --max-paths -1
81
+ python -m cpp_pd_code_simplify_interface --pd-file inputs.pd --max-paths -1 --verbose
80
82
  ```
81
83
 
82
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,12 +24,14 @@ 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
30
  The default `max_paths=-1` uses deterministic heuristic green-path sampling in
31
31
  the C++ backend. Use `ban_heuristic=True` to request exhaustive green-path
32
- enumeration for a manageable input.
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.
33
35
 
34
36
  Batch use:
35
37
 
@@ -56,7 +58,7 @@ Python process needs a 64-bit compiler target.
56
58
  Command-line use also supports multi-line PD-code files:
57
59
 
58
60
  ```sh
59
- python -m cpp_pd_code_simplify_interface --pd-file inputs.pd --max-paths -1
61
+ python -m cpp_pd_code_simplify_interface --pd-file inputs.pd --max-paths -1 --verbose
60
62
  ```
61
63
 
62
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>
@@ -50,6 +51,9 @@ struct GreenCrossing {
50
51
  struct SimplifierOptions {
51
52
  int max_paths = -1;
52
53
  bool ban_heuristic = false;
54
+ bool require_applicable = false;
55
+ bool verbose = false;
56
+ std::function<void(const std::string&)> progress;
53
57
  };
54
58
 
55
59
  struct LinkComponentSummary {
@@ -107,6 +111,24 @@ struct SimplificationResult {
107
111
  std::size_t tested_green_paths = 0;
108
112
  };
109
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
+
110
132
  PDCODE_SIMPLIFY_API PDCode parse_pd_code(const std::string& text);
111
133
  PDCODE_SIMPLIFY_API std::string format_pd_code(const PDCode& code);
112
134
  PDCODE_SIMPLIFY_API std::string format_endpoint(const Endpoint& endpoint);
@@ -142,6 +164,17 @@ PDCODE_SIMPLIFY_API SimplificationResult find_simplification(
142
164
  const PDCode& code,
143
165
  const SimplifierOptions& options = SimplifierOptions{});
144
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
+
145
178
  PDCODE_SIMPLIFY_API std::ostream& operator<<(std::ostream& out, const Endpoint& endpoint);
146
179
 
147
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,50 +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
86
  out << "\"tested_green_paths\":" << result.tested_green_paths << ",";
85
- out << "\"path_search_mode\":\"" << json_escape(result.path_search_mode) << "\"";
86
- if (result.found) {
87
- out << ",";
88
- out << "\"direction\":\"" << pdcode_simplify::format_direction(result.direction) << "\",";
89
- out << "\"red_path\":[";
90
- for (std::size_t i = 0; i < result.red_path.size(); ++i) {
91
- if (i != 0) {
92
- out << ",";
93
- }
94
- out << "{\"crossing\":" << result.red_path[i].crossing
95
- << ",\"strand\":" << result.red_path[i].strand << "}";
96
- }
97
- out << "],";
98
- out << "\"green_path\":[";
99
- for (std::size_t i = 0; i < result.green_path.size(); ++i) {
100
- if (i != 0) {
101
- out << ",";
102
- }
103
- out << result.green_path[i];
104
- }
105
- out << "],";
106
- out << "\"green_crossings\":[";
107
- for (std::size_t i = 0; i < result.green_crossings.size(); ++i) {
108
- if (i != 0) {
109
- out << ",";
110
- }
111
- const auto& crossing = result.green_crossings[i];
112
- out << "{\"from_face\":" << crossing.from_face
113
- << ",\"to_face\":" << crossing.to_face
114
- << ",\"strand_level\":\"" << json_escape(crossing.strand_level) << "\"}";
115
- }
116
- out << "]";
117
- }
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");
118
90
  out << "}";
119
91
  return out.str();
120
92
  }
@@ -139,6 +111,8 @@ char* pdcode_simplify_run_json(
139
111
  const char* pd_text,
140
112
  int max_paths,
141
113
  int ban_heuristic,
114
+ int reduction_round,
115
+ int verbose,
142
116
  unsigned long long known_crossingless_components,
143
117
  const int* removed_crossings,
144
118
  unsigned long long removed_crossing_count) {
@@ -151,6 +125,10 @@ char* pdcode_simplify_run_json(
151
125
  pdcode_simplify::SimplifierOptions options;
152
126
  options.max_paths = max_paths;
153
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
+ };
154
132
 
155
133
  const pdcode_simplify::PDCode code = pdcode_simplify::parse_pd_code(text);
156
134
  std::size_t crossingless = static_cast<std::size_t>(known_crossingless_components);
@@ -170,17 +148,15 @@ char* pdcode_simplify_run_json(
170
148
  code, removed, crossingless);
171
149
  }
172
150
 
173
- const auto pd_simplification = pdcode_simplify::simplify_pd_code(code, crossingless);
174
- const auto search_components = pdcode_simplify::analyze_components(
175
- pd_simplification.code,
176
- pd_simplification.crossingless_components);
177
- 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);
178
155
  return copy_string(result_to_json(
179
156
  result,
180
157
  input_components,
181
- has_removal ? &after_removal_components : nullptr,
182
- pd_simplification,
183
- search_components));
158
+ final_components,
159
+ has_removal ? &after_removal_components : nullptr));
184
160
  } catch (const std::exception& error) {
185
161
  return copy_string(std::string("{\"error\":\"") + json_escape(error.what()) + "\"}");
186
162
  } catch (...) {
@@ -1004,18 +1004,30 @@ void reset_weights(DualGraph& graph) {
1004
1004
  }
1005
1005
  }
1006
1006
 
1007
+ std::vector<int> heuristic_distances_to_target(
1008
+ const DualGraph& graph,
1009
+ int target,
1010
+ int cutoff);
1011
+
1007
1012
  void collect_simple_paths_dfs(
1008
1013
  const DualGraph& graph,
1009
1014
  int current,
1010
1015
  int target,
1011
1016
  int cutoff,
1012
1017
  int max_paths,
1018
+ int current_weight,
1019
+ const std::vector<int>& distance,
1013
1020
  std::vector<char>& visited,
1014
1021
  std::vector<int>& current_path,
1015
1022
  std::vector<std::vector<int>>& paths) {
1023
+ const int infinity = std::numeric_limits<int>::max() / 4;
1016
1024
  if (static_cast<int>(current_path.size()) - 1 >= cutoff) {
1017
1025
  return;
1018
1026
  }
1027
+ if (current < 0 || current >= static_cast<int>(distance.size()) ||
1028
+ distance[current] == infinity || current_weight + distance[current] >= cutoff) {
1029
+ return;
1030
+ }
1019
1031
 
1020
1032
  for (int edge_index : graph.adjacency[current]) {
1021
1033
  const GraphEdge& edge = graph.edges[edge_index];
@@ -1023,32 +1035,37 @@ void collect_simple_paths_dfs(
1023
1035
  if (visited[next]) {
1024
1036
  continue;
1025
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
+ }
1026
1046
 
1027
1047
  current_path.push_back(next);
1028
1048
  visited[next] = true;
1029
1049
 
1030
1050
  if (next == target) {
1031
- int path_weight = 0;
1032
- for (std::size_t i = 0; i + 1 < current_path.size(); ++i) {
1033
- const GraphEdge* path_edge = graph.edge(current_path[i], current_path[i + 1]);
1034
- if (path_edge == nullptr) {
1035
- throw std::logic_error("Missing dual edge while weighing a path");
1036
- }
1037
- path_weight += path_edge->weight;
1038
- if (path_weight >= cutoff) {
1039
- break;
1040
- }
1041
- }
1042
- if (path_weight < cutoff) {
1043
- paths.push_back(current_path);
1044
- }
1051
+ paths.push_back(current_path);
1045
1052
  if (max_paths != -1 && static_cast<int>(paths.size()) > max_paths) {
1046
1053
  visited[next] = false;
1047
1054
  current_path.pop_back();
1048
1055
  return;
1049
1056
  }
1050
1057
  } else {
1051
- 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);
1052
1069
  if (max_paths != -1 && static_cast<int>(paths.size()) > max_paths) {
1053
1070
  visited[next] = false;
1054
1071
  current_path.pop_back();
@@ -1077,8 +1094,19 @@ std::vector<std::vector<int>> collect_simple_paths(
1077
1094
 
1078
1095
  std::vector<char> visited(graph.faces.size(), false);
1079
1096
  std::vector<int> current_path{source};
1097
+ const std::vector<int> distance = heuristic_distances_to_target(graph, target, cutoff);
1080
1098
  visited[source] = true;
1081
- 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);
1082
1110
  return paths;
1083
1111
  }
1084
1112
 
@@ -1446,6 +1474,292 @@ bool do_check(
1446
1474
  return true;
1447
1475
  }
1448
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
+
1449
1763
  } // namespace
1450
1764
 
1451
1765
  PDCode parse_pd_code(const std::string& text) {
@@ -1486,13 +1800,13 @@ PDCode parse_pd_code(const std::string& text) {
1486
1800
 
1487
1801
  std::string format_pd_code(const PDCode& code) {
1488
1802
  std::ostringstream out;
1489
- out << '[';
1803
+ out << "PD[";
1490
1804
  for (std::size_t i = 0; i < code.size(); ++i) {
1491
1805
  if (i != 0) {
1492
- out << ", ";
1806
+ out << ',';
1493
1807
  }
1494
- out << '(' << code[i][0] << ", " << code[i][1] << ", "
1495
- << code[i][2] << ", " << code[i][3] << ')';
1808
+ out << "X[" << code[i][0] << ',' << code[i][1] << ','
1809
+ << code[i][2] << ',' << code[i][3] << ']';
1496
1810
  }
1497
1811
  out << ']';
1498
1812
  return out.str();
@@ -1886,10 +2200,16 @@ SimplificationResult find_simplification(
1886
2200
  continue;
1887
2201
  }
1888
2202
  if (do_check(diagram, graph, red_path, green_path, Direction::Left, result)) {
1889
- return result;
2203
+ if (!options.require_applicable || witness_has_applicable_surgery(code, result)) {
2204
+ return result;
2205
+ }
2206
+ clear_witness(result);
1890
2207
  }
1891
2208
  if (do_check(diagram, graph, red_path, green_path, Direction::Right, result)) {
1892
- return result;
2209
+ if (!options.require_applicable || witness_has_applicable_surgery(code, result)) {
2210
+ return result;
2211
+ }
2212
+ clear_witness(result);
1893
2213
  }
1894
2214
  }
1895
2215
  }
@@ -1897,6 +2217,140 @@ SimplificationResult find_simplification(
1897
2217
  return result;
1898
2218
  }
1899
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
+
1900
2354
  std::ostream& operator<<(std::ostream& out, const Endpoint& endpoint) {
1901
2355
  out << format_endpoint(endpoint);
1902
2356
  return out;
@@ -402,6 +402,8 @@ def _load_library() -> ctypes.CDLL:
402
402
  ctypes.c_char_p,
403
403
  ctypes.c_int,
404
404
  ctypes.c_int,
405
+ ctypes.c_int,
406
+ ctypes.c_int,
405
407
  ctypes.c_ulonglong,
406
408
  ctypes.POINTER(ctypes.c_int),
407
409
  ctypes.c_ulonglong,
@@ -419,9 +421,13 @@ def _run_one(
419
421
  *,
420
422
  max_paths: int = -1,
421
423
  ban_heuristic: bool = False,
424
+ reduction_round: int = -1,
425
+ verbose: bool = False,
422
426
  known_crossingless_components: int = 0,
423
427
  remove_crossings: Optional[Sequence[int]] = None,
424
428
  ) -> dict[str, Any]:
429
+ if reduction_round < -1:
430
+ raise ValueError("reduction_round must be -1 or a non-negative integer")
425
431
  library = _load_library()
426
432
  removed_count = 0 if remove_crossings is None else len(remove_crossings)
427
433
  removed_array = None
@@ -432,6 +438,8 @@ def _run_one(
432
438
  pd_text.encode("utf-8"),
433
439
  int(max_paths),
434
440
  1 if ban_heuristic else 0,
441
+ int(reduction_round),
442
+ 1 if verbose else 0,
435
443
  int(known_crossingless_components),
436
444
  removed_array,
437
445
  int(removed_count),
@@ -457,6 +465,8 @@ def simplify(
457
465
  *,
458
466
  max_paths: int = -1,
459
467
  ban_heuristic: bool = False,
468
+ reduction_round: int = -1,
469
+ verbose: bool = False,
460
470
  known_crossingless_components: int = 0,
461
471
  remove_crossings: Optional[Sequence[int]] = None,
462
472
  ) -> dict[str, Any]:
@@ -466,6 +476,8 @@ def simplify(
466
476
  normalize_pd_code(pd_code),
467
477
  max_paths=max_paths,
468
478
  ban_heuristic=ban_heuristic,
479
+ reduction_round=reduction_round,
480
+ verbose=verbose,
469
481
  known_crossingless_components=known_crossingless_components,
470
482
  remove_crossings=remove_crossings,
471
483
  )
@@ -476,6 +488,8 @@ def simplify_many(
476
488
  *,
477
489
  max_paths: int = -1,
478
490
  ban_heuristic: bool = False,
491
+ reduction_round: int = -1,
492
+ verbose: bool = False,
479
493
  known_crossingless_components: int = 0,
480
494
  remove_crossings: Optional[Sequence[int]] = None,
481
495
  ) -> list[dict[str, Any]]:
@@ -486,6 +500,8 @@ def simplify_many(
486
500
  pd_text,
487
501
  max_paths=max_paths,
488
502
  ban_heuristic=ban_heuristic,
503
+ reduction_round=reduction_round,
504
+ verbose=verbose,
489
505
  known_crossingless_components=known_crossingless_components,
490
506
  remove_crossings=remove_crossings,
491
507
  )
@@ -496,16 +512,24 @@ def simplify_many(
496
512
  def main(argv: Optional[Sequence[str]] = None) -> int:
497
513
  parser = argparse.ArgumentParser(description="Run cpp-pd-code-simplify through the Python interface.")
498
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")
499
516
  parser.add_argument("--pd-file", "-f", help="read one file containing one or more labelled PD-code lines")
500
517
  parser.add_argument("--max-paths", type=int, default=-1)
501
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")
502
521
  parser.add_argument("--known-crossingless-components", type=int, default=0)
503
522
  parser.add_argument("--remove-crossings", help="comma-separated zero-based crossing indices")
504
523
  args = parser.parse_args(argv)
505
- if args.pd_file and args.pd_code:
506
- parser.error("pass either a positional PD code or --pd-file, not both")
507
- if not args.pd_file and not args.pd_code:
508
- 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")
509
533
  remove_crossings = None
510
534
  if args.remove_crossings:
511
535
  remove_crossings = [int(token) for token in re.findall(r"-?\d+", args.remove_crossings)]
@@ -521,6 +545,8 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
521
545
  line,
522
546
  max_paths=args.max_paths,
523
547
  ban_heuristic=args.ban_heuristic,
548
+ reduction_round=args.reduction_round,
549
+ verbose=args.verbose,
524
550
  known_crossingless_components=args.known_crossingless_components,
525
551
  remove_crossings=remove_crossings,
526
552
  )
@@ -536,9 +562,11 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
536
562
  payload: Any = batch_payload
537
563
  else:
538
564
  payload = simplify(
539
- args.pd_code or "",
565
+ pd_code_text or "",
540
566
  max_paths=args.max_paths,
541
567
  ban_heuristic=args.ban_heuristic,
568
+ reduction_round=args.reduction_round,
569
+ verbose=args.verbose,
542
570
  known_crossingless_components=args.known_crossingless_components,
543
571
  remove_crossings=remove_crossings,
544
572
  )
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cpp-pd-code-simplify-interface"
3
- version = "0.1.2"
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"}