From 9e8dfec265344e8eb4975fd6a8836f9ccf4b1ba0 Mon Sep 17 00:00:00 2001 From: Chip Hogg Date: Thu, 14 Nov 2024 20:54:31 -0500 Subject: [PATCH 1/4] Add the Strong Lucas Probable Prime test This is more involved than the Miller-Rabin test, but we can tame the complexity by breaking it down into helper functions, performing tasks such as: - Increment the index of the (U_k, V_k) sequence elements by one. - Double the index of the (U_k, V_k) sequence elements. - Find an appropriate D parameter. etc. With these helpers, the algorithm becomes straightforward (see, for instance, https://en.wikipedia.org/wiki/Lucas_pseudoprime#Strong_Lucas_pseudoprimes). We start by ruling out perfect squares (because if we don't, then the search for `D` will never terminate). Then we find our `D`, and decompose `n + 1` into `s` and `d` parameters (exactly as we did for Miller-Rabin, except there we used `n - 1`). At this point, the strong test is easy: check whether `U_d` is 0, then check `V_d`, as well as `V` for all successive doublings of the index less than `s`. A similar testing strategy as for the Miller Rabin gives us sufficient confidence. 1. Test that we get small primes right. 2. Test that we get known pseudoprimes "correctly wrong". 3. Test some really big primes. (Remember, a probable prime test must mark every actual prime as "probably prime".) Helps #509. --- src/core/include/mp-units/ext/prime.h | 118 ++++++++++++++++++++++++++ test/static/prime_test.cpp | 32 +++++++ 2 files changed, 150 insertions(+) diff --git a/src/core/include/mp-units/ext/prime.h b/src/core/include/mp-units/ext/prime.h index 7d6f7c5b2..4adc88e8e 100644 --- a/src/core/include/mp-units/ext/prime.h +++ b/src/core/include/mp-units/ext/prime.h @@ -260,6 +260,124 @@ struct NumberDecomposition { } } +// The "D" parameter in the Strong Lucas Probable Prime test. +// +// Default construction produces the first value to try, according to Selfridge's parameter selection. +// Calling `successor()` on this repeatedly will produce the sequence of values to try. +struct LucasDParameter { + uint64_t mag = 5u; + bool pos = true; + + friend constexpr int as_int(LucasDParameter p) + { + int D = static_cast(p.mag); + return p.pos ? D : -D; + } + friend constexpr LucasDParameter successor(LucasDParameter p) { return {.mag = p.mag + 2u, .pos = !p.pos}; } +}; + +// The first `D` in the infinite sequence {5, -7, 9, -11, ...} whose Jacobi symbol is -1. This is the D we +// want to use for the Strong Lucas Probable Prime test. +// +// Precondition: `n` is not a perfect square. +[[nodiscard]] consteval LucasDParameter find_first_D_with_jacobi_symbol_neg_one(uint64_t n) +{ + LucasDParameter D{}; + while (jacobi_symbol(as_int(D), n) != -1) { + D = successor(D); + } + return D; +} + +// An element of the Lucas sequence, with parameters tuned for the Strong Lucas test. +// +// The default values give the first element (i.e., k=1) of the sequence. +struct LucasSequenceElement { + uint64_t u = 1u; + uint64_t v = 1u; +}; + +// Produce the Lucas element whose index is twice the input element's index. +[[nodiscard]] consteval LucasSequenceElement double_strong_lucas_index(const LucasSequenceElement& element, uint64_t n, + LucasDParameter D) +{ + const auto& [u, v] = element; + + uint64_t v_squared = mul_mod(v, v, n); + uint64_t D_u_squared = mul_mod(D.mag, mul_mod(u, u, n), n); + uint64_t new_v = D.pos ? add_mod(v_squared, D_u_squared, n) : sub_mod(v_squared, D_u_squared, n); + new_v = half_mod_odd(new_v, n); + + return {.u = mul_mod(u, v, n), .v = new_v}; +} + +[[nodiscard]] consteval LucasSequenceElement increment_strong_lucas_index(const LucasSequenceElement& element, + uint64_t n, LucasDParameter D) +{ + const auto& [u, v] = element; + + const auto new_u = half_mod_odd(add_mod(u, v, n), n); + + const auto D_u = mul_mod(D.mag, u, n); + auto new_v = D.pos ? add_mod(v, D_u, n) : sub_mod(v, D_u, n); + new_v = half_mod_odd(new_v, n); + + return {.u = new_u, .v = new_v}; +} + +[[nodiscard]] consteval LucasSequenceElement find_strong_lucas_element(uint64_t i, uint64_t n, LucasDParameter D) +{ + LucasSequenceElement element{}; + + bool bits[64] = {}; + std::size_t n_bits = 0u; + while (i > 1u) { + bits[n_bits++] = (i & 1u); + i >>= 1u; + } + + for (auto j = n_bits; j > 0u; --j) { + element = double_strong_lucas_index(element, n, D); + if (bits[j - 1u]) { + element = increment_strong_lucas_index(element, n, D); + } + } + + return element; +} + +// Perform a Strong Lucas Probable Prime test on `n`. +// +// Precondition: (n >= 2). +// Precondition: (n is odd). +[[nodiscard]] consteval bool strong_lucas_probable_prime(uint64_t n) +{ + MP_UNITS_EXPECTS_DEBUG(n >= 2u); + MP_UNITS_EXPECTS_DEBUG(n % 2u == 1u); + + if (is_perfect_square(n)) { + return false; + } + + const auto D = find_first_D_with_jacobi_symbol_neg_one(n); + + const auto [s, d] = decompose(n + 1u); + + auto element = find_strong_lucas_element(d, n, D); + if (element.u == 0u) { + return true; + } + + for (auto i = 0u; i < s; ++i) { + if (element.v == 0u) { + return true; + } + element = double_strong_lucas_index(element, n, D); + } + + return false; +} + [[nodiscard]] consteval bool is_prime_by_trial_division(std::uintmax_t n) { for (std::uintmax_t f = 2; f * f <= n; f += 1 + (f % 2)) { diff --git a/test/static/prime_test.cpp b/test/static/prime_test.cpp index 5b279527b..5378e2117 100644 --- a/test/static/prime_test.cpp +++ b/test/static/prime_test.cpp @@ -165,4 +165,36 @@ static_assert(!is_perfect_square(BIG_SQUARE - 1u)); static_assert(is_perfect_square(BIG_SQUARE)); static_assert(!is_perfect_square(BIG_SQUARE + 1u)); +// Tests for the Strong Lucas Probable Prime test. +static_assert(as_int(LucasDParameter{.mag = 5, .pos = true}) == 5); +static_assert(as_int(LucasDParameter{.mag = 7, .pos = false}) == -7); + +static_assert(as_int(LucasDParameter{}) == 5, "First D parameter in the sequence is 5"); +static_assert(as_int(successor(LucasDParameter{})) == -7, "Incrementing adds 2 to the mag, and flips the sign"); +static_assert(as_int(successor(successor(LucasDParameter{}))) == 9); +static_assert(as_int(successor(successor(successor(LucasDParameter{})))) == -11); + +static_assert(strong_lucas_probable_prime(3u), "Known small prime"); +static_assert(strong_lucas_probable_prime(5u), "Known small prime"); +static_assert(strong_lucas_probable_prime(7u), "Known small prime"); +static_assert(!strong_lucas_probable_prime(9u), "Known small composite"); + +// Test some Miller-Rabin pseudoprimes (https://oeis.org/A001262), which should NOT be marked prime. +static_assert(!strong_lucas_probable_prime(2047u), "Miller-Rabin pseudoprime"); +static_assert(!strong_lucas_probable_prime(3277u), "Miller-Rabin pseudoprime"); +static_assert(!strong_lucas_probable_prime(486737u), "Miller-Rabin pseudoprime"); + +// Test some Strong Lucas pseudoprimes (https://oeis.org/A217255). +static_assert(strong_lucas_probable_prime(5459u), "Strong Lucas pseudoprime"); +static_assert(strong_lucas_probable_prime(5777u), "Strong Lucas pseudoprime"); +static_assert(strong_lucas_probable_prime(10877u), "Strong Lucas pseudoprime"); +static_assert(strong_lucas_probable_prime(324899u), "Strong Lucas pseudoprime"); + +// Test some actual primes +static_assert(strong_lucas_probable_prime(225'653'407'801u), "Large known prime"); +static_assert(strong_lucas_probable_prime(334'524'384'739u), "Large known prime"); +static_assert(strong_lucas_probable_prime(9'007'199'254'740'881u), "Large known prime"); + +static_assert(strong_lucas_probable_prime(18'446'744'073'709'551'557u), "Largest 64-bit prime"); + } // namespace From 346a0012832868533632530e13c82c06b6a7b04b Mon Sep 17 00:00:00 2001 From: Chip Hogg Date: Fri, 15 Nov 2024 10:56:53 -0500 Subject: [PATCH 2/4] Add more missing `std::` prefixes --- src/core/include/mp-units/ext/prime.h | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/core/include/mp-units/ext/prime.h b/src/core/include/mp-units/ext/prime.h index 4adc88e8e..56b83a44a 100644 --- a/src/core/include/mp-units/ext/prime.h +++ b/src/core/include/mp-units/ext/prime.h @@ -265,7 +265,7 @@ struct NumberDecomposition { // Default construction produces the first value to try, according to Selfridge's parameter selection. // Calling `successor()` on this repeatedly will produce the sequence of values to try. struct LucasDParameter { - uint64_t mag = 5u; + std::uint64_t mag = 5u; bool pos = true; friend constexpr int as_int(LucasDParameter p) @@ -280,7 +280,7 @@ struct LucasDParameter { // want to use for the Strong Lucas Probable Prime test. // // Precondition: `n` is not a perfect square. -[[nodiscard]] consteval LucasDParameter find_first_D_with_jacobi_symbol_neg_one(uint64_t n) +[[nodiscard]] consteval LucasDParameter find_first_D_with_jacobi_symbol_neg_one(std::uint64_t n) { LucasDParameter D{}; while (jacobi_symbol(as_int(D), n) != -1) { @@ -293,26 +293,26 @@ struct LucasDParameter { // // The default values give the first element (i.e., k=1) of the sequence. struct LucasSequenceElement { - uint64_t u = 1u; - uint64_t v = 1u; + std::uint64_t u = 1u; + std::uint64_t v = 1u; }; // Produce the Lucas element whose index is twice the input element's index. -[[nodiscard]] consteval LucasSequenceElement double_strong_lucas_index(const LucasSequenceElement& element, uint64_t n, - LucasDParameter D) +[[nodiscard]] consteval LucasSequenceElement double_strong_lucas_index(const LucasSequenceElement& element, + std::uint64_t n, LucasDParameter D) { const auto& [u, v] = element; - uint64_t v_squared = mul_mod(v, v, n); - uint64_t D_u_squared = mul_mod(D.mag, mul_mod(u, u, n), n); - uint64_t new_v = D.pos ? add_mod(v_squared, D_u_squared, n) : sub_mod(v_squared, D_u_squared, n); + std::uint64_t v_squared = mul_mod(v, v, n); + std::uint64_t D_u_squared = mul_mod(D.mag, mul_mod(u, u, n), n); + std::uint64_t new_v = D.pos ? add_mod(v_squared, D_u_squared, n) : sub_mod(v_squared, D_u_squared, n); new_v = half_mod_odd(new_v, n); return {.u = mul_mod(u, v, n), .v = new_v}; } [[nodiscard]] consteval LucasSequenceElement increment_strong_lucas_index(const LucasSequenceElement& element, - uint64_t n, LucasDParameter D) + std::uint64_t n, LucasDParameter D) { const auto& [u, v] = element; @@ -325,7 +325,8 @@ struct LucasSequenceElement { return {.u = new_u, .v = new_v}; } -[[nodiscard]] consteval LucasSequenceElement find_strong_lucas_element(uint64_t i, uint64_t n, LucasDParameter D) +[[nodiscard]] consteval LucasSequenceElement find_strong_lucas_element(std::uint64_t i, std::uint64_t n, + LucasDParameter D) { LucasSequenceElement element{}; @@ -350,7 +351,7 @@ struct LucasSequenceElement { // // Precondition: (n >= 2). // Precondition: (n is odd). -[[nodiscard]] consteval bool strong_lucas_probable_prime(uint64_t n) +[[nodiscard]] consteval bool strong_lucas_probable_prime(std::uint64_t n) { MP_UNITS_EXPECTS_DEBUG(n >= 2u); MP_UNITS_EXPECTS_DEBUG(n % 2u == 1u); From d963ce8f936aa18ecc2a10795c86cc25c92d9f49 Mon Sep 17 00:00:00 2001 From: Chip Hogg Date: Fri, 15 Nov 2024 11:58:49 -0500 Subject: [PATCH 3/4] Apply mod to D Looks like we were violating a precondition. --- src/core/include/mp-units/ext/prime.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/include/mp-units/ext/prime.h b/src/core/include/mp-units/ext/prime.h index 56b83a44a..918d19679 100644 --- a/src/core/include/mp-units/ext/prime.h +++ b/src/core/include/mp-units/ext/prime.h @@ -304,7 +304,7 @@ struct LucasSequenceElement { const auto& [u, v] = element; std::uint64_t v_squared = mul_mod(v, v, n); - std::uint64_t D_u_squared = mul_mod(D.mag, mul_mod(u, u, n), n); + std::uint64_t D_u_squared = mul_mod(D.mag % n, mul_mod(u, u, n), n); std::uint64_t new_v = D.pos ? add_mod(v_squared, D_u_squared, n) : sub_mod(v_squared, D_u_squared, n); new_v = half_mod_odd(new_v, n); @@ -318,7 +318,7 @@ struct LucasSequenceElement { const auto new_u = half_mod_odd(add_mod(u, v, n), n); - const auto D_u = mul_mod(D.mag, u, n); + const auto D_u = mul_mod(D.mag % n, u, n); auto new_v = D.pos ? add_mod(v, D_u, n) : sub_mod(v, D_u, n); new_v = half_mod_odd(new_v, n); From 82040c1788236e72674f54a8c74acdde71fd605c Mon Sep 17 00:00:00 2001 From: Chip Hogg Date: Fri, 15 Nov 2024 12:58:22 -0500 Subject: [PATCH 4/4] Handle edge case Subtracting from `n` doesn't work in literally every case: it fails for `0` input. --- src/core/include/mp-units/ext/prime.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/include/mp-units/ext/prime.h b/src/core/include/mp-units/ext/prime.h index 918d19679..3c3dc22da 100644 --- a/src/core/include/mp-units/ext/prime.h +++ b/src/core/include/mp-units/ext/prime.h @@ -100,7 +100,7 @@ namespace mp_units::detail { return add_mod( // Transform into "negative space" to make the first parameter as small as possible; // then, transform back. - n - mul_mod(n % a, num_batches, n), + (n - mul_mod(n % a, num_batches, n)) % n, // Handle the leftover product (which is guaranteed to fit in the integer type). (a * (b % batch_size)) % n,