diff --git a/projects/campaigns/factorial-25k-digits.toml b/projects/campaigns/factorial-25k-digits.toml new file mode 100644 index 0000000..d66cd2e --- /dev/null +++ b/projects/campaigns/factorial-25k-digits.toml @@ -0,0 +1,30 @@ +[project] +name = "factorial-25k-digit-primes" +description = "Search for factorial primes n!±1 with 25K+ digits" +objective = "record" +form = "factorial" +tags = ["large-prime", "25k-digits", "factorial"] + +[target] +target_digits = 25000 +range_start = 7000 +range_end = 12000 + +[strategy] +auto_strategy = false + +[[strategy.phases]] +name = "factorial-sweep" +description = "n=7000..12000 (n! gives ~23K-40K digits)" +search_params = { search_type = "factorial", start = 7000, end = 12000 } +block_size = 100 +completion = "all_blocks_done" + +[infrastructure] +min_ram_gb = 16 +min_cores = 8 +preferred_tools = ["pfgw"] + +[budget] +max_cost_usd = 100.0 +cloud_rate_usd_per_core_hour = 0.04 diff --git a/projects/campaigns/kbn-25k-digits.toml b/projects/campaigns/kbn-25k-digits.toml new file mode 100644 index 0000000..d169954 --- /dev/null +++ b/projects/campaigns/kbn-25k-digits.toml @@ -0,0 +1,49 @@ +[project] +name = "kbn-25k-digit-primes" +description = "Search for 25K+ digit primes of the form k*2^n+/-1" +objective = "record" +form = "kbn" +tags = ["large-prime", "25k-digits", "proth", "riesel"] + +[target] +target_digits = 25000 +range_start = 83000 +range_end = 100000 + +[strategy] +auto_strategy = false + +[[strategy.phases]] +name = "k3-base2-sweep" +description = "k=3, base=2, n=83000..100000 (25K-30K digits)" +search_params = { search_type = "kbn", k = 3, base = 2, min_n = 83000, max_n = 100000 } +block_size = 500 +completion = "all_blocks_done" + +[[strategy.phases]] +name = "k5-base2-sweep" +description = "k=5, base=2, n=83000..100000" +search_params = { search_type = "kbn", k = 5, base = 2, min_n = 83000, max_n = 100000 } +block_size = 500 +completion = "all_blocks_done" + +[[strategy.phases]] +name = "k7-base2-sweep" +description = "k=7, base=2, n=83000..100000" +search_params = { search_type = "kbn", k = 7, base = 2, min_n = 83000, max_n = 100000 } +block_size = 500 +completion = "all_blocks_done" + +[infrastructure] +min_ram_gb = 16 +min_cores = 8 +preferred_tools = ["prst", "pfgw"] + +[budget] +max_cost_usd = 200.0 +cost_alert_threshold_usd = 100.0 +cloud_rate_usd_per_core_hour = 0.04 + +[workers] +min_workers = 4 +max_workers = 4 diff --git a/projects/campaigns/palindromic-25k-digits.toml b/projects/campaigns/palindromic-25k-digits.toml new file mode 100644 index 0000000..3ccb430 --- /dev/null +++ b/projects/campaigns/palindromic-25k-digits.toml @@ -0,0 +1,28 @@ +[project] +name = "palindromic-25k-digit-primes" +description = "Search for palindromic primes with 25K+ digits" +objective = "record" +form = "palindromic" +tags = ["large-prime", "25k-digits", "palindromic"] + +[target] +target_digits = 25000 + +[strategy] +auto_strategy = false + +[[strategy.phases]] +name = "palindromic-sweep" +description = "25001-digit base-10 palindromes (odd digit count)" +search_params = { search_type = "palindromic", base = 10, min_digits = 25001, max_digits = 25001 } +block_size = 2 +completion = "all_blocks_done" + +[infrastructure] +min_ram_gb = 32 +min_cores = 8 +preferred_tools = ["pfgw"] + +[budget] +max_cost_usd = 500.0 +cloud_rate_usd_per_core_hour = 0.04 diff --git a/src/ecm.rs b/src/ecm.rs index 54cd81c..d8a6a41 100644 --- a/src/ecm.rs +++ b/src/ecm.rs @@ -206,7 +206,11 @@ fn random_curve_and_point(n: &Integer, seed: u64) -> Option<(EdwardsCurve, Edwar // -x² + y² = 1 + d·x²·y² => d = (-x² + y² - 1)/(x²·y²) let numerator = { let val = (Integer::from(&y2) - Integer::from(&x2) - 1u32) % n; - if val < 0 { val + n } else { val } + if val < 0 { + val + n + } else { + val + } }; let denominator = Integer::from(&x2 * &y2) % n; @@ -243,12 +247,7 @@ fn random_curve_and_point(n: &Integer, seed: u64) -> Option<(EdwardsCurve, Edwar /// multiplies the point by q^e. On Edwards curves, the identity is /// (0:1:0:1), so when the point reaches the identity mod a prime factor p, /// X ≡ 0 mod p (not Z ≡ 0). We check gcd(X, n) to find factors. -fn ecm_stage1( - point: &EdwardsPoint, - curve_d: &Integer, - n: &Integer, - b1: u64, -) -> Option { +fn ecm_stage1(point: &EdwardsPoint, curve_d: &Integer, n: &Integer, b1: u64) -> Option { let primes = crate::sieve::generate_primes(b1); let mut current = point.clone(); @@ -350,13 +349,7 @@ fn ecm_stage2( fn ecm_one_curve(n: &Integer, b1: u64, b2: u64, curve_seed: u64) -> Option { let (curve, point) = random_curve_and_point(n, curve_seed)?; - // Stage 1 - if let Some(factor) = ecm_stage1(&point, &curve.d, n, b1) { - return Some(factor); - } - - // Recompute the point after Stage 1 for Stage 2 - // (We need to re-run Stage 1 to get the accumulated point) + // Stage 1: compute accumulated point P * lcm(1..B1) and check for factors let primes = crate::sieve::generate_primes(b1); let mut accumulated = point.clone(); for &q in &primes { @@ -367,6 +360,23 @@ fn ecm_one_curve(n: &Integer, b1: u64, b2: u64, curve_seed: u64) -> Option 1u32 && &g < n { + return Some(g); + } + } + // Also check T coordinate + let t_mod = Integer::from(&accumulated.t % n); + if t_mod != 0u32 { + let g = t_mod.gcd(n); + if g > 1u32 && &g < n { + return Some(g); + } + } + // Stage 2 ecm_stage2(&accumulated, &curve.d, n, b1, b2) } @@ -391,8 +401,10 @@ pub fn adaptive_ecm_filter(n: &Integer, num_curves: u32) -> bool { (50_000u64, 5_000_000u64, num_curves.min(5)) } else if bits < 80_000 { (200_000u64, 20_000_000u64, num_curves.min(10)) - } else { + } else if bits < 120_000 { (1_000_000u64, 100_000_000u64, num_curves.min(20)) + } else { + (3_000_000u64, 300_000_000u64, num_curves.min(30)) // 36K+ digits }; for seed in 0..curves { @@ -436,7 +448,10 @@ mod tests { } } } - assert!(found, "ECM should find a factor of 41*10007 within 500 curves"); + assert!( + found, + "ECM should find a factor of 41*10007 within 500 curves" + ); } /// ECM returns no factor for primes. @@ -446,10 +461,7 @@ mod tests { for seed in 1..10u64 { if let Some((curve, point)) = random_curve_and_point(&p, seed) { let result = ecm_stage1(&point, &curve.d, &p, 100); - assert!( - result.is_none(), - "ECM should not find factors of primes" - ); + assert!(result.is_none(), "ECM should not find factors of primes"); } } } @@ -458,7 +470,10 @@ mod tests { #[test] fn ecm_adaptive_skips_small() { let n = Integer::from(41u32 * 10007); - assert!(!adaptive_ecm_filter(&n, 10), "ECM should skip small candidates"); + assert!( + !adaptive_ecm_filter(&n, 10), + "ECM should skip small candidates" + ); } /// Edwards point identity check. diff --git a/src/factorial.rs b/src/factorial.rs index 9344d90..ad6410f 100644 --- a/src/factorial.rs +++ b/src/factorial.rs @@ -243,6 +243,10 @@ pub fn search( if crate::p1::adaptive_p1_filter(&plus) { return (IsPrime::No, None); } + // ECM pre-filter — catches composites with smooth curve order + if crate::ecm::adaptive_ecm_filter(&plus, 20) { + return (IsPrime::No, None); + } // Try PFGW acceleration for large candidates if let Some(pfgw_result) = pfgw::try_test(&format!("{}!+1", n), &plus, pfgw::PfgwMode::NMinus1Proof) @@ -274,6 +278,10 @@ pub fn search( if crate::p1::adaptive_p1_filter(&minus) { return (IsPrime::No, None); } + // ECM pre-filter — catches composites with smooth curve order + if crate::ecm::adaptive_ecm_filter(&minus, 20) { + return (IsPrime::No, None); + } // Try PFGW acceleration for large candidates if let Some(pfgw_result) = pfgw::try_test(&format!("{}!-1", n), &minus, pfgw::PfgwMode::NPlus1Proof) diff --git a/src/kbn.rs b/src/kbn.rs index c291ae0..69919b1 100644 --- a/src/kbn.rs +++ b/src/kbn.rs @@ -550,6 +550,12 @@ pub(crate) fn test_prime( return (IsPrime::No, "", None); } + // ECM composite pre-filter for large candidates — catches ~1-3% more composites + // where the curve order is smooth even when p-1 is not (complementary to P-1). + if crate::ecm::adaptive_ecm_filter(candidate, 20) { + return (IsPrime::No, "", None); + } + // Try GWNUM direct FFI for large candidates (when --features gwnum is enabled) #[cfg(feature = "gwnum")] { diff --git a/src/lib.rs b/src/lib.rs index d707cde..b539cda 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -41,6 +41,7 @@ pub mod agent; pub mod ai_engine; +pub mod batch_gcd; pub mod carol_kynea; pub mod certificate; pub mod checkpoint; @@ -50,6 +51,7 @@ pub mod cullen_woodall; pub mod dashboard; pub mod db; pub mod deploy; +pub mod ecm; pub mod events; pub mod factorial; pub mod fleet; @@ -213,8 +215,15 @@ fn poly_pow_mod(exp: &Integer, coeff_b: &Integer, coeff_c: &Integer, n: &Integer result = poly_sqr(&result, coeff_b, coeff_c, n); if exp.get_bit(i) { // Multiply by x: [r0, r1] * [0, 1] = [-r1*c, r0 + r1*b] - let new_r0 = (Integer::from(n) - Integer::from(&result[1] * coeff_c) % n) % n; + // Compute -r1*c mod n = n - (r1*c mod n), with care for the zero case + let r1c_mod_n = Integer::from(&result[1] * coeff_c) % n; + let new_r0 = if r1c_mod_n == 0u32 { + Integer::from(0u32) + } else { + Integer::from(n - &r1c_mod_n) + }; let new_r1 = (Integer::from(&result[0]) + Integer::from(&result[1] * coeff_b)) % n; + let new_r1 = if new_r1 < 0 { new_r1 + n } else { new_r1 }; result = [new_r0, new_r1]; } } diff --git a/src/p1.rs b/src/p1.rs index 2fb687f..ea77764 100644 --- a/src/p1.rs +++ b/src/p1.rs @@ -183,8 +183,10 @@ pub fn adaptive_p1_filter(n: &Integer) -> bool { (100_000u64, 10_000_000u64) } else if bits < 50_000 { (500_000u64, 50_000_000u64) + } else if bits < 100_000 { + (2_000_000u64, 200_000_000u64) // 25K+ digits (~83K bits) } else { - (1_000_000u64, 100_000_000u64) + (5_000_000u64, 500_000_000u64) // 30K+ digits (~100K bits) }; p1_factor(n, b1, Some(b2)).is_some() diff --git a/src/palindromic.rs b/src/palindromic.rs index 2df3dd8..9d0b1ad 100644 --- a/src/palindromic.rs +++ b/src/palindromic.rs @@ -329,7 +329,22 @@ pub fn search( digit_count, base, lead_digit ); - // Only candidates surviving the digit filter need primality testing. + // Batch GCD pre-filter: eliminate candidates sharing a factor with the + // primorial (product of small primes) via Bernstein product/remainder trees. + // O(n log²n) vs O(n·π(L)) for sequential trial division. + let batch: Vec = if batch.len() >= 100 { + let composites = crate::batch_gcd::batch_has_small_factor(&batch, sieve_limit); + batch + .into_iter() + .zip(composites.iter()) + .filter(|(_, &is_composite)| !is_composite) + .map(|(c, _)| c) + .collect() + } else { + batch + }; + + // Only candidates surviving the digit + batch GCD filters need primality testing. // Try PFGW first for large candidates (50-100x faster), fall back to GMP MR. let found_primes: Vec<_> = batch .into_par_iter() @@ -360,6 +375,11 @@ pub fn search( return None; } + // ECM pre-filter — catches composites with smooth curve order + if crate::ecm::adaptive_ecm_filter(&num, 20) { + return None; + } + // GMP Miller-Rabin fallback — defer to_string_radix until prime is found let r = mr_screened_test(&num, mr_rounds); if r != IsPrime::No { diff --git a/src/primorial.rs b/src/primorial.rs index a51e375..3194409 100644 --- a/src/primorial.rs +++ b/src/primorial.rs @@ -264,6 +264,10 @@ pub fn search( if crate::p1::adaptive_p1_filter(&plus) { return (IsPrime::No, None); } + // ECM pre-filter — catches composites with smooth curve order + if crate::ecm::adaptive_ecm_filter(&plus, 20) { + return (IsPrime::No, None); + } if let Some(pfgw_result) = pfgw::try_test(&format!("{}#+1", p), &plus, pfgw::PfgwMode::NMinus1Proof) { @@ -294,6 +298,10 @@ pub fn search( if crate::p1::adaptive_p1_filter(&minus) { return (IsPrime::No, None); } + // ECM pre-filter — catches composites with smooth curve order + if crate::ecm::adaptive_ecm_filter(&minus, 20) { + return (IsPrime::No, None); + } if let Some(pfgw_result) = pfgw::try_test(&format!("{}#-1", p), &minus, pfgw::PfgwMode::NPlus1Proof) { diff --git a/src/sieve.rs b/src/sieve.rs index a5721a1..f31ecea 100644 --- a/src/sieve.rs +++ b/src/sieve.rs @@ -77,8 +77,14 @@ pub fn auto_sieve_depth(candidate_bits: u64, n_range: u64) -> u64 { // Optimal depth ≈ test_cost^(2/3), with scaling let raw_depth = test_cost.powf(2.0 / 3.0) * 10.0; - // Clamp to reasonable range - let depth = (raw_depth as u64).clamp(1_000_000, 1_000_000_000); // 1M minimum (meaningful sieve), 1B max (memory/time limit) + // Clamp to reasonable range — raise cap for very large candidates (25K+ digits / 83K+ bits) + // where each eliminated composite saves 1-40s with PRST/PFGW + let max_depth = if candidate_bits > 50_000 { + 10_000_000_000u64 // 10B for 25K+ digit candidates + } else { + 1_000_000_000u64 // 1B standard cap + }; + let depth = (raw_depth as u64).clamp(1_000_000, max_depth); // For very small ranges, deep sieving isn't worth it // (BSGS cost is per-prime, amortized over the range) @@ -137,8 +143,8 @@ pub fn calibrated_sieve_depth(test_secs: f64, n_range: u64) -> u64 { let depth = budget.powf(2.0 / 3.0) as u64; - // Clamp to [1M, 1B] - depth.clamp(1_000_000, 1_000_000_000) + // Clamp to [1M, 10B] — higher cap for expensive candidates via calibration + depth.clamp(1_000_000, 10_000_000_000) } /// Generate all primes up to `limit` using a wheel-30 sieve. @@ -1113,13 +1119,13 @@ mod tests { assert_eq!(calibrated_sieve_depth(1.0, 0), SIEVE_LIMIT); } - /// Very expensive candidates (1000s each) should not exceed 1B cap. + /// Very expensive candidates (1000s each) should not exceed 10B cap. #[test] - fn calibrated_sieve_depth_capped_at_1b() { + fn calibrated_sieve_depth_capped_at_10b() { let depth = calibrated_sieve_depth(1000.0, 1_000_000); assert!( - depth <= 1_000_000_000, - "should be capped at 1B, got {}", + depth <= 10_000_000_000, + "should be capped at 10B, got {}", depth ); } diff --git a/tests/db_integration.rs b/tests/db_integration.rs index 352d0af..446186d 100644 --- a/tests/db_integration.rs +++ b/tests/db_integration.rs @@ -1851,9 +1851,9 @@ async fn operator_node_register_and_heartbeat() { let entry = leaderboard.iter().find(|e| e.username == "noderunner"); assert!(entry.is_some(), "Operator should appear on leaderboard"); assert_eq!( - entry.unwrap().worker_count, + entry.unwrap().node_count, Some(1), - "Should have 1 worker (upsert, not duplicate)" + "Should have 1 node (upsert, not duplicate)" ); }