polysolve 0.5.0__tar.gz → 0.6.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polysolve
3
- Version: 0.5.0
3
+ Version: 0.6.0
4
4
  Summary: A Python library for representing, manipulating, and solving exponential functions using analytical methods and genetic algorithms, with optional CUDA acceleration.
5
5
  Author-email: Jonathan Rampersad <jonathan@jono-rams.work>
6
6
  License: MIT License
@@ -43,6 +43,7 @@ Requires-Python: >=3.8
43
43
  Description-Content-Type: text/markdown
44
44
  License-File: LICENSE
45
45
  Requires-Dist: numpy>=1.21
46
+ Requires-Dist: numba
46
47
  Provides-Extra: cuda12
47
48
  Requires-Dist: cupy-cuda12x; extra == "cuda12"
48
49
  Provides-Extra: dev
@@ -56,15 +57,16 @@ Dynamic: license-file
56
57
  [![PyPI version](https://img.shields.io/pypi/v/polysolve.svg)](https://pypi.org/project/polysolve/)
57
58
  [![PyPI pyversions](https://img.shields.io/pypi/pyversions/polysolve.svg)](https://pypi.org/project/polysolve/)
58
59
 
59
- A Python library for representing, manipulating, and solving polynomial equations using a high-performance genetic algorithm, with optional CUDA/GPU acceleration.
60
+ A Python library for representing, manipulating, and solving polynomial equations. Features a high-performance, Numba-accelerated genetic algorithm for CPU, with an optional CUDA/GPU backend for massive-scale parallel solving.
60
61
 
61
62
  ---
62
63
 
63
64
  ## Key Features
64
65
 
65
- * **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.
66
- * **Genetic Algorithm Solver**: Find approximate real roots for complex polynomials where analytical solutions are difficult or impossible.
66
+ * **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.
67
+ * **Numba Accelerated CPU Solver**: The default genetic algorithm is JIT-compiled with Numba for high-speed CPU performance, right out of the box.
67
68
  * **CUDA Accelerated**: Leverage NVIDIA GPUs for a massive performance boost when finding roots in large solution spaces.
69
+ * **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.
68
70
  * **Analytical Solvers**: Includes standard, exact solvers for simple cases (e.g., `quadratic_solve`).
69
71
  * **Simple API**: Designed to be intuitive and easy to integrate into any project.
70
72
 
@@ -125,8 +127,8 @@ roots_analytic = f1.quadratic_solve()
125
127
  print(f"Analytic roots: {sorted(roots_analytic)}")
126
128
  # > Analytic roots: [-1.0, 2.5]
127
129
 
128
- # 6. Find roots with the genetic algorithm (CPU)
129
- # This can solve polynomials of any degree.
130
+ # 6. Find roots with the genetic algorithm (Numba CPU)
131
+ #    This is the default, JIT-compiled CPU solver.
130
132
  ga_opts = GA_Options(num_of_generations=20)
131
133
  roots_ga = f1.get_real_roots(ga_opts, use_cuda=False)
132
134
  print(f"Approximate roots from GA: {roots_ga[:2]}")
@@ -149,13 +151,22 @@ The default options are balanced, but for very complex polynomials, you may want
149
151
  ```python
150
152
  from polysolve import GA_Options
151
153
 
152
- # Create a config for a much deeper, more accurate search
153
- # (slower, but better for high-degree, complex functions)
154
- ga_accurate = GA_Options(
154
+ # Create a config for a deep search, optimized for finding
155
+ # *all* real roots (even if they are far apart).
156
+ ga_robust_search = GA_Options(
155
157
  num_of_generations=50, # Run for more generations
156
158
  data_size=500000, # Use a larger population
157
- elite_ratio=0.1, # Keep the top 10%
158
- mutation_ratio=0.5 # Mutate 50%
159
+
160
+ # --- Key Tuning Parameters for Multi-Root Finding ---
161
+
162
+ # Widen the parent pool to 75% to keep more "niches"
163
+ # (solution-clouds around different roots) alive.
164
+ selection_percentile=0.75,
165
+
166
+ # Increase the crossover blend factor to 0.75.
167
+ # This allows new solutions to be created further
168
+ # away from their parents, increasing exploration.
169
+ blend_alpha=0.75
159
170
  )
160
171
 
161
172
  # Pass the custom options to the solver
@@ -5,15 +5,16 @@
5
5
  [![PyPI version](https://img.shields.io/pypi/v/polysolve.svg)](https://pypi.org/project/polysolve/)
6
6
  [![PyPI pyversions](https://img.shields.io/pypi/pyversions/polysolve.svg)](https://pypi.org/project/polysolve/)
7
7
 
8
- A Python library for representing, manipulating, and solving polynomial equations using a high-performance genetic algorithm, with optional CUDA/GPU acceleration.
8
+ A Python library for representing, manipulating, and solving polynomial equations. Features a high-performance, Numba-accelerated genetic algorithm for CPU, with an optional CUDA/GPU backend for massive-scale parallel solving.
9
9
 
10
10
  ---
11
11
 
12
12
  ## Key Features
13
13
 
14
- * **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.
15
- * **Genetic Algorithm Solver**: Find approximate real roots for complex polynomials where analytical solutions are difficult or impossible.
14
+ * **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.
15
+ * **Numba Accelerated CPU Solver**: The default genetic algorithm is JIT-compiled with Numba for high-speed CPU performance, right out of the box.
16
16
  * **CUDA Accelerated**: Leverage NVIDIA GPUs for a massive performance boost when finding roots in large solution spaces.
17
+ * **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.
17
18
  * **Analytical Solvers**: Includes standard, exact solvers for simple cases (e.g., `quadratic_solve`).
18
19
  * **Simple API**: Designed to be intuitive and easy to integrate into any project.
19
20
 
@@ -74,8 +75,8 @@ roots_analytic = f1.quadratic_solve()
74
75
  print(f"Analytic roots: {sorted(roots_analytic)}")
75
76
  # > Analytic roots: [-1.0, 2.5]
76
77
 
77
- # 6. Find roots with the genetic algorithm (CPU)
78
- # This can solve polynomials of any degree.
78
+ # 6. Find roots with the genetic algorithm (Numba CPU)
79
+ #    This is the default, JIT-compiled CPU solver.
79
80
  ga_opts = GA_Options(num_of_generations=20)
80
81
  roots_ga = f1.get_real_roots(ga_opts, use_cuda=False)
81
82
  print(f"Approximate roots from GA: {roots_ga[:2]}")
@@ -98,13 +99,22 @@ The default options are balanced, but for very complex polynomials, you may want
98
99
  ```python
99
100
  from polysolve import GA_Options
100
101
 
101
- # Create a config for a much deeper, more accurate search
102
- # (slower, but better for high-degree, complex functions)
103
- ga_accurate = GA_Options(
102
+ # Create a config for a deep search, optimized for finding
103
+ # *all* real roots (even if they are far apart).
104
+ ga_robust_search = GA_Options(
104
105
  num_of_generations=50, # Run for more generations
105
106
  data_size=500000, # Use a larger population
106
- elite_ratio=0.1, # Keep the top 10%
107
- mutation_ratio=0.5 # Mutate 50%
107
+
108
+ # --- Key Tuning Parameters for Multi-Root Finding ---
109
+
110
+ # Widen the parent pool to 75% to keep more "niches"
111
+ # (solution-clouds around different roots) alive.
112
+ selection_percentile=0.75,
113
+
114
+ # Increase the crossover blend factor to 0.75.
115
+ # This allows new solutions to be created further
116
+ # away from their parents, increasing exploration.
117
+ blend_alpha=0.75
108
118
  )
109
119
 
110
120
  # Pass the custom options to the solver
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  # --- Core Metadata ---
7
7
  name = "polysolve"
8
- version = "0.5.0"
8
+ version = "0.6.0"
9
9
  authors = [
10
10
  { name="Jonathan Rampersad", email="jonathan@jono-rams.work" },
11
11
  ]
@@ -33,7 +33,8 @@ classifiers = [
33
33
 
34
34
  # --- Dependencies ---
35
35
  dependencies = [
36
- "numpy>=1.21"
36
+ "numpy>=1.21",
37
+ "numba"
37
38
  ]
38
39
 
39
40
  # --- Optional Dependencies (Extras) ---
@@ -1,5 +1,6 @@
1
1
  import math
2
2
  import numpy as np
3
+ import numba
3
4
  from dataclasses import dataclass
4
5
  from typing import List, Optional, Union
5
6
  import warnings
@@ -37,6 +38,31 @@ extern "C" __global__ void fitness_kernel(
37
38
  }
38
39
  """
39
40
 
41
+ @numba.jit(nopython=True, fastmath=True, parallel=True)
42
+ def _calculate_ranks_numba(solutions, coefficients, y_val, ranks):
43
+ """
44
+ A Numba-jitted, parallel function to calculate fitness.
45
+ This replaces np.polyval and the rank calculation.
46
+ """
47
+ num_coefficients = coefficients.shape[0]
48
+ data_size = solutions.shape[0]
49
+
50
+ # This prange will be run in parallel on all your CPU cores
51
+ for idx in numba.prange(data_size):
52
+ x_val = solutions[idx]
53
+
54
+ # Horner's method (same as np.polyval)
55
+ ans = coefficients[0]
56
+ for i in range(1, num_coefficients):
57
+ ans = ans * x_val + coefficients[i]
58
+
59
+ ans -= y_val
60
+
61
+ if ans == 0.0:
62
+ ranks[idx] = 1.7976931348623157e+308 # np.finfo(float).max
63
+ else:
64
+ ranks[idx] = abs(1.0 / ans)
65
+
40
66
  @dataclass
41
67
  class GA_Options:
42
68
  """
@@ -62,6 +88,16 @@ class GA_Options:
62
88
  mutation_ratio (float): The percentage (e.g., 0.40 for 40%) of the next
63
89
  generation to be created by mutating solutions
64
90
  from the parent pool. Default: 0.40
91
+ selection_percentile (float): The top percentage (e.g., 0.66 for 66%)
92
+ of solutions to use as the parent pool
93
+ for crossover. A smaller value speeds
94
+ up single-root convergence; a larger
95
+ value helps find multiple roots.
96
+ Default: 0.66
97
+ blend_alpha (float): The expansion factor for Blend Crossover (BLX-alpha).
98
+ 0.0 = average crossover (no expansion).
99
+ 0.5 = 50% expansion beyond the parent range.
100
+ Default: 0.5
65
101
  root_precision (int): The number of decimal places to round roots to
66
102
  when clustering. A smaller number (e.g., 3)
67
103
  groups roots more aggressively. A larger number
@@ -76,6 +112,8 @@ class GA_Options:
76
112
  elite_ratio: float = 0.05
77
113
  crossover_ratio: float = 0.45
78
114
  mutation_ratio: float = 0.40
115
+ selection_percentile: float = 0.66
116
+ blend_alpha: float = 0.5
79
117
  root_precision: int = 5
80
118
 
81
119
  def __post_init__(self):
@@ -87,6 +125,14 @@ class GA_Options:
87
125
  )
88
126
  if any(r < 0 for r in [self.elite_ratio, self.crossover_ratio, self.mutation_ratio]):
89
127
  raise ValueError("GA ratios cannot be negative.")
128
+ if not (0 < self.selection_percentile <= 1.0):
129
+ raise ValueError(
130
+ f"selection_percentile must be between 0 (exclusive) and 1.0 (inclusive), but got {self.selection_percentile}"
131
+ )
132
+ if self.blend_alpha < 0:
133
+ raise ValueError(
134
+ f"blend_alpha cannot be negative, but got {self.blend_alpha}"
135
+ )
90
136
 
91
137
  def _get_cauchy_bound(coeffs: np.ndarray) -> float:
92
138
  """
@@ -320,40 +366,57 @@ class Function:
320
366
  # Create initial random solutions
321
367
  solutions = np.random.uniform(min_r, max_r, data_size)
322
368
 
369
+ # Pre-allocate ranks array
370
+ ranks = np.empty(data_size, dtype=np.float64)
371
+
323
372
  for _ in range(options.num_of_generations):
324
373
  # Calculate fitness for all solutions (vectorized)
325
- y_calculated = np.polyval(self.coefficients, solutions)
326
- error = y_calculated - y_val
327
-
328
- with np.errstate(divide='ignore'):
329
- ranks = np.where(error == 0, np.finfo(float).max, np.abs(1.0 / error))
374
+ _calculate_ranks_numba(solutions, self.coefficients, y_val, ranks)
375
+
376
+ parent_pool_size = int(data_size * options.selection_percentile)
377
+
378
+ # 1. Get indices for the elite solutions (O(N) operation)
379
+ # We find the 'elite_size'-th largest element.
380
+ elite_indices = np.argpartition(-ranks, elite_size)[:elite_size]
330
381
 
331
- # Sort solutions by fitness (descending)
332
- sorted_indices = np.argsort(-ranks)
333
- solutions = solutions[sorted_indices]
382
+ # 2. Get indices for the parent pool (O(N) operation)
383
+ # We find the 'parent_pool_size'-th largest element.
384
+ parent_pool_indices = np.argpartition(-ranks, parent_pool_size)[:parent_pool_size]
334
385
 
335
386
  # --- Create the next generation ---
336
387
 
337
388
  # 1. Elitism: Keep the best solutions as-is
338
- elite_solutions = solutions[:elite_size]
339
-
340
- # Define a "parent pool" of the top 50% of solutions to breed from
341
- parent_pool = solutions[:data_size // 2]
389
+ elite_solutions = solutions[elite_indices]
342
390
 
343
391
  # 2. Crossover: Breed two parents to create a child
344
- # Select from the full list (indices 0 to data_size-1)
345
- parent1_indices = np.random.randint(0, data_size, crossover_size)
346
- parent2_indices = np.random.randint(0, data_size, crossover_size)
347
- parents1 = solutions[parent1_indices]
348
- parents2 = solutions[parent2_indices]
349
- # Simple "average" crossover
350
- crossover_solutions = (parents1 + parents2) / 2.0
392
+ # Select from the fitter PARENT POOL
393
+ parents1_idx = np.random.choice(parent_pool_indices, crossover_size)
394
+ parents2_idx = np.random.choice(parent_pool_indices, crossover_size)
395
+
396
+ parents1 = solutions[parents1_idx]
397
+ parents2 = solutions[parents2_idx]
398
+ # Blend Crossover (BLX-alpha)
399
+ alpha = options.blend_alpha
400
+
401
+ # Find min/max for all parent pairs
402
+ p_min = np.minimum(parents1, parents2)
403
+ p_max = np.maximum(parents1, parents2)
404
+
405
+ # Calculate range (I)
406
+ parent_range = p_max - p_min
407
+
408
+ # Calculate new min/max for the expanded range
409
+ new_min = p_min - (alpha * parent_range)
410
+ new_max = p_max + (alpha * parent_range)
411
+
412
+ # Create a new random child within the expanded range
413
+ crossover_solutions = np.random.uniform(new_min, new_max, crossover_size)
351
414
 
352
415
  # 3. Mutation:
353
416
  # Select from the full list (indices 0 to data_size-1)
354
417
  mutation_candidates = solutions[np.random.randint(0, data_size, mutation_size)]
355
418
 
356
- # Use mutation_strength (the new name)
419
+ # Use mutation_strength
357
420
  mutation_factors = np.random.uniform(
358
421
  1 - options.mutation_strength,
359
422
  1 + options.mutation_strength,
@@ -374,10 +437,7 @@ class Function:
374
437
 
375
438
  # --- Final Step: Return the best results ---
376
439
  # After all generations, do one last ranking to find the best solutions
377
- y_calculated = np.polyval(self.coefficients, solutions)
378
- error = y_calculated - y_val
379
- with np.errstate(divide='ignore'):
380
- ranks = np.where(error == 0, np.finfo(float).max, np.abs(1.0 / error))
440
+ _calculate_ranks_numba(solutions, self.coefficients, y_val, ranks)
381
441
 
382
442
  # 1. Define quality based on the user's desired precision
383
443
  # (e.g., precision=5 -> rank > 1e6, precision=8 -> rank > 1e9)
@@ -458,17 +518,31 @@ class Function:
458
518
  # 1. Elitism
459
519
  d_elite_solutions = d_solutions[:elite_size]
460
520
 
461
- # Define a "parent pool" of the top 50% of solutions to breed from
462
- d_parent_pool = d_solutions[:data_size // 2]
463
- parent_pool_size = d_parent_pool.size
464
-
465
521
  # 2. Crossover
466
- # Select from the full list (indices 0 to data_size-1)
467
- parent1_indices = cupy.random.randint(0, data_size, crossover_size)
468
- parent2_indices = cupy.random.randint(0, data_size, crossover_size)
522
+ parent_pool_size = int(data_size * options.selection_percentile)
523
+ # Select from the fitter PARENT POOL
524
+ parent1_indices = cupy.random.randint(0, parent_pool_size, crossover_size)
525
+ parent2_indices = cupy.random.randint(0, parent_pool_size, crossover_size)
526
+ # Get parents directly from the sorted solutions array using the pool-sized indices
469
527
  d_parents1 = d_solutions[parent1_indices]
470
528
  d_parents2 = d_solutions[parent2_indices]
471
- d_crossover_solutions = (d_parents1 + d_parents2) / 2.0
529
+
530
+ # Blend Crossover (BLX-alpha)
531
+ alpha = options.blend_alpha
532
+
533
+ # Find min/max for all parent pairs
534
+ d_p_min = cupy.minimum(d_parents1, d_parents2)
535
+ d_p_max = cupy.maximum(d_parents1, d_parents2)
536
+
537
+ # Calculate range (I)
538
+ d_parent_range = d_p_max - d_p_min
539
+
540
+ # Calculate new min/max for the expanded range
541
+ d_new_min = d_p_min - (alpha * d_parent_range)
542
+ d_new_max = d_p_max + (alpha * d_parent_range)
543
+
544
+ # Create a new random child within the expanded range
545
+ d_crossover_solutions = cupy.random.uniform(d_new_min, d_new_max, crossover_size)
472
546
 
473
547
  # 3. Mutation
474
548
  # Select from the full list (indices 0 to data_size-1)
@@ -713,7 +787,7 @@ if __name__ == '__main__':
713
787
 
714
788
  # --- Root Finding ---
715
789
  # 1. Analytical solution for quadratic
716
- roots_analytic = quadratic_solve(f1)
790
+ roots_analytic = f1.quadratic_solve()
717
791
  print(f"Analytic roots of f1: {roots_analytic}") # Expected: -1, 2.5
718
792
 
719
793
  # 2. Genetic algorithm solution
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polysolve
3
- Version: 0.5.0
3
+ Version: 0.6.0
4
4
  Summary: A Python library for representing, manipulating, and solving exponential functions using analytical methods and genetic algorithms, with optional CUDA acceleration.
5
5
  Author-email: Jonathan Rampersad <jonathan@jono-rams.work>
6
6
  License: MIT License
@@ -43,6 +43,7 @@ Requires-Python: >=3.8
43
43
  Description-Content-Type: text/markdown
44
44
  License-File: LICENSE
45
45
  Requires-Dist: numpy>=1.21
46
+ Requires-Dist: numba
46
47
  Provides-Extra: cuda12
47
48
  Requires-Dist: cupy-cuda12x; extra == "cuda12"
48
49
  Provides-Extra: dev
@@ -56,15 +57,16 @@ Dynamic: license-file
56
57
  [![PyPI version](https://img.shields.io/pypi/v/polysolve.svg)](https://pypi.org/project/polysolve/)
57
58
  [![PyPI pyversions](https://img.shields.io/pypi/pyversions/polysolve.svg)](https://pypi.org/project/polysolve/)
58
59
 
59
- A Python library for representing, manipulating, and solving polynomial equations using a high-performance genetic algorithm, with optional CUDA/GPU acceleration.
60
+ A Python library for representing, manipulating, and solving polynomial equations. Features a high-performance, Numba-accelerated genetic algorithm for CPU, with an optional CUDA/GPU backend for massive-scale parallel solving.
60
61
 
61
62
  ---
62
63
 
63
64
  ## Key Features
64
65
 
65
- * **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.
66
- * **Genetic Algorithm Solver**: Find approximate real roots for complex polynomials where analytical solutions are difficult or impossible.
66
+ * **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.
67
+ * **Numba Accelerated CPU Solver**: The default genetic algorithm is JIT-compiled with Numba for high-speed CPU performance, right out of the box.
67
68
  * **CUDA Accelerated**: Leverage NVIDIA GPUs for a massive performance boost when finding roots in large solution spaces.
69
+ * **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.
68
70
  * **Analytical Solvers**: Includes standard, exact solvers for simple cases (e.g., `quadratic_solve`).
69
71
  * **Simple API**: Designed to be intuitive and easy to integrate into any project.
70
72
 
@@ -125,8 +127,8 @@ roots_analytic = f1.quadratic_solve()
125
127
  print(f"Analytic roots: {sorted(roots_analytic)}")
126
128
  # > Analytic roots: [-1.0, 2.5]
127
129
 
128
- # 6. Find roots with the genetic algorithm (CPU)
129
- # This can solve polynomials of any degree.
130
+ # 6. Find roots with the genetic algorithm (Numba CPU)
131
+ #    This is the default, JIT-compiled CPU solver.
130
132
  ga_opts = GA_Options(num_of_generations=20)
131
133
  roots_ga = f1.get_real_roots(ga_opts, use_cuda=False)
132
134
  print(f"Approximate roots from GA: {roots_ga[:2]}")
@@ -149,13 +151,22 @@ The default options are balanced, but for very complex polynomials, you may want
149
151
  ```python
150
152
  from polysolve import GA_Options
151
153
 
152
- # Create a config for a much deeper, more accurate search
153
- # (slower, but better for high-degree, complex functions)
154
- ga_accurate = GA_Options(
154
+ # Create a config for a deep search, optimized for finding
155
+ # *all* real roots (even if they are far apart).
156
+ ga_robust_search = GA_Options(
155
157
  num_of_generations=50, # Run for more generations
156
158
  data_size=500000, # Use a larger population
157
- elite_ratio=0.1, # Keep the top 10%
158
- mutation_ratio=0.5 # Mutate 50%
159
+
160
+ # --- Key Tuning Parameters for Multi-Root Finding ---
161
+
162
+ # Widen the parent pool to 75% to keep more "niches"
163
+ # (solution-clouds around different roots) alive.
164
+ selection_percentile=0.75,
165
+
166
+ # Increase the crossover blend factor to 0.75.
167
+ # This allows new solutions to be created further
168
+ # away from their parents, increasing exploration.
169
+ blend_alpha=0.75
159
170
  )
160
171
 
161
172
  # Pass the custom options to the solver
@@ -1,4 +1,5 @@
1
1
  numpy>=1.21
2
+ numba
2
3
 
3
4
  [cuda12]
4
5
  cupy-cuda12x
@@ -1,5 +1,6 @@
1
1
  import pytest
2
2
  import numpy as np
3
+ import numpy.testing as npt
3
4
 
4
5
  # Try to import cupy to check for CUDA availability
5
6
  try:
@@ -101,7 +102,7 @@ def test_get_real_roots_numpy(quadratic_func):
101
102
  Tests that the NumPy-based genetic algorithm approximates the roots correctly.
102
103
  """
103
104
  # Using more generations for higher accuracy in testing
104
- ga_opts = GA_Options(num_of_generations=50, data_size=200000, root_precision=3)
105
+ ga_opts = GA_Options(num_of_generations=50, data_size=200000, selection_percentile=0.66, root_precision=3)
105
106
 
106
107
  roots = quadratic_func.get_real_roots(ga_opts, use_cuda=False)
107
108
 
@@ -109,11 +110,7 @@ def test_get_real_roots_numpy(quadratic_func):
109
110
  # We don't know which order they'll be in, so we check for presence.
110
111
  expected_roots = np.array([-1.0, 2.5])
111
112
 
112
- # Check that at least one found root is close to -1.0
113
- assert np.any(np.isclose(roots, expected_roots[0], atol=1e-2))
114
-
115
- # Check that at least one found root is close to 2.5
116
- assert np.any(np.isclose(roots, expected_roots[1], atol=1e-2))
113
+ npt.assert_allclose(np.sort(roots), np.sort(expected_roots), atol=1e-2)
117
114
 
118
115
 
119
116
  @pytest.mark.skipif(not _CUPY_AVAILABLE, reason="CuPy is not installed, skipping CUDA test.")
@@ -124,13 +121,12 @@ def test_get_real_roots_cuda(quadratic_func):
124
121
  It will be skipped automatically if CuPy is not available.
125
122
  """
126
123
 
127
- ga_opts = GA_Options(num_of_generations=50, data_size=200000, root_precision=3)
124
+ ga_opts = GA_Options(num_of_generations=50, data_size=200000, selection_percentile=0.66, root_precision=3)
128
125
 
129
126
  roots = quadratic_func.get_real_roots(ga_opts, use_cuda=True)
130
127
 
131
128
  expected_roots = np.array([-1.0, 2.5])
132
129
 
133
130
  # Verify that the CUDA implementation also finds the correct roots within tolerance.
134
- assert np.any(np.isclose(roots, expected_roots[0], atol=1e-2))
135
- assert np.any(np.isclose(roots, expected_roots[1], atol=1e-2))
131
+ npt.assert_allclose(np.sort(roots), np.sort(expected_roots), atol=1e-2)
136
132
 
File without changes
File without changes