import numpy as np
import math
import galois
import seaborn as sns
import pandas as pd
import matplotlib.pyplot as plt
import plotly.express as px
from itertools import combinations_with_replacement, permutations, product
from typing import Dict, Tuple, List
from fractions import Fraction
[docs]
def generate_D_vectors(t: int, m: int, s: int) -> np.ndarray:
"""
Generates all possible integer vectors D = (d_1, ..., d_s) for a (t, m, s)-net.
These vectors satisfy d_i >= 0 and sum(d_i) = m - t, and they define the elementary intervals used for the verification check.
"""
n = m - t
D_list = []
for D in combinations_with_replacement(range(n + 1), s):
if sum(D) == n:
D_list.extend(set(permutations(D)))
D_array = np.array(D_list)
return np.unique(D_array, axis=0)
[docs]
def generate_D_A_pairs(t: int, m: int, s: int, b: int) -> Dict[Tuple[int, ...], List[Tuple[int, ...]]]:
"""
Generates a dictionary that maps each vector D to a list of all possible corresponding vectors A.
For a given D=(d_1, ..., d_s), a vector A is (a_1, ..., a_s) where 0 <= a_j < b^d_j for each j.
"""
D_array = generate_D_vectors(t, m, s)
D_A_to_index = {}
for D in D_array:
A_ranges = [range(b ** d_i) for d_i in D]
D_A_to_index[tuple(D)] = list(product(*A_ranges))
return D_A_to_index
[docs]
def convert_points_to_fractions(points: np.ndarray, b: int) -> np.ndarray:
"""
Converts an array of floating-point coordinates to an array of Fraction objects.
This is crucial for exact arithmetic when checking if points fall into b-adic intervals.
"""
points_fractions = []
for point in points:
point_fractions_row = []
for x in point:
if isinstance(x, Fraction):
point_fractions_row.append(x)
continue
x_float = float(f"{x:.20f}".rstrip('0').rstrip('.'))
if x_float < 1e-10:
point_fractions_row.append(Fraction(0, 1))
elif x_float > 1 - 1e-10:
point_fractions_row.append(Fraction(1, 1))
else:
found = False
for n in range(1, 15):
denominator = b**n
numerator = int(round(x_float * denominator))
if abs(x_float - numerator / denominator) < 1e-10:
point_fractions_row.append(Fraction(numerator, denominator))
found = True
break
if not found:
point_fractions_row.append(Fraction(str(x_float)))
points_fractions.append(point_fractions_row)
return np.array(points_fractions)
def _is_tms_network(points_fractions: np.ndarray, t: int, m: int, s: int, b: int) -> bool:
"""
Checks whether the set of points forms a (t,m,s)-net in base b.
Returns True if the points form a (t,m,s)-net, False otherwise.
:param points: An array of points to be checked for the (t, m, s)-net property.
:param t: The quality parameter t.
:param m: The number of points in the net is b^m.
:param s: The dimension of the net.
:param b: The base of the (t, m, s)-net.
"""
D_A_pairs = generate_D_A_pairs(t, m, s, b)
for D, A_list in D_A_pairs.items():
for A in A_list:
count = 0
lower_bounds = [Fraction(A[j], b**D[j]) for j in range(s)]
upper_bounds = [Fraction(A[j] + 1, b**D[j]) for j in range(s)]
for point in points_fractions:
if all(lower_bounds[j] <= point[j] < upper_bounds[j] for j in range(s)):
count += 1
if count != b**t:
return False
return True
[docs]
class TMSNet:
"""
A class for storing, analyzing, and visualizing (t,m,s)-networks.
:param b: base of (t, m, s)-net. Using for calculation over GF(b).
:param t: quality measure of (t, m, s)-net.
:param m: the number of points in the (t, m, s)-net is characterized as b^m
:param s: dimension of (t, m, s)-net
"""
def __init__(self, t, m, s, b, points):
"""
Initializes an instance of the (t,m,s)-network.
"""
self.t = t
self.m = m
self.s = s
self.b = b
self.points = points
print(f"A ({self.t}, {self.m}, {self.s})-network is created on base {self.b}, containing {self.points.shape[0]} points.")
[docs]
def visualize(self):
"""
Visualizes network points.
Supports both 2D and 3D cases.
"""
if self.points.shape[1] < 2:
print("Visualization is only possible for s >= 2.")
return
if self.points.shape[1] == 2:
df = pd.DataFrame(self.points, columns=["x", "y"])
plt.figure(figsize=(8, 8))
sns.scatterplot(data=df, x="x", y="y", s=20).set_title(f'({self.t}, {self.m}, {self.s})-сеть')
plt.grid(True)
plt.show()
elif self.points.shape[1] == 3:
df = pd.DataFrame(self.points, columns=["x", "y", "z"])
fig = px.scatter_3d(df, x='x', y='y', z='z', title=f'({self.t}, {self.m}, {self.s})-сеть')
fig.update_traces(marker=dict(size=3))
fig.show()
else:
print(f"Visualization for s={self.s} is not supported. Only the first 2 dimensions will be shown.")
df = pd.DataFrame(self.points[:, :2], columns=["x", "y"])
plt.figure(figsize=(8, 8))
sns.scatterplot(data=df, x="x", y="y", s=20).set_title(f'({self.t}, {self.m}, {self.s})-сеть (первые 2 измерения)')
plt.grid(True)
plt.show()
[docs]
def verify(self) -> bool:
"""
Checks whether a set of points is a (t,m,s)-network.If the check with the initial 't' fails,
the function finds the smallest possible value of t for which the points form a valid network and updates self.t.
"""
print(f"--- Starting verification for t = {self.t}... ---")
if self.points.shape[0] != self.b**self.m:
print(f"Critical error: For a (t, m, s)-network of radix {self.b} with m={self.m} there should be {self.b**self.m} points, but {self.points.shape[0]} is provided.")
print("Verification interrupted.")
return False
points_fractions = convert_points_to_fractions(self.points, self.b)
best_t_found = -1
for new_t in range(self.m + 1):
if _is_tms_network(points_fractions, new_t, self.m, self.s, self.b):
best_t_found = new_t
break
if best_t_found == -1:
print("Verification failed: No suitable value for t could be found.")
return False
if best_t_found == self.t:
print(f"Verification is successful: the points indeed form a ({self.t}, {self.m}, {self.s})-network.")
return True
else:
old_t = self.t
self.t = best_t_found
print(f"Initial check for t={old_t} failed.")
print(f"---Updating---")
print(f"The best value of t for a given set of points is found: t = {self.t}.")
print(f"The object's t parameter was updated from {old_t} to {self.t}.")
return True
[docs]
class PolynomialNetConstructor:
"""
A class implementing algorithms for constructing (t,m,s)-networks using polynomials over finite fields.
Current implementation include Niederreiter algorithm and Rosenbloom-Tsfasman algorithm.
"""
[docs]
@staticmethod
def is_prime(n):
if n < 2: return False
if n == 2: return True
if n % 2 == 0: return False
for i in range(3, int(n ** 0.5) + 1, 2):
if n % i == 0: return False
return True
[docs]
@staticmethod
def is_prime_power(n):
if n < 2: return False
if PolynomialNetConstructor.is_prime(n): return True
for p in range(2, int(n ** 0.5) + 1):
if PolynomialNetConstructor.is_prime(p):
power = p
while power <= n:
if power == n: return True
if n % power != 0: break
power *= p
return False
@staticmethod
def _e_param(t, m, s):
n = t + s
x = s
if n < x: return None
result = [1] * x
remaining = n - x
i = 0
while remaining > 0:
result[i] += 1
remaining -= 1
i = (i + 1) % x
return result
@staticmethod
def _generate_excellent_poly(b, e, s):
assert PolynomialNetConstructor.is_prime_power(b), "b must be a prime power"
pi = []
unique_polys = {degree: set() for degree in set(e)}
for deg in set(e):
all_irred = list(galois.irreducible_polys(b, deg))
all_irred = [p for p in all_irred if p != galois.Poly([1, 0], field=galois.GF(b))]
if len(all_irred) < e.count(deg):
raise ValueError(f"There are not enough irreducible polynomials of degree {deg} in GF({b}) for {e.count(deg)} queries ({len(all_irred)} available).")
unique_polys[deg] = set(all_irred)
used = {deg: set() for deg in set(e)}
for deg in e:
available = sorted(unique_polys[deg] - used[deg], key=lambda p: tuple(p.coeffs))
P = available[0]
used[deg].add(P)
pi.append(P)
return pi
@staticmethod
def _generate_recurrent_sequence(poly, u, m):
e = poly.degree
degree = e * u
poly_u = poly ** u
coeffs = poly_u.coeffs
GF = poly.field
alpha = [GF(0)] * (e * (u - 1))
alpha += [GF(1)] + [GF(0)] * (degree - (e * (u - 1)) - 1)
while len(alpha) < m + degree:
acc = GF(0)
for k in range(1, degree + 1):
acc -= coeffs[k] * alpha[-k]
alpha.append(acc)
return alpha
@staticmethod
def _build_generator_matrix(poly, m):
e = poly.degree
num_sections = (m + e - 1) // e
G = np.zeros((m, m), dtype=int)
for u in range(1, num_sections + 1):
alpha = PolynomialNetConstructor._generate_recurrent_sequence(poly, u, m)
r_h = e - 1 if u < num_sections else (m - 1) % e
for r in range(r_h + 1):
j = e * (u - 1) + r
if j >= m:
break
for k in range(m):
G[j, k] = int(alpha[r + k])
return G
@staticmethod
def _generate_generator_matrices(b, t, m, s, verbose=False):
e = PolynomialNetConstructor._e_param(t, m, s)
assert e is not None, "Wrong params: t, m, s"
assert t <= m, "t must be less or equal m"
pi_list = PolynomialNetConstructor._generate_excellent_poly(b, e, s)
if verbose:
print("list of polys:", pi_list)
matrices = []
for i in range(s):
G = PolynomialNetConstructor._build_generator_matrix(pi_list[i], m)
matrices.append(G)
return matrices
@staticmethod
def _vecbm_opt(b, m, n):
"""
Converts an array of numbers n to their b-ary representations of fixed length m
:param b: base of (t, m, s)-net. Using for calculation over GF(b).
:param m: the number of points in the (t, m, s)-net is characterized as b^m
"""
n = np.asarray(n)
shape = n.shape
n = n.ravel()
x = (n[:, None] // b**np.arange(m)) % b
return x.reshape(*shape, m)
[docs]
@staticmethod
def construct_niederreiter(b, t, m, s, verbose=False):
"""
Constructing (t, m, s)-net via Niederreiter algorithm
:param b: base of (t, m, s)-net. Using for calculation over GF(b).
:param t: quality measure of (t, m, s)-net.
:param m: the number of points in the (t, m, s)-net is characterized as b^m
:param s: dimension of (t, m, s)-net
"""
gf = galois.GF(b)
G = PolynomialNetConstructor._generate_generator_matrices(b, t, m, s, verbose)
if verbose:
for i, matrix in enumerate(G):
print(f"generating matricies G_{i+1}:\n{matrix}\n")
n_values = np.arange(b**m)
vecs = PolynomialNetConstructor._vecbm_opt(b, m, n_values)
G_gf = gf(G)
vecs_gf = gf((vecs.T)[::-1])
result = np.empty((s,m,b**m), dtype=object)
for i in range(s):
result[i] = G_gf[i] @ vecs_gf
powers = b ** np.arange(m-1, -1, -1)
rnums = np.tensordot(result, powers, axes=(1, 0))
points = (rnums.T) * (b**(-m))
return points
[docs]
@staticmethod
def construct_rosenbloom_tsfasman(q, m, s, beta=None):
"""
Constructs a (0, m, s)-network using the Rosenblum-Tsfasman method.Due to its combinatorial nature, this method takes a long time to work.
:param q: base of (0, m, s)-net. Using for calculation over GF(q).
:param m: the number of points in the (0, m, s)-net is characterized as q^m
:param s: dimension of (0, m, s)-net
"""
GF = galois.GF(q)
if s > q:
raise ValueError("s must be least or equal q")
S_gf = GF.elements[:s]
is_default_beta = beta is None
if not is_default_beta:
beta_keys = np.array(sorted(list(beta.keys())))
if np.array_equal(beta_keys, np.arange(q)):
beta_lookup_array = np.array([beta[i] for i in range(q)], dtype=np.float64)
beta_map_func = lambda x_int_array: beta_lookup_array[x_int_array.astype(np.int64)]
else:
_beta_vec = np.vectorize(lambda val_int: beta.get(val_int), otypes=[np.float64])
beta_map_func = lambda x_int_array: _beta_vec(x_int_array)
coeffs_int_tuples = product(range(q), repeat=m)
poly_list = [galois.Poly(list(c_tuple)[::-1], field=GF) for c_tuple in coeffs_int_tuples]
num_polynomials = q**m
points = np.zeros((num_polynomials, s), dtype=np.float64)
q_powers = q**(-(np.arange(m, dtype=np.float64) + 1.0))
eval_matrix_int = np.zeros((s, m), dtype=np.int64)
for idx_f, f_poly in enumerate(poly_list):
current_deriv_poly = f_poly
for j_deriv_order in range(m):
eval_results_gf = current_deriv_poly(S_gf)
eval_matrix_int[:, j_deriv_order] = eval_results_gf.astype(np.int64)
if j_deriv_order < m - 1:
current_deriv_poly = current_deriv_poly.derivative()
if is_default_beta:
digits_matrix = eval_matrix_int.astype(np.float64)
else:
digits_matrix = beta_map_func(eval_matrix_int)
points[idx_f, :] = np.dot(digits_matrix, q_powers)
return points