cpp-pd-code-simplify-interface 0.1.1__tar.gz → 0.1.2__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.2
4
4
  Summary: Python interface for cpp-pd-code-simplify with runtime C++ compilation.
5
5
  License-Expression: MIT
6
6
  Author: GGN_2015
@@ -47,6 +47,10 @@ result = simplify.simplify(pd_code)
47
47
  print(result["simplification_found"])
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.
53
+
50
54
  Batch use:
51
55
 
52
56
  ```python
@@ -72,7 +76,7 @@ Python process needs a 64-bit compiler target.
72
76
  Command-line use also supports multi-line PD-code files:
73
77
 
74
78
  ```sh
75
- python -m cpp_pd_code_simplify_interface --pd-file inputs.pd --max-paths 100
79
+ python -m cpp_pd_code_simplify_interface --pd-file inputs.pd --max-paths -1
76
80
  ```
77
81
 
78
82
  ## Build And Publish
@@ -27,6 +27,10 @@ result = simplify.simplify(pd_code)
27
27
  print(result["simplification_found"])
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.
33
+
30
34
  Batch use:
31
35
 
32
36
  ```python
@@ -52,7 +56,7 @@ Python process needs a 64-bit compiler target.
52
56
  Command-line use also supports multi-line PD-code files:
53
57
 
54
58
  ```sh
55
- python -m cpp_pd_code_simplify_interface --pd-file inputs.pd --max-paths 100
59
+ python -m cpp_pd_code_simplify_interface --pd-file inputs.pd --max-paths -1
56
60
  ```
57
61
 
58
62
  ## Build And Publish
@@ -48,7 +48,8 @@ struct GreenCrossing {
48
48
  };
49
49
 
50
50
  struct SimplifierOptions {
51
- int max_paths = 100;
51
+ int max_paths = -1;
52
+ bool ban_heuristic = false;
52
53
  };
53
54
 
54
55
  struct LinkComponentSummary {
@@ -98,6 +99,7 @@ struct PDSimplificationResult {
98
99
  struct SimplificationResult {
99
100
  bool found = false;
100
101
  Direction direction = Direction::Left;
102
+ std::string path_search_mode;
101
103
  std::vector<Endpoint> red_path;
102
104
  std::vector<int> green_path;
103
105
  std::vector<GreenCrossing> green_crossings;
@@ -81,7 +81,8 @@ std::string result_to_json(
81
81
  append_component_counts(out, search_components);
82
82
  out << "},";
83
83
  out << "\"tested_red_paths\":" << result.tested_red_paths << ",";
84
- out << "\"tested_green_paths\":" << result.tested_green_paths;
84
+ out << "\"tested_green_paths\":" << result.tested_green_paths << ",";
85
+ out << "\"path_search_mode\":\"" << json_escape(result.path_search_mode) << "\"";
85
86
  if (result.found) {
86
87
  out << ",";
87
88
  out << "\"direction\":\"" << pdcode_simplify::format_direction(result.direction) << "\",";
@@ -137,6 +138,7 @@ __declspec(dllexport)
137
138
  char* pdcode_simplify_run_json(
138
139
  const char* pd_text,
139
140
  int max_paths,
141
+ int ban_heuristic,
140
142
  unsigned long long known_crossingless_components,
141
143
  const int* removed_crossings,
142
144
  unsigned long long removed_crossing_count) {
@@ -148,6 +150,7 @@ char* pdcode_simplify_run_json(
148
150
  const std::string text(pd_text);
149
151
  pdcode_simplify::SimplifierOptions options;
150
152
  options.max_paths = max_paths;
153
+ options.ban_heuristic = ban_heuristic != 0;
151
154
 
152
155
  const pdcode_simplify::PDCode code = pdcode_simplify::parse_pd_code(text);
153
156
  std::size_t crossingless = static_cast<std::size_t>(known_crossingless_components);
@@ -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;
@@ -1076,6 +1082,203 @@ std::vector<std::vector<int>> collect_simple_paths(
1076
1082
  return paths;
1077
1083
  }
1078
1084
 
1085
+ std::vector<int> heuristic_distances_to_target(
1086
+ const DualGraph& graph,
1087
+ int target,
1088
+ int cutoff) {
1089
+ const int face_count = static_cast<int>(graph.faces.size());
1090
+ const int infinity = std::numeric_limits<int>::max() / 4;
1091
+ std::vector<int> distance(face_count, infinity);
1092
+ std::deque<int> queue;
1093
+ distance[target] = 0;
1094
+ queue.push_back(target);
1095
+
1096
+ while (!queue.empty()) {
1097
+ const int current = queue.front();
1098
+ queue.pop_front();
1099
+ for (int edge_index : graph.adjacency[current]) {
1100
+ const GraphEdge& edge = graph.edges[edge_index];
1101
+ if (edge.weight >= cutoff) {
1102
+ continue;
1103
+ }
1104
+ const int next = edge.u == current ? edge.v : edge.u;
1105
+ if (distance[next] != infinity) {
1106
+ continue;
1107
+ }
1108
+ distance[next] = distance[current] + 1;
1109
+ queue.push_back(next);
1110
+ }
1111
+ }
1112
+ return distance;
1113
+ }
1114
+
1115
+ struct HeuristicState {
1116
+ std::vector<int> path;
1117
+ std::vector<char> visited;
1118
+ int weight = 0;
1119
+ int branch_penalty = 0;
1120
+ int estimated_weight = 0;
1121
+ int estimated_length = 0;
1122
+ int serial = 0;
1123
+ };
1124
+
1125
+ struct HeuristicStateWorse {
1126
+ bool operator()(const HeuristicState& lhs, const HeuristicState& rhs) const {
1127
+ if (lhs.estimated_weight != rhs.estimated_weight) {
1128
+ return lhs.estimated_weight > rhs.estimated_weight;
1129
+ }
1130
+ if (lhs.estimated_length != rhs.estimated_length) {
1131
+ return lhs.estimated_length > rhs.estimated_length;
1132
+ }
1133
+ if (lhs.branch_penalty != rhs.branch_penalty) {
1134
+ return lhs.branch_penalty > rhs.branch_penalty;
1135
+ }
1136
+ if (lhs.weight != rhs.weight) {
1137
+ return lhs.weight > rhs.weight;
1138
+ }
1139
+ if (lhs.path.size() != rhs.path.size()) {
1140
+ return lhs.path.size() > rhs.path.size();
1141
+ }
1142
+ return lhs.serial > rhs.serial;
1143
+ }
1144
+ };
1145
+
1146
+ struct HeuristicStep {
1147
+ int next = -1;
1148
+ int edge_index = -1;
1149
+ int edge_weight = 0;
1150
+ int distance = 0;
1151
+ int degree_penalty = 0;
1152
+ };
1153
+
1154
+ bool heuristic_step_less(const HeuristicStep& lhs, const HeuristicStep& rhs) {
1155
+ if (lhs.edge_weight != rhs.edge_weight) {
1156
+ return lhs.edge_weight < rhs.edge_weight;
1157
+ }
1158
+ if (lhs.distance != rhs.distance) {
1159
+ return lhs.distance < rhs.distance;
1160
+ }
1161
+ if (lhs.degree_penalty != rhs.degree_penalty) {
1162
+ return lhs.degree_penalty < rhs.degree_penalty;
1163
+ }
1164
+ if (lhs.next != rhs.next) {
1165
+ return lhs.next < rhs.next;
1166
+ }
1167
+ return lhs.edge_index < rhs.edge_index;
1168
+ }
1169
+
1170
+ std::vector<std::vector<int>> collect_heuristic_paths(
1171
+ const DualGraph& graph,
1172
+ int source,
1173
+ int target,
1174
+ int cutoff) {
1175
+ std::vector<std::vector<int>> paths;
1176
+ const int face_count = static_cast<int>(graph.faces.size());
1177
+ if (source == target || source < 0 || target < 0 ||
1178
+ source >= face_count || target >= face_count || cutoff <= 0) {
1179
+ return paths;
1180
+ }
1181
+
1182
+ const std::vector<int> distance = heuristic_distances_to_target(graph, target, cutoff);
1183
+ const int infinity = std::numeric_limits<int>::max() / 4;
1184
+ if (distance[source] == infinity || distance[source] >= cutoff) {
1185
+ return paths;
1186
+ }
1187
+
1188
+ const int state_budget = std::max(
1189
+ kHeuristicMinStateBudget,
1190
+ std::min(kHeuristicMaxStateBudget, face_count * std::max(1, cutoff) * 8));
1191
+ const int path_budget = std::max(
1192
+ kHeuristicMinPathBudget,
1193
+ std::min(kHeuristicMaxPathBudget, face_count * 2 + cutoff * 8));
1194
+
1195
+ std::priority_queue<HeuristicState, std::vector<HeuristicState>, HeuristicStateWorse> queue;
1196
+ int serial = 0;
1197
+ HeuristicState initial;
1198
+ initial.path.push_back(source);
1199
+ initial.visited.assign(face_count, false);
1200
+ initial.visited[source] = true;
1201
+ initial.estimated_weight = distance[source];
1202
+ initial.estimated_length = distance[source];
1203
+ initial.serial = serial++;
1204
+ queue.push(initial);
1205
+
1206
+ std::map<long long, int> popped_by_depth_face;
1207
+ int popped_states = 0;
1208
+ while (!queue.empty() && popped_states < state_budget &&
1209
+ static_cast<int>(paths.size()) < path_budget) {
1210
+ HeuristicState state = queue.top();
1211
+ queue.pop();
1212
+ ++popped_states;
1213
+
1214
+ const int current = state.path.back();
1215
+ const int depth = static_cast<int>(state.path.size()) - 1;
1216
+ if (current == target) {
1217
+ if (state.weight < cutoff) {
1218
+ paths.push_back(state.path);
1219
+ }
1220
+ continue;
1221
+ }
1222
+ if (depth >= cutoff - 1) {
1223
+ continue;
1224
+ }
1225
+
1226
+ const long long beam_key =
1227
+ static_cast<long long>(depth) * static_cast<long long>(face_count) + current;
1228
+ int& beam_count = popped_by_depth_face[beam_key];
1229
+ if (beam_count >= kHeuristicBeamWidth) {
1230
+ continue;
1231
+ }
1232
+ ++beam_count;
1233
+
1234
+ std::vector<HeuristicStep> steps;
1235
+ for (int edge_index : graph.adjacency[current]) {
1236
+ const GraphEdge& edge = graph.edges[edge_index];
1237
+ const int next = edge.u == current ? edge.v : edge.u;
1238
+ if (state.visited[next]) {
1239
+ continue;
1240
+ }
1241
+ if (distance[next] == infinity) {
1242
+ continue;
1243
+ }
1244
+ const int new_weight = state.weight + edge.weight;
1245
+ if (new_weight >= cutoff) {
1246
+ continue;
1247
+ }
1248
+ const int new_depth = depth + 1;
1249
+ if (new_depth + distance[next] >= cutoff) {
1250
+ continue;
1251
+ }
1252
+ HeuristicStep step;
1253
+ step.next = next;
1254
+ step.edge_index = edge_index;
1255
+ step.edge_weight = edge.weight;
1256
+ step.distance = distance[next];
1257
+ step.degree_penalty = std::max(0, static_cast<int>(graph.adjacency[next].size()) - 2);
1258
+ steps.push_back(step);
1259
+ }
1260
+ std::sort(steps.begin(), steps.end(), heuristic_step_less);
1261
+
1262
+ for (const HeuristicStep& step : steps) {
1263
+ const GraphEdge& edge = graph.edges[step.edge_index];
1264
+ HeuristicState next_state;
1265
+ next_state.path = state.path;
1266
+ next_state.path.push_back(step.next);
1267
+ next_state.visited = state.visited;
1268
+ next_state.visited[step.next] = true;
1269
+ next_state.weight = state.weight + edge.weight;
1270
+ next_state.branch_penalty = state.branch_penalty + step.degree_penalty;
1271
+ next_state.estimated_weight = next_state.weight + distance[step.next];
1272
+ next_state.estimated_length =
1273
+ static_cast<int>(next_state.path.size()) - 1 + distance[step.next];
1274
+ next_state.serial = serial++;
1275
+ queue.push(std::move(next_state));
1276
+ }
1277
+ }
1278
+
1279
+ return paths;
1280
+ }
1281
+
1079
1282
  bool contains_endpoint_key(const std::vector<int>& endpoints, int key) {
1080
1283
  return std::find(endpoints.begin(), endpoints.end(), key) != endpoints.end();
1081
1284
  }
@@ -1627,6 +1830,13 @@ SimplificationResult find_simplification(
1627
1830
  const PDCode& code,
1628
1831
  const SimplifierOptions& options) {
1629
1832
  SimplificationResult result;
1833
+ if (options.max_paths == -1 && !options.ban_heuristic) {
1834
+ result.path_search_mode = "heuristic";
1835
+ } else if (options.max_paths == -1) {
1836
+ result.path_search_mode = "bruteforce";
1837
+ } else {
1838
+ result.path_search_mode = "bounded";
1839
+ }
1630
1840
  Diagram diagram(code);
1631
1841
  DualGraph graph(diagram);
1632
1842
  const auto red_lines = possible_red_lines(diagram);
@@ -1657,7 +1867,12 @@ SimplificationResult find_simplification(
1657
1867
  const int cutoff = static_cast<int>(red_path.size()) - 1;
1658
1868
  for (int source : sources) {
1659
1869
  for (int destination : destinations) {
1660
- auto found_paths = collect_simple_paths(graph, source, destination, cutoff, options.max_paths);
1870
+ std::vector<std::vector<int>> found_paths;
1871
+ if (options.max_paths == -1 && !options.ban_heuristic) {
1872
+ found_paths = collect_heuristic_paths(graph, source, destination, cutoff);
1873
+ } else {
1874
+ found_paths = collect_simple_paths(graph, source, destination, cutoff, options.max_paths);
1875
+ }
1661
1876
  paths.insert(paths.end(), found_paths.begin(), found_paths.end());
1662
1877
  if (options.max_paths != -1 && static_cast<int>(paths.size()) > options.max_paths) {
1663
1878
  break;
@@ -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,7 @@ 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,
338
405
  ctypes.c_ulonglong,
339
406
  ctypes.POINTER(ctypes.c_int),
340
407
  ctypes.c_ulonglong,
@@ -350,7 +417,8 @@ def _load_library() -> ctypes.CDLL:
350
417
  def _run_one(
351
418
  pd_text: str,
352
419
  *,
353
- max_paths: int = 100,
420
+ max_paths: int = -1,
421
+ ban_heuristic: bool = False,
354
422
  known_crossingless_components: int = 0,
355
423
  remove_crossings: Optional[Sequence[int]] = None,
356
424
  ) -> dict[str, Any]:
@@ -363,6 +431,7 @@ def _run_one(
363
431
  pointer = library.pdcode_simplify_run_json(
364
432
  pd_text.encode("utf-8"),
365
433
  int(max_paths),
434
+ 1 if ban_heuristic else 0,
366
435
  int(known_crossingless_components),
367
436
  removed_array,
368
437
  int(removed_count),
@@ -386,7 +455,8 @@ def _run_one(
386
455
  def simplify(
387
456
  pd_code: PdInput,
388
457
  *,
389
- max_paths: int = 100,
458
+ max_paths: int = -1,
459
+ ban_heuristic: bool = False,
390
460
  known_crossingless_components: int = 0,
391
461
  remove_crossings: Optional[Sequence[int]] = None,
392
462
  ) -> dict[str, Any]:
@@ -395,6 +465,7 @@ def simplify(
395
465
  return _run_one(
396
466
  normalize_pd_code(pd_code),
397
467
  max_paths=max_paths,
468
+ ban_heuristic=ban_heuristic,
398
469
  known_crossingless_components=known_crossingless_components,
399
470
  remove_crossings=remove_crossings,
400
471
  )
@@ -403,7 +474,8 @@ def simplify(
403
474
  def simplify_many(
404
475
  pd_codes: PdManyInput,
405
476
  *,
406
- max_paths: int = 100,
477
+ max_paths: int = -1,
478
+ ban_heuristic: bool = False,
407
479
  known_crossingless_components: int = 0,
408
480
  remove_crossings: Optional[Sequence[int]] = None,
409
481
  ) -> list[dict[str, Any]]:
@@ -413,6 +485,7 @@ def simplify_many(
413
485
  _run_one(
414
486
  pd_text,
415
487
  max_paths=max_paths,
488
+ ban_heuristic=ban_heuristic,
416
489
  known_crossingless_components=known_crossingless_components,
417
490
  remove_crossings=remove_crossings,
418
491
  )
@@ -424,7 +497,8 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
424
497
  parser = argparse.ArgumentParser(description="Run cpp-pd-code-simplify through the Python interface.")
425
498
  parser.add_argument("pd_code", nargs="?", help="PD code as PD[...] text or a Python-style list of crossings.")
426
499
  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)
500
+ parser.add_argument("--max-paths", type=int, default=-1)
501
+ parser.add_argument("--ban-heuristic", action="store_true")
428
502
  parser.add_argument("--known-crossingless-components", type=int, default=0)
429
503
  parser.add_argument("--remove-crossings", help="comma-separated zero-based crossing indices")
430
504
  args = parser.parse_args(argv)
@@ -438,32 +512,33 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
438
512
 
439
513
  exit_code = 0
440
514
  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)
515
+ jobs = _pd_file_jobs(args.pd_file)
448
516
  batch_payload = []
449
- for line in lines:
517
+ show_labels = len(jobs) > 1
518
+ for label, line in jobs:
450
519
  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
- )
520
+ item = simplify(
521
+ line,
522
+ max_paths=args.max_paths,
523
+ ban_heuristic=args.ban_heuristic,
524
+ known_crossingless_components=args.known_crossingless_components,
525
+ remove_crossings=remove_crossings,
458
526
  )
527
+ if show_labels:
528
+ item = {"label": label, **item}
529
+ batch_payload.append(item)
459
530
  except Exception as exc:
460
531
  exit_code = 2
461
- batch_payload.append({"error": str(exc)})
532
+ item = {"error": str(exc)}
533
+ if show_labels:
534
+ item = {"label": label, **item}
535
+ batch_payload.append(item)
462
536
  payload: Any = batch_payload
463
537
  else:
464
538
  payload = simplify(
465
539
  args.pd_code or "",
466
540
  max_paths=args.max_paths,
541
+ ban_heuristic=args.ban_heuristic,
467
542
  known_crossingless_components=args.known_crossingless_components,
468
543
  remove_crossings=remove_crossings,
469
544
  )
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cpp-pd-code-simplify-interface"
3
- version = "0.1.1"
3
+ version = "0.1.2"
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"}