pd-code-connected-sum 0.0.3__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.
@@ -0,0 +1,67 @@
1
+ Metadata-Version: 2.4
2
+ Name: pd-code-connected-sum
3
+ Version: 0.1.1
4
+ Summary: make connected sum for link pd_code
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Author: GGN_2015
8
+ Author-email: neko@jlulug.org
9
+ Requires-Python: >=3.10
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Requires-Dist: pd-code-pre-nxt (>=0.1.0)
18
+ Requires-Dist: pd-code-sanity (>=0.1.0)
19
+ Description-Content-Type: text/markdown
20
+
21
+ # pd-code-connected-sum
22
+
23
+ Join two selected oriented PD-code components and canonically renumber the result.
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ pip install pd-code-connected-sum
29
+ ```
30
+
31
+ ## Usage example
32
+
33
+ ```python
34
+ from pd_code_connected_sum import connected_sum
35
+
36
+ left = [[1, 5, 2, 4], [3, 1, 4, 6], [5, 3, 6, 2]]
37
+ right = [[1, 5, 2, 4], [3, 1, 4, 6], [5, 3, 6, 2]]
38
+ result, label_map = connected_sum(left, right, 1, 1)
39
+ print(result)
40
+ print(label_map["a_1"], label_map["b_1"])
41
+ ```
42
+
43
+ ## Algorithm
44
+
45
+ Each input component is converted to a deterministic oriented cycle. The algorithm locates the incidence immediately after each selected arc, offsets the second code to avoid collisions, cuts those two oriented arcs, and glues the endpoints crosswise. It then canonically orders component cycles, assigns contiguous labels, orients every crossing consistently, and returns maps for all original labels. This avoids the incorrect local crossing replacement used by older releases.
46
+
47
+ ## Input conventions
48
+
49
+ A PD code is represented as a list of four-entry crossings. Arc labels normally occur exactly twice. Public functions validate inputs and return new values rather than mutating caller-owned data unless their API explicitly says otherwise.
50
+
51
+ ## External software
52
+
53
+ No external software is required.
54
+
55
+ ## Development
56
+
57
+ Run examples and package checks before release. Python packages require Python 3.10 or newer. Build PyPI artifacts with:
58
+
59
+ ```bash
60
+ poetry check
61
+ poetry build
62
+ ```
63
+
64
+ ## License
65
+
66
+ MIT. See `LICENSE`.
67
+
@@ -0,0 +1,46 @@
1
+ # pd-code-connected-sum
2
+
3
+ Join two selected oriented PD-code components and canonically renumber the result.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install pd-code-connected-sum
9
+ ```
10
+
11
+ ## Usage example
12
+
13
+ ```python
14
+ from pd_code_connected_sum import connected_sum
15
+
16
+ left = [[1, 5, 2, 4], [3, 1, 4, 6], [5, 3, 6, 2]]
17
+ right = [[1, 5, 2, 4], [3, 1, 4, 6], [5, 3, 6, 2]]
18
+ result, label_map = connected_sum(left, right, 1, 1)
19
+ print(result)
20
+ print(label_map["a_1"], label_map["b_1"])
21
+ ```
22
+
23
+ ## Algorithm
24
+
25
+ Each input component is converted to a deterministic oriented cycle. The algorithm locates the incidence immediately after each selected arc, offsets the second code to avoid collisions, cuts those two oriented arcs, and glues the endpoints crosswise. It then canonically orders component cycles, assigns contiguous labels, orients every crossing consistently, and returns maps for all original labels. This avoids the incorrect local crossing replacement used by older releases.
26
+
27
+ ## Input conventions
28
+
29
+ A PD code is represented as a list of four-entry crossings. Arc labels normally occur exactly twice. Public functions validate inputs and return new values rather than mutating caller-owned data unless their API explicitly says otherwise.
30
+
31
+ ## External software
32
+
33
+ No external software is required.
34
+
35
+ ## Development
36
+
37
+ Run examples and package checks before release. Python packages require Python 3.10 or newer. Build PyPI artifacts with:
38
+
39
+ ```bash
40
+ poetry check
41
+ poetry build
42
+ ```
43
+
44
+ ## License
45
+
46
+ MIT. See `LICENSE`.
@@ -0,0 +1,6 @@
1
+ from .main import connected_sum, normalize_pd_code
2
+
3
+ __all__ = [
4
+ "connected_sum",
5
+ "normalize_pd_code",
6
+ ]
@@ -0,0 +1,90 @@
1
+ from copy import deepcopy
2
+
3
+ import pd_code_pre_nxt
4
+ import pd_code_sanity
5
+
6
+
7
+ def _normalize(pd_code: list[list[int]]) -> tuple[list[list[int]], dict[int, int]]:
8
+ if not pd_code:
9
+ return [], {}
10
+ pre, nxt = pd_code_pre_nxt.get_pre_nxt(pd_code)
11
+ old_to_new: dict[int, int] = {}
12
+ next_label = 1
13
+ for start in pd_code_pre_nxt.get_num_set(pd_code):
14
+ if start in old_to_new:
15
+ continue
16
+ current = start
17
+ while current not in old_to_new:
18
+ old_to_new[current] = next_label
19
+ next_label += 1
20
+ current = nxt[current]
21
+
22
+ normalized = [[old_to_new[label] for label in crossing] for crossing in pd_code]
23
+ new_next = {old_to_new[label]: old_to_new[target] for label, target in nxt.items()}
24
+ for index, crossing in enumerate(normalized):
25
+ if new_next[crossing[0]] == crossing[2]:
26
+ continue
27
+ if new_next[crossing[2]] == crossing[0]:
28
+ normalized[index] = crossing[2:] + crossing[:2]
29
+ else:
30
+ raise ValueError("crossing is inconsistent with component orientation")
31
+ normalized.sort()
32
+ if not pd_code_sanity.sanity(normalized):
33
+ raise AssertionError("normalization produced an invalid PD code")
34
+ return normalized, old_to_new
35
+
36
+
37
+ def normalize_pd_code(pd_code: list[list[int]]) -> tuple[list[list[int]], dict[int, int]]:
38
+ """Return a canonically oriented, contiguously labelled copy and its map."""
39
+ if not pd_code_sanity.sanity(pd_code):
40
+ raise ValueError("invalid PD code")
41
+ return _normalize(deepcopy(pd_code))
42
+
43
+
44
+ def _endpoint(pd_code, label, nxt, pre, want_after: bool) -> tuple[int, int]:
45
+ slots = [(i, j) for i, crossing in enumerate(pd_code) for j, value in enumerate(crossing) if value == label]
46
+ if len(slots) != 2:
47
+ raise ValueError("connected-sum label must occur exactly twice")
48
+ if nxt[label] == pre[label]:
49
+ return slots[1 if want_after else 0]
50
+ target = nxt[label] if want_after else pre[label]
51
+ for i, j in slots:
52
+ if pd_code[i][(j + 2) % 4] == target:
53
+ return i, j
54
+ raise ValueError("could not locate oriented endpoint for label")
55
+
56
+
57
+ def connected_sum(
58
+ pd_code1: list[list[int]], pd_code2: list[list[int]], val_1: int, val_2: int
59
+ ) -> tuple[list[list[int]], dict[str, int]]:
60
+ """Join the oriented components containing ``val_1`` and ``val_2``."""
61
+ for pd_code in (pd_code1, pd_code2):
62
+ if not pd_code_sanity.sanity(pd_code):
63
+ raise ValueError("invalid PD code")
64
+ if any(isinstance(x, bool) or not isinstance(x, int) for crossing in pd_code for x in crossing):
65
+ raise TypeError("connected_sum requires integer labels")
66
+
67
+ if not pd_code1 or not pd_code2:
68
+ normalized, mapping = _normalize(deepcopy(pd_code1 or pd_code2))
69
+ prefix = "a_" if pd_code1 else "b_"
70
+ return normalized, {prefix + str(old): new for old, new in mapping.items()}
71
+ if sum(label == val_1 for crossing in pd_code1 for label in crossing) != 2:
72
+ raise ValueError("val_1 must occur exactly twice in pd_code1")
73
+ if sum(label == val_2 for crossing in pd_code2 for label in crossing) != 2:
74
+ raise ValueError("val_2 must occur exactly twice in pd_code2")
75
+
76
+ first = deepcopy(pd_code1)
77
+ offset = max(label for crossing in first for label in crossing)
78
+ second = [[label + offset for label in crossing] for crossing in deepcopy(pd_code2)]
79
+ second_label = val_2 + offset
80
+ pre_a, nxt_a = pd_code_pre_nxt.get_pre_nxt(first)
81
+ pre_b, nxt_b = pd_code_pre_nxt.get_pre_nxt(second)
82
+ ai, aj = _endpoint(first, val_1, nxt_a, pre_a, True)
83
+ bi, bj = _endpoint(second, second_label, nxt_b, pre_b, True)
84
+
85
+ first[ai][aj] = second_label
86
+ second[bi][bj] = val_1
87
+ normalized, mapping = _normalize(first + second)
88
+ output_map = {"a_" + str(old): new for old, new in mapping.items() if old <= offset}
89
+ output_map.update({"b_" + str(old - offset): new for old, new in mapping.items() if old > offset})
90
+ return normalized, output_map
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pd-code-connected-sum"
3
- version = "0.0.3"
3
+ version = "0.1.1"
4
4
  description = "make connected sum for link pd_code"
5
5
  authors = [
6
6
  {name = "GGN_2015",email = "neko@jlulug.org"}
@@ -9,8 +9,8 @@ license = {text = "MIT"}
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
11
11
  dependencies = [
12
- "pd-code-sanity",
13
- "pd-code-pre-nxt"
12
+ "pd-code-sanity>=0.1.0",
13
+ "pd-code-pre-nxt>=0.1.0"
14
14
  ]
15
15
 
16
16
 
@@ -1,50 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: pd-code-connected-sum
3
- Version: 0.0.3
4
- Summary: make connected sum for link pd_code
5
- License: MIT
6
- License-File: LICENSE
7
- Author: GGN_2015
8
- Author-email: neko@jlulug.org
9
- Requires-Python: >=3.10
10
- Classifier: License :: OSI Approved :: MIT License
11
- Classifier: Programming Language :: Python :: 3
12
- Classifier: Programming Language :: Python :: 3.10
13
- Classifier: Programming Language :: Python :: 3.11
14
- Classifier: Programming Language :: Python :: 3.12
15
- Classifier: Programming Language :: Python :: 3.13
16
- Classifier: Programming Language :: Python :: 3.14
17
- Requires-Dist: pd-code-pre-nxt
18
- Requires-Dist: pd-code-sanity
19
- Description-Content-Type: text/markdown
20
-
21
- # pd_code_connected_sum
22
- caculate connected sum for link pd_code.
23
-
24
- ## Install
25
-
26
- ```bash
27
- pip install pd-code-connected-sum
28
- ```
29
-
30
- ## Usage
31
-
32
- ```python
33
- import pd_code_connected_sum
34
-
35
- # L2a1
36
- link_pd_code_1 = [[4, 1, 3, 2], [2, 3, 1, 4]]
37
-
38
- # L4a1
39
- link_pd_code_2 = [[6, 1, 7, 2], [8, 3, 5, 4], [2, 5, 3, 6], [4, 7, 1, 8]]
40
-
41
- connect_pos_1 = 1
42
- connect_pos_2 = 1
43
-
44
- new_pd_code, num_map = pd_code_connected_sum.connected_sum(
45
- link_pd_code_1, link_pd_code_2, connect_pos_1, connect_pos_2)
46
-
47
- print(new_pd_code)
48
- print(num_map)
49
- ```
50
-
@@ -1,29 +0,0 @@
1
- # pd_code_connected_sum
2
- caculate connected sum for link pd_code.
3
-
4
- ## Install
5
-
6
- ```bash
7
- pip install pd-code-connected-sum
8
- ```
9
-
10
- ## Usage
11
-
12
- ```python
13
- import pd_code_connected_sum
14
-
15
- # L2a1
16
- link_pd_code_1 = [[4, 1, 3, 2], [2, 3, 1, 4]]
17
-
18
- # L4a1
19
- link_pd_code_2 = [[6, 1, 7, 2], [8, 3, 5, 4], [2, 5, 3, 6], [4, 7, 1, 8]]
20
-
21
- connect_pos_1 = 1
22
- connect_pos_2 = 1
23
-
24
- new_pd_code, num_map = pd_code_connected_sum.connected_sum(
25
- link_pd_code_1, link_pd_code_2, connect_pos_1, connect_pos_2)
26
-
27
- print(new_pd_code)
28
- print(num_map)
29
- ```
@@ -1,5 +0,0 @@
1
- from .main import connected_sum
2
-
3
- __all__ = [
4
- "connected_sum"
5
- ]
@@ -1,245 +0,0 @@
1
- import pd_code_sanity
2
- import pd_code_pre_nxt
3
- import json
4
-
5
- # 为编码增加前缀
6
- def add_prefix(pd_code:list[list], prefix:str) -> list[list]:
7
- pd_code = json.loads(json.dumps(pd_code))
8
- for i in range(len(pd_code)):
9
- for j in range(len(pd_code[i])):
10
- pd_code[i][j] = prefix + str(pd_code[i][j])
11
- return json.loads(json.dumps(pd_code))
12
-
13
- # 替换一个值恰好一次
14
- def replace_val(crossing:list, val_now:str, val_new:str):
15
- cnt = 0
16
- for i in range(len(crossing)):
17
- if crossing[i] == val_now:
18
- crossing[i] = val_new
19
- cnt += 1
20
- if cnt != 1:
21
- raise ValueError()
22
-
23
- # 检查一个 crossing 是否包含某个值
24
- def check_has(crossing:list, val_now:str) -> bool:
25
- for i in range(len(crossing)):
26
- if crossing[i] == val_now:
27
- return True
28
- return False
29
-
30
- # 检查两个元素同时存在
31
- def check_has2(crossing:list, val1:str, val2:str) -> bool:
32
- return (
33
- check_has(crossing, val1) and
34
- check_has(crossing, val2))
35
-
36
- get_num_set = pd_code_pre_nxt.get_num_set
37
-
38
- # 按照顺序对循环进行遍历
39
- def dfs(val, nxt:dict[str, str], vis: set[int], idx:dict[str, int]):
40
- if val in vis:
41
- return
42
- assert nxt.get(val) is not None
43
-
44
- # 向已经访问元素队列中添加该节点
45
- vis.add(val)
46
- idx[val] = len(vis) # 当前元素个数,就是当前节点新编号
47
-
48
- if nxt[val] not in vis:
49
- dfs(nxt[val], nxt, vis, idx)
50
-
51
- # 在有向图中重新编号
52
- def get_new_number_map(nxt:dict[str, str], num_set:list[int]) -> dict[str, int]:
53
- vis = set()
54
- idx = dict()
55
- for val in num_set:
56
- if val not in vis:
57
- dfs(val, nxt, vis, idx)
58
- return idx
59
-
60
- # 检查有多少个 crossing 包含了 val_now
61
- def count_crossing_exists(pd_code_now:list[list], val_now) -> int:
62
- ans = 0
63
- for crossing in pd_code_now:
64
- if check_has(crossing, val_now):
65
- ans += 1
66
- return ans
67
-
68
- # 找到同一个连通分量中合法的位置
69
- def find_avaible_pos(pd_code:list[list], val_now):
70
- for crossing in pd_code:
71
- if check_has(crossing, val_now):
72
- values = []
73
- for i in range(len(crossing)):
74
- if crossing[i] != val_now and crossing[i] not in values:
75
- values.append(crossing[i])
76
- if len(values) == 1:
77
- return None
78
- else:
79
- return values[0]
80
-
81
- # 在这里报错说明 pd_code 中没找到 val_now
82
- raise AssertionError()
83
-
84
- # 删除 pd_code1 中 val_1 所在的 crossing
85
- # 然后把两个扭结直接合并
86
- # 然后重新编号
87
- def del_component_and_merge(pd_code1, pd_code2, val_1, val_2) -> tuple[list[list], dict]:
88
- suc = False
89
- for i in range(len(pd_code1)):
90
- if check_has(pd_code1[i], val_1):
91
- suc = True
92
- pd_code1 = pd_code1[:i] + pd_code1[i+1:] # 跳过第 i 个元素
93
-
94
- # 这里报错说明没找到 val_1 对应的 component
95
- if not suc:
96
- raise AssertionError()
97
-
98
- # 获取元素集合
99
- num_set_1 = add_prefix([get_num_set(pd_code1)], "a_")[0]
100
- num_set_2 = add_prefix([get_num_set(pd_code2)], "b_")[0]
101
-
102
- # 增加前缀编码
103
- pd_code1 = add_prefix(pd_code1, "a_")
104
- pd_code2 = add_prefix(pd_code2, "b_")
105
-
106
- # 计算前驱后继
107
- _, nxt1 = pd_code_pre_nxt.get_pre_nxt(pd_code1)
108
- _, nxt2 = pd_code_pre_nxt.get_pre_nxt(pd_code2)
109
- nxt = nxt1 | nxt2
110
-
111
- # 合并得到新的 pd_code
112
- new_pd_code = json.loads(json.dumps(pd_code1 + pd_code2))
113
-
114
- # 获得新的编号方式
115
- num_map = get_new_number_map(nxt, num_set_1 + num_set_2)
116
- for crossing in new_pd_code:
117
- for i in range(len(crossing)):
118
- if num_map.get(crossing[i]) is None:
119
- assert AssertionError()
120
- crossing[i] = num_map[crossing[i]]
121
- return sorted(new_pd_code), num_map
122
-
123
- # 将 pd_code1 里面的 val_1 和 pd_code2 里面的 val_2 连接起来
124
- def connected_sum(
125
- pd_code1:list[list], pd_code2:list[list], val_1, val_2) -> tuple[list[list], dict]:
126
-
127
- # 如果有一个扭结平凡,则不需要连接
128
- if pd_code1 == [] or pd_code2 == []:
129
- return json.loads(json.dumps(pd_code1 + pd_code2))
130
-
131
- # 检查 pd_code 的弱合法性
132
- for pd_code in [pd_code1, pd_code2]:
133
- if not pd_code_sanity.sanity(pd_code):
134
- raise TypeError()
135
-
136
- # 检查 val_1 和 val_2 是否出现了
137
- lis_wrap_1 = [0]
138
- lis_wrap_2 = [0]
139
- lis_wrap = [lis_wrap_1, lis_wrap_2]
140
- for cnt_wrap, val_now, pd_now in [
141
- (lis_wrap_1, val_1, pd_code1),
142
- (lis_wrap_2, val_2, pd_code2)
143
- ]:
144
- for crossing in pd_now:
145
- for term in crossing:
146
- if term == val_now:
147
- cnt_wrap[0] += 1
148
-
149
- # 检查出现次数是否正确
150
- for i in range(2):
151
- if lis_wrap[i][0] != 2:
152
- raise ValueError(f"val_{i+1} not in pd_code{i+1}")
153
-
154
- # 分别计算两个需要连接的地方
155
- # 是不是在一个 r1 crossing 上
156
- # 如果 has_val_x_cnt[0] != 2
157
- # 则说明,val_x 在一个 r1 crossing
158
- # 其中 x = 1 或者 2
159
- has_val_1_cnt = [0]
160
- has_val_2_cnt = [0]
161
- for pd_code_now, has_val_cnt, val_now in [
162
- (pd_code1, has_val_1_cnt, val_1),
163
- (pd_code2, has_val_2_cnt, val_2)
164
- ]:
165
- has_val_cnt[0] = count_crossing_exists(pd_code_now, val_now)
166
-
167
- vals = [val_1, val_2]
168
- for val_pos, pd_code_now, pd_code_other, has_cnt_now in [
169
- (0, pd_code1, pd_code2, has_val_1_cnt),
170
- (1, pd_code2, pd_code1, has_val_2_cnt),
171
- ]:
172
- if has_cnt_now[0] != 2:
173
-
174
- # 在同一个连通分量中
175
- # 找到一个出现在多个 crossing 中的编号
176
- vals_new = find_avaible_pos(pd_code_now, vals[val_pos])
177
- if vals_new is None:
178
-
179
- # 遇到了 [a, b, b, a] 或者 [a, a, b, b] 之类的
180
- # 直接删掉当前 components 然后 merge 即可
181
- return del_component_and_merge(
182
- pd_code_now, pd_code_other, vals[val_pos], vals[1-val_pos])
183
-
184
- vals[val_pos] = vals_new
185
-
186
- # 覆盖旧的元素
187
- val_1, val_2 = vals
188
-
189
- # 获取元素集合
190
- num_set_1 = add_prefix([get_num_set(pd_code1)], "a_")[0]
191
- num_set_2 = add_prefix([get_num_set(pd_code2)], "b_")[0]
192
-
193
- # 增加前缀编码
194
- pd_code1 = add_prefix(pd_code1, "a_")
195
- pd_code2 = add_prefix(pd_code2, "b_")
196
-
197
- # 由于不存在 nugatory crossing 和 r1 crossing
198
- # 所以两侧各有两个相关 crossing
199
- _, nxt1 = pd_code_pre_nxt.get_pre_nxt(pd_code1)
200
- _, nxt2 = pd_code_pre_nxt.get_pre_nxt(pd_code2)
201
- nxt = nxt1 | nxt2
202
-
203
- # 带有字母前缀的成分
204
- a_val_1 = f"a_{val_1}"
205
- b_val_2 = f"b_{val_2}"
206
-
207
- # 将两组 pd_code 合并
208
- # 需要注意 nxt[a_val_1] 和 pre[a_val_1] 可能相同
209
- # 所以要保证只替换一次
210
- suc_1 = False
211
- suc_2 = False
212
- new_pd_code = json.loads(json.dumps(pd_code1 + pd_code2))
213
- for crossing in new_pd_code:
214
- if not suc_1 and check_has2(crossing, a_val_1, nxt[a_val_1]):
215
- replace_val(crossing, a_val_1, b_val_2)
216
- suc_1 = True
217
- if not suc_2 and check_has2(crossing, b_val_2, nxt[b_val_2]):
218
- replace_val(crossing, b_val_2, a_val_1)
219
- suc_2 = True
220
- if suc_1 and suc_2:
221
- break
222
-
223
- # 调整前驱后继关系
224
- nxt[b_val_2] = nxt1[a_val_1]
225
- nxt[a_val_1] = nxt2[b_val_2]
226
-
227
- # 获得新的编号方式
228
- num_map = get_new_number_map(nxt, num_set_1 + num_set_2)
229
- for crossing in new_pd_code:
230
- for i in range(len(crossing)):
231
- if num_map.get(crossing[i]) is None:
232
- assert AssertionError()
233
- crossing[i] = num_map[crossing[i]]
234
-
235
- ans_pd_code = sorted(new_pd_code)
236
- if not pd_code_sanity.sanity(ans_pd_code):
237
- raise AssertionError()
238
-
239
- return ans_pd_code, num_map
240
-
241
- if __name__ == "__main__":
242
- pd_code, num_map = connected_sum(
243
- [[1, 5, 2, 4], [3, 1, 4, 6], [5, 3, 6, 2]],
244
- [[1, 5, 2, 4], [3, 1, 4, 6], [5, 3, 6, 2]], 1, 1)
245
- print(num_map)