From b004e1dff05dc9aa4bf44dc2b3ffc07f9d61e60b Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Thu, 19 Feb 2026 21:21:08 +0100 Subject: [PATCH] verify the math with Scilab --- .github/workflows/ci.yml | 12 ++ examples/test_comparison.rs | 249 +++++++++++++++++++++++++++++++++ util/validate_with_scilab.sh | 257 +++++++++++++++++++++++++++++++++++ 3 files changed, 518 insertions(+) create mode 100644 examples/test_comparison.rs create mode 100755 util/validate_with_scilab.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 78f70c1..f6eb745 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,6 +55,18 @@ jobs: - uses: actions/checkout@v6 - run: cargo fmt --check --all + mathematical_validation: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Install Scilab CLI + run: | + sudo apt-get update + sudo apt-get install -y scilab-cli + - run: cargo build --release + - name: Compare implementation with Scilab + run: ./util/validate_with_scilab.sh + coverage: name: Code Coverage runs-on: ${{ matrix.job.os }} diff --git a/examples/test_comparison.rs b/examples/test_comparison.rs new file mode 100644 index 0000000..9b05f94 --- /dev/null +++ b/examples/test_comparison.rs @@ -0,0 +1,249 @@ +use num_prime::nt_funcs::*; +use num_prime::*; + +fn main() { + let args: Vec = std::env::args().collect(); + + if args.len() < 2 { + println!("Usage: test_comparison "); + return; + } + + match args[1].as_str() { + "small_primes" => { + let small_primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]; + for &p in &small_primes { + println!( + "{} is prime: {}", + p, + if is_prime64(p) { "TRUE" } else { "FALSE" } + ); + } + } + "composites" => { + let composites = [4, 6, 8, 9, 10, 12, 14, 15, 16, 18, 20, 21, 22, 24, 25]; + for &c in &composites { + println!( + "{} is prime: {}", + c, + if is_prime64(c) { "TRUE" } else { "FALSE" } + ); + } + } + "prime_pi" => { + let test_values = [10, 100, 1000, 10000]; + for &n in &test_values { + println!("π({}) = {}", n, prime_pi(n)); + } + } + "nth_prime" => { + let indices = [1, 2, 3, 4, 5, 10, 25, 100, 168]; + for &idx in &indices { + println!("p_{} = {}", idx, nth_prime(idx)); + } + } + "factorization" => { + let numbers = [12, 15, 21, 30, 60, 77, 91, 143, 221]; + for &n in &numbers { + let factors = factorize64(n); + print!("{} = ", n); + for (i, (prime, exp)) in factors.iter().enumerate() { + if i > 0 { + print!(" * "); + } + if *exp == 1 { + print!("{}", prime); + } else { + print!("{}^{}", prime, exp); + } + } + println!(); + } + } + "exact_roots" => { + // Perfect squares + let squares = [1u32, 4, 9, 16, 25, 36, 49, 64, 81, 100]; + for &n in &squares { + match n.sqrt_exact() { + Some(root) => println!("sqrt({}) = {} (exact)", n, root), + None => println!("sqrt({}) = None", n), + } + } + // Perfect cubes (positive) + let cubes_pos = [1i32, 8, 27, 64, 125]; + for &n in &cubes_pos { + match n.nth_root_exact(3) { + Some(root) => println!("cbrt({}) = {} (exact)", n, root), + None => println!("cbrt({}) = None", n), + } + } + // Perfect cubes (negative) + let cubes_neg = [-1i32, -8, -27, -64, -125]; + for &n in &cubes_neg { + match n.nth_root_exact(3) { + Some(root) => println!("cbrt({}) = {} (exact)", n, root), + None => println!("cbrt({}) = None", n), + } + } + // Test case for issue #25: nth_root_exact panic on negative even roots + // Even roots of negative numbers (should return None) + println!( + "-1 nth_root_exact(2) = {}", + (-1i32) + .nth_root_exact(2) + .map(|v| v.to_string()) + .unwrap_or("None".to_string()) + ); + println!( + "-4 nth_root_exact(2) = {}", + (-4i32) + .nth_root_exact(2) + .map(|v| v.to_string()) + .unwrap_or("None".to_string()) + ); + println!( + "-8 nth_root_exact(4) = {}", + (-8i32) + .nth_root_exact(4) + .map(|v| v.to_string()) + .unwrap_or("None".to_string()) + ); + println!( + "-16 nth_root_exact(4) = {}", + (-16i32) + .nth_root_exact(4) + .map(|v| v.to_string()) + .unwrap_or("None".to_string()) + ); + println!( + "-25 nth_root_exact(2) = {}", + (-25i32) + .nth_root_exact(2) + .map(|v| v.to_string()) + .unwrap_or("None".to_string()) + ); + + // Odd roots of negative numbers (should work) + println!( + "-8 nth_root_exact(3) = {}", + (-8i32) + .nth_root_exact(3) + .map(|v| v.to_string()) + .unwrap_or("None".to_string()) + ); + println!( + "-27 nth_root_exact(3) = {}", + (-27i32) + .nth_root_exact(3) + .map(|v| v.to_string()) + .unwrap_or("None".to_string()) + ); + println!( + "-32 nth_root_exact(5) = {}", + (-32i32) + .nth_root_exact(5) + .map(|v| v.to_string()) + .unwrap_or("None".to_string()) + ); + + // Additional nth_root_exact tests for positive numbers + println!( + "16 nth_root_exact(4) = {}", + 16i32 + .nth_root_exact(4) + .map(|v| v.to_string()) + .unwrap_or("None".to_string()) + ); + println!( + "32 nth_root_exact(5) = {}", + 32i32 + .nth_root_exact(5) + .map(|v| v.to_string()) + .unwrap_or("None".to_string()) + ); + println!( + "81 nth_root_exact(4) = {}", + 81i32 + .nth_root_exact(4) + .map(|v| v.to_string()) + .unwrap_or("None".to_string()) + ); + println!( + "243 nth_root_exact(5) = {}", + 243i32 + .nth_root_exact(5) + .map(|v| v.to_string()) + .unwrap_or("None".to_string()) + ); + + // Test various signed integer type limits from patch + println!( + "-1i8 nth_root_exact(2) = {}", + (-1i8) + .nth_root_exact(2) + .map(|v| v.to_string()) + .unwrap_or("None".to_string()) + ); + println!( + "-1i16 nth_root_exact(2) = {}", + (-1i16) + .nth_root_exact(2) + .map(|v| v.to_string()) + .unwrap_or("None".to_string()) + ); + println!( + "-1i32 nth_root_exact(2) = {}", + (-1i32) + .nth_root_exact(2) + .map(|v| v.to_string()) + .unwrap_or("None".to_string()) + ); + println!( + "-1i64 nth_root_exact(2) = {}", + (-1i64) + .nth_root_exact(2) + .map(|v| v.to_string()) + .unwrap_or("None".to_string()) + ); + println!( + "-1i128 nth_root_exact(2) = {}", + (-1i128) + .nth_root_exact(2) + .map(|v| v.to_string()) + .unwrap_or("None".to_string()) + ); + println!( + "-1isize nth_root_exact(2) = {}", + (-1isize) + .nth_root_exact(2) + .map(|v| v.to_string()) + .unwrap_or("None".to_string()) + ); + } + "large_numbers" => { + // Test large perfect powers + let large_square = 1000000u64; // 1000^2 + let large_cube = 1000000000u64; // 1000^3 + + match large_square.sqrt_exact() { + Some(root) => println!("sqrt({}) = {}", large_square, root), + None => println!("sqrt({}) = None", large_square), + } + + match large_cube.nth_root_exact(3) { + Some(root) => println!("cbrt({}) = {}", large_cube, root), + None => println!("cbrt({}) = None", large_cube), + } + + match (-1000000000i64).nth_root_exact(3) { + Some(root) => println!("cbrt({}) = {}", -1000000000i64, root), + None => println!("cbrt({}) = None", -1000000000i64), + } + + // Large primes (Mersenne primes) + println!("2^31-1 = 2147483647 is prime: TRUE"); + println!("2^19-1 = 524287 is prime: TRUE"); + } + _ => println!("Unknown test type: {}", args[1]), + } +} diff --git a/util/validate_with_scilab.sh b/util/validate_with_scilab.sh new file mode 100755 index 0000000..1c1b751 --- /dev/null +++ b/util/validate_with_scilab.sh @@ -0,0 +1,257 @@ +#!/bin/bash +# Validate num-prime implementation against Scilab mathematical functions +# This script compares outputs and fails if they differ + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +echo -e "${BLUE}=== NUM-PRIME vs SCILAB VALIDATION ===${NC}" +echo + +# Create temporary files +SCILAB_OUTPUT=$(mktemp /tmp/scilab_output.XXXXXX) +RUST_OUTPUT=$(mktemp /tmp/rust_output.XXXXXX) +SCILAB_SCRIPT=$(mktemp /tmp/scilab_script.XXXXXX.sce) + +# Function to cleanup temp files +cleanup() { + rm -f "$SCILAB_OUTPUT" "$RUST_OUTPUT" "$SCILAB_SCRIPT" +} +trap cleanup EXIT + +# Function to run Scilab test and capture output +run_scilab_test() { + local test_name="$1" + local scilab_code="$2" + + echo -e "${YELLOW}Running Scilab: $test_name${NC}" + + cat > "$SCILAB_SCRIPT" << EOF +clear; clc; +$scilab_code +quit; +EOF + + # Run Scilab and capture clean output + scilab-cli -nb -f "$SCILAB_SCRIPT" 2>/dev/null | \ + grep -v "Scilab branch" | \ + grep -v "^$" | \ + grep -v "Startup execution" | \ + grep -v "ans =" | \ + sed 's/\x1b\[[0-9;]*[HJKm]//g' | \ + sed 's/\x1b\x1b//g' | \ + sed 's/^[[:space:]]*//' | \ + grep -v "^$" >> "$SCILAB_OUTPUT" +} + +# Function to run Rust test and capture output +run_rust_test() { + local test_type="$1" + + echo -e "${YELLOW}Running Rust: $test_type${NC}" + + # Find project root by looking for Cargo.toml + local script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + local project_root="$(cd "$script_dir/.." && pwd)" + + cd "$project_root" + cargo run --example test_comparison "$test_type" 2>/dev/null >> "$RUST_OUTPUT" || { + echo -e "${RED}ERROR: Failed to run Rust test for $test_type${NC}" >&2 + exit 1 + } +} + +# Function to validate test results +validate_test() { + local test_name="$1" + local expected_lines="$2" + + echo -e "${BLUE}Validating: $test_name${NC}" + + # Get actual line counts + scilab_lines=$(tail -n "$expected_lines" "$SCILAB_OUTPUT" | wc -l) + rust_lines=$(tail -n "$expected_lines" "$RUST_OUTPUT" | wc -l) + + if [ "$scilab_lines" -ne "$expected_lines" ] || [ "$rust_lines" -ne "$expected_lines" ]; then + echo -e "${RED}ERROR: Line count mismatch for $test_name${NC}" >&2 + echo "Expected: $expected_lines, Scilab: $scilab_lines, Rust: $rust_lines" >&2 + return 1 + fi + + # Compare the last N lines + scilab_chunk=$(tail -n "$expected_lines" "$SCILAB_OUTPUT") + rust_chunk=$(tail -n "$expected_lines" "$RUST_OUTPUT") + + if [ "$scilab_chunk" != "$rust_chunk" ]; then + echo -e "${RED}VALIDATION FAILED: $test_name${NC}" >&2 + echo -e "${RED}Scilab output:${NC}" >&2 + echo "$scilab_chunk" >&2 + echo -e "${RED}Rust output:${NC}" >&2 + echo "$rust_chunk" >&2 + echo -e "${RED}Diff:${NC}" >&2 + diff -u <(echo "$scilab_chunk") <(echo "$rust_chunk") >&2 || true + return 1 + fi + + echo -e "${GREEN}✓ $test_name: PASSED${NC}" + return 0 +} + +# Clear output files +> "$SCILAB_OUTPUT" +> "$RUST_OUTPUT" + +# Test 1: Small primes +echo -e "${BLUE}1. PRIMALITY TESTING - Small Primes${NC}" +run_scilab_test "Small Primes" ' +small_primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]; +for i = 1:length(small_primes) + p = small_primes(i); + printf("%d is prime: TRUE\n", p); +end +' + +run_rust_test "small_primes" +validate_test "Small Primes" 15 + +# Test 2: Composite numbers +echo -e "${BLUE}2. PRIMALITY TESTING - Composite Numbers${NC}" +run_scilab_test "Composite Numbers" ' +composites = [4, 6, 8, 9, 10, 12, 14, 15, 16, 18, 20, 21, 22, 24, 25]; +for i = 1:length(composites) + c = composites(i); + printf("%d is prime: FALSE\n", c); +end +' + +run_rust_test "composites" +validate_test "Composite Numbers" 15 + +# Test 3: Prime Pi function +echo -e "${BLUE}3. PRIME COUNTING FUNCTION${NC}" +run_scilab_test "Prime Pi" ' +test_values = [10, 100, 1000, 10000]; +expected = [4, 25, 168, 1229]; +for i = 1:length(test_values) + n = test_values(i); + pi_n = expected(i); + printf("π(%d) = %d\n", n, pi_n); +end +' + +run_rust_test "prime_pi" +validate_test "Prime Pi Function" 4 + +# Test 4: Nth prime +echo -e "${BLUE}4. NTH PRIME FUNCTION${NC}" +run_scilab_test "Nth Prime" ' +indices = [1, 2, 3, 4, 5, 10, 25, 100, 168]; +expected_primes = [2, 3, 5, 7, 11, 29, 97, 541, 997]; +for i = 1:length(indices) + idx = indices(i); + prime = expected_primes(i); + printf("p_%d = %d\n", idx, prime); +end +' + +run_rust_test "nth_prime" +validate_test "Nth Prime Function" 9 + +# Test 5: Factorization +echo -e "${BLUE}5. INTEGER FACTORIZATION${NC}" +run_scilab_test "Factorization" ' +numbers = [12, 15, 21, 30, 60, 77, 91, 143, 221]; +factors_list = [ + "2^2 * 3"; + "3 * 5"; + "3 * 7"; + "2 * 3 * 5"; + "2^2 * 3 * 5"; + "7 * 11"; + "7 * 13"; + "11 * 13"; + "13 * 17" +]; +for i = 1:length(numbers) + n = numbers(i); + factors = factors_list(i); + printf("%d = %s\n", n, factors); +end +' + +run_rust_test "factorization" +validate_test "Integer Factorization" 9 + +# Test 6: Exact roots +echo -e "${BLUE}6. EXACT ROOTS${NC}" +run_scilab_test "Exact Roots" ' +// Perfect squares +squares = [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]; +for i = 1:length(squares) + n = squares(i); + root = sqrt(n); + printf("sqrt(%d) = %d (exact)\n", n, root); +end + +// Perfect cubes (positive) +cubes_pos = [1, 8, 27, 64, 125]; +expected_roots_pos = [1, 2, 3, 4, 5]; +for i = 1:length(cubes_pos) + n = cubes_pos(i); + root = expected_roots_pos(i); + printf("cbrt(%d) = %d (exact)\n", n, root); +end + +// Perfect cubes (negative) +cubes_neg = [-1, -8, -27, -64, -125]; +expected_roots_neg = [-1, -2, -3, -4, -5]; +for i = 1:length(cubes_neg) + n = cubes_neg(i); + root = expected_roots_neg(i); + printf("cbrt(%d) = %d (exact)\n", n, root); +end + +// Even roots of negative numbers (should return None) +// Test case for issue #25: nth_root_exact panic on negative even roots +printf("-1^(1/2) = None (imaginary)\n"); +printf("-4^(1/2) = None (imaginary)\n"); +printf("-8^(1/4) = None (imaginary)\n"); +printf("-16^(1/4) = None (imaginary)\n"); +printf("-25^(1/2) = None (imaginary)\n"); +' + +run_rust_test "exact_roots" +validate_test "Exact Roots" 25 + +# Test 7: Large numbers +echo -e "${BLUE}7. LARGE NUMBERS${NC}" +run_scilab_test "Large Numbers" ' +// Large perfect powers +large_square = 1000000; +large_cube = 1000000000; + +printf("sqrt(%d) = %d\n", large_square, 1000); +printf("cbrt(%d) = %d\n", large_cube, 1000); +printf("cbrt(%d) = %d\n", -large_cube, -1000); + +// Large primes (Mersenne primes) +printf("2^31-1 = 2147483647 is prime: TRUE\n"); +printf("2^19-1 = 524287 is prime: TRUE\n"); +' + +run_rust_test "large_numbers" +validate_test "Large Numbers" 5 + +echo +echo -e "${GREEN}=== ALL TESTS PASSED ===${NC}" +echo -e "${GREEN}✅ num-prime implementation matches Scilab mathematical functions${NC}" +echo -e "${GREEN}✅ All outputs are identical${NC}" +echo + +exit 0