synkit 1.6.5__py3-none-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (636) hide show
  1. synkit/CRN/Benchmark/__init__.py +53 -0
  2. synkit/CRN/Benchmark/crosschecks.py +13 -0
  3. synkit/CRN/Benchmark/kegg.py +12 -0
  4. synkit/CRN/Benchmark/networks.py +13 -0
  5. synkit/CRN/Benchmark/scaling.py +13 -0
  6. synkit/CRN/Benchmark/validate.py +13 -0
  7. synkit/CRN/Construct/__init__.py +16 -0
  8. synkit/CRN/Construct/abstract.py +668 -0
  9. synkit/CRN/Construct/arity.py +36 -0
  10. synkit/CRN/Construct/builder.py +619 -0
  11. synkit/CRN/Construct/derivation.py +63 -0
  12. synkit/CRN/Construct/flattener.py +100 -0
  13. synkit/CRN/Construct/keys.py +14 -0
  14. synkit/CRN/Construct/mixtures.py +117 -0
  15. synkit/CRN/Construct/rdkit_worker.py +69 -0
  16. synkit/CRN/Construct/smiles.py +99 -0
  17. synkit/CRN/Construct/state.py +31 -0
  18. synkit/CRN/Construct/strategy.py +75 -0
  19. synkit/CRN/Construct/worker.py +42 -0
  20. synkit/CRN/IO/__init__.py +36 -0
  21. synkit/CRN/IO/sbml.py +925 -0
  22. synkit/CRN/Pathway/__init__.py +72 -0
  23. synkit/CRN/Pathway/_adapter.py +333 -0
  24. synkit/CRN/Pathway/pathfinder.py +500 -0
  25. synkit/CRN/Pathway/reachability.py +796 -0
  26. synkit/CRN/Pathway/realizability.py +1322 -0
  27. synkit/CRN/Petrinet/__init__.py +93 -0
  28. synkit/CRN/Petrinet/analyzer.py +486 -0
  29. synkit/CRN/Petrinet/minimal_semiflows.py +436 -0
  30. synkit/CRN/Petrinet/net.py +1041 -0
  31. synkit/CRN/Petrinet/persistence.py +97 -0
  32. synkit/CRN/Petrinet/semiflows.py +483 -0
  33. synkit/CRN/Petrinet/siphon_search.py +402 -0
  34. synkit/CRN/Petrinet/structure.py +286 -0
  35. synkit/CRN/Props/__init__.py +130 -0
  36. synkit/CRN/Props/_rank.py +29 -0
  37. synkit/CRN/Props/deficiency.py +953 -0
  38. synkit/CRN/Props/dynamics.py +832 -0
  39. synkit/CRN/Props/helper.py +151 -0
  40. synkit/CRN/Props/stoich.py +751 -0
  41. synkit/CRN/Props/thermo.py +584 -0
  42. synkit/CRN/Query/__init__.py +66 -0
  43. synkit/CRN/Query/cache.py +41 -0
  44. synkit/CRN/Query/kegg_api.py +99 -0
  45. synkit/CRN/Query/kegg_extract.py +714 -0
  46. synkit/CRN/Query/kegg_impute.py +707 -0
  47. synkit/CRN/Query/kegg_parse.py +480 -0
  48. synkit/CRN/Query/to_syncrn.py +290 -0
  49. synkit/CRN/README.md +234 -0
  50. synkit/CRN/Structure/__init__.py +12 -0
  51. synkit/CRN/Structure/_graph_io.py +713 -0
  52. synkit/CRN/Structure/_matrices.py +88 -0
  53. synkit/CRN/Structure/_parse.py +544 -0
  54. synkit/CRN/Structure/reaction.py +237 -0
  55. synkit/CRN/Structure/rule.py +55 -0
  56. synkit/CRN/Structure/species.py +46 -0
  57. synkit/CRN/Structure/syncrn.py +929 -0
  58. synkit/CRN/Symmetry/__init__.py +20 -0
  59. synkit/CRN/Symmetry/_common.py +1059 -0
  60. synkit/CRN/Symmetry/_ir.py +940 -0
  61. synkit/CRN/Symmetry/automorphism.py +472 -0
  62. synkit/CRN/Symmetry/canon.py +441 -0
  63. synkit/CRN/Symmetry/isomorphism.py +412 -0
  64. synkit/CRN/Symmetry/symmetry.py +61 -0
  65. synkit/CRN/Symmetry/wl_canon.py +967 -0
  66. synkit/CRN/Visualize/__init__.py +11 -0
  67. synkit/CRN/Visualize/labels.py +113 -0
  68. synkit/CRN/Visualize/layout.py +708 -0
  69. synkit/CRN/Visualize/palette.py +292 -0
  70. synkit/CRN/Visualize/validation.py +122 -0
  71. synkit/CRN/Visualize/vis.py +1152 -0
  72. synkit/CRN/__init__.py +233 -0
  73. synkit/CRN/_build.py +50 -0
  74. synkit/CRN/_native_build.py +175 -0
  75. synkit/CRN/analysis.py +629 -0
  76. synkit/CRN/backend.py +74 -0
  77. synkit/CRN/cpp/CMakeLists.txt +58 -0
  78. synkit/CRN/cpp/algebra.cpp +46 -0
  79. synkit/CRN/cpp/closure.cpp +29 -0
  80. synkit/CRN/cpp/exact.h +55 -0
  81. synkit/CRN/cpp/pathway.cpp +46 -0
  82. synkit/CRN/cpp/semiflows.cpp +209 -0
  83. synkit/CRN/cpp/structural.cpp +240 -0
  84. synkit/CRN/cpp/symmetry.cpp +45 -0
  85. synkit/CRN/cpp/write_manifest.cmake +28 -0
  86. synkit/CRN/kinds.py +137 -0
  87. synkit/CRN/lib/synkit_crn.dll +0 -0
  88. synkit/CRN/lib/synkit_crn.json +33 -0
  89. synkit/CRN/native.py +388 -0
  90. synkit/CRN/native_analysis.py +459 -0
  91. synkit/Chem/Cluster/__init__.py +0 -0
  92. synkit/Chem/Cluster/butina.py +140 -0
  93. synkit/Chem/Fingerprint/__init__.py +0 -0
  94. synkit/Chem/Fingerprint/fp_calculator.py +155 -0
  95. synkit/Chem/Fingerprint/smiles_featurizer.py +258 -0
  96. synkit/Chem/Fingerprint/transformation_fp.py +139 -0
  97. synkit/Chem/Mapper/README.rst +43 -0
  98. synkit/Chem/Mapper/__init__.py +255 -0
  99. synkit/Chem/Mapper/_compat.py +130 -0
  100. synkit/Chem/Mapper/adapters/__init__.py +1 -0
  101. synkit/Chem/Mapper/adapters/prediction.py +126 -0
  102. synkit/Chem/Mapper/adapters/template.py +412 -0
  103. synkit/Chem/Mapper/chem/__init__.py +52 -0
  104. synkit/Chem/Mapper/chem/aam.py +673 -0
  105. synkit/Chem/Mapper/chem/blind.py +160 -0
  106. synkit/Chem/Mapper/chem/its.py +309 -0
  107. synkit/Chem/Mapper/chem/smiles.py +564 -0
  108. synkit/Chem/Mapper/graph/__init__.py +66 -0
  109. synkit/Chem/Mapper/graph/automorphism.py +395 -0
  110. synkit/Chem/Mapper/graph/blockcut.py +98 -0
  111. synkit/Chem/Mapper/graph/labeled_graph.py +104 -0
  112. synkit/Chem/Mapper/graph/refine.py +176 -0
  113. synkit/Chem/Mapper/graph/synkit_adapter.py +110 -0
  114. synkit/Chem/Mapper/io/__init__.py +13 -0
  115. synkit/Chem/Mapper/io/idxmapstr.py +82 -0
  116. synkit/Chem/Mapper/mapper.py +685 -0
  117. synkit/Chem/Mapper/reaction_search.py +554 -0
  118. synkit/Chem/Mapper/reporting/__init__.py +1 -0
  119. synkit/Chem/Mapper/reporting/alternatives.py +451 -0
  120. synkit/Chem/Mapper/reporting/analysis.py +681 -0
  121. synkit/Chem/Mapper/reporting/native_analysis.py +485 -0
  122. synkit/Chem/Mapper/reporting/spectrum.py +642 -0
  123. synkit/Chem/Mapper/solvers/__init__.py +1 -0
  124. synkit/Chem/Mapper/solvers/exact/__init__.py +141 -0
  125. synkit/Chem/Mapper/solvers/exact/assignment/__init__.py +1 -0
  126. synkit/Chem/Mapper/solvers/exact/assignment/assignment_certificate.py +113 -0
  127. synkit/Chem/Mapper/solvers/exact/assignment/assignment_domains.py +162 -0
  128. synkit/Chem/Mapper/solvers/exact/assignment/assignment_verification.py +58 -0
  129. synkit/Chem/Mapper/solvers/exact/assignment/incremental_assignment.py +247 -0
  130. synkit/Chem/Mapper/solvers/exact/certificate.py +128 -0
  131. synkit/Chem/Mapper/solvers/exact/core.py +183 -0
  132. synkit/Chem/Mapper/solvers/exact/distance_search/__init__.py +1 -0
  133. synkit/Chem/Mapper/solvers/exact/distance_search/bounds.py +319 -0
  134. synkit/Chem/Mapper/solvers/exact/distance_search/exhaustive.py +179 -0
  135. synkit/Chem/Mapper/solvers/exact/distance_search/inputs.py +283 -0
  136. synkit/Chem/Mapper/solvers/exact/distance_search/records.py +242 -0
  137. synkit/Chem/Mapper/solvers/exact/distance_search/search.py +1025 -0
  138. synkit/Chem/Mapper/solvers/exact/distance_search/verify.py +378 -0
  139. synkit/Chem/Mapper/solvers/exact/edit_support.py +375 -0
  140. synkit/Chem/Mapper/solvers/exact/hybrid.py +224 -0
  141. synkit/Chem/Mapper/solvers/exact/hydrogen.py +264 -0
  142. synkit/Chem/Mapper/solvers/exact/kernel_solvers/__init__.py +1 -0
  143. synkit/Chem/Mapper/solvers/exact/kernel_solvers/branching.py +499 -0
  144. synkit/Chem/Mapper/solvers/exact/kernel_solvers/enumerate.py +1050 -0
  145. synkit/Chem/Mapper/solvers/exact/kernel_solvers/enumeration_result.py +30 -0
  146. synkit/Chem/Mapper/solvers/exact/kernel_solvers/kernel.py +256 -0
  147. synkit/Chem/Mapper/solvers/exact/kernel_solvers/milp.py +194 -0
  148. synkit/Chem/Mapper/solvers/exact/native/__init__.py +1 -0
  149. synkit/Chem/Mapper/solvers/exact/native/build.py +154 -0
  150. synkit/Chem/Mapper/solvers/exact/native/candidates.py +593 -0
  151. synkit/Chem/Mapper/solvers/exact/native/canonical.py +396 -0
  152. synkit/Chem/Mapper/solvers/exact/native/disk_frontier.py +415 -0
  153. synkit/Chem/Mapper/solvers/exact/native/frontier.py +390 -0
  154. synkit/Chem/Mapper/solvers/exact/native/hydrogen_quotient.py +213 -0
  155. synkit/Chem/Mapper/solvers/exact/native/its.py +528 -0
  156. synkit/Chem/Mapper/solvers/exact/native/native_distance.cpp +2803 -0
  157. synkit/Chem/Mapper/solvers/exact/native/orbit_aggregation.py +328 -0
  158. synkit/Chem/Mapper/solvers/exact/native/parallel.py +205 -0
  159. synkit/Chem/Mapper/solvers/exact/native/pmcd.py +203 -0
  160. synkit/Chem/Mapper/solvers/exact/native/runtime.py +40 -0
  161. synkit/Chem/Mapper/solvers/exact/native/search.py +294 -0
  162. synkit/Chem/Mapper/solvers/exact/native/separator.py +224 -0
  163. synkit/Chem/Mapper/solvers/exact/native_build.py +12 -0
  164. synkit/Chem/Mapper/solvers/exact/pabs/__init__.py +1 -0
  165. synkit/Chem/Mapper/solvers/exact/pabs/cost_lattice.py +43 -0
  166. synkit/Chem/Mapper/solvers/exact/pabs/factor_spectrum.py +110 -0
  167. synkit/Chem/Mapper/solvers/exact/pabs/multi_shell.py +368 -0
  168. synkit/Chem/Mapper/solvers/exact/pabs/propagation.py +679 -0
  169. synkit/Chem/Mapper/solvers/exact/pabs/propagation_bounds.py +436 -0
  170. synkit/Chem/Mapper/solvers/exact/pabs/propagation_limits.py +13 -0
  171. synkit/Chem/Mapper/solvers/exact/pabs/propagation_search.py +856 -0
  172. synkit/Chem/Mapper/solvers/exact/pabs/propagation_symmetry.py +66 -0
  173. synkit/Chem/Mapper/solvers/exact/pabs/residual_typed_bonds.py +136 -0
  174. synkit/Chem/Mapper/solvers/exact/pabs/reward_frontier.py +201 -0
  175. synkit/Chem/Mapper/solvers/exact/pabs/separator_bound.py +258 -0
  176. synkit/Chem/Mapper/solvers/exact/pabs/separator_spectrum.py +308 -0
  177. synkit/Chem/Mapper/solvers/exact/pabs/sparse_reward_bound.py +207 -0
  178. synkit/Chem/Mapper/solvers/exact/pabs/suffix_spectrum.py +299 -0
  179. synkit/Chem/Mapper/solvers/exact/search.py +55 -0
  180. synkit/Chem/Mapper/solvers/exact/seeds/__init__.py +1 -0
  181. synkit/Chem/Mapper/solvers/exact/seeds/seed.py +104 -0
  182. synkit/Chem/Mapper/solvers/exact/seeds/seed_fragments.py +148 -0
  183. synkit/Chem/Mapper/solvers/exact/seeds/seed_relaxation.py +181 -0
  184. synkit/Chem/Mapper/solvers/exact/symmetry.py +223 -0
  185. synkit/Chem/Mapper/solvers/native/__init__.py +4 -0
  186. synkit/Chem/Mapper/solvers/native/assignment.py +99 -0
  187. synkit/Chem/Mapper/solvers/native/astar.py +22 -0
  188. synkit/Chem/Mapper/solvers/native/build.py +385 -0
  189. synkit/Chem/Mapper/solvers/native/classification.py +641 -0
  190. synkit/Chem/Mapper/solvers/native/core.py +378 -0
  191. synkit/Chem/Mapper/solvers/native/cpp/CMakeLists.txt +120 -0
  192. synkit/Chem/Mapper/solvers/native/cpp/examples/map_reaction.cpp +29 -0
  193. synkit/Chem/Mapper/solvers/native/cpp/examples/slap_graph.cpp +63 -0
  194. synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/assignment/assignment.h +18 -0
  195. synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/astar/astar.h +18 -0
  196. synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/astar/bound.h +33 -0
  197. synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/core/pmcd_witness.h +16 -0
  198. synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/core/runtime.h +130 -0
  199. synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/hydrogen/hydrogen.h +73 -0
  200. synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/mapper.h +175 -0
  201. synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/mces/mces.h +9 -0
  202. synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/pabs/seed.h +34 -0
  203. synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/reaction/reaction.h +30 -0
  204. synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/sepdp/sepdp.h +21 -0
  205. synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/slap.h +72 -0
  206. synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/solvers/solvers.h +16 -0
  207. synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/symmetry/classification.h +140 -0
  208. synkit/Chem/Mapper/solvers/native/cpp/include/synkit_mapper/symmetry/cpabs.h +33 -0
  209. synkit/Chem/Mapper/solvers/native/cpp/src/assignment/abb.cpp +302 -0
  210. synkit/Chem/Mapper/solvers/native/cpp/src/astar/CMakeLists.txt +12 -0
  211. synkit/Chem/Mapper/solvers/native/cpp/src/astar/README.md +47 -0
  212. synkit/Chem/Mapper/solvers/native/cpp/src/astar/astar.cpp +228 -0
  213. synkit/Chem/Mapper/solvers/native/cpp/src/astar/astar_graph.cpp +37 -0
  214. synkit/Chem/Mapper/solvers/native/cpp/src/astar/bound.cpp +212 -0
  215. synkit/Chem/Mapper/solvers/native/cpp/src/core/dispatch.cpp +272 -0
  216. synkit/Chem/Mapper/solvers/native/cpp/src/core/pmcd_witness.cpp +36 -0
  217. synkit/Chem/Mapper/solvers/native/cpp/src/hydrogen/hydrogen.cpp +380 -0
  218. synkit/Chem/Mapper/solvers/native/cpp/src/mces/README.md +57 -0
  219. synkit/Chem/Mapper/solvers/native/cpp/src/mces/mces.cpp +287 -0
  220. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/bridge.cpp +334 -0
  221. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/chemical_floor.inc +181 -0
  222. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/endpoint_symmetry.inc +104 -0
  223. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/exact_helpers.h +20 -0
  224. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/lp_assignment_certificate.h +165 -0
  225. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/lp_certificate.h +125 -0
  226. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/lp_model.h +365 -0
  227. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/lp_provider.inc +319 -0
  228. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/lp_rounding.inc +170 -0
  229. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/lp_test_hooks.h +342 -0
  230. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/objective_reduction.inc +137 -0
  231. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/overlap_dual.inc +351 -0
  232. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/profile_seed.inc +214 -0
  233. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/seed_heuristics.inc +640 -0
  234. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/sparse_embedding.cpp +430 -0
  235. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/sparse_root_lp.h +176 -0
  236. synkit/Chem/Mapper/solvers/native/cpp/src/pabs/test_hooks.inc +262 -0
  237. synkit/Chem/Mapper/solvers/native/cpp/src/reaction/reaction.cpp +611 -0
  238. synkit/Chem/Mapper/solvers/native/cpp/src/sepdp/README.md +61 -0
  239. synkit/Chem/Mapper/solvers/native/cpp/src/sepdp/sepdp.cpp +253 -0
  240. synkit/Chem/Mapper/solvers/native/cpp/src/slap.cpp +810 -0
  241. synkit/Chem/Mapper/solvers/native/cpp/src/solvers/formulation.h +138 -0
  242. synkit/Chem/Mapper/solvers/native/cpp/src/solvers/maxsat.cpp +526 -0
  243. synkit/Chem/Mapper/solvers/native/cpp/src/solvers/ortools.cpp +475 -0
  244. synkit/Chem/Mapper/solvers/native/cpp/src/solvers/seed.h +80 -0
  245. synkit/Chem/Mapper/solvers/native/cpp/src/solvers/weighted_cnf.h +256 -0
  246. synkit/Chem/Mapper/solvers/native/cpp/src/supporting/README.md +43 -0
  247. synkit/Chem/Mapper/solvers/native/cpp/src/supporting/dfs.cpp +123 -0
  248. synkit/Chem/Mapper/solvers/native/cpp/src/supporting/edit_support.cpp +224 -0
  249. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/classification.cpp +553 -0
  250. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/classification_pipeline.cpp +124 -0
  251. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/classification_stream.h +13 -0
  252. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/cpabs.cpp +455 -0
  253. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/edge_deck.cpp +743 -0
  254. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/COPYRIGHT +44 -0
  255. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/LICENSE-2.0.txt +202 -0
  256. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/naugraph.c +732 -0
  257. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/naurng.c +94 -0
  258. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/naurng.h +49 -0
  259. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/nausparse.c +1771 -0
  260. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/nausparse.h +133 -0
  261. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/nautil.c +750 -0
  262. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/naututil.h +355 -0
  263. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/nauty.c +1256 -0
  264. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/nauty.h +1544 -0
  265. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/schreier.c +1131 -0
  266. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/schreier.h +70 -0
  267. synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/sorttemplates.c +580 -0
  268. synkit/Chem/Mapper/solvers/native/hydrogen.py +711 -0
  269. synkit/Chem/Mapper/solvers/native/implicit.py +506 -0
  270. synkit/Chem/Mapper/solvers/native/implicit_certificate.py +187 -0
  271. synkit/Chem/Mapper/solvers/native/implicit_preparation.py +309 -0
  272. synkit/Chem/Mapper/solvers/native/lib/synkit_mapper.dll +0 -0
  273. synkit/Chem/Mapper/solvers/native/mces.py +16 -0
  274. synkit/Chem/Mapper/solvers/native/reaction.py +179 -0
  275. synkit/Chem/Mapper/solvers/native/reaction_build.py +277 -0
  276. synkit/Chem/Mapper/solvers/native/reaction_text.py +26 -0
  277. synkit/Chem/Mapper/solvers/native/runtime.py +242 -0
  278. synkit/Chem/Mapper/solvers/native/sepdp.py +47 -0
  279. synkit/Chem/Mapper/solvers/native/slap.py +266 -0
  280. synkit/Chem/Mapper/solvers/native/solvers.py +67 -0
  281. synkit/Chem/Mapper/solvers/native/stereo.py +86 -0
  282. synkit/Chem/Mapper/solvers/native/supporting.py +34 -0
  283. synkit/Chem/Mapper/solvers/native/symmetry.py +91 -0
  284. synkit/Chem/Mapper/solvers/slap/__init__.py +29 -0
  285. synkit/Chem/Mapper/solvers/slap/lap.py +174 -0
  286. synkit/Chem/Mapper/solvers/slap/sequential.py +696 -0
  287. synkit/Chem/Mapper/validation/__init__.py +1 -0
  288. synkit/Chem/Mapper/validation/aam_validator.py +442 -0
  289. synkit/Chem/Mapper/validation/annotations.py +274 -0
  290. synkit/Chem/Mapper/validation/bonds.py +124 -0
  291. synkit/Chem/Mapper/validation/cohorts.py +72 -0
  292. synkit/Chem/Mapper/validation/identifiability.py +150 -0
  293. synkit/Chem/Mapper/validation/numerical_scope.py +36 -0
  294. synkit/Chem/Mapper/validation/orbits.py +137 -0
  295. synkit/Chem/Molecule/__init__.py +0 -0
  296. synkit/Chem/Molecule/_chirality_loci.py +165 -0
  297. synkit/Chem/Molecule/_chirality_types.py +231 -0
  298. synkit/Chem/Molecule/_helical_loci.py +167 -0
  299. synkit/Chem/Molecule/_stereo_axis_evidence.py +202 -0
  300. synkit/Chem/Molecule/_stereo_element_detection.py +193 -0
  301. synkit/Chem/Molecule/_stereo_orientation_constraints.py +511 -0
  302. synkit/Chem/Molecule/_tetrahedral_carriers.py +63 -0
  303. synkit/Chem/Molecule/atom_features.py +379 -0
  304. synkit/Chem/Molecule/chirality.py +1041 -0
  305. synkit/Chem/Molecule/cip_assignment.py +569 -0
  306. synkit/Chem/Molecule/cip_ranking.py +843 -0
  307. synkit/Chem/Molecule/coordinate_stereo.py +503 -0
  308. synkit/Chem/Molecule/derived_stereo_naming.py +165 -0
  309. synkit/Chem/Molecule/descriptors.py +452 -0
  310. synkit/Chem/Molecule/formula.py +249 -0
  311. synkit/Chem/Molecule/global_stereo.py +175 -0
  312. synkit/Chem/Molecule/graph_annotator.py +300 -0
  313. synkit/Chem/Molecule/standardize.py +817 -0
  314. synkit/Chem/Molecule/stereo_evidence.py +179 -0
  315. synkit/Chem/Molecule/stereo_perception.py +1032 -0
  316. synkit/Chem/Molecule/valence.py +123 -0
  317. synkit/Chem/Reaction/Mapper/__init__.py +44 -0
  318. synkit/Chem/Reaction/__init__.py +20 -0
  319. synkit/Chem/Reaction/aam_validator.py +5 -0
  320. synkit/Chem/Reaction/balance_check.py +138 -0
  321. synkit/Chem/Reaction/canon_rsmi.py +249 -0
  322. synkit/Chem/Reaction/cleaning.py +58 -0
  323. synkit/Chem/Reaction/deionize.py +199 -0
  324. synkit/Chem/Reaction/explicit_h_audit.py +232 -0
  325. synkit/Chem/Reaction/fix_aam.py +64 -0
  326. synkit/Chem/Reaction/neutralize.py +207 -0
  327. synkit/Chem/Reaction/radical_wildcard.py +224 -0
  328. synkit/Chem/Reaction/standardize.py +203 -0
  329. synkit/Chem/Reaction/tautomerize.py +205 -0
  330. synkit/Chem/__init__.py +1 -0
  331. synkit/Chem/utils.py +315 -0
  332. synkit/Data/__init__.py +0 -0
  333. synkit/Data/aldol.json.gz +1 -0
  334. synkit/Data/gen_partial_aam.py +147 -0
  335. synkit/Data/paracetamol.json.gz +1 -0
  336. synkit/Graph/Canon/__init__.py +16 -0
  337. synkit/Graph/Canon/_native/__init__.py +13 -0
  338. synkit/Graph/Canon/_native/build.py +93 -0
  339. synkit/Graph/Canon/_native/lib/synkit_canon.dll +0 -0
  340. synkit/Graph/Canon/_native/prepare.py +49 -0
  341. synkit/Graph/Canon/_native/runtime.py +140 -0
  342. synkit/Graph/Canon/_native/search.py +137 -0
  343. synkit/Graph/Canon/canon_algs.py +228 -0
  344. synkit/Graph/Canon/canon_graph.py +531 -0
  345. synkit/Graph/Canon/cpp/CMakeLists.txt +42 -0
  346. synkit/Graph/Canon/cpp/README.md +123 -0
  347. synkit/Graph/Canon/cpp/examples/canonical_graph.cpp +16 -0
  348. synkit/Graph/Canon/cpp/include/synkit_canon/canon.h +85 -0
  349. synkit/Graph/Canon/cpp/src/canonical.cpp +391 -0
  350. synkit/Graph/Canon/cpp/src/symmetry.h +83 -0
  351. synkit/Graph/Canon/cpp/tests/symmetry.cpp +93 -0
  352. synkit/Graph/Canon/exact.py +1020 -0
  353. synkit/Graph/Canon/exact_refinement.py +368 -0
  354. synkit/Graph/Canon/nauty.py +143 -0
  355. synkit/Graph/Context/__init__.py +0 -0
  356. synkit/Graph/Context/hier_context.py +244 -0
  357. synkit/Graph/Context/radius_expand.py +252 -0
  358. synkit/Graph/FG/__init__.py +19 -0
  359. synkit/Graph/FG/api.py +39 -0
  360. synkit/Graph/FG/audit.py +155 -0
  361. synkit/Graph/FG/catalog.py +919 -0
  362. synkit/Graph/FG/detector.py +288 -0
  363. synkit/Graph/FG/model.py +97 -0
  364. synkit/Graph/FG/recognizers.py +274 -0
  365. synkit/Graph/FG/ring_system.py +154 -0
  366. synkit/Graph/Feature/Descriptors/topology.py +857 -0
  367. synkit/Graph/Feature/Fingerprint/__init__.py +0 -0
  368. synkit/Graph/Feature/Fingerprint/wl_rxn_fps.py +222 -0
  369. synkit/Graph/Feature/__init__.py +5 -0
  370. synkit/Graph/Feature/graph_descriptors.py +323 -0
  371. synkit/Graph/Feature/graph_fps.py +96 -0
  372. synkit/Graph/Feature/graph_signature.py +239 -0
  373. synkit/Graph/Feature/hash_fps.py +133 -0
  374. synkit/Graph/Feature/morgan_fps.py +88 -0
  375. synkit/Graph/Feature/path_fps.py +81 -0
  376. synkit/Graph/Feature/wl_hash.py +134 -0
  377. synkit/Graph/Fusion/__init__.py +85 -0
  378. synkit/Graph/Fusion/candidate.py +559 -0
  379. synkit/Graph/Fusion/constructor.py +877 -0
  380. synkit/Graph/Fusion/identity.py +184 -0
  381. synkit/Graph/Fusion/interface.py +700 -0
  382. synkit/Graph/Fusion/search.py +241 -0
  383. synkit/Graph/Hyrogen/__init__.py +6 -0
  384. synkit/Graph/Hyrogen/_misc.py +591 -0
  385. synkit/Graph/Hyrogen/hcomplete.py +995 -0
  386. synkit/Graph/Hyrogen/hcomplete_legacy.py +368 -0
  387. synkit/Graph/Hyrogen/hcompletion_algorithms.py +713 -0
  388. synkit/Graph/Hyrogen/hextend.py +570 -0
  389. synkit/Graph/Hyrogen/hextend_legacy.py +215 -0
  390. synkit/Graph/ITS/__init__.py +4 -0
  391. synkit/Graph/ITS/its_builder.py +120 -0
  392. synkit/Graph/ITS/its_construction.py +719 -0
  393. synkit/Graph/ITS/its_decompose.py +572 -0
  394. synkit/Graph/ITS/its_destruction.py +331 -0
  395. synkit/Graph/ITS/its_expand.py +1251 -0
  396. synkit/Graph/ITS/its_relabel.py +187 -0
  397. synkit/Graph/ITS/its_reverter.py +356 -0
  398. synkit/Graph/ITS/normalize_aam.py +145 -0
  399. synkit/Graph/ITS/partial_its.py +238 -0
  400. synkit/Graph/ITS/rc_extractor.py +656 -0
  401. synkit/Graph/ITS/stereo.py +74 -0
  402. synkit/Graph/MTG/__init__.py +107 -0
  403. synkit/Graph/MTG/gluing.py +451 -0
  404. synkit/Graph/MTG/history.py +928 -0
  405. synkit/Graph/MTG/inventory.py +118 -0
  406. synkit/Graph/MTG/local.py +447 -0
  407. synkit/Graph/MTG/mcs_matcher.py +246 -0
  408. synkit/Graph/MTG/mtg.py +611 -0
  409. synkit/Graph/MTG/mtg_explore.py +74 -0
  410. synkit/Graph/MTG/observation.py +209 -0
  411. synkit/Graph/MTG/process.py +577 -0
  412. synkit/Graph/MTG/queries.py +66 -0
  413. synkit/Graph/MTG/realization.py +95 -0
  414. synkit/Graph/MTG/series_parallel.py +107 -0
  415. synkit/Graph/MTG/utils.py +424 -0
  416. synkit/Graph/Matcher/__init__.py +10 -0
  417. synkit/Graph/Matcher/approx_mcs.py +1264 -0
  418. synkit/Graph/Matcher/auto_est.py +736 -0
  419. synkit/Graph/Matcher/automorphism.py +314 -0
  420. synkit/Graph/Matcher/batch_cluster.py +224 -0
  421. synkit/Graph/Matcher/dedup_matches.py +247 -0
  422. synkit/Graph/Matcher/graph_cluster.py +191 -0
  423. synkit/Graph/Matcher/graph_matcher.py +312 -0
  424. synkit/Graph/Matcher/graph_morphism.py +421 -0
  425. synkit/Graph/Matcher/mcs_matcher.py +804 -0
  426. synkit/Graph/Matcher/multi_turbo_iso.py +186 -0
  427. synkit/Graph/Matcher/orbit.py +284 -0
  428. synkit/Graph/Matcher/partial_matcher.py +694 -0
  429. synkit/Graph/Matcher/sing.py +544 -0
  430. synkit/Graph/Matcher/subgraph_matcher.py +812 -0
  431. synkit/Graph/Matcher/turbo_iso.py +272 -0
  432. synkit/Graph/Matcher/wl_sel.py +537 -0
  433. synkit/Graph/Mech/__init__.py +26 -0
  434. synkit/Graph/Mech/conversion.py +1466 -0
  435. synkit/Graph/Mech/conversion_debug.py +237 -0
  436. synkit/Graph/Mech/electron_accounting.py +200 -0
  437. synkit/Graph/Mech/lwg_editor.py +406 -0
  438. synkit/Graph/Mech/lwg_ops.py +313 -0
  439. synkit/Graph/Morphism/__init__.py +99 -0
  440. synkit/Graph/Morphism/constraints.py +460 -0
  441. synkit/Graph/Morphism/llg.py +999 -0
  442. synkit/Graph/Morphism/morphism.py +349 -0
  443. synkit/Graph/Morphism/stereo.py +258 -0
  444. synkit/Graph/Morphism/stereo_morphism.py +383 -0
  445. synkit/Graph/Morphism/stereo_morphism_helpers.py +957 -0
  446. synkit/Graph/Morphism/stereo_morphism_types.py +117 -0
  447. synkit/Graph/Stereo/__init__.py +376 -0
  448. synkit/Graph/Stereo/_descriptor_core.py +207 -0
  449. synkit/Graph/Stereo/_descriptor_serialization.py +138 -0
  450. synkit/Graph/Stereo/canonical.py +648 -0
  451. synkit/Graph/Stereo/changes.py +964 -0
  452. synkit/Graph/Stereo/composition.py +391 -0
  453. synkit/Graph/Stereo/configured.py +946 -0
  454. synkit/Graph/Stereo/couplings.py +245 -0
  455. synkit/Graph/Stereo/descriptors.py +1030 -0
  456. synkit/Graph/Stereo/enumeration.py +597 -0
  457. synkit/Graph/Stereo/extended_descriptors.py +406 -0
  458. synkit/Graph/Stereo/global_configured.py +152 -0
  459. synkit/Graph/Stereo/global_stereo.py +304 -0
  460. synkit/Graph/Stereo/identity.py +648 -0
  461. synkit/Graph/Stereo/interchange.py +496 -0
  462. synkit/Graph/Stereo/legacy.py +460 -0
  463. synkit/Graph/Stereo/matching.py +899 -0
  464. synkit/Graph/Stereo/native.py +154 -0
  465. synkit/Graph/Stereo/orbits.py +668 -0
  466. synkit/Graph/Stereo/outcomes.py +99 -0
  467. synkit/Graph/Stereo/pairwise.py +350 -0
  468. synkit/Graph/Stereo/rdkit_adapter.py +662 -0
  469. synkit/Graph/Stereo/rdkit_identity.py +118 -0
  470. synkit/Graph/Stereo/rdkit_resonance.py +140 -0
  471. synkit/Graph/Stereo/reaction_interchange.py +266 -0
  472. synkit/Graph/Stereo/semantics.py +296 -0
  473. synkit/Graph/Stereo/supports.py +257 -0
  474. synkit/Graph/Stereo/wire.py +339 -0
  475. synkit/Graph/Wildcard/__init__.py +0 -0
  476. synkit/Graph/Wildcard/fuse_graph.py +156 -0
  477. synkit/Graph/Wildcard/graph_wc.py +303 -0
  478. synkit/Graph/Wildcard/its_merge.py +857 -0
  479. synkit/Graph/Wildcard/radwc.py +114 -0
  480. synkit/Graph/Wildcard/wc_matcher.py +576 -0
  481. synkit/Graph/Wildcard/wildcard.py +225 -0
  482. synkit/Graph/__init__.py +17 -0
  483. synkit/Graph/canon_graph.py +14 -0
  484. synkit/Graph/syn_graph.py +199 -0
  485. synkit/Graph/utils.py +324 -0
  486. synkit/IO/__init__.py +4 -0
  487. synkit/IO/chem_converter.py +1059 -0
  488. synkit/IO/combinatorial/__init__.py +8 -0
  489. synkit/IO/combinatorial/gml_to_graph.py +223 -0
  490. synkit/IO/combinatorial/graph_to_gml.py +280 -0
  491. synkit/IO/combinatorial/graph_to_smarts.py +186 -0
  492. synkit/IO/combinatorial/smarts_expander.py +147 -0
  493. synkit/IO/combinatorial/smarts_generalizer.py +133 -0
  494. synkit/IO/combinatorial/smarts_to_graph.py +179 -0
  495. synkit/IO/conversion.py +42 -0
  496. synkit/IO/data_io.py +269 -0
  497. synkit/IO/data_process.py +143 -0
  498. synkit/IO/debug.py +73 -0
  499. synkit/IO/gml_to_nx.py +150 -0
  500. synkit/IO/graph_to_mol.py +199 -0
  501. synkit/IO/mol_to_graph.py +1105 -0
  502. synkit/IO/mol_to_graph_chemistry.py +545 -0
  503. synkit/IO/nx_to_gml.py +209 -0
  504. synkit/IO/smiles_to_id.py +123 -0
  505. synkit/Mechanism/__init__.py +151 -0
  506. synkit/Mechanism/adapters.py +136 -0
  507. synkit/Mechanism/audit.py +323 -0
  508. synkit/Mechanism/benchmark.py +638 -0
  509. synkit/Mechanism/compatibility.py +13 -0
  510. synkit/Mechanism/drawing.py +85 -0
  511. synkit/Mechanism/electron_models.py +725 -0
  512. synkit/Mechanism/equivalence.py +521 -0
  513. synkit/Mechanism/evidence.py +262 -0
  514. synkit/Mechanism/interchange.py +141 -0
  515. synkit/Mechanism/model.py +918 -0
  516. synkit/Mechanism/neighbor_stereo.py +312 -0
  517. synkit/Mechanism/radical_data.py +740 -0
  518. synkit/Mechanism/replay.py +986 -0
  519. synkit/Mechanism/schema.py +264 -0
  520. synkit/Mechanism/stereo_state.py +416 -0
  521. synkit/Mechanism/symbols.py +91 -0
  522. synkit/Rule/Apply/__init__.py +35 -0
  523. synkit/Rule/Apply/conditions.py +45 -0
  524. synkit/Rule/Apply/dpo.py +998 -0
  525. synkit/Rule/Apply/rule_matcher.py +218 -0
  526. synkit/Rule/Apply/validation.py +354 -0
  527. synkit/Rule/Compose/__init__.py +106 -0
  528. synkit/Rule/Compose/_identity.py +43 -0
  529. synkit/Rule/Compose/composition.py +976 -0
  530. synkit/Rule/Compose/laws.py +439 -0
  531. synkit/Rule/Compose/search.py +771 -0
  532. synkit/Rule/Compose/synrule.py +170 -0
  533. synkit/Rule/Modify/__init__.py +0 -0
  534. synkit/Rule/Modify/implict_rule.py +56 -0
  535. synkit/Rule/Modify/longest_path.py +92 -0
  536. synkit/Rule/Modify/molecule_rule.py +115 -0
  537. synkit/Rule/Modify/prune_templates.py +78 -0
  538. synkit/Rule/Modify/rule_utils.py +194 -0
  539. synkit/Rule/Modify/strip_rule.py +97 -0
  540. synkit/Rule/__init__.py +28 -0
  541. synkit/Rule/generic_stereo.py +884 -0
  542. synkit/Rule/stereo_identity.py +271 -0
  543. synkit/Rule/syn_rule.py +1090 -0
  544. synkit/Synthesis/MSR/__init__.py +0 -0
  545. synkit/Synthesis/MSR/path_finder.py +239 -0
  546. synkit/Synthesis/Metrics/__init__.py +0 -0
  547. synkit/Synthesis/Metrics/_base.py +52 -0
  548. synkit/Synthesis/Metrics/_plot.py +129 -0
  549. synkit/Synthesis/Metrics/_ranking.py +179 -0
  550. synkit/Synthesis/RBL/__init__.py +80 -0
  551. synkit/Synthesis/RBL/engine.py +1249 -0
  552. synkit/Synthesis/RBL/matching.py +652 -0
  553. synkit/Synthesis/RBL/overlap.py +336 -0
  554. synkit/Synthesis/RBL/pipeline.py +376 -0
  555. synkit/Synthesis/RBL/policy.py +139 -0
  556. synkit/Synthesis/RBL/proof.py +370 -0
  557. synkit/Synthesis/RBL/reaction.py +506 -0
  558. synkit/Synthesis/RBL/state.py +169 -0
  559. synkit/Synthesis/RBL/validation.py +1060 -0
  560. synkit/Synthesis/Reactor/__init__.py +60 -0
  561. synkit/Synthesis/Reactor/core/__init__.py +1 -0
  562. synkit/Synthesis/Reactor/core/engine.py +967 -0
  563. synkit/Synthesis/Reactor/core/product.py +621 -0
  564. synkit/Synthesis/Reactor/core/rewrite.py +561 -0
  565. synkit/Synthesis/Reactor/core/strategy.py +44 -0
  566. synkit/Synthesis/Reactor/matching/__init__.py +1 -0
  567. synkit/Synthesis/Reactor/matching/mixin.py +920 -0
  568. synkit/Synthesis/Reactor/matching/policy.py +345 -0
  569. synkit/Synthesis/Reactor/matching/symmetry.py +309 -0
  570. synkit/Synthesis/Reactor/output/__init__.py +1 -0
  571. synkit/Synthesis/Reactor/output/deduplication.py +703 -0
  572. synkit/Synthesis/Reactor/output/policy.py +16 -0
  573. synkit/Synthesis/Reactor/output/serialization.py +485 -0
  574. synkit/Synthesis/Reactor/output/structural.py +893 -0
  575. synkit/Synthesis/Reactor/stereo/__init__.py +1 -0
  576. synkit/Synthesis/Reactor/stereo/assignment.py +35 -0
  577. synkit/Synthesis/Reactor/stereo/mixin.py +780 -0
  578. synkit/Synthesis/Reactor/variants/__init__.py +1 -0
  579. synkit/Synthesis/Reactor/variants/imbalanced.py +180 -0
  580. synkit/Synthesis/Reactor/variants/partial.py +84 -0
  581. synkit/Synthesis/Reactor/workflow/__init__.py +1 -0
  582. synkit/Synthesis/Reactor/workflow/batch.py +550 -0
  583. synkit/Synthesis/Reactor/workflow/benchmark.py +143 -0
  584. synkit/Synthesis/Reactor/workflow/postprocess.py +267 -0
  585. synkit/Synthesis/Reactor/workflow/rule_filter.py +210 -0
  586. synkit/Synthesis/__init__.py +0 -0
  587. synkit/Synthesis/reactor_utils.py +316 -0
  588. synkit/Utils/__init__.py +0 -0
  589. synkit/Utils/utils.py +172 -0
  590. synkit/Vis/__init__.py +91 -0
  591. synkit/Vis/crn/__init__.py +40 -0
  592. synkit/Vis/epd/__init__.py +20 -0
  593. synkit/Vis/epd/arrows.py +425 -0
  594. synkit/Vis/epd/chem.py +197 -0
  595. synkit/Vis/epd/constants.py +83 -0
  596. synkit/Vis/epd/layout.py +283 -0
  597. synkit/Vis/epd/mapping.py +51 -0
  598. synkit/Vis/epd/models.py +136 -0
  599. synkit/Vis/epd/render.py +543 -0
  600. synkit/Vis/epd/utils.py +108 -0
  601. synkit/Vis/epd/visualizer.py +811 -0
  602. synkit/Vis/graph_visualizer.py +375 -0
  603. synkit/Vis/its/__init__.py +19 -0
  604. synkit/Vis/its/drawer.py +615 -0
  605. synkit/Vis/its_drawer.py +35 -0
  606. synkit/Vis/molecule/__init__.py +31 -0
  607. synkit/Vis/molecule/drawer.py +563 -0
  608. synkit/Vis/molecule/reaction.py +285 -0
  609. synkit/Vis/molecule_drawer.py +33 -0
  610. synkit/Vis/mtg/__init__.py +17 -0
  611. synkit/Vis/mtg/drawer.py +769 -0
  612. synkit/Vis/mtg_drawer.py +34 -0
  613. synkit/Vis/pdf_writer.py +146 -0
  614. synkit/Vis/reaction/__init__.py +19 -0
  615. synkit/Vis/reaction/rule.py +141 -0
  616. synkit/Vis/reaction/rxn.py +532 -0
  617. synkit/Vis/reaction_drawer.py +41 -0
  618. synkit/Vis/space/__init__.py +17 -0
  619. synkit/Vis/space/chemical.py +80 -0
  620. synkit/Vis/space/embedding.py +87 -0
  621. synkit/Vis/vis_synedu/Vis/__init__.py +7 -0
  622. synkit/Vis/vis_synedu/Vis/dpo.py +862 -0
  623. synkit/Vis/vis_synedu/rxn_vis.py +377 -0
  624. synkit/Vis/vis_synedu/vis.py +501 -0
  625. synkit/Vis/visual_drawer.py +215 -0
  626. synkit/Vis/visual_model.py +520 -0
  627. synkit/__init__.py +3 -0
  628. synkit/_native.py +91 -0
  629. synkit/examples.py +50 -0
  630. synkit/version.py +23 -0
  631. synkit-1.6.5.dist-info/METADATA +156 -0
  632. synkit-1.6.5.dist-info/RECORD +636 -0
  633. synkit-1.6.5.dist-info/WHEEL +4 -0
  634. synkit-1.6.5.dist-info/licenses/LICENSE +21 -0
  635. synkit-1.6.5.dist-info/licenses/synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/COPYRIGHT +44 -0
  636. synkit-1.6.5.dist-info/licenses/synkit/Chem/Mapper/solvers/native/cpp/src/symmetry/vendor/nauty/LICENSE-2.0.txt +202 -0
@@ -0,0 +1,2803 @@
1
+ // Exact integer-distance candidate enumeration for two-sided symmetry analysis.
2
+ // ABI 3: all matrices and indices are validated by the Python wrapper.
3
+ #include <algorithm>
4
+ #include <array>
5
+ #include <chrono>
6
+ #include <cmath>
7
+ #include <cstdint>
8
+ #include <map>
9
+ #include <memory>
10
+ #include <numeric>
11
+ #include <queue>
12
+ #include <stdexcept>
13
+ #include <unordered_set>
14
+ #include <vector>
15
+ using Perm = std::vector<int>;
16
+ using Group = std::vector<Perm>;
17
+ using Clock = std::chrono::steady_clock;
18
+ using Callback = int (*)(const int *, void *);
19
+ using CostCallback = int (*)(const int *, int, void *);
20
+ using PrefixBoundCallback = int64_t (*)(const int *, const int *, int, void *);
21
+ constexpr int INF = 100000000;
22
+ // Full PABS search only; canonicalization and pattern-cache limits below are
23
+ // separate contracts and deliberately remain at 256 vertices.
24
+ constexpr int SYNKIT_PABS_MAX_VERTICES = 512;
25
+ // Hash only an initial branching prefix: each surviving subtree belongs to
26
+ // exactly one shard. Leaves with fewer choices are partitioned at the leaf.
27
+ constexpr int SHARD_BRANCHES = 24;
28
+ struct VectorHash {
29
+ std::size_t operator()(const Perm &p) const noexcept {
30
+ std::size_t h = 0;
31
+ for (int x : p)
32
+ h = h * 1315423911u + static_cast<unsigned>(x + 1);
33
+ return h;
34
+ }
35
+ };
36
+ static Perm compose(const Perm &a, const Perm &b) {
37
+ Perm p(a.size());
38
+ for (size_t i = 0; i < a.size(); ++i)
39
+ p[i] = b[a[i]];
40
+ return p;
41
+ }
42
+ static std::shared_ptr<Group> stabilize(std::shared_ptr<Group> gs, int point,
43
+ int n, bool *complete = nullptr) {
44
+ bool fixed = true;
45
+ for (const auto &g : *gs)
46
+ if (g[point] != point) {
47
+ fixed = false;
48
+ break;
49
+ }
50
+ if (fixed)
51
+ return gs;
52
+ Perm identity(n);
53
+ std::iota(identity.begin(), identity.end(), 0);
54
+ std::vector<Perm> trans(n), inv(n);
55
+ std::vector<int> orbit{point};
56
+ trans[point] = identity;
57
+ int work = 0;
58
+ for (size_t k = 0; k < orbit.size(); ++k)
59
+ for (const auto &g : *gs) {
60
+ if (++work > 100000) {
61
+ if (complete)
62
+ *complete = false;
63
+ return std::make_shared<Group>();
64
+ }
65
+ int x = orbit[k], y = g[x];
66
+ if (trans[y].empty()) {
67
+ trans[y] = compose(trans[x], g);
68
+ orbit.push_back(y);
69
+ }
70
+ }
71
+ for (int x : orbit) {
72
+ inv[x].resize(n);
73
+ for (int i = 0; i < n; ++i)
74
+ inv[x][trans[x][i]] = i;
75
+ }
76
+ auto out = std::make_shared<Group>();
77
+ std::unordered_set<Perm, VectorHash> seen;
78
+ for (int x : orbit)
79
+ for (const auto &g : *gs) {
80
+ Perm h = compose(compose(trans[x], g), inv[g[x]]);
81
+ if (h != identity && seen.insert(h).second) {
82
+ out->push_back(std::move(h));
83
+ if (out->size() > 512) {
84
+ if (complete)
85
+ *complete = false;
86
+ return std::make_shared<Group>();
87
+ }
88
+ }
89
+ }
90
+ return out;
91
+ }
92
+
93
+ // Experimental, bounded SR-GED-style residual minimum oracle. A separator
94
+ // removes all SOURCE edges between the two child row sets. Their remaining
95
+ // cross cost is then the absolute TARGET cut mass, independent of the child
96
+ // bijections. Unary costs already include every previously assigned atom.
97
+ // Completing this recursion proves an exact relaxed-domain residual minimum;
98
+ // a work limit, deadline or unsuitable separator returns unknown, never a
99
+ // bound. This bounded strategy does not implement the full SR-GED complexity
100
+ // theorem.
101
+ struct SeparatorResidualMinimum {
102
+ struct Stop {};
103
+ int n;
104
+ const int *a, *b;
105
+ Perm unary;
106
+ int64_t max_states, states = 0;
107
+ Clock::time_point started;
108
+ double seconds;
109
+ SeparatorResidualMinimum(int size, const int *aa, const int *bb, Perm costs,
110
+ int64_t limit, Clock::time_point start, double secs)
111
+ : n(size), a(aa), b(bb), unary(std::move(costs)), max_states(limit),
112
+ started(start), seconds(secs) {}
113
+ void tick() {
114
+ if (++states > max_states)
115
+ throw Stop{};
116
+ if ((states & 63) == 1 &&
117
+ std::chrono::duration<double>(Clock::now() - started).count() >=
118
+ seconds)
119
+ throw Stop{};
120
+ }
121
+ static int size(unsigned mask) {
122
+ int answer = 0;
123
+ for (; mask; mask &= mask - 1)
124
+ ++answer;
125
+ return answer;
126
+ }
127
+ static int first(unsigned mask) {
128
+ int bit = 0;
129
+ while (!(mask & (1u << bit)))
130
+ ++bit;
131
+ return bit;
132
+ }
133
+ bool split(unsigned rows, unsigned &separator, unsigned &left,
134
+ unsigned &right) {
135
+ const int bound = 2 * size(rows) / 3;
136
+ auto attempt = [&](unsigned removed) {
137
+ tick();
138
+ std::vector<unsigned> components;
139
+ unsigned unseen = rows & ~removed;
140
+ while (unseen) {
141
+ unsigned component = 0, pending = unseen & (~unseen + 1u);
142
+ unseen &= ~pending;
143
+ while (pending) {
144
+ int x = first(pending);
145
+ pending &= pending - 1;
146
+ component |= 1u << x;
147
+ unsigned remaining = unseen;
148
+ while (remaining) {
149
+ int y = first(remaining);
150
+ remaining &= remaining - 1;
151
+ if (a[x * n + y] != 0) {
152
+ unseen &= ~(1u << y);
153
+ pending |= 1u << y;
154
+ }
155
+ }
156
+ }
157
+ components.push_back(component);
158
+ }
159
+ if (components.size() < 2)
160
+ return false;
161
+ for (unsigned selected = 1; selected < (1u << components.size()) - 1;
162
+ ++selected) {
163
+ tick();
164
+ if (!(selected & 1u))
165
+ continue;
166
+ unsigned one = 0;
167
+ for (size_t k = 0; k < components.size(); ++k)
168
+ if (selected & (1u << k))
169
+ one |= components[k];
170
+ unsigned two = rows & ~(removed | one);
171
+ if (one && two && std::max(size(one), size(two)) <= bound) {
172
+ separator = removed;
173
+ left = one;
174
+ right = two;
175
+ return true;
176
+ }
177
+ }
178
+ return false;
179
+ };
180
+ if (attempt(0))
181
+ return true;
182
+ for (unsigned bits = rows; bits; bits &= bits - 1) {
183
+ int x = first(bits);
184
+ if (attempt(1u << x))
185
+ return true;
186
+ }
187
+ for (unsigned bits = rows; bits; bits &= bits - 1) {
188
+ int x = first(bits);
189
+ for (unsigned other = bits & (bits - 1); other; other &= other - 1) {
190
+ int y = first(other);
191
+ if (attempt((1u << x) | (1u << y)))
192
+ return true;
193
+ }
194
+ }
195
+ return false;
196
+ }
197
+ int brute(unsigned rows, unsigned columns, const Perm &costs) {
198
+ int best = INF;
199
+ Perm mapping(n, -1);
200
+ auto visit = [&](auto &&self, unsigned remaining, unsigned free,
201
+ unsigned assigned, int value) -> void {
202
+ tick();
203
+ if (value >= best)
204
+ return;
205
+ if (!remaining) {
206
+ best = value;
207
+ return;
208
+ }
209
+ int x = first(remaining);
210
+ for (unsigned bits = free; bits; bits &= bits - 1) {
211
+ int y = first(bits), delta = costs[x * n + y];
212
+ if (delta >= INF / 2)
213
+ continue;
214
+ for (unsigned previous = assigned; previous; previous &= previous - 1) {
215
+ int p = first(previous);
216
+ delta += 2 * std::abs(a[x * n + p] - b[y * n + mapping[p]]);
217
+ }
218
+ mapping[x] = y;
219
+ self(self, remaining & ~(1u << x), free & ~(1u << y),
220
+ assigned | (1u << x), value + delta);
221
+ mapping[x] = -1;
222
+ }
223
+ };
224
+ visit(visit, rows, columns, 0, 0);
225
+ return best;
226
+ }
227
+ int solve(unsigned rows, unsigned columns, const Perm &costs) {
228
+ tick();
229
+ int count = size(rows);
230
+ if (!count)
231
+ return 0;
232
+ for (unsigned bits = rows; bits; bits &= bits - 1) {
233
+ int x = first(bits);
234
+ bool possible = false;
235
+ for (unsigned choices = columns; choices; choices &= choices - 1) {
236
+ int y = first(choices);
237
+ if (costs[x * n + y] < INF / 2) {
238
+ possible = true;
239
+ break;
240
+ }
241
+ }
242
+ if (!possible)
243
+ return INF;
244
+ }
245
+ if (count <= 4)
246
+ return brute(rows, columns, costs);
247
+ unsigned separator, left, right;
248
+ if (!split(rows, separator, left, right))
249
+ throw Stop{};
250
+ Perm assigned(n, -1);
251
+ int best = INF, left_count = size(left);
252
+ auto extend = [&](auto &&self, unsigned remaining, unsigned free,
253
+ int offset) -> void {
254
+ tick();
255
+ if (offset >= best)
256
+ return;
257
+ if (remaining) {
258
+ int x = first(remaining);
259
+ for (unsigned bits = free; bits; bits &= bits - 1) {
260
+ int y = first(bits), delta = costs[x * n + y];
261
+ if (delta >= INF / 2)
262
+ continue;
263
+ for (unsigned previous = separator & ~remaining; previous;
264
+ previous &= previous - 1) {
265
+ int p = first(previous);
266
+ delta += 2 * std::abs(a[x * n + p] - b[y * n + assigned[p]]);
267
+ }
268
+ assigned[x] = y;
269
+ self(self, remaining & ~(1u << x), free & ~(1u << y), offset + delta);
270
+ assigned[x] = -1;
271
+ }
272
+ return;
273
+ }
274
+ Perm updated = costs;
275
+ for (unsigned bits = rows & ~separator; bits; bits &= bits - 1) {
276
+ int x = first(bits);
277
+ for (unsigned choices = free; choices; choices &= choices - 1) {
278
+ int y = first(choices);
279
+ if (updated[x * n + y] >= INF / 2)
280
+ continue;
281
+ for (unsigned previous = separator; previous;
282
+ previous &= previous - 1) {
283
+ int p = first(previous);
284
+ updated[x * n + y] +=
285
+ 2 * std::abs(a[x * n + p] - b[y * n + assigned[p]]);
286
+ }
287
+ }
288
+ }
289
+ for (unsigned one = free; one; one = (one - 1) & free) {
290
+ tick();
291
+ if (size(one) != left_count)
292
+ continue;
293
+ unsigned two = free & ~one;
294
+ int cut = 0;
295
+ for (unsigned l = one; l; l &= l - 1) {
296
+ int i = first(l);
297
+ for (unsigned r = two; r; r &= r - 1) {
298
+ int j = first(r);
299
+ cut += 2 * std::abs(b[i * n + j]);
300
+ }
301
+ }
302
+ if (offset + cut >= best)
303
+ continue;
304
+ int lcost = solve(left, one, updated);
305
+ if (lcost >= INF / 2 || offset + cut + lcost >= best)
306
+ continue;
307
+ int rcost = solve(right, two, updated);
308
+ if (rcost >= INF / 2)
309
+ continue;
310
+ best = std::min(best, offset + cut + lcost + rcost);
311
+ }
312
+ };
313
+ extend(extend, separator, columns, 0);
314
+ return best;
315
+ }
316
+ bool run(int &value) {
317
+ try {
318
+ value = solve((1u << n) - 1, (1u << n) - 1, unary);
319
+ return true;
320
+ } catch (const Stop &) {
321
+ return false;
322
+ }
323
+ }
324
+ };
325
+
326
+ // FULL-objective lower bound, in doubled-CD units. This is not a residual
327
+ // cost and must never be added to the traversal's committed quarter cost.
328
+
329
+ // Exact minima of -2min(w,B[j,l])-first[j]-second[l], j!=l.
330
+ // Zero target entries need only the two largest opposite potentials.
331
+ static void sparse_pair_marginals(
332
+ int m, int64_t weight, const std::vector<int64_t> &first,
333
+ const std::vector<int64_t> &second, const Perm &own, const Perm &other,
334
+ const std::vector<std::vector<std::pair<int, int64_t>>> &positive,
335
+ std::vector<int64_t> &out, int64_t forbidden = INT64_MAX) {
336
+ out.assign(m, forbidden);
337
+ std::vector<char> allowed(m, 0);
338
+ int best = -1, next = -1;
339
+ for (int l : other) {
340
+ allowed[l] = 1;
341
+ if (best < 0 || second[l] > second[best]) {
342
+ next = best;
343
+ best = l;
344
+ } else if (next < 0 || second[l] > second[next])
345
+ next = l;
346
+ }
347
+ for (int j : own) {
348
+ const int l = best == j ? next : best;
349
+ if (l >= 0)
350
+ out[j] = -first[j] - second[l];
351
+ for (auto edge : positive[j])
352
+ if (edge.first != j && allowed[edge.first])
353
+ out[j] = std::min(out[j], -2 * std::min(weight, edge.second) -
354
+ first[j] - second[edge.first]);
355
+ }
356
+ }
357
+
358
+ struct RootDualBranchCertificate {
359
+ static constexpr int64_t forbidden = INT64_MAX / 16;
360
+ struct Factor {
361
+ int i, k;
362
+ int64_t weight, minimum;
363
+ std::vector<int64_t> first, second, conditional_i, conditional_k;
364
+ };
365
+ int n = 0, m = 0;
366
+ int64_t constant = 0, root_value = INT64_MIN;
367
+ bool ready = false, normalized = false;
368
+ Perm rows, columns, column_index, types_r, types_p;
369
+ std::vector<int64_t> reduced_unary, target;
370
+ std::vector<Perm> row_columns;
371
+ std::vector<Factor> factors;
372
+ bool finalize(Clock::time_point deadline) {
373
+ ready = false;
374
+ if (!n)
375
+ return false;
376
+ std::vector<Perm> domains(m);
377
+ std::vector<std::vector<std::pair<int, int64_t>>> positive(m);
378
+ for (int i = 0; i < m; ++i) {
379
+ if (Clock::now() >= deadline)
380
+ return false;
381
+ for (int j = 0; j < m; ++j) {
382
+ if (reduced_unary[i * m + j] < forbidden)
383
+ domains[i].push_back(j);
384
+ if (target[i * m + j] > 0)
385
+ positive[i].push_back({j, target[i * m + j]});
386
+ }
387
+ }
388
+ for (auto &e : factors) {
389
+ if (Clock::now() >= deadline)
390
+ return false;
391
+ sparse_pair_marginals(m, e.weight, e.first, e.second, domains[e.i],
392
+ domains[e.k], positive, e.conditional_i, forbidden);
393
+ sparse_pair_marginals(m, e.weight, e.second, e.first, domains[e.k],
394
+ domains[e.i], positive, e.conditional_k, forbidden);
395
+ int64_t minimum = forbidden;
396
+ for (int j : domains[e.i])
397
+ minimum = std::min(minimum, e.conditional_i[j]);
398
+ if (minimum != e.minimum)
399
+ throw std::runtime_error("Invalid sparse conditioned factor minimum");
400
+ }
401
+ if (Clock::now() >= deadline)
402
+ return false;
403
+ if (!normalized) {
404
+ for (const auto &e : factors)
405
+ constant += e.minimum;
406
+ normalized = true;
407
+ }
408
+ ready = true;
409
+ return true;
410
+ }
411
+
412
+ int64_t triplet_floor(Clock::time_point deadline, int64_t *stats) const {
413
+ const auto begun = Clock::now();
414
+ std::fill(stats, stats + 8, 0);
415
+ stats[5] = root_value;
416
+ if (!ready || m < 3 || factors.size() < 2) {
417
+ stats[6] = root_value;
418
+ return root_value;
419
+ }
420
+ struct Two {
421
+ int first = -1, second = -1;
422
+ int64_t a = forbidden, b = forbidden;
423
+ };
424
+ struct Cut {
425
+ int center, left, right;
426
+ int64_t gain;
427
+ };
428
+ std::vector<Perm> incident(m), positive(m);
429
+ for (size_t k = 0; k < factors.size(); ++k) {
430
+ incident[factors[k].i].push_back(k);
431
+ incident[factors[k].k].push_back(k);
432
+ }
433
+ for (int j = 0; j < m; ++j)
434
+ for (int l = 0; l < m; ++l)
435
+ if (target[j * m + l] > 0)
436
+ positive[j].push_back(l);
437
+ std::vector<std::array<std::vector<Two>, 2>> table(factors.size());
438
+ std::vector<std::array<bool, 2>> done(factors.size());
439
+ auto prepare = [&](int index, int side) {
440
+ if (done[index][side])
441
+ return true;
442
+ if (Clock::now() >= deadline)
443
+ return false;
444
+ const auto &e = factors[index];
445
+ const int center = side ? e.k : e.i, outer = side ? e.i : e.k;
446
+ const auto &own = side ? e.second : e.first;
447
+ const auto &other = side ? e.first : e.second;
448
+ Perm leading(3, -1);
449
+ std::vector<char> allowed(m, 0);
450
+ auto potential = [&](int j) {
451
+ return other[j] - reduced_unary[outer * m + j];
452
+ };
453
+ for (int l : row_columns[outer]) {
454
+ allowed[l] = 1;
455
+ for (int p = 0; p < 3; ++p)
456
+ if (leading[p] < 0 || potential(l) > potential(leading[p])) {
457
+ for (int q = 2; q > p; --q)
458
+ leading[q] = leading[q - 1];
459
+ leading[p] = l;
460
+ break;
461
+ }
462
+ }
463
+ auto &result = table[index][side];
464
+ result.resize(m);
465
+ for (int j : row_columns[center]) {
466
+ auto accept = [&](int l) {
467
+ if (l < 0 || l == j)
468
+ return;
469
+ const int64_t value = -2 * std::min(e.weight, target[j * m + l]) -
470
+ own[j] - other[l] - e.minimum +
471
+ reduced_unary[outer * m + l];
472
+ if (value < 0)
473
+ throw std::runtime_error("Negative normalized triplet term");
474
+ auto &t = result[j];
475
+ if (l == t.first || l == t.second)
476
+ return;
477
+ if (value < t.a || (value == t.a && (t.first < 0 || l < t.first))) {
478
+ t.second = t.first;
479
+ t.b = t.a;
480
+ t.first = l;
481
+ t.a = value;
482
+ } else if (value < t.b ||
483
+ (value == t.b && (t.second < 0 || l < t.second))) {
484
+ t.second = l;
485
+ t.b = value;
486
+ }
487
+ };
488
+ // Two largest legal baseline potentials after excluding j suffice;
489
+ // every target-positive candidate is also included at its exact cost.
490
+ for (int l : leading)
491
+ accept(l);
492
+ for (int l : positive[j])
493
+ if (allowed[l])
494
+ accept(l);
495
+ }
496
+ done[index][side] = true;
497
+ return true;
498
+ };
499
+ std::vector<Cut> cuts;
500
+ bool interrupted = false;
501
+ for (int center = 0; center < m && !interrupted; ++center) {
502
+ const auto &edges = incident[center];
503
+ for (size_t a = 0; a < edges.size() && !interrupted; ++a)
504
+ for (size_t b = a + 1; b < edges.size(); ++b) {
505
+ if (Clock::now() >= deadline) {
506
+ interrupted = true;
507
+ break;
508
+ }
509
+ const int first = edges[a], second = edges[b];
510
+ const auto &e = factors[first];
511
+ const auto &f = factors[second];
512
+ const int es = e.i == center ? 0 : 1, fs = f.i == center ? 0 : 1;
513
+ const int left = es ? e.i : e.k, right = fs ? f.i : f.k;
514
+ if (left == right)
515
+ continue;
516
+ if (!prepare(first, es) || !prepare(second, fs)) {
517
+ interrupted = true;
518
+ break;
519
+ }
520
+ int64_t minimum = forbidden;
521
+ for (int j : row_columns[center]) {
522
+ const auto &x = table[first][es][j];
523
+ const auto &y = table[second][fs][j];
524
+ if (x.first < 0 || y.first < 0)
525
+ continue;
526
+ const int64_t pair =
527
+ x.first == y.first ? std::min(x.b + y.a, x.a + y.b) : x.a + y.a;
528
+ if (pair < forbidden)
529
+ minimum = std::min(minimum, pair + reduced_unary[center * m + j]);
530
+ }
531
+ ++stats[0];
532
+ if (minimum > 0 && minimum < forbidden) {
533
+ cuts.push_back({center, left, right, minimum});
534
+ ++stats[1];
535
+ }
536
+ }
537
+ }
538
+ std::sort(cuts.begin(), cuts.end(), [](const Cut &a, const Cut &b) {
539
+ if (a.gain != b.gain)
540
+ return a.gain > b.gain;
541
+ if (a.center != b.center)
542
+ return a.center < b.center;
543
+ if (a.left != b.left)
544
+ return a.left < b.left;
545
+ return a.right < b.right;
546
+ });
547
+ std::vector<char> used(m, 0);
548
+ int64_t gain = 0;
549
+ for (const auto &cut : cuts)
550
+ if (!used[cut.center] && !used[cut.left] && !used[cut.right]) {
551
+ used[cut.center] = used[cut.left] = used[cut.right] = 1;
552
+ gain += cut.gain;
553
+ ++stats[2];
554
+ }
555
+ stats[3] = gain;
556
+ stats[4] = std::chrono::duration_cast<std::chrono::nanoseconds>(
557
+ Clock::now() - begun)
558
+ .count();
559
+ stats[6] = root_value + gain;
560
+ stats[7] = interrupted ? 1 : 0;
561
+ return root_value + gain;
562
+ }
563
+
564
+ int64_t lower_bound(const int *mapping, const int *used,
565
+ int quarter_target = -1,
566
+ int64_t cached_factor_total = -1) const {
567
+ int64_t value =
568
+ constant + (cached_factor_total >= 0 ? cached_factor_total : 0);
569
+ // Every remaining contribution is nonnegative after exact factor minima
570
+ // are absorbed. A threshold return is therefore still a FULL lower bound.
571
+ if (quarter_target >= 0 && 2 * value > quarter_target)
572
+ return value;
573
+ std::array<int, 128> images;
574
+ for (int i = 0; i < m; ++i) {
575
+ images[i] = -1;
576
+ const int image = mapping[rows[i]];
577
+ if (image >= 0) {
578
+ int j = image < n ? column_index[image] : -1;
579
+ if (j < 0 || reduced_unary[i * m + j] >= forbidden)
580
+ return forbidden;
581
+ images[i] = j;
582
+ value += reduced_unary[i * m + j];
583
+ } else {
584
+ int64_t minimum = forbidden;
585
+ for (int j : row_columns[i])
586
+ if (!used[columns[j]]) {
587
+ minimum = std::min(minimum, reduced_unary[i * m + j]);
588
+ if (!minimum)
589
+ break;
590
+ }
591
+ if (minimum >= forbidden)
592
+ return forbidden;
593
+ value += minimum;
594
+ }
595
+ if (quarter_target >= 0 && 2 * value > quarter_target)
596
+ return value;
597
+ }
598
+ if (cached_factor_total >= 0)
599
+ return value;
600
+ for (const auto &e : factors) {
601
+ const int j = images[e.i], l = images[e.k];
602
+ if (j >= 0 && l >= 0)
603
+ value += -2 * std::min(e.weight, target[j * m + l]) - e.first[j] -
604
+ e.second[l] - e.minimum;
605
+ else if (j >= 0)
606
+ value += e.conditional_i[j] - e.minimum;
607
+ else if (l >= 0)
608
+ value += e.conditional_k[l] - e.minimum;
609
+ if (quarter_target >= 0 && 2 * value > quarter_target)
610
+ return value;
611
+ }
612
+ return value;
613
+ }
614
+ };
615
+
616
+ // Performance scheduling only. Skipping a valid optional bound does not
617
+ // restrict any assignment, stabilizer, or permitted domain.
618
+ static bool conditioned_gap_allowed(int best4, int floor4, int step4) {
619
+ if (floor4 < 0 || step4 <= 0 || best4 < floor4)
620
+ return false;
621
+ return static_cast<int64_t>(best4) - floor4 <= 2LL * step4;
622
+ }
623
+
624
+ // Solver-owned, exact normalized factor sum. No domain or group is changed.
625
+ struct RootDualFactorCache {
626
+ const RootDualBranchCertificate *certificate;
627
+ Perm row_index, images;
628
+ std::vector<Perm> incident;
629
+ int64_t total = 0, updates = 0, touches = 0;
630
+ explicit RootDualFactorCache(const RootDualBranchCertificate *c,
631
+ const int *mapping)
632
+ : certificate(c) {
633
+ row_index.assign(c->n, -1);
634
+ images.assign(c->m, -1);
635
+ incident.resize(c->m);
636
+ for (int i = 0; i < c->m; ++i) {
637
+ row_index[c->rows[i]] = i;
638
+ int image = mapping[c->rows[i]];
639
+ if (image >= 0)
640
+ images[i] = c->column_index[image];
641
+ }
642
+ for (size_t k = 0; k < c->factors.size(); ++k) {
643
+ const auto &e = c->factors[k];
644
+ incident[e.i].push_back(k);
645
+ incident[e.k].push_back(k);
646
+ total += value(e, images[e.i], images[e.k]);
647
+ }
648
+ }
649
+ int64_t value(const RootDualBranchCertificate::Factor &e, int j,
650
+ int l) const {
651
+ if (j >= 0 && l >= 0)
652
+ return -2 * std::min(e.weight,
653
+ certificate->target[j * certificate->m + l]) -
654
+ e.first[j] - e.second[l] - e.minimum;
655
+ if (j >= 0)
656
+ return e.conditional_i[j] - e.minimum;
657
+ if (l >= 0)
658
+ return e.conditional_k[l] - e.minimum;
659
+ return 0;
660
+ }
661
+ void update(int atom, int image, int sign) {
662
+ const int i = row_index[atom];
663
+ if (i < 0)
664
+ return;
665
+ const int old = images[i],
666
+ next = sign > 0 ? certificate->column_index[image] : -1;
667
+ for (int k : incident[i]) {
668
+ const auto &e = certificate->factors[k];
669
+ const int j = images[e.i], l = images[e.k];
670
+ total +=
671
+ value(e, e.i == i ? next : j, e.k == i ? next : l) - value(e, j, l);
672
+ ++touches;
673
+ }
674
+ images[i] = next;
675
+ ++updates;
676
+ (void)old;
677
+ }
678
+ };
679
+ static thread_local int64_t conditioned_factor_cache_statistics[8] = {};
680
+
681
+ struct Solver {
682
+ int n, nt, nl, target;
683
+ double seconds;
684
+ int64_t cap;
685
+ int shard_index = 0, shards = 1;
686
+ // Legacy callers use an exact shell. Additive objective entry points widen
687
+ // this interval or shrink its upper end as better minimum witnesses arrive.
688
+ int lower_target;
689
+ bool minimize = false;
690
+ int best_cost = INF, cost_step = 1, global_floor = -1;
691
+ Perm best_mapping;
692
+ CostCallback cost_callback = nullptr;
693
+ const RootDualBranchCertificate *root_certificate = nullptr;
694
+ PrefixBoundCallback prefix_bound = nullptr;
695
+ void *prefix_bound_data = nullptr;
696
+ std::unique_ptr<RootDualFactorCache> factor_cache;
697
+ int64_t root_certificate_calls = 0, root_certificate_pruned = 0;
698
+ int64_t forced_closures = 0, leaf_full_recomputes = 0;
699
+ int64_t *diagnostic_gaps = nullptr;
700
+ bool separator_enabled = false;
701
+ int separator_limit = 10;
702
+ int64_t separator_max_states = 20000;
703
+ int64_t separator_calls = 0, separator_known = 0, separator_pruned = 0,
704
+ separator_states = 0;
705
+ const int *a, *b, *ca, *cb, *order,
706
+ *pred; // pred is reserved for ABI compatibility.
707
+ std::vector<int> mapping, used, cross, ha, hb, levels, row_order, row_min;
708
+ std::vector<char> permitted;
709
+ std::vector<Perm> nonzero_b_columns;
710
+ Callback callback;
711
+ void *userdata;
712
+ Clock::time_point started;
713
+ // Resumable jobs name a subtree by its deterministic sequence of images.
714
+ // Counting only nodes below that prefix guarantees progress even for tiny
715
+ // slices and long chains of forced assignments.
716
+ Perm prefix, path;
717
+ std::vector<Perm> frontier;
718
+ struct PrefixNode {
719
+ std::map<int, int> children;
720
+ bool terminal = false;
721
+ };
722
+ std::vector<PrefixNode> requests;
723
+ bool batch_mode = false, balance_slice = false;
724
+ int64_t replay_nodes = 0, new_nodes = 0;
725
+ int64_t slice_nodes = 0, work_nodes = 0;
726
+ Clock::time_point slice_started{};
727
+ int status = 0;
728
+ int64_t nodes = 0, leaves = 0, accepted = 0, pruned = 0;
729
+ Solver(int nn, int types, int lev, int tar, double sec, int64_t limit,
730
+ const int *aa, const int *bb, const int *cca, const int *ccb,
731
+ const int *ord, const int *pre, Callback call, void *data)
732
+ : n(nn), nt(types), nl(lev), target(tar), seconds(sec), cap(limit),
733
+ lower_target(tar), a(aa), b(bb), ca(cca), cb(ccb), order(ord),
734
+ pred(pre), mapping(n, -1), used(n, 0), cross(n * n, 0), ha(n * nt * nl),
735
+ hb(n * nt * nl), row_order(ord, ord + n), row_min(n, -1),
736
+ permitted(n * n, 1), callback(call), userdata(data),
737
+ started(Clock::now()) {
738
+ std::vector<int> values(a, a + n * n);
739
+ values.insert(values.end(), b, b + n * n);
740
+ std::sort(values.begin(), values.end());
741
+ values.erase(std::unique(values.begin(), values.end()), values.end());
742
+ if (static_cast<int>(values.size()) != nl + 1)
743
+ throw std::runtime_error("levels");
744
+ levels = values;
745
+ nonzero_b_columns.resize(n);
746
+ for (int image = 0; image < n; ++image)
747
+ for (int j = 0; j < n; ++j)
748
+ if (b[j * n + image] != 0)
749
+ nonzero_b_columns[image].push_back(j);
750
+ for (int i = 0; i < n; ++i)
751
+ for (int j = 0; j < n; ++j)
752
+ for (int k = 0; k < nl; ++k) {
753
+ if (a[i * n + j] >= levels[k + 1])
754
+ ++ha[(i * nt + ca[j]) * nl + k];
755
+ if (b[i * n + j] >= levels[k + 1])
756
+ ++hb[(i * nt + cb[j]) * nl + k];
757
+ }
758
+ }
759
+ bool expired() {
760
+ return std::chrono::duration<double>(Clock::now() - started).count() >=
761
+ seconds;
762
+ }
763
+ bool accepts(int cost) const {
764
+ return cost >= lower_target && cost <= target;
765
+ }
766
+ void update(int atom, int image, int sign, int depth, const Perm &columns) {
767
+ if (factor_cache)
768
+ factor_cache->update(atom, image, sign);
769
+ // row_order[depth+1:] and columns minus image are exactly the unassigned
770
+ // vertices, both on entry and after the recursive row-order restoration.
771
+ for (int pos = depth + 1; pos < n; ++pos) {
772
+ int i = row_order[pos];
773
+ for (int k = 0; k < nl; ++k)
774
+ if (a[i * n + atom] >= levels[k + 1])
775
+ ha[(i * nt + ca[atom]) * nl + k] -= sign;
776
+ const int weight = a[i * n + atom];
777
+ if (weight != 0) {
778
+ for (int j : columns)
779
+ if (j != image)
780
+ cross[i * n + j] += sign * 2 * std::abs(weight - b[j * n + image]);
781
+ } else {
782
+ // Zero/zero pairs have exactly zero update. The static column support
783
+ // is intersected with the CURRENT unassigned columns; no edge cost,
784
+ // bound, domain or traversal decision is approximated or omitted.
785
+ for (int j : nonzero_b_columns[image])
786
+ if (j != image && !used[j])
787
+ cross[i * n + j] += sign * 2 * std::abs(b[j * n + image]);
788
+ }
789
+ }
790
+ for (int j : columns)
791
+ if (j != image)
792
+ for (int k = 0; k < nl; ++k)
793
+ if (b[j * n + image] >= levels[k + 1])
794
+ hb[(j * nt + cb[image]) * nl + k] -= sign;
795
+ }
796
+ // Dense Hungarian assignment with integer duals. Infeasibility fails closed.
797
+ static int assignment(const std::vector<int> &cost, int m, Perm &match,
798
+ Perm &u, Perm &v, const Perm *seed_v = nullptr,
799
+ const Perm *seed_match = nullptr) {
800
+ Perm p(m + 1), way(m + 1);
801
+ u.assign(m + 1, 0);
802
+ v.assign(m + 1, 0);
803
+ // Inherited potentials are hints. Repair every row against CURRENT finite
804
+ // edges; never assume the parent dual remains feasible after a cost change.
805
+ const bool warm = seed_v && static_cast<int>(seed_v->size()) == m;
806
+ if (warm)
807
+ for (int j = 1; j <= m; ++j)
808
+ v[j] = std::clamp((*seed_v)[j - 1], -1000000, 1000000);
809
+ // Feasible row/column dual reductions, followed by a complementary
810
+ // zero-reduced-cost partial matching. Augment only unmatched rows.
811
+ for (int i = 1; i <= m; ++i) {
812
+ int best = INF;
813
+ if (warm) {
814
+ for (int j = 1; j <= m; ++j)
815
+ if (cost[(i - 1) * m + j - 1] < INF / 2)
816
+ best = std::min(best, cost[(i - 1) * m + j - 1] - v[j]);
817
+ } else {
818
+ // Preserve the simple vectorizable reduction for fresh small problems.
819
+ for (int j = 1; j <= m; ++j)
820
+ best = std::min(best, cost[(i - 1) * m + j - 1]);
821
+ }
822
+ if (best >= INF / 2)
823
+ return INF;
824
+ u[i] = best;
825
+ }
826
+ for (int j = 1; j <= m; ++j) {
827
+ int best = INF;
828
+ for (int i = 1; i <= m; ++i)
829
+ if (cost[(i - 1) * m + j - 1] < INF / 2)
830
+ best = std::min(best, cost[(i - 1) * m + j - 1] - u[i]);
831
+ if (best >= INF / 2)
832
+ return INF;
833
+ v[j] = best;
834
+ }
835
+ std::vector<char> matched(m + 1, 0);
836
+ // Keep only complementary, finite, disjoint inherited matching edges.
837
+ if (seed_match && static_cast<int>(seed_match->size()) == m)
838
+ for (int i = 1; i <= m; ++i) {
839
+ int column = (*seed_match)[i - 1];
840
+ if (column < 0 || column >= m)
841
+ continue;
842
+ int j = column + 1;
843
+ if (!p[j] && cost[(i - 1) * m + j - 1] < INF / 2 &&
844
+ cost[(i - 1) * m + j - 1] == u[i] + v[j]) {
845
+ p[j] = i;
846
+ matched[i] = 1;
847
+ }
848
+ }
849
+ for (int i = 1; i <= m; ++i)
850
+ if (!matched[i])
851
+ for (int j = 1; j <= m; ++j)
852
+ if (!p[j] && cost[(i - 1) * m + j - 1] < INF / 2 &&
853
+ cost[(i - 1) * m + j - 1] == u[i] + v[j]) {
854
+ p[j] = i;
855
+ matched[i] = 1;
856
+ break;
857
+ }
858
+ for (int i = 1; i <= m; ++i) {
859
+ if (matched[i])
860
+ continue;
861
+ p[0] = i;
862
+ int j0 = 0;
863
+ Perm minv(m + 1, INF);
864
+ std::vector<char> seen(m + 1, 0);
865
+ do {
866
+ seen[j0] = 1;
867
+ int i0 = p[j0], delta = INF, j1 = 0;
868
+ for (int j = 1; j <= m; ++j)
869
+ if (!seen[j]) {
870
+ int cur = cost[(i0 - 1) * m + j - 1] - u[i0] - v[j];
871
+ if (cur < minv[j]) {
872
+ minv[j] = cur;
873
+ way[j] = j0;
874
+ }
875
+ if (minv[j] < delta) {
876
+ delta = minv[j];
877
+ j1 = j;
878
+ }
879
+ }
880
+ if (delta >= INF / 2)
881
+ return INF;
882
+ for (int j = 0; j <= m; ++j)
883
+ if (seen[j]) {
884
+ u[p[j]] += delta;
885
+ v[j] -= delta;
886
+ } else
887
+ minv[j] -= delta;
888
+ j0 = j1;
889
+ } while (p[j0] != 0);
890
+ do {
891
+ int j1 = way[j0];
892
+ p[j0] = p[j1];
893
+ j0 = j1;
894
+ } while (j0);
895
+ }
896
+ match.resize(m);
897
+ int total = 0;
898
+ for (int j = 1; j <= m; ++j) {
899
+ match[p[j] - 1] = j - 1;
900
+ int value = cost[(p[j] - 1) * m + j - 1];
901
+ if (value >= INF / 2)
902
+ return INF;
903
+ total += value;
904
+ }
905
+ return total;
906
+ }
907
+
908
+ // Exact forced-LAP return paths for ONE source row. Unlike the ordinary
909
+ // traversal, root message updates never consume other rows' path values.
910
+ static void forced_to_row(const Perm &cost, int m, const Perm &match,
911
+ const Perm &u, const Perm &v, int chosen,
912
+ Perm &distance) {
913
+ distance.assign(m, INF);
914
+ distance[chosen] = 0;
915
+ std::vector<char> closed(m, 0);
916
+ for (int step = 0; step < m; ++step) {
917
+ int next = -1;
918
+ for (int i = 0; i < m; ++i)
919
+ if (!closed[i] && (next < 0 || distance[i] < distance[next]))
920
+ next = i;
921
+ if (next < 0 || distance[next] >= INF)
922
+ break;
923
+ closed[next] = 1;
924
+ const int column = match[next];
925
+ for (int i = 0; i < m; ++i)
926
+ if (!closed[i] && cost[i * m + column] < INF / 2) {
927
+ const int weight = cost[i * m + column] - u[i + 1] - v[column + 1];
928
+ if (weight < 0)
929
+ throw std::runtime_error("Negative reduced assignment arc");
930
+ distance[i] = std::min(distance[i], distance[next] + weight);
931
+ }
932
+ }
933
+ }
934
+
935
+ static int forced_paths(const Perm &cost, int m, const Perm &match,
936
+ const Perm &u, const Perm &v, Perm &dist,
937
+ Perm &component, bool zero_only = false) {
938
+ dist.assign(m * m, INF);
939
+ for (int i = 0; i < m; ++i)
940
+ for (int j = 0; j < m; ++j)
941
+ if (cost[i * m + match[j]] < INF / 2)
942
+ dist[i * m + j] = cost[i * m + match[j]] - u[i + 1] - v[match[j] + 1];
943
+ // Contract strongly connected zero-reduced-cost components. Movement
944
+ // within each component is free, so shortest-path distances are exactly
945
+ // the distances in this smaller weighted quotient graph.
946
+ component.resize(m);
947
+ std::iota(component.begin(), component.end(), 0);
948
+ int dim = m;
949
+ if (m > 4 || zero_only) {
950
+ std::vector<char> seen(m, 0);
951
+ Perm finished;
952
+ auto forward = [&](auto &&self, int x) -> void {
953
+ seen[x] = 1;
954
+ for (int y = 0; y < m; ++y)
955
+ if (dist[x * m + y] == 0 && !seen[y])
956
+ self(self, y);
957
+ finished.push_back(x);
958
+ };
959
+ for (int x = 0; x < m; ++x)
960
+ if (!seen[x])
961
+ forward(forward, x);
962
+ std::fill(seen.begin(), seen.end(), 0);
963
+ dim = 0;
964
+ auto reverse = [&](auto &&self, int x) -> void {
965
+ seen[x] = 1;
966
+ component[x] = dim;
967
+ for (int y = 0; y < m; ++y)
968
+ if (dist[y * m + x] == 0 && !seen[y])
969
+ self(self, y);
970
+ };
971
+ for (auto it = finished.rbegin(); it != finished.rend(); ++it)
972
+ if (!seen[*it]) {
973
+ reverse(reverse, *it);
974
+ ++dim;
975
+ }
976
+ if (zero_only) {
977
+ // At zero slack a forced edge is feasible precisely when its reduced
978
+ // cost is zero and its endpoints share a zero-cost SCC. Nonnegative
979
+ // reduced costs make every other alternating cycle strictly positive.
980
+ dist.assign(dim * dim, INF);
981
+ for (int i = 0; i < dim; ++i)
982
+ dist[i * dim + i] = 0;
983
+ return dim;
984
+ }
985
+ if (dim < m) {
986
+ std::vector<int> quotient(dim * dim, INF);
987
+ for (int i = 0; i < m; ++i)
988
+ for (int j = 0; j < m; ++j) {
989
+ int pos = component[i] * dim + component[j];
990
+ quotient[pos] = std::min(quotient[pos], dist[i * m + j]);
991
+ }
992
+ dist.swap(quotient);
993
+ } else
994
+ std::iota(component.begin(), component.end(), 0);
995
+ }
996
+ for (int k = 0; k < dim; ++k)
997
+ for (int i = 0; i < dim; ++i) {
998
+ // A finite alternating path can exceed INF/2 in the diagnostic domain.
999
+ int first = dist[i * dim + k];
1000
+ if (first >= INF)
1001
+ continue;
1002
+ for (int j = 0; j < dim; ++j)
1003
+ dist[i * dim + j] =
1004
+ std::min(dist[i * dim + j], first + dist[k * dim + j]);
1005
+ }
1006
+ return dim;
1007
+ }
1008
+ static uint64_t shard_mix(uint64_t x) {
1009
+ x ^= x >> 30;
1010
+ x *= 0xbf58476d1ce4e5b9ULL;
1011
+ x ^= x >> 27;
1012
+ x *= 0x94d049bb133111ebULL;
1013
+ return x ^ (x >> 31);
1014
+ }
1015
+ struct AssignmentSeed {
1016
+ Perm columns, images;
1017
+ };
1018
+ void visit(int depth, int committed, std::shared_ptr<Group> generators,
1019
+ std::shared_ptr<Group> reactant_generators, int branch_level = 0,
1020
+ uint64_t branch_hash = 0, int request = 0,
1021
+ const AssignmentSeed *parent_seed = nullptr) {
1022
+ if (status)
1023
+ return;
1024
+ if (minimize && global_floor >= 0 && best_cost <= global_floor)
1025
+ return;
1026
+ if (shards > 1 && branch_level == SHARD_BRANCHES &&
1027
+ shard_mix(branch_hash) % shards != static_cast<unsigned>(shard_index))
1028
+ return;
1029
+ bool inside = batch_mode ? (request < 0 || requests[request].terminal)
1030
+ : depth >= static_cast<int>(prefix.size());
1031
+ if (batch_mode && inside)
1032
+ request = -1;
1033
+ if (slice_nodes > 0 && inside) {
1034
+ if (work_nodes == 0)
1035
+ slice_started = Clock::now();
1036
+ // Leaf classification cost varies widely; a node budget alone can strand
1037
+ // most workers behind a long final batch. Yield only at subtree
1038
+ // boundaries, preserving the same disjoint complete frontier, with
1039
+ // guaranteed progress.
1040
+ bool wall_slice =
1041
+ balance_slice && work_nodes >= 64 &&
1042
+ std::chrono::duration<double>(Clock::now() - slice_started).count() >=
1043
+ 0.05;
1044
+ if (work_nodes >= slice_nodes || wall_slice) {
1045
+ frontier.push_back(path);
1046
+ return;
1047
+ }
1048
+ ++work_nodes;
1049
+ }
1050
+ ++nodes;
1051
+ if (inside)
1052
+ ++new_nodes;
1053
+ else
1054
+ ++replay_nodes;
1055
+ if (expired()) {
1056
+ status = 1;
1057
+ return;
1058
+ }
1059
+ if (diagnostic_gaps) {
1060
+ const int64_t gap = best_cost - global_floor;
1061
+ ++diagnostic_gaps[3];
1062
+ ++diagnostic_gaps[gap <= cost_step ? 4 : gap <= 2LL * cost_step ? 5 : 6];
1063
+ diagnostic_gaps[13] = std::min(diagnostic_gaps[13], gap);
1064
+ diagnostic_gaps[14] = std::max(diagnostic_gaps[14], gap);
1065
+ }
1066
+ if (committed > target) {
1067
+ ++pruned;
1068
+ return;
1069
+ }
1070
+ if (prefix_bound) {
1071
+ const int64_t full = prefix_bound(mapping.data(), used.data(), target,
1072
+ prefix_bound_data);
1073
+ if (expired()) {
1074
+ status = 1;
1075
+ return;
1076
+ }
1077
+ // This is a FULL-objective bound in core doubled-CD units.
1078
+ if (2 * full > target) {
1079
+ ++pruned;
1080
+ return;
1081
+ }
1082
+ }
1083
+ if (root_certificate &&
1084
+ conditioned_gap_allowed(best_cost, global_floor, cost_step)) {
1085
+ ++root_certificate_calls;
1086
+ if (diagnostic_gaps) {
1087
+ const int64_t gap = best_cost - global_floor;
1088
+ ++diagnostic_gaps[gap <= cost_step ? 7
1089
+ : gap <= 2LL * cost_step ? 8
1090
+ : 9];
1091
+ }
1092
+ // close_forced temporarily installs full images without cache updates.
1093
+ // Recompute the original exact full bound at every leaf; leave parent
1094
+ // cache untouched.
1095
+ if (depth < n && !factor_cache)
1096
+ factor_cache.reset(
1097
+ new RootDualFactorCache(root_certificate, mapping.data()));
1098
+ if (depth == n)
1099
+ ++leaf_full_recomputes;
1100
+ const int64_t full =
1101
+ root_certificate->lower_bound(mapping.data(), used.data(), target,
1102
+ depth < n ? factor_cache->total : -1);
1103
+ if (expired()) {
1104
+ status = 1;
1105
+ return;
1106
+ }
1107
+ if (full >= RootDualBranchCertificate::forbidden || 2 * full > target) {
1108
+ if (diagnostic_gaps) {
1109
+ const int64_t gap = best_cost - global_floor;
1110
+ if (gap <= 2LL * cost_step) {
1111
+ ++diagnostic_gaps[10];
1112
+ ++diagnostic_gaps[gap <= cost_step ? 22 : 23];
1113
+ } else
1114
+ ++diagnostic_gaps[11];
1115
+ }
1116
+ ++root_certificate_pruned;
1117
+ ++pruned;
1118
+ return;
1119
+ }
1120
+ }
1121
+ if (depth == n) {
1122
+ if (shards > 1 && branch_level < SHARD_BRANCHES &&
1123
+ shard_mix(branch_hash) % shards != static_cast<unsigned>(shard_index))
1124
+ return;
1125
+ if (!inside)
1126
+ return;
1127
+ ++leaves;
1128
+ if (accepts(committed)) {
1129
+ ++accepted;
1130
+ if (minimize) {
1131
+ // All costs are nonnegative integers. Having a feasible incumbent
1132
+ // lets every subsequent bound exclude equality; a separate pass
1133
+ // enumerates all mappings at the proved minimum after this traversal
1134
+ // finishes.
1135
+ if (diagnostic_gaps) {
1136
+ if (!diagnostic_gaps[17]) {
1137
+ diagnostic_gaps[15] = nodes;
1138
+ diagnostic_gaps[16] = committed;
1139
+ }
1140
+ ++diagnostic_gaps[17];
1141
+ }
1142
+ best_cost = committed;
1143
+ best_mapping = mapping;
1144
+ target = committed - cost_step;
1145
+ } else {
1146
+ int stop = cost_callback
1147
+ ? cost_callback(mapping.data(), committed, userdata)
1148
+ : callback(mapping.data(), userdata);
1149
+ if (stop) {
1150
+ status = 3;
1151
+ return;
1152
+ }
1153
+ if (cap > 0 && accepted >= cap)
1154
+ status = 2;
1155
+ }
1156
+ }
1157
+ return;
1158
+ }
1159
+ int m = n - depth, atom = row_order[depth];
1160
+ Perm columns;
1161
+ columns.reserve(m);
1162
+ for (int j = 0; j < n; ++j)
1163
+ if (!used[j])
1164
+ columns.push_back(j);
1165
+ Perm candidates, candidate_bounds(minimize ? n : 0, INF);
1166
+ Perm forced_images(m);
1167
+ int chosen = -1;
1168
+ bool all_forced = true;
1169
+ // A singleton hard domain is forced in every feasible continuation.
1170
+ // Assigning it without a relaxation only defers pruning, never removes a
1171
+ // feasible completion. Apply the usual two-sided domain updates below.
1172
+ for (int i = 0; i < m; ++i) {
1173
+ int x = row_order[depth + i], count = 0, last = -1;
1174
+ for (int y : columns)
1175
+ if (ca[x] == cb[y] && y > row_min[x] && permitted[x * n + y] &&
1176
+ committed + cross[x * n + y] <= target) {
1177
+ ++count;
1178
+ last = y;
1179
+ // This guard only distinguishes empty, singleton, and nonsingleton
1180
+ // domains. Two witnesses settle the last case; exact LAP domains
1181
+ // below still inspect every allowed edge before any pruning or
1182
+ // branching.
1183
+ if (count == 2)
1184
+ break;
1185
+ }
1186
+ if (count == 0) {
1187
+ ++pruned;
1188
+ return;
1189
+ }
1190
+ if (count == 1) {
1191
+ forced_images[i] = last;
1192
+ if (chosen < 0) {
1193
+ chosen = i;
1194
+ candidates.push_back(last);
1195
+ }
1196
+ } else
1197
+ all_forced = false;
1198
+ }
1199
+ auto close_forced = [&]() -> bool {
1200
+ std::vector<char> images_seen(n, 0);
1201
+ bool fixed = true;
1202
+ for (int i = 0; i < m; ++i) {
1203
+ int x = row_order[depth + i], y = forced_images[i];
1204
+ if (images_seen[y]) {
1205
+ ++pruned;
1206
+ return true;
1207
+ }
1208
+ images_seen[y] = 1;
1209
+ for (const auto &g : *reactant_generators)
1210
+ if (g[x] != x) {
1211
+ fixed = false;
1212
+ break;
1213
+ }
1214
+ for (const auto &g : *generators)
1215
+ if (g[y] != y) {
1216
+ fixed = false;
1217
+ break;
1218
+ }
1219
+ }
1220
+ if (fixed) {
1221
+ // There is one bijective hard-domain completion and every future
1222
+ // stabilizer/domain update is inert. Verify its exact cost directly.
1223
+ int exact = committed;
1224
+ for (int i = 0; i < m; ++i) {
1225
+ int x = row_order[depth + i], y = forced_images[i];
1226
+ exact += cross[x * n + y];
1227
+ for (int j = 0; j < i; ++j)
1228
+ exact += 2 * std::abs(a[x * n + row_order[depth + j]] -
1229
+ b[y * n + forced_images[j]]);
1230
+ }
1231
+ if (!accepts(exact)) {
1232
+ ++pruned;
1233
+ return true;
1234
+ }
1235
+ int child_request = request;
1236
+ for (int i = 0; i < m; ++i) {
1237
+ int y = forced_images[i];
1238
+ if (batch_mode && child_request >= 0) {
1239
+ if (requests[child_request].terminal)
1240
+ child_request = -1;
1241
+ else {
1242
+ auto found = requests[child_request].children.find(y);
1243
+ if (found == requests[child_request].children.end())
1244
+ return true;
1245
+ child_request = found->second;
1246
+ }
1247
+ } else if (!batch_mode &&
1248
+ depth + i < static_cast<int>(prefix.size()) &&
1249
+ y != prefix[depth + i])
1250
+ return true;
1251
+ }
1252
+ for (int i = 0; i < m; ++i) {
1253
+ mapping[row_order[depth + i]] = forced_images[i];
1254
+ path.push_back(forced_images[i]);
1255
+ }
1256
+ ++forced_closures;
1257
+ visit(n, exact, generators, reactant_generators, branch_level,
1258
+ branch_hash, child_request);
1259
+ for (int i = 0; i < m; ++i) {
1260
+ mapping[row_order[depth + i]] = -1;
1261
+ path.pop_back();
1262
+ }
1263
+ return true;
1264
+ }
1265
+ return false;
1266
+ };
1267
+ if (all_forced && m > 1 && close_forced())
1268
+ return;
1269
+ if (separator_enabled && chosen < 0 && m >= 5 && m <= separator_limit) {
1270
+ Perm rows(row_order.begin() + depth, row_order.end()), aa(m * m),
1271
+ bb(m * m), unary(m * m, INF);
1272
+ for (int i = 0; i < m; ++i)
1273
+ for (int j = 0; j < m; ++j) {
1274
+ aa[i * m + j] = a[rows[i] * n + rows[j]];
1275
+ bb[i * m + j] = b[columns[i] * n + columns[j]];
1276
+ int x = rows[i], y = columns[j];
1277
+ if (ca[x] == cb[y] && y > row_min[x] && permitted[x * n + y])
1278
+ unary[i * m + j] = cross[x * n + y];
1279
+ }
1280
+ SeparatorResidualMinimum oracle(m, aa.data(), bb.data(), std::move(unary),
1281
+ separator_max_states, started, seconds);
1282
+ ++separator_calls;
1283
+ int lower = 0;
1284
+ bool known = oracle.run(lower);
1285
+ separator_states += oracle.states;
1286
+ if (expired()) {
1287
+ status = 1;
1288
+ return;
1289
+ }
1290
+ if (known) {
1291
+ ++separator_known;
1292
+ if (committed + lower > target) {
1293
+ ++pruned;
1294
+ ++separator_pruned;
1295
+ return;
1296
+ }
1297
+ }
1298
+ }
1299
+ // Forced-edge LAP minima below are necessary conditions for every
1300
+ // completion of this node. Retain their rejected domains in descendants;
1301
+ // restore exactly these removals when leaving this subtree. The values are
1302
+ // equivariant under the current two endpoint stabilizers, so the domain
1303
+ // invariant used by the existing orbit leaders is preserved.
1304
+ Perm certified_removed;
1305
+ struct RestoreCertifiedDomains {
1306
+ std::vector<char> &mask;
1307
+ Perm &removed;
1308
+ ~RestoreCertifiedDomains() {
1309
+ for (int index : removed)
1310
+ mask[index] = 1;
1311
+ }
1312
+ } restore_certified{permitted, certified_removed};
1313
+ AssignmentSeed next_seed;
1314
+ const AssignmentSeed *child_seed = parent_seed;
1315
+ if (chosen < 0) {
1316
+ Perm active_types;
1317
+ std::vector<char> type_seen(nt, 0);
1318
+ for (int i = depth; i < n; ++i)
1319
+ if (!type_seen[ca[row_order[i]]]) {
1320
+ type_seen[ca[row_order[i]]] = 1;
1321
+ active_types.push_back(ca[row_order[i]]);
1322
+ }
1323
+ std::vector<int> cost(m * m, INF);
1324
+ int64_t row_lower = 0;
1325
+ int remaining_budget = target - committed;
1326
+ for (int i = 0; i < m; ++i) {
1327
+ int x = row_order[depth + i], minimum = row_min[x], row_best = INF;
1328
+ for (int j = 0; j < m; ++j) {
1329
+ int y = columns[j];
1330
+ if (ca[x] != cb[y] || y <= minimum || !permitted[x * n + y])
1331
+ continue;
1332
+ int value = cross[x * n + y];
1333
+ if (value > remaining_budget)
1334
+ continue;
1335
+ for (int t : active_types) {
1336
+ for (int k = 0; k < nl; ++k)
1337
+ value += (levels[k + 1] - levels[k]) *
1338
+ std::abs(ha[(x * nt + t) * nl + k] -
1339
+ hb[(y * nt + t) * nl + k]);
1340
+ if (value > remaining_budget)
1341
+ break;
1342
+ }
1343
+ if (value > remaining_budget)
1344
+ continue;
1345
+ cost[i * m + j] = value;
1346
+ row_best = std::min(row_best, value);
1347
+ }
1348
+ // Each row contributes one nonnegative assignment cost. The partial
1349
+ // sum of row minima is already a valid lower bound for the entire LAP.
1350
+ if (row_best >= INF / 2 || row_lower + row_best > remaining_budget) {
1351
+ ++pruned;
1352
+ return;
1353
+ }
1354
+ row_lower += row_best;
1355
+ }
1356
+ Perm match, u, v, seed_v, seed_match;
1357
+ #ifndef SYNKIT_COLD_ASSIGNMENT
1358
+ if (parent_seed && m >= 16) {
1359
+ Perm indices(n, -1);
1360
+ for (int j = 0; j < m; ++j)
1361
+ indices[columns[j]] = j;
1362
+ for (int j : columns)
1363
+ seed_v.push_back(parent_seed->columns[j]);
1364
+ for (int i = 0; i < m; ++i) {
1365
+ int image = parent_seed->images[row_order[depth + i]];
1366
+ seed_match.push_back(image < 0 ? -1 : indices[image]);
1367
+ }
1368
+ }
1369
+ #endif
1370
+ int lower =
1371
+ assignment(cost, m, match, u, v, parent_seed ? &seed_v : nullptr,
1372
+ parent_seed ? &seed_match : nullptr);
1373
+ if (lower >= INF / 2 || committed + lower > target) {
1374
+ ++pruned;
1375
+ return;
1376
+ }
1377
+ #ifndef SYNKIT_COLD_ASSIGNMENT
1378
+ // Below this dimension bookkeeping costs more than a fresh small LAP.
1379
+ // This is an implementation selector; both branches solve the same LAP.
1380
+ if (m > 16) {
1381
+ next_seed.columns.assign(n, 0);
1382
+ next_seed.images.assign(n, -1);
1383
+ for (int j = 0; j < m; ++j)
1384
+ next_seed.columns[columns[j]] = v[j + 1];
1385
+ for (int i = 0; i < m; ++i)
1386
+ next_seed.images[row_order[depth + i]] = columns[match[i]];
1387
+ child_seed = &next_seed;
1388
+ } else
1389
+ child_seed = nullptr;
1390
+ #endif
1391
+ Perm dist, component;
1392
+ int dim = forced_paths(cost, m, match, u, v, dist, component,
1393
+ lower == remaining_budget);
1394
+ chosen = 0;
1395
+ int best_count = INF, best_neighbors = -1;
1396
+ all_forced = true;
1397
+ for (int i = 0; i < m; ++i) {
1398
+ int count = 0, neighbors = 0, x = row_order[depth + i];
1399
+ for (int j = 0; j < m; ++j) {
1400
+ int edge = cost[i * m + match[j]] - u[i + 1] - v[match[j] + 1];
1401
+ if (committed + lower + edge +
1402
+ dist[component[j] * dim + component[i]] <=
1403
+ target) {
1404
+ ++count;
1405
+ forced_images[i] = columns[match[j]];
1406
+ } else {
1407
+ int y = columns[match[j]], index = x * n + y;
1408
+ if (ca[x] == cb[y] && y > row_min[x] && permitted[index]) {
1409
+ permitted[index] = 0;
1410
+ certified_removed.push_back(index);
1411
+ }
1412
+ }
1413
+ }
1414
+ if (count != 1)
1415
+ all_forced = false;
1416
+ for (int y = 0; y < n; ++y)
1417
+ if (mapping[y] >= 0 && a[x * n + y] != 0)
1418
+ ++neighbors;
1419
+ if (count < best_count ||
1420
+ (count == best_count && neighbors > best_neighbors)) {
1421
+ chosen = i;
1422
+ best_count = count;
1423
+ best_neighbors = neighbors;
1424
+ }
1425
+ }
1426
+ // Forced-edge relaxation certificates are necessary conditions for exact
1427
+ // completion. Singleton certified domains can close the same unique leaf.
1428
+ if (all_forced && m > 1 && close_forced())
1429
+ return;
1430
+ for (int i = 0; i < m; ++i) {
1431
+ int col = match[i], value = cost[chosen * m + col];
1432
+ if (value >= INF / 2 ||
1433
+ dist[component[i] * dim + component[chosen]] >= INF / 2)
1434
+ continue;
1435
+ int forced = lower + value - u[chosen + 1] - v[col + 1] +
1436
+ dist[component[i] * dim + component[chosen]];
1437
+ if (committed + forced <= target) {
1438
+ candidates.push_back(columns[col]);
1439
+ if (minimize)
1440
+ candidate_bounds[columns[col]] = forced;
1441
+ }
1442
+ }
1443
+ std::sort(candidates.begin(), candidates.end());
1444
+ }
1445
+ std::swap(row_order[depth], row_order[depth + chosen]);
1446
+ struct Restore {
1447
+ std::vector<int> &v;
1448
+ int a, b;
1449
+ ~Restore() { std::swap(v[a], v[b]); }
1450
+ } restore{row_order, depth, depth + chosen};
1451
+ atom = row_order[depth];
1452
+ if (candidates.size() == 1) {
1453
+ int image = candidates[0];
1454
+ bool fixed = true;
1455
+ for (const auto &g : *reactant_generators)
1456
+ if (g[atom] != atom) {
1457
+ fixed = false;
1458
+ break;
1459
+ }
1460
+ if (fixed)
1461
+ for (const auto &g : *generators)
1462
+ if (g[image] != image) {
1463
+ fixed = false;
1464
+ break;
1465
+ }
1466
+ if (fixed) {
1467
+ // Both stabilizers already fix the selected points. The row orbit is
1468
+ // just the assigned row, whose row_min/permitted entries will never be
1469
+ // read by descendants. Thus all omitted group/domain updates are inert.
1470
+ int child_request = request;
1471
+ if (batch_mode && request >= 0) {
1472
+ auto found = requests[request].children.find(image);
1473
+ if (found == requests[request].children.end())
1474
+ return;
1475
+ child_request = found->second;
1476
+ } else if (!batch_mode && depth < static_cast<int>(prefix.size()) &&
1477
+ image != prefix[depth])
1478
+ return;
1479
+ int delta = cross[atom * n + image];
1480
+ mapping[atom] = image;
1481
+ used[image] = 1;
1482
+ update(atom, image, 1, depth, columns);
1483
+ path.push_back(image);
1484
+ visit(depth + 1, committed + delta, generators, reactant_generators,
1485
+ branch_level, branch_hash, child_request, child_seed);
1486
+ path.pop_back();
1487
+ update(atom, image, -1, depth, columns);
1488
+ used[image] = 0;
1489
+ mapping[atom] = -1;
1490
+ return;
1491
+ }
1492
+ }
1493
+ Perm parent(n);
1494
+ std::iota(parent.begin(), parent.end(), 0);
1495
+ auto find = [&](int x) {
1496
+ while (parent[x] != x) {
1497
+ parent[x] = parent[parent[x]];
1498
+ x = parent[x];
1499
+ }
1500
+ return x;
1501
+ };
1502
+ for (const auto &g : *generators)
1503
+ for (int j : columns) {
1504
+ int x = find(j), y = find(g[j]);
1505
+ if (x != y)
1506
+ parent[y] = x;
1507
+ }
1508
+ Perm orbit_min(n, INF);
1509
+ for (int image : columns) {
1510
+ int root = find(image);
1511
+ orbit_min[root] = std::min(orbit_min[root], image);
1512
+ }
1513
+ std::vector<char> seen(n, 0);
1514
+ Perm row_orbit{atom};
1515
+ std::vector<char> row_seen(n, 0);
1516
+ row_seen[atom] = 1;
1517
+ for (size_t i = 0; i < row_orbit.size(); ++i)
1518
+ for (const auto &g : *reactant_generators) {
1519
+ int image = g[row_orbit[i]];
1520
+ if (!row_seen[image]) {
1521
+ row_seen[image] = 1;
1522
+ row_orbit.push_back(image);
1523
+ }
1524
+ }
1525
+ auto child_rows = stabilize(reactant_generators, atom, n);
1526
+ Perm choices;
1527
+ for (int image : candidates) {
1528
+ int orbit = find(image);
1529
+ if (!seen[orbit]) {
1530
+ seen[orbit] = 1;
1531
+ choices.push_back(image);
1532
+ }
1533
+ }
1534
+ // Keep numeric orbit leaders and their symmetry inequalities unchanged.
1535
+ // Only visit the surviving branches in a lower-bound-first order when
1536
+ // optimizing, so a good incumbent can tighten the other branches sooner.
1537
+ if (minimize)
1538
+ std::stable_sort(choices.begin(), choices.end(), [&](int x, int y) {
1539
+ return candidate_bounds[x] < candidate_bounds[y];
1540
+ });
1541
+ for (int image : choices) {
1542
+ int child_request = request;
1543
+ if (batch_mode && request >= 0) {
1544
+ auto found = requests[request].children.find(image);
1545
+ if (found == requests[request].children.end())
1546
+ continue;
1547
+ child_request = found->second;
1548
+ } else if (!batch_mode && depth < static_cast<int>(prefix.size()) &&
1549
+ image != prefix[depth])
1550
+ continue;
1551
+ int delta = cross[atom * n + image];
1552
+ mapping[atom] = image;
1553
+ used[image] = 1;
1554
+ Perm old_min;
1555
+ for (int row : row_orbit) {
1556
+ old_min.push_back(row_min[row]);
1557
+ row_min[row] = std::max(row_min[row], image);
1558
+ }
1559
+ // Earlier two-sided choices exclude entire product orbits, so these
1560
+ // domains remain invariant under the current independent stabilizers.
1561
+ // A smaller orbit leader at any equivalent reactant row belongs to an
1562
+ // already enumerated branch, even when that row's numeric image is
1563
+ // larger.
1564
+ Perm removed;
1565
+ for (int row : row_orbit)
1566
+ for (int col : columns)
1567
+ if (orbit_min[find(col)] < image && permitted[row * n + col]) {
1568
+ permitted[row * n + col] = 0;
1569
+ removed.push_back(row * n + col);
1570
+ }
1571
+ update(atom, image, 1, depth, columns);
1572
+ int next_level = branch_level;
1573
+ uint64_t next_hash = branch_hash;
1574
+ if (choices.size() > 1 && branch_level < SHARD_BRANCHES) {
1575
+ ++next_level;
1576
+ next_hash = shard_mix(branch_hash + static_cast<unsigned>(image + 1) +
1577
+ 0x9e3779b97f4a7c15ULL);
1578
+ }
1579
+ path.push_back(image);
1580
+ visit(depth + 1, committed + delta, stabilize(generators, image, n),
1581
+ child_rows, next_level, next_hash, child_request, child_seed);
1582
+ path.pop_back();
1583
+ update(atom, image, -1, depth, columns);
1584
+ for (size_t i = 0; i < row_orbit.size(); ++i)
1585
+ row_min[row_orbit[i]] = old_min[i];
1586
+ for (int index : removed)
1587
+ permitted[index] = 1;
1588
+ used[image] = 0;
1589
+ mapping[atom] = -1;
1590
+ if (status)
1591
+ return;
1592
+ }
1593
+ }
1594
+ };
1595
+ extern "C" int synkit_distance_abi() { return 3; }
1596
+ extern "C" int synkit_distance_candidates(
1597
+ int n, int nt, int nl, int target, double seconds, int64_t cap,
1598
+ const int *a, const int *b, const int *ca, const int *cb, const int *order,
1599
+ const int *pred, const int *gens, int count, const int *row_gens,
1600
+ int row_count, int shard_index, int shards, Callback callback,
1601
+ void *userdata, int64_t *statistics) {
1602
+ try {
1603
+ if (n < 1 || n > SYNKIT_PABS_MAX_VERTICES || nt < 1 || nt > n || nl < 0 ||
1604
+ nl > 15 || target < 0 || target > 1000000 || !callback || shards < 1 ||
1605
+ shard_index < 0 || shard_index >= shards)
1606
+ return -2;
1607
+ Solver solver(n, nt, nl, target, seconds, cap, a, b, ca, cb, order, pred,
1608
+ callback, userdata);
1609
+ solver.shard_index = shard_index;
1610
+ solver.shards = shards;
1611
+ auto group = std::make_shared<Group>();
1612
+ for (int k = 0; k < count; ++k)
1613
+ group->emplace_back(gens + k * n, gens + (k + 1) * n);
1614
+ auto rows = std::make_shared<Group>();
1615
+ for (int k = 0; k < row_count; ++k)
1616
+ rows->emplace_back(row_gens + k * n, row_gens + (k + 1) * n);
1617
+ solver.visit(0, 0, group, rows);
1618
+ statistics[0] = solver.nodes;
1619
+ statistics[1] = solver.leaves;
1620
+ statistics[2] = solver.accepted;
1621
+ statistics[3] = solver.pruned;
1622
+ return solver.status;
1623
+ } catch (...) {
1624
+ return -1;
1625
+ }
1626
+ }
1627
+
1628
+ static void objective_statistics(const Solver &solver, int64_t *statistics) {
1629
+ statistics[0] = solver.nodes;
1630
+ statistics[1] = solver.leaves;
1631
+ statistics[2] = solver.accepted;
1632
+ statistics[3] = solver.pruned;
1633
+ statistics[4] = solver.replay_nodes;
1634
+ statistics[5] = solver.new_nodes;
1635
+ }
1636
+
1637
+ // Additive ABI 3 objectives. Matrices use doubled bond units; reported costs
1638
+ // use quarter-CD units, exactly as the existing shell entry points do.
1639
+ static int native_minimum_impl(
1640
+ int n, int nt, int nl, double seconds, const int *a, const int *b,
1641
+ const int *ca, const int *cb, const int *order, const int *pred,
1642
+ const int *gens, int count, const int *row_gens, int row_count,
1643
+ const int *seed, int *best_mapping, int *best_cost, int64_t *statistics,
1644
+ int separator_limit, int64_t separator_budget, bool report_separator,
1645
+ int global_floor = -1,
1646
+ const RootDualBranchCertificate *root_certificate = nullptr,
1647
+ int64_t *certificate_stats = nullptr, int64_t *diagnostic_gaps = nullptr,
1648
+ PrefixBoundCallback prefix_bound = nullptr, void *prefix_bound_data = nullptr) {
1649
+ try {
1650
+ if (statistics)
1651
+ std::fill(statistics, statistics + (report_separator ? 10 : 6), 0);
1652
+ if (n < 1 || n > SYNKIT_PABS_MAX_VERTICES || nt < 1 || nt > n || nl < 0 ||
1653
+ nl > 15 || !std::isfinite(seconds) || seconds < 0 || !a || !b || !ca ||
1654
+ !cb || !order || !seed || !best_mapping || !best_cost || !statistics ||
1655
+ count < 0 || row_count < 0 || (count && !gens) ||
1656
+ (row_count && !row_gens) || separator_limit < 0 ||
1657
+ separator_limit > 12 || (separator_limit && separator_budget < 1) ||
1658
+ global_floor < -1)
1659
+ return -2;
1660
+ Perm seen(n, 0), ordered(n, 0);
1661
+ for (int i = 0; i < n; ++i) {
1662
+ if (seed[i] < 0 || seed[i] >= n || seen[seed[i]]++ || order[i] < 0 ||
1663
+ order[i] >= n || ordered[order[i]]++ || ca[i] < 0 || ca[i] >= nt ||
1664
+ cb[i] < 0 || cb[i] >= nt || ca[i] != cb[seed[i]])
1665
+ return -2;
1666
+ }
1667
+ int incumbent = 0, bond_gcd = 0;
1668
+ for (int i = 0; i < n; ++i)
1669
+ for (int j = 0; j < i; ++j) {
1670
+ incumbent += 2 * std::abs(a[i * n + j] - b[seed[i] * n + seed[j]]);
1671
+ bond_gcd = std::gcd(bond_gcd, a[i * n + j]);
1672
+ bond_gcd = std::gcd(bond_gcd, b[i * n + j]);
1673
+ if (incumbent > 1000000)
1674
+ return -2;
1675
+ }
1676
+ if (global_floor > incumbent)
1677
+ return -2;
1678
+ // For doubled bonds divisible by g, |x-y| == x+y (mod 2g), including
1679
+ // signed bonds. Quarter-CD costs therefore share one residue modulo 4g.
1680
+ // Every improvement is at least this step below the feasible incumbent.
1681
+ int step = bond_gcd ? 4 * bond_gcd : 1;
1682
+ Solver solver(n, nt, nl, incumbent - step, seconds, -1, a, b, ca, cb, order,
1683
+ pred, nullptr, nullptr);
1684
+ solver.lower_target = 0;
1685
+ solver.minimize = true;
1686
+ solver.global_floor = global_floor;
1687
+ solver.prefix_bound = prefix_bound;
1688
+ solver.prefix_bound_data = prefix_bound_data;
1689
+ solver.cost_step = step;
1690
+ solver.diagnostic_gaps = diagnostic_gaps;
1691
+ if (diagnostic_gaps) {
1692
+ std::fill(diagnostic_gaps, diagnostic_gaps + 18, 0);
1693
+ diagnostic_gaps[0] = incumbent;
1694
+ diagnostic_gaps[1] = global_floor;
1695
+ diagnostic_gaps[2] = step;
1696
+ diagnostic_gaps[13] = INT64_MAX;
1697
+ diagnostic_gaps[15] = diagnostic_gaps[16] = -1;
1698
+ }
1699
+
1700
+ std::fill(conditioned_factor_cache_statistics,
1701
+ conditioned_factor_cache_statistics + 8, 0);
1702
+ solver.root_certificate = root_certificate && root_certificate->ready
1703
+ ? root_certificate
1704
+ : nullptr;
1705
+ solver.separator_enabled = separator_limit > 0;
1706
+ solver.separator_limit = separator_limit;
1707
+ solver.separator_max_states = separator_budget;
1708
+ solver.best_cost = incumbent;
1709
+ solver.best_mapping.assign(seed, seed + n);
1710
+ auto group = std::make_shared<Group>();
1711
+ for (int k = 0; k < count; ++k)
1712
+ group->emplace_back(gens + k * n, gens + (k + 1) * n);
1713
+ auto rows = std::make_shared<Group>();
1714
+ for (int k = 0; k < row_count; ++k)
1715
+ rows->emplace_back(row_gens + k * n, row_gens + (k + 1) * n);
1716
+ solver.visit(0, 0, group, rows);
1717
+ // A timeout still returns a feasible incumbent, while status 0 alone
1718
+ // certifies that every compatible improvement has been excluded.
1719
+ std::copy(solver.best_mapping.begin(), solver.best_mapping.end(),
1720
+ best_mapping);
1721
+ *best_cost = solver.best_cost;
1722
+ objective_statistics(solver, statistics);
1723
+ if (diagnostic_gaps)
1724
+ diagnostic_gaps[12] = solver.best_cost;
1725
+ conditioned_factor_cache_statistics[6] = solver.forced_closures;
1726
+ conditioned_factor_cache_statistics[7] = solver.leaf_full_recomputes;
1727
+ if (solver.factor_cache) {
1728
+ conditioned_factor_cache_statistics[0] = 1;
1729
+ conditioned_factor_cache_statistics[1] = solver.factor_cache->updates;
1730
+ conditioned_factor_cache_statistics[2] = solver.factor_cache->touches;
1731
+ conditioned_factor_cache_statistics[3] = solver.factor_cache->total;
1732
+ conditioned_factor_cache_statistics[4] = solver.root_certificate_calls;
1733
+ conditioned_factor_cache_statistics[5] = solver.root_certificate_pruned;
1734
+ }
1735
+ if (certificate_stats) {
1736
+ certificate_stats[0] = solver.root_certificate_calls;
1737
+ certificate_stats[1] = solver.root_certificate_pruned;
1738
+ }
1739
+ if (report_separator) {
1740
+ statistics[6] = solver.separator_calls;
1741
+ statistics[7] = solver.separator_known;
1742
+ statistics[8] = solver.separator_pruned;
1743
+ statistics[9] = solver.separator_states;
1744
+ }
1745
+ return solver.status;
1746
+ } catch (...) {
1747
+ return -3;
1748
+ }
1749
+ }
1750
+
1751
+ extern "C" int synkit_distance_minimum(
1752
+ int n, int nt, int nl, double seconds, const int *a, const int *b,
1753
+ const int *ca, const int *cb, const int *order, const int *pred,
1754
+ const int *gens, int count, const int *row_gens, int row_count,
1755
+ const int *seed, int *best_mapping, int *best_cost, int64_t *statistics) {
1756
+ return native_minimum_impl(n, nt, nl, seconds, a, b, ca, cb, order, pred,
1757
+ gens, count, row_gens, row_count, seed,
1758
+ best_mapping, best_cost, statistics, 0, 0, false);
1759
+ }
1760
+
1761
+ // Experimental additive ABI: stats[0:6] are unchanged, followed by oracle
1762
+ // calls, completed minima, pruned nodes, and oracle states. A limit of zero
1763
+ // disables the oracle. This entry point is intentionally opt-in.
1764
+ extern "C" int synkit_distance_minimum_separator(
1765
+ int n, int nt, int nl, double seconds, const int *a, const int *b,
1766
+ const int *ca, const int *cb, const int *order, const int *pred,
1767
+ const int *gens, int count, const int *row_gens, int row_count,
1768
+ const int *seed, int *best_mapping, int *best_cost, int64_t *statistics,
1769
+ int residual_limit, int64_t max_states) {
1770
+ return native_minimum_impl(n, nt, nl, seconds, a, b, ca, cb, order, pred,
1771
+ gens, count, row_gens, row_count, seed,
1772
+ best_mapping, best_cost, statistics,
1773
+ residual_limit, max_states, true);
1774
+ }
1775
+
1776
+ static int native_range_impl(int n, int nt, int nl, int lower, int upper,
1777
+ double seconds, int64_t cap, const int *a,
1778
+ const int *b, const int *ca, const int *cb,
1779
+ const int *order, const int *pred, const int *gens,
1780
+ int count, const int *row_gens, int row_count,
1781
+ CostCallback callback, void *userdata,
1782
+ int64_t *statistics, int separator_limit,
1783
+ int64_t separator_budget, bool report_separator) {
1784
+ try {
1785
+ if (statistics)
1786
+ std::fill(statistics, statistics + (report_separator ? 10 : 6), 0);
1787
+ if (n < 1 || n > SYNKIT_PABS_MAX_VERTICES || nt < 1 || nt > n || nl < 0 ||
1788
+ nl > 15 || lower < 0 || upper < lower || upper > 1000000 ||
1789
+ !std::isfinite(seconds) || seconds < 0 || !a || !b || !ca || !cb ||
1790
+ !order || !callback || !statistics || count < 0 || row_count < 0 ||
1791
+ (count && !gens) || (row_count && !row_gens) || separator_limit < 0 ||
1792
+ separator_limit > 12 || (separator_limit && separator_budget < 1))
1793
+ return -2;
1794
+ Solver solver(n, nt, nl, upper, seconds, cap, a, b, ca, cb, order, pred,
1795
+ nullptr, userdata);
1796
+ solver.lower_target = lower;
1797
+ solver.cost_callback = callback;
1798
+ solver.separator_enabled = separator_limit > 0;
1799
+ solver.separator_limit = separator_limit;
1800
+ solver.separator_max_states = separator_budget;
1801
+ auto group = std::make_shared<Group>();
1802
+ for (int k = 0; k < count; ++k)
1803
+ group->emplace_back(gens + k * n, gens + (k + 1) * n);
1804
+ auto rows = std::make_shared<Group>();
1805
+ for (int k = 0; k < row_count; ++k)
1806
+ rows->emplace_back(row_gens + k * n, row_gens + (k + 1) * n);
1807
+ solver.visit(0, 0, group, rows);
1808
+ objective_statistics(solver, statistics);
1809
+ if (report_separator) {
1810
+ statistics[6] = solver.separator_calls;
1811
+ statistics[7] = solver.separator_known;
1812
+ statistics[8] = solver.separator_pruned;
1813
+ statistics[9] = solver.separator_states;
1814
+ }
1815
+ return solver.status;
1816
+ } catch (...) {
1817
+ return -3;
1818
+ }
1819
+ }
1820
+
1821
+ extern "C" int synkit_distance_range(
1822
+ int n, int nt, int nl, int lower, int upper, double seconds, int64_t cap,
1823
+ const int *a, const int *b, const int *ca, const int *cb, const int *order,
1824
+ const int *pred, const int *gens, int count, const int *row_gens,
1825
+ int row_count, CostCallback callback, void *userdata, int64_t *statistics) {
1826
+ return native_range_impl(n, nt, nl, lower, upper, seconds, cap, a, b, ca, cb,
1827
+ order, pred, gens, count, row_gens, row_count,
1828
+ callback, userdata, statistics, 0, 0, false);
1829
+ }
1830
+
1831
+ extern "C" int synkit_distance_range_separator(
1832
+ int n, int nt, int nl, int lower, int upper, double seconds, int64_t cap,
1833
+ const int *a, const int *b, const int *ca, const int *cb, const int *order,
1834
+ const int *pred, const int *gens, int count, const int *row_gens,
1835
+ int row_count, CostCallback callback, void *userdata, int64_t *statistics,
1836
+ int residual_limit, int64_t max_states) {
1837
+ return native_range_impl(n, nt, nl, lower, upper, seconds, cap, a, b, ca, cb,
1838
+ order, pred, gens, count, row_gens, row_count,
1839
+ callback, userdata, statistics, residual_limit,
1840
+ max_states, true);
1841
+ }
1842
+
1843
+ // Isolated oracle interface for independent residual-cost tests. Input bond
1844
+ // matrices use doubled units, unary costs use quarter-CD units, and allowed
1845
+ // is a zero/nonzero domain mask. Status 1 means unknown, not infeasibility.
1846
+ extern "C" int synkit_separator_residual_minimum(
1847
+ int n, const int *a, const int *b, const int *unary, const int *allowed,
1848
+ double seconds, int64_t max_states, int *value, int64_t *states) {
1849
+ auto started = Clock::now();
1850
+ try {
1851
+ if (value)
1852
+ *value = -1;
1853
+ if (states)
1854
+ *states = 0;
1855
+ if (n < 1 || n > 12 || !a || !b || !unary || !allowed || !value ||
1856
+ !states || max_states < 1 || !std::isfinite(seconds) || seconds < 0)
1857
+ return -2;
1858
+ Perm costs(n * n, INF);
1859
+ for (int i = 0; i < n; ++i)
1860
+ for (int j = 0; j < n; ++j) {
1861
+ if (a[i * n + j] != a[j * n + i] || b[i * n + j] != b[j * n + i] ||
1862
+ a[i * n + j] < -1024 || a[i * n + j] > 1024 ||
1863
+ b[i * n + j] < -1024 || b[i * n + j] > 1024 ||
1864
+ (i == j && (a[i * n + j] || b[i * n + j])) ||
1865
+ unary[i * n + j] < 0 || unary[i * n + j] > 1000000)
1866
+ return -2;
1867
+ if (allowed[i * n + j])
1868
+ costs[i * n + j] = unary[i * n + j];
1869
+ }
1870
+ SeparatorResidualMinimum oracle(n, a, b, std::move(costs), max_states,
1871
+ started, seconds);
1872
+ bool known = oracle.run(*value);
1873
+ *states = oracle.states;
1874
+ if (!known)
1875
+ *value = -1;
1876
+ return known ? 0 : 1;
1877
+ } catch (...) {
1878
+ return -3;
1879
+ }
1880
+ }
1881
+
1882
+ // Additive ABI: the original entry point remains available for V8 callers.
1883
+ // A normal slice emits a disjoint cover of every unvisited subtree.
1884
+ using FrontierCallback = int (*)(const int *, int, void *);
1885
+ extern "C" int synkit_distance_frontier(
1886
+ int n, int nt, int nl, int target, double seconds, int64_t cap,
1887
+ const int *a, const int *b, const int *ca, const int *cb, const int *order,
1888
+ const int *pred, const int *gens, int count, const int *row_gens,
1889
+ int row_count, int shard_index, int shards, Callback callback,
1890
+ void *userdata, int64_t *statistics, const int *prefix, int prefix_size,
1891
+ int64_t slice_nodes, FrontierCallback emit) {
1892
+ try {
1893
+ if (n < 1 || n > SYNKIT_PABS_MAX_VERTICES || nt < 1 || nt > n || nl < 0 ||
1894
+ nl > 15 || target < 0 || target > 1000000 || !callback || !emit ||
1895
+ shards != 1 || shard_index != 0 || prefix_size < 0 || prefix_size > n ||
1896
+ slice_nodes < 1)
1897
+ return -2;
1898
+ Solver solver(n, nt, nl, target, seconds, cap, a, b, ca, cb, order, pred,
1899
+ callback, userdata);
1900
+ solver.prefix.assign(prefix, prefix + prefix_size);
1901
+ solver.slice_nodes = slice_nodes;
1902
+ auto group = std::make_shared<Group>();
1903
+ for (int k = 0; k < count; ++k)
1904
+ group->emplace_back(gens + k * n, gens + (k + 1) * n);
1905
+ auto rows = std::make_shared<Group>();
1906
+ for (int k = 0; k < row_count; ++k)
1907
+ rows->emplace_back(row_gens + k * n, row_gens + (k + 1) * n);
1908
+ solver.visit(0, 0, group, rows);
1909
+ statistics[0] = solver.nodes;
1910
+ statistics[1] = solver.leaves;
1911
+ statistics[2] = solver.accepted;
1912
+ statistics[3] = solver.pruned;
1913
+ for (const auto &job : solver.frontier)
1914
+ if (emit(job.data(), job.size(), userdata))
1915
+ return 3;
1916
+ return solver.status;
1917
+ } catch (...) {
1918
+ return -1;
1919
+ }
1920
+ }
1921
+
1922
+ #include <map>
1923
+
1924
+ // Replay the trie of disjoint requested prefixes once. Below a terminal,
1925
+ // normal DFS resumes; the emitted frontier remains a disjoint exact cover.
1926
+ extern "C" int synkit_distance_frontier_batch(
1927
+ int n, int nt, int nl, int target, double seconds, int64_t cap,
1928
+ const int *a, const int *b, const int *ca, const int *cb, const int *order,
1929
+ const int *pred, const int *gens, int count, const int *row_gens,
1930
+ int row_count, int shard_index, int shards, Callback callback,
1931
+ void *userdata, int64_t *statistics, const int *values, const int *offsets,
1932
+ int prefix_count, int64_t slice_nodes, FrontierCallback emit) {
1933
+ try {
1934
+ if (n < 1 || n > SYNKIT_PABS_MAX_VERTICES || nt < 1 || nt > n || nl < 0 ||
1935
+ nl > 15 || target < 0 || target > 1000000 || !callback || !emit ||
1936
+ shards != 1 || shard_index != 0 || prefix_count < 1 ||
1937
+ prefix_count > 16384 || offsets[0] != 0 || slice_nodes < 1)
1938
+ return -2;
1939
+ Solver solver(n, nt, nl, target, seconds, cap, a, b, ca, cb, order, pred,
1940
+ callback, userdata);
1941
+ solver.batch_mode = true;
1942
+ solver.balance_slice = prefix_count <= 4;
1943
+ solver.requests.emplace_back();
1944
+ solver.slice_nodes = slice_nodes;
1945
+ for (int k = 0; k < prefix_count; ++k) {
1946
+ int begin = offsets[k], end = offsets[k + 1], node = 0;
1947
+ if (begin < 0 || end < begin || end - begin > n)
1948
+ return -2;
1949
+ for (int j = begin; j < end; ++j) {
1950
+ if (solver.requests[node].terminal || values[j] < 0 || values[j] >= n)
1951
+ return -2;
1952
+ auto found = solver.requests[node].children.find(values[j]);
1953
+ if (found == solver.requests[node].children.end()) {
1954
+ int next = solver.requests.size();
1955
+ solver.requests[node].children[values[j]] = next;
1956
+ solver.requests.emplace_back();
1957
+ node = next;
1958
+ } else
1959
+ node = found->second;
1960
+ }
1961
+ if (solver.requests[node].terminal ||
1962
+ !solver.requests[node].children.empty())
1963
+ return -2;
1964
+ solver.requests[node].terminal = true;
1965
+ }
1966
+ auto group = std::make_shared<Group>();
1967
+ for (int k = 0; k < count; ++k)
1968
+ group->emplace_back(gens + k * n, gens + (k + 1) * n);
1969
+ auto rows = std::make_shared<Group>();
1970
+ for (int k = 0; k < row_count; ++k)
1971
+ rows->emplace_back(row_gens + k * n, row_gens + (k + 1) * n);
1972
+ solver.visit(0, 0, group, rows);
1973
+ statistics[0] = solver.nodes;
1974
+ statistics[1] = solver.leaves;
1975
+ statistics[2] = solver.accepted;
1976
+ statistics[3] = solver.pruned;
1977
+ statistics[4] = solver.replay_nodes;
1978
+ statistics[5] = solver.new_nodes;
1979
+ for (const auto &job : solver.frontier)
1980
+ if (emit(job.data(), job.size(), userdata))
1981
+ return 3;
1982
+ return solver.status;
1983
+ } catch (...) {
1984
+ return -1;
1985
+ }
1986
+ }
1987
+
1988
+ #include <limits>
1989
+ struct NativeCanon {
1990
+ int n;
1991
+ const int *colors;
1992
+ const int *edges;
1993
+ double seconds;
1994
+ int64_t max_nodes;
1995
+ Clock::time_point start = Clock::now();
1996
+ int64_t nodes = 0;
1997
+ bool incomplete = false, root_twins_only = false;
1998
+ Group generators;
1999
+ std::unordered_set<Perm, VectorHash> signatures;
2000
+ Perm best, best_key, twin_class;
2001
+ struct NeighborRows {
2002
+ Perm offsets;
2003
+ std::vector<std::pair<int, int>> entries;
2004
+ explicit NeighborRows(int n) : offsets(n + 1) { entries.reserve(4 * n); }
2005
+ struct Range {
2006
+ const std::pair<int, int> *first, *last;
2007
+ auto begin() const { return first; }
2008
+ auto end() const { return last; }
2009
+ size_t size() const { return last - first; }
2010
+ };
2011
+ Range operator[](int i) const {
2012
+ return {entries.data() + offsets[i], entries.data() + offsets[i + 1]};
2013
+ }
2014
+ };
2015
+ NeighborRows neighbors;
2016
+ NativeCanon(int nn, const int *c, const int *e, double sec, int64_t budget)
2017
+ : n(nn), colors(c), edges(e), seconds(sec), max_nodes(budget),
2018
+ twin_class(nn), neighbors(nn) {
2019
+ std::iota(twin_class.begin(), twin_class.end(), 0);
2020
+ for (int i = 0; i < n; ++i) {
2021
+ for (int j = 0; j < n; ++j) {
2022
+ if (edges[i * n + j] < 0)
2023
+ throw std::runtime_error("negative edge palette ID");
2024
+ if (edges[i * n + j])
2025
+ neighbors.entries.push_back({j, edges[i * n + j]});
2026
+ }
2027
+ neighbors.offsets[i + 1] = neighbors.entries.size();
2028
+ }
2029
+ }
2030
+ bool valid(const Perm &g) {
2031
+ // A bijection preserving every nonzero colored edge also preserves absent
2032
+ // edges: its injection on the finite colored edge sets is a bijection.
2033
+ std::vector<char> seen(n, 0);
2034
+ for (int i = 0; i < n; ++i) {
2035
+ if (g[i] < 0 || g[i] >= n || seen[g[i]])
2036
+ return false;
2037
+ seen[g[i]] = 1;
2038
+ if (colors[i] != colors[g[i]])
2039
+ return false;
2040
+ }
2041
+ for (int i = 0; i < n; ++i)
2042
+ for (const auto &entry : neighbors[i])
2043
+ if (entry.second != edges[g[i] * n + g[entry.first]])
2044
+ return false;
2045
+ return true;
2046
+ }
2047
+ void accept(const Perm &g) {
2048
+ bool identity = true;
2049
+ for (int i = 0; i < n; ++i)
2050
+ if (g[i] != i) {
2051
+ identity = false;
2052
+ break;
2053
+ }
2054
+ if (!identity && signatures.insert(g).second && valid(g))
2055
+ generators.push_back(g);
2056
+ }
2057
+ using Partition = std::vector<Perm>;
2058
+ // Positive edge IDs allow 0 to terminate a cell's (color,count) list.
2059
+ // This flat encoding has exactly the lexicographic order of the legacy
2060
+ // vector<pair<-cell, vector<pair<color,count>>>> signature.
2061
+ using Signature = Perm;
2062
+ Partition refine(Partition p) {
2063
+ std::vector<std::pair<int, int>> entries;
2064
+ entries.reserve(n);
2065
+ Signature sig;
2066
+ sig.reserve(4 * n);
2067
+ while (true) {
2068
+ Perm ci(n);
2069
+ for (size_t k = 0; k < p.size(); ++k)
2070
+ for (int x : p[k])
2071
+ ci[x] = k;
2072
+ Partition next;
2073
+ next.reserve(n);
2074
+ bool split = false;
2075
+ for (auto &cell : p) {
2076
+ // Singleton cells cannot split; their position and contribution to ci
2077
+ // remain unchanged, so no refinement signature is needed for them.
2078
+ if (cell.size() == 1) {
2079
+ next.push_back(std::move(cell));
2080
+ continue;
2081
+ }
2082
+ std::map<Signature, Perm> buckets;
2083
+ for (int x : cell) {
2084
+ entries.clear();
2085
+ for (const auto &entry : neighbors[x])
2086
+ entries.push_back({ci[entry.first], entry.second});
2087
+ std::sort(entries.begin(), entries.end());
2088
+ sig.clear();
2089
+ for (size_t k = 0; k < entries.size();) {
2090
+ int cell_id = entries[k].first;
2091
+ sig.push_back(-cell_id);
2092
+ while (k < entries.size() && entries[k].first == cell_id) {
2093
+ int color = entries[k].second, count = 0;
2094
+ do {
2095
+ ++count;
2096
+ ++k;
2097
+ } while (k < entries.size() &&
2098
+ entries[k] == std::make_pair(cell_id, color));
2099
+ sig.push_back(color);
2100
+ sig.push_back(count);
2101
+ }
2102
+ sig.push_back(0);
2103
+ }
2104
+ buckets.try_emplace(sig).first->second.push_back(x);
2105
+ }
2106
+ split |= buckets.size() > 1;
2107
+ for (auto &entry : buckets)
2108
+ next.push_back(std::move(entry.second));
2109
+ }
2110
+ p = std::move(next);
2111
+ if (!split)
2112
+ return p;
2113
+ }
2114
+ }
2115
+ void visit(Partition p, bool already_refined = false) {
2116
+ if (incomplete)
2117
+ return;
2118
+ if (nodes >= max_nodes ||
2119
+ std::chrono::duration<double>(Clock::now() - start).count() >=
2120
+ seconds) {
2121
+ incomplete = true;
2122
+ return;
2123
+ }
2124
+ ++nodes;
2125
+ if (!already_refined)
2126
+ p = refine(std::move(p));
2127
+ int target = -1;
2128
+ while (true) {
2129
+ target = -1;
2130
+ for (size_t k = 0; k < p.size(); ++k)
2131
+ if (p[k].size() > 1 && (target < 0 || p[k].size() < p[target].size()))
2132
+ target = k;
2133
+ if (target < 0)
2134
+ break;
2135
+ const auto &cell = p[target];
2136
+ bool twins = true;
2137
+ for (int x : cell)
2138
+ if (twin_class[x] != twin_class[cell[0]]) {
2139
+ twins = false;
2140
+ break;
2141
+ }
2142
+ if (!twins)
2143
+ break;
2144
+ // Exact equal-row twins are freely interchangeable (all transpositions
2145
+ // are already seeded). In an equitable partition, individualizing such
2146
+ // twins cannot split any other cell. This is the same first branch as
2147
+ // repeated legacy individualization, with identical final cell ordering.
2148
+ Partition next;
2149
+ for (size_t k = 0; k < p.size(); ++k) {
2150
+ if (static_cast<int>(k) == target) {
2151
+ for (int x : p[k])
2152
+ next.push_back(Perm{x});
2153
+ } else
2154
+ next.push_back(std::move(p[k]));
2155
+ }
2156
+ p = std::move(next);
2157
+ }
2158
+ if (target < 0) {
2159
+ Perm order, key;
2160
+ order.reserve(n);
2161
+ key.reserve(n + 4 * n);
2162
+ for (const auto &cell : p)
2163
+ order.push_back(cell[0]);
2164
+ for (int x : order)
2165
+ key.push_back(colors[x]);
2166
+ // Sparse certificate has exactly dense lexicographic order: an earlier
2167
+ // nonzero entry compares larger than an implicit zero, hence -position.
2168
+ // Node colors have a fixed n-entry prefix; all present edge IDs are
2169
+ // positive. Enumerate the same upper-triangle certificate through present
2170
+ // edges. Sorting each row by canonical column preserves the exact dense
2171
+ // order, including loops, without scanning every absent edge at every
2172
+ // leaf.
2173
+ Perm inverse(n);
2174
+ for (int i = 0; i < n; ++i)
2175
+ inverse[order[i]] = i;
2176
+ std::vector<std::pair<int, int>> row;
2177
+ row.reserve(n);
2178
+ for (int i = 0; i < n; ++i) {
2179
+ row.clear();
2180
+ for (const auto &entry : neighbors[order[i]]) {
2181
+ int j = inverse[entry.first];
2182
+ if (j >= i)
2183
+ row.push_back({j, entry.second});
2184
+ }
2185
+ std::sort(row.begin(), row.end());
2186
+ for (const auto &entry : row) {
2187
+ key.push_back(-(i * n + entry.first));
2188
+ key.push_back(entry.second);
2189
+ }
2190
+ }
2191
+ if (best.empty() || key < best_key) {
2192
+ best = std::move(order);
2193
+ best_key = std::move(key);
2194
+ } else if (key == best_key) {
2195
+ Perm g(n);
2196
+ for (int i = 0; i < n; ++i)
2197
+ g[best[i]] = order[i];
2198
+ accept(g);
2199
+ }
2200
+ return;
2201
+ }
2202
+ Perm ci(n);
2203
+ for (size_t k = 0; k < p.size(); ++k)
2204
+ for (int x : p[k])
2205
+ ci[x] = k;
2206
+ Perm explored, parent(n);
2207
+ std::iota(parent.begin(), parent.end(), 0);
2208
+ auto find = [&](int x) {
2209
+ while (parent[x] != x) {
2210
+ parent[x] = parent[parent[x]];
2211
+ x = parent[x];
2212
+ }
2213
+ return x;
2214
+ };
2215
+ size_t processed = 0;
2216
+ for (int chosen : p[target]) {
2217
+ // Generators only append during this search. Keep each node's orbit union
2218
+ // and incorporate newly discovered witnesses after returning from a
2219
+ // child.
2220
+ if (!explored.empty())
2221
+ while (processed < generators.size()) {
2222
+ const auto &g = generators[processed++];
2223
+ bool fixed = true;
2224
+ for (int i = 0; i < n; ++i)
2225
+ if (ci[i] != ci[g[i]]) {
2226
+ fixed = false;
2227
+ break;
2228
+ }
2229
+ if (fixed)
2230
+ for (int i : p[target]) {
2231
+ int x = find(i), y = find(g[i]);
2232
+ if (x != y)
2233
+ parent[y] = x;
2234
+ }
2235
+ }
2236
+ bool seen = false;
2237
+ for (int old : explored)
2238
+ if (find(old) == find(chosen)) {
2239
+ seen = true;
2240
+ break;
2241
+ }
2242
+ if (seen)
2243
+ continue;
2244
+ Partition child = p;
2245
+ Perm rest;
2246
+ for (int x : p[target])
2247
+ if (x != chosen)
2248
+ rest.push_back(x);
2249
+ child[target] = Perm{chosen};
2250
+ child.insert(child.begin() + target + 1, rest);
2251
+ visit(std::move(child));
2252
+ if (incomplete)
2253
+ return;
2254
+ explored.push_back(chosen);
2255
+ }
2256
+ }
2257
+ bool group_order(uint64_t &answer) {
2258
+ answer = 1;
2259
+ if (root_twins_only) {
2260
+ // The invariant root partition separates all non-twin vertices. Its
2261
+ // automorphism group is exactly the direct product of the twin symmetric
2262
+ // groups; no stabilizer-chain construction is needed.
2263
+ Perm counts(n, 0);
2264
+ for (int id : twin_class)
2265
+ ++counts[id];
2266
+ for (int count : counts)
2267
+ for (int k = 2; k <= count; ++k) {
2268
+ if (answer > std::numeric_limits<uint64_t>::max() / k)
2269
+ return false;
2270
+ answer *= k;
2271
+ }
2272
+ return true;
2273
+ }
2274
+ // Every generator is supported in exactly one component of this union.
2275
+ // Thus the generated group is a direct product on disjoint supports.
2276
+ Perm parent(n);
2277
+ std::iota(parent.begin(), parent.end(), 0);
2278
+ auto find = [&](int x) {
2279
+ while (parent[x] != x) {
2280
+ parent[x] = parent[parent[x]];
2281
+ x = parent[x];
2282
+ }
2283
+ return x;
2284
+ };
2285
+ for (const auto &g : generators) {
2286
+ int first = -1;
2287
+ for (int i = 0; i < n; ++i)
2288
+ if (g[i] != i) {
2289
+ if (first < 0)
2290
+ first = i;
2291
+ else
2292
+ parent[find(i)] = find(first);
2293
+ }
2294
+ }
2295
+ std::map<int, Perm> components;
2296
+ for (int i = 0; i < n; ++i)
2297
+ components[find(i)].push_back(i);
2298
+ Perm index(n);
2299
+ for (const auto &entry : components) {
2300
+ const auto &points = entry.second;
2301
+ int size = points.size();
2302
+ if (size == 1)
2303
+ continue;
2304
+ // Adjacent transpositions of every exact equal-row twin class were
2305
+ // seeded before search. A support component contained in one such class
2306
+ // therefore has the full symmetric group: m!, with no Schreier work.
2307
+ // Components joining different twin classes retain the exact fallback.
2308
+ bool twins = true;
2309
+ for (int x : points)
2310
+ if (twin_class[x] != twin_class[points[0]]) {
2311
+ twins = false;
2312
+ break;
2313
+ }
2314
+ if (twins) {
2315
+ for (int k = 2; k <= size; ++k) {
2316
+ if (answer > std::numeric_limits<uint64_t>::max() / k)
2317
+ return false;
2318
+ answer *= k;
2319
+ }
2320
+ continue;
2321
+ }
2322
+ for (int i = 0; i < size; ++i)
2323
+ index[points[i]] = i;
2324
+ Group restricted;
2325
+ for (const auto &g : generators) {
2326
+ bool moved = false;
2327
+ for (int x : points)
2328
+ if (g[x] != x) {
2329
+ moved = true;
2330
+ break;
2331
+ }
2332
+ if (!moved)
2333
+ continue;
2334
+ Perm small(size);
2335
+ for (int i = 0; i < size; ++i)
2336
+ small[i] = index[g[points[i]]];
2337
+ restricted.push_back(std::move(small));
2338
+ }
2339
+ auto group = std::make_shared<Group>(std::move(restricted));
2340
+ for (int base = 0; base < size && !group->empty(); ++base) {
2341
+ bool moved = false;
2342
+ for (const auto &g : *group)
2343
+ if (g[base] != base) {
2344
+ moved = true;
2345
+ break;
2346
+ }
2347
+ if (!moved)
2348
+ continue;
2349
+ Perm orbit{base};
2350
+ std::vector<char> seen(size, 0);
2351
+ seen[base] = 1;
2352
+ for (size_t k = 0; k < orbit.size(); ++k)
2353
+ for (const auto &g : *group)
2354
+ if (!seen[g[orbit[k]]]) {
2355
+ seen[g[orbit[k]]] = 1;
2356
+ orbit.push_back(g[orbit[k]]);
2357
+ }
2358
+ if (answer > std::numeric_limits<uint64_t>::max() / orbit.size())
2359
+ return false;
2360
+ answer *= orbit.size();
2361
+ bool complete = true;
2362
+ group = stabilize(group, base, size, &complete);
2363
+ if (!complete)
2364
+ return false;
2365
+ }
2366
+ }
2367
+ return true;
2368
+ }
2369
+ };
2370
+ static thread_local bool profile_canonical = false;
2371
+ static thread_local int64_t canonical_times[8] = {};
2372
+ extern "C" void synkit_canonical_profile_enable(int enabled) {
2373
+ profile_canonical = enabled != 0;
2374
+ std::fill(canonical_times, canonical_times + 8, 0);
2375
+ }
2376
+ extern "C" void synkit_canonical_profile_read(int64_t *out) {
2377
+ std::copy(canonical_times, canonical_times + 8, out);
2378
+ }
2379
+ static int canonical_undirected_impl(int n, const int *colors, const int *edges,
2380
+ const int *seeds, int seed_count,
2381
+ double seconds, int64_t max_nodes,
2382
+ int *order, uint64_t *group_order,
2383
+ int64_t *nodes, bool require_group) {
2384
+ try {
2385
+ if (n < 1 || n > 256 || seconds < 0 || max_nodes < 1)
2386
+ return -2;
2387
+ auto profile_start = profile_canonical ? Clock::now() : Clock::time_point{};
2388
+ NativeCanon canon(n, colors, edges, seconds, max_nodes);
2389
+ // Nonadjacent twin transpositions are exact and inexpensive initial
2390
+ // witnesses.
2391
+ std::map<Perm, Perm> twins;
2392
+ for (int i = 0; i < n; ++i) {
2393
+ Perm key{colors[i]};
2394
+ key.reserve(1 + 2 * canon.neighbors[i].size());
2395
+ for (const auto &entry : canon.neighbors[i]) {
2396
+ key.push_back(entry.first);
2397
+ key.push_back(entry.second);
2398
+ }
2399
+ twins.try_emplace(std::move(key)).first->second.push_back(i);
2400
+ }
2401
+ for (const auto &entry : twins)
2402
+ for (int x : entry.second)
2403
+ canon.twin_class[x] = entry.second[0];
2404
+ NativeCanon::Partition partition;
2405
+ std::map<int, Perm> buckets;
2406
+ for (int i = 0; i < n; ++i)
2407
+ buckets[colors[i]].push_back(i);
2408
+ for (const auto &entry : buckets)
2409
+ partition.push_back(entry.second);
2410
+ partition = canon.refine(std::move(partition));
2411
+ canon.root_twins_only = true;
2412
+ for (const auto &cell : partition)
2413
+ for (int x : cell)
2414
+ if (canon.twin_class[x] != canon.twin_class[cell[0]])
2415
+ canon.root_twins_only = false;
2416
+ if (!canon.root_twins_only) {
2417
+ for (int k = 0; k < seed_count; ++k)
2418
+ canon.accept(Perm(seeds + k * n, seeds + (k + 1) * n));
2419
+ for (const auto &entry : twins)
2420
+ for (size_t k = 1; k < entry.second.size(); ++k) {
2421
+ Perm g(n);
2422
+ std::iota(g.begin(), g.end(), 0);
2423
+ std::swap(g[entry.second[k - 1]], g[entry.second[k]]);
2424
+ canon.accept(g);
2425
+ }
2426
+ }
2427
+ auto profile_ready = profile_canonical ? Clock::now() : Clock::time_point{};
2428
+ canon.visit(partition, true);
2429
+ *nodes = canon.nodes;
2430
+ auto profile_searched =
2431
+ profile_canonical ? Clock::now() : Clock::time_point{};
2432
+ if (canon.incomplete)
2433
+ return 1;
2434
+ std::copy(canon.best.begin(), canon.best.end(), order);
2435
+ *group_order = 0;
2436
+ if (require_group && !canon.group_order(*group_order))
2437
+ return 2;
2438
+ if (profile_canonical) {
2439
+ int offset = require_group ? 0 : 4;
2440
+ canonical_times[offset] +=
2441
+ std::chrono::duration_cast<std::chrono::nanoseconds>(profile_ready -
2442
+ profile_start)
2443
+ .count();
2444
+ canonical_times[offset + 1] +=
2445
+ std::chrono::duration_cast<std::chrono::nanoseconds>(
2446
+ profile_searched - profile_ready)
2447
+ .count();
2448
+ canonical_times[offset + 2] +=
2449
+ std::chrono::duration_cast<std::chrono::nanoseconds>(Clock::now() -
2450
+ profile_searched)
2451
+ .count();
2452
+ ++canonical_times[offset + 3];
2453
+ }
2454
+ return 0;
2455
+ } catch (...) {
2456
+ return -1;
2457
+ }
2458
+ }
2459
+
2460
+ // Additive export: old callers still receive a complete group-order proof.
2461
+ extern "C" int synkit_canonical_undirected(int n, const int *colors,
2462
+ const int *edges, const int *seeds,
2463
+ int seed_count, double seconds,
2464
+ int64_t max_nodes, int *order,
2465
+ uint64_t *group_order,
2466
+ int64_t *nodes) {
2467
+ return canonical_undirected_impl(n, colors, edges, seeds, seed_count, seconds,
2468
+ max_nodes, order, group_order, nodes, true);
2469
+ }
2470
+ extern "C" int synkit_canonical_undirected_certificate(
2471
+ int n, const int *colors, const int *edges, const int *seeds,
2472
+ int seed_count, double seconds, int64_t max_nodes, int *order,
2473
+ uint64_t *group_order, int64_t *nodes) {
2474
+ return canonical_undirected_impl(n, colors, edges, seeds, seed_count, seconds,
2475
+ max_nodes, order, group_order, nodes, false);
2476
+ }
2477
+
2478
+ // Controlled ITS matrices have zero diagonal. Emit upper-triangle edges in
2479
+ // canonical order directly, avoiding per-edge Python relabeling and sorting.
2480
+ extern "C" int synkit_its_sparse_edges(int n, const int *order,
2481
+ const int *edges, int *out) {
2482
+ if (n < 1 || n > 256)
2483
+ return -1;
2484
+ std::vector<char> seen(n, 0);
2485
+ for (int i = 0; i < n; ++i) {
2486
+ if (order[i] < 0 || order[i] >= n || seen[order[i]])
2487
+ return -1;
2488
+ seen[order[i]] = 1;
2489
+ }
2490
+ int count = 0;
2491
+ for (int i = 0; i < n; ++i)
2492
+ for (int j = i + 1; j < n; ++j) {
2493
+ int color = edges[order[i] * n + order[j]];
2494
+ if (color) {
2495
+ out[3 * count] = i;
2496
+ out[3 * count + 1] = j;
2497
+ out[3 * count + 2] = color;
2498
+ ++count;
2499
+ }
2500
+ }
2501
+ return count;
2502
+ }
2503
+
2504
+ #include <bitset>
2505
+ // A bounded cache of exact ITS patterns and explicitly witnessed R-images.
2506
+ // Cache misses always fall back to complete canonicalization.
2507
+ struct PatternCache {
2508
+ int n, work_limit;
2509
+ Perm base_nodes, base_edges, pending;
2510
+ Group generators;
2511
+ std::vector<std::bitset<256>> supports;
2512
+ std::unordered_set<Perm, VectorHash> known;
2513
+ size_t payload_ints = 0;
2514
+ static constexpr size_t MAX_PATTERNS = 1000000, MAX_INTS = 16 * 1024 * 1024;
2515
+ PatternCache(int nn, const int *nodes, const int *edges, const int *gens,
2516
+ int count, int work)
2517
+ : n(nn), work_limit(work), base_nodes(nodes, nodes + nn),
2518
+ base_edges(edges, edges + nn * nn) {
2519
+ for (int k = 0; k < count; ++k) {
2520
+ Perm g(gens + k * n, gens + (k + 1) * n);
2521
+ std::vector<char> seen(n, 0);
2522
+ bool valid = true;
2523
+ std::bitset<256> support;
2524
+ for (int i = 0; i < n; ++i) {
2525
+ if (g[i] < 0 || g[i] >= n || seen[g[i]] ||
2526
+ base_nodes[i] != base_nodes[g[i]]) {
2527
+ valid = false;
2528
+ break;
2529
+ }
2530
+ seen[g[i]] = 1;
2531
+ if (g[i] != i)
2532
+ support.set(i);
2533
+ }
2534
+ if (valid)
2535
+ for (int i = 0; i < n && valid; ++i)
2536
+ for (int j = 0; j < n; ++j)
2537
+ if (base_edges[i * n + j] != base_edges[g[i] * n + g[j]]) {
2538
+ valid = false;
2539
+ break;
2540
+ }
2541
+ if (valid && !support.none()) {
2542
+ generators.push_back(std::move(g));
2543
+ supports.push_back(support);
2544
+ }
2545
+ }
2546
+ }
2547
+ bool lookup(const int *nodes, const int *edges) {
2548
+ pending.clear();
2549
+ for (int i = 0; i < n; ++i)
2550
+ for (int j = i + 1; j < n; ++j)
2551
+ if (edges[i * n + j] != base_edges[i * n + j]) {
2552
+ pending.push_back(i * n + j);
2553
+ pending.push_back(edges[i * n + j]);
2554
+ }
2555
+ for (int i = 0; i < n; ++i)
2556
+ if (nodes[i] != base_nodes[i]) {
2557
+ pending.push_back(n * n + i);
2558
+ pending.push_back(nodes[i]);
2559
+ }
2560
+ return known.find(pending) != known.end();
2561
+ }
2562
+ void remember() {
2563
+ if (pending.size() > MAX_INTS)
2564
+ return;
2565
+ if (known.size() + work_limit + 1 > MAX_PATTERNS ||
2566
+ payload_ints + (work_limit + 1) * pending.size() > MAX_INTS) {
2567
+ known.clear();
2568
+ payload_ints = 0;
2569
+ }
2570
+ std::vector<Perm> queue{pending};
2571
+ if (known.insert(pending).second)
2572
+ payload_ints += pending.size();
2573
+ int work = 0;
2574
+ int limit =
2575
+ std::min<size_t>(work_limit, (MAX_INTS - pending.size()) /
2576
+ std::max<size_t>(1, pending.size()));
2577
+ for (size_t q = 0; q < queue.size() && work < limit; ++q) {
2578
+ const Perm current = queue[q];
2579
+ std::bitset<256> support;
2580
+ for (size_t j = 0; j < current.size(); j += 2) {
2581
+ int pos = current[j];
2582
+ if (pos >= n * n)
2583
+ support.set(pos - n * n);
2584
+ else {
2585
+ support.set(pos / n);
2586
+ support.set(pos % n);
2587
+ }
2588
+ }
2589
+ for (size_t gi = 0; gi < generators.size() && work < limit; ++gi) {
2590
+ if ((support & supports[gi]).none())
2591
+ continue;
2592
+ ++work;
2593
+ const auto &g = generators[gi];
2594
+ std::vector<std::pair<int, int>> entries;
2595
+ entries.reserve(current.size() / 2);
2596
+ for (size_t j = 0; j < current.size(); j += 2) {
2597
+ int pos = current[j], image;
2598
+ if (pos >= n * n)
2599
+ image = n * n + g[pos - n * n];
2600
+ else {
2601
+ int a = g[pos / n], b = g[pos % n];
2602
+ image = std::min(a, b) * n + std::max(a, b);
2603
+ }
2604
+ entries.push_back({image, current[j + 1]});
2605
+ }
2606
+ std::sort(entries.begin(), entries.end());
2607
+ Perm next;
2608
+ next.reserve(current.size());
2609
+ for (const auto &entry : entries) {
2610
+ next.push_back(entry.first);
2611
+ next.push_back(entry.second);
2612
+ }
2613
+ if (known.insert(next).second) {
2614
+ payload_ints += next.size();
2615
+ queue.push_back(std::move(next));
2616
+ }
2617
+ }
2618
+ }
2619
+ }
2620
+ };
2621
+ extern "C" void *synkit_pattern_cache_create(int n, const int *nodes,
2622
+ const int *edges, const int *gens,
2623
+ int count, int work) {
2624
+ try {
2625
+ if (n < 1 || n > 256 || count < 0 || work < 0 || work > 1024)
2626
+ return nullptr;
2627
+ return new PatternCache(n, nodes, edges, gens, count, work);
2628
+ } catch (...) {
2629
+ return nullptr;
2630
+ }
2631
+ }
2632
+ extern "C" int synkit_pattern_cache_lookup(void *handle, const int *nodes,
2633
+ const int *edges) {
2634
+ try {
2635
+ return static_cast<PatternCache *>(handle)->lookup(nodes, edges) ? 1 : 0;
2636
+ } catch (...) {
2637
+ return -1;
2638
+ }
2639
+ }
2640
+ extern "C" int synkit_pattern_cache_remember(void *handle) {
2641
+ try {
2642
+ static_cast<PatternCache *>(handle)->remember();
2643
+ return 0;
2644
+ } catch (...) {
2645
+ return -1;
2646
+ }
2647
+ }
2648
+ extern "C" void synkit_pattern_cache_destroy(void *handle) {
2649
+ delete static_cast<PatternCache *>(handle);
2650
+ }
2651
+
2652
+ // Inputs are precomputed with the public Python equality/isclose semantics.
2653
+ // Output: n unary flags, n context flags, then changed upper-triangle pairs.
2654
+ extern "C" int synkit_its_changes(int n, const int *mapping, const int *unary,
2655
+ const int *paired, const int *changed,
2656
+ int radius, int *out) {
2657
+ if (n < 1 || n > 256 || radius < 0)
2658
+ return -1;
2659
+ std::vector<int> frontier;
2660
+ for (int i = 0; i < n; ++i) {
2661
+ out[i] = unary[i * n + mapping[i]];
2662
+ out[n + i] = out[i];
2663
+ }
2664
+ int count = 0;
2665
+ for (int i = 0; i < n; ++i)
2666
+ for (int j = i + 1; j < n; ++j)
2667
+ if (changed[paired[i * n + j]]) {
2668
+ out[2 * n + 2 * count] = i;
2669
+ out[2 * n + 2 * count + 1] = j;
2670
+ ++count;
2671
+ out[n + i] = out[n + j] = 1;
2672
+ }
2673
+ for (int i = 0; i < n; ++i)
2674
+ if (out[n + i])
2675
+ frontier.push_back(i);
2676
+ for (int depth = 0; depth < radius && !frontier.empty(); ++depth) {
2677
+ std::vector<int> next;
2678
+ for (int i : frontier)
2679
+ for (int j = 0; j < n; ++j)
2680
+ if (paired[i * n + j] && !out[n + j]) {
2681
+ out[n + j] = 1;
2682
+ next.push_back(j);
2683
+ }
2684
+ frontier = std::move(next);
2685
+ }
2686
+ return count;
2687
+ }
2688
+
2689
+ // Ordered resource IDs have exactly the order of (typed element, A bond, B
2690
+ // bond). Each selected vertex receives its sorted external-resource multiset.
2691
+ extern "C" int synkit_its_boundary(int n, const int *selected, int count,
2692
+ const int *paired, int palette_size,
2693
+ const int *resources, int *offsets,
2694
+ int *out) {
2695
+ if (n < 1 || n > 256 || count < 0 || count > n || palette_size < 1)
2696
+ return -1;
2697
+ std::vector<char> inside(n, 0);
2698
+ for (int k = 0; k < count; ++k) {
2699
+ int i = selected[k];
2700
+ if (i < 0 || i >= n || inside[i])
2701
+ return -1;
2702
+ inside[i] = 1;
2703
+ }
2704
+ int size = 0;
2705
+ offsets[0] = 0;
2706
+ for (int k = 0; k < count; ++k) {
2707
+ int start = size, i = selected[k];
2708
+ for (int j = 0; j < n; ++j)
2709
+ if (!inside[j] && paired[i * n + j])
2710
+ out[size++] = resources[j * palette_size + paired[i * n + j]];
2711
+ std::sort(out + start, out + size);
2712
+ offsets[k + 1] = size;
2713
+ }
2714
+ return size;
2715
+ }
2716
+
2717
+ // Diagnostic oracle surface: exact assignment duals and each forced-edge
2718
+ // optimum. Costs are bounded nonnegative integers; INF denotes a forbidden
2719
+ // edge.
2720
+ extern "C" int synkit_assignment_certificate(int m, const int *input,
2721
+ int *match_out, int *u_out,
2722
+ int *v_out, int *forced_out) {
2723
+ try {
2724
+ if (m < 1 || m > 64)
2725
+ return -2;
2726
+ Perm cost(input, input + m * m), match, u, v, dist, component;
2727
+ for (int value : cost)
2728
+ if (value < 0 || (value > 1000000 && value != INF))
2729
+ return -2;
2730
+ int lower = Solver::assignment(cost, m, match, u, v);
2731
+ if (lower >= INF)
2732
+ return INF;
2733
+ int dim = Solver::forced_paths(cost, m, match, u, v, dist, component);
2734
+ std::copy(match.begin(), match.end(), match_out);
2735
+ std::copy(u.begin() + 1, u.end(), u_out);
2736
+ std::copy(v.begin() + 1, v.end(), v_out);
2737
+ for (int i = 0; i < m; ++i)
2738
+ for (int j = 0; j < m; ++j) {
2739
+ int column = match[j], path = dist[component[j] * dim + component[i]];
2740
+ forced_out[i * m + column] =
2741
+ (cost[i * m + column] >= INF / 2 || path >= INF)
2742
+ ? INF
2743
+ : lower + cost[i * m + column] - u[i + 1] - v[column + 1] +
2744
+ path;
2745
+ }
2746
+ return lower;
2747
+ } catch (...) {
2748
+ return -1;
2749
+ }
2750
+ }
2751
+
2752
+ // Diagnostic: arbitrary stale hints must produce independently valid duals.
2753
+ extern "C" int synkit_assignment_seed_certificate(int m, const int *input,
2754
+ const int *initial_v,
2755
+ const int *initial_match,
2756
+ int *match_out, int *u_out,
2757
+ int *v_out, int *forced_out) {
2758
+ try {
2759
+ if (m < 1 || m > 64 || !initial_v || !initial_match)
2760
+ return -2;
2761
+ Perm cost(input, input + m * m), match, u, v, dist, component;
2762
+ for (int value : cost)
2763
+ if (value < 0 || (value > 1000000 && value != INF))
2764
+ return -2;
2765
+ Perm seed_v(initial_v, initial_v + m),
2766
+ seed_match(initial_match, initial_match + m);
2767
+ int lower = Solver::assignment(cost, m, match, u, v, &seed_v, &seed_match);
2768
+ if (lower >= INF)
2769
+ return INF;
2770
+ int dim = Solver::forced_paths(cost, m, match, u, v, dist, component);
2771
+ std::copy(match.begin(), match.end(), match_out);
2772
+ std::copy(u.begin() + 1, u.end(), u_out);
2773
+ std::copy(v.begin() + 1, v.end(), v_out);
2774
+ for (int i = 0; i < m; ++i)
2775
+ for (int j = 0; j < m; ++j) {
2776
+ int column = match[j], path = dist[component[j] * dim + component[i]];
2777
+ forced_out[i * m + column] =
2778
+ (cost[i * m + column] >= INF / 2 || path >= INF)
2779
+ ? INF
2780
+ : lower + cost[i * m + column] - u[i + 1] - v[column + 1] +
2781
+ path;
2782
+ }
2783
+ return lower;
2784
+ } catch (...) {
2785
+ return -1;
2786
+ }
2787
+ }
2788
+
2789
+ extern "C" int synkit_its_pair(int n, const int *mapping, const int *a,
2790
+ const int *b, int levels, const int *pair_ids,
2791
+ int *out) {
2792
+ if (n < 1 || n > 256 || levels < 1)
2793
+ return -1;
2794
+ for (int i = 0; i < n; ++i)
2795
+ if (mapping[i] < 0 || mapping[i] >= n)
2796
+ return -1;
2797
+ for (int i = 0; i < n; ++i)
2798
+ for (int j = 0; j < n; ++j)
2799
+ out[i * n + j] = (i == j) ? 0
2800
+ : pair_ids[a[i * n + j] * levels +
2801
+ b[mapping[i] * n + mapping[j]]];
2802
+ return 0;
2803
+ }