square-rulerpack 0.2.0__py3-none-any.whl
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.
- square_rulerpack/__init__.py +36 -0
- square_rulerpack/__main__.py +47 -0
- square_rulerpack/arith.py +176 -0
- square_rulerpack/engine.py +980 -0
- square_rulerpack/lessons.py +774 -0
- square_rulerpack/linalg.py +101 -0
- square_rulerpack/native.py +101 -0
- square_rulerpack/params.py +128 -0
- square_rulerpack/walkthrough.py +426 -0
- square_rulerpack/workers.py +69 -0
- square_rulerpack-0.2.0.dist-info/METADATA +253 -0
- square_rulerpack-0.2.0.dist-info/RECORD +16 -0
- square_rulerpack-0.2.0.dist-info/WHEEL +5 -0
- square_rulerpack-0.2.0.dist-info/entry_points.txt +2 -0
- square_rulerpack-0.2.0.dist-info/licenses/LICENSE +21 -0
- square_rulerpack-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""square_rulerpack: the self-initialising quadratic sieve of square-rulerpack 0.2.0 in Python.
|
|
2
|
+
|
|
3
|
+
Two ways in:
|
|
4
|
+
|
|
5
|
+
>>> import square_rulerpack as sq
|
|
6
|
+
>>> sq.factor_completely(1009732533765211)
|
|
7
|
+
[11344301, 89007911]
|
|
8
|
+
|
|
9
|
+
>>> w = sq.Walkthrough(1009732533765211) # step by step, every stage as a DataFrame
|
|
10
|
+
>>> w.multiplier(); w.factor_base(); w.family(); w.sieve(0); w.trial_divide(0); ...
|
|
11
|
+
|
|
12
|
+
>>> sq.factor_completely(n, workers=8) # the Rust engine when square-rulerpack[native] is installed
|
|
13
|
+
|
|
14
|
+
>>> sq.factor_walkthrough() # the tiny exact N = 221 lesson, by hand
|
|
15
|
+
FactorWalkthrough(n=221, samples=5, relations=3, rank=2, nullity=1, split=(13, 17))
|
|
16
|
+
"""
|
|
17
|
+
from .params import Params, Row, YAFU_DIRECT
|
|
18
|
+
from .engine import (Settings, FactorBase, Family, Relation, Candidate, FamilySieve, Cycles,
|
|
19
|
+
multiplier, multiplier_scores, factor_base, choose_a, threshold_bits,
|
|
20
|
+
relations, factor, factor_with, factor_completely, square_root, HAVE_NUMBA)
|
|
21
|
+
from .workers import default_workers
|
|
22
|
+
from . import native
|
|
23
|
+
from .native import HAVE_NATIVE
|
|
24
|
+
from .walkthrough import Walkthrough
|
|
25
|
+
from . import lessons
|
|
26
|
+
from .lessons import (prime_seeds, polynomial_roots, LessonWindow, square_window, siqs_window, sieve_walk,
|
|
27
|
+
construct_polynomial, FactorWalkthrough, factor_walkthrough, RelationStudy, relation_study,
|
|
28
|
+
Capture, engine_polynomials)
|
|
29
|
+
|
|
30
|
+
__version__ = "0.2.0"
|
|
31
|
+
__all__ = ["Params", "Row", "YAFU_DIRECT", "Settings", "FactorBase", "Family", "Relation", "Candidate",
|
|
32
|
+
"FamilySieve", "Cycles", "multiplier", "multiplier_scores", "factor_base", "choose_a",
|
|
33
|
+
"threshold_bits", "relations", "factor", "factor_with", "factor_completely", "square_root",
|
|
34
|
+
"default_workers", "Walkthrough", "HAVE_NUMBA", "HAVE_NATIVE", "native", "__version__", "lessons", "prime_seeds",
|
|
35
|
+
"polynomial_roots", "LessonWindow", "square_window", "siqs_window", "sieve_walk", "construct_polynomial",
|
|
36
|
+
"FactorWalkthrough", "factor_walkthrough", "RelationStudy", "relation_study", "Capture", "engine_polynomials"]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""python -m square_rulerpack N [--workers W] [--engine auto|native|python] [--plain] [--no-q2] [--no-dlp] [--quiet]"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
from .arith import is_probable_prime
|
|
9
|
+
from .engine import Settings, factor_completely
|
|
10
|
+
from .workers import default_workers
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main(argv=None) -> int:
|
|
14
|
+
ap = argparse.ArgumentParser(prog="square_rulerpack", description="Factor N with the self-initialising quadratic sieve.")
|
|
15
|
+
ap.add_argument("n", help="a positive decimal integer")
|
|
16
|
+
ap.add_argument("--workers", "-w", type=int, default=default_workers(), help="sieve processes (default: half the hardware threads)")
|
|
17
|
+
ap.add_argument("--plain", action="store_true", help="plain threshold instead of the exact one")
|
|
18
|
+
ap.add_argument("--no-q2", action="store_true", help="Q1 polynomials even when k·n ≡ 1 (mod 8)")
|
|
19
|
+
ap.add_argument("--no-dlp", action="store_true", help="single large prime only")
|
|
20
|
+
ap.add_argument("--no-numba", action="store_true", help="numpy sieve kernel even if numba is installed")
|
|
21
|
+
ap.add_argument("--engine", choices=["auto", "native", "python"], default="auto",
|
|
22
|
+
help="auto: the Rust extension when installed, else this Python port")
|
|
23
|
+
ap.add_argument("--quiet", "-q", action="store_true", help="no progress on stderr")
|
|
24
|
+
a = ap.parse_args(argv)
|
|
25
|
+
if not a.n.isdigit() or int(a.n) == 0:
|
|
26
|
+
print("The input must be a positive decimal integer.", file=sys.stderr)
|
|
27
|
+
return 2
|
|
28
|
+
n = int(a.n)
|
|
29
|
+
st = Settings(exact=False if a.plain else None, q2=not a.no_q2, dlp=not a.no_dlp, use_numba=not a.no_numba)
|
|
30
|
+
t = time.time()
|
|
31
|
+
if a.engine != "native" and not a.quiet:
|
|
32
|
+
from .native import HAVE_NATIVE
|
|
33
|
+
if a.engine == "python" or not HAVE_NATIVE:
|
|
34
|
+
print(" engine: python" + ("" if HAVE_NATIVE else " (pip install square-rulerpack[native] for the Rust engine)"), file=sys.stderr)
|
|
35
|
+
fs = factor_completely(n, workers=a.workers, settings=st, verbose=not a.quiet, engine=a.engine)
|
|
36
|
+
print(f"{n} = {' × '.join(map(str, fs)) if fs else 1}")
|
|
37
|
+
prod = 1
|
|
38
|
+
for f in fs:
|
|
39
|
+
prod *= f
|
|
40
|
+
ok = prod == n
|
|
41
|
+
prime = all(is_probable_prime(f) for f in fs)
|
|
42
|
+
print(f"{len(fs)} factors, product checks: {str(ok).lower()}, all probable prime: {str(prime).lower()}, {time.time() - t:.2f} s")
|
|
43
|
+
return 0 if ok and prime else 1
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
if __name__ == "__main__":
|
|
47
|
+
sys.exit(main())
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Integer arithmetic the sieve needs: primes, Legendre symbols, modular square roots,
|
|
2
|
+
probable-prime tests and a Pollard–Brent splitter for double-large-prime cofactors.
|
|
3
|
+
|
|
4
|
+
Everything here is plain Python integers (arbitrary precision) plus numpy for the prime sieve,
|
|
5
|
+
so each function is short enough to read in one sitting. The Rust engine (square-rulerpack 0.2.0)
|
|
6
|
+
has the same functions in ``siqs.rs``, ``arith64.rs`` and ``uecm.rs``; the Python versions keep
|
|
7
|
+
the same names where it helps to read the two side by side.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import math
|
|
12
|
+
import random
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"primes_upto", "legendre", "sqrt_mod", "modinv", "is_probable_prime", "is_square",
|
|
18
|
+
"pollard_brent", "XorShift64", "log2_int",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def primes_upto(n: int) -> np.ndarray:
|
|
23
|
+
"""All primes ≤ n as an int64 array (sieve of Eratosthenes on a boolean array)."""
|
|
24
|
+
if n < 2:
|
|
25
|
+
return np.zeros(0, dtype=np.int64)
|
|
26
|
+
flags = np.ones(n + 1, dtype=bool)
|
|
27
|
+
flags[:2] = False
|
|
28
|
+
for p in range(2, math.isqrt(n) + 1):
|
|
29
|
+
if flags[p]:
|
|
30
|
+
flags[p * p::p] = False
|
|
31
|
+
return np.flatnonzero(flags).astype(np.int64)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def legendre(a: int, p: int) -> int:
|
|
35
|
+
"""The Legendre symbol (a/p) for an odd prime p: 1 if a is a nonzero square mod p,
|
|
36
|
+
-1 if not, 0 if p divides a. Euler's criterion: a^((p-1)/2) mod p."""
|
|
37
|
+
a %= p
|
|
38
|
+
if a == 0:
|
|
39
|
+
return 0
|
|
40
|
+
r = pow(a, (p - 1) // 2, p)
|
|
41
|
+
return -1 if r == p - 1 else r
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def sqrt_mod(a: int, p: int) -> int:
|
|
45
|
+
"""A square root of a modulo the odd prime p (a must be a quadratic residue), by
|
|
46
|
+
Tonelli–Shanks. Returns the root in [0, p); the other root is p − root."""
|
|
47
|
+
a %= p
|
|
48
|
+
if a == 0:
|
|
49
|
+
return 0
|
|
50
|
+
if p == 2:
|
|
51
|
+
return a
|
|
52
|
+
if p % 4 == 3:
|
|
53
|
+
return pow(a, (p + 1) // 4, p)
|
|
54
|
+
# write p − 1 = q·2^s with q odd
|
|
55
|
+
q, s = p - 1, 0
|
|
56
|
+
while q % 2 == 0:
|
|
57
|
+
q //= 2
|
|
58
|
+
s += 1
|
|
59
|
+
z = 2
|
|
60
|
+
while legendre(z, p) != -1:
|
|
61
|
+
z += 1
|
|
62
|
+
m, c, t, r = s, pow(z, q, p), pow(a, q, p), pow(a, (q + 1) // 2, p)
|
|
63
|
+
while t != 1:
|
|
64
|
+
i, t2 = 0, t
|
|
65
|
+
while t2 != 1:
|
|
66
|
+
t2 = t2 * t2 % p
|
|
67
|
+
i += 1
|
|
68
|
+
b = pow(c, 1 << (m - i - 1), p)
|
|
69
|
+
m, c, t, r = i, b * b % p, t * b * b % p, r * b % p
|
|
70
|
+
return r
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def modinv(a: int, m: int) -> int:
|
|
74
|
+
"""a⁻¹ mod m (a and m coprime)."""
|
|
75
|
+
return pow(a, -1, m)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
_MR_BASES = (2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def is_probable_prime(n: int, rounds: int = 16) -> bool:
|
|
82
|
+
"""Miller–Rabin. Deterministic with the first twelve prime bases below 3.3·10^24 (the
|
|
83
|
+
same bases the Rust ``is_prime_u64`` uses), plus ``rounds`` random bases above that."""
|
|
84
|
+
if n < 2:
|
|
85
|
+
return False
|
|
86
|
+
for p in _MR_BASES:
|
|
87
|
+
if n % p == 0:
|
|
88
|
+
return n == p
|
|
89
|
+
d, s = n - 1, 0
|
|
90
|
+
while d % 2 == 0:
|
|
91
|
+
d //= 2
|
|
92
|
+
s += 1
|
|
93
|
+
bases = list(_MR_BASES)
|
|
94
|
+
if n >= 3_317_044_064_679_887_385_961_981:
|
|
95
|
+
rng = random.Random(n & 0xFFFFFFFF)
|
|
96
|
+
bases += [rng.randrange(2, n - 1) for _ in range(rounds)]
|
|
97
|
+
for a in bases:
|
|
98
|
+
x = pow(a, d, n)
|
|
99
|
+
if x == 1 or x == n - 1:
|
|
100
|
+
continue
|
|
101
|
+
for _ in range(s - 1):
|
|
102
|
+
x = x * x % n
|
|
103
|
+
if x == n - 1:
|
|
104
|
+
break
|
|
105
|
+
else:
|
|
106
|
+
return False
|
|
107
|
+
return True
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def is_square(n: int) -> bool:
|
|
111
|
+
if n < 0:
|
|
112
|
+
return False
|
|
113
|
+
r = math.isqrt(n)
|
|
114
|
+
return r * r == n
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def pollard_brent(n: int, seed: int = 1, max_iter: int = 1 << 22) -> int | None:
|
|
118
|
+
"""A nontrivial factor of the odd composite n by Pollard's rho with Brent's cycle
|
|
119
|
+
detection and batched gcds, or None if the budget runs out. This is what the Rust engine's
|
|
120
|
+
tiny ECM (``uecm.rs``) does for the cofactor of a double-large-prime candidate: the
|
|
121
|
+
cofactor is below 2^63 and is known to be composite and not a square."""
|
|
122
|
+
if n % 2 == 0:
|
|
123
|
+
return 2
|
|
124
|
+
rng = random.Random(seed ^ n)
|
|
125
|
+
for _attempt in range(8):
|
|
126
|
+
y, c, m = rng.randrange(1, n), rng.randrange(1, n), 128
|
|
127
|
+
g, r, q = 1, 1, 1
|
|
128
|
+
it = 0
|
|
129
|
+
while g == 1:
|
|
130
|
+
x = y
|
|
131
|
+
for _ in range(r):
|
|
132
|
+
y = (y * y + c) % n
|
|
133
|
+
k = 0
|
|
134
|
+
while k < r and g == 1:
|
|
135
|
+
ys = y
|
|
136
|
+
for _ in range(min(m, r - k)):
|
|
137
|
+
y = (y * y + c) % n
|
|
138
|
+
q = q * abs(x - y) % n
|
|
139
|
+
g = math.gcd(q, n)
|
|
140
|
+
k += m
|
|
141
|
+
r *= 2
|
|
142
|
+
it += r
|
|
143
|
+
if it > max_iter:
|
|
144
|
+
break
|
|
145
|
+
if g == n:
|
|
146
|
+
# the batch overshot: step one at a time from the saved point
|
|
147
|
+
g = 1
|
|
148
|
+
while g == 1:
|
|
149
|
+
ys = (ys * ys + c) % n
|
|
150
|
+
g = math.gcd(abs(x - ys), n)
|
|
151
|
+
if 1 < g < n:
|
|
152
|
+
return g
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class XorShift64:
|
|
157
|
+
"""The 64-bit xorshift generator ``choose_a`` in the Rust engine uses, seeded the same
|
|
158
|
+
way, so that family number i picks the same primes for a here and there."""
|
|
159
|
+
|
|
160
|
+
MASK = (1 << 64) - 1
|
|
161
|
+
|
|
162
|
+
def __init__(self, seed: int):
|
|
163
|
+
self.state = ((seed * 0x9E3779B97F4A7C15) + 0xD1B54A32D192ED03) & self.MASK
|
|
164
|
+
|
|
165
|
+
def next(self) -> int:
|
|
166
|
+
x = self.state
|
|
167
|
+
x ^= (x << 13) & self.MASK
|
|
168
|
+
x ^= x >> 7
|
|
169
|
+
x ^= (x << 17) & self.MASK
|
|
170
|
+
self.state = x
|
|
171
|
+
return x
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def log2_int(n: int) -> float:
|
|
175
|
+
"""log2 of a positive integer of any size (``math.log2`` handles big ints exactly enough)."""
|
|
176
|
+
return math.log2(n)
|