ffspaces 0.2.0__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.
- ffspaces-0.2.0/PKG-INFO +77 -0
- ffspaces-0.2.0/README.md +61 -0
- ffspaces-0.2.0/pyproject.toml +30 -0
- ffspaces-0.2.0/setup.cfg +4 -0
- ffspaces-0.2.0/src/ffspaces/__init__.py +27 -0
- ffspaces-0.2.0/src/ffspaces/core.py +75 -0
- ffspaces-0.2.0/src/ffspaces/covers.py +120 -0
- ffspaces-0.2.0/src/ffspaces/fwht_operators.py +97 -0
- ffspaces-0.2.0/src/ffspaces/geometries.py +46 -0
- ffspaces-0.2.0/src/ffspaces/operators.py +146 -0
- ffspaces-0.2.0/src/ffspaces.egg-info/PKG-INFO +77 -0
- ffspaces-0.2.0/src/ffspaces.egg-info/SOURCES.txt +16 -0
- ffspaces-0.2.0/src/ffspaces.egg-info/dependency_links.txt +1 -0
- ffspaces-0.2.0/src/ffspaces.egg-info/requires.txt +4 -0
- ffspaces-0.2.0/src/ffspaces.egg-info/top_level.txt +1 -0
- ffspaces-0.2.0/tests/test_core_and_operators.py +142 -0
- ffspaces-0.2.0/tests/test_fwht_sumset_equivalence.py +50 -0
- ffspaces-0.2.0/tests/test_public_api.py +6 -0
ffspaces-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ffspaces
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: A computational laboratory for finite field vector spaces, Hamming balls, and sumset structural analysis.
|
|
5
|
+
Author: Nathan Hoehndorf
|
|
6
|
+
Project-URL: Homepage, https://github.com/nathanhoehndorf/finite-field-spaces
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: numpy>=1.24.0
|
|
14
|
+
Provides-Extra: test
|
|
15
|
+
Requires-Dist: pytest>=7.0; extra == "test"
|
|
16
|
+
|
|
17
|
+
# Finite-Field Spaces (`ffspaces`)
|
|
18
|
+
|
|
19
|
+
`ffspaces` is a lightweight Python toolkit for working with vectors over finite fields $\mathbb{F}_p^n$, with a focus on sumset analysis, linear-subspace structure, and Hamming-ball geometry.
|
|
20
|
+
|
|
21
|
+
The package is designed for small-to-medium computational experiments in additive combinatorics, coding theory, and related discrete geometry settings. It provides both direct implementations and faster specialized routines for common operations over finite fields.
|
|
22
|
+
|
|
23
|
+
## Current functionality
|
|
24
|
+
|
|
25
|
+
- **Finite-field vector spaces:** generate the full vector space $\mathbb{F}_p^n$ or construct random invertible basis changes.
|
|
26
|
+
- **Finite-field rank:** compute matrix rank over $\mathbb{F}_p$ using modular Gaussian elimination.
|
|
27
|
+
- **Sumsets:** compute $S+S$ for a set of vectors over $\mathbb{F}_p^n$, including a direct reference method and a faster FWHT-based implementation.
|
|
28
|
+
- **Subspace detection:** find the largest linear subspace contained in a sumset, with both greedy and exhaustive search modes.
|
|
29
|
+
- **Hamming-ball geometry:** generate standard Hamming balls, compute Hamming weights, and build balls under arbitrary linear transforms.
|
|
30
|
+
- **Cover constructions:** build unions of Hamming balls and compute complements relative to a universe.
|
|
31
|
+
|
|
32
|
+
## Features
|
|
33
|
+
|
|
34
|
+
- **NumPy-based operations:** vectorized generation and manipulation of finite-field vectors.
|
|
35
|
+
- **Field-aware linear algebra:** rank and invertibility checks performed over $\mathbb{F}_p$, not over the reals.
|
|
36
|
+
- **Flexible geometry tools:** support for binary and higher-characteristic experiments.
|
|
37
|
+
- **Reusable experiment helpers:** scripts and utilities in the `experiments/` directory for one-off analysis.
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
Clone the repository and install it in editable mode:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
git clone https://github.com/nathanhoehndorf/finite-field-spaces
|
|
45
|
+
cd finite-field-spaces
|
|
46
|
+
pip install -e .
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The package requires Python 3.10+ and NumPy.
|
|
50
|
+
|
|
51
|
+
## Mathematical background
|
|
52
|
+
|
|
53
|
+
This package works in the vector space $\mathbb{F}_p^n$, where each coordinate is taken modulo a prime $p$. In particular, $\mathbb{F}_2^n$ is the set of binary vectors of length $n$, with addition done coordinatewise modulo $2$.
|
|
54
|
+
|
|
55
|
+
- **What is $\mathbb{F}_p^n$?** A finite-dimensional vector space over the finite field with $p$ elements. For $p=2$, this is the space of binary vectors.
|
|
56
|
+
- **What is $S+S$?** For a set $S \subseteq \mathbb{F}_p^n$, the sumset $S+S$ is the set of all pairwise sums $x+y$ with $x,y \in S$, taken modulo $p$.
|
|
57
|
+
- **What is a Hamming ball?** A Hamming ball is the set of vectors within a given distance from a center, where distance is the number of coordinates in which two vectors differ (known as Hamming weight, or the Hamming metric).
|
|
58
|
+
- **What is the covering problem?** A covering problem asks whether a collection of balls (or other subsets) can be chosen so that their union contains all points in a given universe, or perhaps all points of a particular structure.
|
|
59
|
+
- **Why does this matter?** These objects are central in additive combinatorics, coding theory, and discrete geometry, and they provide a natural setting for studying how small sets expand under addition or how geometric configurations cover a space.
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
## Quick example
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
import numpy as np
|
|
66
|
+
from ffspaces import generate_space, compute_sumset, find_maximum_subspace_dimension
|
|
67
|
+
|
|
68
|
+
space = generate_space(3, p=2)
|
|
69
|
+
subset = space[[0, 1, 2, 4]]
|
|
70
|
+
sumset = compute_sumset(subset, p=2)
|
|
71
|
+
dimension = find_maximum_subspace_dimension(sumset, p=2)
|
|
72
|
+
print(dimension)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Experiments
|
|
76
|
+
|
|
77
|
+
One-off analysis and experiment scripts live in the `experiments/` directory so they stay separate from the installable package code in `src/`.
|
ffspaces-0.2.0/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Finite-Field Spaces (`ffspaces`)
|
|
2
|
+
|
|
3
|
+
`ffspaces` is a lightweight Python toolkit for working with vectors over finite fields $\mathbb{F}_p^n$, with a focus on sumset analysis, linear-subspace structure, and Hamming-ball geometry.
|
|
4
|
+
|
|
5
|
+
The package is designed for small-to-medium computational experiments in additive combinatorics, coding theory, and related discrete geometry settings. It provides both direct implementations and faster specialized routines for common operations over finite fields.
|
|
6
|
+
|
|
7
|
+
## Current functionality
|
|
8
|
+
|
|
9
|
+
- **Finite-field vector spaces:** generate the full vector space $\mathbb{F}_p^n$ or construct random invertible basis changes.
|
|
10
|
+
- **Finite-field rank:** compute matrix rank over $\mathbb{F}_p$ using modular Gaussian elimination.
|
|
11
|
+
- **Sumsets:** compute $S+S$ for a set of vectors over $\mathbb{F}_p^n$, including a direct reference method and a faster FWHT-based implementation.
|
|
12
|
+
- **Subspace detection:** find the largest linear subspace contained in a sumset, with both greedy and exhaustive search modes.
|
|
13
|
+
- **Hamming-ball geometry:** generate standard Hamming balls, compute Hamming weights, and build balls under arbitrary linear transforms.
|
|
14
|
+
- **Cover constructions:** build unions of Hamming balls and compute complements relative to a universe.
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
- **NumPy-based operations:** vectorized generation and manipulation of finite-field vectors.
|
|
19
|
+
- **Field-aware linear algebra:** rank and invertibility checks performed over $\mathbb{F}_p$, not over the reals.
|
|
20
|
+
- **Flexible geometry tools:** support for binary and higher-characteristic experiments.
|
|
21
|
+
- **Reusable experiment helpers:** scripts and utilities in the `experiments/` directory for one-off analysis.
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
Clone the repository and install it in editable mode:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
git clone https://github.com/nathanhoehndorf/finite-field-spaces
|
|
29
|
+
cd finite-field-spaces
|
|
30
|
+
pip install -e .
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The package requires Python 3.10+ and NumPy.
|
|
34
|
+
|
|
35
|
+
## Mathematical background
|
|
36
|
+
|
|
37
|
+
This package works in the vector space $\mathbb{F}_p^n$, where each coordinate is taken modulo a prime $p$. In particular, $\mathbb{F}_2^n$ is the set of binary vectors of length $n$, with addition done coordinatewise modulo $2$.
|
|
38
|
+
|
|
39
|
+
- **What is $\mathbb{F}_p^n$?** A finite-dimensional vector space over the finite field with $p$ elements. For $p=2$, this is the space of binary vectors.
|
|
40
|
+
- **What is $S+S$?** For a set $S \subseteq \mathbb{F}_p^n$, the sumset $S+S$ is the set of all pairwise sums $x+y$ with $x,y \in S$, taken modulo $p$.
|
|
41
|
+
- **What is a Hamming ball?** A Hamming ball is the set of vectors within a given distance from a center, where distance is the number of coordinates in which two vectors differ (known as Hamming weight, or the Hamming metric).
|
|
42
|
+
- **What is the covering problem?** A covering problem asks whether a collection of balls (or other subsets) can be chosen so that their union contains all points in a given universe, or perhaps all points of a particular structure.
|
|
43
|
+
- **Why does this matter?** These objects are central in additive combinatorics, coding theory, and discrete geometry, and they provide a natural setting for studying how small sets expand under addition or how geometric configurations cover a space.
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
## Quick example
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
import numpy as np
|
|
50
|
+
from ffspaces import generate_space, compute_sumset, find_maximum_subspace_dimension
|
|
51
|
+
|
|
52
|
+
space = generate_space(3, p=2)
|
|
53
|
+
subset = space[[0, 1, 2, 4]]
|
|
54
|
+
sumset = compute_sumset(subset, p=2)
|
|
55
|
+
dimension = find_maximum_subspace_dimension(sumset, p=2)
|
|
56
|
+
print(dimension)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Experiments
|
|
60
|
+
|
|
61
|
+
One-off analysis and experiment scripts live in the `experiments/` directory so they stay separate from the installable package code in `src/`.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ffspaces"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
authors = [{name="Nathan Hoehndorf"}]
|
|
9
|
+
description = "A computational laboratory for finite field vector spaces, Hamming balls, and sumset structural analysis."
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"License :: OSI Approved :: MIT License",
|
|
15
|
+
"Operating System :: OS Independent",
|
|
16
|
+
"Topic :: Scientific/Engineering :: Mathematics"]
|
|
17
|
+
dependencies = ["numpy>=1.24.0"]
|
|
18
|
+
optional-dependencies = { test = ["pytest>=7.0"] }
|
|
19
|
+
[project.urls]
|
|
20
|
+
"Homepage" = "https://github.com/nathanhoehndorf/finite-field-spaces"
|
|
21
|
+
|
|
22
|
+
[tool.setuptools]
|
|
23
|
+
license-files = []
|
|
24
|
+
|
|
25
|
+
[tool.setuptools.package-dir]
|
|
26
|
+
"" = "src"
|
|
27
|
+
|
|
28
|
+
[tool.setuptools.packages.find]
|
|
29
|
+
where = ["src"]
|
|
30
|
+
include = ["ffspaces*"]
|
ffspaces-0.2.0/setup.cfg
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from .core import generate_space, generate_random_basis, is_invertible
|
|
2
|
+
from .fwht_operators import vectors_to_ints, ints_to_vectors, compute_sumset_fwht
|
|
3
|
+
from .operators import (
|
|
4
|
+
_compute_sumset_original,
|
|
5
|
+
compute_sumset,
|
|
6
|
+
compare_sumset_methods,
|
|
7
|
+
find_maximum_subspace_dimension,
|
|
8
|
+
)
|
|
9
|
+
from .covers import generate_covering, complement
|
|
10
|
+
from .geometries import compute_hamming_weight, generate_hamming_ball, generate_standard_ball
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"generate_space",
|
|
14
|
+
"generate_random_basis",
|
|
15
|
+
"is_invertible",
|
|
16
|
+
"vectors_to_ints",
|
|
17
|
+
"ints_to_vectors",
|
|
18
|
+
"compute_sumset_fwht",
|
|
19
|
+
"_compute_sumset_original",
|
|
20
|
+
"compute_sumset",
|
|
21
|
+
"compare_sumset_methods",
|
|
22
|
+
"find_maximum_subspace_dimension",
|
|
23
|
+
"generate_covering",
|
|
24
|
+
"complement",
|
|
25
|
+
"compute_hamming_weight",
|
|
26
|
+
"generate_hamming_ball",
|
|
27
|
+
]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import itertools
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def generate_space(n: int, p: int = 2) -> np.ndarray:
|
|
7
|
+
"""
|
|
8
|
+
Generates all p^n vectors in the vector space F_p^n.
|
|
9
|
+
Returns a 2D numpy array of shape (p^n, n).
|
|
10
|
+
"""
|
|
11
|
+
elements = list(itertools.product(range(p), repeat=n))
|
|
12
|
+
return np.array(elements, dtype=np.int8)
|
|
13
|
+
|
|
14
|
+
def rank_mod_p(matrix, p: int = 2) -> int:
|
|
15
|
+
"""
|
|
16
|
+
Computes the rank of `matrix` over the finite field F_p using Guassian elimination
|
|
17
|
+
with modular arithmetic. Works for any prime p; matrix need not be square
|
|
18
|
+
"""
|
|
19
|
+
if p<=1:
|
|
20
|
+
raise ValueError("p must be a prime greater than 1")
|
|
21
|
+
|
|
22
|
+
A = np.array(matrix, dtype=np.int64, copy=True) % p
|
|
23
|
+
if A.ndim != 2:
|
|
24
|
+
raise ValueError("matrix must be 2-dimensional")
|
|
25
|
+
|
|
26
|
+
m, n = A.shape
|
|
27
|
+
rank = 0
|
|
28
|
+
for col in range(n):
|
|
29
|
+
pivot_row = None
|
|
30
|
+
for row in range(rank, m):
|
|
31
|
+
if A[row, col] % p != 0:
|
|
32
|
+
pivot_row = row
|
|
33
|
+
break
|
|
34
|
+
if pivot_row is None:
|
|
35
|
+
continue
|
|
36
|
+
|
|
37
|
+
if pivot_row != rank:
|
|
38
|
+
A[[rank, pivot_row], :] = A[[pivot_row, rank], :]
|
|
39
|
+
|
|
40
|
+
pivot_inverse = pow(int(A[rank, col]), -1, p)
|
|
41
|
+
for row in range(m):
|
|
42
|
+
if row == rank:
|
|
43
|
+
continue
|
|
44
|
+
factor = (A[row, col] * pivot_inverse) % p
|
|
45
|
+
if factor != 0:
|
|
46
|
+
A[row, :] = (A[row, :] - factor * A[rank, :]) % p
|
|
47
|
+
|
|
48
|
+
rank += 1
|
|
49
|
+
if rank == min(m, n):
|
|
50
|
+
break
|
|
51
|
+
|
|
52
|
+
return rank
|
|
53
|
+
|
|
54
|
+
def is_invertible(matrix: np.ndarray, p: int = 2) -> bool:
|
|
55
|
+
"""Checks if an n x n matrix is invertible over the finite field F_p."""
|
|
56
|
+
matrix = np.asarray(matrix)
|
|
57
|
+
if matrix.ndim != 2 or matrix.shape[0] != matrix.shape[1]:
|
|
58
|
+
return False
|
|
59
|
+
return rank_mod_p(matrix, p) == matrix.shape[0]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def generate_random_basis(n: int, p: int = 2, rng=None) -> np.ndarray:
|
|
63
|
+
"""Generates a random invertible n x n matrix over F_p to represent a basis change."""
|
|
64
|
+
if rng is None:
|
|
65
|
+
rng = np.random.default_rng()
|
|
66
|
+
|
|
67
|
+
while True:
|
|
68
|
+
matrix = rng.integers(0, p, size=(n, n)).astype(np.int8)
|
|
69
|
+
if is_invertible(matrix, p):
|
|
70
|
+
return matrix
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
from typing import Optional, Sequence
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from .fwht_operators import vectors_to_ints
|
|
6
|
+
from .geometries import generate_hamming_ball
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _generate_weight_ball(n: int, radius: int) -> np.ndarray:
|
|
10
|
+
"""
|
|
11
|
+
Generate all binary vectors in F_2^n with Hamming weight <= radius.
|
|
12
|
+
"""
|
|
13
|
+
import itertools
|
|
14
|
+
|
|
15
|
+
vectors = []
|
|
16
|
+
for weight in range(radius + 1):
|
|
17
|
+
for combo in itertools.combinations(range(n), weight):
|
|
18
|
+
vec = np.zeros(n, dtype=np.int8)
|
|
19
|
+
vec[list(combo)] = 1
|
|
20
|
+
vectors.append(vec)
|
|
21
|
+
if len(vectors) == 0:
|
|
22
|
+
return np.empty((0, n), dtype=np.int8)
|
|
23
|
+
return np.array(vectors, dtype=np.int8)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def generate_covering(
|
|
27
|
+
centers: Sequence[np.ndarray],
|
|
28
|
+
radii,
|
|
29
|
+
bases: Optional[Sequence[np.ndarray]] = None,
|
|
30
|
+
p: int = 2,
|
|
31
|
+
universe: Optional[np.ndarray] = None,
|
|
32
|
+
) -> np.ndarray:
|
|
33
|
+
"""
|
|
34
|
+
Produce the union of Hamming balls defined by `centers`, `radii` and `bases`.
|
|
35
|
+
|
|
36
|
+
- `centers`: sequence of center vectors (each length n)
|
|
37
|
+
- `radii`: either a single int or sequence matching centers
|
|
38
|
+
- `bases`: either None, a single linear transform, or sequence of linear transforms
|
|
39
|
+
- `universe`: optional full universe array; when provided `generate_hamming_ball`
|
|
40
|
+
will be used (supports arbitrary p). When omitted and p==2 a combinatorial
|
|
41
|
+
weight-based generation is used for speed.
|
|
42
|
+
|
|
43
|
+
Returns a numpy array of unique covered vectors (dtype=int8) with shape (M,n).
|
|
44
|
+
"""
|
|
45
|
+
if isinstance(radii, int):
|
|
46
|
+
radii = [radii] * len(centers)
|
|
47
|
+
if bases is None:
|
|
48
|
+
bases = [None] * len(centers)
|
|
49
|
+
if not (len(centers) == len(radii) == len(bases)):
|
|
50
|
+
raise ValueError("centers, radii and bases must have the same length")
|
|
51
|
+
|
|
52
|
+
centers = [np.array(c, dtype=np.int8) for c in centers]
|
|
53
|
+
n = centers[0].size if len(centers) > 0 else 0
|
|
54
|
+
|
|
55
|
+
covered_ints = None
|
|
56
|
+
covered_rows = []
|
|
57
|
+
|
|
58
|
+
if universe is None and p == 2:
|
|
59
|
+
weight_balls = {}
|
|
60
|
+
for c, r, B in zip(centers, radii, bases):
|
|
61
|
+
if r not in weight_balls:
|
|
62
|
+
weight_balls[r] = _generate_weight_ball(n, r)
|
|
63
|
+
W_r = weight_balls[r]
|
|
64
|
+
if B is None:
|
|
65
|
+
A = W_r.copy()
|
|
66
|
+
else:
|
|
67
|
+
A = (W_r @ B.T) % 2
|
|
68
|
+
ball = (A ^ c).astype(np.int8)
|
|
69
|
+
ints = vectors_to_ints(ball)
|
|
70
|
+
if covered_ints is None:
|
|
71
|
+
covered_ints = ints
|
|
72
|
+
else:
|
|
73
|
+
covered_ints = np.concatenate([covered_ints, ints])
|
|
74
|
+
|
|
75
|
+
if covered_ints is None:
|
|
76
|
+
return np.empty((0, n), dtype=np.int8)
|
|
77
|
+
|
|
78
|
+
unique_ints = np.unique(covered_ints)
|
|
79
|
+
from .fwht_operators import ints_to_vectors
|
|
80
|
+
|
|
81
|
+
return ints_to_vectors(unique_ints, n)
|
|
82
|
+
|
|
83
|
+
for c, r, B in zip(centers, radii, bases):
|
|
84
|
+
ball = generate_hamming_ball(universe, c, r, B, p)
|
|
85
|
+
covered_rows.append(ball.astype(np.int8))
|
|
86
|
+
|
|
87
|
+
if len(covered_rows) == 0:
|
|
88
|
+
return np.empty((0, n), dtype=np.int8)
|
|
89
|
+
|
|
90
|
+
all_covered = np.vstack(covered_rows)
|
|
91
|
+
if p == 2:
|
|
92
|
+
ints = vectors_to_ints(all_covered)
|
|
93
|
+
unique_ints = np.unique(ints)
|
|
94
|
+
from .fwht_operators import ints_to_vectors
|
|
95
|
+
|
|
96
|
+
return ints_to_vectors(unique_ints, all_covered.shape[1])
|
|
97
|
+
else:
|
|
98
|
+
return np.unique(all_covered, axis=0)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def complement(universe: np.ndarray, covered: np.ndarray) -> np.ndarray:
|
|
102
|
+
"""
|
|
103
|
+
Return rows of `universe` that are not in `covered`.
|
|
104
|
+
Both inputs are arrays of vectors (rows). Result preserves order from `universe`.
|
|
105
|
+
"""
|
|
106
|
+
if len(universe) == 0:
|
|
107
|
+
return universe.copy()
|
|
108
|
+
if len(covered) == 0:
|
|
109
|
+
return universe.copy()
|
|
110
|
+
|
|
111
|
+
n = universe.shape[1]
|
|
112
|
+
try:
|
|
113
|
+
covered_ints = vectors_to_ints(covered)
|
|
114
|
+
universe_ints = vectors_to_ints(universe)
|
|
115
|
+
covered_set = set(covered_ints.tolist())
|
|
116
|
+
mask = [i not in covered_set for i in universe_ints.tolist()]
|
|
117
|
+
return universe[np.array(mask, dtype=bool)]
|
|
118
|
+
except Exception:
|
|
119
|
+
covered_set = {tuple(row) for row in covered}
|
|
120
|
+
return np.array([row for row in universe if tuple(row) not in covered_set], dtype=np.int8)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def vectors_to_ints(vectors: np.ndarray, p: int = 2) -> np.ndarray:
|
|
5
|
+
"""
|
|
6
|
+
Converts a matrix of Z_p^n vectors (shape M x n) to a 1d array of integers.
|
|
7
|
+
"""
|
|
8
|
+
if len(vectors) == 0:
|
|
9
|
+
return np.array([], dtype=np.int64)
|
|
10
|
+
|
|
11
|
+
if p <= 1:
|
|
12
|
+
raise ValueError("p must be at least 2")
|
|
13
|
+
|
|
14
|
+
n = vectors.shape[1]
|
|
15
|
+
if np.any(vectors < 0) or np.any(vectors >= p):
|
|
16
|
+
raise ValueError("All vector entries must lie in {0, ..., p-1}")
|
|
17
|
+
|
|
18
|
+
powers = p ** np.arange(n - 1, -1, -1, dtype=np.int64)
|
|
19
|
+
return np.dot(vectors.astype(np.int64), powers).astype(np.int64)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def ints_to_vectors(ints: np.ndarray, n: int, p: int = 2) -> np.ndarray:
|
|
23
|
+
"""
|
|
24
|
+
Converts a 1d array of integers back to a matrix of Z_p^n vectors.
|
|
25
|
+
"""
|
|
26
|
+
if p <= 1:
|
|
27
|
+
raise ValueError("p must be at least 2")
|
|
28
|
+
|
|
29
|
+
if len(ints) == 0:
|
|
30
|
+
return np.empty((0, n), dtype=np.int8)
|
|
31
|
+
|
|
32
|
+
ints_expanded = ints.astype(np.int64)[:, np.newaxis]
|
|
33
|
+
digits = np.empty((len(ints), n), dtype=np.int8)
|
|
34
|
+
remaining = ints_expanded.copy()
|
|
35
|
+
|
|
36
|
+
for axis in range(n - 1, -1, -1):
|
|
37
|
+
digits[:, axis] = (remaining[:, 0] % p).astype(np.int8)
|
|
38
|
+
remaining = remaining // p
|
|
39
|
+
|
|
40
|
+
return digits
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def fwht(a: np.ndarray) -> np.ndarray:
|
|
44
|
+
"""
|
|
45
|
+
In-place Fast Walsh-Hadamard Transform of 1d array `a`.
|
|
46
|
+
Array size must be exactly 2^n. Fully vectorized in NumPy.
|
|
47
|
+
"""
|
|
48
|
+
n_elements = len(a)
|
|
49
|
+
n_bits = int(np.log2(n_elements))
|
|
50
|
+
|
|
51
|
+
if 1 << n_bits != n_elements:
|
|
52
|
+
raise ValueError("Array length must be a power of 2")
|
|
53
|
+
|
|
54
|
+
res = a.astype(np.float64)
|
|
55
|
+
|
|
56
|
+
for d in range(n_bits):
|
|
57
|
+
half_step = 1 << d
|
|
58
|
+
shape = (-1, 2, half_step)
|
|
59
|
+
res_reshaped = res.reshape(shape)
|
|
60
|
+
|
|
61
|
+
a0 = res_reshaped[:, 0, :].copy()
|
|
62
|
+
a1 = res_reshaped[:, 1, :].copy()
|
|
63
|
+
|
|
64
|
+
res_reshaped[:, 0, :] = a0 + a1
|
|
65
|
+
res_reshaped[:, 1, :] = a0 - a1
|
|
66
|
+
|
|
67
|
+
return res
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def compute_sumset_fwht(set_elements: np.ndarray, p: int = 2) -> np.ndarray:
|
|
71
|
+
"""
|
|
72
|
+
Computes S+S over Z_p^n using the Fourier transform on the finite abelian group.
|
|
73
|
+
Time Complexity: O(p^n * n * log p) for a dense transform over the universe size p^n.
|
|
74
|
+
Space Complexity: O(p^n)
|
|
75
|
+
"""
|
|
76
|
+
if len(set_elements) == 0:
|
|
77
|
+
return np.empty((0, set_elements.shape[1]), dtype=np.int8)
|
|
78
|
+
|
|
79
|
+
if p <= 1:
|
|
80
|
+
raise ValueError("p must be at least 2")
|
|
81
|
+
|
|
82
|
+
n = set_elements.shape[1]
|
|
83
|
+
universe_size = p ** n
|
|
84
|
+
|
|
85
|
+
ints = vectors_to_ints(set_elements, p=p)
|
|
86
|
+
|
|
87
|
+
indicator = np.zeros((p,) * n, dtype=np.complex128)
|
|
88
|
+
indicator.reshape(-1)[ints] = 1.0
|
|
89
|
+
|
|
90
|
+
transformed = np.fft.fftn(indicator, axes=tuple(range(n)))
|
|
91
|
+
transformed_squared = transformed * transformed
|
|
92
|
+
|
|
93
|
+
convolution = np.fft.ifftn(transformed_squared, axes=tuple(range(n))).real
|
|
94
|
+
sumset_ints = np.flatnonzero(np.rint(convolution).astype(np.int64) > 0)
|
|
95
|
+
sumset_vectors = ints_to_vectors(sumset_ints, n, p=p)
|
|
96
|
+
|
|
97
|
+
return sumset_vectors
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import itertools
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def generate_standard_ball(n: int, r: int) -> np.ndarray:
|
|
7
|
+
"""
|
|
8
|
+
Generates all vectors in F_2^n with Hamming weight <= r.
|
|
9
|
+
"""
|
|
10
|
+
vectors = []
|
|
11
|
+
for weight in range(r + 1):
|
|
12
|
+
for combo in itertools.combinations(range(n), weight):
|
|
13
|
+
vec = np.zeros(n, dtype=np.int8)
|
|
14
|
+
vec[list(combo)] = 1
|
|
15
|
+
vectors.append(vec)
|
|
16
|
+
return np.array(vectors, dtype=np.int8)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def compute_hamming_weight(vectors: np.ndarray) -> np.ndarray:
|
|
20
|
+
"""
|
|
21
|
+
Computes the Hamming weight for an array of vectors.
|
|
22
|
+
Input shape: (M, n) -> Output shape (M,)
|
|
23
|
+
"""
|
|
24
|
+
return np.count_nonzero(vectors, axis=1)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def generate_hamming_ball(
|
|
28
|
+
universe: np.ndarray,
|
|
29
|
+
center: np.ndarray,
|
|
30
|
+
radius: int,
|
|
31
|
+
linear_transform: np.ndarray,
|
|
32
|
+
p: int = 2,
|
|
33
|
+
) -> np.ndarray:
|
|
34
|
+
"""
|
|
35
|
+
Generates a Hamming ball centered at `center` with a given `radius`
|
|
36
|
+
under the basis defined by `linear_transform` over F_p^n.
|
|
37
|
+
|
|
38
|
+
B_{L}(v, r)= { x in F_p^n : weight( L(x-v) mod p ) <= r }
|
|
39
|
+
"""
|
|
40
|
+
shifted = (universe - center) % p
|
|
41
|
+
transformed = (shifted @ linear_transform.T) % p
|
|
42
|
+
|
|
43
|
+
weights = compute_hamming_weight(transformed)
|
|
44
|
+
mask = weights <= radius
|
|
45
|
+
|
|
46
|
+
return universe[mask]
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
from itertools import combinations, product
|
|
2
|
+
from typing import Optional, Tuple
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
from .core import rank_mod_p
|
|
7
|
+
from .fwht_operators import compute_sumset_fwht
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _compute_sumset_original(set_elements: np.ndarray, p: int = 2) -> np.ndarray:
|
|
11
|
+
"""
|
|
12
|
+
Computes the unique elements of the sumset S + S over F_p
|
|
13
|
+
using the direct broadcast-based approach.
|
|
14
|
+
"""
|
|
15
|
+
if len(set_elements) == 0:
|
|
16
|
+
return np.empty((0, set_elements.shape[1]), dtype=np.int8)
|
|
17
|
+
|
|
18
|
+
broadcasted_sum = (set_elements[:, np.newaxis, :] + set_elements[np.newaxis, :, :]) % p
|
|
19
|
+
reshaped_sums = broadcasted_sum.reshape(-1, set_elements.shape[1])
|
|
20
|
+
|
|
21
|
+
return np.unique(reshaped_sums, axis=0)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def compute_sumset(set_elements: np.ndarray, p: int = 2) -> np.ndarray:
|
|
25
|
+
"""
|
|
26
|
+
Computes the unique elements of the sumset S + S over Z_p^n.
|
|
27
|
+
This uses the generalized Fourier-transform fast path for all p >= 2.
|
|
28
|
+
"""
|
|
29
|
+
if p <= 1:
|
|
30
|
+
raise ValueError("p must be at least 2")
|
|
31
|
+
|
|
32
|
+
return compute_sumset_fwht(set_elements, p=p)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def compare_sumset_methods(n: int = 6, subset_size: int = 10, seed: Optional[int] = None) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
36
|
+
"""
|
|
37
|
+
Generates a random subset of F_2^n and compares the FWHT-based sumset against
|
|
38
|
+
the original direct implementation.
|
|
39
|
+
"""
|
|
40
|
+
if n < 1:
|
|
41
|
+
raise ValueError("n must be at least 1")
|
|
42
|
+
if subset_size < 1:
|
|
43
|
+
raise ValueError("subset_size must be at least 1")
|
|
44
|
+
|
|
45
|
+
universe = np.array(list(product([0, 1], repeat=n)), dtype=np.int8)
|
|
46
|
+
rng = np.random.default_rng(seed)
|
|
47
|
+
subset_indices = rng.choice(len(universe), size=subset_size, replace=False)
|
|
48
|
+
subset = universe[subset_indices]
|
|
49
|
+
|
|
50
|
+
fwht_sumset = compute_sumset(subset, p=2)
|
|
51
|
+
original_sumset = _compute_sumset_original(subset, p=2)
|
|
52
|
+
|
|
53
|
+
return subset, fwht_sumset, original_sumset
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _find_maximum_subspace_dimension_greedy(sumset: np.ndarray, p: int = 2) -> int:
|
|
57
|
+
"""Greedily build a maximal set of independent generators whose span is contained in sumset."""
|
|
58
|
+
n = sumset.shape[1]
|
|
59
|
+
sumset_set = {tuple(row) for row in sumset}
|
|
60
|
+
independent_generators = []
|
|
61
|
+
|
|
62
|
+
for candidate in sumset:
|
|
63
|
+
if np.all(candidate == 0):
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
valid_generator = True
|
|
67
|
+
current_span = [np.zeros(n, dtype=np.int8)]
|
|
68
|
+
|
|
69
|
+
for gen in independent_generators:
|
|
70
|
+
current_span += [(gen * k + s) % p for k in range(1, p) for s in current_span]
|
|
71
|
+
|
|
72
|
+
for vec in current_span:
|
|
73
|
+
for k in range(1, p):
|
|
74
|
+
test_vec = (vec + k * candidate) % p
|
|
75
|
+
if tuple(test_vec) not in sumset_set:
|
|
76
|
+
valid_generator = False
|
|
77
|
+
break
|
|
78
|
+
if not valid_generator:
|
|
79
|
+
break
|
|
80
|
+
|
|
81
|
+
if valid_generator:
|
|
82
|
+
test_matrix = np.array(independent_generators + [candidate])
|
|
83
|
+
if rank_mod_p(test_matrix, p) == len(independent_generators) + 1:
|
|
84
|
+
independent_generators.append(candidate)
|
|
85
|
+
|
|
86
|
+
return len(independent_generators)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def find_maximum_subspace_dimension(
|
|
90
|
+
sumset: np.ndarray,
|
|
91
|
+
p: int = 2,
|
|
92
|
+
exhaustive: bool = False,
|
|
93
|
+
max_combinations: Optional[int] = 10_000,
|
|
94
|
+
) -> int:
|
|
95
|
+
"""
|
|
96
|
+
Finds the dimension of the largest linear subspace completely contained within S+S.
|
|
97
|
+
By default uses a greedy iterative check of linearly independent generators. If
|
|
98
|
+
`exhaustive=True` it will search combinations of generators (starting from the
|
|
99
|
+
largest possible dimension) to find the maximum dimension whose span is fully
|
|
100
|
+
contained in `sumset`. A `max_combinations` limit can be supplied to avoid
|
|
101
|
+
combinatorial blowups in large experiments; if reached, the greedy result is returned.
|
|
102
|
+
"""
|
|
103
|
+
n = sumset.shape[1]
|
|
104
|
+
sumset_set = {tuple(row) for row in sumset}
|
|
105
|
+
|
|
106
|
+
if tuple(np.zeros(n, dtype=np.int8)) not in sumset_set:
|
|
107
|
+
return -1
|
|
108
|
+
|
|
109
|
+
max_possible_d = 0
|
|
110
|
+
size = len(sumset)
|
|
111
|
+
if size > 0:
|
|
112
|
+
while p ** (max_possible_d + 1) <= size and max_possible_d < n:
|
|
113
|
+
max_possible_d += 1
|
|
114
|
+
|
|
115
|
+
greedy_dimension = _find_maximum_subspace_dimension_greedy(sumset, p)
|
|
116
|
+
|
|
117
|
+
if exhaustive:
|
|
118
|
+
checked_combinations = 0
|
|
119
|
+
for d in range(max_possible_d, 0, -1):
|
|
120
|
+
nonzero_rows = [row for row in sumset if not np.all(row == 0)]
|
|
121
|
+
for combo in combinations(nonzero_rows, d):
|
|
122
|
+
if max_combinations is not None and checked_combinations >= max_combinations:
|
|
123
|
+
return greedy_dimension
|
|
124
|
+
|
|
125
|
+
checked_combinations += 1
|
|
126
|
+
mat = np.vstack(combo)
|
|
127
|
+
if rank_mod_p(mat, p) != d:
|
|
128
|
+
continue
|
|
129
|
+
|
|
130
|
+
all_in_sumset = True
|
|
131
|
+
for coeffs in product(range(p), repeat=d):
|
|
132
|
+
if all(c == 0 for c in coeffs):
|
|
133
|
+
vec = tuple(np.zeros(n, dtype=np.int8))
|
|
134
|
+
else:
|
|
135
|
+
vec_arr = sum((coeffs[i] * mat[i]) for i in range(d)) % p
|
|
136
|
+
vec = tuple(vec_arr.astype(np.int8))
|
|
137
|
+
if vec not in sumset_set:
|
|
138
|
+
all_in_sumset = False
|
|
139
|
+
break
|
|
140
|
+
|
|
141
|
+
if all_in_sumset:
|
|
142
|
+
return d
|
|
143
|
+
|
|
144
|
+
return 0
|
|
145
|
+
|
|
146
|
+
return greedy_dimension
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ffspaces
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: A computational laboratory for finite field vector spaces, Hamming balls, and sumset structural analysis.
|
|
5
|
+
Author: Nathan Hoehndorf
|
|
6
|
+
Project-URL: Homepage, https://github.com/nathanhoehndorf/finite-field-spaces
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: numpy>=1.24.0
|
|
14
|
+
Provides-Extra: test
|
|
15
|
+
Requires-Dist: pytest>=7.0; extra == "test"
|
|
16
|
+
|
|
17
|
+
# Finite-Field Spaces (`ffspaces`)
|
|
18
|
+
|
|
19
|
+
`ffspaces` is a lightweight Python toolkit for working with vectors over finite fields $\mathbb{F}_p^n$, with a focus on sumset analysis, linear-subspace structure, and Hamming-ball geometry.
|
|
20
|
+
|
|
21
|
+
The package is designed for small-to-medium computational experiments in additive combinatorics, coding theory, and related discrete geometry settings. It provides both direct implementations and faster specialized routines for common operations over finite fields.
|
|
22
|
+
|
|
23
|
+
## Current functionality
|
|
24
|
+
|
|
25
|
+
- **Finite-field vector spaces:** generate the full vector space $\mathbb{F}_p^n$ or construct random invertible basis changes.
|
|
26
|
+
- **Finite-field rank:** compute matrix rank over $\mathbb{F}_p$ using modular Gaussian elimination.
|
|
27
|
+
- **Sumsets:** compute $S+S$ for a set of vectors over $\mathbb{F}_p^n$, including a direct reference method and a faster FWHT-based implementation.
|
|
28
|
+
- **Subspace detection:** find the largest linear subspace contained in a sumset, with both greedy and exhaustive search modes.
|
|
29
|
+
- **Hamming-ball geometry:** generate standard Hamming balls, compute Hamming weights, and build balls under arbitrary linear transforms.
|
|
30
|
+
- **Cover constructions:** build unions of Hamming balls and compute complements relative to a universe.
|
|
31
|
+
|
|
32
|
+
## Features
|
|
33
|
+
|
|
34
|
+
- **NumPy-based operations:** vectorized generation and manipulation of finite-field vectors.
|
|
35
|
+
- **Field-aware linear algebra:** rank and invertibility checks performed over $\mathbb{F}_p$, not over the reals.
|
|
36
|
+
- **Flexible geometry tools:** support for binary and higher-characteristic experiments.
|
|
37
|
+
- **Reusable experiment helpers:** scripts and utilities in the `experiments/` directory for one-off analysis.
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
Clone the repository and install it in editable mode:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
git clone https://github.com/nathanhoehndorf/finite-field-spaces
|
|
45
|
+
cd finite-field-spaces
|
|
46
|
+
pip install -e .
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The package requires Python 3.10+ and NumPy.
|
|
50
|
+
|
|
51
|
+
## Mathematical background
|
|
52
|
+
|
|
53
|
+
This package works in the vector space $\mathbb{F}_p^n$, where each coordinate is taken modulo a prime $p$. In particular, $\mathbb{F}_2^n$ is the set of binary vectors of length $n$, with addition done coordinatewise modulo $2$.
|
|
54
|
+
|
|
55
|
+
- **What is $\mathbb{F}_p^n$?** A finite-dimensional vector space over the finite field with $p$ elements. For $p=2$, this is the space of binary vectors.
|
|
56
|
+
- **What is $S+S$?** For a set $S \subseteq \mathbb{F}_p^n$, the sumset $S+S$ is the set of all pairwise sums $x+y$ with $x,y \in S$, taken modulo $p$.
|
|
57
|
+
- **What is a Hamming ball?** A Hamming ball is the set of vectors within a given distance from a center, where distance is the number of coordinates in which two vectors differ (known as Hamming weight, or the Hamming metric).
|
|
58
|
+
- **What is the covering problem?** A covering problem asks whether a collection of balls (or other subsets) can be chosen so that their union contains all points in a given universe, or perhaps all points of a particular structure.
|
|
59
|
+
- **Why does this matter?** These objects are central in additive combinatorics, coding theory, and discrete geometry, and they provide a natural setting for studying how small sets expand under addition or how geometric configurations cover a space.
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
## Quick example
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
import numpy as np
|
|
66
|
+
from ffspaces import generate_space, compute_sumset, find_maximum_subspace_dimension
|
|
67
|
+
|
|
68
|
+
space = generate_space(3, p=2)
|
|
69
|
+
subset = space[[0, 1, 2, 4]]
|
|
70
|
+
sumset = compute_sumset(subset, p=2)
|
|
71
|
+
dimension = find_maximum_subspace_dimension(sumset, p=2)
|
|
72
|
+
print(dimension)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Experiments
|
|
76
|
+
|
|
77
|
+
One-off analysis and experiment scripts live in the `experiments/` directory so they stay separate from the installable package code in `src/`.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/ffspaces/__init__.py
|
|
4
|
+
src/ffspaces/core.py
|
|
5
|
+
src/ffspaces/covers.py
|
|
6
|
+
src/ffspaces/fwht_operators.py
|
|
7
|
+
src/ffspaces/geometries.py
|
|
8
|
+
src/ffspaces/operators.py
|
|
9
|
+
src/ffspaces.egg-info/PKG-INFO
|
|
10
|
+
src/ffspaces.egg-info/SOURCES.txt
|
|
11
|
+
src/ffspaces.egg-info/dependency_links.txt
|
|
12
|
+
src/ffspaces.egg-info/requires.txt
|
|
13
|
+
src/ffspaces.egg-info/top_level.txt
|
|
14
|
+
tests/test_core_and_operators.py
|
|
15
|
+
tests/test_fwht_sumset_equivalence.py
|
|
16
|
+
tests/test_public_api.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ffspaces
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
from ffspaces.core import generate_space, generate_random_basis, is_invertible, rank_mod_p
|
|
5
|
+
from ffspaces.operators import (
|
|
6
|
+
_compute_sumset_original,
|
|
7
|
+
compute_sumset,
|
|
8
|
+
find_maximum_subspace_dimension,
|
|
9
|
+
)
|
|
10
|
+
from ffspaces.geometries import generate_standard_ball
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.mark.parametrize(
|
|
14
|
+
("n", "p", "expected"),
|
|
15
|
+
[
|
|
16
|
+
(0, 2, 1),
|
|
17
|
+
(1, 2, 2),
|
|
18
|
+
(2, 2, 4),
|
|
19
|
+
(3, 3, 27),
|
|
20
|
+
],
|
|
21
|
+
)
|
|
22
|
+
def test_generate_space_edge_cases(n, p, expected):
|
|
23
|
+
space = generate_space(n, p=p)
|
|
24
|
+
assert space.shape == (expected, n)
|
|
25
|
+
assert np.array_equal(space[0], np.zeros(n, dtype=np.int8))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_generate_space_full_space_contains_all_vectors():
|
|
29
|
+
space = generate_space(3, p=2)
|
|
30
|
+
assert set(map(tuple, space)) == {
|
|
31
|
+
(0, 0, 0),
|
|
32
|
+
(0, 0, 1),
|
|
33
|
+
(0, 1, 0),
|
|
34
|
+
(0, 1, 1),
|
|
35
|
+
(1, 0, 0),
|
|
36
|
+
(1, 0, 1),
|
|
37
|
+
(1, 1, 0),
|
|
38
|
+
(1, 1, 1),
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_is_invertible_edge_cases():
|
|
43
|
+
assert is_invertible(np.array([[1]], dtype=np.int8), p=2)
|
|
44
|
+
assert not is_invertible(np.array([[0]], dtype=np.int8), p=2)
|
|
45
|
+
assert not is_invertible(np.array([[1, 0], [0, 0]], dtype=np.int8), p=2)
|
|
46
|
+
assert is_invertible(np.array([[1, 1], [1, 0]], dtype=np.int8), p=2)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_compute_sumset_handles_empty_and_singleton_sets():
|
|
50
|
+
empty = np.empty((0, 3), dtype=np.int8)
|
|
51
|
+
singleton = np.array([[1, 0, 1]], dtype=np.int8)
|
|
52
|
+
|
|
53
|
+
assert compute_sumset(empty, p=2).shape == (0, 3)
|
|
54
|
+
assert _compute_sumset_original(empty, p=2).shape == (0, 3)
|
|
55
|
+
|
|
56
|
+
expected_singleton = np.array([[0, 0, 0]], dtype=np.int8)
|
|
57
|
+
computed_singleton = compute_sumset(singleton, p=2)
|
|
58
|
+
assert computed_singleton.shape == (1, 3)
|
|
59
|
+
assert np.array_equal(computed_singleton, expected_singleton)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_compute_sumset_full_space_matches_original():
|
|
63
|
+
full_space = generate_space(3, p=2)
|
|
64
|
+
computed = compute_sumset(full_space, p=2)
|
|
65
|
+
original = _compute_sumset_original(full_space, p=2)
|
|
66
|
+
assert set(map(tuple, computed)) == set(map(tuple, original))
|
|
67
|
+
assert set(map(tuple, computed)) == set(map(tuple, full_space))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@pytest.mark.parametrize(
|
|
71
|
+
("sumset", "p", "expected"),
|
|
72
|
+
[
|
|
73
|
+
(np.array([[0, 0]], dtype=np.int8), 2, 0),
|
|
74
|
+
(np.array([[0, 0], [1, 0]], dtype=np.int8), 2, 1),
|
|
75
|
+
(np.array([[0, 0], [1, 0], [0, 1], [1, 1]], dtype=np.int8), 2, 2),
|
|
76
|
+
],
|
|
77
|
+
)
|
|
78
|
+
def test_find_maximum_subspace_dimension_edge_cases(sumset, p, expected):
|
|
79
|
+
assert find_maximum_subspace_dimension(sumset, p=p) == expected
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_find_maximum_subspace_dimension_exhaustive_matches_greedy():
|
|
83
|
+
sumset = np.array(
|
|
84
|
+
[
|
|
85
|
+
[0, 0, 0],
|
|
86
|
+
[1, 0, 0],
|
|
87
|
+
[0, 1, 0],
|
|
88
|
+
[1, 1, 0],
|
|
89
|
+
[0, 0, 1],
|
|
90
|
+
[1, 0, 1],
|
|
91
|
+
[0, 1, 1],
|
|
92
|
+
[1, 1, 1],
|
|
93
|
+
],
|
|
94
|
+
dtype=np.int8,
|
|
95
|
+
)
|
|
96
|
+
assert find_maximum_subspace_dimension(sumset, p=2) == 3
|
|
97
|
+
assert find_maximum_subspace_dimension(sumset, p=2, exhaustive=True) == 3
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_find_maximum_subspace_dimension_uses_field_rank_over_f2():
|
|
101
|
+
sumset = np.array(
|
|
102
|
+
[
|
|
103
|
+
[0, 0, 0, 0],
|
|
104
|
+
[1, 1, 0, 0],
|
|
105
|
+
[1, 0, 1, 0],
|
|
106
|
+
[0, 1, 1, 0],
|
|
107
|
+
],
|
|
108
|
+
dtype=np.int8,
|
|
109
|
+
)
|
|
110
|
+
assert find_maximum_subspace_dimension(sumset, p=2) == 2
|
|
111
|
+
assert find_maximum_subspace_dimension(sumset, p=2, exhaustive=True) == 2
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_find_maximum_subspace_dimension_exhaustive_respects_combinatorial_limit():
|
|
115
|
+
sumset = generate_space(6, p=2)
|
|
116
|
+
greedy = find_maximum_subspace_dimension(sumset, p=2)
|
|
117
|
+
assert find_maximum_subspace_dimension(sumset, p=2, exhaustive=True, max_combinations=10) == greedy
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_generate_standard_ball_regression_matches_expected_vectors():
|
|
121
|
+
ball = generate_standard_ball(3, 1)
|
|
122
|
+
expected = np.array(
|
|
123
|
+
[
|
|
124
|
+
[0, 0, 0],
|
|
125
|
+
[1, 0, 0],
|
|
126
|
+
[0, 1, 0],
|
|
127
|
+
[0, 0, 1],
|
|
128
|
+
],
|
|
129
|
+
dtype=np.int8,
|
|
130
|
+
)
|
|
131
|
+
assert ball.shape == (4, 3)
|
|
132
|
+
assert np.array_equal(ball, expected)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_generate_standard_ball_radius_zero_is_singleton_zero_vector():
|
|
136
|
+
ball = generate_standard_ball(4, 0)
|
|
137
|
+
assert ball.shape == (1, 4)
|
|
138
|
+
assert np.array_equal(ball, np.zeros((1, 4), dtype=np.int8))
|
|
139
|
+
|
|
140
|
+
def test_rank_mod_p_catches_dependency_that_looks_indepenent_over_reals():
|
|
141
|
+
a, b, c = [1,1,0,0], [1,0,1,0], [0,1,1,0]
|
|
142
|
+
assert rank_mod_p(np.array([a,b,c]), p=2) == 2
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
from ffspaces.core import generate_random_basis, is_invertible
|
|
5
|
+
from ffspaces.fwht_operators import compute_sumset_fwht
|
|
6
|
+
from ffspaces.operators import _compute_sumset_original, compare_sumset_methods
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SumsetFwhtComparisonTest(unittest.TestCase):
|
|
10
|
+
def test_random_subset_matches_fwht_and_original(self):
|
|
11
|
+
subset, fwht_sumset, original_sumset = compare_sumset_methods(n=6, subset_size=10, seed=7)
|
|
12
|
+
|
|
13
|
+
self.assertEqual(subset.shape[1], 6)
|
|
14
|
+
self.assertEqual(subset.shape[0], 10)
|
|
15
|
+
self.assertEqual({tuple(row) for row in fwht_sumset}, {tuple(row) for row in original_sumset})
|
|
16
|
+
|
|
17
|
+
def test_generate_random_basis_accepts_rng(self):
|
|
18
|
+
rng = np.random.default_rng(17)
|
|
19
|
+
basis = generate_random_basis(4, p=2, rng=rng)
|
|
20
|
+
basis_again = generate_random_basis(4, p=2, rng=np.random.default_rng(17))
|
|
21
|
+
|
|
22
|
+
self.assertEqual(basis.shape, (4, 4))
|
|
23
|
+
self.assertTrue(is_invertible(basis, p=2))
|
|
24
|
+
self.assertTrue(np.array_equal(basis, basis_again))
|
|
25
|
+
|
|
26
|
+
def test_general_fourier_sumset_matches_original_for_p3(self):
|
|
27
|
+
subset = np.array([
|
|
28
|
+
[0, 0],
|
|
29
|
+
[1, 0],
|
|
30
|
+
[0, 1],
|
|
31
|
+
[1, 1],
|
|
32
|
+
[2, 0],
|
|
33
|
+
[0, 2],
|
|
34
|
+
], dtype=np.int8)
|
|
35
|
+
|
|
36
|
+
fwht_sumset = compute_sumset_fwht(subset, p=3)
|
|
37
|
+
original_sumset = _compute_sumset_original(subset, p=3)
|
|
38
|
+
|
|
39
|
+
self.assertEqual({tuple(row) for row in fwht_sumset}, {tuple(row) for row in original_sumset})
|
|
40
|
+
|
|
41
|
+
def test_is_invertible_uses_rank_over_f2(self):
|
|
42
|
+
invertible = np.array([[1, 1], [1, 0]], dtype=np.int8)
|
|
43
|
+
singular = np.array([[1, 1], [1, 1]], dtype=np.int8)
|
|
44
|
+
|
|
45
|
+
self.assertTrue(is_invertible(invertible, p=2))
|
|
46
|
+
self.assertFalse(is_invertible(singular, p=2))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
if __name__ == "__main__":
|
|
50
|
+
unittest.main()
|