cppkh-interface 0.1.0__py3-none-any.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.
@@ -0,0 +1,352 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import ast
5
+ import hashlib
6
+ import os
7
+ import pathlib
8
+ import platform
9
+ import re
10
+ import shlex
11
+ import subprocess
12
+ import sys
13
+ import tempfile
14
+ from importlib import resources
15
+ from typing import Optional, Sequence, Union
16
+
17
+ import cpp_simple_interface
18
+ import pd_code_de_r1
19
+ import pd_code_delete_nugatory
20
+ import pd_code_sanity
21
+
22
+
23
+ PathLike = Union[str, os.PathLike]
24
+ PdInput = Union[str, Sequence[Sequence[int]]]
25
+ UNKNOT_RESULT = "q^-1*t^0*Z[0] + q^1*t^0*Z[0]"
26
+
27
+
28
+ class CppkhInterfaceError(RuntimeError):
29
+ """Raised when the C++ executable cannot be built or run."""
30
+
31
+
32
+ def _format_pd(crossings: Sequence[Sequence[int]]) -> str:
33
+ parts = []
34
+ for crossing in crossings:
35
+ values = list(crossing)
36
+ if len(values) != 4:
37
+ raise ValueError(f"PD crossing must have four entries: {crossing!r}")
38
+ parts.append("X[{},{},{},{}]".format(*(int(value) for value in values)))
39
+ return "PD[" + ",".join(parts) + "]"
40
+
41
+
42
+ def _parse_x_crossings(text: str) -> Optional[list[list[int]]]:
43
+ pattern = r"X\s*\[\s*(-?\d+)\s*,\s*(-?\d+)\s*,\s*(-?\d+)\s*,\s*(-?\d+)\s*\]"
44
+ crossings = []
45
+ for match in re.finditer(pattern, text):
46
+ crossings.append([int(match.group(i)) for i in range(1, 5)])
47
+ return crossings if crossings else None
48
+
49
+
50
+ def _as_crossings(pd_code: PdInput) -> list[list[int]]:
51
+ if isinstance(pd_code, str):
52
+ body = pd_code.strip()
53
+ if ":" in body:
54
+ body = body.split(":", 1)[1].strip()
55
+ if body.replace(" ", "") in ("PD[]", "[]"):
56
+ return []
57
+
58
+ parsed = _parse_x_crossings(body)
59
+ if parsed is not None:
60
+ return parsed
61
+
62
+ try:
63
+ value = ast.literal_eval(body)
64
+ except (SyntaxError, ValueError) as exc:
65
+ raise ValueError(f"unsupported PD-code string format: {pd_code!r}") from exc
66
+ else:
67
+ value = pd_code
68
+
69
+ crossings = []
70
+ for crossing in value:
71
+ values = list(crossing)
72
+ if len(values) != 4:
73
+ raise ValueError(f"PD crossing must have four entries: {crossing!r}")
74
+ crossings.append([int(item) for item in values])
75
+ return crossings
76
+
77
+
78
+ def _check_sanity(crossings: list[list[int]]) -> None:
79
+ if crossings == []:
80
+ return
81
+ if not pd_code_sanity.sanity(crossings):
82
+ raise TypeError("pd_code does not satisfy PD-code sanity checks")
83
+
84
+
85
+ def normalize_pd_code(pd_code: PdInput) -> str:
86
+ """Normalize a supported PD-code value into standard ``PD[X[...],...]`` text."""
87
+
88
+ return _format_pd(_as_crossings(pd_code))
89
+
90
+
91
+ def _resource_source_path():
92
+ source = resources.files("cppkh_interface") / "data" / "src" / "main.cpp"
93
+ return resources.as_file(source)
94
+
95
+
96
+ def _cache_dir() -> pathlib.Path:
97
+ env_value = os.environ.get("CPPKH_INTERFACE_CACHE_DIR")
98
+ if env_value:
99
+ root = pathlib.Path(env_value)
100
+ elif sys.platform == "win32":
101
+ root = pathlib.Path(os.environ.get("LOCALAPPDATA", pathlib.Path.home())) / "cppkh-interface"
102
+ elif sys.platform == "darwin":
103
+ root = pathlib.Path.home() / "Library" / "Caches" / "cppkh-interface"
104
+ else:
105
+ root = pathlib.Path(os.environ.get("XDG_CACHE_HOME", pathlib.Path.home() / ".cache")) / "cppkh-interface"
106
+ root.mkdir(parents=True, exist_ok=True)
107
+ return root
108
+
109
+
110
+ def _default_compile_flags() -> list[str]:
111
+ flags = ["-std=c++14", "-O3", "-DNDEBUG"]
112
+ native = os.environ.get("CPPKH_INTERFACE_NATIVE", "1").strip().lower()
113
+ if native not in ("0", "false", "no", "off"):
114
+ flags.append("-march=native")
115
+ if platform.system() != "Windows":
116
+ flags.append("-pthread")
117
+ extra = os.environ.get("CPPKH_INTERFACE_CXXFLAGS", "").strip()
118
+ if extra:
119
+ flags.extend(shlex.split(extra))
120
+ return flags
121
+
122
+
123
+ def _exe_suffix() -> str:
124
+ return ".exe" if platform.system() == "Windows" else ""
125
+
126
+
127
+ def _compiler_runtime_path_entries() -> list[str]:
128
+ compiler = cpp_simple_interface.get_gpp_filepath().strip()
129
+ if not compiler:
130
+ return []
131
+
132
+ candidates = []
133
+ unquoted = compiler
134
+ if len(unquoted) >= 2 and unquoted[0] == unquoted[-1] and unquoted[0] in ("'", '"'):
135
+ unquoted = unquoted[1:-1]
136
+ candidates.append(unquoted)
137
+
138
+ try:
139
+ candidates.extend(shlex.split(compiler, posix=True))
140
+ except ValueError:
141
+ pass
142
+
143
+ paths = []
144
+ for candidate in candidates:
145
+ path = pathlib.Path(candidate)
146
+ if path.exists() and path.is_file():
147
+ parent = str(path.resolve().parent)
148
+ if parent not in paths:
149
+ paths.append(parent)
150
+ return paths
151
+
152
+
153
+ def _cache_key(source_bytes: bytes, flags: Sequence[str]) -> str:
154
+ digest = hashlib.sha256()
155
+ digest.update(source_bytes)
156
+ digest.update("\0".join(flags).encode("utf-8"))
157
+ digest.update(cpp_simple_interface.get_gpp_filepath().encode("utf-8"))
158
+ digest.update(platform.platform().encode("utf-8"))
159
+ return digest.hexdigest()[:20]
160
+
161
+
162
+ def compile_cppkh(
163
+ *,
164
+ force: bool = False,
165
+ cxx: Optional[str] = None,
166
+ extra_flags: Optional[Sequence[str]] = None,
167
+ ) -> pathlib.Path:
168
+ """Compile the packaged C++ source with cpp-simple-interface and return the executable path."""
169
+
170
+ if cxx:
171
+ cpp_simple_interface.set_gpp_filepath(cxx)
172
+
173
+ with _resource_source_path() as source:
174
+ source_path = pathlib.Path(source)
175
+ source_bytes = source_path.read_bytes()
176
+ flags = _default_compile_flags()
177
+ if extra_flags:
178
+ flags.extend(str(flag) for flag in extra_flags)
179
+
180
+ cache = _cache_dir()
181
+ exe = cache / f"cppkh-{_cache_key(source_bytes, flags)}{_exe_suffix()}"
182
+ if exe.exists() and not force:
183
+ return exe
184
+
185
+ tmp_exe = cache / f"{exe.name}.tmp-{os.getpid()}{_exe_suffix()}"
186
+ if tmp_exe.exists():
187
+ tmp_exe.unlink()
188
+
189
+ success, message = cpp_simple_interface.compile_cpp_files(
190
+ [str(source_path)],
191
+ str(tmp_exe),
192
+ other_flags=flags,
193
+ )
194
+ if not success and "-march=native" in flags:
195
+ fallback_flags = [flag for flag in flags if flag != "-march=native"]
196
+ success, message = cpp_simple_interface.compile_cpp_files(
197
+ [str(source_path)],
198
+ str(tmp_exe),
199
+ other_flags=fallback_flags,
200
+ )
201
+
202
+ if not success:
203
+ raise CppkhInterfaceError(message)
204
+ if not tmp_exe.exists():
205
+ raise CppkhInterfaceError(f"compiled executable was not created: {tmp_exe}")
206
+ os.replace(tmp_exe, exe)
207
+ try:
208
+ exe.chmod(exe.stat().st_mode | 0o755)
209
+ except OSError:
210
+ pass
211
+ return exe
212
+
213
+
214
+ def get_cppkh_executable() -> pathlib.Path:
215
+ """Return the cached executable path, compiling it first when necessary."""
216
+
217
+ return compile_cppkh()
218
+
219
+
220
+ def _run_cppkh(
221
+ pd_text: str,
222
+ *,
223
+ encoding: Optional[str] = None,
224
+ threads: Union[str, int] = "1",
225
+ print_simplified_pd: bool = False,
226
+ ) -> str:
227
+ exe = compile_cppkh()
228
+ with tempfile.NamedTemporaryFile("w", suffix=".pd", encoding="utf-8", delete=False) as handle:
229
+ handle.write(pd_text)
230
+ handle.write("\n")
231
+ pd_file = handle.name
232
+
233
+ command = [
234
+ str(exe),
235
+ "--pd-file",
236
+ pd_file,
237
+ "--quiet",
238
+ "--threads",
239
+ str(threads),
240
+ "--no-simplify-pd",
241
+ ]
242
+ if print_simplified_pd:
243
+ command.append("--print-simplified-pd")
244
+
245
+ kwargs = {
246
+ "stdout": subprocess.PIPE,
247
+ "stderr": subprocess.PIPE,
248
+ "text": True,
249
+ "encoding": encoding or "utf-8",
250
+ "errors": "replace",
251
+ }
252
+ env = os.environ.copy()
253
+ runtime_paths = _compiler_runtime_path_entries()
254
+ if runtime_paths:
255
+ env["PATH"] = os.pathsep.join(runtime_paths + [env.get("PATH", "")])
256
+ kwargs["env"] = env
257
+ if platform.system() == "Windows":
258
+ kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
259
+
260
+ try:
261
+ result = subprocess.run(command, **kwargs)
262
+ finally:
263
+ try:
264
+ os.unlink(pd_file)
265
+ except OSError:
266
+ pass
267
+
268
+ if result.returncode != 0:
269
+ detail = (result.stderr or result.stdout or "").strip()
270
+ raise CppkhInterfaceError(detail or f"cppkh exited with code {result.returncode}")
271
+
272
+ if print_simplified_pd:
273
+ return result.stdout.strip()
274
+
275
+ matches = re.findall(r'"([^"]*)"', result.stdout)
276
+ if not matches:
277
+ raise CppkhInterfaceError(f"result not found in cppkh output: {result.stdout!r}")
278
+ return matches[-1]
279
+
280
+
281
+ def _prepare_crossings(pd_code: PdInput, de_r1: bool, de_k8: bool) -> list[list[int]]:
282
+ crossings = _as_crossings(pd_code)
283
+ _check_sanity(crossings)
284
+ if de_r1:
285
+ crossings = pd_code_de_r1.de_r1(crossings)
286
+ if de_k8:
287
+ crossings = pd_code_delete_nugatory.erase_all_nugatory(crossings)
288
+ return crossings
289
+
290
+
291
+ def solve_khovanov(
292
+ pd_code: PdInput,
293
+ encoding: Optional[str] = None,
294
+ de_r1: bool = True,
295
+ de_k8: bool = True,
296
+ show_real_pdcode: bool = False,
297
+ ) -> str:
298
+ """Compute Khovanov homology with a javakh-interface compatible signature."""
299
+
300
+ crossings = _prepare_crossings(pd_code, de_r1=de_r1, de_k8=de_k8)
301
+ if show_real_pdcode:
302
+ print(f"Real PD code after de_r1 and de_k8: {crossings}")
303
+ if crossings == []:
304
+ return UNKNOT_RESULT
305
+ return _run_cppkh(_format_pd(crossings), encoding=encoding)
306
+
307
+
308
+ def compute_pd(
309
+ pd_code: PdInput,
310
+ *,
311
+ encoding: Optional[str] = None,
312
+ de_r1: bool = True,
313
+ de_k8: bool = True,
314
+ show_real_pdcode: bool = False,
315
+ threads: Union[str, int] = "1",
316
+ ) -> str:
317
+ """Compute Khovanov homology using the same defaults as solve_khovanov."""
318
+
319
+ crossings = _prepare_crossings(pd_code, de_r1=de_r1, de_k8=de_k8)
320
+ if show_real_pdcode:
321
+ print(f"Real PD code after de_r1 and de_k8: {crossings}")
322
+ if crossings == []:
323
+ return UNKNOT_RESULT
324
+ return _run_cppkh(_format_pd(crossings), encoding=encoding, threads=threads)
325
+
326
+
327
+ def simplify_pd(pd_code: PdInput, *, de_r1: bool = True, de_k8: bool = True) -> str:
328
+ """Return the normalized PD string after optional R1 and nugatory simplification."""
329
+
330
+ return _format_pd(_prepare_crossings(pd_code, de_r1=de_r1, de_k8=de_k8))
331
+
332
+
333
+ def main(argv: Optional[Sequence[str]] = None) -> int:
334
+ parser = argparse.ArgumentParser(description="Compute Khovanov homology with cppkh-interface.")
335
+ parser.add_argument("pd_code", help="PD code as PD[...] text or a Python-style list of crossings.")
336
+ parser.add_argument("--no-de-r1", action="store_true", help="Disable R1-move removal.")
337
+ parser.add_argument("--no-de-k8", action="store_true", help="Disable nugatory-crossing removal.")
338
+ parser.add_argument("--threads", default="1", help="cppkh --threads value for direct compute_pd mode.")
339
+ args = parser.parse_args(argv)
340
+ print(
341
+ compute_pd(
342
+ args.pd_code,
343
+ de_r1=not args.no_de_r1,
344
+ de_k8=not args.no_de_k8,
345
+ threads=args.threads,
346
+ )
347
+ )
348
+ return 0
349
+
350
+
351
+ if __name__ == "__main__":
352
+ raise SystemExit(main())
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,76 @@
1
+ Metadata-Version: 2.4
2
+ Name: cppkh-interface
3
+ Version: 0.1.0
4
+ Summary: Python interface for cppkh Khovanov homology computation with runtime C++ compilation.
5
+ License-Expression: MIT
6
+ Author: GGN_2015
7
+ Author-email: neko@jlulug.org
8
+ Requires-Python: >=3.10
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Requires-Dist: cpp-simple-interface (>=0.1.2)
16
+ Requires-Dist: pd-code-de-r1
17
+ Requires-Dist: pd-code-delete-nugatory
18
+ Requires-Dist: pd-code-sanity (>=0.0.1)
19
+ Project-URL: Documentation, https://github.com/GGN-2015/cppkh/blob/main/docs/PYTHON_PACKAGE.md
20
+ Project-URL: Homepage, https://github.com/GGN-2015/cppkh
21
+ Project-URL: Repository, https://github.com/GGN-2015/cppkh
22
+ Description-Content-Type: text/markdown
23
+
24
+ # cppkh-interface
25
+
26
+ `cppkh-interface` is a Python package for computing integer Khovanov homology
27
+ with the C++ `cppkh` implementation.
28
+
29
+ The package is compatible with the main `javakh-interface` function:
30
+
31
+ ```python
32
+ import cppkh_interface
33
+
34
+ pd_code = [[1, 5, 2, 4], [3, 1, 4, 6], [5, 3, 6, 2]]
35
+ print(cppkh_interface.solve_khovanov(pd_code, de_r1=True, de_k8=True))
36
+ ```
37
+
38
+ 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.
41
+
42
+ ## Install
43
+
44
+ ```sh
45
+ pip install cppkh-interface
46
+ ```
47
+
48
+ A `g++` compatible compiler must be available at runtime. To select a compiler,
49
+ set `CXX` before importing or calling the package:
50
+
51
+ ```sh
52
+ CXX=clang++ python your_script.py
53
+ ```
54
+
55
+ Windows PowerShell:
56
+
57
+ ```powershell
58
+ $env:CXX = "C:\path\to\g++.exe"
59
+ python your_script.py
60
+ ```
61
+
62
+ ## Build And Publish
63
+
64
+ From this directory:
65
+
66
+ ```sh
67
+ poetry build
68
+ poetry publish
69
+ ```
70
+
71
+ For local testing:
72
+
73
+ ```sh
74
+ poetry run python -m cppkh_interface "[[1,5,2,4], [3,1,4,6], [5,3,6,2]]"
75
+ ```
76
+
@@ -0,0 +1,9 @@
1
+ cppkh_interface/__init__.py,sha256=WJNYCnxkp3e-ebXMgoo20S5N3eJxJGlkqbiW844M0VQ,343
2
+ cppkh_interface/__main__.py,sha256=fdA1QWBe5LNX0fbtaJXa1-dwEhrg_4sS_5UnjaajcMo,80
3
+ cppkh_interface/data/src/main.cpp,sha256=T56Ftt152CN5a23juzwSAOXnCnLhdeL-0Wgj7Cbo3dw,141886
4
+ cppkh_interface/main.py,sha256=WOwZd9owRN5K8wall_PW7JyRP0FYITBfOiHUrBwvsxY,11077
5
+ cppkh_interface/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
6
+ cppkh_interface-0.1.0.dist-info/entry_points.txt,sha256=CsnHBNaO_RJe4eFXmEZpSxWu-c1aF0RpjVFaEGlIm4Y,61
7
+ cppkh_interface-0.1.0.dist-info/METADATA,sha256=bmeNcT8rRTjyDrODAeurHKEMq6tZR-bqcEn6WBQTcAQ,2071
8
+ cppkh_interface-0.1.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
9
+ cppkh_interface-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.2.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ cppkh-interface=cppkh_interface.main:main
3
+