rulerpack 0.2.1__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,372 @@
1
+ Metadata-Version: 2.4
2
+ Name: rulerpack
3
+ Version: 0.2.1
4
+ Summary: Lars Rocha's XOR rulers: pack a prime and its index into one integer and recover both with mod and XOR.
5
+ Author: Lars Rocha
6
+ License: MIT
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: numpy>=1.24
11
+ Provides-Extra: test
12
+ Requires-Dist: pytest; extra == "test"
13
+ Provides-Extra: fast
14
+ Requires-Dist: gmpy2; extra == "fast"
15
+ Dynamic: license-file
16
+
17
+ # rulerpack
18
+
19
+ XOR rulers over the primes, and the pack that rides on them.
20
+ Created by Lars Rocha. Python implementation with Claude assisting.
21
+
22
+ ```python
23
+ import rulerpack as rp
24
+
25
+ N = rp.pack(78501, 1000039) # 8592468642374094, one integer
26
+ rp.unpack(N) # (78501, 1000039), the index and the prime back
27
+ ```
28
+
29
+ Decoding is mod, div and two XORs. No search, no primality test, no table.
30
+
31
+ ## Credit
32
+
33
+ The ideas in this library are Lars Rocha's. Claude assisted with the
34
+ implementation and the measurements; that does not imply ownership.
35
+
36
+ | idea | what it became here |
37
+ |---|---|
38
+ | XOR the doubled prime against a ruler and against the index lattice | the pack: `K ^ X = L(i+c)`, one integer opens to the pair in four ops, no collisions |
39
+ | sieve the leftovers with the 3..131 seeds | `allowed`, `window`; taken to the square root it is `primes_between`, no primality test |
40
+ | anchor at a number and calculate the residues there | Eratosthenes started anywhere: one division per seed, then strike-outs |
41
+ | the prime closest to the anchor | survivors in distance order; `next_prime_after` is the first |
42
+ | the survivors are a list, not a stream | `next_prime_after_parallel`: certify on every core, 4x gmpy2's `next_prime` at 12,000 bits |
43
+ | count once, then increment | `indexed_after`: next n primes with exact indices and packs; thousands of times faster than calling primecount's `nth_prime` per index, about 5x slower than primesieve's list mode, which returns no indices |
44
+ | remember the counts | `Marks` and `LiveMarks`: each checkpoint a packed N |
45
+ | the seed rule at the cube root | its rejections are Meissel's correction term, measured equal |
46
+
47
+ The segmented sieve, Legendre's phi, Meissel's formula, Miller-Rabin and the
48
+ li / R estimates are classical and credited to their authors. primecount is
49
+ Kim Walisch's and is used as an optional counter.
50
+
51
+ ## The ruler
52
+
53
+ Every ruler here is a Mersenne ladder, `m ^ (m+1)`, scaled by a power of two.
54
+ Drawn as tick marks it is a tape measure: every block gets a tick, and the
55
+ tick is taller at every power of two.
56
+
57
+ ```
58
+ 0 | 1
59
+ 1 || 3
60
+ 2 | 1
61
+ 3 ||| 7
62
+ 4 | 1
63
+ 5 || 3
64
+ 6 | 1
65
+ 7 |||| 15
66
+ ```
67
+
68
+ | name | formula | equals |
69
+ |---|---|---|
70
+ | `rp.RP(i)` | `(4i) ^ (4i+7)` | `2^(v2(i+1)+3) - 1`, the original 7, 15, 7, 31, ... |
71
+ | `rp.ruler(i, L, c)` | `((Li) ^ (Li+cL+1)) ^ 1` | `L * (i ^ (i+c))` |
72
+ | `rp.R257(i)` | L=4, c=64 | `256 * ladder(i >> 6)` |
73
+ | `rp.R513(i)` | L=256, c=2 | `512 * ladder(i >> 1)` |
74
+ | `rp.R896(i)` | `(896 + 512i) ^ (512i)` | `384 + 512 * ladder(i)`: 896, 1920, 896, 3968, ... |
75
+ | `rp.carry(i)` | `ladder(i) ^ ladder(i+1)` | 2, 6, 6, 2, 2, 14, 14, 2, ... the ruler's carry |
76
+
77
+ **The 512 slide.** `rp.slide512(primes)` is the session's `((p*512) ^ 512) ^ R896(i)`;
78
+ every entry is `384 + 512 * ((p ^ 1) ^ ladder(i))`, so the array is 384 mod 512 and
79
+ 128 mod 256 for any input at all, and `rp.unslide512(B, i)` reads the primes straight
80
+ back. `rp.slide_repeats(primes)` finds where it repeats, `B[i] == B[i+1]`, which is
81
+ exactly where `p ^ p' == carry(i)`: 165 of the first 2,261 steps, two thirds of them
82
+ twin primes. The carry is independent of the gap, so the repeats are a lamp on the
83
+ sequence rather than a sieve of it; the measurements are in `ruler_stack` next door.
84
+
85
+ ## The pack
86
+
87
+ For a prime `p` at zero-based index `i`, in the family `(L, c, M)`:
88
+
89
+ ```
90
+ K = 2p ^ ruler(i, L, c)
91
+ X = 2p ^ L*i
92
+ N = K*M + X M = 2^shift, fixed for the family; X < M is checked on encode
93
+ ```
94
+
95
+ Then `K ^ X = L*(i + c)` for every `i` and `p`, so
96
+
97
+ ```
98
+ X = N mod M, K = N div M, i = (K ^ X)/L - c, p = (X ^ L*i)/2
99
+ ```
100
+
101
+ `M` is a parameter of the family like `L` and `c`. It is never stored in `N`
102
+ and never guessed back from it; guessing it is a search and the search collides.
103
+
104
+ ```python
105
+ rp.F513 # Family(L=256, c=2, shift=32), the default
106
+ rp.F257 # Family(L=4, c=64, shift=32)
107
+ rp.Family(256, 2, 1100) # for 1000-bit primes
108
+ rp.encode(i, p), rp.decode(K, X)
109
+ rp.encode_array(P) # numpy, a million primes at once
110
+ ```
111
+
112
+ Verified round trip on the first 1,000,000 primes for both families.
113
+
114
+ ## Counting, the forward map
115
+
116
+ Each method is its own function so they can be compared.
117
+
118
+ | function | what | π(10^9) |
119
+ |---|---|---|
120
+ | `rp.primepi_sieve(x)` | sieve to x | 28.9 s |
121
+ | `rp.phi(x, a)` | Legendre: numbers up to x with no factor among the first a primes | |
122
+ | `rp.primepi_legendre(x)` | `phi(x, a) + a - 1`, a = π(√x) | 4.3 s |
123
+ | `rp.primepi_meissel(x)` | a = π(∛x) plus the correction sum; `rp.primepi` | 0.07 s |
124
+ | `rp.index_of(p)` | `primepi(p) - 1`, refuses a composite | |
125
+ | `rp.nth_prime(i)` | Riemann R guesses, Meissel pins, the walk closes | billionth in 0.67 s |
126
+
127
+ All exact, checked against the published values of π up to 10^12.
128
+
129
+ The seed rule (odd, no factor in 3..131, not a square) is Legendre's `phi`
130
+ applied one candidate at a time. With seeds up to ∛x, the composites that
131
+ survive the rule are exactly the terms of Meissel's correction sum. Checked:
132
+ at x = 10^6 both are 42,286.
133
+
134
+ If you need fast counting, use [primecount](https://github.com/kimwalisch/primecount).
135
+ It is about 100x faster than this at 10^12. This library counts so the pack
136
+ can be tested from nothing; counting is not what it is for.
137
+
138
+ ## Marks, our own way
139
+
140
+ Sieve once, keep every k-th prime as a packed `N`, and from then on walk from
141
+ the nearest mark with the seed rule and `isprime`. No li, no Riemann, no
142
+ Meissel. Each mark carries its own index and prime, so the table is self-describing.
143
+
144
+ ```python
145
+ M = rp.Marks.build(50_000_000, every=1024) # 4.4 s, 48,829 marks
146
+ M.nth_prime(49_999_999) # 982451653 in about 1 ms
147
+ M.index_of(982451653) # 49999999
148
+ ```
149
+
150
+ Flat a few milliseconds at any index the table covers. The Rust crate
151
+ (`rulerpack-rs`) does the same with a spacing of 64 in about 100 µs.
152
+ `M.pi(x)` counts to any x the table covers by the same walk.
153
+
154
+ ## Live marks, the table that fills itself
155
+
156
+ Same walk, same pack, no table up front. A block you have never touched costs
157
+ one counting call (`nth_prime`: Riemann guess, Meissel pin, walk) and is stored
158
+ as a packed N; every later lookup in that block is a walk.
159
+
160
+ ```python
161
+ lm = rp.LiveMarks(every=64)
162
+ lm.nth_prime(999_999_999) # 22801763489 in 0.67 s, one mark computed
163
+ lm.nth_prime(999_999_998) # 22801763477 in 1 ms, same mark
164
+ lm.pi(10**9) # 50847534 in 0.19 s, three marks
165
+ lm.seed(rp.Marks.load()) # start from a built table
166
+ ```
167
+
168
+ ## The window: a sieve of Eratosthenes that starts anywhere
169
+
170
+ Every candidate is `anchor + d`, so its residue mod a seed q is `(anchor mod q) + d`.
171
+ One division per seed finds where the multiples of q fall in the window; after
172
+ that it is strike-outs, no arithmetic per candidate.
173
+
174
+ ```python
175
+ rp.window(10**12, 1_000_000) # survivor offsets, closest first; 2 ms
176
+ rp.next_prime_after(10**12) # 1000000000039 in about 100 us
177
+ rp.next_prime_after(2**60) # 1152921504606847009
178
+ rp.primes_between(10**15, 10**15 + 10**7) # every prime there, certified, 0.5 s
179
+ ```
180
+
181
+ Any size. The first window is the largest known prime gap plus slack below
182
+ 2^64 (the next prime is always inside) and 7 ln(anchor) above (one miss in a
183
+ thousand, then it slides). Seed depth follows the cost of a test: the 31 small
184
+ seeds through 512 bits, 8191 through 1536, up to a million above, where a
185
+ single certification costs 100 ms and the deeper sieve still pays. A base-2
186
+ probable-prime test runs before the full one.
187
+
188
+ The survivors are independent, so the parallel paths certify them on every
189
+ core. `next_prime_after_parallel` returns the smallest that passes, same
190
+ answers as gmpy2's `next_prime`. `any_prime_after` returns the first pass from
191
+ any core: a prime above the anchor, not necessarily the next one, with no stop
192
+ rule to wait on. For "a random prime of this size" that is the fast path.
193
+ The worker pool stays alive between calls (`rp.close_pools()` to release it).
194
+
195
+ | anchor | gmpy2.next_prime | any_prime_after | next_prime_after_parallel |
196
+ |---|---|---|---|
197
+ | 2^2048 | 0.15 s | 0.07 s | 0.08 s |
198
+ | 2^4096 | 1.29 s | 0.43 s | 0.68 s |
199
+ | 2^8191 | 7.84 s | 2.91 s | 3.31 s |
200
+ | 2^12191 | 61.6 s | 14.7 s | 21.2 s |
201
+
202
+ (16 cores, warm pool, every result a verified prime.) The ordering of the
203
+ survivors is the one knob that cannot be improved: the next prime needs every
204
+ survivor below it cleared, and offset order tests exactly those. We measured
205
+ residue statistics, bit patterns, Jacobi symbols and offset keys as sort keys
206
+ against a random key; the only things that sort are the offset itself (the
207
+ gap distribution) and a modular power of the candidate. `ranked_window` shows
208
+ that order; `order="squeeze"` (both ends toward the centre) and `key=` are
209
+ there to try your own. `gap_profile` accounts for a range: gap histogram,
210
+ twin share against 1.32/ln x, mean against ln x.
211
+
212
+ `window` strikes with the 31 seeds up to 131 and leaves primes plus a few
213
+ composites for `isprime` to settle. `primes_between` brings every prime up to
214
+ the square root of the top, so what survives is prime and nothing is tested.
215
+ Counts agree with primecount at 10^12, 10^15 and 10^18.
216
+
217
+ | primes in | count | Python | Rust |
218
+ |---|---|---|---|
219
+ | [10^12, +10^7) | 361,726 | 0.12 s | 38 ms |
220
+ | [10^15, +10^7) | 289,394 | 0.52 s | 161 ms |
221
+ | [10^18, +10^6) | 24,280 | 7.7 s | 3.7 s |
222
+
223
+ (The 10^18 case is mostly building the 50 million seeds up to 10^9.)
224
+
225
+ ```
226
+ rulerpack --between 1000000000000 1000000000100
227
+ rulerpack --next 1000000000000
228
+ ```
229
+
230
+ ## Hints: the next prime from its low bits, no test
231
+
232
+ A number between two consecutive primes is less than one gap from the next
233
+ prime, so it cannot share that prime's low `b` bits once 2^b exceeds the gap.
234
+ `hinted_next(anchor, hint, bits)` is therefore one modulo and one add, and it
235
+ is right whenever the gap is under 2^bits: proven for 11 bits below 2^64
236
+ (largest gap 1,550), heuristic above (13 bits to about 1e39, 17 bits to about
237
+ 2^256 with headroom). Choose the bits.
238
+
239
+ ```python
240
+ p = rp.next_prime_after(2**256) # certified once: 2^256 + 297
241
+ rp.hinted_next(2**256, rp.hint_of(p, 17)) # p again, about 1 us, no test
242
+ ```
243
+
244
+ The hint can live inside the pack. `hinted_family(bits)` uses L = 2^(bits+1),
245
+ so the bottom bits of X are the bottom bits of 2p, untouched by the index, and
246
+ `K ^ X = L(i+c)` still opens the pair. A bare stream of X values then walks
247
+ the primes from any start, no K, no index, no count, no test:
248
+
249
+ ```python
250
+ fam = rp.hinted_family(17, shift=300)
251
+ K, X = fam.encode_array(rp.primes_first(1_000_000))
252
+ rp.primes_from_x(2, X[1:], 17) # every prime back, 0.5 s, zero tests
253
+ ```
254
+
255
+ **Seventeen bits per prime is the whole thing.** Strip X down to the hints and
256
+ throw the rest away; the walk still runs, and so does the comparison Lars ran
257
+ in the session:
258
+
259
+ ```python
260
+ hints = (X >> 1) & ((1 << 17) - 1) # rp.hints_from_x(X, 17)
261
+
262
+ p = 2
263
+ out = []
264
+ for h in hints[1:31]:
265
+ p = rp.hinted_next(p, int(h), 17)
266
+ out.append(p)
267
+
268
+ out == rp.primes_from_x(2, X[1:31], 17) # True
269
+ rp.hint_walk(2, hints[1:], 17) # the same walk, as one call
270
+ ```
271
+
272
+ So one encoding reads two ways: K and X together open any pair directly, by
273
+ index or by prime, and the low bits of X alone rebuild the whole sequence in
274
+ order. The walk is not about primes in particular; any increasing integers
275
+ rebuild from their low bits as long as each step stays under the hint modulus,
276
+ and the prime gaps do so to about 2^256 at 17 bits.
277
+
278
+ ## Indexed primes above an anchor
279
+
280
+ Count once at the anchor, then every prime found above it takes the next index
281
+ and packs. With primecount installed the count is milliseconds at any size.
282
+
283
+ Baseline, stated exactly. The only route in Kim Walisch's tools that returns an
284
+ index is primecount's `nth_prime`, called once per prime; against that this is
285
+ thousands of times faster (10,000 indexed primes above 10^12: 39 ms here, 190 s
286
+ there). His list mode, primesieve, sieves a range without indices and is faster
287
+ than this at listing: a million primes above 10^14 in 0.04 s against 1.1 s for
288
+ `primes_between` in Python and 0.22 s for the Rust `between`. A reader with both
289
+ tools gets an indexed list in about 0.3 s. What they do not get is the pack,
290
+ the marks, or random access by index; that is what this library is for.
291
+ The Rust crate's `indexed_after` runs the same race in compiled code: given the
292
+ count, primesieve lists a million primes above 10^14 in 49 ms and the Rust
293
+ sieve takes 597 ms while also attaching indices and packs.
294
+
295
+ ```python
296
+ rp.indexed_after(10**12, 3) # [(37607912018, 1000000000039, N), ...]
297
+ rp.indexed_after(2**44, 3) # 53 ms with primecount installed
298
+ rp.indexed_after(2**60, 3) # 20 s, all of it primecount counting to 1.15e18
299
+ rp.primepi_fastest(x) # primecount if present, else Meissel
300
+ ```
301
+
302
+ ## Which to use
303
+
304
+ | tool | holds | cost per call | reaches |
305
+ |---|---|---|---|
306
+ | `primepi` (Meissel) | nothing | grows with x, 0.07 s at 10^9 | about 10^12 here |
307
+ | `Marks` | a table built once | flat, the walk length | the end of the table |
308
+ | `LiveMarks` | only blocks you have touched | first touch counts, after that the walk | wherever the counter reaches |
309
+ | [primecount](https://github.com/kimwalisch/primecount) | nothing | 20 ms at 10^12, one thread | 10^20 and beyond |
310
+ | `any_prime_after` | a warm pool | 2.9 s at 8191 bits, 16 cores | any size; a prime, not the next |
311
+ | `next_prime_after_parallel` | a warm pool | 3.3 s at 8191 bits | any size; the next prime |
312
+
313
+ primecount computes; marks remember. Use the calculator for a one-off count of
314
+ any size, the ruler for many lookups in a range you have drawn.
315
+
316
+ ## Also
317
+
318
+ - `rp.isprime(n)`: gmpy2's Miller-Rabin when gmpy2 is present, else a pure
319
+ Python deterministic one exact below 3.3·10^24.
320
+ - `rp.sweep(P, step, prime_test)`: the window sweep. With a residue hint of
321
+ p mod 256 the seed rule finds the next prime with no primality test for the
322
+ first 831,000 primes.
323
+ - `rp.li`, `rp.li_inverse`, `rp.riemann_r`, `rp.riemann_r_inverse`.
324
+ - `rp.key(n)`, `rp.topo(n)`, `rp.build_kx(n)`: the original session arrays.
325
+ - `rp.wheel_pass(a, count=100, seeds=None)`: the seed wheel run over `a, a+1, ...`; each
326
+ offset's first divisor among `[2, *seeds]`, or `None` when it survives. `seeds` defaults
327
+ to `SEEDS`; `a` is any int, so `rp.wheel_report(2**8191 - 1)` prints which of the hundred
328
+ numbers past the Mersenne prime the wheel removes and by what (twelve survive).
329
+
330
+ ## Experiments
331
+
332
+ `rulerpack.experiments` is the drawer for things that were fun in a session and might be
333
+ more. Nothing else imports it. Each function reproduces an IPython moment exactly and says
334
+ what it saw.
335
+
336
+ ```python
337
+ import rulerpack.experiments as rx
338
+ rx.stripe(primes) # (idx ^ 2p) % 4 with idx = arange(2, n, 10): 2, 0, 2, 0, ...
339
+ rx.jumps(primes) # 97, 172, 309, 564 among them: 2p crossing 1024, 2048, 4096, 8192
340
+ rx.last_before_blocks(primes) # 127, 251, 383, 509, ... 1021, 2039, 4093, 8191 among them
341
+ rx.layers(primes) # z % 4, 8, 16, 32: one more bit of the primes per row, the stripe under all of them
342
+ rx.recover(primes, 32) # ((z % 32) ^ (idx % 32)) >> 1 == p % 16: XOR hides nothing
343
+ rx.stripe_control() # odd composites stripe the same way: the stripe is not about primes
344
+ ```
345
+
346
+ ## Install
347
+
348
+ ```
349
+ pip install -e . # numpy only; gmpy2 optional (pip install -e ".[fast]")
350
+ pytest
351
+ rulerpack --check 1000000
352
+ rulerpack 78501 1000039
353
+ rulerpack --ladder 32
354
+ ```
355
+
356
+ MIT license.
357
+
358
+ ## Changes
359
+
360
+ **0.2.1** (2026-11-01)
361
+
362
+ - `rulerpack.experiments`: the drawer, with the step-10 index, its layers, the recovery and the
363
+ odd-composite control. Ported to the Rust crate's `experiments` module in the same release.
364
+
365
+ **0.2.0** (2026-11-01)
366
+
367
+ - `wheel_pass`, `wheel_report`: the seed wheel over any anchor, `seeds` as a parameter with
368
+ `SEEDS` the default; `2**8191 - 1` works as an anchor.
369
+ - `hints_from_x`, `hint_walk`: the primes back in order from seventeen bits each, no test.
370
+ - `R896`, `carry`, `slide512`, `unslide512`, `slide_repeats`: the 512 slide and where it repeats.
371
+
372
+ **0.1.0** — the rulers, the pack, counting, marks, the window, hints, indexed primes.