Skip to content

Commit 6dbd4f8

Browse files
committed
feat(math): add Green relations (L, R, H, D, J) to finite semigroup domain
Add the five Green relations for a finite semigroup computed via principal ideal equality: - L: elements with the same principal left ideal S^1 a - R: elements with the same principal right ideal a S^1 - H = L ∩ R - D = L ∨ R (join via union-find) - J: elements with the same principal two-sided ideal S^1 a S^1 Each relation is returned as a tuple of equivalence-class tuples in declared element order. The result model re-runs the native kernel to verify exactness. Covers part of GitHub issue #1858.
1 parent 0db39a2 commit 6dbd4f8

5 files changed

Lines changed: 404 additions & 0 deletions

File tree

src/jacobian/math/finite_semigroups/_admission.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@
3333
AdmissionDecision.KEEP,
3434
"exact principal two-sided ideals S^1 a S^1 of requested elements",
3535
),
36+
OperationAdmission(
37+
"semigroup.green_relations.compute",
38+
AdmissionDecision.KEEP,
39+
"exact Green relations L, R, H, D, J via principal ideal equality",
40+
),
3641
)
3742

3843
REGISTRATION = OperationRegistration(TOOLS, ADMISSIONS)

src/jacobian/math/finite_semigroups/_models.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,3 +239,45 @@ def bind_ideals(self) -> Self:
239239
"ideals must be the exact principal ideals of the elements"
240240
)
241241
return self
242+
243+
244+
class GreenRelationsRequest(StrictModel):
245+
"""Request the Green relations L, R, H, D, J of a finite semigroup."""
246+
247+
semigroup: FiniteSemigroup
248+
249+
250+
class GreenRelationsResult(StrictModel):
251+
"""Green relations of a finite semigroup.
252+
253+
Each relation is a tuple of equivalence-class tuples, in declared
254+
element order, partitioning the semigroup elements. ``L`` and ``R``
255+
are Green's left and right equivalences; ``H = L ∩ R``;
256+
``D = L ∘ R`` (the join); ``J`` is the two-sided Green relation.
257+
"""
258+
259+
semigroup: FiniteSemigroup
260+
L: tuple[tuple[str, ...], ...]
261+
R: tuple[tuple[str, ...], ...]
262+
H: tuple[tuple[str, ...], ...]
263+
D: tuple[tuple[str, ...], ...]
264+
J: tuple[tuple[str, ...], ...]
265+
266+
@model_validator(mode="after")
267+
def bind_green_relations(self) -> Self:
268+
from jacobian.math.finite_semigroups._operations import _green_relations
269+
270+
L, R, H, D, J = _green_relations(
271+
self.semigroup.elements, self.semigroup.multiplication
272+
)
273+
if self.L != L:
274+
raise ValueError("L must be the exact Green L-relation")
275+
if self.R != R:
276+
raise ValueError("R must be the exact Green R-relation")
277+
if self.H != H:
278+
raise ValueError("H must be the exact Green H-relation")
279+
if self.D != D:
280+
raise ValueError("D must be the exact Green D-relation")
281+
if self.J != J:
282+
raise ValueError("J must be the exact Green J-relation")
283+
return self

src/jacobian/math/finite_semigroups/_operations.py

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"""Exact bounded finite semigroup operations."""
22

33
from jacobian.math.finite_semigroups._models import (
4+
GreenRelationsRequest,
5+
GreenRelationsResult,
46
ElementPowerRequest,
57
ElementPowerResult,
68
GeneratedSubsemigroupRequest,
@@ -198,3 +200,216 @@ def compute_principal_ideals(request: PrincipalIdealsRequest) -> PrincipalIdeals
198200
elements=request.elements,
199201
ideals=ideals,
200202
)
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+
idx = {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+
idx = {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+
"""
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)
311+
R_classes = _partition_from_ideals(elements, right)
312+
J_classes = _partition_from_ideals(elements, two_sided)
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)
316+
317+
# D = L ∨ R: build a graph where two elements are connected if L-related or R-related
318+
# then D-classes are the connected components
319+
D_classes = _join_partition(elements, L_classes, R_classes)
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(
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+
405+
L, R, H, D, J = _green_relations(
406+
request.semigroup.elements, request.semigroup.multiplication
407+
)
408+
return GreenRelationsResult(
409+
semigroup=request.semigroup,
410+
L=L,
411+
R=R,
412+
H=H,
413+
D=D,
414+
J=J,
415+
)

src/jacobian/math/finite_semigroups/_tools.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,3 +181,40 @@ def _op[
181181
)
182182

183183
__all__ = ["TOOLS"]
184+
185+
# Append Green relations to TOOLS
186+
from jacobian.math.finite_semigroups._models import (
187+
GreenRelationsRequest,
188+
GreenRelationsResult,
189+
)
190+
from jacobian.math.finite_semigroups._operations import compute_green_relations
191+
192+
_TOOLS_LIST = list(TOOLS)
193+
_TOOLS_LIST.append(
194+
_op(
195+
"semigroup.green_relations.compute",
196+
"Compute Green relations of a finite semigroup",
197+
"Compute the Green relations L, R, H, D, and J of a finite semigroup. "
198+
"L relates elements with the same principal left ideal, R with the "
199+
"same principal right ideal, H = L ∩ R, D = L ∨ R (join), and J is "
200+
"the two-sided Green relation defined by principal two-sided ideals.",
201+
GreenRelationsRequest,
202+
GreenRelationsResult,
203+
compute_green_relations,
204+
"algebra",
205+
"semigroup",
206+
"exact",
207+
examples=(
208+
example(
209+
"green_relations_z3",
210+
"Compute the Green relations of Z/3Z.",
211+
{
212+
"semigroup": _SEMIGROUP,
213+
},
214+
),
215+
),
216+
)
217+
)
218+
TOOLS = tuple(_TOOLS_LIST)
219+
220+
__all__ = ["TOOLS"]

0 commit comments

Comments
 (0)