diff --git a/pyproject.toml b/pyproject.toml index 03803e076..538466d5e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,6 +68,8 @@ dev = [ "pytest-cov[toml]", "pytest-xdist", "pytest-benchmark >= 4.0.0", + "requests", + "pdfminer.six" ] [project.urls] diff --git a/scripts/create_irreducible_polys_database.py b/scripts/create_irreducible_polys_database.py new file mode 100644 index 000000000..44e8b9045 --- /dev/null +++ b/scripts/create_irreducible_polys_database.py @@ -0,0 +1,114 @@ +""" +A script to create a database of irreducible polynomials + +Sources: + - Gadiel Seroussi. Table of Low-Weight Binary Irreducible Polynomials (1998): https://www.hpl.hp.com/techreports/98/HPL-98-135.html + +""" +from __future__ import annotations + +import os +import sqlite3 +from pathlib import Path + +import requests +import hashlib +import io +from pdfminer.high_level import extract_text + + +def main(): + """ + The main routine to create a database of irreducible polynomials + """ + + database_file = Path(__file__).parent.parent / "src" / "galois" / "_databases" / "irreducible_polys.db" + conn, cursor = create_database(database_file) + + _add_hpl_1998(conn, cursor) + + conn.close() + + +def create_database(file: Path) -> tuple[sqlite3.Connection, sqlite3.Cursor]: + """ + Deletes the old database, makes a new one, and returns the database connection. + """ + if file.exists(): + os.remove(file) + + conn = sqlite3.connect(file) + cursor = conn.cursor() + create_table(conn, cursor) + + return conn, cursor + + +def create_table(conn: sqlite3.Connection, cursor: sqlite3.Cursor): + """ + Creates an empty 'polys' table. + """ + cursor.execute( + """ + CREATE TABLE polys ( + characteristic INTEGER NOT NULL, + degree INTEGER NOT NULL, + nonzero_degrees TEXT NOT NULL, + nonzero_coeffs TEXT NOT NULL, + PRIMARY KEY (characteristic, degree) + ) + """ + ) + conn.commit() + + +def add_to_database( + cursor: sqlite3.Cursor, characteristic: int, degree: int, nonzero_degrees: str, nonzero_coeffs: str +): + """ + Adds the given irreducible polynomial to the database. + """ + cursor.execute( + """ + INSERT INTO polys (characteristic, degree, nonzero_degrees, nonzero_coeffs) + VALUES (?,?,?,?) + """, + (characteristic, degree, nonzero_degrees, nonzero_coeffs), + ) + + +def _add_hpl_1998(conn, cursor): + """ + Add Gadiel Seroussi's table to the database. + GF(2^m) for 2 <= m <= 10_000 + """ + url = "https://www.hpl.hp.com/techreports/98/HPL-98-135.pdf" + # There is an issue with the SSL certificate using CURL_CA_BUNDLE + # We don't validate https, but we do check the PDF's checksum + pdf = requests.get(url, stream=True, verify=False).content + sha256 = hashlib.sha256() + sha256.update(pdf) + assert sha256.hexdigest() == "78f02d84a0957ad261c53a0d1107adb2ff9d72f52ba5e10ea77eaa8cf766a0ee" + + coefficients = [] + print("Parsing Table of Low-Weight Binary Irreducible Polynomials (1998)...") + for page in range(3, 16): + text = extract_text(io.BytesIO(pdf), page_numbers=[page]) # extract_text doesn't accept Bytes as input + # Tabs are parsed as \n\n, except when the irreducible poly is a pentanomial. + # In that case, there is only a space. First replace takes care of that. + # Second replace unifies tabs and changes of lines. + # Every page ends with the page number and the form feed \x0c, hence the [:-2]. + coefficients += text.replace(" ", "\n").replace("\n\n", "\n").split("\n")[:-2] + + for coeffs in coefficients: + degree = coeffs.split(",")[0] + nonzero_degrees = coeffs + ",0" + nonzero_coeffs = ("1," * len(nonzero_degrees.split(",")))[:-1] + print(f"Irreducible polynomial for GF(2^{degree})") + add_to_database(cursor, 2, degree, nonzero_degrees, nonzero_coeffs) + + conn.commit() + + +if __name__ == "__main__": + main() diff --git a/src/galois/_databases/__init__.py b/src/galois/_databases/__init__.py index 8783d5010..98e80fd16 100644 --- a/src/galois/_databases/__init__.py +++ b/src/galois/_databases/__init__.py @@ -3,3 +3,4 @@ """ from ._conway import ConwayPolyDatabase from ._prime import PrimeFactorsDatabase +from ._irreducible import IrreduciblePolyDatabase diff --git a/src/galois/_databases/_irreducible.py b/src/galois/_databases/_irreducible.py new file mode 100644 index 000000000..2addfcc71 --- /dev/null +++ b/src/galois/_databases/_irreducible.py @@ -0,0 +1,46 @@ +""" +A module that handles accessing the database of irreducible polynomials. +""" +import os +import sqlite3 + +DATABASE = None # Database singleton class +DATABASE_FILE = os.path.join(os.path.dirname(__file__), "irreducible_polys.db") + + +class IrreduciblePolyDatabase: + """ + Class to interface with the irreducible polynomials database. + """ + + def __new__(cls): + global DATABASE + if DATABASE is None: + DATABASE = super().__new__(cls) + return DATABASE + + def __init__(self): + self.conn = sqlite3.connect(DATABASE_FILE) + self.cursor = self.conn.cursor() + + def fetch(self, characteristic: int, degree: int): + self.cursor.execute( + """ + SELECT nonzero_degrees,nonzero_coeffs + FROM polys + WHERE characteristic=? AND degree=?""", + (characteristic, degree), + ) + result = self.cursor.fetchone() + + if result is None: + raise LookupError( + f"The irreducible polynomials database does not contain an entry for GF({characteristic}^{degree}).\n\n" + "Alternatively, you can construct irreducible polynomials with `galois.irreducible_poly(p, m)` " + "or primitive polynomials with `galois.primitive_poly(p, m)`." + ) + + nonzero_degrees = [int(_) for _ in result[0].split(",")] + nonzero_coeffs = [int(_) for _ in result[1].split(",")] + + return nonzero_degrees, nonzero_coeffs diff --git a/src/galois/_databases/irreducible_polys.db b/src/galois/_databases/irreducible_polys.db new file mode 100644 index 000000000..bb500acbe Binary files /dev/null and b/src/galois/_databases/irreducible_polys.db differ diff --git a/src/galois/_polys/_irreducible.py b/src/galois/_polys/_irreducible.py index e42d3aa2a..a0d71f4bf 100644 --- a/src/galois/_polys/_irreducible.py +++ b/src/galois/_polys/_irreducible.py @@ -8,6 +8,7 @@ from typing_extensions import Literal +from .._databases import IrreduciblePolyDatabase from .._domains import _factory from .._helper import export, method_of, verify_isinstance from .._prime import factors, is_prime_power @@ -128,6 +129,7 @@ def irreducible_poly( degree: int, terms: int | str | None = None, method: Literal["min", "max", "random"] = "min", + use_database: bool = True, ) -> Poly: r""" Returns a monic irreducible polynomial :math:`f(x)` over :math:`\mathrm{GF}(q)` with degree :math:`m`. @@ -215,6 +217,16 @@ def irreducible_poly( if not method in ["min", "max", "random"]: raise ValueError(f"Argument 'method' must be in ['min', 'max', 'random'], not {method!r}.") + if terms == "min" and method == "min" and use_database: + try: + db = IrreduciblePolyDatabase() + degrees, coeffs = db.fetch(order, degree) + field = _factory.FIELD_FACTORY(order) + poly = Poly.Degrees(degrees, coeffs, field=field) + return poly + except LookupError: + pass + try: if method == "min": return next(irreducible_polys(order, degree, terms)) diff --git a/tests/polys/luts/irreducible_polys_database.py b/tests/polys/luts/irreducible_polys_database.py new file mode 100644 index 000000000..282d5e38f --- /dev/null +++ b/tests/polys/luts/irreducible_polys_database.py @@ -0,0 +1,15 @@ +""" +A module containing LUTs for irreducible polynomials with min terms from irreducible_polys.db +""" + +# LUT items are poly nonzero degrees and coefficients in degree-descending order + +# Gadiel Seroussi's table (1998) +# LUT items obtained by randomly picking degrees and checking the PDF +# sorted(numpy.random.default_rng(1337).integers(size=5, low=500, high=10_000, endpoint=True)) + +IRREDUCIBLE_POLY_MIN_TERMS_2_2262 = [[2262, 57, 0], [1, 1, 1]] +IRREDUCIBLE_POLY_MIN_TERMS_2_5632 = [[5632, 17, 15, 5, 0], [1, 1, 1, 1, 1]] +IRREDUCIBLE_POLY_MIN_TERMS_2_5690 = [[5690, 1623, 0], [1, 1, 1]] +IRREDUCIBLE_POLY_MIN_TERMS_2_7407 = [[7407, 27, 21, 17, 0], [1, 1, 1, 1, 1]] +IRREDUCIBLE_POLY_MIN_TERMS_2_8842 = [[8842, 4143, 0], [1, 1, 1]] diff --git a/tests/polys/test_irreducible_polys.py b/tests/polys/test_irreducible_polys.py index 44bcd98af..9a7a4f8b2 100644 --- a/tests/polys/test_irreducible_polys.py +++ b/tests/polys/test_irreducible_polys.py @@ -1,6 +1,7 @@ """ A pytest module to test generating irreducible polynomials over finite fields. """ +import time import numpy as np import pytest @@ -42,6 +43,14 @@ ) from .luts.irreducible_polys_25 import IRREDUCIBLE_POLYS_25_1, IRREDUCIBLE_POLYS_25_2 +from .luts.irreducible_polys_database import ( + IRREDUCIBLE_POLY_MIN_TERMS_2_2262, + IRREDUCIBLE_POLY_MIN_TERMS_2_5632, + IRREDUCIBLE_POLY_MIN_TERMS_2_5690, + IRREDUCIBLE_POLY_MIN_TERMS_2_7407, + IRREDUCIBLE_POLY_MIN_TERMS_2_8842, +) + PARAMS = [ (2, 1, IRREDUCIBLE_POLYS_2_1), (2, 2, IRREDUCIBLE_POLYS_2_2), @@ -71,6 +80,14 @@ (5**2, 2, IRREDUCIBLE_POLYS_25_2), ] +PARAMS_DB = [ + (2, 2262, IRREDUCIBLE_POLY_MIN_TERMS_2_2262), + (2, 5632, IRREDUCIBLE_POLY_MIN_TERMS_2_5632), + (2, 5690, IRREDUCIBLE_POLY_MIN_TERMS_2_5690), + (2, 7407, IRREDUCIBLE_POLY_MIN_TERMS_2_7407), + (2, 8842, IRREDUCIBLE_POLY_MIN_TERMS_2_8842), +] + def test_irreducible_poly_exceptions(): with pytest.raises(TypeError): @@ -160,6 +177,18 @@ def test_minimum_terms(order, degree, polys): assert f.coeffs.tolist() in min_term_polys +@pytest.mark.parametrize("order,degree,polys", PARAMS_DB) +def test_minimum_terms_from_database(order, degree, polys): + tick = time.time() + p = galois.irreducible_poly(order, degree, terms="min") + tock = time.time() + assert tock - tick < 1.0 + db_degrees = p.nonzero_degrees.tolist() + db_coeffs = p.nonzero_coeffs.tolist() + exp_degrees, exp_coeffs = polys + assert db_degrees == exp_degrees and db_coeffs == exp_coeffs + + def test_large_degree(): """ See https://github.com/mhostetter/galois/issues/360.