kunz

pocketpartition.core.kunz

Kunz coordinate machinery for numerical semigroups.

Sub-modules

_coords : _compute_kunz_coords, kunz_tuple _vector : KunzVector _fourier : FourierKunzVector _distance : kunz_distance _polyhedron : KunzPolyhedron

All public names are re-exported from this package so that existing imports of the form from pocketpartition.core.kunz import ... continue to work without modification.

pocketpartition.core.kunz.kunz_tuple(S)[source]

Return the Kunz coordinate tuple of a numerical semigroup.

For a semigroup S with multiplicity m, the Kunz tuple is the vector (w_1, ..., w_{m-1}) where w_i = min{ n in S : n i (mod m) } / m.

Parameters:

S (NumericalSemigroup)

Return type:

tuple

Returns:

tuple of int – Length m - 1, all entries positive.

Examples

>>> from pocketpartition import NumericalSemigroup
>>> from pocketpartition.core.kunz import kunz_tuple
>>> S = NumericalSemigroup(generators=[3, 4, 5])
>>> kunz_tuple(S)
(1, 1)
class pocketpartition.core.kunz.KunzVector(S)[source]

Bases: tuple

The Kunz coordinate vector of a NumericalSemigroup, stored as an immutable tuple of positive integers of length m-1.

Entry k (0-indexed) is w_{k+1} = min{ n in S : n ≡ k+1 (mod m) } / m, where m is the multiplicity of S.

Because KunzVector subclasses tuple, it supports all standard tuple operations. The originating semigroup is accessible via .semigroup.

property semigroup: NumericalSemigroup

The NumericalSemigroup this vector was built from.

property multiplicity: int

The multiplicity of the underlying semigroup (equals m, the vector length plus one).

property genus: int

The genus (number of gaps) of the underlying semigroup.

property frobenius_number: int

The Frobenius number (largest gap) of the underlying semigroup.

coord(i)[source]

Return w_i (1-indexed). i must satisfy 1 <= i <= m-1.

Return type:

int

class pocketpartition.core.kunz.FourierKunzVector(source)[source]

Bases: object

The normalized Kunz function viewed as a step function on the circle S^1.

Starting from a KunzVector v = (w_1, …, w_{m-1}) we build:

f : S^1 → [0, 1] f(i/m) = w_i / max_j(w_j) for i = 1, …, m-1 f(0) = 0 (convention: 0 residue class maps to 0)

The domain is discretised as {0, 1/m, 2/m, …, (m-1)/m} ⊂ [0,1) and f is extended to all of [0,1) as a right-continuous step function (piecewise-constant on each interval [i/m, (i+1)/m)).

Parameters:

source (KunzVector | NumericalSemigroup) – Either a KunzVector or a NumericalSemigroup (converted automatically).

property kunz_vector: KunzVector
property multiplicity: int
property grid_points: tuple[float, ...]

The m normalised domain points {0, 1/m, …, (m-1)/m}.

property grid_values: tuple[float, ...]

Normalised function values at each grid point.

evaluate(x)[source]

Alias for __call__.

Return type:

float

fourier_coefficient(n)[source]

Compute the n-th Fourier coefficient of f.

Because f is piecewise-constant on each half-open interval [i/m, (i+1)/m), the integral

\[c_n = \int_0^1 f(x)\, e^{-2\pi i n x}\, dx\]

reduces exactly to a finite sum. Integrating f(i/m) * e^{-2πinx} over [i/m, (i+1)/m) gives (1/m) * f(i/m) * e^{-2πi n (i/m)}, so:

\[c_n = \frac{1}{m} \sum_{i=0}^{m-1} f\!\left(\frac{i}{m}\right) e^{-2\pi i n i/m}\]

This is the standard DFT of the grid values, scaled by 1/m.

Parameters:

n (int) – Fourier mode index.

Return type:

complex

Returns:

complex – The n-th Fourier coefficient c_n.

fourier_coefficients(n_max)[source]

Compute Fourier coefficients c_n for n = -n_max, …, n_max.

Uses numpy’s FFT when available (O(m log m)), otherwise falls back to the per-coefficient DFT loop (O(m * n_max)).

Return type:

dict[int, complex]

Returns:

dict mapping int -> complex

partial_sum(x, n_max)[source]

Evaluate the Fourier partial sum \(S_{n_{\max}}(x) = \sum_{|n| \le n_{\max}} c_n e^{2\pi i n x}\).

Useful for visualising how well the Fourier series reconstructs f.

Parameters:
  • x (float) – Point in [0, 1).

  • n_max (int) – Number of modes on each side.

Return type:

float

Returns:

float – Real part of the partial sum (imaginary part is ~0 for real f).

distance(other, norm='L2')[source]

Compute the distance between this FourierKunzVector and another.

Both functions are evaluated on a common grid of size lcm(m_self, m_other), which is the finest grid on which both step functions are simultaneously constant.

Parameters:
  • other (FourierKunzVector)

  • norm ({“L1”, “L2”, “Linf”}) – Which norm to use:

    • "L1"\(\int_0^1 |f(x) - g(x)| \, dx\)

    • "L2"\(\left(\int_0^1 |f(x) - g(x)|^2 \, dx\right)^{1/2}\) (default)

    • "Linf"\(\sup_{x \in [0,1)} |f(x) - g(x)|\)

Return type:

float

Returns:

float – The distance ≥ 0.

Raises:
  • ValueError – If norm is not one of the supported values.

  • TypeError – If other is not a FourierKunzVector, or if norm is not a string.

pocketpartition.core.kunz.kunz_distance(S, T, norm='L2')[source]

Compute the distance between two numerical semigroups (or already-built Kunz / FourierKunz vectors) using their normalised Kunz step functions.

Parameters:
  • S, T (NumericalSemigroup | KunzVector | FourierKunzVector) – The two objects to compare. NumericalSemigroup and KunzVector inputs are automatically converted to FourierKunzVector.

  • norm ({“L1”, “L2”, “Linf”}) – Which norm to use (default "L2"):

    • "L1" — sum of absolute differences (normalised by grid size)

    • "L2" — root-mean-square differences (default)

    • "Linf" — maximum absolute difference

Returns:

float – d(f_S, f_T) ≥ 0, where equality holds iff the two normalised Kunz step functions are identical.

Return type:

float

Examples

>>> from pocketpartition import NumericalSemigroup, kunz_distance
>>> S = NumericalSemigroup(generators=[3, 4, 5])
>>> T = NumericalSemigroup(generators=[3, 5])
>>> kunz_distance(S, T)                  # L2 (default)  
0.816...
>>> kunz_distance(S, T, norm="L1")       
0.666...
>>> kunz_distance(S, T, norm="Linf")     
1.0...
class pocketpartition.core.kunz.KunzPolyhedron(m)[source]

Bases: object

The Kunz polyhedron for a given multiplicity m.

The Kunz polyhedron P(m) is the rational polyhedral cone in R^{m-1} defined by the inequalities:

  • c_i + c_j >= c_{i+j} for all 1 <= i <= j <= m-1 with i+j < m

  • c_i + c_j + 1 >= c_{i+j-m} for all 1 <= i <= j <= m-1 with i+j > m

Every numerical semigroup of multiplicity m corresponds to an integer lattice point inside P(m), and conversely every such lattice point with all positive coordinates determines a numerical semigroup.

Parameters:

m (int) – The multiplicity. Must be a positive integer.

m

The multiplicity this polyhedron was built for.

Type:

int

corner

The m equally-spaced points (0, 1/m, 2/m, ..., (m-1)/m), representing the origin corner of the polyhedron in the normalised coordinate chart.

Type:

tuple of float

Examples

>>> from pocketpartition.core.kunz import KunzPolyhedron, kunz_tuple
>>> from pocketpartition import NumericalSemigroup
>>> kp = KunzPolyhedron(3)
>>> S = NumericalSemigroup(generators=[3, 4, 5])
>>> kp.is_point(kunz_tuple(S))
True
is_point(p)[source]

Check whether a coordinate vector lies inside the Kunz polyhedron.

Parameters:

p (tuple of int or float) – A vector of length m - 1. Entry p[k] represents the coordinate c_{k+1} (1-indexed residue k+1 mod m).

Return type:

bool

Returns:

boolTrue if p satisfies all Kunz polyhedron inequalities, False otherwise.

Notes

The inequalities checked are, for all 1 <= i <= j <= m-1:

  • i + j < mc_i + c_j >= c_{i+j}

  • i + j > mc_i + c_j + 1 >= c_{i+j-m}

  • i + j == m → always satisfied for non-negative coordinates

Raises:

ValueError – If p does not have length m - 1.

Examples

>>> kp = KunzPolyhedron(3)
>>> kp.is_point((1, 1))   # Kunz tuple of <3, 4, 5>
True
>>> kp.is_point((-1, 1))  # negative coordinate
False