cppkh-interface 0.1.0__tar.gz → 0.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cppkh-interface
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: Python interface for cppkh Khovanov homology computation with runtime C++ compilation.
5
5
  License-Expression: MIT
6
6
  Author: GGN_2015
@@ -26,6 +26,10 @@ Description-Content-Type: text/markdown
26
26
  `cppkh-interface` is a Python package for computing integer Khovanov homology
27
27
  with the C++ `cppkh` implementation.
28
28
 
29
+ Version `0.1.2` resolves link crossing signs by tracing directed PD edge
30
+ incidences with the SageMath convention. It does not infer signs from numeric
31
+ arc-label ordering.
32
+
29
33
  The package is compatible with the main `javakh-interface` function:
30
34
 
31
35
  ```python
@@ -33,11 +37,18 @@ import cppkh_interface
33
37
 
34
38
  pd_code = [[1, 5, 2, 4], [3, 1, 4, 6], [5, 3, 6, 2]]
35
39
  print(cppkh_interface.solve_khovanov(pd_code, de_r1=True, de_k8=True))
40
+ print(cppkh_interface.solve_many_khovanov([pd_code, pd_code]))
36
41
  ```
37
42
 
38
43
  Unlike wrappers that ship a prebuilt DLL or shared object, this package ships
39
- the `cppkh` C++ source file and compiles a local executable on first use through
40
- `cpp-simple-interface`. The compiled executable is cached for later calls.
44
+ the `cppkh` C++ source file in built distributions and compiles a local
45
+ executable on first use through `cpp-simple-interface`. The compiled executable
46
+ is cached for later calls.
47
+
48
+ In the repository checkout, the package does not keep a committed backup copy
49
+ of the C++ source. The build backend copies `../../src/main.cpp` into the
50
+ package data directory only while `poetry build` or `poetry publish --build` is
51
+ running, then removes that temporary copy.
41
52
 
42
53
  ## Install
43
54
 
@@ -68,6 +79,8 @@ poetry build
68
79
  poetry publish
69
80
  ```
70
81
 
82
+ Use `poetry publish --build` to build and upload in one command.
83
+
71
84
  For local testing:
72
85
 
73
86
  ```sh
@@ -3,6 +3,10 @@
3
3
  `cppkh-interface` is a Python package for computing integer Khovanov homology
4
4
  with the C++ `cppkh` implementation.
5
5
 
6
+ Version `0.1.2` resolves link crossing signs by tracing directed PD edge
7
+ incidences with the SageMath convention. It does not infer signs from numeric
8
+ arc-label ordering.
9
+
6
10
  The package is compatible with the main `javakh-interface` function:
7
11
 
8
12
  ```python
@@ -10,11 +14,18 @@ import cppkh_interface
10
14
 
11
15
  pd_code = [[1, 5, 2, 4], [3, 1, 4, 6], [5, 3, 6, 2]]
12
16
  print(cppkh_interface.solve_khovanov(pd_code, de_r1=True, de_k8=True))
17
+ print(cppkh_interface.solve_many_khovanov([pd_code, pd_code]))
13
18
  ```
14
19
 
15
20
  Unlike wrappers that ship a prebuilt DLL or shared object, this package ships
16
- the `cppkh` C++ source file and compiles a local executable on first use through
17
- `cpp-simple-interface`. The compiled executable is cached for later calls.
21
+ the `cppkh` C++ source file in built distributions and compiles a local
22
+ executable on first use through `cpp-simple-interface`. The compiled executable
23
+ is cached for later calls.
24
+
25
+ In the repository checkout, the package does not keep a committed backup copy
26
+ of the C++ source. The build backend copies `../../src/main.cpp` into the
27
+ package data directory only while `poetry build` or `poetry publish --build` is
28
+ running, then removes that temporary copy.
18
29
 
19
30
  ## Install
20
31
 
@@ -45,6 +56,8 @@ poetry build
45
56
  poetry publish
46
57
  ```
47
58
 
59
+ Use `poetry publish --build` to build and upload in one command.
60
+
48
61
  For local testing:
49
62
 
50
63
  ```sh
@@ -0,0 +1,207 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import hashlib
5
+ import io
6
+ import shutil
7
+ import tarfile
8
+ import tempfile
9
+ import zipfile
10
+ from pathlib import Path
11
+
12
+ from poetry.core.masonry import api as poetry_api
13
+
14
+
15
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
16
+ REPO_ROOT = PROJECT_ROOT.parents[1]
17
+ SOURCE = REPO_ROOT / "src" / "main.cpp"
18
+ TARGET = PROJECT_ROOT / "cppkh_interface" / "data" / "src" / "main.cpp"
19
+ WHEEL_SOURCE_NAME = "cppkh_interface/data/src/main.cpp"
20
+
21
+
22
+ def _sync_cpp_source() -> None:
23
+ if SOURCE.exists():
24
+ TARGET.parent.mkdir(parents=True, exist_ok=True)
25
+ shutil.copyfile(SOURCE, TARGET)
26
+ return
27
+ if TARGET.exists():
28
+ return
29
+ raise FileNotFoundError(
30
+ "cppkh source file was not found. Expected either "
31
+ f"{SOURCE} in the repository checkout or {TARGET} in the source tree."
32
+ )
33
+
34
+
35
+ def _clean_cpp_source() -> None:
36
+ if TARGET.exists() and SOURCE.exists():
37
+ TARGET.unlink()
38
+
39
+
40
+ def _source_bytes() -> bytes:
41
+ if TARGET.exists():
42
+ return TARGET.read_bytes()
43
+ if SOURCE.exists():
44
+ return SOURCE.read_bytes()
45
+ raise FileNotFoundError("cppkh source file was not available for packaging")
46
+
47
+
48
+ def _wheel_hash_and_size(data: bytes) -> tuple[str, str]:
49
+ digest = hashlib.sha256(data).digest()
50
+ encoded = base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii")
51
+ return f"sha256={encoded}", str(len(data))
52
+
53
+
54
+ def _make_universal_wheel_metadata(data: bytes) -> bytes:
55
+ text = data.decode("utf-8")
56
+ lines = []
57
+ tag_written = False
58
+ root_written = False
59
+ for line in text.splitlines():
60
+ if line.startswith("Root-Is-Purelib:"):
61
+ lines.append("Root-Is-Purelib: true")
62
+ root_written = True
63
+ elif line.startswith("Tag:"):
64
+ if not tag_written:
65
+ lines.append("Tag: py3-none-any")
66
+ tag_written = True
67
+ else:
68
+ lines.append(line)
69
+ if not root_written:
70
+ lines.append("Root-Is-Purelib: true")
71
+ if not tag_written:
72
+ lines.append("Tag: py3-none-any")
73
+ return ("\n".join(lines) + "\n").encode("utf-8")
74
+
75
+
76
+ def _universal_wheel_path(wheel_path: Path, record_name: str) -> Path:
77
+ dist_info = record_name.split("/", 1)[0]
78
+ base = dist_info.removesuffix(".dist-info")
79
+ return wheel_path.with_name(f"{base}-py3-none-any.whl")
80
+
81
+
82
+ def _rewrite_wheel_with_source(wheel_path: Path) -> Path:
83
+ source_data = _source_bytes()
84
+ rows: list[tuple[str, str, str]] = []
85
+
86
+ with zipfile.ZipFile(wheel_path, "r") as zin:
87
+ record_name = next(
88
+ (name for name in zin.namelist() if name.endswith(".dist-info/RECORD")),
89
+ None,
90
+ )
91
+ if record_name is None:
92
+ raise RuntimeError(f"wheel RECORD file not found in {wheel_path}")
93
+ output_path = _universal_wheel_path(wheel_path, record_name)
94
+ temp_path = output_path.with_name(output_path.name + ".tmp")
95
+
96
+ with zipfile.ZipFile(temp_path, "w", compression=zipfile.ZIP_DEFLATED) as zout:
97
+ wheel_metadata_name = record_name.removesuffix("RECORD") + "WHEEL"
98
+
99
+ for item in zin.infolist():
100
+ if item.filename in (WHEEL_SOURCE_NAME, record_name):
101
+ continue
102
+ data = zin.read(item.filename)
103
+ if item.filename == wheel_metadata_name:
104
+ data = _make_universal_wheel_metadata(data)
105
+ zout.writestr(item, data)
106
+ if not item.is_dir():
107
+ digest, size = _wheel_hash_and_size(data)
108
+ rows.append((item.filename, digest, size))
109
+
110
+ source_info = zipfile.ZipInfo(WHEEL_SOURCE_NAME, date_time=(2016, 1, 1, 0, 0, 0))
111
+ source_info.compress_type = zipfile.ZIP_DEFLATED
112
+ zout.writestr(source_info, source_data)
113
+ digest, size = _wheel_hash_and_size(source_data)
114
+ rows.append((WHEEL_SOURCE_NAME, digest, size))
115
+
116
+ rows.append((record_name, "", ""))
117
+ record_text = "".join(f"{path},{digest},{size}\n" for path, digest, size in rows)
118
+ record_info = zipfile.ZipInfo(record_name, date_time=(2016, 1, 1, 0, 0, 0))
119
+ record_info.compress_type = zipfile.ZIP_DEFLATED
120
+ zout.writestr(record_info, record_text.encode("utf-8"))
121
+
122
+ temp_path.replace(output_path)
123
+ if output_path != wheel_path and wheel_path.exists():
124
+ wheel_path.unlink()
125
+ return output_path
126
+
127
+
128
+ def finalize_poetry_wheels() -> None:
129
+ dist = PROJECT_ROOT / "dist"
130
+ if not dist.exists():
131
+ return
132
+ for wheel_path in sorted(dist.glob("cppkh_interface-*.whl")):
133
+ _rewrite_wheel_with_source(wheel_path)
134
+
135
+
136
+ def _rewrite_sdist_with_source(sdist_path: Path) -> None:
137
+ source_data = _source_bytes()
138
+ temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".tar.gz")
139
+ temp_path = Path(temp_file.name)
140
+ temp_file.close()
141
+
142
+ try:
143
+ with tarfile.open(sdist_path, "r:gz") as tin, tarfile.open(temp_path, "w:gz") as tout:
144
+ names = tin.getnames()
145
+ if not names:
146
+ raise RuntimeError(f"sdist is empty: {sdist_path}")
147
+ root = names[0].split("/", 1)[0]
148
+ source_name = f"{root}/{WHEEL_SOURCE_NAME}"
149
+
150
+ for member in tin.getmembers():
151
+ if member.name == source_name:
152
+ continue
153
+ if member.isfile():
154
+ extracted = tin.extractfile(member)
155
+ if extracted is None:
156
+ raise RuntimeError(f"could not read {member.name} from {sdist_path}")
157
+ with extracted:
158
+ tout.addfile(member, extracted)
159
+ else:
160
+ tout.addfile(member)
161
+
162
+ source_info = tarfile.TarInfo(source_name)
163
+ source_info.size = len(source_data)
164
+ source_info.mtime = 0
165
+ source_info.mode = 0o644
166
+ tout.addfile(source_info, io.BytesIO(source_data))
167
+
168
+ temp_path.replace(sdist_path)
169
+ finally:
170
+ if temp_path.exists():
171
+ temp_path.unlink()
172
+
173
+
174
+ def get_requires_for_build_wheel(config_settings=None):
175
+ return poetry_api.get_requires_for_build_wheel(config_settings)
176
+
177
+
178
+ def get_requires_for_build_sdist(config_settings=None):
179
+ return poetry_api.get_requires_for_build_sdist(config_settings)
180
+
181
+
182
+ def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None):
183
+ _sync_cpp_source()
184
+ try:
185
+ return poetry_api.prepare_metadata_for_build_wheel(metadata_directory, config_settings)
186
+ finally:
187
+ _clean_cpp_source()
188
+
189
+
190
+ def build_wheel(wheel_directory, config_settings=None, metadata_directory=None):
191
+ _sync_cpp_source()
192
+ try:
193
+ wheel_name = poetry_api.build_wheel(wheel_directory, config_settings, metadata_directory)
194
+ final_path = _rewrite_wheel_with_source(Path(wheel_directory) / wheel_name)
195
+ return final_path.name
196
+ finally:
197
+ _clean_cpp_source()
198
+
199
+
200
+ def build_sdist(sdist_directory, config_settings=None):
201
+ _sync_cpp_source()
202
+ try:
203
+ sdist_name = poetry_api.build_sdist(sdist_directory, config_settings)
204
+ _rewrite_sdist_with_source(Path(sdist_directory) / sdist_name)
205
+ return sdist_name
206
+ finally:
207
+ _clean_cpp_source()
@@ -1,19 +1,25 @@
1
1
  from .main import (
2
2
  CppkhInterfaceError,
3
3
  compile_cppkh,
4
+ compute_many_pd,
4
5
  compute_pd,
5
6
  get_cppkh_executable,
6
7
  normalize_pd_code,
8
+ normalize_pd_codes,
7
9
  simplify_pd,
10
+ solve_many_khovanov,
8
11
  solve_khovanov,
9
12
  )
10
13
 
11
14
  __all__ = [
12
15
  "CppkhInterfaceError",
13
16
  "compile_cppkh",
17
+ "compute_many_pd",
14
18
  "compute_pd",
15
19
  "get_cppkh_executable",
16
20
  "normalize_pd_code",
21
+ "normalize_pd_codes",
17
22
  "simplify_pd",
23
+ "solve_many_khovanov",
18
24
  "solve_khovanov",
19
25
  ]
@@ -2758,9 +2758,12 @@ std::string Komplex::KhForZ() {
2758
2758
  return ret;
2759
2759
  }
2760
2760
 
2761
- static int chooseXingRecursive(const std::vector<int>& edges, const std::vector<std::vector<int> >& pd,
2762
- std::vector<char>& in, std::vector<char>& done, int depth, std::vector<int>& retmax) {
2763
- int nedges = static_cast<int>(edges.size());
2761
+ static int chooseXingRecursive(const std::vector<int>& edges, const std::vector<std::vector<int> >& pd,
2762
+ std::vector<char>& in, std::vector<char>& done, int depth, std::vector<int>& retmax) {
2763
+ // Choose the next crossing for the growing tangle. The heuristic maximizes
2764
+ // already-attached boundary arcs, with a short lookahead to keep later
2765
+ // intermediate girth smaller. It affects runtime, not the homology result.
2766
+ int nedges = static_cast<int>(edges.size());
2764
2767
  int best = -1, nconbest = -1;
2765
2768
  std::vector<int> rbest(depth, 0);
2766
2769
  for (size_t i = 0; i < pd.size(); ++i) if (!done[i]) {
@@ -2819,15 +2822,91 @@ static int takeNextCrossing(const std::vector<int>&, const std::vector<std::vect
2819
2822
  throw std::runtime_error("no crossing left");
2820
2823
  }
2821
2824
 
2822
- static std::vector<int> getSigns(const std::vector<std::vector<int> >& pd) {
2823
- std::vector<int> xsigns(pd.size());
2824
- for (size_t i = 0; i < pd.size(); ++i) {
2825
- if (pd[i][1] - pd[i][3] == 1 || pd[i][3] - pd[i][1] > 1) xsigns[i] = 1;
2826
- else if (pd[i][3] - pd[i][1] == 1 || pd[i][1] - pd[i][3] > 1) xsigns[i] = -1;
2827
- else throw std::runtime_error("error finding crossing signs");
2828
- }
2829
- return xsigns;
2830
- }
2825
+ static std::vector<int> getSigns(const std::vector<std::vector<int> >& pd) {
2826
+ const int nodeCount = static_cast<int>(pd.size() * 4);
2827
+ if (nodeCount == 0) return std::vector<int>();
2828
+
2829
+ int maxLabel = -1;
2830
+ for (size_t i = 0; i < pd.size(); ++i) {
2831
+ if (pd[i].size() != 4) throw std::runtime_error("PD crossing must have four entries");
2832
+ for (int label : pd[i]) {
2833
+ if (label < 0) throw std::runtime_error("PD arc labels must be positive");
2834
+ maxLabel = std::max(maxLabel, label);
2835
+ }
2836
+ }
2837
+
2838
+ std::vector<int> first(maxLabel + 1, -1), second(maxLabel + 1, -1);
2839
+ for (size_t i = 0; i < pd.size(); ++i) {
2840
+ for (int slot = 0; slot < 4; ++slot) {
2841
+ int label = pd[i][slot];
2842
+ int node = static_cast<int>(4 * i) + slot;
2843
+ if (first[label] == -1) first[label] = node;
2844
+ else if (second[label] == -1) second[label] = node;
2845
+ else throw std::runtime_error("PD arc label appears more than twice");
2846
+ }
2847
+ }
2848
+
2849
+ std::vector<int> other(nodeCount, -1);
2850
+ for (int label = 0; label <= maxLabel; ++label) {
2851
+ if (first[label] == -1) continue;
2852
+ if (second[label] == -1) throw std::runtime_error("PD arc label does not appear twice");
2853
+ other[first[label]] = second[label];
2854
+ other[second[label]] = first[label];
2855
+ }
2856
+
2857
+ // 1 means that this edge leaves the crossing at this incidence, and 0
2858
+ // means that it enters. Opposite incidences at a crossing and the two
2859
+ // incidences of one edge always have opposite directions.
2860
+ std::vector<signed char> outgoing(nodeCount, -1);
2861
+ std::vector<int> queue;
2862
+ queue.reserve(nodeCount);
2863
+ auto orientComponent = [&](int seed, signed char direction) {
2864
+ if (outgoing[seed] != -1) {
2865
+ if (outgoing[seed] != direction) throw std::runtime_error("inconsistent PD orientation");
2866
+ return;
2867
+ }
2868
+ queue.clear();
2869
+ outgoing[seed] = direction;
2870
+ queue.push_back(seed);
2871
+ for (size_t head = 0; head < queue.size(); ++head) {
2872
+ int node = queue[head];
2873
+ int crossing = node / 4;
2874
+ int slot = node % 4;
2875
+ int neighbors[2] = {4 * crossing + ((slot + 2) % 4), other[node]};
2876
+ for (int next : neighbors) {
2877
+ if (next < 0) throw std::runtime_error("broken PD edge incidence");
2878
+ signed char nextDirection = static_cast<signed char>(1 - outgoing[node]);
2879
+ if (outgoing[next] == -1) {
2880
+ outgoing[next] = nextDirection;
2881
+ queue.push_back(next);
2882
+ } else if (outgoing[next] != nextDirection) {
2883
+ throw std::runtime_error("inconsistent PD orientation");
2884
+ }
2885
+ }
2886
+ }
2887
+ };
2888
+
2889
+ // In SageMath's PD convention slot 0 is the incoming under-edge and slot
2890
+ // 2 is the outgoing under-edge. These seeds orient every component that
2891
+ // passes under at least once.
2892
+ for (size_t i = 0; i < pd.size(); ++i) orientComponent(static_cast<int>(4 * i + 2), 1);
2893
+
2894
+ // A component which is over at every crossing has no orientation encoded
2895
+ // by the PD tuples. Match SageMath's deterministic traversal by taking the
2896
+ // first occurrence of the smallest still-unoriented edge as a tail.
2897
+ for (int label = 0; label <= maxLabel; ++label) {
2898
+ if (first[label] != -1 && outgoing[first[label]] == -1) orientComponent(first[label], 1);
2899
+ }
2900
+
2901
+ std::vector<int> xsigns(pd.size());
2902
+ for (size_t i = 0; i < pd.size(); ++i) {
2903
+ const std::vector<int>& crossing = pd[i];
2904
+ if (crossing[0] == crossing[3] || crossing[2] == crossing[1]) xsigns[i] = -1;
2905
+ else if (crossing[3] == crossing[2] || crossing[0] == crossing[1]) xsigns[i] = 1;
2906
+ else xsigns[i] = outgoing[4 * i + 3] ? -1 : 1;
2907
+ }
2908
+ return xsigns;
2909
+ }
2831
2910
 
2832
2911
  static bool sanityPD(const PDCode& pd) {
2833
2912
  std::map<int, int> counts;
@@ -2910,15 +2989,17 @@ static PDCode renumberR1Order(PDCode pd) {
2910
2989
  return pd;
2911
2990
  }
2912
2991
 
2913
- static PDCode eraseR1(PDCode pd) {
2914
- if (!sanityPD(pd)) throw std::runtime_error("invalid PD code: every arc label must appear exactly twice");
2915
- bool hasR1 = true;
2992
+ static PDCode eraseR1(PDCode pd, std::vector<int>* signs) {
2993
+ if (!sanityPD(pd)) throw std::runtime_error("invalid PD code: every arc label must appear exactly twice");
2994
+ if (signs && signs->size() != pd.size()) throw std::runtime_error("crossing sign count does not match PD code");
2995
+ bool hasR1 = true;
2916
2996
  while (hasR1) {
2917
2997
  hasR1 = false;
2918
2998
  for (size_t i = 0; i < pd.size(); ++i) {
2919
2999
  if (uniqueCount(pd[i]) <= 3) {
2920
- std::vector<int> crossing = pd[i];
2921
- pd.erase(pd.begin() + static_cast<std::ptrdiff_t>(i));
3000
+ std::vector<int> crossing = pd[i];
3001
+ pd.erase(pd.begin() + static_cast<std::ptrdiff_t>(i));
3002
+ if (signs) signs->erase(signs->begin() + static_cast<std::ptrdiff_t>(i));
2922
3003
  std::vector<int> singles;
2923
3004
  for (int v : crossing) {
2924
3005
  if (std::count(crossing.begin(), crossing.end(), v) == 1) singles.push_back(v);
@@ -3073,8 +3154,9 @@ static PDCode renumberFullDfs(PDCode pd) {
3073
3154
  return pd;
3074
3155
  }
3075
3156
 
3076
- static PDCode eraseOneNugatory(PDCode pd, size_t index) {
3077
- if (uniqueCount(pd[index]) != 4) throw std::runtime_error("nugatory erase requires R1-free PD code");
3157
+ static PDCode eraseOneNugatory(PDCode pd, size_t index, std::vector<int>* signs) {
3158
+ if (uniqueCount(pd[index]) != 4) throw std::runtime_error("nugatory erase requires R1-free PD code");
3159
+ if (signs && signs->size() != pd.size()) throw std::runtime_error("crossing sign count does not match PD code");
3078
3160
  int ax = pd[index][0];
3079
3161
  int bx = pd[index][1];
3080
3162
  int cx = pd[index][2];
@@ -3096,22 +3178,23 @@ static PDCode eraseOneNugatory(PDCode pd, size_t index) {
3096
3178
  if (!loopSet.count(ax) || !loopSet.count(bx) || !loopSet.count(cx) || !loopSet.count(dx)) {
3097
3179
  throw std::runtime_error("nugatory crossing arcs are not in one component");
3098
3180
  }
3099
- PDCode bad = pd;
3100
- bad.erase(bad.begin() + static_cast<std::ptrdiff_t>(index));
3181
+ PDCode bad = pd;
3182
+ bad.erase(bad.begin() + static_cast<std::ptrdiff_t>(index));
3183
+ if (signs) signs->erase(signs->begin() + static_cast<std::ptrdiff_t>(index));
3101
3184
  bad = replaceArcValue(bad, ax, cx);
3102
3185
  bad = replaceArcValue(bad, dx, bx);
3103
3186
  return renumberFullDfs(bad);
3104
3187
  }
3105
3188
 
3106
- static PDCode simplifyPDCode(PDCode pd) {
3107
- pd = eraseR1(pd);
3108
- while (true) {
3109
- int index = findNugatory(pd);
3110
- if (index < 0) break;
3111
- pd = eraseOneNugatory(pd, static_cast<size_t>(index));
3112
- }
3113
- return pd;
3114
- }
3189
+ static PDCode simplifyPDCode(PDCode pd, std::vector<int>* signs = nullptr) {
3190
+ pd = eraseR1(pd, signs);
3191
+ while (true) {
3192
+ int index = findNugatory(pd);
3193
+ if (index < 0) break;
3194
+ pd = eraseOneNugatory(pd, static_cast<size_t>(index), signs);
3195
+ }
3196
+ return pd;
3197
+ }
3115
3198
 
3116
3199
  static int komplexSize(const Komplex& k) {
3117
3200
  int total = 0;
@@ -3119,8 +3202,8 @@ static int komplexSize(const Komplex& k) {
3119
3202
  return total;
3120
3203
  }
3121
3204
 
3122
- static Komplex generateFast(const std::vector<std::vector<int> >& pd, const std::vector<int>& xsigns) {
3123
- KH_PROFILE(generateFast);
3205
+ static Komplex generateFast(const std::vector<std::vector<int> >& pd, const std::vector<int>& xsigns) {
3206
+ KH_PROFILE(generateFast);
3124
3207
  if (pd.empty()) {
3125
3208
  Komplex kom(1);
3126
3209
  kom.columns[0] = SmoothingColumn(1);
@@ -3128,11 +3211,13 @@ static Komplex generateFast(const std::vector<std::vector<int> >& pd, const std:
3128
3211
  kom.reduce();
3129
3212
  return kom;
3130
3213
  }
3131
- std::vector<char> in(pd.size() * 4, 0), done(pd.size(), 0);
3132
- std::vector<std::vector<int> > pd1(1, std::vector<int>{0,1,2,3});
3133
- Komplex kplus(pd1, std::vector<int>{1}, 4);
3134
- Komplex kminus(pd1, std::vector<int>{-1}, 4);
3135
- std::vector<int> edges;
3214
+ std::vector<char> in(pd.size() * 4, 0), done(pd.size(), 0);
3215
+ std::vector<std::vector<int> > pd1(1, std::vector<int>{0,1,2,3});
3216
+ Komplex kplus(pd1, std::vector<int>{1}, 4);
3217
+ Komplex kminus(pd1, std::vector<int>{-1}, 4);
3218
+ // Build the full complex by composing one crossing complex at a time and
3219
+ // reducing after every composition, following JavaKh's tangle-growth path.
3220
+ std::vector<int> edges;
3136
3221
  int firstdepth = pd.size() > 4 ? 3 : static_cast<int>(pd.size()) - 1;
3137
3222
  std::vector<int> firstdummy(firstdepth + 1, 0);
3138
3223
  int first = g_options.reorderCrossings ? chooseXingRecursive(edges, pd, in, done, firstdepth, firstdummy)
@@ -3312,16 +3397,17 @@ static std::vector<std::string> listInputFiles(const std::string& dir) {
3312
3397
  return files;
3313
3398
  }
3314
3399
 
3315
- static std::string computePD(const std::vector<std::vector<int> >& pd) {
3316
- flushCobCache();
3317
- g_smallArena.reset();
3318
- PDCode working = g_options.simplifyPD ? simplifyPDCode(pd) : pd;
3319
- Komplex k = generateFast(working, getSigns(working));
3400
+ static std::string computePD(const std::vector<std::vector<int> >& pd) {
3401
+ flushCobCache();
3402
+ g_smallArena.reset();
3403
+ std::vector<int> signs = getSigns(pd);
3404
+ PDCode working = g_options.simplifyPD ? simplifyPDCode(pd, &signs) : pd;
3405
+ Komplex k = generateFast(working, signs);
3320
3406
  ProfileScope khScope(g_profile.kh);
3321
3407
  return k.KhForZ();
3322
3408
  }
3323
3409
 
3324
- static std::string formatPDCode(const PDCode& pd) {
3410
+ static std::string formatPDCode(const PDCode& pd) {
3325
3411
  std::ostringstream out;
3326
3412
  out << "PD[";
3327
3413
  for (size_t i = 0; i < pd.size(); ++i) {
@@ -3334,10 +3420,22 @@ static std::string formatPDCode(const PDCode& pd) {
3334
3420
  out << "]";
3335
3421
  }
3336
3422
  out << "]";
3337
- return out.str();
3338
- }
3339
-
3340
- static std::string simplifiedPDString(const PDCode& pd) {
3423
+ return out.str();
3424
+ }
3425
+
3426
+ static std::string formatCrossingSigns(const PDCode& pd) {
3427
+ std::vector<int> signs = getSigns(pd);
3428
+ std::ostringstream out;
3429
+ out << "[";
3430
+ for (size_t i = 0; i < signs.size(); ++i) {
3431
+ if (i) out << ",";
3432
+ out << signs[i];
3433
+ }
3434
+ out << "]";
3435
+ return out.str();
3436
+ }
3437
+
3438
+ static std::string simplifiedPDString(const PDCode& pd) {
3341
3439
  PDCode working = g_options.simplifyPD ? simplifyPDCode(pd) : pd;
3342
3440
  return formatPDCode(working);
3343
3441
  }
@@ -3409,6 +3507,38 @@ CPPKH_API char* cppkh_compute_pd(const char* pd_code) {
3409
3507
  return cppkh_compute_pd_ex(pd_code, 1, 1);
3410
3508
  }
3411
3509
 
3510
+ CPPKH_API char* cppkh_compute_pd_batch_ex(const char* pd_codes, int simplify_pd, int reorder_crossings) {
3511
+ try {
3512
+ g_cppkhLastError.clear();
3513
+ if (!pd_codes) throw std::runtime_error("pd_codes is null");
3514
+ std::vector<std::pair<std::string, kh::PDCode> > parsed = kh::parsePDDocument(pd_codes, "ctypes-batch");
3515
+ if (parsed.empty()) throw std::runtime_error("no PD code found");
3516
+
3517
+ CppkhOptionsGuard guard;
3518
+ kh::g_options.progress = false;
3519
+ kh::g_options.profile = false;
3520
+ kh::g_options.simplifyPD = simplify_pd != 0;
3521
+ kh::g_options.reorderCrossings = reorder_crossings != 0;
3522
+
3523
+ std::ostringstream out;
3524
+ for (size_t i = 0; i < parsed.size(); ++i) {
3525
+ if (i) out << "\n";
3526
+ out << kh::computePD(parsed[i].second);
3527
+ }
3528
+ return cppkhDuplicateString(out.str());
3529
+ } catch (const std::exception& e) {
3530
+ g_cppkhLastError = e.what();
3531
+ return nullptr;
3532
+ } catch (...) {
3533
+ g_cppkhLastError = "unknown error";
3534
+ return nullptr;
3535
+ }
3536
+ }
3537
+
3538
+ CPPKH_API char* cppkh_compute_pd_batch(const char* pd_codes) {
3539
+ return cppkh_compute_pd_batch_ex(pd_codes, 1, 1);
3540
+ }
3541
+
3412
3542
  CPPKH_API char* cppkh_simplify_pd(const char* pd_code) {
3413
3543
  try {
3414
3544
  g_cppkhLastError.clear();
@@ -3432,7 +3562,7 @@ CPPKH_API char* cppkh_simplify_pd(const char* pd_code) {
3432
3562
 
3433
3563
  #ifndef CPPKH_SHARED_LIBRARY
3434
3564
  static void usage() {
3435
- std::cout << "Usage: javakh_cpp [--pd-file FILE] [--pd-dir DIR] [--pd-code CODE] [--ordered] [--threads N|auto] [--quiet] [--profile] [--no-simplify-pd] [--print-simplified-pd]\n";
3565
+ std::cout << "Usage: cppkh [--pd-file FILE] [--pd-dir DIR] [--pd-code CODE] [--ordered] [--threads N|auto] [--quiet] [--profile] [--no-simplify-pd] [--print-simplified-pd] [--print-crossing-signs]\n";
3436
3566
  std::cout << "Thread backend: " << KH_THREAD_BACKEND_NAME << "\n";
3437
3567
  std::cout << "Detected CPU threads: " << kh::detectHardwareThreads() << "\n";
3438
3568
  std::cout << "PD simplification: R1 removal then nugatory crossing removal is enabled by default.\n";
@@ -3440,9 +3570,10 @@ static void usage() {
3440
3570
 
3441
3571
  int main(int argc, char** argv) {
3442
3572
  try {
3443
- std::vector<std::string> files;
3444
- std::vector<std::pair<std::string, std::vector<std::vector<int> > > > jobs;
3445
- bool printSimplifiedPD = false;
3573
+ std::vector<std::string> files;
3574
+ std::vector<std::pair<std::string, std::vector<std::vector<int> > > > jobs;
3575
+ bool printSimplifiedPD = false;
3576
+ bool printCrossingSigns = false;
3446
3577
  for (int i = 1; i < argc; ++i) {
3447
3578
  std::string a = argv[i];
3448
3579
  if (a == "--help" || a == "-h") { usage(); return 0; }
@@ -3461,8 +3592,9 @@ int main(int argc, char** argv) {
3461
3592
  else if (a == "--quiet" || a == "-q") kh::g_options.progress = false;
3462
3593
  else if (a == "--profile") kh::g_options.profile = true;
3463
3594
  else if (a == "--no-simplify-pd" || a == "--raw-pd") kh::g_options.simplifyPD = false;
3464
- else if (a == "--simplify-pd") kh::g_options.simplifyPD = true;
3465
- else if (a == "--print-simplified-pd") printSimplifiedPD = true;
3595
+ else if (a == "--simplify-pd") kh::g_options.simplifyPD = true;
3596
+ else if (a == "--print-simplified-pd") printSimplifiedPD = true;
3597
+ else if (a == "--print-crossing-signs") printCrossingSigns = true;
3466
3598
  else if (a == "--threads" || a == "-j") {
3467
3599
  if (++i >= argc) throw std::runtime_error("--threads needs a number");
3468
3600
  std::string v = argv[i];
@@ -3489,9 +3621,10 @@ int main(int argc, char** argv) {
3489
3621
  kh::g_profile.reset();
3490
3622
  profileStart = kh::profileNowNs();
3491
3623
  }
3492
- if (label) std::cout << jobs[i].first << "\t";
3493
- if (printSimplifiedPD) std::cout << kh::simplifiedPDString(jobs[i].second) << "\n";
3494
- else std::cout << "\"" << kh::computePD(jobs[i].second) << "\"\n";
3624
+ if (label) std::cout << jobs[i].first << "\t";
3625
+ if (printSimplifiedPD) std::cout << kh::simplifiedPDString(jobs[i].second) << "\n";
3626
+ else if (printCrossingSigns) std::cout << kh::formatCrossingSigns(jobs[i].second) << "\n";
3627
+ else std::cout << "\"" << kh::computePD(jobs[i].second) << "\"\n";
3495
3628
  if (kh::g_options.profile) {
3496
3629
  kh::g_profile.totalNs = kh::profileNowNs() - profileStart;
3497
3630
  kh::printProfile();
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import argparse
4
4
  import ast
5
+ import contextlib
5
6
  import hashlib
6
7
  import os
7
8
  import pathlib
@@ -22,6 +23,7 @@ import pd_code_sanity
22
23
 
23
24
  PathLike = Union[str, os.PathLike]
24
25
  PdInput = Union[str, Sequence[Sequence[int]]]
26
+ PdManyInput = Union[str, Sequence[PdInput]]
25
27
  UNKNOT_RESULT = "q^-1*t^0*Z[0] + q^1*t^0*Z[0]"
26
28
 
27
29
 
@@ -88,9 +90,37 @@ def normalize_pd_code(pd_code: PdInput) -> str:
88
90
  return _format_pd(_as_crossings(pd_code))
89
91
 
90
92
 
93
+ def normalize_pd_codes(pd_codes: PdManyInput) -> str:
94
+ """Normalize one or more PD codes into a newline-separated PD document."""
95
+
96
+ if isinstance(pd_codes, str):
97
+ return pd_codes.strip()
98
+ return "\n".join(normalize_pd_code(pd_code) for pd_code in pd_codes)
99
+
100
+
101
+ @contextlib.contextmanager
91
102
  def _resource_source_path():
92
103
  source = resources.files("cppkh_interface") / "data" / "src" / "main.cpp"
93
- return resources.as_file(source)
104
+ try:
105
+ with resources.as_file(source) as resource_path:
106
+ if pathlib.Path(resource_path).exists():
107
+ yield pathlib.Path(resource_path)
108
+ return
109
+ except FileNotFoundError:
110
+ pass
111
+
112
+ current = pathlib.Path(__file__).resolve()
113
+ for parent in current.parents:
114
+ candidate = parent / "src" / "main.cpp"
115
+ if candidate.exists():
116
+ yield candidate
117
+ return
118
+
119
+ raise CppkhInterfaceError(
120
+ "cppkh C++ source was not found. Installed wheels include it under "
121
+ "cppkh_interface/data/src/main.cpp; editable checkouts use the "
122
+ "repository root src/main.cpp."
123
+ )
94
124
 
95
125
 
96
126
  def _cache_dir() -> pathlib.Path:
@@ -217,17 +247,19 @@ def get_cppkh_executable() -> pathlib.Path:
217
247
  return compile_cppkh()
218
248
 
219
249
 
220
- def _run_cppkh(
250
+ def _run_cppkh_document(
221
251
  pd_text: str,
222
252
  *,
223
253
  encoding: Optional[str] = None,
224
254
  threads: Union[str, int] = "1",
255
+ simplify_pd: bool = False,
225
256
  print_simplified_pd: bool = False,
226
- ) -> str:
257
+ ) -> list[str]:
227
258
  exe = compile_cppkh()
228
259
  with tempfile.NamedTemporaryFile("w", suffix=".pd", encoding="utf-8", delete=False) as handle:
229
260
  handle.write(pd_text)
230
- handle.write("\n")
261
+ if pd_text and not pd_text.endswith("\n"):
262
+ handle.write("\n")
231
263
  pd_file = handle.name
232
264
 
233
265
  command = [
@@ -237,8 +269,9 @@ def _run_cppkh(
237
269
  "--quiet",
238
270
  "--threads",
239
271
  str(threads),
240
- "--no-simplify-pd",
241
272
  ]
273
+ if not simplify_pd:
274
+ command.append("--no-simplify-pd")
242
275
  if print_simplified_pd:
243
276
  command.append("--print-simplified-pd")
244
277
 
@@ -270,12 +303,32 @@ def _run_cppkh(
270
303
  raise CppkhInterfaceError(detail or f"cppkh exited with code {result.returncode}")
271
304
 
272
305
  if print_simplified_pd:
273
- return result.stdout.strip()
306
+ return [line.strip() for line in result.stdout.splitlines() if line.strip()]
274
307
 
275
308
  matches = re.findall(r'"([^"]*)"', result.stdout)
276
309
  if not matches:
277
310
  raise CppkhInterfaceError(f"result not found in cppkh output: {result.stdout!r}")
278
- return matches[-1]
311
+ return matches
312
+
313
+
314
+ def _run_cppkh(
315
+ pd_text: str,
316
+ *,
317
+ encoding: Optional[str] = None,
318
+ threads: Union[str, int] = "1",
319
+ simplify_pd: bool = False,
320
+ print_simplified_pd: bool = False,
321
+ ) -> str:
322
+ results = _run_cppkh_document(
323
+ pd_text,
324
+ encoding=encoding,
325
+ threads=threads,
326
+ simplify_pd=simplify_pd,
327
+ print_simplified_pd=print_simplified_pd,
328
+ )
329
+ if len(results) != 1:
330
+ raise CppkhInterfaceError(f"expected exactly one result, got {len(results)}")
331
+ return results[0]
279
332
 
280
333
 
281
334
  def _prepare_crossings(pd_code: PdInput, de_r1: bool, de_k8: bool) -> list[list[int]]:
@@ -288,6 +341,29 @@ def _prepare_crossings(pd_code: PdInput, de_r1: bool, de_k8: bool) -> list[list[
288
341
  return crossings
289
342
 
290
343
 
344
+ def _prepare_many_for_cppkh(
345
+ pd_codes: PdManyInput,
346
+ *,
347
+ de_r1: bool,
348
+ de_k8: bool,
349
+ ) -> tuple[str, bool]:
350
+ if de_r1 != de_k8:
351
+ raise ValueError(
352
+ "cppkh batch mode supports de_r1 and de_k8 only as a pair. "
353
+ "Use both True for backend R1+nugatory simplification or both False for raw PD input."
354
+ )
355
+ if isinstance(pd_codes, str):
356
+ return pd_codes.strip(), bool(de_r1 and de_k8)
357
+
358
+ prepared = []
359
+ use_cpp_simplify = bool(de_r1 and de_k8)
360
+ for pd_code in pd_codes:
361
+ crossings = _as_crossings(pd_code)
362
+ _check_sanity(crossings)
363
+ prepared.append(_format_pd(crossings))
364
+ return "\n".join(prepared), use_cpp_simplify
365
+
366
+
291
367
  def solve_khovanov(
292
368
  pd_code: PdInput,
293
369
  encoding: Optional[str] = None,
@@ -297,6 +373,24 @@ def solve_khovanov(
297
373
  ) -> str:
298
374
  """Compute Khovanov homology with a javakh-interface compatible signature."""
299
375
 
376
+ if de_r1 == de_k8:
377
+ crossings = _as_crossings(pd_code)
378
+ _check_sanity(crossings)
379
+ if show_real_pdcode:
380
+ if de_r1:
381
+ simplified = _run_cppkh_document(
382
+ _format_pd(crossings),
383
+ encoding=encoding,
384
+ simplify_pd=True,
385
+ print_simplified_pd=True,
386
+ )
387
+ print(f"Real PD code after de_r1 and de_k8: {simplified[0] if simplified else ''}")
388
+ else:
389
+ print(f"Real PD code after de_r1 and de_k8: {crossings}")
390
+ if crossings == []:
391
+ return UNKNOT_RESULT
392
+ return _run_cppkh(_format_pd(crossings), encoding=encoding, simplify_pd=de_r1)
393
+
300
394
  crossings = _prepare_crossings(pd_code, de_r1=de_r1, de_k8=de_k8)
301
395
  if show_real_pdcode:
302
396
  print(f"Real PD code after de_r1 and de_k8: {crossings}")
@@ -305,6 +399,39 @@ def solve_khovanov(
305
399
  return _run_cppkh(_format_pd(crossings), encoding=encoding)
306
400
 
307
401
 
402
+ def solve_many_khovanov(
403
+ pd_codes: PdManyInput,
404
+ encoding: Optional[str] = None,
405
+ de_r1: bool = True,
406
+ de_k8: bool = True,
407
+ show_real_pdcode: bool = False,
408
+ threads: Union[str, int] = "1",
409
+ ) -> list[str]:
410
+ """Compute many PD codes in one cppkh process.
411
+
412
+ With the default ``de_r1=True`` and ``de_k8=True`` settings, the raw PD
413
+ document is passed directly to cppkh and the C++ simplifier handles R1 then
414
+ nugatory crossing removal for the whole batch.
415
+ """
416
+
417
+ document, use_cpp_simplify = _prepare_many_for_cppkh(pd_codes, de_r1=de_r1, de_k8=de_k8)
418
+ if show_real_pdcode:
419
+ if use_cpp_simplify:
420
+ simplified = _run_cppkh_document(
421
+ document,
422
+ encoding=encoding,
423
+ threads=threads,
424
+ simplify_pd=True,
425
+ print_simplified_pd=True,
426
+ )
427
+ print(f"Real PD code after de_r1 and de_k8: {simplified}")
428
+ else:
429
+ print(f"Real PD code after de_r1 and de_k8: {document.splitlines()}")
430
+ if not document:
431
+ return []
432
+ return _run_cppkh_document(document, encoding=encoding, threads=threads, simplify_pd=use_cpp_simplify)
433
+
434
+
308
435
  def compute_pd(
309
436
  pd_code: PdInput,
310
437
  *,
@@ -316,6 +443,25 @@ def compute_pd(
316
443
  ) -> str:
317
444
  """Compute Khovanov homology using the same defaults as solve_khovanov."""
318
445
 
446
+ if de_r1 == de_k8:
447
+ crossings = _as_crossings(pd_code)
448
+ _check_sanity(crossings)
449
+ if show_real_pdcode:
450
+ if de_r1:
451
+ simplified = _run_cppkh_document(
452
+ _format_pd(crossings),
453
+ encoding=encoding,
454
+ threads=threads,
455
+ simplify_pd=True,
456
+ print_simplified_pd=True,
457
+ )
458
+ print(f"Real PD code after de_r1 and de_k8: {simplified[0] if simplified else ''}")
459
+ else:
460
+ print(f"Real PD code after de_r1 and de_k8: {crossings}")
461
+ if crossings == []:
462
+ return UNKNOT_RESULT
463
+ return _run_cppkh(_format_pd(crossings), encoding=encoding, threads=threads, simplify_pd=de_r1)
464
+
319
465
  crossings = _prepare_crossings(pd_code, de_r1=de_r1, de_k8=de_k8)
320
466
  if show_real_pdcode:
321
467
  print(f"Real PD code after de_r1 and de_k8: {crossings}")
@@ -324,6 +470,27 @@ def compute_pd(
324
470
  return _run_cppkh(_format_pd(crossings), encoding=encoding, threads=threads)
325
471
 
326
472
 
473
+ def compute_many_pd(
474
+ pd_codes: PdManyInput,
475
+ *,
476
+ encoding: Optional[str] = None,
477
+ de_r1: bool = True,
478
+ de_k8: bool = True,
479
+ show_real_pdcode: bool = False,
480
+ threads: Union[str, int] = "1",
481
+ ) -> list[str]:
482
+ """Compute many PD codes in one cached cppkh executable invocation."""
483
+
484
+ return solve_many_khovanov(
485
+ pd_codes,
486
+ encoding=encoding,
487
+ de_r1=de_r1,
488
+ de_k8=de_k8,
489
+ show_real_pdcode=show_real_pdcode,
490
+ threads=threads,
491
+ )
492
+
493
+
327
494
  def simplify_pd(pd_code: PdInput, *, de_r1: bool = True, de_k8: bool = True) -> str:
328
495
  """Return the normalized PD string after optional R1 and nugatory simplification."""
329
496
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cppkh-interface"
3
- version = "0.1.0"
3
+ version = "0.1.2"
4
4
  description = "Python interface for cppkh Khovanov homology computation with runtime C++ compilation."
5
5
  authors = [
6
6
  {name = "GGN_2015", email = "neko@jlulug.org"}
@@ -26,9 +26,11 @@ cppkh-interface = "cppkh_interface.main:main"
26
26
  [tool.poetry]
27
27
  packages = [{ include = "cppkh_interface" }]
28
28
  include = [
29
+ { path = "build_backend/cppkh_interface_build_backend.py", format = ["sdist"] },
29
30
  { path = "cppkh_interface/data/src/main.cpp", format = ["sdist", "wheel"] }
30
31
  ]
31
32
 
32
33
  [build-system]
33
- requires = ["poetry-core>=2.0.0,<3.0.0"]
34
- build-backend = "poetry.core.masonry.api"
34
+ requires = ["poetry-core>=2.0.0,<3.0.0", "setuptools>=61"]
35
+ build-backend = "cppkh_interface_build_backend"
36
+ backend-path = ["build_backend"]