cppkh-interface 0.1.0__tar.gz → 0.1.2__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.2}/PKG-INFO +16 -3
- {cppkh_interface-0.1.0 → cppkh_interface-0.1.2}/README.md +15 -2
- cppkh_interface-0.1.2/build_backend/cppkh_interface_build_backend.py +207 -0
- {cppkh_interface-0.1.0 → cppkh_interface-0.1.2}/cppkh_interface/__init__.py +6 -0
- {cppkh_interface-0.1.0 → cppkh_interface-0.1.2}/cppkh_interface/data/src/main.cpp +189 -56
- {cppkh_interface-0.1.0 → cppkh_interface-0.1.2}/cppkh_interface/main.py +174 -7
- {cppkh_interface-0.1.0 → cppkh_interface-0.1.2}/pyproject.toml +5 -3
- {cppkh_interface-0.1.0 → cppkh_interface-0.1.2}/cppkh_interface/__main__.py +0 -0
- {cppkh_interface-0.1.0 → cppkh_interface-0.1.2}/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.2
|
|
4
4
|
Summary: Python interface for cppkh Khovanov homology computation with runtime C++ compilation.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Author: GGN_2015
|
|
@@ -26,6 +26,10 @@ Description-Content-Type: text/markdown
|
|
|
26
26
|
`cppkh-interface` is a Python package for computing integer Khovanov homology
|
|
27
27
|
with the C++ `cppkh` implementation.
|
|
28
28
|
|
|
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.
|
|
32
|
+
|
|
29
33
|
The package is compatible with the main `javakh-interface` function:
|
|
30
34
|
|
|
31
35
|
```python
|
|
@@ -33,11 +37,18 @@ import cppkh_interface
|
|
|
33
37
|
|
|
34
38
|
pd_code = [[1, 5, 2, 4], [3, 1, 4, 6], [5, 3, 6, 2]]
|
|
35
39
|
print(cppkh_interface.solve_khovanov(pd_code, de_r1=True, de_k8=True))
|
|
40
|
+
print(cppkh_interface.solve_many_khovanov([pd_code, pd_code]))
|
|
36
41
|
```
|
|
37
42
|
|
|
38
43
|
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
|
|
44
|
+
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.
|
|
47
|
+
|
|
48
|
+
In the repository checkout, the package does not keep a committed backup copy
|
|
49
|
+
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.
|
|
41
52
|
|
|
42
53
|
## Install
|
|
43
54
|
|
|
@@ -68,6 +79,8 @@ poetry build
|
|
|
68
79
|
poetry publish
|
|
69
80
|
```
|
|
70
81
|
|
|
82
|
+
Use `poetry publish --build` to build and upload in one command.
|
|
83
|
+
|
|
71
84
|
For local testing:
|
|
72
85
|
|
|
73
86
|
```sh
|
|
@@ -3,6 +3,10 @@
|
|
|
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.
|
|
9
|
+
|
|
6
10
|
The package is compatible with the main `javakh-interface` function:
|
|
7
11
|
|
|
8
12
|
```python
|
|
@@ -10,11 +14,18 @@ import cppkh_interface
|
|
|
10
14
|
|
|
11
15
|
pd_code = [[1, 5, 2, 4], [3, 1, 4, 6], [5, 3, 6, 2]]
|
|
12
16
|
print(cppkh_interface.solve_khovanov(pd_code, de_r1=True, de_k8=True))
|
|
17
|
+
print(cppkh_interface.solve_many_khovanov([pd_code, pd_code]))
|
|
13
18
|
```
|
|
14
19
|
|
|
15
20
|
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
|
|
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.
|
|
24
|
+
|
|
25
|
+
In the repository checkout, the package does not keep a committed backup copy
|
|
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.
|
|
18
29
|
|
|
19
30
|
## Install
|
|
20
31
|
|
|
@@ -45,6 +56,8 @@ poetry build
|
|
|
45
56
|
poetry publish
|
|
46
57
|
```
|
|
47
58
|
|
|
59
|
+
Use `poetry publish --build` to build and upload in one command.
|
|
60
|
+
|
|
48
61
|
For local testing:
|
|
49
62
|
|
|
50
63
|
```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
|
]
|
|
@@ -2758,9 +2758,12 @@ std::string Komplex::KhForZ() {
|
|
|
2758
2758
|
return ret;
|
|
2759
2759
|
}
|
|
2760
2760
|
|
|
2761
|
-
static int chooseXingRecursive(const std::vector<int>& edges, const std::vector<std::vector<int> >& pd,
|
|
2762
|
-
std::vector<char>& in, std::vector<char>& done, int depth, std::vector<int>& retmax) {
|
|
2763
|
-
|
|
2761
|
+
static int chooseXingRecursive(const std::vector<int>& edges, const std::vector<std::vector<int> >& pd,
|
|
2762
|
+
std::vector<char>& in, std::vector<char>& done, int depth, std::vector<int>& retmax) {
|
|
2763
|
+
// Choose the next crossing for the growing tangle. The heuristic maximizes
|
|
2764
|
+
// already-attached boundary arcs, with a short lookahead to keep later
|
|
2765
|
+
// intermediate girth smaller. It affects runtime, not the homology result.
|
|
2766
|
+
int nedges = static_cast<int>(edges.size());
|
|
2764
2767
|
int best = -1, nconbest = -1;
|
|
2765
2768
|
std::vector<int> rbest(depth, 0);
|
|
2766
2769
|
for (size_t i = 0; i < pd.size(); ++i) if (!done[i]) {
|
|
@@ -2819,15 +2822,91 @@ static int takeNextCrossing(const std::vector<int>&, const std::vector<std::vect
|
|
|
2819
2822
|
throw std::runtime_error("no crossing left");
|
|
2820
2823
|
}
|
|
2821
2824
|
|
|
2822
|
-
static std::vector<int> getSigns(const std::vector<std::vector<int> >& pd) {
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2825
|
+
static std::vector<int> getSigns(const std::vector<std::vector<int> >& pd) {
|
|
2826
|
+
const int nodeCount = static_cast<int>(pd.size() * 4);
|
|
2827
|
+
if (nodeCount == 0) return std::vector<int>();
|
|
2828
|
+
|
|
2829
|
+
int maxLabel = -1;
|
|
2830
|
+
for (size_t i = 0; i < pd.size(); ++i) {
|
|
2831
|
+
if (pd[i].size() != 4) throw std::runtime_error("PD crossing must have four entries");
|
|
2832
|
+
for (int label : pd[i]) {
|
|
2833
|
+
if (label < 0) throw std::runtime_error("PD arc labels must be positive");
|
|
2834
|
+
maxLabel = std::max(maxLabel, label);
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
std::vector<int> first(maxLabel + 1, -1), second(maxLabel + 1, -1);
|
|
2839
|
+
for (size_t i = 0; i < pd.size(); ++i) {
|
|
2840
|
+
for (int slot = 0; slot < 4; ++slot) {
|
|
2841
|
+
int label = pd[i][slot];
|
|
2842
|
+
int node = static_cast<int>(4 * i) + slot;
|
|
2843
|
+
if (first[label] == -1) first[label] = node;
|
|
2844
|
+
else if (second[label] == -1) second[label] = node;
|
|
2845
|
+
else throw std::runtime_error("PD arc label appears more than twice");
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
|
|
2849
|
+
std::vector<int> other(nodeCount, -1);
|
|
2850
|
+
for (int label = 0; label <= maxLabel; ++label) {
|
|
2851
|
+
if (first[label] == -1) continue;
|
|
2852
|
+
if (second[label] == -1) throw std::runtime_error("PD arc label does not appear twice");
|
|
2853
|
+
other[first[label]] = second[label];
|
|
2854
|
+
other[second[label]] = first[label];
|
|
2855
|
+
}
|
|
2856
|
+
|
|
2857
|
+
// 1 means that this edge leaves the crossing at this incidence, and 0
|
|
2858
|
+
// means that it enters. Opposite incidences at a crossing and the two
|
|
2859
|
+
// incidences of one edge always have opposite directions.
|
|
2860
|
+
std::vector<signed char> outgoing(nodeCount, -1);
|
|
2861
|
+
std::vector<int> queue;
|
|
2862
|
+
queue.reserve(nodeCount);
|
|
2863
|
+
auto orientComponent = [&](int seed, signed char direction) {
|
|
2864
|
+
if (outgoing[seed] != -1) {
|
|
2865
|
+
if (outgoing[seed] != direction) throw std::runtime_error("inconsistent PD orientation");
|
|
2866
|
+
return;
|
|
2867
|
+
}
|
|
2868
|
+
queue.clear();
|
|
2869
|
+
outgoing[seed] = direction;
|
|
2870
|
+
queue.push_back(seed);
|
|
2871
|
+
for (size_t head = 0; head < queue.size(); ++head) {
|
|
2872
|
+
int node = queue[head];
|
|
2873
|
+
int crossing = node / 4;
|
|
2874
|
+
int slot = node % 4;
|
|
2875
|
+
int neighbors[2] = {4 * crossing + ((slot + 2) % 4), other[node]};
|
|
2876
|
+
for (int next : neighbors) {
|
|
2877
|
+
if (next < 0) throw std::runtime_error("broken PD edge incidence");
|
|
2878
|
+
signed char nextDirection = static_cast<signed char>(1 - outgoing[node]);
|
|
2879
|
+
if (outgoing[next] == -1) {
|
|
2880
|
+
outgoing[next] = nextDirection;
|
|
2881
|
+
queue.push_back(next);
|
|
2882
|
+
} else if (outgoing[next] != nextDirection) {
|
|
2883
|
+
throw std::runtime_error("inconsistent PD orientation");
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
};
|
|
2888
|
+
|
|
2889
|
+
// In SageMath's PD convention slot 0 is the incoming under-edge and slot
|
|
2890
|
+
// 2 is the outgoing under-edge. These seeds orient every component that
|
|
2891
|
+
// passes under at least once.
|
|
2892
|
+
for (size_t i = 0; i < pd.size(); ++i) orientComponent(static_cast<int>(4 * i + 2), 1);
|
|
2893
|
+
|
|
2894
|
+
// A component which is over at every crossing has no orientation encoded
|
|
2895
|
+
// by the PD tuples. Match SageMath's deterministic traversal by taking the
|
|
2896
|
+
// first occurrence of the smallest still-unoriented edge as a tail.
|
|
2897
|
+
for (int label = 0; label <= maxLabel; ++label) {
|
|
2898
|
+
if (first[label] != -1 && outgoing[first[label]] == -1) orientComponent(first[label], 1);
|
|
2899
|
+
}
|
|
2900
|
+
|
|
2901
|
+
std::vector<int> xsigns(pd.size());
|
|
2902
|
+
for (size_t i = 0; i < pd.size(); ++i) {
|
|
2903
|
+
const std::vector<int>& crossing = pd[i];
|
|
2904
|
+
if (crossing[0] == crossing[3] || crossing[2] == crossing[1]) xsigns[i] = -1;
|
|
2905
|
+
else if (crossing[3] == crossing[2] || crossing[0] == crossing[1]) xsigns[i] = 1;
|
|
2906
|
+
else xsigns[i] = outgoing[4 * i + 3] ? -1 : 1;
|
|
2907
|
+
}
|
|
2908
|
+
return xsigns;
|
|
2909
|
+
}
|
|
2831
2910
|
|
|
2832
2911
|
static bool sanityPD(const PDCode& pd) {
|
|
2833
2912
|
std::map<int, int> counts;
|
|
@@ -2910,15 +2989,17 @@ static PDCode renumberR1Order(PDCode pd) {
|
|
|
2910
2989
|
return pd;
|
|
2911
2990
|
}
|
|
2912
2991
|
|
|
2913
|
-
static PDCode eraseR1(PDCode pd) {
|
|
2914
|
-
if (!sanityPD(pd)) throw std::runtime_error("invalid PD code: every arc label must appear exactly twice");
|
|
2915
|
-
|
|
2992
|
+
static PDCode eraseR1(PDCode pd, std::vector<int>* signs) {
|
|
2993
|
+
if (!sanityPD(pd)) throw std::runtime_error("invalid PD code: every arc label must appear exactly twice");
|
|
2994
|
+
if (signs && signs->size() != pd.size()) throw std::runtime_error("crossing sign count does not match PD code");
|
|
2995
|
+
bool hasR1 = true;
|
|
2916
2996
|
while (hasR1) {
|
|
2917
2997
|
hasR1 = false;
|
|
2918
2998
|
for (size_t i = 0; i < pd.size(); ++i) {
|
|
2919
2999
|
if (uniqueCount(pd[i]) <= 3) {
|
|
2920
|
-
std::vector<int> crossing = pd[i];
|
|
2921
|
-
pd.erase(pd.begin() + static_cast<std::ptrdiff_t>(i));
|
|
3000
|
+
std::vector<int> crossing = pd[i];
|
|
3001
|
+
pd.erase(pd.begin() + static_cast<std::ptrdiff_t>(i));
|
|
3002
|
+
if (signs) signs->erase(signs->begin() + static_cast<std::ptrdiff_t>(i));
|
|
2922
3003
|
std::vector<int> singles;
|
|
2923
3004
|
for (int v : crossing) {
|
|
2924
3005
|
if (std::count(crossing.begin(), crossing.end(), v) == 1) singles.push_back(v);
|
|
@@ -3073,8 +3154,9 @@ static PDCode renumberFullDfs(PDCode pd) {
|
|
|
3073
3154
|
return pd;
|
|
3074
3155
|
}
|
|
3075
3156
|
|
|
3076
|
-
static PDCode eraseOneNugatory(PDCode pd, size_t index) {
|
|
3077
|
-
if (uniqueCount(pd[index]) != 4) throw std::runtime_error("nugatory erase requires R1-free PD code");
|
|
3157
|
+
static PDCode eraseOneNugatory(PDCode pd, size_t index, std::vector<int>* signs) {
|
|
3158
|
+
if (uniqueCount(pd[index]) != 4) throw std::runtime_error("nugatory erase requires R1-free PD code");
|
|
3159
|
+
if (signs && signs->size() != pd.size()) throw std::runtime_error("crossing sign count does not match PD code");
|
|
3078
3160
|
int ax = pd[index][0];
|
|
3079
3161
|
int bx = pd[index][1];
|
|
3080
3162
|
int cx = pd[index][2];
|
|
@@ -3096,22 +3178,23 @@ static PDCode eraseOneNugatory(PDCode pd, size_t index) {
|
|
|
3096
3178
|
if (!loopSet.count(ax) || !loopSet.count(bx) || !loopSet.count(cx) || !loopSet.count(dx)) {
|
|
3097
3179
|
throw std::runtime_error("nugatory crossing arcs are not in one component");
|
|
3098
3180
|
}
|
|
3099
|
-
PDCode bad = pd;
|
|
3100
|
-
bad.erase(bad.begin() + static_cast<std::ptrdiff_t>(index));
|
|
3181
|
+
PDCode bad = pd;
|
|
3182
|
+
bad.erase(bad.begin() + static_cast<std::ptrdiff_t>(index));
|
|
3183
|
+
if (signs) signs->erase(signs->begin() + static_cast<std::ptrdiff_t>(index));
|
|
3101
3184
|
bad = replaceArcValue(bad, ax, cx);
|
|
3102
3185
|
bad = replaceArcValue(bad, dx, bx);
|
|
3103
3186
|
return renumberFullDfs(bad);
|
|
3104
3187
|
}
|
|
3105
3188
|
|
|
3106
|
-
static PDCode simplifyPDCode(PDCode pd) {
|
|
3107
|
-
pd = eraseR1(pd);
|
|
3108
|
-
while (true) {
|
|
3109
|
-
int index = findNugatory(pd);
|
|
3110
|
-
if (index < 0) break;
|
|
3111
|
-
pd = eraseOneNugatory(pd, static_cast<size_t>(index));
|
|
3112
|
-
}
|
|
3113
|
-
return pd;
|
|
3114
|
-
}
|
|
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);
|
|
3195
|
+
}
|
|
3196
|
+
return pd;
|
|
3197
|
+
}
|
|
3115
3198
|
|
|
3116
3199
|
static int komplexSize(const Komplex& k) {
|
|
3117
3200
|
int total = 0;
|
|
@@ -3119,8 +3202,8 @@ static int komplexSize(const Komplex& k) {
|
|
|
3119
3202
|
return total;
|
|
3120
3203
|
}
|
|
3121
3204
|
|
|
3122
|
-
static Komplex generateFast(const std::vector<std::vector<int> >& pd, const std::vector<int>& xsigns) {
|
|
3123
|
-
KH_PROFILE(generateFast);
|
|
3205
|
+
static Komplex generateFast(const std::vector<std::vector<int> >& pd, const std::vector<int>& xsigns) {
|
|
3206
|
+
KH_PROFILE(generateFast);
|
|
3124
3207
|
if (pd.empty()) {
|
|
3125
3208
|
Komplex kom(1);
|
|
3126
3209
|
kom.columns[0] = SmoothingColumn(1);
|
|
@@ -3128,11 +3211,13 @@ static Komplex generateFast(const std::vector<std::vector<int> >& pd, const std:
|
|
|
3128
3211
|
kom.reduce();
|
|
3129
3212
|
return kom;
|
|
3130
3213
|
}
|
|
3131
|
-
std::vector<char> in(pd.size() * 4, 0), done(pd.size(), 0);
|
|
3132
|
-
std::vector<std::vector<int> > pd1(1, std::vector<int>{0,1,2,3});
|
|
3133
|
-
Komplex kplus(pd1, std::vector<int>{1}, 4);
|
|
3134
|
-
Komplex kminus(pd1, std::vector<int>{-1}, 4);
|
|
3135
|
-
|
|
3214
|
+
std::vector<char> in(pd.size() * 4, 0), done(pd.size(), 0);
|
|
3215
|
+
std::vector<std::vector<int> > pd1(1, std::vector<int>{0,1,2,3});
|
|
3216
|
+
Komplex kplus(pd1, std::vector<int>{1}, 4);
|
|
3217
|
+
Komplex kminus(pd1, std::vector<int>{-1}, 4);
|
|
3218
|
+
// Build the full complex by composing one crossing complex at a time and
|
|
3219
|
+
// reducing after every composition, following JavaKh's tangle-growth path.
|
|
3220
|
+
std::vector<int> edges;
|
|
3136
3221
|
int firstdepth = pd.size() > 4 ? 3 : static_cast<int>(pd.size()) - 1;
|
|
3137
3222
|
std::vector<int> firstdummy(firstdepth + 1, 0);
|
|
3138
3223
|
int first = g_options.reorderCrossings ? chooseXingRecursive(edges, pd, in, done, firstdepth, firstdummy)
|
|
@@ -3312,16 +3397,17 @@ static std::vector<std::string> listInputFiles(const std::string& dir) {
|
|
|
3312
3397
|
return files;
|
|
3313
3398
|
}
|
|
3314
3399
|
|
|
3315
|
-
static std::string computePD(const std::vector<std::vector<int> >& pd) {
|
|
3316
|
-
flushCobCache();
|
|
3317
|
-
g_smallArena.reset();
|
|
3318
|
-
|
|
3319
|
-
|
|
3400
|
+
static std::string computePD(const std::vector<std::vector<int> >& pd) {
|
|
3401
|
+
flushCobCache();
|
|
3402
|
+
g_smallArena.reset();
|
|
3403
|
+
std::vector<int> signs = getSigns(pd);
|
|
3404
|
+
PDCode working = g_options.simplifyPD ? simplifyPDCode(pd, &signs) : pd;
|
|
3405
|
+
Komplex k = generateFast(working, signs);
|
|
3320
3406
|
ProfileScope khScope(g_profile.kh);
|
|
3321
3407
|
return k.KhForZ();
|
|
3322
3408
|
}
|
|
3323
3409
|
|
|
3324
|
-
static std::string formatPDCode(const PDCode& pd) {
|
|
3410
|
+
static std::string formatPDCode(const PDCode& pd) {
|
|
3325
3411
|
std::ostringstream out;
|
|
3326
3412
|
out << "PD[";
|
|
3327
3413
|
for (size_t i = 0; i < pd.size(); ++i) {
|
|
@@ -3334,10 +3420,22 @@ static std::string formatPDCode(const PDCode& pd) {
|
|
|
3334
3420
|
out << "]";
|
|
3335
3421
|
}
|
|
3336
3422
|
out << "]";
|
|
3337
|
-
return out.str();
|
|
3338
|
-
}
|
|
3339
|
-
|
|
3340
|
-
static std::string
|
|
3423
|
+
return out.str();
|
|
3424
|
+
}
|
|
3425
|
+
|
|
3426
|
+
static std::string formatCrossingSigns(const PDCode& pd) {
|
|
3427
|
+
std::vector<int> signs = getSigns(pd);
|
|
3428
|
+
std::ostringstream out;
|
|
3429
|
+
out << "[";
|
|
3430
|
+
for (size_t i = 0; i < signs.size(); ++i) {
|
|
3431
|
+
if (i) out << ",";
|
|
3432
|
+
out << signs[i];
|
|
3433
|
+
}
|
|
3434
|
+
out << "]";
|
|
3435
|
+
return out.str();
|
|
3436
|
+
}
|
|
3437
|
+
|
|
3438
|
+
static std::string simplifiedPDString(const PDCode& pd) {
|
|
3341
3439
|
PDCode working = g_options.simplifyPD ? simplifyPDCode(pd) : pd;
|
|
3342
3440
|
return formatPDCode(working);
|
|
3343
3441
|
}
|
|
@@ -3409,6 +3507,38 @@ CPPKH_API char* cppkh_compute_pd(const char* pd_code) {
|
|
|
3409
3507
|
return cppkh_compute_pd_ex(pd_code, 1, 1);
|
|
3410
3508
|
}
|
|
3411
3509
|
|
|
3510
|
+
CPPKH_API char* cppkh_compute_pd_batch_ex(const char* pd_codes, int simplify_pd, int reorder_crossings) {
|
|
3511
|
+
try {
|
|
3512
|
+
g_cppkhLastError.clear();
|
|
3513
|
+
if (!pd_codes) throw std::runtime_error("pd_codes is null");
|
|
3514
|
+
std::vector<std::pair<std::string, kh::PDCode> > parsed = kh::parsePDDocument(pd_codes, "ctypes-batch");
|
|
3515
|
+
if (parsed.empty()) throw std::runtime_error("no PD code found");
|
|
3516
|
+
|
|
3517
|
+
CppkhOptionsGuard guard;
|
|
3518
|
+
kh::g_options.progress = false;
|
|
3519
|
+
kh::g_options.profile = false;
|
|
3520
|
+
kh::g_options.simplifyPD = simplify_pd != 0;
|
|
3521
|
+
kh::g_options.reorderCrossings = reorder_crossings != 0;
|
|
3522
|
+
|
|
3523
|
+
std::ostringstream out;
|
|
3524
|
+
for (size_t i = 0; i < parsed.size(); ++i) {
|
|
3525
|
+
if (i) out << "\n";
|
|
3526
|
+
out << kh::computePD(parsed[i].second);
|
|
3527
|
+
}
|
|
3528
|
+
return cppkhDuplicateString(out.str());
|
|
3529
|
+
} catch (const std::exception& e) {
|
|
3530
|
+
g_cppkhLastError = e.what();
|
|
3531
|
+
return nullptr;
|
|
3532
|
+
} catch (...) {
|
|
3533
|
+
g_cppkhLastError = "unknown error";
|
|
3534
|
+
return nullptr;
|
|
3535
|
+
}
|
|
3536
|
+
}
|
|
3537
|
+
|
|
3538
|
+
CPPKH_API char* cppkh_compute_pd_batch(const char* pd_codes) {
|
|
3539
|
+
return cppkh_compute_pd_batch_ex(pd_codes, 1, 1);
|
|
3540
|
+
}
|
|
3541
|
+
|
|
3412
3542
|
CPPKH_API char* cppkh_simplify_pd(const char* pd_code) {
|
|
3413
3543
|
try {
|
|
3414
3544
|
g_cppkhLastError.clear();
|
|
@@ -3432,7 +3562,7 @@ CPPKH_API char* cppkh_simplify_pd(const char* pd_code) {
|
|
|
3432
3562
|
|
|
3433
3563
|
#ifndef CPPKH_SHARED_LIBRARY
|
|
3434
3564
|
static void usage() {
|
|
3435
|
-
std::cout << "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";
|
|
3436
3566
|
std::cout << "Thread backend: " << KH_THREAD_BACKEND_NAME << "\n";
|
|
3437
3567
|
std::cout << "Detected CPU threads: " << kh::detectHardwareThreads() << "\n";
|
|
3438
3568
|
std::cout << "PD simplification: R1 removal then nugatory crossing removal is enabled by default.\n";
|
|
@@ -3440,9 +3570,10 @@ static void usage() {
|
|
|
3440
3570
|
|
|
3441
3571
|
int main(int argc, char** argv) {
|
|
3442
3572
|
try {
|
|
3443
|
-
std::vector<std::string> files;
|
|
3444
|
-
std::vector<std::pair<std::string, std::vector<std::vector<int> > > > jobs;
|
|
3445
|
-
bool printSimplifiedPD = false;
|
|
3573
|
+
std::vector<std::string> files;
|
|
3574
|
+
std::vector<std::pair<std::string, std::vector<std::vector<int> > > > jobs;
|
|
3575
|
+
bool printSimplifiedPD = false;
|
|
3576
|
+
bool printCrossingSigns = false;
|
|
3446
3577
|
for (int i = 1; i < argc; ++i) {
|
|
3447
3578
|
std::string a = argv[i];
|
|
3448
3579
|
if (a == "--help" || a == "-h") { usage(); return 0; }
|
|
@@ -3461,8 +3592,9 @@ int main(int argc, char** argv) {
|
|
|
3461
3592
|
else if (a == "--quiet" || a == "-q") kh::g_options.progress = false;
|
|
3462
3593
|
else if (a == "--profile") kh::g_options.profile = true;
|
|
3463
3594
|
else if (a == "--no-simplify-pd" || a == "--raw-pd") kh::g_options.simplifyPD = false;
|
|
3464
|
-
else if (a == "--simplify-pd") kh::g_options.simplifyPD = true;
|
|
3465
|
-
else if (a == "--print-simplified-pd") printSimplifiedPD = true;
|
|
3595
|
+
else if (a == "--simplify-pd") kh::g_options.simplifyPD = true;
|
|
3596
|
+
else if (a == "--print-simplified-pd") printSimplifiedPD = true;
|
|
3597
|
+
else if (a == "--print-crossing-signs") printCrossingSigns = true;
|
|
3466
3598
|
else if (a == "--threads" || a == "-j") {
|
|
3467
3599
|
if (++i >= argc) throw std::runtime_error("--threads needs a number");
|
|
3468
3600
|
std::string v = argv[i];
|
|
@@ -3489,9 +3621,10 @@ int main(int argc, char** argv) {
|
|
|
3489
3621
|
kh::g_profile.reset();
|
|
3490
3622
|
profileStart = kh::profileNowNs();
|
|
3491
3623
|
}
|
|
3492
|
-
if (label) std::cout << jobs[i].first << "\t";
|
|
3493
|
-
if (printSimplifiedPD) std::cout << kh::simplifiedPDString(jobs[i].second) << "\n";
|
|
3494
|
-
else std::cout <<
|
|
3624
|
+
if (label) std::cout << jobs[i].first << "\t";
|
|
3625
|
+
if (printSimplifiedPD) std::cout << kh::simplifiedPDString(jobs[i].second) << "\n";
|
|
3626
|
+
else if (printCrossingSigns) std::cout << kh::formatCrossingSigns(jobs[i].second) << "\n";
|
|
3627
|
+
else std::cout << "\"" << kh::computePD(jobs[i].second) << "\"\n";
|
|
3495
3628
|
if (kh::g_options.profile) {
|
|
3496
3629
|
kh::g_profile.totalNs = kh::profileNowNs() - profileStart;
|
|
3497
3630
|
kh::printProfile();
|
|
@@ -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.2"
|
|
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
|