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/main.py CHANGED
@@ -1,519 +1,656 @@
1
- from __future__ import annotations
2
-
3
- import argparse
4
- import ast
5
- import contextlib
6
- import hashlib
7
- import os
8
- import pathlib
9
- import platform
10
- import re
11
- import shlex
12
- import subprocess
13
- import sys
14
- import tempfile
15
- from importlib import resources
16
- from typing import Optional, Sequence, Union
17
-
18
- import cpp_simple_interface
19
- import pd_code_de_r1
20
- import pd_code_delete_nugatory
21
- import pd_code_sanity
22
-
23
-
24
- PathLike = Union[str, os.PathLike]
25
- PdInput = Union[str, Sequence[Sequence[int]]]
26
- PdManyInput = Union[str, Sequence[PdInput]]
27
- UNKNOT_RESULT = "q^-1*t^0*Z[0] + q^1*t^0*Z[0]"
28
-
29
-
30
- class CppkhInterfaceError(RuntimeError):
31
- """Raised when the C++ executable cannot be built or run."""
32
-
33
-
34
- def _format_pd(crossings: Sequence[Sequence[int]]) -> str:
35
- parts = []
36
- for crossing in crossings:
37
- values = list(crossing)
38
- if len(values) != 4:
39
- raise ValueError(f"PD crossing must have four entries: {crossing!r}")
40
- parts.append("X[{},{},{},{}]".format(*(int(value) for value in values)))
41
- return "PD[" + ",".join(parts) + "]"
42
-
43
-
44
- def _parse_x_crossings(text: str) -> Optional[list[list[int]]]:
45
- pattern = r"X\s*\[\s*(-?\d+)\s*,\s*(-?\d+)\s*,\s*(-?\d+)\s*,\s*(-?\d+)\s*\]"
46
- crossings = []
47
- for match in re.finditer(pattern, text):
48
- crossings.append([int(match.group(i)) for i in range(1, 5)])
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
- if crossings == []:
82
- return
83
- if not pd_code_sanity.sanity(crossings):
84
- raise TypeError("pd_code does not satisfy PD-code sanity checks")
85
-
86
-
87
- def normalize_pd_code(pd_code: PdInput) -> str:
88
- """Normalize a supported PD-code value into standard ``PD[X[...],...]`` text."""
89
-
90
- return _format_pd(_as_crossings(pd_code))
91
-
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
102
- def _resource_source_path():
103
- source = resources.files("cppkh_interface") / "data" / "src" / "main.cpp"
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
- )
124
-
125
-
126
- def _cache_dir() -> pathlib.Path:
127
- env_value = os.environ.get("CPPKH_INTERFACE_CACHE_DIR")
128
- if env_value:
129
- root = pathlib.Path(env_value)
130
- elif sys.platform == "win32":
131
- root = pathlib.Path(os.environ.get("LOCALAPPDATA", pathlib.Path.home())) / "cppkh-interface"
132
- elif sys.platform == "darwin":
133
- root = pathlib.Path.home() / "Library" / "Caches" / "cppkh-interface"
134
- else:
135
- root = pathlib.Path(os.environ.get("XDG_CACHE_HOME", pathlib.Path.home() / ".cache")) / "cppkh-interface"
136
- root.mkdir(parents=True, exist_ok=True)
137
- return root
138
-
139
-
140
- def _default_compile_flags() -> list[str]:
141
- flags = ["-std=c++14", "-O3", "-DNDEBUG"]
142
- native = os.environ.get("CPPKH_INTERFACE_NATIVE", "1").strip().lower()
143
- if native not in ("0", "false", "no", "off"):
144
- flags.append("-march=native")
145
- if platform.system() != "Windows":
146
- flags.append("-pthread")
147
- extra = os.environ.get("CPPKH_INTERFACE_CXXFLAGS", "").strip()
148
- if extra:
149
- flags.extend(shlex.split(extra))
150
- return flags
151
-
152
-
153
- def _exe_suffix() -> str:
154
- return ".exe" if platform.system() == "Windows" else ""
155
-
156
-
157
- def _compiler_runtime_path_entries() -> list[str]:
158
- compiler = cpp_simple_interface.get_gpp_filepath().strip()
159
- if not compiler:
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
- candidates = []
163
- unquoted = compiler
164
- if len(unquoted) >= 2 and unquoted[0] == unquoted[-1] and unquoted[0] in ("'", '"'):
165
- unquoted = unquoted[1:-1]
166
- candidates.append(unquoted)
167
-
168
- try:
169
- candidates.extend(shlex.split(compiler, posix=True))
170
- except ValueError:
171
- pass
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 candidates:
175
- path = pathlib.Path(candidate)
176
- if path.exists() and path.is_file():
177
- parent = str(path.resolve().parent)
178
- if parent not in paths:
179
- paths.append(parent)
180
- return paths
181
-
182
-
183
- def _cache_key(source_bytes: bytes, flags: Sequence[str]) -> str:
184
- digest = hashlib.sha256()
185
- digest.update(source_bytes)
186
- digest.update("\0".join(flags).encode("utf-8"))
187
- digest.update(cpp_simple_interface.get_gpp_filepath().encode("utf-8"))
188
- digest.update(platform.platform().encode("utf-8"))
189
- return digest.hexdigest()[:20]
190
-
191
-
192
- def compile_cppkh(
193
- *,
194
- force: bool = False,
195
- cxx: Optional[str] = None,
196
- extra_flags: Optional[Sequence[str]] = None,
197
- ) -> pathlib.Path:
198
- """Compile the packaged C++ source with cpp-simple-interface and return the executable path."""
199
-
200
- if cxx:
201
- cpp_simple_interface.set_gpp_filepath(cxx)
202
-
203
- with _resource_source_path() as source:
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
- source_bytes = source_path.read_bytes()
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
- exe = cache / f"cppkh-{_cache_key(source_bytes, flags)}{_exe_suffix()}"
212
- if exe.exists() and not force:
213
- return exe
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
- os.unlink(pd_file)
298
- except OSError:
299
- pass
300
-
301
- if result.returncode != 0:
302
- detail = (result.stderr or result.stdout or "").strip()
303
- raise CppkhInterfaceError(detail or f"cppkh exited with code {result.returncode}")
304
-
305
- if print_simplified_pd:
306
- return [line.strip() for line in result.stdout.splitlines() if line.strip()]
307
-
308
- matches = re.findall(r'"([^"]*)"', result.stdout)
309
- if not matches:
310
- raise CppkhInterfaceError(f"result not found in cppkh output: {result.stdout!r}")
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]
332
-
333
-
334
- def _prepare_crossings(pd_code: PdInput, de_r1: bool, de_k8: bool) -> list[list[int]]:
335
- crossings = _as_crossings(pd_code)
336
- _check_sanity(crossings)
337
- if de_r1:
338
- crossings = pd_code_de_r1.de_r1(crossings)
339
- if de_k8:
340
- crossings = pd_code_delete_nugatory.erase_all_nugatory(crossings)
341
- return crossings
342
-
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
-
367
- def solve_khovanov(
368
- pd_code: PdInput,
369
- encoding: Optional[str] = None,
370
- de_r1: bool = True,
371
- de_k8: bool = True,
372
- show_real_pdcode: bool = False,
373
- ) -> str:
374
- """Compute Khovanov homology with a javakh-interface compatible signature."""
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
-
394
- crossings = _prepare_crossings(pd_code, de_r1=de_r1, de_k8=de_k8)
395
- if show_real_pdcode:
396
- print(f"Real PD code after de_r1 and de_k8: {crossings}")
397
- if crossings == []:
398
- return UNKNOT_RESULT
399
- return _run_cppkh(_format_pd(crossings), encoding=encoding)
400
-
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
-
435
- def compute_pd(
436
- pd_code: PdInput,
437
- *,
438
- encoding: Optional[str] = None,
439
- de_r1: bool = True,
440
- de_k8: bool = True,
441
- show_real_pdcode: bool = False,
442
- threads: Union[str, int] = "1",
443
- ) -> str:
444
- """Compute Khovanov homology using the same defaults as solve_khovanov."""
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
-
465
- crossings = _prepare_crossings(pd_code, de_r1=de_r1, de_k8=de_k8)
466
- if show_real_pdcode:
467
- print(f"Real PD code after de_r1 and de_k8: {crossings}")
468
- if crossings == []:
469
- return UNKNOT_RESULT
470
- return _run_cppkh(_format_pd(crossings), encoding=encoding, threads=threads)
471
-
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
-
494
- def simplify_pd(pd_code: PdInput, *, de_r1: bool = True, de_k8: bool = True) -> str:
495
- """Return the normalized PD string after optional R1 and nugatory simplification."""
496
-
497
- return _format_pd(_prepare_crossings(pd_code, de_r1=de_r1, de_k8=de_k8))
498
-
499
-
500
- def main(argv: Optional[Sequence[str]] = None) -> int:
501
- parser = argparse.ArgumentParser(description="Compute Khovanov homology with cppkh-interface.")
502
- parser.add_argument("pd_code", help="PD code as PD[...] text or a Python-style list of crossings.")
503
- parser.add_argument("--no-de-r1", action="store_true", help="Disable R1-move removal.")
504
- parser.add_argument("--no-de-k8", action="store_true", help="Disable nugatory-crossing removal.")
505
- parser.add_argument("--threads", default="1", help="cppkh --threads value for direct compute_pd mode.")
506
- args = parser.parse_args(argv)
507
- print(
508
- compute_pd(
509
- args.pd_code,
510
- de_r1=not args.no_de_r1,
511
- de_k8=not args.no_de_k8,
512
- threads=args.threads,
513
- )
514
- )
515
- return 0
516
-
517
-
518
- if __name__ == "__main__":
519
- raise SystemExit(main())
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())