├── algnuth ├── __init__.py ├── ideals.py ├── quadratic.py ├── jacobi.py └── polynom.py ├── .gitignore ├── docs ├── algnuth.txt ├── algnuth.ideals.txt ├── algnuth.jacobi.txt ├── algnuth.quadratic.txt └── algnuth.polynom.txt ├── setup.py ├── .travis.yml ├── Makefile ├── LICENSE ├── TODO.md └── README.md /algnuth/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.py[cod] 3 | 4 | build/ 5 | dist/ 6 | *.egg-info/ 7 | -------------------------------------------------------------------------------- /docs/algnuth.txt: -------------------------------------------------------------------------------- 1 | NAME 2 | algnuth 3 | 4 | PACKAGE CONTENTS 5 | ideals 6 | jacobi 7 | polynom 8 | quadratic 9 | 10 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | from setuptools import setup 5 | 6 | 7 | def read(fname): 8 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 9 | 10 | 11 | setup( 12 | name='algnuth', 13 | version='0.0.3', 14 | author='Louis Abraham', 15 | author_email='louis.abraham@yahoo.fr', 16 | description='Algebraic Number Theory package', 17 | license='MIT', 18 | keywords='algebra', 19 | url='https://github.com/louisabraham/algnuth', 20 | packages=['algnuth'], 21 | install_requires=['numpy'], 22 | long_description=read('README.md'), 23 | classifiers=[ 24 | 'Topic :: Scientific/Engineering :: Mathematics' 25 | ], 26 | ) 27 | -------------------------------------------------------------------------------- /docs/algnuth.ideals.txt: -------------------------------------------------------------------------------- 1 | NAME 2 | algnuth.ideals 3 | 4 | DESCRIPTION 5 | This module provides functions to manipulate 6 | extension fields given their minimal polynomial. 7 | 8 | FUNCTIONS 9 | factorIdeals(P) 10 | Finds the ideals of the ring of integers 11 | of the algebraic number field whose 12 | minimal polynomial is P 13 | 14 | factorial(...) 15 | factorial(x) -> Integral 16 | 17 | Find x!. Raise a ValueError if x is negative or non-integral. 18 | 19 | idealsContaining(P, p) 20 | Ideals of the extension field of minimal 21 | polynomial P containing the prime p 22 | 23 | minkowski_bound(P) 24 | Any ideal of the ring of integers 25 | of the algebraic number field whose 26 | minimal polynomial is P contains 27 | an integer N such that 28 | 1 ≤ N ≤ minkowski_bound(P) 29 | 30 | -------------------------------------------------------------------------------- /docs/algnuth.jacobi.txt: -------------------------------------------------------------------------------- 1 | NAME 2 | algnuth.jacobi 3 | 4 | - Jacobi symbol, Solovay–Strassen primality test and sieve of Eratosthenes 5 | 6 | FUNCTIONS 7 | expsign(sign, exp) 8 | optimization of sign ** exp 9 | 10 | gcd(...) 11 | gcd(x, y) -> int 12 | greatest common divisor of x and y 13 | 14 | isprimerm(n, l=[2, 325, 9375, 28178, 450775, 9780504, 1795265022]) 15 | Miller–Rabin primality test 16 | 17 | jacobi(m, n) 18 | Jacobi's symbol 19 | the rule for (-1/n) is not used 20 | 21 | sieve(n) 22 | Sieve of Eratosthenes 23 | sieve(n) -> list of primes in range(n) 24 | 25 | solovay_strassen(n, prec=50) 26 | Solovay–Strassen primality test 27 | with error probability less than 2^-prec 28 | 29 | test_solovay_strassen(limit=100000) 30 | Runs in ~20s with limit = 10^5 31 | 32 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | cache: pip 3 | python: 4 | - 3.5 5 | - 3.6 6 | #- nightly 7 | #- pypy 8 | #- pypy3 9 | matrix: 10 | allow_failures: 11 | - python: nightly 12 | - python: pypy 13 | - python: pypy3 14 | install: 15 | #- pip install -r requirements.txt 16 | - pip install flake8 # pytest # add another testing frameworks later 17 | before_script: 18 | # stop the build if there are Python syntax errors or undefined names 19 | - flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics 20 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 21 | - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 22 | script: 23 | - true # pytest --capture=sys # add other tests here 24 | notifications: 25 | on_success: change 26 | on_failure: change # `always` will be the setting once code changes slow down 27 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | define exportdoc 2 | import sys 3 | from importlib import import_module 4 | import pydoc 5 | pydoc.isdata = lambda _: False 6 | class MarkdownDoc(pydoc._PlainTextDoc): 7 | def getdocloc(self, _): 8 | return None 9 | def docmodule(self, m): 10 | m.__name__ += '\n\n' 11 | return '\n'.join(super().docmodule(m).split('\n')[:-4]) 12 | 13 | renderer = MarkdownDoc() 14 | for m in sys.argv[1:]: 15 | print(renderer.docmodule(import_module(m)), 16 | file=open(m + '.txt', 'w')) 17 | endef 18 | export exportdoc 19 | 20 | doc: 21 | @-mkdir docs 22 | @path=$$(pwd); \ 23 | cd docs; \ 24 | PYTHONPATH=$$path:$$PYTHONPATH python3 -c "$$exportdoc" algnuth algnuth.polynom algnuth.quadratic algnuth.jacobi algnuth.ideals 25 | 26 | pypi: dist 27 | twine upload dist/* 28 | 29 | dist: flake8 30 | -rm dist/* 31 | ./setup.py bdist_wheel 32 | 33 | clean: 34 | rm -rf algnuth.egg-info build dist 35 | 36 | flake8: 37 | flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics 38 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 39 | 40 | .PHONY: doc dist -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Louis Abraham, Yassir Akram 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /algnuth/ideals.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module provides functions to manipulate 3 | extension fields given their minimal polynomial. 4 | """ 5 | 6 | from math import pi, factorial 7 | 8 | from .polynom import Polynomial 9 | from .jacobi import sieve 10 | 11 | 12 | def minkowski_bound(P): 13 | """ 14 | Any ideal of the ring of integers 15 | of the algebraic number field whose 16 | minimal polynomial is P contains 17 | an integer N such that 18 | 1 ≤ N ≤ minkowski_bound(P) 19 | """ 20 | return (4 / pi) ** P.r2 * factorial(P.deg) / P.deg ** P.deg * abs(P.disc) ** .5 21 | 22 | 23 | def idealsContaining(P, p): 24 | """ 25 | Ideals of the extension field of minimal 26 | polynomial P containing the prime p 27 | """ 28 | Pmodp = P.reduceP(p) 29 | c, Ds = Pmodp.factor() 30 | print('%s mod %s = %s' % (P, p, Polynomial.ppfactors((c, Ds)))) 31 | print("(%s) = " % p + 32 | '⋅'.join(("(%s, %s)" % (p, D) 33 | + (v > 1) * ("^%s" % v)) 34 | if sum(Ds.values()) > 1 else "(%s)" % p 35 | for D, v in Ds.items()).replace("X", "α")) 36 | 37 | 38 | def factorIdeals(P): 39 | """ 40 | Finds the ideals of the ring of integers 41 | of the algebraic number field whose 42 | minimal polynomial is P 43 | """ 44 | b = int(minkowski_bound(P)) 45 | if b == 1: 46 | print('Principal!') 47 | for p in sieve(b + 1): 48 | idealsContaining(P, p) 49 | print() 50 | -------------------------------------------------------------------------------- /docs/algnuth.quadratic.txt: -------------------------------------------------------------------------------- 1 | NAME 2 | algnuth.quadratic 3 | 4 | - Basic functions on quadratic forms 5 | 6 | FUNCTIONS 7 | Cl(D) 8 | List of classes of discriminant D 9 | 10 | Prim(D) 11 | List of primitive forms of discriminant D 12 | 13 | a(D) 14 | Number of ambiguous classes 15 | 16 | ambiguous_classes(D) 17 | 18 | display(a, b, c) 19 | Displays a form 20 | 21 | display_ambiguous_classes(D) 22 | 23 | display_classes(D) 24 | 25 | display_primitive_forms(D) 26 | 27 | func(a, b, c) 28 | Transforms a triple into a function 29 | 30 | gcd(...) 31 | gcd(x, y) -> int 32 | greatest common divisor of x and y 33 | 34 | genera(D) 35 | 36 | genus(a, b, c) 37 | 38 | h(D) 39 | Number of primitive forms of discriminant D 40 | 41 | index(D) 42 | 43 | is_ambiguous(a, b, c) 44 | 45 | reduce(...) 46 | reduce(function, sequence[, initial]) -> value 47 | 48 | Apply a function of two arguments cumulatively to the items of a sequence, 49 | from left to right, so as to reduce the sequence to a single value. 50 | For example, reduce(lambda x, y: x+y, [1, 2, 3, 4, 5]) calculates 51 | ((((1+2)+3)+4)+5). If initial is present, it is placed before the items 52 | of the sequence in the calculation, and serves as a default when the 53 | sequence is empty. 54 | 55 | reduced(a, b, c) 56 | Reduced form 57 | 58 | sqrt(...) 59 | sqrt(x) 60 | 61 | Return the square root of x. 62 | 63 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | Pseudoprimality tests 2 | ===================== 3 | 4 | - [Miller-Rabin](https://en.wikipedia.org/wiki/Miller–Rabin_primality_test) 5 | - [Fermat](https://en.wikipedia.org/wiki/Fermat_pseudoprime) 6 | - [Lucas](https://en.wikipedia.org/wiki/Lucas_pseudoprime) 7 | - [Baillie-PSW](https://en.wikipedia.org/wiki/Baillie%E2%80%93PSW_primality_test) 8 | - Deterministic variants of Miller-Rabin 9 | 10 | Elliptic curves 11 | =============== 12 | 13 | - [Weierstrass form](https://github.com/pkruk/pylenstra) 14 | - Montgomery form 15 | - Generic class for other int implementations 16 | - For Montgomery: [PRAC and safe 17 | Ladder](https://arxiv.org/pdf/1703.01863.pdf) 18 | - Add other curves and formulas from the [Explicit-Formulas 19 | Database](https://hyperelliptic.org/EFD/)? 20 | 21 | Other references: 22 | ----------------- 23 | 24 | - 25 | - 26 | - 27 | - 28 | 29 | gmpy2 support 30 | ============= 31 | 32 | - [gmpy2](https://github.com/aleaxit/gmpy) support for elliptic curves 33 | - implement alternative faster sieves using 34 | [gmpy2](https://gmpy2.readthedocs.io/en/latest/advmpz.html) or 35 | [numpy](https://stackoverflow.com/a/3035188/5133167) 36 | - interface the [Advanced Number Theory Functions from 37 | gmpy2](https://gmpy2.readthedocs.io/en/latest/advmpz.html#advanced-number-theory-functions) 38 | and replicate them in pure Python for compatibility 39 | 40 | Factorization algorithms 41 | ======================== 42 | 43 | - [Lenstra's 44 | algorithm](https://wstein.org/edu/124/lenstra/lenstra.pdf) on 45 | elliptic curves 46 | - [Multiple polynomial quadratic 47 | sieve](https://codegolf.stackexchange.com/a/9088/47040) 48 | - Parallelism with 49 | [SCOOP](https://scoop.readthedocs.io/en/0.7/api.html?highlight=futures#scoop.futures.as_completed) 50 | - Hart's one line factoring algorithm 51 | ([pdf](http://wrap.warwick.ac.uk/54707/1/WRAP_Hart_S1446788712000146a.pdf)) 52 | - Other algorithms from 53 | [primefac](https://pypi.python.org/pypi/primefac) 54 | 55 | Other algorithms 56 | ================ 57 | 58 | - Modular square root: Tonelli--Shanks and Cipolla's algorithms 59 | - Algorithms from [E. Bach, J.O. Shallit *Algorithmic Number Theory: 60 | Efficient algorithms* MIT 61 | Press, (1996)](https://mitpress.mit.edu/books/algorithmic-number-theory) 62 | 63 | Maybe 64 | ===== 65 | 66 | - Multiprocessing support: better than SCOOP locally and because of 67 | the ability to terminate 68 | - [General number field 69 | sieve](https://wstein.org/129/references/Lenstra-Lenstra-Manasse-Pollard-The%20number%20field%20sieve.pdf), 70 | see also 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![travis](https://travis-ci.org/louisabraham/algnuth.svg?branch=master) 2 | 3 | Algebraic Number Theory package 4 | =============================== 5 | 6 | **Louis Abraham** and **Yassir Akram** 7 | 8 | Installation 9 | ------------ 10 | 11 | pip install --upgrade algnuth 12 | 13 | or get the development version with: 14 | 15 | pip install --upgrade git+https://github.com/louisabraham/algnuth 16 | 17 | Features 18 | -------- 19 | 20 | ### Jacobi symbol 21 | 22 | ``` pycon 23 | >>> from algnuth.jacobi import jacobi 24 | >>> jacobi(3763, 20353) 25 | -1 26 | ``` 27 | 28 | ### Solovay-Strassen primality test 29 | 30 | ``` pycon 31 | >>> from algnuth.jacobi import solovay_strassen 32 | >>> p = 12779877140635552275193974526927174906313992988726945426212616053383820179306398832891367199026816638983953765799977121840616466620283861630627224899026453 33 | >>> q = 12779877140635552275193974526927174906313992988726945426212616053383820179306398832891367199026816638983953765799977121840616466620283861630627224899027521 34 | >>> n = p * q 35 | >>> solovay_strassen(p) 36 | True 37 | >>> solovay_strassen(q) 38 | True 39 | >>> solovay_strassen(n) 40 | False 41 | ``` 42 | 43 | ### Quadratic forms 44 | 45 | ``` pycon 46 | >>> from algnuth.quadratic import * 47 | >>> display_classes(-44) 48 | x^2 + 11⋅y^2 49 | 2⋅x^2 + 2⋅xy + 6⋅y^2 50 | 3⋅x^2 - 2⋅xy + 4⋅y^2 51 | 3⋅x^2 + 2⋅xy + 4⋅y^2 52 | >>> display_primitive_forms(-44) 53 | x^2 + 11⋅y^2 54 | 3⋅x^2 - 2⋅xy + 4⋅y^2 55 | 3⋅x^2 + 2⋅xy + 4⋅y^2 56 | >>> display_ambiguous_classes(-44) 57 | x^2 + 11⋅y^2 58 | 2⋅x^2 + 2⋅xy + 6⋅y^2 59 | >>> display(*reduced(18, -10, 2)) 60 | 2⋅x^2 + 2⋅xy + 6⋅y^2 61 | ``` 62 | 63 | ### Real polynomials 64 | 65 | ``` pycon 66 | >>> from algnuth.polynom import Polynomial 67 | >>> P = Polynomial([0] * 10 + [-1, 0, 1]) 68 | >>> print(P) 69 | X^12-X^10 70 | >>> P(2) 71 | 3072 72 | >>> P.disc 73 | 0 74 | >>> P.sturm() # Number of distinct real roots 75 | 3 76 | >>> P.r1 # Number of real roots with multiplicity 77 | 12 78 | ``` 79 | 80 | ### Modular arithmetic 81 | 82 | ``` pycon 83 | >>> P = Polynomial([1, 2, 3]) 84 | >>> Pmodp = P % 41 85 | >>> print(Pmodp ** 3) 86 | 27⋅X^6+13⋅X^5+22⋅X^4+3⋅X^3+21⋅X^2+6⋅X+1 87 | >>> print((P ** 3) % 41) 88 | 27⋅X^6+13⋅X^5+22⋅X^4+3⋅X^3+21⋅X^2+6⋅X+1 89 | ``` 90 | 91 | ### Polynomial division 92 | 93 | ``` pycon 94 | >>> A = Polynomial([1, 2, 3, 4]) % 7 95 | >>> B = Polynomial([0, 1, 2]) % 7 96 | >>> print(A) 97 | 4⋅X^3+3⋅X^2+2⋅X+1 98 | >>> print(B) 99 | 2⋅X^2+X 100 | >>> print(A % B) 101 | 5⋅X+1 102 | >>> print(A // B) 103 | 2⋅X+4 104 | >>> print((A // B) * B + A % B) 105 | 4⋅X^3+3⋅X^2+2⋅X+1 106 | ``` 107 | 108 | ### Berlekamp's factorization algorithm 109 | 110 | ``` pycon 111 | >>> P = Polynomial([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]) 112 | >>> Pmodp = P % 41 113 | >>> print(Polynomial.ppfactors(Pmodp.factor())) 114 | 12⋅(X+31)⋅X⋅(X^2+40⋅X+24)⋅(X^2+36⋅X+13)⋅(X^6+34⋅X^5+26⋅X^4+13⋅X^3+25⋅X^2+26⋅X+35) 115 | ``` 116 | 117 | ### Unique Factorization of Ideals 118 | 119 | ``` pycon 120 | >>> from algnuth.ideals import factorIdeals 121 | >>> factorIdeals(Polynomial([4, 0, 0, 1])) 122 | X^3+4 mod 2 = X^3 123 | (2) = (2, α)^3 124 | X^3+4 mod 3 = (X+1)^3 125 | (3) = (3, α+1)^3 126 | X^3+4 mod 5 = (X+4)⋅(X^2+X+1) 127 | (5) = (5, α+4)⋅(5, α^2+α+1) 128 | ``` 129 | -------------------------------------------------------------------------------- /algnuth/quadratic.py: -------------------------------------------------------------------------------- 1 | """ 2 | Basic functions on quadratic forms 3 | """ 4 | 5 | from math import sqrt, gcd 6 | 7 | from functools import reduce, partial 8 | gcdl = partial(reduce, gcd) 9 | 10 | 11 | def Cl(D): 12 | """ 13 | List of classes of discriminant D 14 | """ 15 | assert D < 0 16 | assert D % 4 in [0, 1] 17 | l = [] 18 | for a in range(1, int(sqrt(-D / 3)) + 1): 19 | for b in range(-a, a + 1): 20 | if (b**2 - D) % (4 * a) == 0: 21 | c = (b**2 - D) // (4 * a) 22 | if c >= a: 23 | if b >= 0 or (abs(b) != a and a != c): 24 | l.append((a, b, c)) 25 | return l 26 | 27 | 28 | def Prim(D): 29 | """ 30 | List of primitive forms of discriminant D 31 | """ 32 | return [i for i in Cl(D) if gcdl(i) == 1] 33 | 34 | 35 | def display(a, b, c): 36 | """ 37 | Displays a form 38 | """ 39 | if a == b == c == 0: 40 | print(0) 41 | ans = '' 42 | if a: 43 | if a < 0: 44 | ans += ' - ' 45 | ans += (abs(a) != 1) * (str(abs(a)) + '⋅') + "x^2" 46 | if b: 47 | if b > 0: 48 | ans += ' + ' 49 | elif b < 0: 50 | ans += ' - ' 51 | ans += (abs(b) != 1) * (str(abs(b)) + '⋅') + "xy" 52 | if c: 53 | if c > 0: 54 | ans += ' + ' 55 | elif c < 0: 56 | ans += ' - ' 57 | ans += (abs(c) != 1) * (str(abs(c)) + '⋅') + "y^2" 58 | print(ans) 59 | 60 | 61 | def reduced(a, b, c): 62 | """ 63 | Reduced form 64 | """ 65 | while not -abs(a) < b <= abs(a) <= abs(c): 66 | if abs(c) < abs(a): 67 | a, b, c = c, -b, a 68 | elif abs(c) >= abs(a) and abs(b) >= abs(a): 69 | sign = 1 if abs(b + a) < abs(b) else -1 70 | b, c = b + sign * 2 * a, c + a + sign * b 71 | return a, b, c 72 | 73 | 74 | def display_classes(D): 75 | for i in Cl(D): 76 | display(*i) 77 | 78 | 79 | def display_primitive_forms(D): 80 | for i in Prim(D): 81 | display(*i) 82 | 83 | 84 | def h(D): 85 | """ 86 | Number of primitive forms of discriminant D 87 | """ 88 | return len(Prim(D)) 89 | 90 | 91 | def is_ambiguous(a, b, c): 92 | return b == 0 or b == a or c == a 93 | 94 | 95 | def ambiguous_classes(D): 96 | return [i for i in Cl(D) if is_ambiguous(*i)] 97 | 98 | 99 | def display_ambiguous_classes(D): 100 | for i in ambiguous_classes(D): 101 | display(*i) 102 | 103 | 104 | def a(D): 105 | """ 106 | Number of ambiguous classes 107 | """ 108 | return len(ambiguous_classes(D)) 109 | 110 | 111 | def func(a, b, c): 112 | """ 113 | Transforms a triple into a function 114 | """ 115 | return lambda x, y: a * x * x + b * x * y + c * y * y 116 | 117 | 118 | def genus(a, b, c): 119 | f = func(a, b, c) 120 | D = -(b**2 - 4 * a * c) 121 | return tuple(sorted(set(f(x, y) % D for x in range(D) for y in range(D) if gcd(f(x, y), D) == 1))) 122 | 123 | 124 | def genera(D): 125 | l = Prim(D) 126 | s = set() 127 | for t in l: 128 | s.add(genus(*t)) 129 | return s 130 | 131 | 132 | def index(D): 133 | c = Cl(D) 134 | g = genera(D) 135 | assert len(c) % len(g) == 0 136 | return len(c) // len(g) 137 | -------------------------------------------------------------------------------- /algnuth/jacobi.py: -------------------------------------------------------------------------------- 1 | """ 2 | Jacobi symbol, Solovay–Strassen primality test and sieve of Eratosthenes 3 | """ 4 | 5 | from random import randrange 6 | from math import gcd 7 | 8 | 9 | def expsign(sign, exp): 10 | """ 11 | optimization of sign ** exp 12 | """ 13 | if sign == 1: 14 | return 1 15 | assert sign == -1 16 | return -1 if exp % 2 else 1 17 | 18 | 19 | def jacobi(m, n): 20 | """ 21 | Jacobi's symbol 22 | the rule for (-1/n) is not used 23 | """ 24 | assert n % 2 25 | if m == 2: 26 | if n % 8 in [1, 7]: 27 | return 1 28 | return -1 29 | m %= n 30 | q = 0 31 | while m & 1 == 0: 32 | m >>= 1 33 | q += 1 34 | if m == 1: 35 | return expsign(jacobi(2, n), q) 36 | return (expsign(jacobi(2, n), q) 37 | * (-1 if (n % 4 == 3) and (m % 4 == 3) else 1) 38 | * jacobi(n, m)) 39 | 40 | 41 | def solovay_strassen(n, prec=50): 42 | """ 43 | Solovay–Strassen primality test 44 | with error probability less than 2^-prec 45 | """ 46 | if n == 1: 47 | return False 48 | if n % 2 == 0: 49 | return n == 2 50 | e = (n - 1) // 2 51 | for _ in range(prec): 52 | x = randrange(1, n) 53 | if gcd(x, n) != 1 or pow(x, e, n) != (jacobi(x, n) % n): 54 | return False 55 | return True 56 | 57 | 58 | def sieve(n): 59 | """ 60 | Sieve of Eratosthenes 61 | sieve(n) -> list of primes in range(n) 62 | """ 63 | n -= 1 64 | # l[i] = True iff i is prime 65 | # ignore the first two values 66 | l = [True] * (n + 1) 67 | for x in range(2, round(n**.5) + 1): 68 | # all factors are ≤ int(n**.5) 69 | # round is there in case of float error 70 | if l[x]: 71 | # there are exactly (n // x - 1) 72 | # multiples of x greater than x 73 | l[2 * x::x] = [False] * (n // x - 1) 74 | return [i for i in range(2, n + 1) if l[i]] 75 | 76 | 77 | def isprimerm(n, l=[2, 325, 9375, 28178, 450775, 9780504, 1795265022]): 78 | """ 79 | Miller–Rabin primality test 80 | """ 81 | if n == 1: 82 | return False 83 | if n in l: 84 | return True 85 | if n % 2 == 0: 86 | return False 87 | b = 0 88 | for a in l: 89 | if pow(a, n - 1, n) != 1: 90 | return False 91 | if b == 0 or pow(a, b, n) != 1: 92 | r = 1 93 | b = n - 1 94 | while b % 2 == 0 and r == 1: 95 | r = pow(a, b, n) 96 | b = b // 2 97 | if r != 1 and r != n - 1: 98 | return False 99 | return True 100 | 101 | 102 | def test_solovay_strassen(limit=10**5): 103 | """ 104 | Runs in ~20s with limit = 10^5 105 | """ 106 | primes = set(sieve(limit)) 107 | for i in range(limit): 108 | assert (i in primes) == solovay_strassen(i) 109 | 110 | 111 | if __name__ == '__main__': 112 | test_solovay_strassen(10**3) 113 | p = 12779877140635552275193974526927174906313992988726945426212616053383820179306398832891367199026816638983953765799977121840616466620283861630627224899026453 114 | q = 12779877140635552275193974526927174906313992988726945426212616053383820179306398832891367199026816638983953765799977121840616466620283861630627224899027521 115 | n = p * q 116 | assert solovay_strassen(p) 117 | assert solovay_strassen(q) 118 | assert not solovay_strassen(n) 119 | -------------------------------------------------------------------------------- /docs/algnuth.polynom.txt: -------------------------------------------------------------------------------- 1 | NAME 2 | algnuth.polynom 3 | 4 | - Modular arithmetic 5 | 6 | CLASSES 7 | builtins.object 8 | algnuth.polynom.ModInt 9 | algnuth.polynom.Polynomial 10 | 11 | class ModInt(builtins.object) 12 | | Integers of Z/pZ 13 | | 14 | | Methods defined here: 15 | | 16 | | __add__(a, b) 17 | | 18 | | __bool__(self) 19 | | 20 | | __eq__(a, b) 21 | | Return self==value. 22 | | 23 | | __hash__(self) 24 | | Return hash(self). 25 | | 26 | | __init__(self, a, n) 27 | | Initialize self. See help(type(self)) for accurate signature. 28 | | 29 | | __mul__(a, b) 30 | | 31 | | __neg__(a) 32 | | 33 | | __pow__(P, k) 34 | | 35 | | __radd__(a, b) 36 | | 37 | | __repr__(self) 38 | | Return repr(self). 39 | | 40 | | __rmul__(a, b) 41 | | 42 | | __rtruediv__(a, k) 43 | | 44 | | __str__(self) 45 | | Return str(self). 46 | | 47 | | __sub__(a, b) 48 | | 49 | | __truediv__(a, b) 50 | | 51 | | ---------------------------------------------------------------------- 52 | | Static methods defined here: 53 | | 54 | | extended_euclid(a, b) 55 | | Extended Euclid algorithm 56 | | 57 | | Return 58 | | ------ 59 | | x : int 60 | | y : int 61 | | a * x + b * y = gcd(a, b) 62 | | 63 | | ---------------------------------------------------------------------- 64 | | Data descriptors defined here: 65 | | 66 | | __dict__ 67 | | dictionary for instance variables (if defined) 68 | | 69 | | __weakref__ 70 | | list of weak references to the object (if defined) 71 | 72 | class Polynomial(builtins.object) 73 | | Generic class for polynomials 74 | | Works with int, float and ModInt 75 | | 76 | | Methods defined here: 77 | | 78 | | __add__(P, Q) 79 | | 80 | | __bool__(self) 81 | | 82 | | __call__(self, x) 83 | | Call self as a function. 84 | | 85 | | __eq__(P, Q) 86 | | Return self==value. 87 | | 88 | | __floordiv__(A, B) 89 | | 90 | | __getitem__(self, x) 91 | | 92 | | __hash__(self) 93 | | Return hash(self). 94 | | 95 | | __init__(self, C=None) 96 | | Initialize self. See help(type(self)) for accurate signature. 97 | | 98 | | __iter__(self) 99 | | 100 | | __len__(self) 101 | | 102 | | __lt__(A, B) 103 | | Return self>= 1 63 | if not k: 64 | break 65 | A *= A 66 | return V 67 | 68 | 69 | def inv(self): 70 | if self.v == 0: 71 | raise ZeroDivisionError 72 | return ModInt(ModInt._inv(self.v, self.n), self.n) 73 | 74 | @staticmethod 75 | def _inv(k, n): 76 | k %= n 77 | if k == 1: 78 | return k 79 | return (n - n // k) * ModInt._inv(n % k, n) % n 80 | 81 | def __truediv__(a, b): 82 | assert isinstance(b, ModInt) 83 | assert a.n == b.n 84 | return a * b.inv() 85 | 86 | def __rtruediv__(a, k): 87 | assert isinstance(k, int) 88 | return ModInt(k, a.n) / a 89 | 90 | @staticmethod 91 | def extended_euclid(a, b): 92 | """Extended Euclid algorithm 93 | 94 | Return 95 | ------ 96 | x : int 97 | y : int 98 | a * x + b * y = gcd(a, b) 99 | """ 100 | A, B = a, b 101 | sa, sb = (1 if a >= 0 else -1), (1 if b >= 0 else -1) 102 | xp, yp = 1, 0 103 | x, y = 0, 1 104 | while b: 105 | assert A * xp + B * yp == a 106 | assert A * x + B * y == b 107 | r = a // b 108 | a, b = b, a % b 109 | x, xp = xp - r * x, x 110 | y, yp = yp - r * y, y 111 | return sa * xp, sb * yp 112 | 113 | def __repr__(self): 114 | return '%s(%s, %s)' % (self.__class__.__name__, self.v, self.n) 115 | 116 | def __str__(self): 117 | return '%s' % self.v 118 | 119 | 120 | class Polynomial: 121 | 122 | """ 123 | Generic class for polynomials 124 | Works with int, float and ModInt 125 | """ 126 | 127 | def __len__(self): 128 | return len(self.C) 129 | 130 | def trim(C): 131 | i = len(C) - 1 132 | while i >= 0 and not C[i]: 133 | i -= 1 134 | return C[:i + 1] 135 | 136 | def __init__(self, C=None): 137 | if C is None: 138 | C = [] 139 | self.C = Polynomial.trim(C) 140 | 141 | @property 142 | def deg(self): 143 | return len(self.C) - 1 144 | 145 | def prime(self): return Polynomial([i * self[i] 146 | for i in range(1, len(self))]) 147 | 148 | def eval(self, x): 149 | if not self: 150 | return 0 151 | v = self[-1] 152 | for c in self[-2::-1]: 153 | v = v * x + c 154 | return v 155 | 156 | def shift(self, d): return Polynomial( 157 | [0 * self[0]] * d + self.C if self else []) 158 | 159 | def __eq__(P, Q): 160 | return P.deg == Q.deg and all(cP == cQ for cP, cQ in zip(P, Q)) 161 | 162 | def __hash__(self): 163 | return hash(tuple(self.C)) 164 | 165 | def __call__(self, x): return Polynomial.eval(self, x) 166 | 167 | def __getitem__(self, x): return self.C[x] 168 | 169 | def __neg__(P): return Polynomial([-c for c in P.C]) 170 | 171 | def __add__(P, Q): 172 | if len(P.C) < len(Q.C): 173 | P, Q = Q, P 174 | return Polynomial([P[d] + Q[d] for d in range(len(Q))] + P[len(Q):]) 175 | 176 | def __sub__(P, Q): return P + (-Q) 177 | 178 | def _mulpoly(P, Q): 179 | assert isinstance(Q, Polynomial) 180 | return Polynomial([sum(P[k] * Q[d - k] 181 | for k in range(max(0, d + 1 - len(Q)), 182 | min(d + 1, len(P))) 183 | ) for d in range(len(P) + len(Q) - 1)]) 184 | 185 | def _mulscal(P, k): 186 | return Polynomial([k * c for c in P]) 187 | 188 | def __mul__(P, Q): 189 | if isinstance(Q, Polynomial): 190 | return P._mulpoly(Q) 191 | return P._mulscal(Q) 192 | 193 | def __rmul__(P, Q): 194 | return P * Q 195 | 196 | def __pow__(P, k): 197 | assert isinstance(k, int) 198 | V = 1 199 | A = P 200 | while k: 201 | if k & 1: 202 | V *= A 203 | k >>= 1 204 | if not k: 205 | break 206 | A *= A 207 | return V 208 | 209 | def __iter__(self): 210 | yield from self.C 211 | 212 | def euclidean_division(A, B): 213 | Q = [0 * B[0]] * max(0, len(A) - len(B) + 1) 214 | while len(A.C) >= len(B.C): 215 | Q[len(A.C) - len(B.C)] = A[-1] / B[-1] 216 | A -= B.shift(len(A) - len(B)) * (A[-1] / B[-1]) 217 | return Polynomial(Q), A 218 | 219 | def __floordiv__(A, B): 220 | assert isinstance(B, Polynomial) 221 | return A.euclidean_division(B)[0] 222 | 223 | def __mod__(A, B): 224 | """ 225 | Polynomial euclidian division 226 | or modular reduction 227 | """ 228 | if isinstance(B, Polynomial): 229 | return A.euclidean_division(B)[1] 230 | else: 231 | assert isinstance(B, int) 232 | assert all(isinstance(c, int) for c in A) 233 | return A.reduceP(B) 234 | 235 | def __lt__(A, B): return A.deg < B.deg 236 | 237 | def __bool__(self): return bool(self.C) 238 | 239 | def gcd(A, B): 240 | while B: 241 | A, B = B, A % B 242 | return A * (1 / A[-1]) 243 | 244 | @staticmethod 245 | def gaussianElimKer(M, zero, one): 246 | """ 247 | Outputs an element of the kernel of M 248 | zero and one are elements of the same field 249 | """ 250 | # V satisfies the invariant 251 | # M = V M_0 252 | V = [Polynomial([zero] * i + [one]) for i in range(len(M))] 253 | pivots = [None] * (len(M) + 1) 254 | for l in range(len(M)): 255 | while M[l].deg >= 0: 256 | idp = M[l].deg 257 | if pivots[idp] is None: 258 | pivots[idp] = l 259 | break 260 | else: 261 | c = M[l][idp] / M[pivots[idp]][idp] 262 | M[l] -= c * M[pivots[idp]] 263 | V[l] -= c * V[pivots[idp]] 264 | else: 265 | # If a line is null, we found an element of the kernel 266 | return V[l] 267 | return None 268 | 269 | def computeQ(P): 270 | # only for Z/pZ[X] square-free polynoms, for p prime 271 | p = P[0].n 272 | # We ignore the image of 1 because (F-Id)(1) = 0 273 | M = [Polynomial(([ModInt(0, p)] * (i * p)) + [ModInt(1, p)]) % P 274 | for i in range(1, P.deg)] 275 | # M -= Id 276 | for i in range(1, P.deg): 277 | M[i - 1] -= Polynomial([ModInt(0, p)] * i + [ModInt(1, p)]) 278 | # We find an element of the kernel by Gaussian elimination 279 | pQ = Polynomial.gaussianElimKer(M, ModInt(0, p), ModInt(1, p)) 280 | # We put back the 1 tha was removed 281 | return pQ.shift(1) if pQ is not None else None 282 | 283 | def factor_unit(P): 284 | """ 285 | Berlekamp's algorithm 286 | only in Z/pZ 287 | """ 288 | assert all(isinstance(c, ModInt) for c in P) 289 | assert len(set(c.n for c in P)) == 1 290 | if P.deg == 1: 291 | return defaultdict(int, {P: 1}) 292 | 293 | p = P[0].n 294 | 295 | S = Polynomial.gcd(P, P.prime()) 296 | if S.deg == P.deg: 297 | # P' = 0 so P = R^p 298 | R = Polynomial(P.C[::p]) 299 | return defaultdict(int, 300 | {D: p * v 301 | for D, v in Polynomial.factor_unit(R).items()}) 302 | else: 303 | factors = defaultdict(int) 304 | if S.deg: 305 | for D, v in S.factor_unit().items(): 306 | factors[D] += v 307 | P //= S 308 | # P is now square-free 309 | # We look for Q in Ker(F-Id) \ {1} 310 | Q = Polynomial.computeQ(P) 311 | if Q is None: 312 | # P is irreducible 313 | factors[P] += 1 314 | else: 315 | # P is the product of the gcd(P, Q-i) 316 | # that are factored recursively 317 | for i in range(p): 318 | D = Polynomial.gcd(P, Q - Polynomial([ModInt(i, p)])) 319 | if D.deg: 320 | for DD, v in D.factor_unit().items(): 321 | factors[DD] += v 322 | return factors 323 | 324 | def factor(P): 325 | """ 326 | Factorization of P 327 | only in Z/pZ 328 | """ 329 | cd = P[-1] 330 | if P.deg == 0: 331 | return (cd, defaultdict(int)) 332 | P = P * (1 / cd) 333 | return (cd, P.factor_unit()) 334 | 335 | @staticmethod 336 | def ppfactors(fz): 337 | c, Ds = fz 338 | a = str(c) if not Ds or c * c != c else '' 339 | l = [a] + [(str(D) if D.deg == 1 and not D[0] else ('(%s)' % D)) 340 | + (v > 1) * ('^%s' % v) 341 | for D, v in sorted(Ds.items(), 342 | key=lambda e: (e[0].deg, e[1]))] 343 | return '⋅'.join(i for i in l if i) 344 | 345 | def reduceP(P, p): 346 | return Polynomial([ModInt(c, p) for c in P]) 347 | 348 | @staticmethod 349 | def sign_changes(l): 350 | return sum(a * b < 0 for a, b in zip(l, l[1:])) 351 | 352 | def isreal(P): 353 | return not any(isinstance(c, ModInt) for c in P) 354 | 355 | def isinteger(P): 356 | return all(isinstance(c, int) for c in P) 357 | 358 | def sturm(P): 359 | """ 360 | Number of distinct real roots 361 | by Sturm's theorem. 362 | Only works on int or float coefficients 363 | """ 364 | inf = float('inf') 365 | assert P.isreal() 366 | A = P 367 | B = A.prime() 368 | l1 = [A(-inf)] 369 | l2 = [A(inf)] 370 | while B: 371 | l1.append(B(-inf)) 372 | l2.append(B(inf)) 373 | B, A = -A % B, B 374 | return Polynomial.sign_changes(l1) - Polynomial.sign_changes(l2) 375 | 376 | @property 377 | def r1(P): 378 | """ 379 | Number of real roots with multiplicity 380 | """ 381 | assert P.isreal() 382 | ans = 0 383 | s = P.sturm() 384 | while s: 385 | ans += s 386 | P = P.gcd(P.prime()) 387 | s = P.sturm() 388 | return ans 389 | 390 | @property 391 | def r2(P): 392 | ans = P.deg - P.r1 393 | assert ans % 2 == 0 394 | return ans // 2 395 | 396 | def sylvester(P, Q): 397 | """ 398 | Sylvester's matrix 399 | """ 400 | assert P.isreal() 401 | assert Q.isreal() 402 | p = P.deg 403 | q = Q.deg 404 | P = np.array(P) 405 | Q = np.array(Q) 406 | m = np.zeros((p + q, p + q)) 407 | for i in range(q): 408 | m[i][i:i + p + 1] = P 409 | for i in range(p): 410 | m[q + i][i:i + q + 1] = Q 411 | return m 412 | 413 | def resultant(P, Q): 414 | """ 415 | Resultant of two real polynomials 416 | """ 417 | return np.linalg.det(P.sylvester(Q)) 418 | 419 | @property 420 | def disc(P): 421 | """ 422 | Discriminant of a real polynomial 423 | """ 424 | ans = P.resultant(P.prime()) / P[-1] 425 | if P.isinteger(): 426 | ans = int(ans.round()) 427 | if P.deg % 4 in [0, 1]: 428 | return ans 429 | else: 430 | return -ans 431 | 432 | def __repr__(self): 433 | return '%s(%s)' % (self.__class__.__name__, self.C) 434 | 435 | @staticmethod 436 | def _formatmonomial(c, d): 437 | assert c 438 | a = b = '' 439 | if c * c != c or not d: 440 | a = str(c) + (d != 0) * '⋅' 441 | if d > 1: 442 | b = 'X^' + str(d) 443 | elif d == 1: 444 | b = 'X' 445 | return a + b 446 | 447 | def __str__(self): 448 | if not self.C: 449 | return "0" 450 | ans = '+'.join(self._formatmonomial(c, d) 451 | for (d, c) in reversed(list(enumerate(self))) if c) 452 | return ans.replace("+-", "-").replace('-1⋅', '-') 453 | --------------------------------------------------------------------------------