cppkh-interface 0.1.2__tar.gz → 0.1.3__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,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cppkh-interface
3
- Version: 0.1.2
4
- Summary: Python interface for cppkh Khovanov homology computation with runtime C++ compilation.
3
+ Version: 0.1.3
4
+ Summary: Dependency-free Python interface for cppkh with runtime C++ compilation.
5
5
  License-Expression: MIT
6
6
  Author: GGN_2015
7
7
  Author-email: neko@jlulug.org
@@ -12,10 +12,6 @@ Classifier: Programming Language :: Python :: 3.11
12
12
  Classifier: Programming Language :: Python :: 3.12
13
13
  Classifier: Programming Language :: Python :: 3.13
14
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
15
  Project-URL: Documentation, https://github.com/GGN-2015/cppkh/blob/main/docs/PYTHON_PACKAGE.md
20
16
  Project-URL: Homepage, https://github.com/GGN-2015/cppkh
21
17
  Project-URL: Repository, https://github.com/GGN-2015/cppkh
@@ -26,9 +22,9 @@ Description-Content-Type: text/markdown
26
22
  `cppkh-interface` is a Python package for computing integer Khovanov homology
27
23
  with the C++ `cppkh` implementation.
28
24
 
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.
25
+ Version `0.1.3` has no runtime Python-package dependencies. Link crossing signs,
26
+ PD validation, R1 removal, and nugatory-crossing removal all use the bundled
27
+ canonical `cppkh` C++ source and its SageMath-compatible orientation rules.
32
28
 
33
29
  The package is compatible with the main `javakh-interface` function:
34
30
 
@@ -42,13 +38,13 @@ print(cppkh_interface.solve_many_khovanov([pd_code, pd_code]))
42
38
 
43
39
  Unlike wrappers that ship a prebuilt DLL or shared object, this package ships
44
40
  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.
41
+ executable on first use using only Python's standard library. The compiled
42
+ executable is cached for later calls.
47
43
 
48
44
  In the repository checkout, the package does not keep a committed backup copy
49
45
  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.
46
+ package data directory only while the PEP 517 build is running, then removes
47
+ that temporary copy.
52
48
 
53
49
  ## Install
54
50
 
@@ -56,17 +52,18 @@ running, then removes that temporary copy.
56
52
  pip install cppkh-interface
57
53
  ```
58
54
 
59
- A `g++` compatible compiler must be available at runtime. To select a compiler,
60
- set `CXX` before importing or calling the package:
55
+ A C++14 compiler must be available at runtime. The package looks at
56
+ `CPPKH_INTERFACE_CXX`, then `CXX`, then searches `PATH` for `g++`, `clang++`, or
57
+ `c++`. To select a compiler explicitly:
61
58
 
62
59
  ```sh
63
- CXX=clang++ python your_script.py
60
+ CPPKH_INTERFACE_CXX=clang++ python your_script.py
64
61
  ```
65
62
 
66
63
  Windows PowerShell:
67
64
 
68
65
  ```powershell
69
- $env:CXX = "C:\path\to\g++.exe"
66
+ $env:CPPKH_INTERFACE_CXX = "C:\path\to\g++.exe"
70
67
  python your_script.py
71
68
  ```
72
69
 
@@ -75,11 +72,13 @@ python your_script.py
75
72
  From this directory:
76
73
 
77
74
  ```sh
78
- poetry build
75
+ python -m build
79
76
  poetry publish
80
77
  ```
81
78
 
82
- Use `poetry publish --build` to build and upload in one command.
79
+ Do not use `poetry build` or `poetry publish --build`: Poetry's direct builder
80
+ bypasses the source-synchronizing PEP 517 backend. Build first with
81
+ `python -m build`, inspect/test the wheel, then publish the existing artifacts.
83
82
 
84
83
  For local testing:
85
84
 
@@ -3,9 +3,9 @@
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.
6
+ Version `0.1.3` has no runtime Python-package dependencies. Link crossing signs,
7
+ PD validation, R1 removal, and nugatory-crossing removal all use the bundled
8
+ canonical `cppkh` C++ source and its SageMath-compatible orientation rules.
9
9
 
10
10
  The package is compatible with the main `javakh-interface` function:
11
11
 
@@ -19,13 +19,13 @@ print(cppkh_interface.solve_many_khovanov([pd_code, pd_code]))
19
19
 
20
20
  Unlike wrappers that ship a prebuilt DLL or shared object, this package ships
21
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.
22
+ executable on first use using only Python's standard library. The compiled
23
+ executable is cached for later calls.
24
24
 
25
25
  In the repository checkout, the package does not keep a committed backup copy
26
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.
27
+ package data directory only while the PEP 517 build is running, then removes
28
+ that temporary copy.
29
29
 
30
30
  ## Install
31
31
 
@@ -33,17 +33,18 @@ running, then removes that temporary copy.
33
33
  pip install cppkh-interface
34
34
  ```
35
35
 
36
- A `g++` compatible compiler must be available at runtime. To select a compiler,
37
- set `CXX` before importing or calling the package:
36
+ A C++14 compiler must be available at runtime. The package looks at
37
+ `CPPKH_INTERFACE_CXX`, then `CXX`, then searches `PATH` for `g++`, `clang++`, or
38
+ `c++`. To select a compiler explicitly:
38
39
 
39
40
  ```sh
40
- CXX=clang++ python your_script.py
41
+ CPPKH_INTERFACE_CXX=clang++ python your_script.py
41
42
  ```
42
43
 
43
44
  Windows PowerShell:
44
45
 
45
46
  ```powershell
46
- $env:CXX = "C:\path\to\g++.exe"
47
+ $env:CPPKH_INTERFACE_CXX = "C:\path\to\g++.exe"
47
48
  python your_script.py
48
49
  ```
49
50
 
@@ -52,11 +53,13 @@ python your_script.py
52
53
  From this directory:
53
54
 
54
55
  ```sh
55
- poetry build
56
+ python -m build
56
57
  poetry publish
57
58
  ```
58
59
 
59
- Use `poetry publish --build` to build and upload in one command.
60
+ Do not use `poetry build` or `poetry publish --build`: Poetry's direct builder
61
+ bypasses the source-synchronizing PEP 517 backend. Build first with
62
+ `python -m build`, inspect/test the wheel, then publish the existing artifacts.
60
63
 
61
64
  For local testing:
62
65
 
@@ -298,13 +298,14 @@ static BigInt coeffProduct(const BigInt& a, const BigInt& b) {
298
298
  return a * b;
299
299
  }
300
300
 
301
- struct Options {
302
- int matrixThreads = 1;
303
- bool reorderCrossings = true;
304
- bool progress = true;
305
- bool simplifyPD = true;
306
- bool profile = false;
307
- };
301
+ struct Options {
302
+ int matrixThreads = 1;
303
+ bool reorderCrossings = true;
304
+ bool progress = true;
305
+ bool simplifyR1 = true;
306
+ bool simplifyNugatory = true;
307
+ bool profile = false;
308
+ };
308
309
 
309
310
  static Options g_options;
310
311
  typedef std::vector<std::vector<int> > PDCode;
@@ -3186,12 +3187,17 @@ static PDCode eraseOneNugatory(PDCode pd, size_t index, std::vector<int>* signs)
3186
3187
  return renumberFullDfs(bad);
3187
3188
  }
3188
3189
 
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);
3190
+ static PDCode simplifyPDCode(PDCode pd, std::vector<int>* signs,
3191
+ bool simplifyR1, bool simplifyNugatory) {
3192
+ // The nugatory-crossing operation historically used by the Python API also
3193
+ // removes R1 crossings, so nugatory-only mode still starts with R1 cleanup.
3194
+ if (simplifyR1 || simplifyNugatory) pd = eraseR1(pd, signs);
3195
+ if (simplifyNugatory) {
3196
+ while (true) {
3197
+ int index = findNugatory(pd);
3198
+ if (index < 0) break;
3199
+ pd = eraseOneNugatory(pd, static_cast<size_t>(index), signs);
3200
+ }
3195
3201
  }
3196
3202
  return pd;
3197
3203
  }
@@ -3401,7 +3407,7 @@ static std::string computePD(const std::vector<std::vector<int> >& pd) {
3401
3407
  flushCobCache();
3402
3408
  g_smallArena.reset();
3403
3409
  std::vector<int> signs = getSigns(pd);
3404
- PDCode working = g_options.simplifyPD ? simplifyPDCode(pd, &signs) : pd;
3410
+ PDCode working = simplifyPDCode(pd, &signs, g_options.simplifyR1, g_options.simplifyNugatory);
3405
3411
  Komplex k = generateFast(working, signs);
3406
3412
  ProfileScope khScope(g_profile.kh);
3407
3413
  return k.KhForZ();
@@ -3436,9 +3442,10 @@ static std::string formatCrossingSigns(const PDCode& pd) {
3436
3442
  }
3437
3443
 
3438
3444
  static std::string simplifiedPDString(const PDCode& pd) {
3439
- PDCode working = g_options.simplifyPD ? simplifyPDCode(pd) : pd;
3440
- return formatPDCode(working);
3441
- }
3445
+ getSigns(pd);
3446
+ PDCode working = simplifyPDCode(pd, nullptr, g_options.simplifyR1, g_options.simplifyNugatory);
3447
+ return formatPDCode(working);
3448
+ }
3442
3449
 
3443
3450
  } // namespace kh
3444
3451
 
@@ -3490,7 +3497,8 @@ CPPKH_API char* cppkh_compute_pd_ex(const char* pd_code, int simplify_pd, int re
3490
3497
  CppkhOptionsGuard guard;
3491
3498
  kh::g_options.progress = false;
3492
3499
  kh::g_options.profile = false;
3493
- kh::g_options.simplifyPD = simplify_pd != 0;
3500
+ kh::g_options.simplifyR1 = simplify_pd != 0;
3501
+ kh::g_options.simplifyNugatory = simplify_pd != 0;
3494
3502
  kh::g_options.reorderCrossings = reorder_crossings != 0;
3495
3503
  std::string result = kh::computePD(parsed[0].second);
3496
3504
  return cppkhDuplicateString(result);
@@ -3517,7 +3525,8 @@ CPPKH_API char* cppkh_compute_pd_batch_ex(const char* pd_codes, int simplify_pd,
3517
3525
  CppkhOptionsGuard guard;
3518
3526
  kh::g_options.progress = false;
3519
3527
  kh::g_options.profile = false;
3520
- kh::g_options.simplifyPD = simplify_pd != 0;
3528
+ kh::g_options.simplifyR1 = simplify_pd != 0;
3529
+ kh::g_options.simplifyNugatory = simplify_pd != 0;
3521
3530
  kh::g_options.reorderCrossings = reorder_crossings != 0;
3522
3531
 
3523
3532
  std::ostringstream out;
@@ -3547,7 +3556,8 @@ CPPKH_API char* cppkh_simplify_pd(const char* pd_code) {
3547
3556
  if (parsed.empty()) throw std::runtime_error("no PD code found");
3548
3557
  if (parsed.size() != 1) throw std::runtime_error("cppkh_simplify_pd expects exactly one PD code");
3549
3558
  CppkhOptionsGuard guard;
3550
- kh::g_options.simplifyPD = true;
3559
+ kh::g_options.simplifyR1 = true;
3560
+ kh::g_options.simplifyNugatory = true;
3551
3561
  std::string result = kh::simplifiedPDString(parsed[0].second);
3552
3562
  return cppkhDuplicateString(result);
3553
3563
  } catch (const std::exception& e) {
@@ -3562,7 +3572,7 @@ CPPKH_API char* cppkh_simplify_pd(const char* pd_code) {
3562
3572
 
3563
3573
  #ifndef CPPKH_SHARED_LIBRARY
3564
3574
  static void usage() {
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";
3575
+ std::cout << "Usage: cppkh [--pd-file FILE] [--pd-dir DIR] [--pd-code CODE] [--ordered] [--threads N|auto] [--quiet] [--profile] [--no-simplify-pd] [--simplify-r1|--no-simplify-r1] [--simplify-nugatory|--no-simplify-nugatory] [--print-simplified-pd] [--print-crossing-signs]\n";
3566
3576
  std::cout << "Thread backend: " << KH_THREAD_BACKEND_NAME << "\n";
3567
3577
  std::cout << "Detected CPU threads: " << kh::detectHardwareThreads() << "\n";
3568
3578
  std::cout << "PD simplification: R1 removal then nugatory crossing removal is enabled by default.\n";
@@ -3591,8 +3601,16 @@ int main(int argc, char** argv) {
3591
3601
  } else if (a == "--ordered" || a == "-O") kh::g_options.reorderCrossings = false;
3592
3602
  else if (a == "--quiet" || a == "-q") kh::g_options.progress = false;
3593
3603
  else if (a == "--profile") kh::g_options.profile = true;
3594
- else if (a == "--no-simplify-pd" || a == "--raw-pd") kh::g_options.simplifyPD = false;
3595
- else if (a == "--simplify-pd") kh::g_options.simplifyPD = true;
3604
+ else if (a == "--no-simplify-pd" || a == "--raw-pd") {
3605
+ kh::g_options.simplifyR1 = false;
3606
+ kh::g_options.simplifyNugatory = false;
3607
+ } else if (a == "--simplify-pd") {
3608
+ kh::g_options.simplifyR1 = true;
3609
+ kh::g_options.simplifyNugatory = true;
3610
+ } else if (a == "--no-simplify-r1") kh::g_options.simplifyR1 = false;
3611
+ else if (a == "--simplify-r1") kh::g_options.simplifyR1 = true;
3612
+ else if (a == "--no-simplify-nugatory") kh::g_options.simplifyNugatory = false;
3613
+ else if (a == "--simplify-nugatory") kh::g_options.simplifyNugatory = true;
3596
3614
  else if (a == "--print-simplified-pd") printSimplifiedPD = true;
3597
3615
  else if (a == "--print-crossing-signs") printCrossingSigns = true;
3598
3616
  else if (a == "--threads" || a == "-j") {
@@ -9,22 +9,17 @@ import pathlib
9
9
  import platform
10
10
  import re
11
11
  import shlex
12
+ import shutil
12
13
  import subprocess
13
14
  import sys
14
15
  import tempfile
16
+ import uuid
15
17
  from importlib import resources
16
18
  from typing import Optional, Sequence, Union
17
19
 
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
20
  PdInput = Union[str, Sequence[Sequence[int]]]
26
21
  PdManyInput = Union[str, Sequence[PdInput]]
27
- UNKNOT_RESULT = "q^-1*t^0*Z[0] + q^1*t^0*Z[0]"
22
+ _configured_cxx: Optional[str] = None
28
23
 
29
24
 
30
25
  class CppkhInterfaceError(RuntimeError):
@@ -77,13 +72,6 @@ def _as_crossings(pd_code: PdInput) -> list[list[int]]:
77
72
  return crossings
78
73
 
79
74
 
80
- 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
75
  def normalize_pd_code(pd_code: PdInput) -> str:
88
76
  """Normalize a supported PD-code value into standard ``PD[X[...],...]`` text."""
89
77
 
@@ -154,25 +142,84 @@ def _exe_suffix() -> str:
154
142
  return ".exe" if platform.system() == "Windows" else ""
155
143
 
156
144
 
157
- def _compiler_runtime_path_entries() -> list[str]:
158
- compiler = cpp_simple_interface.get_gpp_filepath().strip()
159
- if not compiler:
145
+ def _compiler_parts(command: str) -> list[str]:
146
+ command = command.strip()
147
+ if not command:
160
148
  return []
161
-
162
- candidates = []
163
- unquoted = compiler
149
+ unquoted = command
164
150
  if len(unquoted) >= 2 and unquoted[0] == unquoted[-1] and unquoted[0] in ("'", '"'):
165
151
  unquoted = unquoted[1:-1]
166
- candidates.append(unquoted)
152
+ if pathlib.Path(unquoted).is_file():
153
+ return [unquoted]
154
+ try:
155
+ parts = shlex.split(command, posix=os.name != "nt")
156
+ except ValueError as exc:
157
+ raise CppkhInterfaceError(f"invalid C++ compiler command: {command!r}") from exc
158
+ return [part.strip("\"'") for part in parts if part.strip("\"'")]
159
+
167
160
 
161
+ def _subprocess_kwargs() -> dict:
162
+ kwargs = {
163
+ "stdout": subprocess.PIPE,
164
+ "stderr": subprocess.PIPE,
165
+ "text": True,
166
+ "encoding": "utf-8",
167
+ "errors": "replace",
168
+ }
169
+ if platform.system() == "Windows":
170
+ kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
171
+ return kwargs
172
+
173
+
174
+ def _probe_compiler(parts: Sequence[str]) -> subprocess.CompletedProcess:
175
+ if not parts:
176
+ raise CppkhInterfaceError("empty C++ compiler command")
168
177
  try:
169
- candidates.extend(shlex.split(compiler, posix=True))
170
- except ValueError:
171
- pass
178
+ result = subprocess.run([*parts, "--version"], timeout=15, **_subprocess_kwargs())
179
+ except (OSError, subprocess.SubprocessError) as exc:
180
+ raise CppkhInterfaceError(f"could not run C++ compiler {' '.join(parts)!r}: {exc}") from exc
181
+ if result.returncode != 0:
182
+ detail = (result.stderr or result.stdout or "").strip()
183
+ raise CppkhInterfaceError(detail or f"C++ compiler {' '.join(parts)!r} is not usable")
184
+ return result
185
+
186
+
187
+ def _resolve_compiler(cxx: Optional[str] = None) -> list[str]:
188
+ global _configured_cxx
189
+
190
+ explicit = cxx or _configured_cxx
191
+ if not explicit:
192
+ explicit = os.environ.get("CPPKH_INTERFACE_CXX") or os.environ.get("CXX")
193
+ if explicit:
194
+ parts = _compiler_parts(explicit)
195
+ _probe_compiler(parts)
196
+ if cxx:
197
+ _configured_cxx = cxx
198
+ return parts
199
+
200
+ errors = []
201
+ for name in ("g++", "clang++", "c++"):
202
+ candidate = shutil.which(name)
203
+ if not candidate:
204
+ continue
205
+ parts = [candidate]
206
+ try:
207
+ _probe_compiler(parts)
208
+ return parts
209
+ except CppkhInterfaceError as exc:
210
+ errors.append(str(exc))
211
+ detail = f" ({'; '.join(errors)})" if errors else ""
212
+ raise CppkhInterfaceError(
213
+ "no usable C++ compiler was found. Install a C++14 compiler or set "
214
+ f"CPPKH_INTERFACE_CXX/CXX to its command{detail}"
215
+ )
216
+
172
217
 
218
+ def _compiler_runtime_path_entries(compiler: Sequence[str]) -> list[str]:
173
219
  paths = []
174
- for candidate in candidates:
175
- path = pathlib.Path(candidate)
220
+ for candidate in compiler:
221
+ resolved = shutil.which(candidate) or candidate
222
+ path = pathlib.Path(resolved)
176
223
  if path.exists() and path.is_file():
177
224
  parent = str(path.resolve().parent)
178
225
  if parent not in paths:
@@ -180,65 +227,84 @@ def _compiler_runtime_path_entries() -> list[str]:
180
227
  return paths
181
228
 
182
229
 
183
- def _cache_key(source_bytes: bytes, flags: Sequence[str]) -> str:
230
+ def _compiler_identity(compiler: Sequence[str]) -> str:
231
+ result = _probe_compiler(compiler)
232
+ executable = shutil.which(compiler[0]) or compiler[0]
233
+ return "\0".join([str(pathlib.Path(executable).resolve()), *compiler[1:], result.stdout.strip()])
234
+
235
+
236
+ def _cache_key(source_bytes: bytes, flags: Sequence[str], compiler: Sequence[str]) -> str:
184
237
  digest = hashlib.sha256()
185
238
  digest.update(source_bytes)
186
239
  digest.update("\0".join(flags).encode("utf-8"))
187
- digest.update(cpp_simple_interface.get_gpp_filepath().encode("utf-8"))
240
+ digest.update(_compiler_identity(compiler).encode("utf-8"))
188
241
  digest.update(platform.platform().encode("utf-8"))
189
242
  return digest.hexdigest()[:20]
190
243
 
191
244
 
245
+ def _compile_source(
246
+ compiler: Sequence[str],
247
+ source: pathlib.Path,
248
+ output: pathlib.Path,
249
+ flags: Sequence[str],
250
+ ) -> subprocess.CompletedProcess:
251
+ command = [*compiler, *flags, str(source), "-o", str(output)]
252
+ try:
253
+ return subprocess.run(command, **_subprocess_kwargs())
254
+ except OSError as exc:
255
+ raise CppkhInterfaceError(f"could not start C++ compiler: {exc}") from exc
256
+
257
+
192
258
  def compile_cppkh(
193
259
  *,
194
260
  force: bool = False,
195
261
  cxx: Optional[str] = None,
196
262
  extra_flags: Optional[Sequence[str]] = None,
197
263
  ) -> 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)
264
+ """Compile the packaged C++ source and return the cached executable path."""
202
265
 
203
266
  with _resource_source_path() as source:
204
267
  source_path = pathlib.Path(source)
205
268
  source_bytes = source_path.read_bytes()
269
+ compiler = _resolve_compiler(cxx)
206
270
  flags = _default_compile_flags()
207
271
  if extra_flags:
208
272
  flags.extend(str(flag) for flag in extra_flags)
209
273
 
210
274
  cache = _cache_dir()
211
- exe = cache / f"cppkh-{_cache_key(source_bytes, flags)}{_exe_suffix()}"
275
+ exe = cache / f"cppkh-{_cache_key(source_bytes, flags, compiler)}{_exe_suffix()}"
212
276
  if exe.exists() and not force:
213
277
  return exe
214
278
 
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)
279
+ tmp_exe = cache / f"{exe.name}.tmp-{os.getpid()}-{uuid.uuid4().hex}{_exe_suffix()}"
237
280
  try:
238
- exe.chmod(exe.stat().st_mode | 0o755)
239
- except OSError:
240
- pass
241
- return exe
281
+ result = _compile_source(compiler, source_path, tmp_exe, flags)
282
+ if result.returncode != 0 and "-march=native" in flags:
283
+ fallback_flags = [flag for flag in flags if flag != "-march=native"]
284
+ result = _compile_source(compiler, source_path, tmp_exe, fallback_flags)
285
+ if result.returncode != 0:
286
+ detail = (result.stderr or result.stdout or "").strip()
287
+ raise CppkhInterfaceError(detail or "C++ compilation failed")
288
+ if not tmp_exe.exists():
289
+ raise CppkhInterfaceError(f"compiled executable was not created: {tmp_exe}")
290
+
291
+ if exe.exists() and not force:
292
+ return exe
293
+ try:
294
+ os.replace(tmp_exe, exe)
295
+ except OSError:
296
+ if force or not exe.exists():
297
+ raise
298
+ try:
299
+ exe.chmod(exe.stat().st_mode | 0o755)
300
+ except OSError:
301
+ pass
302
+ return exe
303
+ finally:
304
+ try:
305
+ tmp_exe.unlink()
306
+ except OSError:
307
+ pass
242
308
 
243
309
 
244
310
  def get_cppkh_executable() -> pathlib.Path:
@@ -252,7 +318,8 @@ def _run_cppkh_document(
252
318
  *,
253
319
  encoding: Optional[str] = None,
254
320
  threads: Union[str, int] = "1",
255
- simplify_pd: bool = False,
321
+ de_r1: bool = False,
322
+ de_k8: bool = False,
256
323
  print_simplified_pd: bool = False,
257
324
  ) -> list[str]:
258
325
  exe = compile_cppkh()
@@ -270,8 +337,8 @@ def _run_cppkh_document(
270
337
  "--threads",
271
338
  str(threads),
272
339
  ]
273
- if not simplify_pd:
274
- command.append("--no-simplify-pd")
340
+ command.append("--simplify-r1" if de_r1 else "--no-simplify-r1")
341
+ command.append("--simplify-nugatory" if de_k8 else "--no-simplify-nugatory")
275
342
  if print_simplified_pd:
276
343
  command.append("--print-simplified-pd")
277
344
 
@@ -283,7 +350,7 @@ def _run_cppkh_document(
283
350
  "errors": "replace",
284
351
  }
285
352
  env = os.environ.copy()
286
- runtime_paths = _compiler_runtime_path_entries()
353
+ runtime_paths = _compiler_runtime_path_entries(_resolve_compiler())
287
354
  if runtime_paths:
288
355
  env["PATH"] = os.pathsep.join(runtime_paths + [env.get("PATH", "")])
289
356
  kwargs["env"] = env
@@ -303,7 +370,8 @@ def _run_cppkh_document(
303
370
  raise CppkhInterfaceError(detail or f"cppkh exited with code {result.returncode}")
304
371
 
305
372
  if print_simplified_pd:
306
- return [line.strip() for line in result.stdout.splitlines() if line.strip()]
373
+ lines = [line.strip() for line in result.stdout.splitlines() if line.strip()]
374
+ return [line.split("\t", 1)[-1] for line in lines]
307
375
 
308
376
  matches = re.findall(r'"([^"]*)"', result.stdout)
309
377
  if not matches:
@@ -316,14 +384,16 @@ def _run_cppkh(
316
384
  *,
317
385
  encoding: Optional[str] = None,
318
386
  threads: Union[str, int] = "1",
319
- simplify_pd: bool = False,
387
+ de_r1: bool = False,
388
+ de_k8: bool = False,
320
389
  print_simplified_pd: bool = False,
321
390
  ) -> str:
322
391
  results = _run_cppkh_document(
323
392
  pd_text,
324
393
  encoding=encoding,
325
394
  threads=threads,
326
- simplify_pd=simplify_pd,
395
+ de_r1=de_r1,
396
+ de_k8=de_k8,
327
397
  print_simplified_pd=print_simplified_pd,
328
398
  )
329
399
  if len(results) != 1:
@@ -331,37 +401,40 @@ def _run_cppkh(
331
401
  return results[0]
332
402
 
333
403
 
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
404
+ def _prepare_many_for_cppkh(pd_codes: PdManyInput) -> str:
405
+ if isinstance(pd_codes, str):
406
+ return pd_codes.strip()
407
+ return "\n".join(normalize_pd_code(pd_code) for pd_code in pd_codes)
342
408
 
343
409
 
344
- def _prepare_many_for_cppkh(
345
- pd_codes: PdManyInput,
410
+ def _compute_one(
411
+ pd_code: PdInput,
346
412
  *,
413
+ encoding: Optional[str],
347
414
  de_r1: bool,
348
415
  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."
416
+ show_real_pdcode: bool,
417
+ threads: Union[str, int],
418
+ ) -> str:
419
+ document = normalize_pd_code(pd_code)
420
+ if show_real_pdcode:
421
+ real_pd = _run_cppkh(
422
+ document,
423
+ encoding=encoding,
424
+ threads=threads,
425
+ de_r1=de_r1,
426
+ de_k8=de_k8,
427
+ print_simplified_pd=True,
354
428
  )
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
429
+ display_pd = real_pd if de_r1 and de_k8 else _as_crossings(real_pd)
430
+ print(f"Real PD code after de_r1 and de_k8: {display_pd}")
431
+ return _run_cppkh(
432
+ document,
433
+ encoding=encoding,
434
+ threads=threads,
435
+ de_r1=de_r1,
436
+ de_k8=de_k8,
437
+ )
365
438
 
366
439
 
367
440
  def solve_khovanov(
@@ -373,30 +446,14 @@ def solve_khovanov(
373
446
  ) -> str:
374
447
  """Compute Khovanov homology with a javakh-interface compatible signature."""
375
448
 
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)
449
+ return _compute_one(
450
+ pd_code,
451
+ encoding=encoding,
452
+ de_r1=de_r1,
453
+ de_k8=de_k8,
454
+ show_real_pdcode=show_real_pdcode,
455
+ threads="1",
456
+ )
400
457
 
401
458
 
402
459
  def solve_many_khovanov(
@@ -414,22 +471,26 @@ def solve_many_khovanov(
414
471
  nugatory crossing removal for the whole batch.
415
472
  """
416
473
 
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()}")
474
+ document = _prepare_many_for_cppkh(pd_codes)
430
475
  if not document:
431
476
  return []
432
- return _run_cppkh_document(document, encoding=encoding, threads=threads, simplify_pd=use_cpp_simplify)
477
+ if show_real_pdcode:
478
+ simplified = _run_cppkh_document(
479
+ document,
480
+ encoding=encoding,
481
+ threads=threads,
482
+ de_r1=de_r1,
483
+ de_k8=de_k8,
484
+ print_simplified_pd=True,
485
+ )
486
+ print(f"Real PD code after de_r1 and de_k8: {simplified}")
487
+ return _run_cppkh_document(
488
+ document,
489
+ encoding=encoding,
490
+ threads=threads,
491
+ de_r1=de_r1,
492
+ de_k8=de_k8,
493
+ )
433
494
 
434
495
 
435
496
  def compute_pd(
@@ -443,31 +504,14 @@ def compute_pd(
443
504
  ) -> str:
444
505
  """Compute Khovanov homology using the same defaults as solve_khovanov."""
445
506
 
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)
507
+ return _compute_one(
508
+ pd_code,
509
+ encoding=encoding,
510
+ de_r1=de_r1,
511
+ de_k8=de_k8,
512
+ show_real_pdcode=show_real_pdcode,
513
+ threads=threads,
514
+ )
471
515
 
472
516
 
473
517
  def compute_many_pd(
@@ -494,7 +538,12 @@ def compute_many_pd(
494
538
  def simplify_pd(pd_code: PdInput, *, de_r1: bool = True, de_k8: bool = True) -> str:
495
539
  """Return the normalized PD string after optional R1 and nugatory simplification."""
496
540
 
497
- return _format_pd(_prepare_crossings(pd_code, de_r1=de_r1, de_k8=de_k8))
541
+ return _run_cppkh(
542
+ normalize_pd_code(pd_code),
543
+ de_r1=de_r1,
544
+ de_k8=de_k8,
545
+ print_simplified_pd=True,
546
+ )
498
547
 
499
548
 
500
549
  def main(argv: Optional[Sequence[str]] = None) -> int:
@@ -1,19 +1,14 @@
1
1
  [project]
2
2
  name = "cppkh-interface"
3
- version = "0.1.2"
4
- description = "Python interface for cppkh Khovanov homology computation with runtime C++ compilation."
3
+ version = "0.1.3"
4
+ description = "Dependency-free Python interface for cppkh with runtime C++ compilation."
5
5
  authors = [
6
6
  {name = "GGN_2015", email = "neko@jlulug.org"}
7
7
  ]
8
8
  license = "MIT"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
11
- dependencies = [
12
- "cpp-simple-interface>=0.1.2",
13
- "pd-code-sanity>=0.0.1",
14
- "pd-code-delete-nugatory",
15
- "pd-code-de-r1"
16
- ]
11
+ dependencies = []
17
12
 
18
13
  [project.urls]
19
14
  Homepage = "https://github.com/GGN-2015/cppkh"
@@ -31,6 +26,6 @@ include = [
31
26
  ]
32
27
 
33
28
  [build-system]
34
- requires = ["poetry-core>=2.0.0,<3.0.0", "setuptools>=61"]
29
+ requires = ["poetry-core>=2.0.0,<3.0.0"]
35
30
  build-backend = "cppkh_interface_build_backend"
36
31
  backend-path = ["build_backend"]