cppkh-interface 0.1.2__py3-none-any.whl → 0.2.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.
- cppkh_interface/__init__.py +29 -25
- cppkh_interface/__main__.py +4 -4
- cppkh_interface/main.py +646 -509
- cppkh_interface/py.typed +1 -1
- {cppkh_interface-0.1.2.dist-info → cppkh_interface-0.2.0.dist-info}/METADATA +18 -19
- cppkh_interface-0.2.0.dist-info/RECORD +8 -0
- {cppkh_interface-0.1.2.dist-info → cppkh_interface-0.2.0.dist-info}/WHEEL +1 -1
- cppkh_interface/data/src/main.cpp +0 -3639
- cppkh_interface-0.1.2.dist-info/RECORD +0 -9
- {cppkh_interface-0.1.2.dist-info → cppkh_interface-0.2.0.dist-info}/entry_points.txt +0 -0
cppkh_interface/main.py
CHANGED
|
@@ -1,519 +1,656 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import argparse
|
|
4
|
-
import ast
|
|
5
|
-
import contextlib
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
import
|
|
13
|
-
import
|
|
14
|
-
import
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
import
|
|
19
|
-
import
|
|
20
|
-
import
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
crossings
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return crossings if crossings else None
|
|
50
|
-
|
|
51
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import ast
|
|
5
|
+
import contextlib
|
|
6
|
+
import ctypes
|
|
7
|
+
import hashlib
|
|
8
|
+
import os
|
|
9
|
+
import pathlib
|
|
10
|
+
import platform
|
|
11
|
+
import re
|
|
12
|
+
import shlex
|
|
13
|
+
import shutil
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
import tempfile
|
|
17
|
+
import uuid
|
|
18
|
+
import threading
|
|
19
|
+
from importlib import resources
|
|
20
|
+
from typing import Optional, Sequence, Union
|
|
21
|
+
|
|
22
|
+
PdInput = Union[str, Sequence[Sequence[int]]]
|
|
23
|
+
PdManyInput = Union[str, Sequence[PdInput]]
|
|
24
|
+
_configured_cxx: Optional[str] = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class CppkhInterfaceError(RuntimeError):
|
|
28
|
+
"""Raised when the C++ executable cannot be built or run."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _format_pd(crossings: Sequence[Sequence[int]]) -> str:
|
|
32
|
+
parts = []
|
|
33
|
+
for crossing in crossings:
|
|
34
|
+
values = list(crossing)
|
|
35
|
+
if len(values) != 4:
|
|
36
|
+
raise ValueError(f"PD crossing must have four entries: {crossing!r}")
|
|
37
|
+
parts.append("X[{},{},{},{}]".format(*(int(value) for value in values)))
|
|
38
|
+
return "PD[" + ",".join(parts) + "]"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _parse_x_crossings(text: str) -> Optional[list[list[int]]]:
|
|
42
|
+
pattern = r"X\s*\[\s*(-?\d+)\s*,\s*(-?\d+)\s*,\s*(-?\d+)\s*,\s*(-?\d+)\s*\]"
|
|
43
|
+
crossings = []
|
|
44
|
+
for match in re.finditer(pattern, text):
|
|
45
|
+
crossings.append([int(match.group(i)) for i in range(1, 5)])
|
|
46
|
+
return crossings if crossings else None
|
|
47
|
+
|
|
48
|
+
|
|
52
49
|
def _as_crossings(pd_code: PdInput) -> list[list[int]]:
|
|
53
|
-
if isinstance(pd_code, str):
|
|
54
|
-
body = pd_code.strip()
|
|
55
|
-
if ":" in body:
|
|
56
|
-
body = body.split(":", 1)[1].strip()
|
|
57
|
-
if body.replace(" ", "") in ("PD[]", "[]"):
|
|
58
|
-
return []
|
|
59
|
-
|
|
60
|
-
parsed = _parse_x_crossings(body)
|
|
61
|
-
if parsed is not None:
|
|
62
|
-
return parsed
|
|
63
|
-
|
|
64
|
-
try:
|
|
65
|
-
value = ast.literal_eval(body)
|
|
66
|
-
except (SyntaxError, ValueError) as exc:
|
|
67
|
-
raise ValueError(f"unsupported PD-code string format: {pd_code!r}") from exc
|
|
68
|
-
else:
|
|
69
|
-
value = pd_code
|
|
70
|
-
|
|
71
|
-
crossings = []
|
|
72
|
-
for crossing in value:
|
|
73
|
-
values = list(crossing)
|
|
74
|
-
if len(values) != 4:
|
|
75
|
-
raise ValueError(f"PD crossing must have four entries: {crossing!r}")
|
|
76
|
-
crossings.append([int(item) for item in values])
|
|
50
|
+
if isinstance(pd_code, str):
|
|
51
|
+
body = pd_code.strip()
|
|
52
|
+
if ":" in body:
|
|
53
|
+
body = body.split(":", 1)[1].strip()
|
|
54
|
+
if body.replace(" ", "") in ("PD[]", "[]"):
|
|
55
|
+
return []
|
|
56
|
+
|
|
57
|
+
parsed = _parse_x_crossings(body)
|
|
58
|
+
if parsed is not None:
|
|
59
|
+
return parsed
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
value = ast.literal_eval(body)
|
|
63
|
+
except (SyntaxError, ValueError) as exc:
|
|
64
|
+
raise ValueError(f"unsupported PD-code string format: {pd_code!r}") from exc
|
|
65
|
+
else:
|
|
66
|
+
value = pd_code
|
|
67
|
+
|
|
68
|
+
crossings = []
|
|
69
|
+
for crossing in value:
|
|
70
|
+
values = list(crossing)
|
|
71
|
+
if len(values) != 4:
|
|
72
|
+
raise ValueError(f"PD crossing must have four entries: {crossing!r}")
|
|
73
|
+
crossings.append([int(item) for item in values])
|
|
77
74
|
return crossings
|
|
78
75
|
|
|
79
76
|
|
|
80
77
|
def _check_sanity(crossings: list[list[int]]) -> None:
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
"
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
root = pathlib.Path(
|
|
132
|
-
elif sys.platform == "
|
|
133
|
-
root = pathlib.Path.
|
|
134
|
-
|
|
135
|
-
root = pathlib.Path
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if
|
|
146
|
-
flags.append("-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
78
|
+
counts = {}
|
|
79
|
+
for crossing in crossings:
|
|
80
|
+
for label in crossing:
|
|
81
|
+
counts[label] = counts.get(label, 0) + 1
|
|
82
|
+
if any(count != 2 for count in counts.values()):
|
|
83
|
+
raise TypeError("each PD label must occur exactly twice")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def normalize_pd_code(pd_code: PdInput) -> str:
|
|
87
|
+
"""Normalize a supported PD-code value into standard ``PD[X[...],...]`` text."""
|
|
88
|
+
|
|
89
|
+
return _format_pd(_as_crossings(pd_code))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def normalize_pd_codes(pd_codes: PdManyInput) -> str:
|
|
93
|
+
"""Normalize one or more PD codes into a newline-separated PD document."""
|
|
94
|
+
|
|
95
|
+
if isinstance(pd_codes, str):
|
|
96
|
+
return pd_codes.strip()
|
|
97
|
+
return "\n".join(normalize_pd_code(pd_code) for pd_code in pd_codes)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@contextlib.contextmanager
|
|
101
|
+
def _resource_source_path():
|
|
102
|
+
source = resources.files("cppkh_interface") / "data" / "src" / "main.cpp"
|
|
103
|
+
try:
|
|
104
|
+
with resources.as_file(source) as resource_path:
|
|
105
|
+
if pathlib.Path(resource_path).exists():
|
|
106
|
+
yield pathlib.Path(resource_path)
|
|
107
|
+
return
|
|
108
|
+
except FileNotFoundError:
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
current = pathlib.Path(__file__).resolve()
|
|
112
|
+
for parent in current.parents:
|
|
113
|
+
candidate = parent / "src" / "main.cpp"
|
|
114
|
+
if candidate.exists():
|
|
115
|
+
yield candidate
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
raise CppkhInterfaceError(
|
|
119
|
+
"cppkh C++ source was not found. Installed wheels include it under "
|
|
120
|
+
"cppkh_interface/data/src/main.cpp; editable checkouts use the "
|
|
121
|
+
"repository root src/main.cpp."
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _cache_dir() -> pathlib.Path:
|
|
126
|
+
env_value = os.environ.get("CPPKH_INTERFACE_CACHE_DIR")
|
|
127
|
+
if env_value:
|
|
128
|
+
root = pathlib.Path(env_value)
|
|
129
|
+
elif sys.platform == "win32":
|
|
130
|
+
root = pathlib.Path(os.environ.get("LOCALAPPDATA", pathlib.Path.home())) / "cppkh-interface"
|
|
131
|
+
elif sys.platform == "darwin":
|
|
132
|
+
root = pathlib.Path.home() / "Library" / "Caches" / "cppkh-interface"
|
|
133
|
+
else:
|
|
134
|
+
root = pathlib.Path(os.environ.get("XDG_CACHE_HOME", pathlib.Path.home() / ".cache")) / "cppkh-interface"
|
|
135
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
136
|
+
return root
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _default_compile_flags() -> list[str]:
|
|
140
|
+
flags = ["-std=c++14", "-O3", "-DNDEBUG"]
|
|
141
|
+
native = os.environ.get("CPPKH_INTERFACE_NATIVE", "1").strip().lower()
|
|
142
|
+
if native not in ("0", "false", "no", "off"):
|
|
143
|
+
flags.append("-march=native")
|
|
144
|
+
if platform.system() != "Windows":
|
|
145
|
+
flags.append("-pthread")
|
|
146
|
+
extra = os.environ.get("CPPKH_INTERFACE_CXXFLAGS", "").strip()
|
|
147
|
+
if extra:
|
|
148
|
+
flags.extend(shlex.split(extra))
|
|
149
|
+
return flags
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _exe_suffix() -> str:
|
|
153
|
+
return ".exe" if platform.system() == "Windows" else ""
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _shared_suffix() -> str:
|
|
157
|
+
return ".dll" if platform.system() == "Windows" else (".dylib" if platform.system() == "Darwin" else ".so")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _compiler_parts(command: str) -> list[str]:
|
|
161
|
+
command = command.strip()
|
|
162
|
+
if not command:
|
|
160
163
|
return []
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
164
|
+
unquoted = command
|
|
165
|
+
if len(unquoted) >= 2 and unquoted[0] == unquoted[-1] and unquoted[0] in ("'", '"'):
|
|
166
|
+
unquoted = unquoted[1:-1]
|
|
167
|
+
if pathlib.Path(unquoted).is_file():
|
|
168
|
+
return [unquoted]
|
|
169
|
+
try:
|
|
170
|
+
parts = shlex.split(command, posix=os.name != "nt")
|
|
171
|
+
except ValueError as exc:
|
|
172
|
+
raise CppkhInterfaceError(f"invalid C++ compiler command: {command!r}") from exc
|
|
173
|
+
return [part.strip("\"'") for part in parts if part.strip("\"'")]
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _subprocess_kwargs() -> dict:
|
|
177
|
+
kwargs = {
|
|
178
|
+
"stdout": subprocess.PIPE,
|
|
179
|
+
"stderr": subprocess.PIPE,
|
|
180
|
+
"text": True,
|
|
181
|
+
"encoding": "utf-8",
|
|
182
|
+
"errors": "replace",
|
|
183
|
+
}
|
|
184
|
+
if platform.system() == "Windows":
|
|
185
|
+
kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
|
|
186
|
+
return kwargs
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _probe_compiler(parts: Sequence[str]) -> subprocess.CompletedProcess:
|
|
190
|
+
if not parts:
|
|
191
|
+
raise CppkhInterfaceError("empty C++ compiler command")
|
|
192
|
+
try:
|
|
193
|
+
result = subprocess.run([*parts, "--version"], timeout=15, **_subprocess_kwargs())
|
|
194
|
+
except (OSError, subprocess.SubprocessError) as exc:
|
|
195
|
+
raise CppkhInterfaceError(f"could not run C++ compiler {' '.join(parts)!r}: {exc}") from exc
|
|
196
|
+
if result.returncode != 0:
|
|
197
|
+
detail = (result.stderr or result.stdout or "").strip()
|
|
198
|
+
raise CppkhInterfaceError(detail or f"C++ compiler {' '.join(parts)!r} is not usable")
|
|
199
|
+
return result
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _resolve_compiler(cxx: Optional[str] = None) -> list[str]:
|
|
203
|
+
global _configured_cxx
|
|
204
|
+
|
|
205
|
+
explicit = cxx or _configured_cxx
|
|
206
|
+
if not explicit:
|
|
207
|
+
explicit = os.environ.get("CPPKH_INTERFACE_CXX") or os.environ.get("CXX")
|
|
208
|
+
if explicit:
|
|
209
|
+
parts = _compiler_parts(explicit)
|
|
210
|
+
_probe_compiler(parts)
|
|
211
|
+
if cxx:
|
|
212
|
+
_configured_cxx = cxx
|
|
213
|
+
return parts
|
|
214
|
+
|
|
215
|
+
errors = []
|
|
216
|
+
for name in ("g++", "clang++", "c++"):
|
|
217
|
+
candidate = shutil.which(name)
|
|
218
|
+
if not candidate:
|
|
219
|
+
continue
|
|
220
|
+
parts = [candidate]
|
|
221
|
+
try:
|
|
222
|
+
_probe_compiler(parts)
|
|
223
|
+
return parts
|
|
224
|
+
except CppkhInterfaceError as exc:
|
|
225
|
+
errors.append(str(exc))
|
|
226
|
+
detail = f" ({'; '.join(errors)})" if errors else ""
|
|
227
|
+
raise CppkhInterfaceError(
|
|
228
|
+
"no usable C++ compiler was found. Install a C++14 compiler or set "
|
|
229
|
+
f"CPPKH_INTERFACE_CXX/CXX to its command{detail}"
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _compiler_runtime_path_entries(compiler: Sequence[str]) -> list[str]:
|
|
173
234
|
paths = []
|
|
174
|
-
for candidate in
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
235
|
+
for candidate in compiler:
|
|
236
|
+
resolved = shutil.which(candidate) or candidate
|
|
237
|
+
path = pathlib.Path(resolved)
|
|
238
|
+
if path.exists() and path.is_file():
|
|
239
|
+
parent = str(path.resolve().parent)
|
|
240
|
+
if parent not in paths:
|
|
241
|
+
paths.append(parent)
|
|
242
|
+
return paths
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _compiler_identity(compiler: Sequence[str]) -> str:
|
|
246
|
+
result = _probe_compiler(compiler)
|
|
247
|
+
executable = shutil.which(compiler[0]) or compiler[0]
|
|
248
|
+
return "\0".join([str(pathlib.Path(executable).resolve()), *compiler[1:], result.stdout.strip()])
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _cache_key(source_bytes: bytes, flags: Sequence[str], compiler: Sequence[str]) -> str:
|
|
252
|
+
digest = hashlib.sha256()
|
|
253
|
+
digest.update(source_bytes)
|
|
254
|
+
digest.update("\0".join(flags).encode("utf-8"))
|
|
255
|
+
digest.update(_compiler_identity(compiler).encode("utf-8"))
|
|
256
|
+
digest.update(platform.platform().encode("utf-8"))
|
|
257
|
+
return digest.hexdigest()[:20]
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _compile_source(
|
|
261
|
+
compiler: Sequence[str],
|
|
262
|
+
source: pathlib.Path,
|
|
263
|
+
output: pathlib.Path,
|
|
264
|
+
flags: Sequence[str],
|
|
265
|
+
) -> subprocess.CompletedProcess:
|
|
266
|
+
command = [*compiler, *flags, str(source), "-o", str(output)]
|
|
267
|
+
try:
|
|
268
|
+
return subprocess.run(command, **_subprocess_kwargs())
|
|
269
|
+
except OSError as exc:
|
|
270
|
+
raise CppkhInterfaceError(f"could not start C++ compiler: {exc}") from exc
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def compile_cppkh(
|
|
274
|
+
*,
|
|
275
|
+
force: bool = False,
|
|
276
|
+
cxx: Optional[str] = None,
|
|
277
|
+
extra_flags: Optional[Sequence[str]] = None,
|
|
278
|
+
) -> pathlib.Path:
|
|
279
|
+
"""Compile the packaged C++ source and return the cached executable path."""
|
|
280
|
+
|
|
281
|
+
with _resource_source_path() as source:
|
|
282
|
+
source_path = pathlib.Path(source)
|
|
283
|
+
source_bytes = source_path.read_bytes()
|
|
284
|
+
compiler = _resolve_compiler(cxx)
|
|
285
|
+
flags = _default_compile_flags()
|
|
286
|
+
if extra_flags:
|
|
287
|
+
flags.extend(str(flag) for flag in extra_flags)
|
|
288
|
+
|
|
289
|
+
cache = _cache_dir()
|
|
290
|
+
exe = cache / f"cppkh-{_cache_key(source_bytes, flags, compiler)}{_exe_suffix()}"
|
|
291
|
+
if exe.exists() and not force:
|
|
292
|
+
return exe
|
|
293
|
+
|
|
294
|
+
tmp_exe = cache / f"{exe.name}.tmp-{os.getpid()}-{uuid.uuid4().hex}{_exe_suffix()}"
|
|
295
|
+
try:
|
|
296
|
+
result = _compile_source(compiler, source_path, tmp_exe, flags)
|
|
297
|
+
if result.returncode != 0 and "-march=native" in flags:
|
|
298
|
+
fallback_flags = [flag for flag in flags if flag != "-march=native"]
|
|
299
|
+
result = _compile_source(compiler, source_path, tmp_exe, fallback_flags)
|
|
300
|
+
if result.returncode != 0:
|
|
301
|
+
detail = (result.stderr or result.stdout or "").strip()
|
|
302
|
+
raise CppkhInterfaceError(detail or "C++ compilation failed")
|
|
303
|
+
if not tmp_exe.exists():
|
|
304
|
+
raise CppkhInterfaceError(f"compiled executable was not created: {tmp_exe}")
|
|
305
|
+
|
|
306
|
+
if exe.exists() and not force:
|
|
307
|
+
return exe
|
|
308
|
+
try:
|
|
309
|
+
os.replace(tmp_exe, exe)
|
|
310
|
+
except OSError:
|
|
311
|
+
if force or not exe.exists():
|
|
312
|
+
raise
|
|
313
|
+
try:
|
|
314
|
+
exe.chmod(exe.stat().st_mode | 0o755)
|
|
315
|
+
except OSError:
|
|
316
|
+
pass
|
|
317
|
+
return exe
|
|
318
|
+
finally:
|
|
319
|
+
try:
|
|
320
|
+
tmp_exe.unlink()
|
|
321
|
+
except OSError:
|
|
322
|
+
pass
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def compile_cppkh_shared(*, force: bool = False) -> pathlib.Path:
|
|
326
|
+
"""Compile and cache the cppkh C API shared library."""
|
|
327
|
+
with _resource_source_path() as source:
|
|
204
328
|
source_path = pathlib.Path(source)
|
|
205
|
-
|
|
206
|
-
flags = _default_compile_flags()
|
|
207
|
-
if extra_flags:
|
|
208
|
-
flags.extend(str(flag) for flag in extra_flags)
|
|
209
|
-
|
|
329
|
+
compiler = _resolve_compiler()
|
|
330
|
+
flags = _default_compile_flags() + ["-shared", "-DCPPKH_SHARED_LIBRARY"]
|
|
210
331
|
cache = _cache_dir()
|
|
211
|
-
|
|
212
|
-
if
|
|
213
|
-
return
|
|
214
|
-
|
|
215
|
-
tmp_exe = cache / f"{exe.name}.tmp-{os.getpid()}{_exe_suffix()}"
|
|
216
|
-
if tmp_exe.exists():
|
|
217
|
-
tmp_exe.unlink()
|
|
218
|
-
|
|
219
|
-
success, message = cpp_simple_interface.compile_cpp_files(
|
|
220
|
-
[str(source_path)],
|
|
221
|
-
str(tmp_exe),
|
|
222
|
-
other_flags=flags,
|
|
223
|
-
)
|
|
224
|
-
if not success and "-march=native" in flags:
|
|
225
|
-
fallback_flags = [flag for flag in flags if flag != "-march=native"]
|
|
226
|
-
success, message = cpp_simple_interface.compile_cpp_files(
|
|
227
|
-
[str(source_path)],
|
|
228
|
-
str(tmp_exe),
|
|
229
|
-
other_flags=fallback_flags,
|
|
230
|
-
)
|
|
231
|
-
|
|
232
|
-
if not success:
|
|
233
|
-
raise CppkhInterfaceError(message)
|
|
234
|
-
if not tmp_exe.exists():
|
|
235
|
-
raise CppkhInterfaceError(f"compiled executable was not created: {tmp_exe}")
|
|
236
|
-
os.replace(tmp_exe, exe)
|
|
237
|
-
try:
|
|
238
|
-
exe.chmod(exe.stat().st_mode | 0o755)
|
|
239
|
-
except OSError:
|
|
240
|
-
pass
|
|
241
|
-
return exe
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
def get_cppkh_executable() -> pathlib.Path:
|
|
245
|
-
"""Return the cached executable path, compiling it first when necessary."""
|
|
246
|
-
|
|
247
|
-
return compile_cppkh()
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
def _run_cppkh_document(
|
|
251
|
-
pd_text: str,
|
|
252
|
-
*,
|
|
253
|
-
encoding: Optional[str] = None,
|
|
254
|
-
threads: Union[str, int] = "1",
|
|
255
|
-
simplify_pd: bool = False,
|
|
256
|
-
print_simplified_pd: bool = False,
|
|
257
|
-
) -> list[str]:
|
|
258
|
-
exe = compile_cppkh()
|
|
259
|
-
with tempfile.NamedTemporaryFile("w", suffix=".pd", encoding="utf-8", delete=False) as handle:
|
|
260
|
-
handle.write(pd_text)
|
|
261
|
-
if pd_text and not pd_text.endswith("\n"):
|
|
262
|
-
handle.write("\n")
|
|
263
|
-
pd_file = handle.name
|
|
264
|
-
|
|
265
|
-
command = [
|
|
266
|
-
str(exe),
|
|
267
|
-
"--pd-file",
|
|
268
|
-
pd_file,
|
|
269
|
-
"--quiet",
|
|
270
|
-
"--threads",
|
|
271
|
-
str(threads),
|
|
272
|
-
]
|
|
273
|
-
if not simplify_pd:
|
|
274
|
-
command.append("--no-simplify-pd")
|
|
275
|
-
if print_simplified_pd:
|
|
276
|
-
command.append("--print-simplified-pd")
|
|
277
|
-
|
|
278
|
-
kwargs = {
|
|
279
|
-
"stdout": subprocess.PIPE,
|
|
280
|
-
"stderr": subprocess.PIPE,
|
|
281
|
-
"text": True,
|
|
282
|
-
"encoding": encoding or "utf-8",
|
|
283
|
-
"errors": "replace",
|
|
284
|
-
}
|
|
285
|
-
env = os.environ.copy()
|
|
286
|
-
runtime_paths = _compiler_runtime_path_entries()
|
|
287
|
-
if runtime_paths:
|
|
288
|
-
env["PATH"] = os.pathsep.join(runtime_paths + [env.get("PATH", "")])
|
|
289
|
-
kwargs["env"] = env
|
|
290
|
-
if platform.system() == "Windows":
|
|
291
|
-
kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
|
|
292
|
-
|
|
293
|
-
try:
|
|
294
|
-
result = subprocess.run(command, **kwargs)
|
|
295
|
-
finally:
|
|
332
|
+
library = cache / f"cppkh-{_cache_key(source_path.read_bytes(), flags, compiler)}{_shared_suffix()}"
|
|
333
|
+
if library.exists() and not force:
|
|
334
|
+
return library
|
|
335
|
+
temporary = cache / f"{library.name}.tmp-{os.getpid()}-{uuid.uuid4().hex}{_shared_suffix()}"
|
|
296
336
|
try:
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
"""
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
if
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
"""
|
|
496
|
-
|
|
497
|
-
return
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
337
|
+
result = _compile_source(compiler, source_path, temporary, flags)
|
|
338
|
+
if result.returncode != 0 and "-march=native" in flags:
|
|
339
|
+
result = _compile_source(compiler, source_path, temporary, [flag for flag in flags if flag != "-march=native"])
|
|
340
|
+
if result.returncode != 0:
|
|
341
|
+
raise CppkhInterfaceError((result.stderr or result.stdout or "C++ shared compilation failed").strip())
|
|
342
|
+
os.replace(temporary, library)
|
|
343
|
+
return library
|
|
344
|
+
finally:
|
|
345
|
+
try:
|
|
346
|
+
temporary.unlink()
|
|
347
|
+
except OSError:
|
|
348
|
+
pass
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
_shared_lock = threading.Lock()
|
|
352
|
+
_shared_library = None
|
|
353
|
+
_dll_directory_handles = []
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _load_shared_library():
|
|
357
|
+
global _shared_library
|
|
358
|
+
if _shared_library is not None:
|
|
359
|
+
return _shared_library
|
|
360
|
+
runtime_paths = _compiler_runtime_path_entries(_resolve_compiler())
|
|
361
|
+
if runtime_paths:
|
|
362
|
+
os.environ["PATH"] = os.pathsep.join(runtime_paths + [os.environ.get("PATH", "")])
|
|
363
|
+
library_path = compile_cppkh_shared()
|
|
364
|
+
if platform.system() == "Windows" and hasattr(os, "add_dll_directory"):
|
|
365
|
+
for directory in [str(library_path.parent), *runtime_paths]:
|
|
366
|
+
_dll_directory_handles.append(os.add_dll_directory(directory))
|
|
367
|
+
library = ctypes.CDLL(str(library_path))
|
|
368
|
+
library.cppkh_compute_pd_signed_variants_ex.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_int]
|
|
369
|
+
library.cppkh_compute_pd_signed_variants_ex.restype = ctypes.c_void_p
|
|
370
|
+
library.cppkh_last_error.restype = ctypes.c_char_p
|
|
371
|
+
library.cppkh_free.argtypes = [ctypes.c_void_p]
|
|
372
|
+
_shared_library = library
|
|
373
|
+
return library
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def compute_signed_variants(pd_code: PdInput, signs: Sequence[Sequence[int]]) -> list[str]:
|
|
377
|
+
"""Compute several explicit crossing-sign variants in one native call."""
|
|
378
|
+
crossings = _as_crossings(pd_code)
|
|
379
|
+
_check_sanity(crossings)
|
|
380
|
+
rows = [list(row) for row in signs]
|
|
381
|
+
if any(len(row) != len(crossings) or any(sign not in (-1, 1) for sign in row) for row in rows):
|
|
382
|
+
raise ValueError("each sign row must contain one +1/-1 value per crossing")
|
|
383
|
+
pd_text = _format_pd(crossings).encode("utf-8")
|
|
384
|
+
signs_text = "\n".join(" ".join(map(str, row)) for row in rows).encode("ascii")
|
|
385
|
+
with _shared_lock:
|
|
386
|
+
library = _load_shared_library()
|
|
387
|
+
pointer = library.cppkh_compute_pd_signed_variants_ex(pd_text, signs_text, 1)
|
|
388
|
+
if not pointer:
|
|
389
|
+
error = library.cppkh_last_error()
|
|
390
|
+
raise CppkhInterfaceError(error.decode("utf-8", "replace") if error else "signed computation failed")
|
|
391
|
+
try:
|
|
392
|
+
result = ctypes.string_at(pointer).decode("utf-8")
|
|
393
|
+
finally:
|
|
394
|
+
library.cppkh_free(pointer)
|
|
395
|
+
return result.splitlines()
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def get_cppkh_executable() -> pathlib.Path:
|
|
399
|
+
"""Return the cached executable path, compiling it first when necessary."""
|
|
400
|
+
|
|
401
|
+
return compile_cppkh()
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _run_cppkh_document(
|
|
405
|
+
pd_text: str,
|
|
406
|
+
*,
|
|
407
|
+
encoding: Optional[str] = None,
|
|
408
|
+
threads: Union[str, int] = "1",
|
|
409
|
+
de_r1: bool = False,
|
|
410
|
+
de_k8: bool = False,
|
|
411
|
+
print_simplified_pd: bool = False,
|
|
412
|
+
) -> list[str]:
|
|
413
|
+
exe = compile_cppkh()
|
|
414
|
+
with tempfile.NamedTemporaryFile("w", suffix=".pd", encoding="utf-8", delete=False) as handle:
|
|
415
|
+
handle.write(pd_text)
|
|
416
|
+
if pd_text and not pd_text.endswith("\n"):
|
|
417
|
+
handle.write("\n")
|
|
418
|
+
pd_file = handle.name
|
|
419
|
+
|
|
420
|
+
command = [
|
|
421
|
+
str(exe),
|
|
422
|
+
"--pd-file",
|
|
423
|
+
pd_file,
|
|
424
|
+
"--quiet",
|
|
425
|
+
"--threads",
|
|
426
|
+
str(threads),
|
|
427
|
+
]
|
|
428
|
+
command.append("--simplify-r1" if de_r1 else "--no-simplify-r1")
|
|
429
|
+
command.append("--simplify-nugatory" if de_k8 else "--no-simplify-nugatory")
|
|
430
|
+
if print_simplified_pd:
|
|
431
|
+
command.append("--print-simplified-pd")
|
|
432
|
+
|
|
433
|
+
kwargs = {
|
|
434
|
+
"stdout": subprocess.PIPE,
|
|
435
|
+
"stderr": subprocess.PIPE,
|
|
436
|
+
"text": True,
|
|
437
|
+
"encoding": encoding or "utf-8",
|
|
438
|
+
"errors": "replace",
|
|
439
|
+
}
|
|
440
|
+
env = os.environ.copy()
|
|
441
|
+
runtime_paths = _compiler_runtime_path_entries(_resolve_compiler())
|
|
442
|
+
if runtime_paths:
|
|
443
|
+
env["PATH"] = os.pathsep.join(runtime_paths + [env.get("PATH", "")])
|
|
444
|
+
kwargs["env"] = env
|
|
445
|
+
if platform.system() == "Windows":
|
|
446
|
+
kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
|
|
447
|
+
|
|
448
|
+
try:
|
|
449
|
+
result = subprocess.run(command, **kwargs)
|
|
450
|
+
finally:
|
|
451
|
+
try:
|
|
452
|
+
os.unlink(pd_file)
|
|
453
|
+
except OSError:
|
|
454
|
+
pass
|
|
455
|
+
|
|
456
|
+
if result.returncode != 0:
|
|
457
|
+
detail = (result.stderr or result.stdout or "").strip()
|
|
458
|
+
raise CppkhInterfaceError(detail or f"cppkh exited with code {result.returncode}")
|
|
459
|
+
|
|
460
|
+
if print_simplified_pd:
|
|
461
|
+
lines = [line.strip() for line in result.stdout.splitlines() if line.strip()]
|
|
462
|
+
return [line.split("\t", 1)[-1] for line in lines]
|
|
463
|
+
|
|
464
|
+
matches = re.findall(r'"([^"]*)"', result.stdout)
|
|
465
|
+
if not matches:
|
|
466
|
+
raise CppkhInterfaceError(f"result not found in cppkh output: {result.stdout!r}")
|
|
467
|
+
return matches
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def _run_cppkh(
|
|
471
|
+
pd_text: str,
|
|
472
|
+
*,
|
|
473
|
+
encoding: Optional[str] = None,
|
|
474
|
+
threads: Union[str, int] = "1",
|
|
475
|
+
de_r1: bool = False,
|
|
476
|
+
de_k8: bool = False,
|
|
477
|
+
print_simplified_pd: bool = False,
|
|
478
|
+
) -> str:
|
|
479
|
+
results = _run_cppkh_document(
|
|
480
|
+
pd_text,
|
|
481
|
+
encoding=encoding,
|
|
482
|
+
threads=threads,
|
|
483
|
+
de_r1=de_r1,
|
|
484
|
+
de_k8=de_k8,
|
|
485
|
+
print_simplified_pd=print_simplified_pd,
|
|
486
|
+
)
|
|
487
|
+
if len(results) != 1:
|
|
488
|
+
raise CppkhInterfaceError(f"expected exactly one result, got {len(results)}")
|
|
489
|
+
return results[0]
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def _prepare_many_for_cppkh(pd_codes: PdManyInput) -> str:
|
|
493
|
+
if isinstance(pd_codes, str):
|
|
494
|
+
return pd_codes.strip()
|
|
495
|
+
return "\n".join(normalize_pd_code(pd_code) for pd_code in pd_codes)
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def _compute_one(
|
|
499
|
+
pd_code: PdInput,
|
|
500
|
+
*,
|
|
501
|
+
encoding: Optional[str],
|
|
502
|
+
de_r1: bool,
|
|
503
|
+
de_k8: bool,
|
|
504
|
+
show_real_pdcode: bool,
|
|
505
|
+
threads: Union[str, int],
|
|
506
|
+
) -> str:
|
|
507
|
+
document = normalize_pd_code(pd_code)
|
|
508
|
+
if show_real_pdcode:
|
|
509
|
+
real_pd = _run_cppkh(
|
|
510
|
+
document,
|
|
511
|
+
encoding=encoding,
|
|
512
|
+
threads=threads,
|
|
513
|
+
de_r1=de_r1,
|
|
514
|
+
de_k8=de_k8,
|
|
515
|
+
print_simplified_pd=True,
|
|
516
|
+
)
|
|
517
|
+
display_pd = real_pd if de_r1 and de_k8 else _as_crossings(real_pd)
|
|
518
|
+
print(f"Real PD code after de_r1 and de_k8: {display_pd}")
|
|
519
|
+
return _run_cppkh(
|
|
520
|
+
document,
|
|
521
|
+
encoding=encoding,
|
|
522
|
+
threads=threads,
|
|
523
|
+
de_r1=de_r1,
|
|
524
|
+
de_k8=de_k8,
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def solve_khovanov(
|
|
529
|
+
pd_code: PdInput,
|
|
530
|
+
encoding: Optional[str] = None,
|
|
531
|
+
de_r1: bool = True,
|
|
532
|
+
de_k8: bool = True,
|
|
533
|
+
show_real_pdcode: bool = False,
|
|
534
|
+
) -> str:
|
|
535
|
+
"""Compute Khovanov homology with a javakh-interface compatible signature."""
|
|
536
|
+
|
|
537
|
+
return _compute_one(
|
|
538
|
+
pd_code,
|
|
539
|
+
encoding=encoding,
|
|
540
|
+
de_r1=de_r1,
|
|
541
|
+
de_k8=de_k8,
|
|
542
|
+
show_real_pdcode=show_real_pdcode,
|
|
543
|
+
threads="1",
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def solve_many_khovanov(
|
|
548
|
+
pd_codes: PdManyInput,
|
|
549
|
+
encoding: Optional[str] = None,
|
|
550
|
+
de_r1: bool = True,
|
|
551
|
+
de_k8: bool = True,
|
|
552
|
+
show_real_pdcode: bool = False,
|
|
553
|
+
threads: Union[str, int] = "1",
|
|
554
|
+
) -> list[str]:
|
|
555
|
+
"""Compute many PD codes in one cppkh process.
|
|
556
|
+
|
|
557
|
+
With the default ``de_r1=True`` and ``de_k8=True`` settings, the raw PD
|
|
558
|
+
document is passed directly to cppkh and the C++ simplifier handles R1 then
|
|
559
|
+
nugatory crossing removal for the whole batch.
|
|
560
|
+
"""
|
|
561
|
+
|
|
562
|
+
document = _prepare_many_for_cppkh(pd_codes)
|
|
563
|
+
if not document:
|
|
564
|
+
return []
|
|
565
|
+
if show_real_pdcode:
|
|
566
|
+
simplified = _run_cppkh_document(
|
|
567
|
+
document,
|
|
568
|
+
encoding=encoding,
|
|
569
|
+
threads=threads,
|
|
570
|
+
de_r1=de_r1,
|
|
571
|
+
de_k8=de_k8,
|
|
572
|
+
print_simplified_pd=True,
|
|
573
|
+
)
|
|
574
|
+
print(f"Real PD code after de_r1 and de_k8: {simplified}")
|
|
575
|
+
return _run_cppkh_document(
|
|
576
|
+
document,
|
|
577
|
+
encoding=encoding,
|
|
578
|
+
threads=threads,
|
|
579
|
+
de_r1=de_r1,
|
|
580
|
+
de_k8=de_k8,
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def compute_pd(
|
|
585
|
+
pd_code: PdInput,
|
|
586
|
+
*,
|
|
587
|
+
encoding: Optional[str] = None,
|
|
588
|
+
de_r1: bool = True,
|
|
589
|
+
de_k8: bool = True,
|
|
590
|
+
show_real_pdcode: bool = False,
|
|
591
|
+
threads: Union[str, int] = "1",
|
|
592
|
+
) -> str:
|
|
593
|
+
"""Compute Khovanov homology using the same defaults as solve_khovanov."""
|
|
594
|
+
|
|
595
|
+
return _compute_one(
|
|
596
|
+
pd_code,
|
|
597
|
+
encoding=encoding,
|
|
598
|
+
de_r1=de_r1,
|
|
599
|
+
de_k8=de_k8,
|
|
600
|
+
show_real_pdcode=show_real_pdcode,
|
|
601
|
+
threads=threads,
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def compute_many_pd(
|
|
606
|
+
pd_codes: PdManyInput,
|
|
607
|
+
*,
|
|
608
|
+
encoding: Optional[str] = None,
|
|
609
|
+
de_r1: bool = True,
|
|
610
|
+
de_k8: bool = True,
|
|
611
|
+
show_real_pdcode: bool = False,
|
|
612
|
+
threads: Union[str, int] = "1",
|
|
613
|
+
) -> list[str]:
|
|
614
|
+
"""Compute many PD codes in one cached cppkh executable invocation."""
|
|
615
|
+
|
|
616
|
+
return solve_many_khovanov(
|
|
617
|
+
pd_codes,
|
|
618
|
+
encoding=encoding,
|
|
619
|
+
de_r1=de_r1,
|
|
620
|
+
de_k8=de_k8,
|
|
621
|
+
show_real_pdcode=show_real_pdcode,
|
|
622
|
+
threads=threads,
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def simplify_pd(pd_code: PdInput, *, de_r1: bool = True, de_k8: bool = True) -> str:
|
|
627
|
+
"""Return the normalized PD string after optional R1 and nugatory simplification."""
|
|
628
|
+
|
|
629
|
+
return _run_cppkh(
|
|
630
|
+
normalize_pd_code(pd_code),
|
|
631
|
+
de_r1=de_r1,
|
|
632
|
+
de_k8=de_k8,
|
|
633
|
+
print_simplified_pd=True,
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
638
|
+
parser = argparse.ArgumentParser(description="Compute Khovanov homology with cppkh-interface.")
|
|
639
|
+
parser.add_argument("pd_code", help="PD code as PD[...] text or a Python-style list of crossings.")
|
|
640
|
+
parser.add_argument("--no-de-r1", action="store_true", help="Disable R1-move removal.")
|
|
641
|
+
parser.add_argument("--no-de-k8", action="store_true", help="Disable nugatory-crossing removal.")
|
|
642
|
+
parser.add_argument("--threads", default="1", help="cppkh --threads value for direct compute_pd mode.")
|
|
643
|
+
args = parser.parse_args(argv)
|
|
644
|
+
print(
|
|
645
|
+
compute_pd(
|
|
646
|
+
args.pd_code,
|
|
647
|
+
de_r1=not args.no_de_r1,
|
|
648
|
+
de_k8=not args.no_de_k8,
|
|
649
|
+
threads=args.threads,
|
|
650
|
+
)
|
|
651
|
+
)
|
|
652
|
+
return 0
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
if __name__ == "__main__":
|
|
656
|
+
raise SystemExit(main())
|