From b6b30008a612d5a8ce053d50e64e29e547adfbea Mon Sep 17 00:00:00 2001 From: Jonathan Rampersad Date: Sat, 31 Jan 2026 15:31:57 +0000 Subject: [PATCH] v0.7.0 - Complex Number Support (#25) Reviewed-on: https://gitea.jono-rams.work/jono/PolySolve/pulls/25 Co-authored-by: Jonathan Rampersad Co-committed-by: Jonathan Rampersad --- PolySolve_Complex_Technical_Paper.pdf | Bin 0 -> 116523 bytes README.md | 21 +- pyproject.toml | 2 +- src/polysolve/__init__.py | 890 +++++++++++++++++++++----- tests/test_polysolve.py | 38 ++ 5 files changed, 769 insertions(+), 182 deletions(-) create mode 100644 PolySolve_Complex_Technical_Paper.pdf diff --git a/PolySolve_Complex_Technical_Paper.pdf b/PolySolve_Complex_Technical_Paper.pdf new file mode 100644 index 0000000000000000000000000000000000000000..98e7b68be96f9666b8974851ccb46911bc5ac1ac GIT binary patch literal 116523 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≥C9b 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 literal 0 HcmV?d00001 diff --git a/README.md b/README.md index 4f9fb05..909e139 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ A Python library for representing, manipulating, and solving polynomial equation ## Key Features * **Numerically Stable Solver**: Makes complex calculations **practical**. Leverage your GPU to power the robust genetic algorithm, solving high-degree polynomials accurately in a reasonable timeframe. +* **Complex Number Support**: Fully supports complex coefficients and finding roots in the complex plane (e.g., $x^2 + 1 = 0 \to \pm i$). * **Numba Accelerated CPU Solver**: The default genetic algorithm is JIT-compiled with Numba for high-speed CPU performance, right out of the box. * **CUDA Accelerated**: Leverage NVIDIA GPUs for a massive performance boost when finding roots in large solution spaces. * **Create and Manipulate Polynomials**: Easily define polynomials of any degree using integer or float coefficients, and perform arithmetic operations like addition, subtraction, multiplication, and scaling. @@ -75,12 +76,19 @@ roots_analytic = f1.quadratic_solve() print(f"Analytic roots: {sorted(roots_analytic)}") # > Analytic roots: [-1.0, 2.5] -# 6. Find roots with the genetic algorithm (Numba CPU) -#    This is the default, JIT-compiled CPU solver. +# 6. Find REAL roots with the genetic algorithm (Numba CPU) +# This is the default, JIT-compiled CPU solver. ga_opts = GA_Options(num_of_generations=20) roots_ga = f1.get_real_roots(ga_opts, use_cuda=False) -print(f"Approximate roots from GA: {roots_ga[:2]}") -# > Approximate roots from GA: [-1.000..., 2.500...] +print(f"Approximate real roots: {roots_ga[:2]}") +# > Approximate real roots: [-1.000..., 2.500...] + +# 7. Find ALL roots (Real + Complex) +# Use get_roots() to search the complex plane. +f_complex = Function(2, [1, 0, 1]) # x^2 + 1 +roots_all = f_complex.get_roots(ga_opts) +print(f"Approximate complex roots: {roots_all}") +# > Approximate complex roots: [-1.00...j, 1.00...j] # If you installed a CUDA extra, you can run it on the GPU: # roots_ga_gpu = f1.get_real_roots(ga_opts, use_cuda=True) @@ -114,7 +122,10 @@ ga_robust_search = GA_Options( # Increase the crossover blend factor to 0.75. # This allows new solutions to be created further # away from their parents, increasing exploration. - blend_alpha=0.75 + blend_alpha=0.75, + + # Enable complex root finding (default is True) + find_complex=True ) # Pass the custom options to the solver diff --git a/pyproject.toml b/pyproject.toml index f0c515f..11a2088 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] # --- Core Metadata --- name = "polysolve" -version = "0.6.3" +version = "0.7.0" authors = [ { name="Jonathan Rampersad", email="jonathan@jono-rams.work" }, ] diff --git a/src/polysolve/__init__.py b/src/polysolve/__init__.py index c146adb..917a8b5 100644 --- a/src/polysolve/__init__.py +++ b/src/polysolve/__init__.py @@ -1,4 +1,5 @@ import math +import cmath import numpy as np import numba from dataclasses import dataclass @@ -16,10 +17,10 @@ except ImportError: # The CUDA kernels for the fitness function _FITNESS_KERNEL_FLOAT = """ extern "C" __global__ void fitness_kernel( - const double* coefficients, + const double* __restrict__ coefficients, int num_coefficients, - const double* x_vals, - double* ranks, + const double* __restrict__ x_vals, + double* __restrict__ ranks, int size, double y_val) { @@ -27,18 +28,189 @@ extern "C" __global__ void fitness_kernel( if (idx < size) { double ans = coefficients[0]; + double x = x_vals[idx]; + for (int i = 1; i < num_coefficients; ++i) { - ans = ans * x_vals[idx] + coefficients[i]; + ans = ans * x + coefficients[i]; } ans -= y_val; - ranks[idx] = (ans == 0) ? 1.7976931348623157e+308 : fabs(1.0 / ans); + + if (isinf(ans) || isnan(ans)) { + ranks[idx] = 0.0; + } else { + ranks[idx] = 1.0 / (fabs(ans) + 1e-15); + } } } """ -@numba.jit(nopython=True, fastmath=True, parallel=True) +_FITNESS_KERNEL_FLOAT_DYNAMIC = """ +extern "C" __global__ void fitness_kernel_shared( + const double* __restrict__ coefficients, + int num_coefficients, + const double* __restrict__ x_vals, + double* __restrict__ ranks, + int size, + double y_val) +{ + // Dynamic Shared Memory declaration + extern __shared__ double s_coeffs[]; + + for (int i = threadIdx.x; i < num_coefficients; i += blockDim.x) { + s_coeffs[i] = coefficients[i]; + } + + __syncthreads(); + + int idx = threadIdx.x + blockIdx.x * blockDim.x; + if (idx < size) + { + double ans = s_coeffs[0]; + double x = x_vals[idx]; + + for (int i = 1; i < num_coefficients; ++i) + { + ans = ans * x + s_coeffs[i]; + } + + ans -= y_val; + + if (isinf(ans) || isnan(ans)) { + ranks[idx] = 0.0; + } else { + ranks[idx] = 1.0 / (fabs(ans) + 1e-15); + } + } +} +""" + +_FITNESS_KERNEL_COMPLEX = """ +struct Complex { + double r; + double i; +}; + +__device__ Complex c_add(Complex a, Complex b) { + return {a.r + b.r, a.i + b.i}; +} + +__device__ Complex c_mul(Complex a, Complex b) { + return { + a.r * b.r - a.i * b.i, + a.r * b.i + a.i * b.r + }; +} + +__device__ double c_abs(Complex a) { + return sqrt(a.r * a.r + a.i * a.i); +} + +extern "C" __global__ void fitness_kernel_complex( + const double* __restrict__ coeffs_real, + const double* __restrict__ coeffs_imag, + int num_coefficients, + const double* __restrict__ sol_real, + const double* __restrict__ sol_imag, + double* __restrict__ ranks, + int size, + double y_real, + double y_imag) +{ + int idx = threadIdx.x + blockIdx.x * blockDim.x; + if (idx < size) + { + Complex x = {sol_real[idx], sol_imag[idx]}; + Complex ans = {coeffs_real[0], coeffs_imag[0]}; + + for (int i = 1; i < num_coefficients; ++i) + { + Complex c = {coeffs_real[i], coeffs_imag[i]}; + ans = c_mul(ans, x); + ans = c_add(ans, c); + } + + Complex diff = {ans.r - y_real, ans.i - y_imag}; + + if (isinf(diff.r) || isinf(diff.i) || isnan(diff.r) || isnan(diff.i)) { + ranks[idx] = 0.0; + } else { + double modulus = hypot(diff.r, diff.i); + ranks[idx] = 1.0 / (modulus + 1e-15); + } + } +} +""" + +_FITNESS_KERNEL_COMPLEX_DYNAMIC = """ +struct Complex { + double r; + double i; +}; + +__device__ Complex c_add(Complex a, Complex b) { + return {a.r + b.r, a.i + b.i}; +} + +__device__ Complex c_mul(Complex a, Complex b) { + return { + a.r * b.r - a.i * b.i, + a.r * b.i + a.i * b.r + }; +} + +__device__ double c_abs(Complex a) { + return sqrt(a.r * a.r + a.i * a.i); +} + +extern "C" __global__ void fitness_kernel_complex_shared( + const double* __restrict__ coeffs_real, + const double* __restrict__ coeffs_imag, + int num_coefficients, + const double* __restrict__ sol_real, + const double* __restrict__ sol_imag, + double* __restrict__ ranks, + int size, + double y_real, + double y_imag) +{ + // Dynamic Shared Memory declaration + extern __shared__ double s_memory[]; + + for (int i = threadIdx.x; i < num_coefficients; i += blockDim.x) { + s_memory[2 * i] = coeffs_real[i]; + s_memory[2 * i + 1] = coeffs_imag[i]; + } + + __syncthreads(); + + int idx = threadIdx.x + blockIdx.x * blockDim.x; + if (idx < size) + { + Complex x = {sol_real[idx], sol_imag[idx]}; + Complex ans = {s_memory[0], s_memory[1]}; + + for (int i = 1; i < num_coefficients; ++i) + { + Complex c = {s_memory[2 * i], s_memory[2 * i + 1]}; + ans = c_mul(ans, x); + ans = c_add(ans, c); + } + + Complex diff = {ans.r - y_real, ans.i - y_imag}; + + if (isinf(diff.r) || isinf(diff.i) || isnan(diff.r) || isnan(diff.i)) { + ranks[idx] = 0.0; + } else { + double modulus = hypot(diff.r, diff.i); + ranks[idx] = 1.0 / (modulus + 1e-15); + } + } +} +""" + +@numba.jit(nopython=True, fastmath=True, parallel=True, cache=True) def _calculate_ranks_numba(solutions, coefficients, y_val, ranks): """ A Numba-jitted, parallel function to calculate fitness. @@ -58,10 +230,36 @@ def _calculate_ranks_numba(solutions, coefficients, y_val, ranks): ans -= y_val - if ans == 0.0: - ranks[idx] = 1.7976931348623157e+308 # np.finfo(float).max - else: - ranks[idx] = abs(1.0 / ans) + ranks[idx] = 1.0 / (abs(ans) + 1e-15) + + +@numba.jit(nopython=True, fastmath=True, parallel=True, cache=True) +def _calculate_ranks_complex_numba(solutions, coefficients, y_val, ranks): + """ + Parallel fitness calculation for Complex numbers on CPU. + Solutions and Coefficients must be of type complex128. + """ + num_coefficients = coefficients.shape[0] + data_size = solutions.shape[0] + + for idx in numba.prange(data_size): + x_val = solutions[idx] + + # Initialize with the leading coefficient + ans = coefficients[0] + + # Horner's Method + for i in range(1, num_coefficients): + ans = ans * x_val + coefficients[i] + + ans -= y_val + + # Calculate rank based on Modulus (Magnitude) + # abs(z) for a complex number returns sqrt(a^2 + b^2) + modulus = abs(ans) + + ranks[idx] = 1.0 / (modulus + 1e-15) + @dataclass class GA_Options: @@ -103,9 +301,10 @@ class GA_Options: groups roots more aggressively. A larger number (e.g., 7) is more precise but may return multiple near-identical roots. Default: 5 + find_complex (bool): Whether to find complex roots as well. Default: True """ - min_range: float = 0.0 - max_range: float = 0.0 + min_range: float = 0.0 # Returned for backwards compatibility even though it's no longer used + max_range: float = 0.0 # Returned for backwards compatibility even though it's no longer used num_of_generations: int = 10 data_size: int = 100000 mutation_strength: float = 0.01 @@ -115,6 +314,7 @@ class GA_Options: selection_percentile: float = 0.66 blend_alpha: float = 0.5 root_precision: int = 5 + find_complex: bool = True def __post_init__(self): """Validates the GA options after initialization.""" @@ -142,6 +342,13 @@ class GA_Options: UserWarning, stacklevel=2 ) + if self.min_range != 0.0 or self.max_range != 0.0: + warnings.warn( + "The 'min_range' and 'max_range' parameters are deprecated and will be ignored. " + "Search bounds are now automatically calculated using Cauchy's bound.", + DeprecationWarning, + stacklevel=2 + ) def _get_cauchy_bound(coeffs: np.ndarray) -> float: """ @@ -152,6 +359,9 @@ def _get_cauchy_bound(coeffs: np.ndarray) -> float: R = 1 + max(|c_n-1/c_n|, |c_n-2/c_n|, ..., |c_0/c_n|) Where c_n is the leading coefficient (coeffs[0]). """ + if len(coeffs) <= 1: + return 1000.0 + # Normalize all coefficients by the leading coefficient normalized_coeffs = np.abs(coeffs[1:] / coeffs[0]) @@ -165,25 +375,31 @@ class Function: Represents an exponential function (polynomial) of the form: c_0*x^n + c_1*x^(n-1) + ... + c_n """ - def __init__(self, largest_exponent: int): + def __init__(self, largest_exponent: int, coefficients: Optional[List[Union[int, float, complex]]] = None): """ Initializes a function with its highest degree. Args: largest_exponent (int): The largest exponent (n) in the function. """ - if not isinstance(largest_exponent, int) or largest_exponent < 0: - raise ValueError("largest_exponent must be a non-negative integer.") self._largest_exponent = largest_exponent - self.coefficients: Optional[np.ndarray] = None - self._initialized = False + if coefficients is not None: + self.set_coeffs(coefficients) + # Verify user provided exponent matches if they provided both + if largest_exponent is not None and self._largest_exponent != largest_exponent: + raise ValueError("Provided largest_exponent does not match coefficient list length.") + elif largest_exponent is not None: + self.coefficients = None + self._initialized = False + else: + raise ValueError("Must provide either coefficients or largest_exponent.") - def set_coeffs(self, coefficients: List[Union[int, float]]): + def set_coeffs(self, coefficients: List[Union[int, float, complex]]): """ Sets the coefficients of the polynomial. Args: - coefficients (List[Union[int, float]]): A list of integer or float + coefficients (List[Union[int, float]]): A list of integer, float or complex coefficients. The list size must be largest_exponent + 1. @@ -199,13 +415,17 @@ class Function: if coefficients[0] == 0 and self._largest_exponent > 0: raise ValueError("The first constant (for the largest exponent) cannot be 0.") - # Check if any coefficient is a float - is_float = any(isinstance(c, float) for c in coefficients) + # Check for complex, then float, then int + is_complex = any(isinstance(c, complex) for c in coefficients) # Choose the dtype based on the input - target_dtype = np.float64 if is_float else np.int64 + if is_complex: + target_dtype = np.complex128 + else: + target_dtype = np.float64 self.coefficients = np.array(coefficients, dtype=target_dtype) + self._largest_exponent = len(coefficients) - 1 self._initialized = True def _check_initialized(self): @@ -306,7 +526,7 @@ class Function: return function - def get_real_roots(self, options: GA_Options = GA_Options(), use_cuda: bool = False) -> np.ndarray: + def get_real_roots(self, options: Optional[GA_Options] = None, use_cuda: bool = False) -> np.ndarray: """ Uses a genetic algorithm to find the approximate real roots of the function (where y=0). @@ -318,9 +538,32 @@ class Function: np.ndarray: An array of approximate root values. """ self._check_initialized() + if options is None: + options = GA_Options() + import copy + safe_options = copy.copy(options) + safe_options.find_complex = False + return self.solve_x(0.0, safe_options, use_cuda) + + + def get_roots(self, options: Optional[GA_Options] = None, use_cuda: bool = False) -> np.ndarray: + """ + Uses a genetic algorithm to find the approximate roots of the function (where y=0). + + Args: + options (GA_Options): Configuration for the genetic algorithm. + use_cuda (bool): If True, attempts to use CUDA for acceleration. + + Returns: + np.ndarray: An array of approximate root values. + """ + self._check_initialized() + if options is None: + options = GA_Options() return self.solve_x(0.0, options, use_cuda) - def solve_x(self, y_val: float, options: GA_Options = GA_Options(), use_cuda: bool = False) -> np.ndarray: + + def solve_x(self, y_val: Union[float, complex], options: Optional[GA_Options] = None, use_cuda: bool = False) -> np.ndarray: """ Uses a genetic algorithm to find x-values for a given y-value. @@ -333,18 +576,46 @@ class Function: np.ndarray: An array of approximate x-values. """ self._check_initialized() - if use_cuda and _CUPY_AVAILABLE: - return self._solve_x_cuda(y_val, options) + if options is None: + options = GA_Options() + if options.find_complex: + target_y = complex(y_val) + + if use_cuda and _CUPY_AVAILABLE: + return self._solve_complex_cuda(target_y, options) + else: + if use_cuda: + # Warn if user wanted CUDA but it's not available + warnings.warn( + "use_cuda=True was specified, but CuPy is not installed. " + "Falling back to NumPy (CPU) for complex roots.", + UserWarning + ) + return self._solve_complex_numpy(target_y, options) else: - if use_cuda: - warnings.warn( - "use_cuda=True was specified, but CuPy is not installed. " - "Falling back to NumPy (CPU). For GPU acceleration, " - "install with 'pip install polysolve[cuda]'.", - UserWarning - ) - - return self._solve_x_numpy(y_val, options) + if isinstance(y_val, complex): + if y_val.imag != 0: + warnings.warn( + "Complex y_val passed but options.find_complex is False. " + "The imaginary part of y_val will be ignored.", + UserWarning + ) + target_y = float(y_val.real) + else: + target_y = float(y_val) + + if use_cuda and _CUPY_AVAILABLE: + return self._solve_x_cuda(target_y, options) + else: + if use_cuda: + warnings.warn( + "use_cuda=True was specified, but CuPy is not installed. " + "Falling back to NumPy (CPU). For GPU acceleration, " + "install with 'pip install polysolve[cuda]'.", + UserWarning + ) + + return self._solve_x_numpy(target_y, options) def _solve_x_numpy(self, y_val: float, options: GA_Options) -> np.ndarray: """Genetic algorithm implementation using NumPy (CPU).""" @@ -359,30 +630,25 @@ class Function: mutation_size = int(data_size * mutation_ratio) random_size = data_size - elite_size - crossover_size - mutation_size - # Check if the user is using the default, non-expert range - user_range_is_default = (options.min_range == 0.0 and options.max_range == 0.0) + # Pre-calculate indices for slicing the destination array + idx_elite_end = elite_size + idx_cross_end = idx_elite_end + crossover_size + idx_mut_end = idx_cross_end + mutation_size - if user_range_is_default: - # User hasn't specified a custom range. - # We are the expert; use the smart, guaranteed bound. - bound = _get_cauchy_bound(self.coefficients) - min_r = -bound - max_r = bound - else: - # User has provided a custom range. - # Trust the expert; use their range. - min_r = options.min_range - max_r = options.max_range + bound = _get_cauchy_bound(self.coefficients) + min_r = -bound + max_r = bound # Create initial random solutions - solutions = np.random.uniform(min_r, max_r, data_size) + src_solutions = np.random.uniform(min_r, max_r, data_size) + dst_solutions = np.empty(data_size, dtype=np.float64) # Pre-allocate ranks array ranks = np.empty(data_size, dtype=np.float64) for _ in range(options.num_of_generations): # Calculate fitness for all solutions (vectorized) - _calculate_ranks_numba(solutions, self.coefficients, y_val, ranks) + _calculate_ranks_numba(src_solutions, self.coefficients, y_val, ranks) parent_pool_size = int(data_size * options.selection_percentile) @@ -397,15 +663,13 @@ class Function: # --- Create the next generation --- # 1. Elitism: Keep the best solutions as-is - elite_solutions = solutions[elite_indices] + dst_solutions[:elite_size] = src_solutions[elite_indices] # 2. Crossover: Breed two parents to create a child # Select from the fitter PARENT POOL - parents1_idx = np.random.choice(parent_pool_indices, crossover_size) - parents2_idx = np.random.choice(parent_pool_indices, crossover_size) + parents1 = src_solutions[np.random.choice(parent_pool_indices, crossover_size)] + parents2 = src_solutions[np.random.choice(parent_pool_indices, crossover_size)] - parents1 = solutions[parents1_idx] - parents2 = solutions[parents2_idx] # Blend Crossover (BLX-alpha) alpha = options.blend_alpha @@ -421,34 +685,25 @@ class Function: new_max = p_max + (alpha * parent_range) # Create a new random child within the expanded range - crossover_solutions = np.random.uniform(new_min, new_max, crossover_size) + dst_solutions[idx_elite_end:idx_cross_end] = np.random.uniform(new_min, new_max) # 3. Mutation: # Select from the full list (indices 0 to data_size-1) - mutation_candidates = solutions[np.random.randint(0, data_size, mutation_size)] + mutation_candidates = src_solutions[np.random.randint(0, data_size, mutation_size)] # Use mutation_strength - mutation_factors = np.random.uniform( - 1 - options.mutation_strength, - 1 + options.mutation_strength, - mutation_size - ) - mutated_solutions = mutation_candidates * mutation_factors + noise = np.random.normal(0, options.mutation_strength, mutation_size) + dst_solutions[idx_cross_end:idx_mut_end] = mutation_candidates * (1.0 + noise) # 4. New Randoms: Add new blood to prevent getting stuck - random_solutions = np.random.uniform(min_r, max_r, random_size) + dst_solutions[idx_mut_end:] = np.random.uniform(min_r, max_r, random_size) # Assemble the new generation - solutions = np.concatenate([ - elite_solutions, - crossover_solutions, - mutated_solutions, - random_solutions - ]) + src_solutions, dst_solutions = dst_solutions, src_solutions # --- Final Step: Return the best results --- # After all generations, do one last ranking to find the best solutions - _calculate_ranks_numba(solutions, self.coefficients, y_val, ranks) + _calculate_ranks_numba(src_solutions, self.coefficients, y_val, ranks) # 1. Define quality based on the user's desired precision # (e.g., precision=5 -> rank > 1e6, precision=8 -> rank > 1e9) @@ -456,7 +711,7 @@ class Function: quality_threshold = 10**(options.root_precision + 1) # 2. Get all solutions that meet this quality threshold - high_quality_solutions = solutions[ranks > quality_threshold] + high_quality_solutions = src_solutions[ranks > quality_threshold] if high_quality_solutions.size == 0: # No roots found that meet the quality, return empty @@ -469,6 +724,110 @@ class Function: unique_roots = np.unique(rounded_solutions) return np.sort(unique_roots) + + + def _solve_complex_numpy(self, y_val: complex, options: GA_Options) -> np.ndarray: + elite_ratio = options.elite_ratio + crossover_ratio = options.crossover_ratio + mutation_ratio = options.mutation_ratio + + data_size = options.data_size + + elite_size = int(data_size * elite_ratio) + crossover_size = int(data_size * crossover_ratio) + mutation_size = int(data_size * mutation_ratio) + random_size = data_size - elite_size - crossover_size - mutation_size + + # Pre-calculate indices for slicing the destination array + idx_elite_end = elite_size + idx_cross_end = idx_elite_end + crossover_size + idx_mut_end = idx_cross_end + mutation_size + + bound = _get_cauchy_bound(self.coefficients) + min_r = -bound + max_r = bound + + # 3. Initialize Population (Complex128) + real_part = np.random.uniform(min_r, max_r, data_size) + imag_part = np.random.uniform(min_r, max_r, data_size) + src_solutions = real_part + 1j * imag_part + + dst_solutions = np.empty(data_size, dtype=np.complex128) + + # Cast coefficients to complex128 for Numba compatibility + coeffs_complex = self.coefficients.astype(np.complex128) + ranks = np.empty(data_size, dtype=np.float64) + + for _ in range(options.num_of_generations): + # Calculate fitness for all solutions (vectorized) + _calculate_ranks_complex_numba(src_solutions, coeffs_complex, y_val, ranks) + + parent_pool_size = int(data_size * options.selection_percentile) + + # 1. Get indices for the elite solutions (O(N) operation) + # We find the 'elite_size'-th largest element. + elite_indices = np.argpartition(-ranks, elite_size)[:elite_size] + + # 2. Get indices for the parent pool (O(N) operation) + # We find the 'parent_pool_size'-th largest element. + parent_pool_indices = np.argpartition(-ranks, parent_pool_size)[:parent_pool_size] + + # --- Create the next generation --- + + # 1. Elitism: Keep the best solutions as-is + dst_solutions[:elite_size] = src_solutions[elite_indices] + + # 2. Crossover: Breed two parents to create a child + # Select from the fitter PARENT POOL + p1 = src_solutions[np.random.choice(parent_pool_indices, crossover_size)] + p2 = src_solutions[np.random.choice(parent_pool_indices, crossover_size)] + + # Calculate difference vectors + diff_real = p2.real - p1.real + diff_imag = p2.imag - p1.imag + + alpha = options.blend_alpha + + # Generate independant weights for Real and Imaginary parts + # This creates a 2D search area instead of a 1D + u_real = np.random.uniform(-alpha, 1.0 + alpha, crossover_size) + u_imag = np.random.uniform(-alpha, 1.0 + alpha, crossover_size) + + child_real = p1.real + (u_real * diff_real) + child_imag = p1.imag + (u_imag * diff_imag) + + dst_solutions[idx_elite_end:idx_cross_end] = child_real + 1j * child_imag + + # 3. Mutation: + # Select from the full list (indices 0 to data_size-1) + mut_candidates = src_solutions[np.random.randint(0, data_size, mutation_size)] + + noise_real = np.random.normal(0, options.mutation_strength, mutation_size) + noise_imag = np.random.normal(0, options.mutation_strength, mutation_size) + + dst_solutions[idx_cross_end:idx_mut_end] = (mut_candidates.real * (1.0 + noise_real)) + 1j * (mut_candidates.imag * (1.0 + noise_imag)) + + # 4. New Randoms: Add new blood to prevent getting stuck + rand_real = np.random.uniform(min_r, max_r, random_size) + rand_imag = np.random.uniform(min_r, max_r, random_size) + dst_solutions[idx_mut_end:] = rand_real + 1j * rand_imag + + # Assemble the new generation + src_solutions, dst_solutions = dst_solutions, src_solutions + + # 5. Final Ranking & Clustering + _calculate_ranks_complex_numba(src_solutions, coeffs_complex, y_val, ranks) + quality_threshold = 10**(options.root_precision + 1) + high_quality_solutions = src_solutions[ranks > quality_threshold] + + if high_quality_solutions.size == 0: return np.array([]) + + # Rounding complex numbers: round real and imag separately + rounded_real = np.round(high_quality_solutions.real, options.root_precision) + rounded_imag = np.round(high_quality_solutions.imag, options.root_precision) + + return np.unique(rounded_real + 1j * rounded_imag) + def _solve_x_cuda(self, y_val: float, options: GA_Options) -> np.ndarray: """Genetic algorithm implementation using CuPy (GPU/CUDA).""" @@ -484,108 +843,115 @@ class Function: mutation_size = int(data_size * mutation_ratio) random_size = data_size - elite_size - crossover_size - mutation_size - # ALWAYS cast coefficients to float64 for the kernel. - fitness_gpu = cupy.RawKernel(_FITNESS_KERNEL_FLOAT, 'fitness_kernel') - d_coefficients = cupy.array(self.coefficients, dtype=cupy.float64) - - # Check if the user is using the default, non-expert range - user_range_is_default = (options.min_range == 0.0 and options.max_range == 0.0) - - if user_range_is_default: - # User hasn't specified a custom range. - # We are the expert; use the smart, guaranteed bound. - bound = _get_cauchy_bound(self.coefficients) - min_r = -bound - max_r = bound - else: - # User has provided a custom range. - # Trust the expert; use their range. - min_r = options.min_range - max_r = options.max_range + bound = _get_cauchy_bound(self.coefficients) + min_r = -bound + max_r = bound # Create initial random solutions on the GPU - d_solutions = cupy.random.uniform( - min_r, max_r, options.data_size, dtype=cupy.float64 + d_src_solutions = cupy.random.uniform( + min_r, max_r, data_size, dtype=cupy.float64 ) - d_ranks = cupy.empty(options.data_size, dtype=cupy.float64) - # Configure kernel launch parameters + d_dst_solutions = cupy.empty(data_size, dtype=cupy.float64) + + d_ranks = cupy.empty(data_size, dtype=cupy.float64) + + d_coefficients = cupy.array(self.coefficients, dtype=cupy.float64) + + # Calculate Shared Memory Size + num_coeffs = len(self.coefficients) + required_shared_mem_bytes = num_coeffs * 8 + + device = cupy.cuda.Device() + max_shared_mem = device.attributes['MaxSharedMemoryPerBlock'] + + use_shared_mem = True + + if required_shared_mem_bytes > max_shared_mem: + # The polynomial is too big for the cache! + # We must fall back to the slower Global Memory kernel to prevent a crash. + use_shared_mem = False + warnings.warn( + f"Polynomial degree ({num_coeffs}) exceeds GPU Shared Memory limit " + f"({max_shared_mem} bytes). Falling back to Global Memory (slower).", + UserWarning + ) + + # Kernel Setup + if use_shared_mem: + fitness_gpu = cupy.RawKernel(_FITNESS_KERNEL_FLOAT_DYNAMIC, 'fitness_kernel_shared') + kwargs = {'shared_mem': required_shared_mem_bytes} + else: + fitness_gpu = cupy.RawKernel(_FITNESS_KERNEL_FLOAT, 'fitness_kernel') + kwargs = {} + threads_per_block = 512 blocks_per_grid = (options.data_size + threads_per_block - 1) // threads_per_block + # Indices for slicing the destination buffer + idx_elite_end = elite_size + idx_cross_end = idx_elite_end + crossover_size + idx_mut_end = idx_cross_end + mutation_size + for i in range(options.num_of_generations): # Run the fitness kernel on the GPU + fitness_gpu( (blocks_per_grid,), (threads_per_block,), - (d_coefficients, d_coefficients.size, d_solutions, d_ranks, d_solutions.size, y_val) + (d_coefficients, d_coefficients.size, d_src_solutions, d_ranks, d_src_solutions.size, y_val), + **kwargs ) # Sort solutions by rank on the GPU sorted_indices = cupy.argsort(-d_ranks) - d_solutions = d_solutions[sorted_indices] + d_sorted_src_solutions = d_src_solutions[sorted_indices] # --- Create the next generation --- # 1. Elitism - d_elite_solutions = d_solutions[:elite_size] + d_dst_solutions[:elite_size] = d_sorted_src_solutions[:elite_size] # 2. Crossover parent_pool_size = int(data_size * options.selection_percentile) # Select from the fitter PARENT POOL - parent1_indices = cupy.random.randint(0, parent_pool_size, crossover_size) - parent2_indices = cupy.random.randint(0, parent_pool_size, crossover_size) + p1_indices = cupy.random.randint(0, parent_pool_size, crossover_size) + p2_indices = cupy.random.randint(0, parent_pool_size, crossover_size) # Get parents directly from the sorted solutions array using the pool-sized indices - d_parents1 = d_solutions[parent1_indices] - d_parents2 = d_solutions[parent2_indices] + d_p1 = d_sorted_src_solutions[p1_indices] + d_p2 = d_sorted_src_solutions[p2_indices] # Blend Crossover (BLX-alpha) alpha = options.blend_alpha - # Find min/max for all parent pairs - d_p_min = cupy.minimum(d_parents1, d_parents2) - d_p_max = cupy.maximum(d_parents1, d_parents2) + diff = d_p2 - d_p1 + u = cupy.random.uniform(-alpha, 1.0 + alpha, crossover_size) - # Calculate range (I) - d_parent_range = d_p_max - d_p_min - - # Calculate new min/max for the expanded range - d_new_min = d_p_min - (alpha * d_parent_range) - d_new_max = d_p_max + (alpha * d_parent_range) - - # Create a new random child within the expanded range - d_crossover_solutions = cupy.random.uniform(d_new_min, d_new_max, crossover_size) + d_dst_solutions[idx_elite_end:idx_cross_end] = d_p1 + (u * diff) # 3. Mutation # Select from the full list (indices 0 to data_size-1) mutation_indices = cupy.random.randint(0, data_size, mutation_size) - d_mutation_candidates = d_solutions[mutation_indices] + d_mutation_candidates = d_sorted_src_solutions[mutation_indices] - # Use mutation_strength (the new name) - d_mutation_factors = cupy.random.uniform( - 1 - options.mutation_strength, - 1 + options.mutation_strength, - mutation_size - ) - d_mutated_solutions = d_mutation_candidates * d_mutation_factors + # Use mutation_strength + noise = cupy.random.normal(0, options.mutation_strength, mutation_size) + d_dst_solutions[idx_cross_end:idx_mut_end] = d_mutation_candidates * (1.0 + noise) # 4. New Randoms - d_random_solutions = cupy.random.uniform( + d_dst_solutions[idx_mut_end:] = cupy.random.uniform( min_r, max_r, random_size, dtype=cupy.float64 ) # Assemble the new generation - d_solutions = cupy.concatenate([ - d_elite_solutions, - d_crossover_solutions, - d_mutated_solutions, - d_random_solutions - ]) + # d_dst becomes the new source for the next generation + d_src_solutions, d_dst_solutions = d_dst_solutions, d_src_solutions # --- Final Step: Return the best results --- # After all generations, do one last ranking to find the best solutions fitness_gpu( (blocks_per_grid,), (threads_per_block,), - (d_coefficients, d_coefficients.size, d_solutions, d_ranks, d_solutions.size, y_val) + (d_coefficients, d_coefficients.size, d_src_solutions, d_ranks, d_src_solutions.size, y_val), + **kwargs ) # 1. Define quality based on the user's desired precision @@ -594,7 +960,7 @@ class Function: quality_threshold = 10**(options.root_precision + 1) # 2. Get all solutions that meet this quality threshold - d_high_quality_solutions = d_solutions[d_ranks > quality_threshold] + d_high_quality_solutions = d_src_solutions[d_ranks > quality_threshold] if d_high_quality_solutions.size == 0: return np.array([]) @@ -610,6 +976,167 @@ class Function: return final_solutions_gpu.get() + def _solve_complex_cuda(self, y_val: complex, options: GA_Options) -> np.ndarray: + elite_ratio = options.elite_ratio + crossover_ratio = options.crossover_ratio + mutation_ratio = options.mutation_ratio + + data_size = options.data_size + + elite_size = int(data_size * elite_ratio) + crossover_size = int(data_size * crossover_ratio) + mutation_size = int(data_size * mutation_ratio) + random_size = data_size - elite_size - crossover_size - mutation_size + + # 1. Prepare Coefficients (Split into Real/Imag for the Kernel) + # We pass real and imag arrays separately to avoid struct alignment issues + coeffs = self.coefficients.astype(np.complex128) + d_coeffs_real = cupy.array(coeffs.real, dtype=cupy.float64) + d_coeffs_imag = cupy.array(coeffs.imag, dtype=cupy.float64) + + d_y_real = cupy.float64(y_val.real) + d_y_imag = cupy.float64(y_val.imag) + + bound = _get_cauchy_bound(self.coefficients) + min_r = -bound + max_r = bound + + real_part = cupy.random.uniform(min_r, max_r, data_size, dtype=cupy.float64) + imag_part = cupy.random.uniform(min_r, max_r, data_size, dtype=cupy.float64) + d_src_solutions = real_part + 1j * imag_part + + d_dst_solutions = cupy.empty(data_size, dtype=cupy.complex128) + d_ranks = cupy.empty(data_size, dtype=cupy.float64) + + # Calculate Shared Memory Size + num_coeffs = len(self.coefficients) + required_shared_mem_bytes = (num_coeffs * 8) * 2 + + device = cupy.cuda.Device() + max_shared_mem = device.attributes['MaxSharedMemoryPerBlock'] + + use_shared_mem = True + + if required_shared_mem_bytes > max_shared_mem: + # The polynomial is too big for the cache! + # We must fall back to the slower Global Memory kernel to prevent a crash. + use_shared_mem = False + warnings.warn( + f"Polynomial degree ({num_coeffs}) exceeds GPU Shared Memory limit " + f"({max_shared_mem} bytes). Falling back to Global Memory (slower).", + UserWarning + ) + + # Kernel Setup + if use_shared_mem: + fitness_gpu = cupy.RawKernel(_FITNESS_KERNEL_COMPLEX_DYNAMIC, 'fitness_kernel_complex_shared') + kwargs = {'shared_mem': required_shared_mem_bytes} + else: + fitness_gpu = cupy.RawKernel(_FITNESS_KERNEL_COMPLEX, 'fitness_kernel_complex') + kwargs = {} + + threads_per_block = 512 + blocks_per_grid = (options.data_size + threads_per_block - 1) // threads_per_block + + idx_elite_end = elite_size + idx_cross_end = idx_elite_end + crossover_size + idx_mut_end = idx_cross_end + mutation_size + + for _ in range(options.num_of_generations): + d_real_cont = cupy.ascontiguousarray(d_src_solutions.real) + d_imag_cont = cupy.ascontiguousarray(d_src_solutions.imag) + + fitness_gpu( + (blocks_per_grid,), (threads_per_block,), + (d_coeffs_real, d_coeffs_imag, d_coeffs_real.size, + d_real_cont, d_imag_cont, d_ranks, data_size, + d_y_real, d_y_imag), + **kwargs + ) + + # Sort (using d_ranks) + sorted_indices = cupy.argsort(-d_ranks) + d_sorted_src_solutions = d_src_solutions[sorted_indices] + + # 1. Elite: Keep the best + d_dst_solutions[:elite_size] = d_sorted_src_solutions[:elite_size] + + # 2. Crossover: Blend Crossover (BLX-alpha) + # Select parents from the top percentile + parent_pool_size = int(data_size * options.selection_percentile) + + # Randomly pair parents + p1_indices = cupy.random.randint(0, parent_pool_size, crossover_size) + p2_indices = cupy.random.randint(0, parent_pool_size, crossover_size) + + p1 = d_sorted_src_solutions[p1_indices] + p2 = d_sorted_src_solutions[p2_indices] + + # Calculate difference vectors + diff_real = p2.real - p1.real + diff_imag = p2.imag - p1.imag + + alpha = options.blend_alpha + + # Generate independant weights for Real and Imaginary parts + # This creates a 2D search area instead of a 1D + u_real = cupy.random.uniform(-alpha, 1.0 + alpha, crossover_size) + u_imag = cupy.random.uniform(-alpha, 1.0 + alpha, crossover_size) + + child_real = p1.real + (u_real * diff_real) + child_imag = p1.imag + (u_imag * diff_imag) + + # Apply Crossover + d_dst_solutions[idx_elite_end:idx_cross_end] = child_real + 1j * child_imag + + # 3. Mutation: Perturb existing solutions + # Pick random candidates from the full population + mut_indices = cupy.random.randint(0, data_size, mutation_size) + mut_candidates = d_sorted_src_solutions[mut_indices] + + # Generate Independent Scaling Factors for Real and Imaginary parts + # Range: [1 - strength, 1 + strength] + noise_real = cupy.random.normal(0, options.mutation_strength, mutation_size) + noise_imag = cupy.random.normal(0, options.mutation_strength, mutation_size) + + # Apply Mutation: Scale Real/Imag independently to allow "rotation" off the line + d_dst_solutions[idx_cross_end:idx_mut_end] = (mut_candidates.real * (1.0 + noise_real)) + 1j * (mut_candidates.imag * (1.0 + noise_imag)) + + # 4. Random Injection: Fresh genetic material + rand_real = cupy.random.uniform(min_r, max_r, random_size, dtype=cupy.float64) + rand_imag = cupy.random.uniform(min_r, max_r, random_size, dtype=cupy.float64) + d_dst_solutions[idx_mut_end:] = rand_real + 1j * rand_imag + + d_src_solutions, d_dst_solutions = d_dst_solutions, d_src_solutions + + d_real_cont = cupy.ascontiguousarray(d_src_solutions.real) + d_imag_cont = cupy.ascontiguousarray(d_src_solutions.imag) + + # Final Rank + fitness_gpu( + (blocks_per_grid,), (threads_per_block,), + (d_coeffs_real, d_coeffs_imag, d_coeffs_real.size, + d_real_cont, d_imag_cont, d_ranks, data_size, + d_y_real, d_y_imag), + **kwargs + ) + + # Filtering & Return + quality_threshold = 10**(options.root_precision + 1) + d_high_quality_solutions = d_src_solutions[d_ranks > quality_threshold] + + if d_high_quality_solutions.size == 0: return np.array([]) + + rounded_real = cupy.round(d_high_quality_solutions.real, options.root_precision) + rounded_imag = cupy.round(d_high_quality_solutions.imag, options.root_precision) + + d_unique = cupy.unique(rounded_real + 1j * rounded_imag) + + # Sort the unique roots and copy back to CPU + final_solutions_gpu = cupy.sort(d_unique) + return final_solutions_gpu.get() + + def __str__(self) -> str: """Returns a human-readable string representation of the function.""" self._check_initialized() @@ -665,10 +1192,17 @@ class Function: other._check_initialized() new_coefficients = np.polyadd(self.coefficients, other.coefficients) + new_coefficients = self._strip_leading_zeros(new_coefficients) result_func = Function(len(new_coefficients) - 1) result_func.set_coeffs(new_coefficients.tolist()) return result_func + + def _strip_leading_zeros(self, coeffs: np.ndarray) -> np.ndarray: + # Remove leading zeros + while len(coeffs) > 1 and np.isclose(coeffs[0], 0): + coeffs = coeffs[1:] + return coeffs def __sub__(self, other: 'Function') -> 'Function': """Subtracts another Function object from this one.""" @@ -676,12 +1210,13 @@ class Function: other._check_initialized() new_coefficients = np.polysub(self.coefficients, other.coefficients) + new_coefficients = self._strip_leading_zeros(new_coefficients) result_func = Function(len(new_coefficients) - 1) result_func.set_coeffs(new_coefficients.tolist()) return result_func - def _multiply_by_scalar(self, scalar: Union[int, float]) -> 'Function': + def _multiply_by_scalar(self, scalar: Union[int, float, complex]) -> 'Function': """Helper method to multiply the function by a scalar constant.""" self._check_initialized() @@ -691,6 +1226,7 @@ class Function: return result_func new_coefficients = self.coefficients * scalar + new_coefficients = self._strip_leading_zeros(new_coefficients) result_func = Function(self._largest_exponent) result_func.set_coeffs(new_coefficients.tolist()) @@ -703,6 +1239,7 @@ class Function: # np.polymul performs convolution of coefficients to multiply polynomials new_coefficients = np.polymul(self.coefficients, other.coefficients) + new_coefficients = self._strip_leading_zeros(new_coefficients) # The degree of the resulting polynomial is derived from the new coefficients new_degree = len(new_coefficients) - 1 @@ -711,7 +1248,7 @@ class Function: result_func.set_coeffs(new_coefficients.tolist()) return result_func - def __mul__(self, other: Union['Function', int, float]) -> 'Function': + def __mul__(self, other: Union['Function', int, float, complex]) -> 'Function': """Multiplies the function by a scalar constant.""" if isinstance(other, (int, float)): return self._multiply_by_scalar(other) @@ -720,17 +1257,17 @@ class Function: else: return NotImplemented - def __rmul__(self, scalar: Union[int, float]) -> 'Function': + def __rmul__(self, scalar: Union[int, float, complex]) -> 'Function': """Handles scalar multiplication from the right (e.g., 3 * func).""" return self.__mul__(scalar) - def __imul__(self, other: Union['Function', int, float]) -> 'Function': + def __imul__(self, other: Union['Function', int, float, complex]) -> 'Function': """Performs in-place multiplication by a scalar (func *= 3).""" self._check_initialized() - if isinstance(other, (int, float)): + if isinstance(other, (int, float, complex)): if other == 0: self.coefficients = np.array([0], dtype=self.coefficients.dtype) self._largest_exponent = 0 @@ -760,18 +1297,21 @@ class Function: if not self._initialized or not other._initialized: return False - return np.array_equal(self.coefficients, other.coefficients) + c1 = self._strip_leading_zeros(self.coefficients) + c2 = self._strip_leading_zeros(other.coefficients) + + if c1.shape != c2.shape: + return False + + return np.allclose(c1, c2) - def quadratic_solve(self) -> Optional[List[float]]: + def quadratic_solve(self) -> Optional[List[Union[complex, float]]]: """ - Calculates the real roots of a quadratic function using the quadratic formula. - - Args: - f (Function): A Function object of degree 2. + Calculates the roots (real or complex) of a quadratic function. Returns: - Optional[List[float]]: A list containing the two real roots, or None if there are no real roots. + Optional[List[complex]]: A list containing the two roots """ self._check_initialized() if self.largest_exponent != 2: @@ -781,40 +1321,28 @@ class Function: discriminant = (b**2) - (4*a*c) - if discriminant < 0: - return None # No real roots + sqrt_discriminant = cmath.sqrt(discriminant) - sqrt_discriminant = math.sqrt(discriminant) + if b >= 0: + sign_b = 1 + else: + sign_b = -1 - # 1. Calculate the first root. - # We use math.copysign(val, sign) to get the sign of b. - # This ensures (-b - sign*sqrt) is always an *addition* - # (or subtraction of a smaller from a larger number), - # avoiding catastrophic cancellation. - root1 = (-b - math.copysign(sqrt_discriminant, b)) / (2 * a) + root1 = (-b - sign_b * sqrt_discriminant) / (2 * a) - # 2. Calculate the second root using Vieta's formulas. - # We know that root1 * root2 = c / a. - # This is just a division, which is numerically stable. - - # Handle the edge case where c=0. - # If c=0, then root1 is 0.0, and root2 is -b/a - # We can't divide by root1=0, so we check. - if root1 == 0.0: - # If c is also 0, the other root is -b/a - if c == 0.0: - root2 = -b / a - else: - # This case (root1=0 but c!=0) shouldn't happen - # with real numbers, but it's safe to just - # return the one root we found. - return [0.0] + if abs(root1) < 1e-15: + root2 = (-b + sign_b * sqrt_discriminant) / (2 * a) else: # Standard case: Use Vieta's formula root2 = (c / a) / root1 + + roots = np.array([root1, root2]) + roots.sort() - # Return roots in a consistent order - return [root1, root2] + if np.all(np.abs(roots.imag) < 1e-15): + return roots.real.astype(np.float64) + + return roots # Example Usage if __name__ == '__main__': @@ -837,24 +1365,34 @@ if __name__ == '__main__': ddf1 = f1.nth_derivative(2) print(f"Second derivative of f1: {ddf1}") + fc = Function(2, coefficients=[1, 2, 2]) + print(f"\nFunction fc: {f1}") + # --- Root Finding --- # 1. Analytical solution for quadratic roots_analytic = f1.quadratic_solve() - print(f"Analytic roots of f1: {roots_analytic}") # Expected: -1, 2.5 + print(f"\nAnalytic roots of f1: {roots_analytic}") # Expected: -1, 2.5 + c_roots_analytic = fc.quadratic_solve() + print(f"Analytic roots of fc: {c_roots_analytic}") # Expected: -1-j, -1+j # 2. Genetic algorithm solution - ga_opts = GA_Options(num_of_generations=20, data_size=50000) - print("\nFinding roots with Genetic Algorithm (CPU)...") + ga_opts = GA_Options(num_of_generations=100, data_size=100000, root_precision=3, selection_percentile=0.75) + print("\nFinding real roots with Genetic Algorithm (CPU)...") roots_ga_cpu = f1.get_real_roots(ga_opts) - print(f"Approximate roots from GA (CPU): {roots_ga_cpu}") - print("(Note: GA provides approximations around the true roots)") + print(f"Approximate real roots from GA (CPU): {roots_ga_cpu}") + print("\nFinding all roots of fc with Genetic Algorithm (CPU)...") + c_roots_ga_cpu = fc.get_roots(ga_opts) + print(f"Approximate roots of fc from GA (CPU): {c_roots_ga_cpu}") # 3. CUDA accelerated genetic algorithm if _CUPY_AVAILABLE: - print("\nFinding roots with Genetic Algorithm (CUDA)...") + print("\nFinding real roots with Genetic Algorithm (GPU)...") # Since this PC has an RTX 4060 Ti, we can use the CUDA version. roots_ga_gpu = f1.get_real_roots(ga_opts, use_cuda=True) - print(f"Approximate roots from GA (GPU): {roots_ga_gpu}") + print(f"Approximate real roots from GA (GPU): {roots_ga_gpu}") + print("\nFinding all roots of fc with Genetic Algorithm (GPU)...") + c_roots_ga_gpu = fc.get_roots(ga_opts) + print(f"Approximate roots of fc from GA (GPU): {c_roots_ga_gpu}") else: print("\nSkipping CUDA example: CuPy library not found or no compatible GPU.") diff --git a/tests/test_polysolve.py b/tests/test_polysolve.py index dcef499..32803a4 100644 --- a/tests/test_polysolve.py +++ b/tests/test_polysolve.py @@ -43,6 +43,11 @@ def base_func(): f.set_coeffs([1, 2, 3]) return f +@pytest.fixture +def complex_func(): + f = Function(2, [1, 2, 2]) + return f + # --- Core Functionality Tests --- def test_solve_y(quadratic_func): @@ -162,3 +167,36 @@ def test_get_real_roots_cuda(quadratic_func): # Verify that the CUDA implementation also finds the correct roots within tolerance. npt.assert_allclose(np.sort(roots), np.sort(expected_roots), atol=1e-2) +def test_get_roots_numpy(complex_func): + """ + Tests that the NumPy-based genetic algorithm approximates the roots correctly. + """ + # Using more generations for higher accuracy in testing + ga_opts = GA_Options(num_of_generations=50, data_size=200000, selection_percentile=0.66, root_precision=3) + + roots = complex_func.get_roots(ga_opts, use_cuda=False) + + # Check if the algorithm found values close to the two known roots. + # We don't know which order they'll be in, so we check for presence. + expected_roots = np.array([-1.0-1.j, -1.0+1.j]) + + npt.assert_allclose(np.sort(roots), np.sort(expected_roots), atol=1e-2) + + +@pytest.mark.skipif(not _CUPY_AVAILABLE, reason="CuPy is not installed, skipping CUDA test.") +def test_get_roots_cuda(complex_func): + """ + Tests that the CUDA-based genetic algorithm approximates the roots correctly. + This test implicitly verifies that the CUDA kernel is functioning. + It will be skipped automatically if CuPy is not available. + """ + + ga_opts = GA_Options(num_of_generations=50, data_size=200000, selection_percentile=0.66, root_precision=3) + + roots = complex_func.get_roots(ga_opts, use_cuda=True) + + expected_roots = np.array([-1.0-1.j, -1+1.j]) + + # Verify that the CUDA implementation also finds the correct roots within tolerance. + npt.assert_allclose(np.sort(roots), np.sort(expected_roots), atol=1e-2) +