|
5 | 5 | ElementPowerResult, |
6 | 6 | GeneratedSubsemigroupRequest, |
7 | 7 | GeneratedSubsemigroupResult, |
| 8 | + GreenRelationsRequest, |
| 9 | + GreenRelationsResult, |
8 | 10 | IdempotentsRequest, |
9 | 11 | IdempotentsResult, |
10 | 12 | PowerProfileRequest, |
@@ -198,3 +200,215 @@ def compute_principal_ideals(request: PrincipalIdealsRequest) -> PrincipalIdeals |
198 | 200 | elements=request.elements, |
199 | 201 | ideals=ideals, |
200 | 202 | ) |
| 203 | + |
| 204 | + |
| 205 | +def _left_ideals( |
| 206 | + elements: tuple[str, ...], |
| 207 | + multiplication: tuple[tuple[str, ...], ...], |
| 208 | +) -> list[frozenset[str]]: |
| 209 | + """Compute the principal left ideal S^1 a of each element.""" |
| 210 | + |
| 211 | + {label: i for i, label in enumerate(elements)} |
| 212 | + n = len(elements) |
| 213 | + ideals: list[frozenset[str]] = [] |
| 214 | + for i in range(n): |
| 215 | + ideal = {elements[i]} |
| 216 | + for j in range(n): |
| 217 | + ideal.add(multiplication[j][i]) |
| 218 | + ideals.append(frozenset(ideal)) |
| 219 | + return ideals |
| 220 | + |
| 221 | + |
| 222 | +def _right_ideals( |
| 223 | + elements: tuple[str, ...], |
| 224 | + multiplication: tuple[tuple[str, ...], ...], |
| 225 | +) -> list[frozenset[str]]: |
| 226 | + """Compute the principal right ideal a S^1 of each element.""" |
| 227 | + |
| 228 | + {label: i for i, label in enumerate(elements)} |
| 229 | + n = len(elements) |
| 230 | + ideals: list[frozenset[str]] = [] |
| 231 | + for i in range(n): |
| 232 | + ideal = {elements[i]} |
| 233 | + for j in range(n): |
| 234 | + ideal.add(multiplication[i][j]) |
| 235 | + ideals.append(frozenset(ideal)) |
| 236 | + return ideals |
| 237 | + |
| 238 | + |
| 239 | +def _two_sided_ideals( |
| 240 | + elements: tuple[str, ...], |
| 241 | + multiplication: tuple[tuple[str, ...], ...], |
| 242 | +) -> list[frozenset[str]]: |
| 243 | + """Compute the principal two-sided ideal S^1 a S^1 of each element.""" |
| 244 | + |
| 245 | + idx = {label: i for i, label in enumerate(elements)} |
| 246 | + n = len(elements) |
| 247 | + ideals: list[frozenset[str]] = [] |
| 248 | + for i in range(n): |
| 249 | + ideal = {elements[i]} |
| 250 | + for j in range(n): |
| 251 | + ideal.add(multiplication[j][i]) |
| 252 | + ideal.add(multiplication[i][j]) |
| 253 | + for k in range(n): |
| 254 | + ideal.add(multiplication[j][idx[multiplication[i][k]]]) |
| 255 | + ideals.append(frozenset(ideal)) |
| 256 | + return ideals |
| 257 | + |
| 258 | + |
| 259 | +def _partition_from_ideals( |
| 260 | + elements: tuple[str, ...], |
| 261 | + ideals: list[frozenset[str]], |
| 262 | +) -> tuple[tuple[str, ...], ...]: |
| 263 | + """Group elements by equality of their principal ideals. |
| 264 | +
|
| 265 | + Returns a tuple of equivalence-class tuples in declared element order. |
| 266 | + """ |
| 267 | + |
| 268 | + groups: list[list[str]] = [] |
| 269 | + assigned: list[bool] = [False] * len(elements) |
| 270 | + for i in range(len(elements)): |
| 271 | + if assigned[i]: |
| 272 | + continue |
| 273 | + group = [elements[i]] |
| 274 | + assigned[i] = True |
| 275 | + for j in range(i + 1, len(elements)): |
| 276 | + if not assigned[j] and ideals[i] == ideals[j]: |
| 277 | + group.append(elements[j]) |
| 278 | + assigned[j] = True |
| 279 | + groups.append(group) |
| 280 | + return tuple(tuple(g) for g in groups) |
| 281 | + |
| 282 | + |
| 283 | +def _green_relations( |
| 284 | + elements: tuple[str, ...], |
| 285 | + multiplication: tuple[tuple[str, ...], ...], |
| 286 | +) -> tuple[ |
| 287 | + tuple[tuple[str, ...], ...], |
| 288 | + tuple[tuple[str, ...], ...], |
| 289 | + tuple[tuple[str, ...], ...], |
| 290 | + tuple[tuple[str, ...], ...], |
| 291 | + tuple[tuple[str, ...], ...], |
| 292 | +]: |
| 293 | + """Compute the Green relations L, R, H, D, J. |
| 294 | +
|
| 295 | + For a finite semigroup S: |
| 296 | + - a L b iff S^1 a = S^1 b (principal left ideals agree) |
| 297 | + - a R b iff a S^1 = b S^1 (principal right ideals agree) |
| 298 | + - H = L ∩ R |
| 299 | + - J is defined by principal two-sided ideals: a J b iff S^1 a S^1 = S^1 b S^1 |
| 300 | + - D = L ∨ R (the join), equivalently the relation whose blocks are the |
| 301 | + connected components of the L-R intersection graph |
| 302 | +
|
| 303 | + Returns each as a tuple of equivalence-class tuples in declared element order. |
| 304 | + """ # noqa: RUF002 |
| 305 | + |
| 306 | + left = _left_ideals(elements, multiplication) |
| 307 | + right = _right_ideals(elements, multiplication) |
| 308 | + two_sided = _two_sided_ideals(elements, multiplication) |
| 309 | + |
| 310 | + L_classes = _partition_from_ideals(elements, left) # noqa: N806 |
| 311 | + R_classes = _partition_from_ideals(elements, right) # noqa: N806 |
| 312 | + J_classes = _partition_from_ideals(elements, two_sided) # noqa: N806 |
| 313 | + |
| 314 | + # H = L ∩ R: two elements are H-related iff they are both L-related and R-related |
| 315 | + H_classes = _intersection_partition(elements, L_classes, R_classes) # noqa: N806 |
| 316 | + |
| 317 | + # D = L ∨ R: build a graph where two elements are connected if L-related or R-related # noqa: RUF003 |
| 318 | + # then D-classes are the connected components |
| 319 | + D_classes = _join_partition(elements, L_classes, R_classes) # noqa: N806 |
| 320 | + |
| 321 | + return L_classes, R_classes, H_classes, D_classes, J_classes |
| 322 | + |
| 323 | + |
| 324 | +def _intersection_partition( |
| 325 | + elements: tuple[str, ...], |
| 326 | + partition_a: tuple[tuple[str, ...], ...], |
| 327 | + partition_b: tuple[tuple[str, ...], ...], |
| 328 | +) -> tuple[tuple[str, ...], ...]: |
| 329 | + """Compute the partition that is the intersection of two partitions.""" |
| 330 | + |
| 331 | + a_map: dict[str, int] = {} |
| 332 | + b_map: dict[str, int] = {} |
| 333 | + for i, cls in enumerate(partition_a): |
| 334 | + for e in cls: |
| 335 | + a_map[e] = i |
| 336 | + for i, cls in enumerate(partition_b): |
| 337 | + for e in cls: |
| 338 | + b_map[e] = i |
| 339 | + groups: dict[tuple[int, int], list[str]] = {} |
| 340 | + for e in elements: |
| 341 | + key = (a_map[e], b_map[e]) |
| 342 | + groups.setdefault(key, []).append(e) |
| 343 | + # Return in declared element order |
| 344 | + seen: set[str] = set() |
| 345 | + result: list[tuple[str, ...]] = [] |
| 346 | + for e in elements: |
| 347 | + if e in seen: |
| 348 | + continue |
| 349 | + key = (a_map[e], b_map[e]) |
| 350 | + result.append(tuple(groups[key])) |
| 351 | + seen.update(groups[key]) |
| 352 | + return tuple(result) |
| 353 | + |
| 354 | + |
| 355 | +def _join_partition( # noqa: C901 |
| 356 | + elements: tuple[str, ...], |
| 357 | + partition_a: tuple[tuple[str, ...], ...], |
| 358 | + partition_b: tuple[tuple[str, ...], ...], |
| 359 | +) -> tuple[tuple[str, ...], ...]: |
| 360 | + """Compute the join (least upper bound) of two partitions via union-find.""" |
| 361 | + |
| 362 | + n = len(elements) |
| 363 | + idx = {e: i for i, e in enumerate(elements)} |
| 364 | + parent = list(range(n)) |
| 365 | + |
| 366 | + def find(x: int) -> int: |
| 367 | + while parent[x] != x: |
| 368 | + parent[x] = parent[parent[x]] |
| 369 | + x = parent[x] |
| 370 | + return x |
| 371 | + |
| 372 | + def union(x: int, y: int) -> None: |
| 373 | + rx, ry = find(x), find(y) |
| 374 | + if rx != ry: |
| 375 | + parent[rx] = ry |
| 376 | + |
| 377 | + for cls in partition_a: |
| 378 | + for i in range(1, len(cls)): |
| 379 | + union(idx[cls[0]], idx[cls[i]]) |
| 380 | + for cls in partition_b: |
| 381 | + for i in range(1, len(cls)): |
| 382 | + union(idx[cls[0]], idx[cls[i]]) |
| 383 | + |
| 384 | + groups: dict[int, list[str]] = {} |
| 385 | + for e in elements: |
| 386 | + groups.setdefault(find(idx[e]), []).append(e) |
| 387 | + # Return in declared element order |
| 388 | + seen: set[str] = set() |
| 389 | + result: list[tuple[str, ...]] = [] |
| 390 | + for e in elements: |
| 391 | + if e in seen: |
| 392 | + continue |
| 393 | + root = find(idx[e]) |
| 394 | + result.append(tuple(groups[root])) |
| 395 | + seen.update(groups[root]) |
| 396 | + return tuple(result) |
| 397 | + |
| 398 | + |
| 399 | +def compute_green_relations( |
| 400 | + request: "GreenRelationsRequest", |
| 401 | +) -> "GreenRelationsResult": |
| 402 | + """Compute the Green relations of a finite semigroup.""" |
| 403 | + |
| 404 | + L, R, H, D, J = _green_relations( # noqa: N806 |
| 405 | + request.semigroup.elements, request.semigroup.multiplication |
| 406 | + ) |
| 407 | + return GreenRelationsResult( |
| 408 | + semigroup=request.semigroup, |
| 409 | + L=L, |
| 410 | + R=R, |
| 411 | + H=H, |
| 412 | + D=D, |
| 413 | + J=J, |
| 414 | + ) |
0 commit comments