polysolve 0.4.2__tar.gz → 0.5.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.4.2
3
+ Version: 0.5.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
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  # --- Core Metadata ---
7
7
  name = "polysolve"
8
- version = "0.4.2"
8
+ version = "0.5.0"
9
9
  authors = [
10
10
  { name="Jonathan Rampersad", email="jonathan@jono-rams.work" },
11
11
  ]
@@ -49,8 +49,6 @@ class GA_Options:
49
49
  Default: 100.0
50
50
  num_of_generations (int): The number of iterations the algorithm will run.
51
51
  Default: 10
52
- sample_size (int): The number of top solutions to *return* at the end.
53
- Default: 1000
54
52
  data_size (int): The total number of solutions (population size)
55
53
  generated in each generation. Default: 100000
56
54
  mutation_strength (float): The percentage (e.g., 0.01 for 1%) by which
@@ -64,16 +62,21 @@ class GA_Options:
64
62
  mutation_ratio (float): The percentage (e.g., 0.40 for 40%) of the next
65
63
  generation to be created by mutating solutions
66
64
  from the parent pool. Default: 0.40
65
+ root_precision (int): The number of decimal places to round roots to
66
+ when clustering. A smaller number (e.g., 3)
67
+ groups roots more aggressively. A larger number
68
+ (e.g., 7) is more precise but may return
69
+ multiple near-identical roots. Default: 5
67
70
  """
68
71
  min_range: float = -100.0
69
72
  max_range: float = 100.0
70
73
  num_of_generations: int = 10
71
- sample_size: int = 1000
72
74
  data_size: int = 100000
73
75
  mutation_strength: float = 0.01
74
76
  elite_ratio: float = 0.05
75
77
  crossover_ratio: float = 0.45
76
78
  mutation_ratio: float = 0.40
79
+ root_precision: int = 5
77
80
 
78
81
  def __post_init__(self):
79
82
  """Validates the GA options after initialization."""
@@ -84,11 +87,6 @@ class GA_Options:
84
87
  )
85
88
  if any(r < 0 for r in [self.elite_ratio, self.crossover_ratio, self.mutation_ratio]):
86
89
  raise ValueError("GA ratios cannot be negative.")
87
- if self.data_size < self.sample_size:
88
- warnings.warn(
89
- f"data_size ({self.data_size}) is less than sample_size ({self.sample_size}). "
90
- "The number of returned solutions will be limited to data_size."
91
- )
92
90
 
93
91
  def _get_cauchy_bound(coeffs: np.ndarray) -> float:
94
92
  """
@@ -380,12 +378,26 @@ class Function:
380
378
  error = y_calculated - y_val
381
379
  with np.errstate(divide='ignore'):
382
380
  ranks = np.where(error == 0, np.finfo(float).max, np.abs(1.0 / error))
383
- sorted_indices = np.argsort(-ranks)
384
381
 
385
- # Get the top 'sample_size' solutions the user asked for
386
- best_solutions = solutions[sorted_indices][:options.sample_size]
382
+ # 1. Define quality based on the user's desired precision
383
+ # (e.g., precision=5 -> rank > 1e6, precision=8 -> rank > 1e9)
384
+ # We add +1 for a buffer, ensuring we only get high-quality roots.
385
+ quality_threshold = 10**(options.root_precision + 1)
386
+
387
+ # 2. Get all solutions that meet this quality threshold
388
+ high_quality_solutions = solutions[ranks > quality_threshold]
389
+
390
+ if high_quality_solutions.size == 0:
391
+ # No roots found that meet the quality, return empty
392
+ return np.array([])
393
+
394
+ # 3. Cluster these high-quality solutions by rounding
395
+ rounded_solutions = np.round(high_quality_solutions, options.root_precision)
396
+
397
+ # 4. Return only the unique roots
398
+ unique_roots = np.unique(rounded_solutions)
387
399
 
388
- return np.sort(best_solutions)
400
+ return np.sort(unique_roots)
389
401
 
390
402
  def _solve_x_cuda(self, y_val: float, options: GA_Options) -> np.ndarray:
391
403
  """Genetic algorithm implementation using CuPy (GPU/CUDA)."""
@@ -490,13 +502,26 @@ class Function:
490
502
  (blocks_per_grid,), (threads_per_block,),
491
503
  (d_coefficients, d_coefficients.size, d_solutions, d_ranks, d_solutions.size, y_val)
492
504
  )
493
- sorted_indices = cupy.argsort(-d_ranks)
494
505
 
495
- # Get the top 'sample_size' solutions
496
- d_best_solutions = d_solutions[sorted_indices][:options.sample_size]
506
+ # 1. Define quality based on the user's desired precision
507
+ # (e.g., precision=5 -> rank > 1e6, precision=8 -> rank > 1e9)
508
+ # We add +1 for a buffer, ensuring we only get high-quality roots.
509
+ quality_threshold = 10**(options.root_precision + 1)
510
+
511
+ # 2. Get all solutions that meet this quality threshold
512
+ d_high_quality_solutions = d_solutions[d_ranks > quality_threshold]
513
+
514
+ if d_high_quality_solutions.size == 0:
515
+ return np.array([])
516
+
517
+ # 3. Cluster these high-quality solutions on the GPU by rounding
518
+ d_rounded_solutions = cupy.round(d_high_quality_solutions, options.root_precision)
519
+
520
+ # 4. Get only the unique roots
521
+ d_unique_roots = cupy.unique(d_rounded_solutions)
497
522
 
498
- # Get the final sample, sort it, and copy back to CPU
499
- final_solutions_gpu = cupy.sort(d_best_solutions)
523
+ # Sort the unique roots and copy back to CPU
524
+ final_solutions_gpu = cupy.sort(d_unique_roots)
500
525
  return final_solutions_gpu.get()
501
526
 
502
527
 
@@ -692,7 +717,7 @@ if __name__ == '__main__':
692
717
  print(f"Analytic roots of f1: {roots_analytic}") # Expected: -1, 2.5
693
718
 
694
719
  # 2. Genetic algorithm solution
695
- ga_opts = GA_Options(num_of_generations=20, data_size=50000, sample_size=10)
720
+ ga_opts = GA_Options(num_of_generations=20, data_size=50000)
696
721
  print("\nFinding roots with Genetic Algorithm (CPU)...")
697
722
  roots_ga_cpu = f1.get_real_roots(ga_opts)
698
723
  print(f"Approximate roots from GA (CPU): {roots_ga_cpu}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polysolve
3
- Version: 0.4.2
3
+ Version: 0.5.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
@@ -101,7 +101,7 @@ def test_get_real_roots_numpy(quadratic_func):
101
101
  Tests that the NumPy-based genetic algorithm approximates the roots correctly.
102
102
  """
103
103
  # Using more generations for higher accuracy in testing
104
- ga_opts = GA_Options(num_of_generations=25, data_size=50000)
104
+ ga_opts = GA_Options(num_of_generations=50, data_size=200000, root_precision=3)
105
105
 
106
106
  roots = quadratic_func.get_real_roots(ga_opts, use_cuda=False)
107
107
 
@@ -124,7 +124,7 @@ def test_get_real_roots_cuda(quadratic_func):
124
124
  It will be skipped automatically if CuPy is not available.
125
125
  """
126
126
 
127
- ga_opts = GA_Options(num_of_generations=25, data_size=50000)
127
+ ga_opts = GA_Options(num_of_generations=50, data_size=200000, root_precision=3)
128
128
 
129
129
  roots = quadratic_func.get_real_roots(ga_opts, use_cuda=True)
130
130
 
File without changes
File without changes
File without changes