polysolve 0.4.1__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.1
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.1"
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,23 @@ 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
- )
90
+
91
+ def _get_cauchy_bound(coeffs: np.ndarray) -> float:
92
+ """
93
+ Calculates Cauchy's bound for the roots of a polynomial.
94
+ This provides a radius R such that all roots (real and complex)
95
+ have an absolute value less than or equal to R.
96
+
97
+ R = 1 + max(|c_n-1/c_n|, |c_n-2/c_n|, ..., |c_0/c_n|)
98
+ Where c_n is the leading coefficient (coeffs[0]).
99
+ """
100
+ # Normalize all coefficients by the leading coefficient
101
+ normalized_coeffs = np.abs(coeffs[1:] / coeffs[0])
102
+
103
+ # The bound is 1 + the maximum of these normalized values
104
+ R = 1 + np.max(normalized_coeffs)
105
+
106
+ return R
92
107
 
93
108
  class Function:
94
109
  """
@@ -287,8 +302,23 @@ class Function:
287
302
  mutation_size = int(data_size * mutation_ratio)
288
303
  random_size = data_size - elite_size - crossover_size - mutation_size
289
304
 
305
+ # Check if the user is using the default, non-expert range
306
+ user_range_is_default = (options.min_range == -100.0 and options.max_range == 100.0)
307
+
308
+ if user_range_is_default:
309
+ # User hasn't specified a custom range.
310
+ # We are the expert; use the smart, guaranteed bound.
311
+ bound = _get_cauchy_bound(self.coefficients)
312
+ min_r = -bound
313
+ max_r = bound
314
+ else:
315
+ # User has provided a custom range.
316
+ # Trust the expert; use their range.
317
+ min_r = options.min_range
318
+ max_r = options.max_range
319
+
290
320
  # Create initial random solutions
291
- solutions = np.random.uniform(options.min_range, options.max_range, data_size)
321
+ solutions = np.random.uniform(min_r, max_r, data_size)
292
322
 
293
323
  for _ in range(options.num_of_generations):
294
324
  # Calculate fitness for all solutions (vectorized)
@@ -332,7 +362,7 @@ class Function:
332
362
  mutated_solutions = mutation_candidates * mutation_factors
333
363
 
334
364
  # 4. New Randoms: Add new blood to prevent getting stuck
335
- random_solutions = np.random.uniform(options.min_range, options.max_range, random_size)
365
+ random_solutions = np.random.uniform(min_r, max_r, random_size)
336
366
 
337
367
  # Assemble the new generation
338
368
  solutions = np.concatenate([
@@ -348,12 +378,26 @@ class Function:
348
378
  error = y_calculated - y_val
349
379
  with np.errstate(divide='ignore'):
350
380
  ranks = np.where(error == 0, np.finfo(float).max, np.abs(1.0 / error))
351
- sorted_indices = np.argsort(-ranks)
352
381
 
353
- # Get the top 'sample_size' solutions the user asked for
354
- 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([])
355
393
 
356
- return np.sort(best_solutions)
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)
399
+
400
+ return np.sort(unique_roots)
357
401
 
358
402
  def _solve_x_cuda(self, y_val: float, options: GA_Options) -> np.ndarray:
359
403
  """Genetic algorithm implementation using CuPy (GPU/CUDA)."""
@@ -373,9 +417,24 @@ class Function:
373
417
  fitness_gpu = cupy.RawKernel(_FITNESS_KERNEL_FLOAT, 'fitness_kernel')
374
418
  d_coefficients = cupy.array(self.coefficients, dtype=cupy.float64)
375
419
 
420
+ # Check if the user is using the default, non-expert range
421
+ user_range_is_default = (options.min_range == -100.0 and options.max_range == 100.0)
422
+
423
+ if user_range_is_default:
424
+ # User hasn't specified a custom range.
425
+ # We are the expert; use the smart, guaranteed bound.
426
+ bound = _get_cauchy_bound(self.coefficients)
427
+ min_r = -bound
428
+ max_r = bound
429
+ else:
430
+ # User has provided a custom range.
431
+ # Trust the expert; use their range.
432
+ min_r = options.min_range
433
+ max_r = options.max_range
434
+
376
435
  # Create initial random solutions on the GPU
377
436
  d_solutions = cupy.random.uniform(
378
- options.min_range, options.max_range, options.data_size, dtype=cupy.float64
437
+ min_r, max_r, options.data_size, dtype=cupy.float64
379
438
  )
380
439
  d_ranks = cupy.empty(options.data_size, dtype=cupy.float64)
381
440
 
@@ -426,7 +485,7 @@ class Function:
426
485
 
427
486
  # 4. New Randoms
428
487
  d_random_solutions = cupy.random.uniform(
429
- options.min_range, options.max_range, random_size, dtype=cupy.float64
488
+ min_r, max_r, random_size, dtype=cupy.float64
430
489
  )
431
490
 
432
491
  # Assemble the new generation
@@ -443,13 +502,26 @@ class Function:
443
502
  (blocks_per_grid,), (threads_per_block,),
444
503
  (d_coefficients, d_coefficients.size, d_solutions, d_ranks, d_solutions.size, y_val)
445
504
  )
446
- sorted_indices = cupy.argsort(-d_ranks)
447
505
 
448
- # Get the top 'sample_size' solutions
449
- 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)
450
522
 
451
- # Get the final sample, sort it, and copy back to CPU
452
- 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)
453
525
  return final_solutions_gpu.get()
454
526
 
455
527
 
@@ -645,7 +717,7 @@ if __name__ == '__main__':
645
717
  print(f"Analytic roots of f1: {roots_analytic}") # Expected: -1, 2.5
646
718
 
647
719
  # 2. Genetic algorithm solution
648
- 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)
649
721
  print("\nFinding roots with Genetic Algorithm (CPU)...")
650
722
  roots_ga_cpu = f1.get_real_roots(ga_opts)
651
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.1
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