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.
- {polysolve-0.4.1/src/polysolve.egg-info → polysolve-0.5.0}/PKG-INFO +1 -1
- {polysolve-0.4.1 → polysolve-0.5.0}/pyproject.toml +1 -1
- {polysolve-0.4.1 → polysolve-0.5.0}/src/polysolve/__init__.py +94 -22
- {polysolve-0.4.1 → polysolve-0.5.0/src/polysolve.egg-info}/PKG-INFO +1 -1
- {polysolve-0.4.1 → polysolve-0.5.0}/tests/test_polysolve.py +2 -2
- {polysolve-0.4.1 → polysolve-0.5.0}/LICENSE +0 -0
- {polysolve-0.4.1 → polysolve-0.5.0}/README.md +0 -0
- {polysolve-0.4.1 → polysolve-0.5.0}/setup.cfg +0 -0
- {polysolve-0.4.1 → polysolve-0.5.0}/src/polysolve.egg-info/SOURCES.txt +0 -0
- {polysolve-0.4.1 → polysolve-0.5.0}/src/polysolve.egg-info/dependency_links.txt +0 -0
- {polysolve-0.4.1 → polysolve-0.5.0}/src/polysolve.egg-info/requires.txt +0 -0
- {polysolve-0.4.1 → polysolve-0.5.0}/src/polysolve.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: polysolve
|
|
3
|
-
Version: 0.
|
|
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
|
|
@@ -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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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(
|
|
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(
|
|
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
|
-
#
|
|
354
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
449
|
-
|
|
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
|
-
#
|
|
452
|
-
final_solutions_gpu = cupy.sort(
|
|
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
|
|
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.
|
|
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=
|
|
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=
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|