Source code for pocketpartition.core.numerical_semigroup

__all__ = ['NumericalSemigroup']

from .numerical_set import NumericalSet
from collections import Counter
from ..utils.helpers import remove_sum_of_two_elements
from functools import lru_cache
from math import ceil

[docs] class NumericalSemigroup(NumericalSet): _instances = {} def __new__(cls, gaps=None, generators=None): if generators is not None: gaps_frozenset = frozenset(cls._compute_gaps_from_generators(generators)) else: gaps_frozenset = frozenset(gaps) if gaps_frozenset in cls._instances: return cls._instances[gaps_frozenset] instance = super(NumericalSemigroup, cls).__new__(cls, gaps=gaps_frozenset) cls._instances[gaps_frozenset] = instance return instance def __init__(self, gaps=None, generators=None): """ Initialize the numerical semigroup with its gaps or generators. Parameters: gaps (list of int): The gaps of the numerical semigroup. generators (list of int): The generators of the numerical semigroup. Raises: ValueError: If the atom monoid of the numerical set is not equal to the set itself. """ if generators is not None: self._gaps = frozenset(self._compute_gaps_from_generators(generators)) else: super().__init__(gaps) gaps = self._gaps if self.atom_monoid_gaps() != gaps: raise ValueError("The provided gaps do not form a numerical semigroup because the atom monoid is not equal to the set itself.") self._frobenius_number = max(self._gaps) if self._gaps else -1 @property def genus(self): return len(self.gaps) def __str__(self): return f"NumericalSemigroup(genus={self.genus})" def __repr__(self): return f"NumericalSemigroup(genus={self.genus}, frobenius_number={self.frobenius_number})" @staticmethod def _compute_gaps_from_generators(generators): """ Compute the gaps of the numerical semigroup given its generators. Parameters: generators (list of int): The generators of the numerical semigroup. Returns: set of int: The gaps of the numerical semigroup. """ # Compute the elements of the semigroup up to twice the maximum generator semigroup = set() reduced_gens = remove_sum_of_two_elements(set(generators)) product_of_gens = 1 for gen in reduced_gens: product_of_gens *= gen bound = product_of_gens for i in range(bound): for g in reduced_gens: if i - g in semigroup or i - g == 0: semigroup.add(i) break # Identify the gaps, excluding 0 gaps = set(range(1, bound)) - semigroup return gaps
[docs] @lru_cache(maxsize=None) def apery_set(self, n): """ Compute the Apéry set of the numerical set with respect to n. Parameters: n (int): The modulus for the Apéry set computation. Returns: set of int: The Apéry set with respect to n. """ gaps = self.gaps.copy() if not isinstance(n, int) or n < 0: raise ValueError("n must be a nonnegative integer") if n in self.atom_monoid_gaps(): raise ValueError("n must not be in the gaps of the atom monoid") apery_set = set() for k in range(n): x = k while x in gaps: x += n apery_set.add(x) return apery_set
[docs] @lru_cache(maxsize=None) def minimal_generating_set(self): """ Compute the minimal generating set of the numerical semigroup. Returns: list of int: The minimal generating set of the numerical semigroup. The algorithm used here is based on the method described by Rosales and Vasco in their works on numerical semigroups. """ generators = self._compute_generators_from_gaps() generators.sort() multiplicity = self.multiplicity() if multiplicity == 1: return [1] elif multiplicity == 2: odd_gen = next(g for g in generators if g % 2 == 1) return [2, odd_gen] # Initial reduction based on congruence modulo multiplicity aux = [multiplicity] for i in range(1, multiplicity): g = next((g for g in generators if g % multiplicity == i), None) if g is not None: aux.append(g) aux.sort() gen = set(aux) min_gens = list(remove_sum_of_two_elements(gen)) min_gens.sort() return min_gens
def _compute_generators_from_gaps(self): """ Compute the generators of the numerical semigroup given its gaps. Returns: list of int: The generators of the numerical semigroup. """ gaps = self.gaps.copy() multiplicity = self.multiplicity() generators = [multiplicity] frobenius_number = max(gaps) if gaps else 0 equiv_classes = [0] for i in range(multiplicity + 1, frobenius_number): if i not in gaps and i % multiplicity not in equiv_classes: generators.append(i) equiv_classes.append(i % multiplicity) for i in range(frobenius_number + 1, frobenius_number + 1 + multiplicity): if i % multiplicity not in equiv_classes: generators.append(i) if len(equiv_classes) == multiplicity: break return generators
[docs] def void(self): """ Compute the void of the numerical semigroup. Returns: set: The void of the numerical semigroup. """ gaps = self.gaps.copy() F = max(gaps) void = [g for g in gaps if (g in gaps) and (F-g in gaps)] return void
[docs] def gap_poset(self): """ Compute the poset of the gaps of the numerical semigroup. Returns: set of tuple: The poset of the gaps of the numerical semigroup. """ gaps = self.gaps.copy() gap_relations = [(y, x) for x in gaps for y in gaps if x <= y and (y - x) not in gaps] return (list(gaps), gap_relations)
[docs] def void_poset(self): """ Compute the poset of the void of the numerical semigroup. Returns: set of tuple: The poset of the void of the numerical semigroup. """ void = self.void().copy() gaps = self.gaps.copy() void_relations = [(y, x) for x in void for y in void if x <= y and (y - x) not in gaps] return (list(void), void_relations)
[docs] @lru_cache(maxsize=None) def effective_weight(self): """ Calculates the effective weight of the numerical partition. The effective weight is the sum of the number of boxes above each minimal generating set element. Returns: int: The effective weight of the numerical partition. """ gaps = self.gaps.copy() def boxes_above(s): above = [] for gap in gaps: if gap > s: above.append(gap - s) return len(above) min_gens = self.minimal_generating_set() ewt = 0 for gen in min_gens: ewt += boxes_above(gen) return ewt
[docs] @lru_cache(maxsize=None) def apery_weight(self): """ Calculates the Apery weight of the numerical partition. The effective weight is the sum of the number of boxes above each Apery set element. We look use m instead of 0 for the weight about 0 residue class. Returns: int: The apery weight of the numerical partition. """ m = self.multiplicity() gaps = self.gaps.copy() def boxes_above(s): if gaps: if s > max(gaps): return 0 above = [] for gap in gaps: if gap > s: above.append(gap - s) return len(above) apery_set = self.apery_set(m) apery_set_adjust = {m} | (apery_set - {0}) awt = 0 for gen in apery_set_adjust: awt += boxes_above(gen) return awt
[docs] def pseudofrobenius_numbers(self): """ Calculate the pseudo-Frobenius numbers of the semigroup. Returns ------- list of int The unique pseudo-Frobenius numbers. """ hookset = [] small_elements = self.small_elements() gaps = self.gaps.copy() for s in small_elements: for gap in gaps: if gap > s: hookset.append(gap - s) element_counts = Counter(hookset) unique_elements = [element for element, count in element_counts.items() if count == 1] return unique_elements
[docs] def type(self): return len(self.pseudofrobenius_numbers())
[docs] def depth(self): return ceil((self.frobenius_number + 1)/self.multiplicity())
[docs] def remove_minimal_generator(self, n): """ Remove a minimal generator from a numerical semigroup. Parameters: n (int): The minimal generator to remove. Returns: NumericalSemigroup: The resulting numerical semigroup after removal. """ if not isinstance(n, int): raise ValueError("The first argument must be an integer.") msg = self.minimal_generating_set() if n not in msg: raise ValueError(f"{n} must be a minimal generator of the numerical semigroup.") gaps = list(self.gaps.copy()) gaps.append(n) return NumericalSemigroup(gaps=gaps)
[docs] def effective_generators(self): mingens = self.minimal_generating_set() frob = self.frobenius_number effective_gens = [egen for egen in mingens if egen > frob] return effective_gens
[docs] def get_children(self): gaps = self.gaps.copy() effective_gens = self.effective_generators() children = [NumericalSemigroup(gaps=list(gaps) + [egen]) for egen in effective_gens] return children
[docs] def get_parent(self): gaps = self.gaps.copy() gaps = set(gaps) gaps.remove(max(gaps)) return NumericalSemigroup(gaps)
[docs] def special_gaps(self): """ Compute the gaps that can be added to S and still yield a numerical semigroup. Returns ------- list of int The special gaps of S. """ gaps = self.gaps.copy() f = self.frobenius_number pf = self.pseudofrobenius_numbers() sgaps = [] for p in pf: mult = [p * i for i in range(2, (f // p) + 1)] if not any(m in gaps for m in mult): sgaps.append(p) return sgaps
[docs] def add_specialgap(self, p): """ Add a special gap to the numerical semigroup. """ if p not in self.special_gaps(): raise ValueError(f"{p} is not a special gap for the numerical semigroup.") gaps = self.gaps.copy() gaps = set(gaps) gaps.remove(p) return NumericalSemigroup(gaps=gaps)
[docs] def get_frobchildren(self): good_specialgaps = [p for p in self.special_gaps() if p != self.frobenius_number] children = [self.add_specialgap(p) for p in good_specialgaps] return children