cppkh-interface 0.1.0__tar.gz → 0.1.1__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.
- {cppkh_interface-0.1.0 → cppkh_interface-0.1.1}/PKG-INFO +12 -3
- {cppkh_interface-0.1.0 → cppkh_interface-0.1.1}/README.md +11 -2
- cppkh_interface-0.1.1/build_backend/cppkh_interface_build_backend.py +207 -0
- {cppkh_interface-0.1.0 → cppkh_interface-0.1.1}/cppkh_interface/__init__.py +6 -0
- {cppkh_interface-0.1.0 → cppkh_interface-0.1.1}/cppkh_interface/data/src/main.cpp +32 -0
- {cppkh_interface-0.1.0 → cppkh_interface-0.1.1}/cppkh_interface/main.py +174 -7
- {cppkh_interface-0.1.0 → cppkh_interface-0.1.1}/pyproject.toml +5 -3
- {cppkh_interface-0.1.0 → cppkh_interface-0.1.1}/cppkh_interface/__main__.py +0 -0
- {cppkh_interface-0.1.0 → cppkh_interface-0.1.1}/cppkh_interface/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cppkh-interface
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: Python interface for cppkh Khovanov homology computation with runtime C++ compilation.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Author: GGN_2015
|
|
@@ -33,11 +33,18 @@ import cppkh_interface
|
|
|
33
33
|
|
|
34
34
|
pd_code = [[1, 5, 2, 4], [3, 1, 4, 6], [5, 3, 6, 2]]
|
|
35
35
|
print(cppkh_interface.solve_khovanov(pd_code, de_r1=True, de_k8=True))
|
|
36
|
+
print(cppkh_interface.solve_many_khovanov([pd_code, pd_code]))
|
|
36
37
|
```
|
|
37
38
|
|
|
38
39
|
Unlike wrappers that ship a prebuilt DLL or shared object, this package ships
|
|
39
|
-
the `cppkh` C++ source file and compiles a local
|
|
40
|
-
`cpp-simple-interface`. The compiled executable
|
|
40
|
+
the `cppkh` C++ source file in built distributions and compiles a local
|
|
41
|
+
executable on first use through `cpp-simple-interface`. The compiled executable
|
|
42
|
+
is cached for later calls.
|
|
43
|
+
|
|
44
|
+
In the repository checkout, the package does not keep a committed backup copy
|
|
45
|
+
of the C++ source. The build backend copies `../../src/main.cpp` into the
|
|
46
|
+
package data directory only while `poetry build` or `poetry publish --build` is
|
|
47
|
+
running, then removes that temporary copy.
|
|
41
48
|
|
|
42
49
|
## Install
|
|
43
50
|
|
|
@@ -68,6 +75,8 @@ poetry build
|
|
|
68
75
|
poetry publish
|
|
69
76
|
```
|
|
70
77
|
|
|
78
|
+
Use `poetry publish --build` to build and upload in one command.
|
|
79
|
+
|
|
71
80
|
For local testing:
|
|
72
81
|
|
|
73
82
|
```sh
|
|
@@ -10,11 +10,18 @@ import cppkh_interface
|
|
|
10
10
|
|
|
11
11
|
pd_code = [[1, 5, 2, 4], [3, 1, 4, 6], [5, 3, 6, 2]]
|
|
12
12
|
print(cppkh_interface.solve_khovanov(pd_code, de_r1=True, de_k8=True))
|
|
13
|
+
print(cppkh_interface.solve_many_khovanov([pd_code, pd_code]))
|
|
13
14
|
```
|
|
14
15
|
|
|
15
16
|
Unlike wrappers that ship a prebuilt DLL or shared object, this package ships
|
|
16
|
-
the `cppkh` C++ source file and compiles a local
|
|
17
|
-
`cpp-simple-interface`. The compiled executable
|
|
17
|
+
the `cppkh` C++ source file in built distributions and compiles a local
|
|
18
|
+
executable on first use through `cpp-simple-interface`. The compiled executable
|
|
19
|
+
is cached for later calls.
|
|
20
|
+
|
|
21
|
+
In the repository checkout, the package does not keep a committed backup copy
|
|
22
|
+
of the C++ source. The build backend copies `../../src/main.cpp` into the
|
|
23
|
+
package data directory only while `poetry build` or `poetry publish --build` is
|
|
24
|
+
running, then removes that temporary copy.
|
|
18
25
|
|
|
19
26
|
## Install
|
|
20
27
|
|
|
@@ -45,6 +52,8 @@ poetry build
|
|
|
45
52
|
poetry publish
|
|
46
53
|
```
|
|
47
54
|
|
|
55
|
+
Use `poetry publish --build` to build and upload in one command.
|
|
56
|
+
|
|
48
57
|
For local testing:
|
|
49
58
|
|
|
50
59
|
```sh
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import hashlib
|
|
5
|
+
import io
|
|
6
|
+
import shutil
|
|
7
|
+
import tarfile
|
|
8
|
+
import tempfile
|
|
9
|
+
import zipfile
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from poetry.core.masonry import api as poetry_api
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
16
|
+
REPO_ROOT = PROJECT_ROOT.parents[1]
|
|
17
|
+
SOURCE = REPO_ROOT / "src" / "main.cpp"
|
|
18
|
+
TARGET = PROJECT_ROOT / "cppkh_interface" / "data" / "src" / "main.cpp"
|
|
19
|
+
WHEEL_SOURCE_NAME = "cppkh_interface/data/src/main.cpp"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _sync_cpp_source() -> None:
|
|
23
|
+
if SOURCE.exists():
|
|
24
|
+
TARGET.parent.mkdir(parents=True, exist_ok=True)
|
|
25
|
+
shutil.copyfile(SOURCE, TARGET)
|
|
26
|
+
return
|
|
27
|
+
if TARGET.exists():
|
|
28
|
+
return
|
|
29
|
+
raise FileNotFoundError(
|
|
30
|
+
"cppkh source file was not found. Expected either "
|
|
31
|
+
f"{SOURCE} in the repository checkout or {TARGET} in the source tree."
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _clean_cpp_source() -> None:
|
|
36
|
+
if TARGET.exists() and SOURCE.exists():
|
|
37
|
+
TARGET.unlink()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _source_bytes() -> bytes:
|
|
41
|
+
if TARGET.exists():
|
|
42
|
+
return TARGET.read_bytes()
|
|
43
|
+
if SOURCE.exists():
|
|
44
|
+
return SOURCE.read_bytes()
|
|
45
|
+
raise FileNotFoundError("cppkh source file was not available for packaging")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _wheel_hash_and_size(data: bytes) -> tuple[str, str]:
|
|
49
|
+
digest = hashlib.sha256(data).digest()
|
|
50
|
+
encoded = base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii")
|
|
51
|
+
return f"sha256={encoded}", str(len(data))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _make_universal_wheel_metadata(data: bytes) -> bytes:
|
|
55
|
+
text = data.decode("utf-8")
|
|
56
|
+
lines = []
|
|
57
|
+
tag_written = False
|
|
58
|
+
root_written = False
|
|
59
|
+
for line in text.splitlines():
|
|
60
|
+
if line.startswith("Root-Is-Purelib:"):
|
|
61
|
+
lines.append("Root-Is-Purelib: true")
|
|
62
|
+
root_written = True
|
|
63
|
+
elif line.startswith("Tag:"):
|
|
64
|
+
if not tag_written:
|
|
65
|
+
lines.append("Tag: py3-none-any")
|
|
66
|
+
tag_written = True
|
|
67
|
+
else:
|
|
68
|
+
lines.append(line)
|
|
69
|
+
if not root_written:
|
|
70
|
+
lines.append("Root-Is-Purelib: true")
|
|
71
|
+
if not tag_written:
|
|
72
|
+
lines.append("Tag: py3-none-any")
|
|
73
|
+
return ("\n".join(lines) + "\n").encode("utf-8")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _universal_wheel_path(wheel_path: Path, record_name: str) -> Path:
|
|
77
|
+
dist_info = record_name.split("/", 1)[0]
|
|
78
|
+
base = dist_info.removesuffix(".dist-info")
|
|
79
|
+
return wheel_path.with_name(f"{base}-py3-none-any.whl")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _rewrite_wheel_with_source(wheel_path: Path) -> Path:
|
|
83
|
+
source_data = _source_bytes()
|
|
84
|
+
rows: list[tuple[str, str, str]] = []
|
|
85
|
+
|
|
86
|
+
with zipfile.ZipFile(wheel_path, "r") as zin:
|
|
87
|
+
record_name = next(
|
|
88
|
+
(name for name in zin.namelist() if name.endswith(".dist-info/RECORD")),
|
|
89
|
+
None,
|
|
90
|
+
)
|
|
91
|
+
if record_name is None:
|
|
92
|
+
raise RuntimeError(f"wheel RECORD file not found in {wheel_path}")
|
|
93
|
+
output_path = _universal_wheel_path(wheel_path, record_name)
|
|
94
|
+
temp_path = output_path.with_name(output_path.name + ".tmp")
|
|
95
|
+
|
|
96
|
+
with zipfile.ZipFile(temp_path, "w", compression=zipfile.ZIP_DEFLATED) as zout:
|
|
97
|
+
wheel_metadata_name = record_name.removesuffix("RECORD") + "WHEEL"
|
|
98
|
+
|
|
99
|
+
for item in zin.infolist():
|
|
100
|
+
if item.filename in (WHEEL_SOURCE_NAME, record_name):
|
|
101
|
+
continue
|
|
102
|
+
data = zin.read(item.filename)
|
|
103
|
+
if item.filename == wheel_metadata_name:
|
|
104
|
+
data = _make_universal_wheel_metadata(data)
|
|
105
|
+
zout.writestr(item, data)
|
|
106
|
+
if not item.is_dir():
|
|
107
|
+
digest, size = _wheel_hash_and_size(data)
|
|
108
|
+
rows.append((item.filename, digest, size))
|
|
109
|
+
|
|
110
|
+
source_info = zipfile.ZipInfo(WHEEL_SOURCE_NAME, date_time=(2016, 1, 1, 0, 0, 0))
|
|
111
|
+
source_info.compress_type = zipfile.ZIP_DEFLATED
|
|
112
|
+
zout.writestr(source_info, source_data)
|
|
113
|
+
digest, size = _wheel_hash_and_size(source_data)
|
|
114
|
+
rows.append((WHEEL_SOURCE_NAME, digest, size))
|
|
115
|
+
|
|
116
|
+
rows.append((record_name, "", ""))
|
|
117
|
+
record_text = "".join(f"{path},{digest},{size}\n" for path, digest, size in rows)
|
|
118
|
+
record_info = zipfile.ZipInfo(record_name, date_time=(2016, 1, 1, 0, 0, 0))
|
|
119
|
+
record_info.compress_type = zipfile.ZIP_DEFLATED
|
|
120
|
+
zout.writestr(record_info, record_text.encode("utf-8"))
|
|
121
|
+
|
|
122
|
+
temp_path.replace(output_path)
|
|
123
|
+
if output_path != wheel_path and wheel_path.exists():
|
|
124
|
+
wheel_path.unlink()
|
|
125
|
+
return output_path
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def finalize_poetry_wheels() -> None:
|
|
129
|
+
dist = PROJECT_ROOT / "dist"
|
|
130
|
+
if not dist.exists():
|
|
131
|
+
return
|
|
132
|
+
for wheel_path in sorted(dist.glob("cppkh_interface-*.whl")):
|
|
133
|
+
_rewrite_wheel_with_source(wheel_path)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _rewrite_sdist_with_source(sdist_path: Path) -> None:
|
|
137
|
+
source_data = _source_bytes()
|
|
138
|
+
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".tar.gz")
|
|
139
|
+
temp_path = Path(temp_file.name)
|
|
140
|
+
temp_file.close()
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
with tarfile.open(sdist_path, "r:gz") as tin, tarfile.open(temp_path, "w:gz") as tout:
|
|
144
|
+
names = tin.getnames()
|
|
145
|
+
if not names:
|
|
146
|
+
raise RuntimeError(f"sdist is empty: {sdist_path}")
|
|
147
|
+
root = names[0].split("/", 1)[0]
|
|
148
|
+
source_name = f"{root}/{WHEEL_SOURCE_NAME}"
|
|
149
|
+
|
|
150
|
+
for member in tin.getmembers():
|
|
151
|
+
if member.name == source_name:
|
|
152
|
+
continue
|
|
153
|
+
if member.isfile():
|
|
154
|
+
extracted = tin.extractfile(member)
|
|
155
|
+
if extracted is None:
|
|
156
|
+
raise RuntimeError(f"could not read {member.name} from {sdist_path}")
|
|
157
|
+
with extracted:
|
|
158
|
+
tout.addfile(member, extracted)
|
|
159
|
+
else:
|
|
160
|
+
tout.addfile(member)
|
|
161
|
+
|
|
162
|
+
source_info = tarfile.TarInfo(source_name)
|
|
163
|
+
source_info.size = len(source_data)
|
|
164
|
+
source_info.mtime = 0
|
|
165
|
+
source_info.mode = 0o644
|
|
166
|
+
tout.addfile(source_info, io.BytesIO(source_data))
|
|
167
|
+
|
|
168
|
+
temp_path.replace(sdist_path)
|
|
169
|
+
finally:
|
|
170
|
+
if temp_path.exists():
|
|
171
|
+
temp_path.unlink()
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def get_requires_for_build_wheel(config_settings=None):
|
|
175
|
+
return poetry_api.get_requires_for_build_wheel(config_settings)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def get_requires_for_build_sdist(config_settings=None):
|
|
179
|
+
return poetry_api.get_requires_for_build_sdist(config_settings)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None):
|
|
183
|
+
_sync_cpp_source()
|
|
184
|
+
try:
|
|
185
|
+
return poetry_api.prepare_metadata_for_build_wheel(metadata_directory, config_settings)
|
|
186
|
+
finally:
|
|
187
|
+
_clean_cpp_source()
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def build_wheel(wheel_directory, config_settings=None, metadata_directory=None):
|
|
191
|
+
_sync_cpp_source()
|
|
192
|
+
try:
|
|
193
|
+
wheel_name = poetry_api.build_wheel(wheel_directory, config_settings, metadata_directory)
|
|
194
|
+
final_path = _rewrite_wheel_with_source(Path(wheel_directory) / wheel_name)
|
|
195
|
+
return final_path.name
|
|
196
|
+
finally:
|
|
197
|
+
_clean_cpp_source()
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def build_sdist(sdist_directory, config_settings=None):
|
|
201
|
+
_sync_cpp_source()
|
|
202
|
+
try:
|
|
203
|
+
sdist_name = poetry_api.build_sdist(sdist_directory, config_settings)
|
|
204
|
+
_rewrite_sdist_with_source(Path(sdist_directory) / sdist_name)
|
|
205
|
+
return sdist_name
|
|
206
|
+
finally:
|
|
207
|
+
_clean_cpp_source()
|
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
from .main import (
|
|
2
2
|
CppkhInterfaceError,
|
|
3
3
|
compile_cppkh,
|
|
4
|
+
compute_many_pd,
|
|
4
5
|
compute_pd,
|
|
5
6
|
get_cppkh_executable,
|
|
6
7
|
normalize_pd_code,
|
|
8
|
+
normalize_pd_codes,
|
|
7
9
|
simplify_pd,
|
|
10
|
+
solve_many_khovanov,
|
|
8
11
|
solve_khovanov,
|
|
9
12
|
)
|
|
10
13
|
|
|
11
14
|
__all__ = [
|
|
12
15
|
"CppkhInterfaceError",
|
|
13
16
|
"compile_cppkh",
|
|
17
|
+
"compute_many_pd",
|
|
14
18
|
"compute_pd",
|
|
15
19
|
"get_cppkh_executable",
|
|
16
20
|
"normalize_pd_code",
|
|
21
|
+
"normalize_pd_codes",
|
|
17
22
|
"simplify_pd",
|
|
23
|
+
"solve_many_khovanov",
|
|
18
24
|
"solve_khovanov",
|
|
19
25
|
]
|
|
@@ -3409,6 +3409,38 @@ CPPKH_API char* cppkh_compute_pd(const char* pd_code) {
|
|
|
3409
3409
|
return cppkh_compute_pd_ex(pd_code, 1, 1);
|
|
3410
3410
|
}
|
|
3411
3411
|
|
|
3412
|
+
CPPKH_API char* cppkh_compute_pd_batch_ex(const char* pd_codes, int simplify_pd, int reorder_crossings) {
|
|
3413
|
+
try {
|
|
3414
|
+
g_cppkhLastError.clear();
|
|
3415
|
+
if (!pd_codes) throw std::runtime_error("pd_codes is null");
|
|
3416
|
+
std::vector<std::pair<std::string, kh::PDCode> > parsed = kh::parsePDDocument(pd_codes, "ctypes-batch");
|
|
3417
|
+
if (parsed.empty()) throw std::runtime_error("no PD code found");
|
|
3418
|
+
|
|
3419
|
+
CppkhOptionsGuard guard;
|
|
3420
|
+
kh::g_options.progress = false;
|
|
3421
|
+
kh::g_options.profile = false;
|
|
3422
|
+
kh::g_options.simplifyPD = simplify_pd != 0;
|
|
3423
|
+
kh::g_options.reorderCrossings = reorder_crossings != 0;
|
|
3424
|
+
|
|
3425
|
+
std::ostringstream out;
|
|
3426
|
+
for (size_t i = 0; i < parsed.size(); ++i) {
|
|
3427
|
+
if (i) out << "\n";
|
|
3428
|
+
out << kh::computePD(parsed[i].second);
|
|
3429
|
+
}
|
|
3430
|
+
return cppkhDuplicateString(out.str());
|
|
3431
|
+
} catch (const std::exception& e) {
|
|
3432
|
+
g_cppkhLastError = e.what();
|
|
3433
|
+
return nullptr;
|
|
3434
|
+
} catch (...) {
|
|
3435
|
+
g_cppkhLastError = "unknown error";
|
|
3436
|
+
return nullptr;
|
|
3437
|
+
}
|
|
3438
|
+
}
|
|
3439
|
+
|
|
3440
|
+
CPPKH_API char* cppkh_compute_pd_batch(const char* pd_codes) {
|
|
3441
|
+
return cppkh_compute_pd_batch_ex(pd_codes, 1, 1);
|
|
3442
|
+
}
|
|
3443
|
+
|
|
3412
3444
|
CPPKH_API char* cppkh_simplify_pd(const char* pd_code) {
|
|
3413
3445
|
try {
|
|
3414
3446
|
g_cppkhLastError.clear();
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
4
|
import ast
|
|
5
|
+
import contextlib
|
|
5
6
|
import hashlib
|
|
6
7
|
import os
|
|
7
8
|
import pathlib
|
|
@@ -22,6 +23,7 @@ import pd_code_sanity
|
|
|
22
23
|
|
|
23
24
|
PathLike = Union[str, os.PathLike]
|
|
24
25
|
PdInput = Union[str, Sequence[Sequence[int]]]
|
|
26
|
+
PdManyInput = Union[str, Sequence[PdInput]]
|
|
25
27
|
UNKNOT_RESULT = "q^-1*t^0*Z[0] + q^1*t^0*Z[0]"
|
|
26
28
|
|
|
27
29
|
|
|
@@ -88,9 +90,37 @@ def normalize_pd_code(pd_code: PdInput) -> str:
|
|
|
88
90
|
return _format_pd(_as_crossings(pd_code))
|
|
89
91
|
|
|
90
92
|
|
|
93
|
+
def normalize_pd_codes(pd_codes: PdManyInput) -> str:
|
|
94
|
+
"""Normalize one or more PD codes into a newline-separated PD document."""
|
|
95
|
+
|
|
96
|
+
if isinstance(pd_codes, str):
|
|
97
|
+
return pd_codes.strip()
|
|
98
|
+
return "\n".join(normalize_pd_code(pd_code) for pd_code in pd_codes)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@contextlib.contextmanager
|
|
91
102
|
def _resource_source_path():
|
|
92
103
|
source = resources.files("cppkh_interface") / "data" / "src" / "main.cpp"
|
|
93
|
-
|
|
104
|
+
try:
|
|
105
|
+
with resources.as_file(source) as resource_path:
|
|
106
|
+
if pathlib.Path(resource_path).exists():
|
|
107
|
+
yield pathlib.Path(resource_path)
|
|
108
|
+
return
|
|
109
|
+
except FileNotFoundError:
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
current = pathlib.Path(__file__).resolve()
|
|
113
|
+
for parent in current.parents:
|
|
114
|
+
candidate = parent / "src" / "main.cpp"
|
|
115
|
+
if candidate.exists():
|
|
116
|
+
yield candidate
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
raise CppkhInterfaceError(
|
|
120
|
+
"cppkh C++ source was not found. Installed wheels include it under "
|
|
121
|
+
"cppkh_interface/data/src/main.cpp; editable checkouts use the "
|
|
122
|
+
"repository root src/main.cpp."
|
|
123
|
+
)
|
|
94
124
|
|
|
95
125
|
|
|
96
126
|
def _cache_dir() -> pathlib.Path:
|
|
@@ -217,17 +247,19 @@ def get_cppkh_executable() -> pathlib.Path:
|
|
|
217
247
|
return compile_cppkh()
|
|
218
248
|
|
|
219
249
|
|
|
220
|
-
def
|
|
250
|
+
def _run_cppkh_document(
|
|
221
251
|
pd_text: str,
|
|
222
252
|
*,
|
|
223
253
|
encoding: Optional[str] = None,
|
|
224
254
|
threads: Union[str, int] = "1",
|
|
255
|
+
simplify_pd: bool = False,
|
|
225
256
|
print_simplified_pd: bool = False,
|
|
226
|
-
) -> str:
|
|
257
|
+
) -> list[str]:
|
|
227
258
|
exe = compile_cppkh()
|
|
228
259
|
with tempfile.NamedTemporaryFile("w", suffix=".pd", encoding="utf-8", delete=False) as handle:
|
|
229
260
|
handle.write(pd_text)
|
|
230
|
-
|
|
261
|
+
if pd_text and not pd_text.endswith("\n"):
|
|
262
|
+
handle.write("\n")
|
|
231
263
|
pd_file = handle.name
|
|
232
264
|
|
|
233
265
|
command = [
|
|
@@ -237,8 +269,9 @@ def _run_cppkh(
|
|
|
237
269
|
"--quiet",
|
|
238
270
|
"--threads",
|
|
239
271
|
str(threads),
|
|
240
|
-
"--no-simplify-pd",
|
|
241
272
|
]
|
|
273
|
+
if not simplify_pd:
|
|
274
|
+
command.append("--no-simplify-pd")
|
|
242
275
|
if print_simplified_pd:
|
|
243
276
|
command.append("--print-simplified-pd")
|
|
244
277
|
|
|
@@ -270,12 +303,32 @@ def _run_cppkh(
|
|
|
270
303
|
raise CppkhInterfaceError(detail or f"cppkh exited with code {result.returncode}")
|
|
271
304
|
|
|
272
305
|
if print_simplified_pd:
|
|
273
|
-
return result.stdout.strip()
|
|
306
|
+
return [line.strip() for line in result.stdout.splitlines() if line.strip()]
|
|
274
307
|
|
|
275
308
|
matches = re.findall(r'"([^"]*)"', result.stdout)
|
|
276
309
|
if not matches:
|
|
277
310
|
raise CppkhInterfaceError(f"result not found in cppkh output: {result.stdout!r}")
|
|
278
|
-
return matches
|
|
311
|
+
return matches
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _run_cppkh(
|
|
315
|
+
pd_text: str,
|
|
316
|
+
*,
|
|
317
|
+
encoding: Optional[str] = None,
|
|
318
|
+
threads: Union[str, int] = "1",
|
|
319
|
+
simplify_pd: bool = False,
|
|
320
|
+
print_simplified_pd: bool = False,
|
|
321
|
+
) -> str:
|
|
322
|
+
results = _run_cppkh_document(
|
|
323
|
+
pd_text,
|
|
324
|
+
encoding=encoding,
|
|
325
|
+
threads=threads,
|
|
326
|
+
simplify_pd=simplify_pd,
|
|
327
|
+
print_simplified_pd=print_simplified_pd,
|
|
328
|
+
)
|
|
329
|
+
if len(results) != 1:
|
|
330
|
+
raise CppkhInterfaceError(f"expected exactly one result, got {len(results)}")
|
|
331
|
+
return results[0]
|
|
279
332
|
|
|
280
333
|
|
|
281
334
|
def _prepare_crossings(pd_code: PdInput, de_r1: bool, de_k8: bool) -> list[list[int]]:
|
|
@@ -288,6 +341,29 @@ def _prepare_crossings(pd_code: PdInput, de_r1: bool, de_k8: bool) -> list[list[
|
|
|
288
341
|
return crossings
|
|
289
342
|
|
|
290
343
|
|
|
344
|
+
def _prepare_many_for_cppkh(
|
|
345
|
+
pd_codes: PdManyInput,
|
|
346
|
+
*,
|
|
347
|
+
de_r1: bool,
|
|
348
|
+
de_k8: bool,
|
|
349
|
+
) -> tuple[str, bool]:
|
|
350
|
+
if de_r1 != de_k8:
|
|
351
|
+
raise ValueError(
|
|
352
|
+
"cppkh batch mode supports de_r1 and de_k8 only as a pair. "
|
|
353
|
+
"Use both True for backend R1+nugatory simplification or both False for raw PD input."
|
|
354
|
+
)
|
|
355
|
+
if isinstance(pd_codes, str):
|
|
356
|
+
return pd_codes.strip(), bool(de_r1 and de_k8)
|
|
357
|
+
|
|
358
|
+
prepared = []
|
|
359
|
+
use_cpp_simplify = bool(de_r1 and de_k8)
|
|
360
|
+
for pd_code in pd_codes:
|
|
361
|
+
crossings = _as_crossings(pd_code)
|
|
362
|
+
_check_sanity(crossings)
|
|
363
|
+
prepared.append(_format_pd(crossings))
|
|
364
|
+
return "\n".join(prepared), use_cpp_simplify
|
|
365
|
+
|
|
366
|
+
|
|
291
367
|
def solve_khovanov(
|
|
292
368
|
pd_code: PdInput,
|
|
293
369
|
encoding: Optional[str] = None,
|
|
@@ -297,6 +373,24 @@ def solve_khovanov(
|
|
|
297
373
|
) -> str:
|
|
298
374
|
"""Compute Khovanov homology with a javakh-interface compatible signature."""
|
|
299
375
|
|
|
376
|
+
if de_r1 == de_k8:
|
|
377
|
+
crossings = _as_crossings(pd_code)
|
|
378
|
+
_check_sanity(crossings)
|
|
379
|
+
if show_real_pdcode:
|
|
380
|
+
if de_r1:
|
|
381
|
+
simplified = _run_cppkh_document(
|
|
382
|
+
_format_pd(crossings),
|
|
383
|
+
encoding=encoding,
|
|
384
|
+
simplify_pd=True,
|
|
385
|
+
print_simplified_pd=True,
|
|
386
|
+
)
|
|
387
|
+
print(f"Real PD code after de_r1 and de_k8: {simplified[0] if simplified else ''}")
|
|
388
|
+
else:
|
|
389
|
+
print(f"Real PD code after de_r1 and de_k8: {crossings}")
|
|
390
|
+
if crossings == []:
|
|
391
|
+
return UNKNOT_RESULT
|
|
392
|
+
return _run_cppkh(_format_pd(crossings), encoding=encoding, simplify_pd=de_r1)
|
|
393
|
+
|
|
300
394
|
crossings = _prepare_crossings(pd_code, de_r1=de_r1, de_k8=de_k8)
|
|
301
395
|
if show_real_pdcode:
|
|
302
396
|
print(f"Real PD code after de_r1 and de_k8: {crossings}")
|
|
@@ -305,6 +399,39 @@ def solve_khovanov(
|
|
|
305
399
|
return _run_cppkh(_format_pd(crossings), encoding=encoding)
|
|
306
400
|
|
|
307
401
|
|
|
402
|
+
def solve_many_khovanov(
|
|
403
|
+
pd_codes: PdManyInput,
|
|
404
|
+
encoding: Optional[str] = None,
|
|
405
|
+
de_r1: bool = True,
|
|
406
|
+
de_k8: bool = True,
|
|
407
|
+
show_real_pdcode: bool = False,
|
|
408
|
+
threads: Union[str, int] = "1",
|
|
409
|
+
) -> list[str]:
|
|
410
|
+
"""Compute many PD codes in one cppkh process.
|
|
411
|
+
|
|
412
|
+
With the default ``de_r1=True`` and ``de_k8=True`` settings, the raw PD
|
|
413
|
+
document is passed directly to cppkh and the C++ simplifier handles R1 then
|
|
414
|
+
nugatory crossing removal for the whole batch.
|
|
415
|
+
"""
|
|
416
|
+
|
|
417
|
+
document, use_cpp_simplify = _prepare_many_for_cppkh(pd_codes, de_r1=de_r1, de_k8=de_k8)
|
|
418
|
+
if show_real_pdcode:
|
|
419
|
+
if use_cpp_simplify:
|
|
420
|
+
simplified = _run_cppkh_document(
|
|
421
|
+
document,
|
|
422
|
+
encoding=encoding,
|
|
423
|
+
threads=threads,
|
|
424
|
+
simplify_pd=True,
|
|
425
|
+
print_simplified_pd=True,
|
|
426
|
+
)
|
|
427
|
+
print(f"Real PD code after de_r1 and de_k8: {simplified}")
|
|
428
|
+
else:
|
|
429
|
+
print(f"Real PD code after de_r1 and de_k8: {document.splitlines()}")
|
|
430
|
+
if not document:
|
|
431
|
+
return []
|
|
432
|
+
return _run_cppkh_document(document, encoding=encoding, threads=threads, simplify_pd=use_cpp_simplify)
|
|
433
|
+
|
|
434
|
+
|
|
308
435
|
def compute_pd(
|
|
309
436
|
pd_code: PdInput,
|
|
310
437
|
*,
|
|
@@ -316,6 +443,25 @@ def compute_pd(
|
|
|
316
443
|
) -> str:
|
|
317
444
|
"""Compute Khovanov homology using the same defaults as solve_khovanov."""
|
|
318
445
|
|
|
446
|
+
if de_r1 == de_k8:
|
|
447
|
+
crossings = _as_crossings(pd_code)
|
|
448
|
+
_check_sanity(crossings)
|
|
449
|
+
if show_real_pdcode:
|
|
450
|
+
if de_r1:
|
|
451
|
+
simplified = _run_cppkh_document(
|
|
452
|
+
_format_pd(crossings),
|
|
453
|
+
encoding=encoding,
|
|
454
|
+
threads=threads,
|
|
455
|
+
simplify_pd=True,
|
|
456
|
+
print_simplified_pd=True,
|
|
457
|
+
)
|
|
458
|
+
print(f"Real PD code after de_r1 and de_k8: {simplified[0] if simplified else ''}")
|
|
459
|
+
else:
|
|
460
|
+
print(f"Real PD code after de_r1 and de_k8: {crossings}")
|
|
461
|
+
if crossings == []:
|
|
462
|
+
return UNKNOT_RESULT
|
|
463
|
+
return _run_cppkh(_format_pd(crossings), encoding=encoding, threads=threads, simplify_pd=de_r1)
|
|
464
|
+
|
|
319
465
|
crossings = _prepare_crossings(pd_code, de_r1=de_r1, de_k8=de_k8)
|
|
320
466
|
if show_real_pdcode:
|
|
321
467
|
print(f"Real PD code after de_r1 and de_k8: {crossings}")
|
|
@@ -324,6 +470,27 @@ def compute_pd(
|
|
|
324
470
|
return _run_cppkh(_format_pd(crossings), encoding=encoding, threads=threads)
|
|
325
471
|
|
|
326
472
|
|
|
473
|
+
def compute_many_pd(
|
|
474
|
+
pd_codes: PdManyInput,
|
|
475
|
+
*,
|
|
476
|
+
encoding: Optional[str] = None,
|
|
477
|
+
de_r1: bool = True,
|
|
478
|
+
de_k8: bool = True,
|
|
479
|
+
show_real_pdcode: bool = False,
|
|
480
|
+
threads: Union[str, int] = "1",
|
|
481
|
+
) -> list[str]:
|
|
482
|
+
"""Compute many PD codes in one cached cppkh executable invocation."""
|
|
483
|
+
|
|
484
|
+
return solve_many_khovanov(
|
|
485
|
+
pd_codes,
|
|
486
|
+
encoding=encoding,
|
|
487
|
+
de_r1=de_r1,
|
|
488
|
+
de_k8=de_k8,
|
|
489
|
+
show_real_pdcode=show_real_pdcode,
|
|
490
|
+
threads=threads,
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
|
|
327
494
|
def simplify_pd(pd_code: PdInput, *, de_r1: bool = True, de_k8: bool = True) -> str:
|
|
328
495
|
"""Return the normalized PD string after optional R1 and nugatory simplification."""
|
|
329
496
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "cppkh-interface"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.1"
|
|
4
4
|
description = "Python interface for cppkh Khovanov homology computation with runtime C++ compilation."
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "GGN_2015", email = "neko@jlulug.org"}
|
|
@@ -26,9 +26,11 @@ cppkh-interface = "cppkh_interface.main:main"
|
|
|
26
26
|
[tool.poetry]
|
|
27
27
|
packages = [{ include = "cppkh_interface" }]
|
|
28
28
|
include = [
|
|
29
|
+
{ path = "build_backend/cppkh_interface_build_backend.py", format = ["sdist"] },
|
|
29
30
|
{ path = "cppkh_interface/data/src/main.cpp", format = ["sdist", "wheel"] }
|
|
30
31
|
]
|
|
31
32
|
|
|
32
33
|
[build-system]
|
|
33
|
-
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
34
|
-
build-backend = "
|
|
34
|
+
requires = ["poetry-core>=2.0.0,<3.0.0", "setuptools>=61"]
|
|
35
|
+
build-backend = "cppkh_interface_build_backend"
|
|
36
|
+
backend-path = ["build_backend"]
|
|
File without changes
|
|
File without changes
|