square-rulerpack 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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lars Rocha
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.
@@ -0,0 +1,253 @@
1
+ Metadata-Version: 2.4
2
+ Name: square-rulerpack
3
+ Version: 0.2.0
4
+ Summary: The self-initialising quadratic sieve of square-rulerpack 0.2.0 in Python, with a step-by-step walkthrough for teaching
5
+ Author: Lars Rocha
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/oppressionslayer/square-rulerpack-py
8
+ Project-URL: Rust crate, https://crates.io/crates/square-rulerpack
9
+ Keywords: quadratic-sieve,siqs,factorization,number-theory,teaching,rulerpack
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Education
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
20
+ Classifier: Topic :: Education
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: numpy>=1.24
25
+ Requires-Dist: pandas>=2.0
26
+ Provides-Extra: fast
27
+ Requires-Dist: numba>=0.59; extra == "fast"
28
+ Provides-Extra: native
29
+ Requires-Dist: square-rulerpack-native==0.2.0; extra == "native"
30
+ Provides-Extra: test
31
+ Requires-Dist: pytest; extra == "test"
32
+ Requires-Dist: sympy; extra == "test"
33
+ Dynamic: license-file
34
+
35
+ # square-rulerpack in Python
36
+
37
+ The self-initialising quadratic sieve with double large primes of
38
+ [square-rulerpack 0.2.0](https://crates.io/crates/square-rulerpack) (Rust, by Lars Rocha),
39
+ ported stage by stage to Python so that every step can be run by hand in IPython and looked
40
+ at as a DataFrame or a numpy array. Same parameter tables, same Knuth–Schroeppel multiplier,
41
+ same family selection (family *i* picks the same primes for *a* as the Rust engine's family *i*),
42
+ same Gray-code polynomial switching, same Q1/Q2 forms, same exact and plain thresholds, same
43
+ large-prime cycle finder and square root. The sieve kernel is numba when it is installed and
44
+ numpy slices otherwise; relation collection runs across processes.
45
+
46
+ ```sh
47
+ pip install square-rulerpack # from PyPI; add '[fast]' for the numba kernel
48
+ pip install -e . # or from this checkout
49
+ python -m square_rulerpack 1009732533765211 --workers 8
50
+ ```
51
+
52
+ ```
53
+ 1009732533765211 = 11344301 × 89007911
54
+ 2 factors, product checks: true, all probable prime: true, 0.34 s
55
+ ```
56
+
57
+ ## Factor a number
58
+
59
+ ```python
60
+ import square_rulerpack as sq
61
+ sq.factor_completely(1009732533765211) # [11344301, 89007911]
62
+ sq.factor_completely(n, workers=8, verbose=True) # progress on stderr, like the Rust CLI
63
+ sq.factor(n, workers=4) # one nontrivial factor or None
64
+ ```
65
+
66
+ `Settings` carries what the Rust engine reads from the environment:
67
+
68
+ | Settings field | Rust variable | default |
69
+ |---|---|---|
70
+ | `exact` | `SIQS_EXACT` | exact threshold from 215 bits (about 65 digits), plain below |
71
+ | `small` | `SIQS_SMALL` | 512 unsieved primes under the exact threshold, 30 under the plain |
72
+ | `q2` | `SIQS_Q2` | Q2 polynomials when k·n ≡ 1 (mod 8) |
73
+ | `dlp` | `SIQS_DLP` | two large primes |
74
+ | `close_a` | `SIQS_CLOSE_A` | repair *a* toward √(2kn)/M |
75
+ | `ecap`, `margin` | `SIQS_ECAP`, `SIQS_MARGIN` | 32, 1 |
76
+ | `multiplier` | `SIQS_MULT` | choose by score |
77
+
78
+ ```python
79
+ sq.factor_completely(n, settings=sq.Settings(exact=False, q2=False))
80
+ ```
81
+
82
+ ## Walk through a factorisation by hand
83
+
84
+ `Walkthrough` runs the stages one at a time. Each method prints a one-line summary on stderr
85
+ and returns the stage's data; the object keeps everything (`w.k`, `w.kn`, `w.base`, `w.fam`,
86
+ `w.fs`, `w.full`, `w.cycles`, `w.deps` …) for further digging. `WALKTHROUGH.md` is a full
87
+ session on a 40-digit number; `examples_walkthrough.py` reproduces it.
88
+
89
+ ```python
90
+ import square_rulerpack as sq
91
+ w = sq.Walkthrough(5857505067604849028619727330686829482307, workers=4)
92
+
93
+ w.summary() # digits, bits, trial division, square and primality checks, parameters
94
+ w.multiplier() # Knuth–Schroeppel scores for every candidate k; the chosen one
95
+ fb = w.factor_base() # DataFrame: p, sqrt(kn) mod p, both roots, log2 p, byte log, sieved?
96
+ w.family() # a = q1 q2 … q_s, the B_l, how many polynomials (2^(s-1))
97
+ w.polynomials() # every b of the family in Gray-code order, lin and c of g(x)
98
+ w.roots(0) # the two roots of g mod each prime, their sieve starts, g(root) mod p == 0
99
+ s = w.sieve(0) # s.acc: the int16 sieve array over x in [-M, M]; s.hits: the survivors
100
+ s.table(20) # positions by sieve sum: x, g(x), log2|g|, sum, threshold, survives
101
+ w.trial_divide(0) # each survivor: factor-base part, cofactor, kind (full / one-large /
102
+ # two-large / drop: why), y, and the check y² ≡ product (mod kn)
103
+ w.collect() # the rest with worker processes until enough relations
104
+ w.relations(); w.partials(); w.cycle_summary(); w.relation(0)
105
+ w.matrix() # 0/1 DataFrame (odd exponents) for small cases, sparse rows otherwise
106
+ w.dependencies() # row sets that XOR to zero
107
+ w.square_root(0) # X, Y, X² ≡ Y² (mod kn), gcd(X − Y, n), gcd(X + Y, n), the factor
108
+ w.factors() # try the dependencies in order
109
+ ```
110
+
111
+ Or `w.run()` for everything at once.
112
+
113
+ What the stages are, in the engine's own words (module docstrings say more):
114
+
115
+ 1. **multiplier** — pick k so that k·n is a square modulo many small primes.
116
+ 2. **factor base** — the first `base` primes with (k·n / p) = 1, each with √(k·n) mod p.
117
+ 3. **family** — a = q₁⋯q_s from the primes just above 1000 with a ≈ √(2kn)/M; B_l with
118
+ B_l ≡ √kn (mod q_l) and ≡ 0 modulo the other q's; every b = Σ ±B_l gives b² ≡ kn (mod a).
119
+ 4. **polynomial** — g(x) = a·x² + 2b·x + (b² − kn)/a (Q1) or a·x² + b·x + (b² − kn)/(4a)
120
+ (Q2, for kn ≡ 1 mod 8); (a·x + b)² ≡ a·g(x) (mod kn), so a smooth g(x) is a relation.
121
+ Consecutive b's differ by one B_l (Gray code), so every root moves by a precomputed delta.
122
+ 5. **sieve** — each prime adds its byte log2 p at x ≡ root (mod p) along the interval; a
123
+ position whose sum reaches the threshold is a survivor. The exact threshold compares each
124
+ survivor with its own log2|g(x)| and counts the unsieved small primes exactly.
125
+ 6. **trial division** — p | g(x) ⇔ x ≡ root (mod p), so the dividing primes are read off the
126
+ roots; the cofactor is 1 (full), one prime below the bound, or two (split by Pollard–Brent).
127
+ 7. **cycles** — partials are edges between large primes; a cycle multiplies to a full relation.
128
+ 8. **matrix** — odd exponents over GF(2); singleton pruning; dense elimination for dependencies.
129
+ 9. **square root** — X = ∏ y, Y = ∏ p^(Σe/2); gcd(X − Y, n).
130
+
131
+ ## Small exact lessons (`square_rulerpack.lessons`)
132
+
133
+ For a class that wants to see the whole idea on numbers that fit on a blackboard, before the
134
+ real sieve. Everything is plain Python integers over a handful of samples; no threshold, no
135
+ large primes, no engine. Carried over from Lars Rocha's `square_research` package.
136
+
137
+ ```python
138
+ import square_rulerpack as sq
139
+
140
+ p = sq.construct_polynomial(221, [5, 7]) # a = 35; every CRT term of b; (c, 2b, a) = (1, 32, 35)
141
+ p["crt_terms"] # per prime: roots of 221 mod p, chosen root, a/p, inverse, term
142
+
143
+ w = sq.siqs_window(p["k"], p["a"], p["b"], start=-2, width=5, seeds=[2, 5, 7, 11])
144
+ w.dataframe() # x, root = a·x + b, Q(x), root² − 221 = a·Q(x), factors, smooth
145
+ sq.sieve_walk(w) # the sieve by hand: roots of Q mod p and the positions they predict
146
+
147
+ lesson = sq.factor_walkthrough(w) # FactorWalkthrough(n=221, relations=3, rank=2, nullity=1, split=(13, 17))
148
+ lesson.matrix() # parity matrix: rows = relations, columns = (−1, 2, 5, 7, 11)
149
+ lesson.dataframe("elimination") # the Gaussian elimination, row by row
150
+ lesson.check_dependency([1, 2]) # 4900 = 70², x = 138, gcd(138 − 70, 221) = 17, gcd(138 + 70, 221) = 13
151
+
152
+ sq.relation_study() # all four CRT root choices of a = 35, left/centre/right windows
153
+ sq.factor_walkthrough() # the same N = 221 lesson with no arguments
154
+ ```
155
+
156
+ The bridge to the real engine: `engine_polynomials` asks this package's own selector (the same
157
+ families as the Rust engine) for a few polynomials and turns them into such windows.
158
+
159
+ ```python
160
+ cap = sq.engine_polynomials(1009732533765211, base=64, half=128, families=8, per_family=4)
161
+ cap.dataframe() # family, index, a, b, c, a_primes, coefficients, identity_valid
162
+ windows = [cap.window(i, start=-128, width=257) for i in range(16)]
163
+ sq.factor_walkthrough(windows, original_n=cap.n).split # (11344301, 89007911)
164
+ ```
165
+
166
+ Also there: `square_window(n)` for x² − n, `polynomial_roots`, `sqrt_mod` (all roots), `prime_seeds`,
167
+ `survivor_mask`, `dependency`, and `LessonWindow.export` for JSON.
168
+
169
+ ## Fast path: the Rust engine as an extension module
170
+
171
+ The teaching code above is Python on purpose. For actually factoring, the same engine is also
172
+ available compiled, the way gmpy2 wraps GMP: `native/` in this repository is a PyO3 crate that links
173
+ the published `square-rulerpack 0.2.0` and builds into the extension module `square_rulerpack_native`
174
+ (PyPI package `square-rulerpack-native`, one abi3 wheel per platform for every Python ≥ 3.10).
175
+
176
+ ```sh
177
+ pip install square-rulerpack[native]
178
+ ```
179
+
180
+ Once it is installed, `factor` and `factor_completely` use it by default (`engine="auto"`); the
181
+ `Walkthrough` and the lessons stay in Python.
182
+
183
+ ```python
184
+ import square_rulerpack as sq
185
+ sq.HAVE_NATIVE # True
186
+ sq.factor_completely(n, workers=8) # Rust, 8 rayon threads, GIL released
187
+ sq.factor_completely(n, workers=8, engine="python") # this port, for comparison
188
+ sq.factor_completely(n, workers=8, verbose=True) # the engine's own progress on stderr
189
+ sq.native.factor_completely(n, threads=8, progress=lambda stage, full, need, partials, polys: ...)
190
+ sq.native.is_prime(2**89 - 1)
191
+ ```
192
+
193
+ `Settings` still applies: its fields are exported as the engine's `SIQS_*` variables for the
194
+ duration of the call. Python ints cross as big integers; there is no string round trip. Ctrl-C
195
+ does not stop a running sieve (the engine has no cancel hook).
196
+
197
+ Measured on an i9-11900H (8 cores), 8 threads, quiet machine:
198
+
199
+ | input | Rust extension | this Python port | `sympy.factorint` |
200
+ |---|---|---|---|
201
+ | 80-digit semiprime 5072…6537 (from an IPython session) | **15.5 s** | hours | not attempted |
202
+ | 40-digit semiprime, two random 20-digit primes | 0.01 s | 0.27 s | 7.8 s |
203
+ | 50-digit semiprime, two random 25-digit primes | 0.06 s | 0.33 s | 8.8 s |
204
+ | 60-digit semiprime, two random 30-digit primes | 0.80 s | 14.8 s | 116.6 s |
205
+
206
+ `sympy.factorint` has Pollard rho, p−1 and ECM but no quadratic sieve, so its time grows with the
207
+ size of the *smaller factor*; the sieve's grows with the size of *n* and does not care how the
208
+ factors are split. For balanced semiprimes above 40 digits the difference is minutes versus
209
+ fractions of a second.
210
+
211
+ Building from source instead of the wheel (`pip install square-rulerpack-native --no-binary :all:`
212
+ or `maturin develop --release` in `native/`) needs a Rust toolchain; `.github/workflows/native-wheels.yml`
213
+ builds the Linux, macOS and Windows wheels.
214
+
215
+ ## Speed
216
+
217
+ Measured on an i9-11900H (8 cores), numba kernel, random semiprimes:
218
+
219
+ | digits | workers | time | note |
220
+ |---|---|---|---|
221
+ | 16 | 1 | 0.3 s | the Rust crate's README example |
222
+ | 40 | 1 | 0.2 s | |
223
+ | 50 | 1 | 1.4 s | |
224
+ | 60 (197 bits) | 2 | 3.7 s | hand table row (base 3000, M = 2^16); Rust 0.26 s at 8 threads |
225
+ | 60 (199 bits) | 1 | 66 s | YAFU table row (base 5744, M = 2^15, slack 1.3) |
226
+ | 60 (199 bits) | 4 | 23 s | same row |
227
+ | 66 | 4 | 54 s | exact threshold |
228
+ | 70 | 8 | 62 s | measured on a busy machine, see `TIMINGS.md` |
229
+
230
+ Python is roughly 30–60× slower than the Rust engine per polynomial. The point of this port
231
+ is the walkthrough; for the 90- and 100-digit runs use the Rust crate (`cargo install
232
+ square-rulerpack`) or the browser page. Above about 75 digits the dense GF(2) elimination also
233
+ becomes the long pole (it is cubic; the Rust engine switches to block Lanczos at 6,000 rows).
234
+
235
+ ## Files
236
+
237
+ - `square_rulerpack/arith.py` — primes, Legendre, Tonelli–Shanks, Miller–Rabin, Pollard–Brent, xorshift64.
238
+ - `square_rulerpack/params.py` — the YAFU direct-sieve table and the hand rows, `Params.for_n`.
239
+ - `square_rulerpack/engine.py` — multiplier, factor base, families, `FamilySieve` (sieve, scan,
240
+ candidates), `Cycles`, `relations`, `square_root`, `factor_completely`.
241
+ - `square_rulerpack/linalg.py` — singleton pruning and bit-packed Gaussian elimination.
242
+ - `square_rulerpack/workers.py` — the process pool; families merged in index order, so the
243
+ relation set is the same at any worker count.
244
+ - `square_rulerpack/walkthrough.py` — `Walkthrough` and `SieveResult`.
245
+ - `square_rulerpack/lessons.py` — `construct_polynomial`, `LessonWindow`, `factor_walkthrough`,
246
+ `relation_study`, `engine_polynomials`.
247
+ - `square_rulerpack/native.py` — loader for the Rust extension, `Settings` → `SIQS_*` bridge, wrappers.
248
+ - `native/` — the PyO3 crate (`square-rulerpack-native`): `Cargo.toml`, `pyproject.toml` (maturin), `src/lib.rs`.
249
+ - `tests/test_factor.py`, `tests/test_lessons.py`, `tests/test_native.py` — `pytest tests` (25 tests, a few seconds; the native tests skip when the extension is absent).
250
+
251
+ ## License
252
+
253
+ MIT, as the Rust crate.
@@ -0,0 +1,219 @@
1
+ # square-rulerpack in Python
2
+
3
+ The self-initialising quadratic sieve with double large primes of
4
+ [square-rulerpack 0.2.0](https://crates.io/crates/square-rulerpack) (Rust, by Lars Rocha),
5
+ ported stage by stage to Python so that every step can be run by hand in IPython and looked
6
+ at as a DataFrame or a numpy array. Same parameter tables, same Knuth–Schroeppel multiplier,
7
+ same family selection (family *i* picks the same primes for *a* as the Rust engine's family *i*),
8
+ same Gray-code polynomial switching, same Q1/Q2 forms, same exact and plain thresholds, same
9
+ large-prime cycle finder and square root. The sieve kernel is numba when it is installed and
10
+ numpy slices otherwise; relation collection runs across processes.
11
+
12
+ ```sh
13
+ pip install square-rulerpack # from PyPI; add '[fast]' for the numba kernel
14
+ pip install -e . # or from this checkout
15
+ python -m square_rulerpack 1009732533765211 --workers 8
16
+ ```
17
+
18
+ ```
19
+ 1009732533765211 = 11344301 × 89007911
20
+ 2 factors, product checks: true, all probable prime: true, 0.34 s
21
+ ```
22
+
23
+ ## Factor a number
24
+
25
+ ```python
26
+ import square_rulerpack as sq
27
+ sq.factor_completely(1009732533765211) # [11344301, 89007911]
28
+ sq.factor_completely(n, workers=8, verbose=True) # progress on stderr, like the Rust CLI
29
+ sq.factor(n, workers=4) # one nontrivial factor or None
30
+ ```
31
+
32
+ `Settings` carries what the Rust engine reads from the environment:
33
+
34
+ | Settings field | Rust variable | default |
35
+ |---|---|---|
36
+ | `exact` | `SIQS_EXACT` | exact threshold from 215 bits (about 65 digits), plain below |
37
+ | `small` | `SIQS_SMALL` | 512 unsieved primes under the exact threshold, 30 under the plain |
38
+ | `q2` | `SIQS_Q2` | Q2 polynomials when k·n ≡ 1 (mod 8) |
39
+ | `dlp` | `SIQS_DLP` | two large primes |
40
+ | `close_a` | `SIQS_CLOSE_A` | repair *a* toward √(2kn)/M |
41
+ | `ecap`, `margin` | `SIQS_ECAP`, `SIQS_MARGIN` | 32, 1 |
42
+ | `multiplier` | `SIQS_MULT` | choose by score |
43
+
44
+ ```python
45
+ sq.factor_completely(n, settings=sq.Settings(exact=False, q2=False))
46
+ ```
47
+
48
+ ## Walk through a factorisation by hand
49
+
50
+ `Walkthrough` runs the stages one at a time. Each method prints a one-line summary on stderr
51
+ and returns the stage's data; the object keeps everything (`w.k`, `w.kn`, `w.base`, `w.fam`,
52
+ `w.fs`, `w.full`, `w.cycles`, `w.deps` …) for further digging. `WALKTHROUGH.md` is a full
53
+ session on a 40-digit number; `examples_walkthrough.py` reproduces it.
54
+
55
+ ```python
56
+ import square_rulerpack as sq
57
+ w = sq.Walkthrough(5857505067604849028619727330686829482307, workers=4)
58
+
59
+ w.summary() # digits, bits, trial division, square and primality checks, parameters
60
+ w.multiplier() # Knuth–Schroeppel scores for every candidate k; the chosen one
61
+ fb = w.factor_base() # DataFrame: p, sqrt(kn) mod p, both roots, log2 p, byte log, sieved?
62
+ w.family() # a = q1 q2 … q_s, the B_l, how many polynomials (2^(s-1))
63
+ w.polynomials() # every b of the family in Gray-code order, lin and c of g(x)
64
+ w.roots(0) # the two roots of g mod each prime, their sieve starts, g(root) mod p == 0
65
+ s = w.sieve(0) # s.acc: the int16 sieve array over x in [-M, M]; s.hits: the survivors
66
+ s.table(20) # positions by sieve sum: x, g(x), log2|g|, sum, threshold, survives
67
+ w.trial_divide(0) # each survivor: factor-base part, cofactor, kind (full / one-large /
68
+ # two-large / drop: why), y, and the check y² ≡ product (mod kn)
69
+ w.collect() # the rest with worker processes until enough relations
70
+ w.relations(); w.partials(); w.cycle_summary(); w.relation(0)
71
+ w.matrix() # 0/1 DataFrame (odd exponents) for small cases, sparse rows otherwise
72
+ w.dependencies() # row sets that XOR to zero
73
+ w.square_root(0) # X, Y, X² ≡ Y² (mod kn), gcd(X − Y, n), gcd(X + Y, n), the factor
74
+ w.factors() # try the dependencies in order
75
+ ```
76
+
77
+ Or `w.run()` for everything at once.
78
+
79
+ What the stages are, in the engine's own words (module docstrings say more):
80
+
81
+ 1. **multiplier** — pick k so that k·n is a square modulo many small primes.
82
+ 2. **factor base** — the first `base` primes with (k·n / p) = 1, each with √(k·n) mod p.
83
+ 3. **family** — a = q₁⋯q_s from the primes just above 1000 with a ≈ √(2kn)/M; B_l with
84
+ B_l ≡ √kn (mod q_l) and ≡ 0 modulo the other q's; every b = Σ ±B_l gives b² ≡ kn (mod a).
85
+ 4. **polynomial** — g(x) = a·x² + 2b·x + (b² − kn)/a (Q1) or a·x² + b·x + (b² − kn)/(4a)
86
+ (Q2, for kn ≡ 1 mod 8); (a·x + b)² ≡ a·g(x) (mod kn), so a smooth g(x) is a relation.
87
+ Consecutive b's differ by one B_l (Gray code), so every root moves by a precomputed delta.
88
+ 5. **sieve** — each prime adds its byte log2 p at x ≡ root (mod p) along the interval; a
89
+ position whose sum reaches the threshold is a survivor. The exact threshold compares each
90
+ survivor with its own log2|g(x)| and counts the unsieved small primes exactly.
91
+ 6. **trial division** — p | g(x) ⇔ x ≡ root (mod p), so the dividing primes are read off the
92
+ roots; the cofactor is 1 (full), one prime below the bound, or two (split by Pollard–Brent).
93
+ 7. **cycles** — partials are edges between large primes; a cycle multiplies to a full relation.
94
+ 8. **matrix** — odd exponents over GF(2); singleton pruning; dense elimination for dependencies.
95
+ 9. **square root** — X = ∏ y, Y = ∏ p^(Σe/2); gcd(X − Y, n).
96
+
97
+ ## Small exact lessons (`square_rulerpack.lessons`)
98
+
99
+ For a class that wants to see the whole idea on numbers that fit on a blackboard, before the
100
+ real sieve. Everything is plain Python integers over a handful of samples; no threshold, no
101
+ large primes, no engine. Carried over from Lars Rocha's `square_research` package.
102
+
103
+ ```python
104
+ import square_rulerpack as sq
105
+
106
+ p = sq.construct_polynomial(221, [5, 7]) # a = 35; every CRT term of b; (c, 2b, a) = (1, 32, 35)
107
+ p["crt_terms"] # per prime: roots of 221 mod p, chosen root, a/p, inverse, term
108
+
109
+ w = sq.siqs_window(p["k"], p["a"], p["b"], start=-2, width=5, seeds=[2, 5, 7, 11])
110
+ w.dataframe() # x, root = a·x + b, Q(x), root² − 221 = a·Q(x), factors, smooth
111
+ sq.sieve_walk(w) # the sieve by hand: roots of Q mod p and the positions they predict
112
+
113
+ lesson = sq.factor_walkthrough(w) # FactorWalkthrough(n=221, relations=3, rank=2, nullity=1, split=(13, 17))
114
+ lesson.matrix() # parity matrix: rows = relations, columns = (−1, 2, 5, 7, 11)
115
+ lesson.dataframe("elimination") # the Gaussian elimination, row by row
116
+ lesson.check_dependency([1, 2]) # 4900 = 70², x = 138, gcd(138 − 70, 221) = 17, gcd(138 + 70, 221) = 13
117
+
118
+ sq.relation_study() # all four CRT root choices of a = 35, left/centre/right windows
119
+ sq.factor_walkthrough() # the same N = 221 lesson with no arguments
120
+ ```
121
+
122
+ The bridge to the real engine: `engine_polynomials` asks this package's own selector (the same
123
+ families as the Rust engine) for a few polynomials and turns them into such windows.
124
+
125
+ ```python
126
+ cap = sq.engine_polynomials(1009732533765211, base=64, half=128, families=8, per_family=4)
127
+ cap.dataframe() # family, index, a, b, c, a_primes, coefficients, identity_valid
128
+ windows = [cap.window(i, start=-128, width=257) for i in range(16)]
129
+ sq.factor_walkthrough(windows, original_n=cap.n).split # (11344301, 89007911)
130
+ ```
131
+
132
+ Also there: `square_window(n)` for x² − n, `polynomial_roots`, `sqrt_mod` (all roots), `prime_seeds`,
133
+ `survivor_mask`, `dependency`, and `LessonWindow.export` for JSON.
134
+
135
+ ## Fast path: the Rust engine as an extension module
136
+
137
+ The teaching code above is Python on purpose. For actually factoring, the same engine is also
138
+ available compiled, the way gmpy2 wraps GMP: `native/` in this repository is a PyO3 crate that links
139
+ the published `square-rulerpack 0.2.0` and builds into the extension module `square_rulerpack_native`
140
+ (PyPI package `square-rulerpack-native`, one abi3 wheel per platform for every Python ≥ 3.10).
141
+
142
+ ```sh
143
+ pip install square-rulerpack[native]
144
+ ```
145
+
146
+ Once it is installed, `factor` and `factor_completely` use it by default (`engine="auto"`); the
147
+ `Walkthrough` and the lessons stay in Python.
148
+
149
+ ```python
150
+ import square_rulerpack as sq
151
+ sq.HAVE_NATIVE # True
152
+ sq.factor_completely(n, workers=8) # Rust, 8 rayon threads, GIL released
153
+ sq.factor_completely(n, workers=8, engine="python") # this port, for comparison
154
+ sq.factor_completely(n, workers=8, verbose=True) # the engine's own progress on stderr
155
+ sq.native.factor_completely(n, threads=8, progress=lambda stage, full, need, partials, polys: ...)
156
+ sq.native.is_prime(2**89 - 1)
157
+ ```
158
+
159
+ `Settings` still applies: its fields are exported as the engine's `SIQS_*` variables for the
160
+ duration of the call. Python ints cross as big integers; there is no string round trip. Ctrl-C
161
+ does not stop a running sieve (the engine has no cancel hook).
162
+
163
+ Measured on an i9-11900H (8 cores), 8 threads, quiet machine:
164
+
165
+ | input | Rust extension | this Python port | `sympy.factorint` |
166
+ |---|---|---|---|
167
+ | 80-digit semiprime 5072…6537 (from an IPython session) | **15.5 s** | hours | not attempted |
168
+ | 40-digit semiprime, two random 20-digit primes | 0.01 s | 0.27 s | 7.8 s |
169
+ | 50-digit semiprime, two random 25-digit primes | 0.06 s | 0.33 s | 8.8 s |
170
+ | 60-digit semiprime, two random 30-digit primes | 0.80 s | 14.8 s | 116.6 s |
171
+
172
+ `sympy.factorint` has Pollard rho, p−1 and ECM but no quadratic sieve, so its time grows with the
173
+ size of the *smaller factor*; the sieve's grows with the size of *n* and does not care how the
174
+ factors are split. For balanced semiprimes above 40 digits the difference is minutes versus
175
+ fractions of a second.
176
+
177
+ Building from source instead of the wheel (`pip install square-rulerpack-native --no-binary :all:`
178
+ or `maturin develop --release` in `native/`) needs a Rust toolchain; `.github/workflows/native-wheels.yml`
179
+ builds the Linux, macOS and Windows wheels.
180
+
181
+ ## Speed
182
+
183
+ Measured on an i9-11900H (8 cores), numba kernel, random semiprimes:
184
+
185
+ | digits | workers | time | note |
186
+ |---|---|---|---|
187
+ | 16 | 1 | 0.3 s | the Rust crate's README example |
188
+ | 40 | 1 | 0.2 s | |
189
+ | 50 | 1 | 1.4 s | |
190
+ | 60 (197 bits) | 2 | 3.7 s | hand table row (base 3000, M = 2^16); Rust 0.26 s at 8 threads |
191
+ | 60 (199 bits) | 1 | 66 s | YAFU table row (base 5744, M = 2^15, slack 1.3) |
192
+ | 60 (199 bits) | 4 | 23 s | same row |
193
+ | 66 | 4 | 54 s | exact threshold |
194
+ | 70 | 8 | 62 s | measured on a busy machine, see `TIMINGS.md` |
195
+
196
+ Python is roughly 30–60× slower than the Rust engine per polynomial. The point of this port
197
+ is the walkthrough; for the 90- and 100-digit runs use the Rust crate (`cargo install
198
+ square-rulerpack`) or the browser page. Above about 75 digits the dense GF(2) elimination also
199
+ becomes the long pole (it is cubic; the Rust engine switches to block Lanczos at 6,000 rows).
200
+
201
+ ## Files
202
+
203
+ - `square_rulerpack/arith.py` — primes, Legendre, Tonelli–Shanks, Miller–Rabin, Pollard–Brent, xorshift64.
204
+ - `square_rulerpack/params.py` — the YAFU direct-sieve table and the hand rows, `Params.for_n`.
205
+ - `square_rulerpack/engine.py` — multiplier, factor base, families, `FamilySieve` (sieve, scan,
206
+ candidates), `Cycles`, `relations`, `square_root`, `factor_completely`.
207
+ - `square_rulerpack/linalg.py` — singleton pruning and bit-packed Gaussian elimination.
208
+ - `square_rulerpack/workers.py` — the process pool; families merged in index order, so the
209
+ relation set is the same at any worker count.
210
+ - `square_rulerpack/walkthrough.py` — `Walkthrough` and `SieveResult`.
211
+ - `square_rulerpack/lessons.py` — `construct_polynomial`, `LessonWindow`, `factor_walkthrough`,
212
+ `relation_study`, `engine_polynomials`.
213
+ - `square_rulerpack/native.py` — loader for the Rust extension, `Settings` → `SIQS_*` bridge, wrappers.
214
+ - `native/` — the PyO3 crate (`square-rulerpack-native`): `Cargo.toml`, `pyproject.toml` (maturin), `src/lib.rs`.
215
+ - `tests/test_factor.py`, `tests/test_lessons.py`, `tests/test_native.py` — `pytest tests` (25 tests, a few seconds; the native tests skip when the extension is absent).
216
+
217
+ ## License
218
+
219
+ MIT, as the Rust crate.
@@ -0,0 +1,43 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "square-rulerpack"
7
+ version = "0.2.0"
8
+ description = "The self-initialising quadratic sieve of square-rulerpack 0.2.0 in Python, with a step-by-step walkthrough for teaching"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{name = "Lars Rocha"}]
14
+ dependencies = ["numpy>=1.24", "pandas>=2.0"]
15
+ keywords = ["quadratic-sieve", "siqs", "factorization", "number-theory", "teaching", "rulerpack"]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Intended Audience :: Education",
19
+ "Intended Audience :: Science/Research",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Programming Language :: Python :: 3.14",
26
+ "Topic :: Scientific/Engineering :: Mathematics",
27
+ "Topic :: Education",
28
+ ]
29
+
30
+ [project.urls]
31
+ Homepage = "https://github.com/oppressionslayer/square-rulerpack-py"
32
+ "Rust crate" = "https://crates.io/crates/square-rulerpack"
33
+
34
+ [project.optional-dependencies]
35
+ fast = ["numba>=0.59"]
36
+ native = ["square-rulerpack-native==0.2.0"]
37
+ test = ["pytest", "sympy"]
38
+
39
+ [project.scripts]
40
+ square-rulerpack-py = "square_rulerpack.__main__:main"
41
+
42
+ [tool.setuptools]
43
+ packages = ["square_rulerpack"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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())