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.
- square_rulerpack-0.2.0/LICENSE +21 -0
- square_rulerpack-0.2.0/PKG-INFO +253 -0
- square_rulerpack-0.2.0/README.md +219 -0
- square_rulerpack-0.2.0/pyproject.toml +43 -0
- square_rulerpack-0.2.0/setup.cfg +4 -0
- square_rulerpack-0.2.0/square_rulerpack/__init__.py +36 -0
- square_rulerpack-0.2.0/square_rulerpack/__main__.py +47 -0
- square_rulerpack-0.2.0/square_rulerpack/arith.py +176 -0
- square_rulerpack-0.2.0/square_rulerpack/engine.py +980 -0
- square_rulerpack-0.2.0/square_rulerpack/lessons.py +774 -0
- square_rulerpack-0.2.0/square_rulerpack/linalg.py +101 -0
- square_rulerpack-0.2.0/square_rulerpack/native.py +101 -0
- square_rulerpack-0.2.0/square_rulerpack/params.py +128 -0
- square_rulerpack-0.2.0/square_rulerpack/walkthrough.py +426 -0
- square_rulerpack-0.2.0/square_rulerpack/workers.py +69 -0
- square_rulerpack-0.2.0/square_rulerpack.egg-info/PKG-INFO +253 -0
- square_rulerpack-0.2.0/square_rulerpack.egg-info/SOURCES.txt +22 -0
- square_rulerpack-0.2.0/square_rulerpack.egg-info/dependency_links.txt +1 -0
- square_rulerpack-0.2.0/square_rulerpack.egg-info/entry_points.txt +2 -0
- square_rulerpack-0.2.0/square_rulerpack.egg-info/requires.txt +12 -0
- square_rulerpack-0.2.0/square_rulerpack.egg-info/top_level.txt +1 -0
- square_rulerpack-0.2.0/tests/test_factor.py +134 -0
- square_rulerpack-0.2.0/tests/test_lessons.py +120 -0
- square_rulerpack-0.2.0/tests/test_native.py +68 -0
|
@@ -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,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())
|