polysolve 0.3.2__tar.gz → 0.4.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.3.2/src/polysolve.egg-info → polysolve-0.4.0}/PKG-INFO +31 -5
- {polysolve-0.3.2 → polysolve-0.4.0}/README.md +30 -4
- {polysolve-0.3.2 → polysolve-0.4.0}/pyproject.toml +1 -1
- {polysolve-0.3.2 → polysolve-0.4.0}/src/polysolve/__init__.py +202 -98
- {polysolve-0.3.2 → polysolve-0.4.0/src/polysolve.egg-info}/PKG-INFO +31 -5
- {polysolve-0.3.2 → polysolve-0.4.0}/tests/test_polysolve.py +2 -2
- {polysolve-0.3.2 → polysolve-0.4.0}/LICENSE +0 -0
- {polysolve-0.3.2 → polysolve-0.4.0}/setup.cfg +0 -0
- {polysolve-0.3.2 → polysolve-0.4.0}/src/polysolve.egg-info/SOURCES.txt +0 -0
- {polysolve-0.3.2 → polysolve-0.4.0}/src/polysolve.egg-info/dependency_links.txt +0 -0
- {polysolve-0.3.2 → polysolve-0.4.0}/src/polysolve.egg-info/requires.txt +0 -0
- {polysolve-0.3.2 → polysolve-0.4.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.4.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
|
|
@@ -94,12 +94,12 @@ pip install polysolve[cuda12]
|
|
|
94
94
|
Here is a simple example of how to define a quadratic function, find its properties, and solve for its roots.
|
|
95
95
|
|
|
96
96
|
```python
|
|
97
|
-
from polysolve import Function, GA_Options
|
|
97
|
+
from polysolve import Function, GA_Options
|
|
98
98
|
|
|
99
99
|
# 1. Define the function f(x) = 2x^2 - 3x - 5
|
|
100
100
|
# Coefficients can be integers or floats.
|
|
101
101
|
f1 = Function(largest_exponent=2)
|
|
102
|
-
f1.
|
|
102
|
+
f1.set_coeffs([2, -3, -5])
|
|
103
103
|
|
|
104
104
|
print(f"Function f1: {f1}")
|
|
105
105
|
# > Function f1: 2x^2 - 3x - 5
|
|
@@ -121,7 +121,7 @@ print(f"2nd Derivative of f1: {ddf1}")
|
|
|
121
121
|
|
|
122
122
|
# 5. Find roots analytically using the quadratic formula
|
|
123
123
|
# This is exact and fast for degree-2 polynomials.
|
|
124
|
-
roots_analytic = quadratic_solve(
|
|
124
|
+
roots_analytic = f1.quadratic_solve()
|
|
125
125
|
print(f"Analytic roots: {sorted(roots_analytic)}")
|
|
126
126
|
# > Analytic roots: [-1.0, 2.5]
|
|
127
127
|
|
|
@@ -140,6 +140,32 @@ print(f"Approximate roots from GA: {roots_ga[:2]}")
|
|
|
140
140
|
|
|
141
141
|
---
|
|
142
142
|
|
|
143
|
+
## Tuning the Genetic Algorithm
|
|
144
|
+
|
|
145
|
+
The `GA_Options` class gives you fine-grained control over the genetic algorithm's performance, letting you trade speed for accuracy.
|
|
146
|
+
|
|
147
|
+
The default options are balanced, but for very complex polynomials, you may want a more exhaustive search.
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
from polysolve import GA_Options
|
|
151
|
+
|
|
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(
|
|
155
|
+
num_of_generations=50, # Run for more generations
|
|
156
|
+
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
|
+
|
|
161
|
+
# Pass the custom options to the solver
|
|
162
|
+
roots = f1.get_real_roots(ga_accurate)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
For a full breakdown of all parameters, including crossover_ratio, mutation_strength, and more, please see [the full GA_Options API Documentation](https://polysolve.jono-rams.work/docs/ga-options-api).
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
143
169
|
## Development & Testing Environment
|
|
144
170
|
|
|
145
171
|
This project is automatically tested against a specific set of dependencies to ensure stability. Our Continuous Integration (CI) pipeline runs on an environment using **CUDA 12.5** on **Ubuntu 24.04**.
|
|
@@ -164,7 +190,7 @@ Please read our `CONTRIBUTING.md` file for details on our code of conduct and th
|
|
|
164
190
|
<table>
|
|
165
191
|
<tbody>
|
|
166
192
|
<tr>
|
|
167
|
-
<td align="center" valign="top" width="14.28%"><a href="https://jono-rams.work"><img src="https://avatars.githubusercontent.com/u/29872001?v=4?s=100" width="100px;" alt="Jonathan Rampersad"/><br /><sub><b>Jonathan Rampersad</b></sub></a><br /><a href="https://github.com/jono-rams/PolySolve/commits?author=jono-rams" title="Code">💻</a> <a href="https://github.com/jono-rams/PolySolve/commits?author=jono-rams" title="Documentation">📖</a> <a href="#infra-jono-rams" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
|
193
|
+
<td align="center" valign="top" width="14.28%"><a href="https://jono-rams.work"><img src="https://avatars.githubusercontent.com/u/29872001?v=4?s=100" width="100px;" alt="Jonathan Rampersad"/><br /><sub><b>Jonathan Rampersad</b></sub></a><br /><a href="https://github.com/jono-rams/PolySolve/commits?author=jono-rams" title="Maintenance">🚧</a> <a href="https://github.com/jono-rams/PolySolve/commits?author=jono-rams" title="Code">💻</a> <a href="https://github.com/jono-rams/PolySolve/commits?author=jono-rams" title="Documentation">📖</a> <a href="#infra-jono-rams" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
|
168
194
|
</tr>
|
|
169
195
|
</tbody>
|
|
170
196
|
<tfoot>
|
|
@@ -43,12 +43,12 @@ pip install polysolve[cuda12]
|
|
|
43
43
|
Here is a simple example of how to define a quadratic function, find its properties, and solve for its roots.
|
|
44
44
|
|
|
45
45
|
```python
|
|
46
|
-
from polysolve import Function, GA_Options
|
|
46
|
+
from polysolve import Function, GA_Options
|
|
47
47
|
|
|
48
48
|
# 1. Define the function f(x) = 2x^2 - 3x - 5
|
|
49
49
|
# Coefficients can be integers or floats.
|
|
50
50
|
f1 = Function(largest_exponent=2)
|
|
51
|
-
f1.
|
|
51
|
+
f1.set_coeffs([2, -3, -5])
|
|
52
52
|
|
|
53
53
|
print(f"Function f1: {f1}")
|
|
54
54
|
# > Function f1: 2x^2 - 3x - 5
|
|
@@ -70,7 +70,7 @@ print(f"2nd Derivative of f1: {ddf1}")
|
|
|
70
70
|
|
|
71
71
|
# 5. Find roots analytically using the quadratic formula
|
|
72
72
|
# This is exact and fast for degree-2 polynomials.
|
|
73
|
-
roots_analytic = quadratic_solve(
|
|
73
|
+
roots_analytic = f1.quadratic_solve()
|
|
74
74
|
print(f"Analytic roots: {sorted(roots_analytic)}")
|
|
75
75
|
# > Analytic roots: [-1.0, 2.5]
|
|
76
76
|
|
|
@@ -89,6 +89,32 @@ print(f"Approximate roots from GA: {roots_ga[:2]}")
|
|
|
89
89
|
|
|
90
90
|
---
|
|
91
91
|
|
|
92
|
+
## Tuning the Genetic Algorithm
|
|
93
|
+
|
|
94
|
+
The `GA_Options` class gives you fine-grained control over the genetic algorithm's performance, letting you trade speed for accuracy.
|
|
95
|
+
|
|
96
|
+
The default options are balanced, but for very complex polynomials, you may want a more exhaustive search.
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from polysolve import GA_Options
|
|
100
|
+
|
|
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(
|
|
104
|
+
num_of_generations=50, # Run for more generations
|
|
105
|
+
data_size=500000, # Use a larger population
|
|
106
|
+
elite_ratio=0.1, # Keep the top 10%
|
|
107
|
+
mutation_ratio=0.5 # Mutate 50%
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Pass the custom options to the solver
|
|
111
|
+
roots = f1.get_real_roots(ga_accurate)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
For a full breakdown of all parameters, including crossover_ratio, mutation_strength, and more, please see [the full GA_Options API Documentation](https://polysolve.jono-rams.work/docs/ga-options-api).
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
92
118
|
## Development & Testing Environment
|
|
93
119
|
|
|
94
120
|
This project is automatically tested against a specific set of dependencies to ensure stability. Our Continuous Integration (CI) pipeline runs on an environment using **CUDA 12.5** on **Ubuntu 24.04**.
|
|
@@ -113,7 +139,7 @@ Please read our `CONTRIBUTING.md` file for details on our code of conduct and th
|
|
|
113
139
|
<table>
|
|
114
140
|
<tbody>
|
|
115
141
|
<tr>
|
|
116
|
-
<td align="center" valign="top" width="14.28%"><a href="https://jono-rams.work"><img src="https://avatars.githubusercontent.com/u/29872001?v=4?s=100" width="100px;" alt="Jonathan Rampersad"/><br /><sub><b>Jonathan Rampersad</b></sub></a><br /><a href="https://github.com/jono-rams/PolySolve/commits?author=jono-rams" title="Code">💻</a> <a href="https://github.com/jono-rams/PolySolve/commits?author=jono-rams" title="Documentation">📖</a> <a href="#infra-jono-rams" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
|
142
|
+
<td align="center" valign="top" width="14.28%"><a href="https://jono-rams.work"><img src="https://avatars.githubusercontent.com/u/29872001?v=4?s=100" width="100px;" alt="Jonathan Rampersad"/><br /><sub><b>Jonathan Rampersad</b></sub></a><br /><a href="https://github.com/jono-rams/PolySolve/commits?author=jono-rams" title="Maintenance">🚧</a> <a href="https://github.com/jono-rams/PolySolve/commits?author=jono-rams" title="Code">💻</a> <a href="https://github.com/jono-rams/PolySolve/commits?author=jono-rams" title="Documentation">📖</a> <a href="#infra-jono-rams" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
|
117
143
|
</tr>
|
|
118
144
|
</tbody>
|
|
119
145
|
<tfoot>
|
|
@@ -25,11 +25,10 @@ extern "C" __global__ void fitness_kernel(
|
|
|
25
25
|
int idx = threadIdx.x + blockIdx.x * blockDim.x;
|
|
26
26
|
if (idx < size)
|
|
27
27
|
{
|
|
28
|
-
double ans = 0;
|
|
29
|
-
int
|
|
30
|
-
for (int i = 0; i < num_coefficients; ++i)
|
|
28
|
+
double ans = coefficients[0];
|
|
29
|
+
for (int i = 1; i < num_coefficients; ++i)
|
|
31
30
|
{
|
|
32
|
-
ans
|
|
31
|
+
ans = ans * x_vals[idx] + coefficients[i];
|
|
33
32
|
}
|
|
34
33
|
|
|
35
34
|
ans -= y_val;
|
|
@@ -37,31 +36,6 @@ extern "C" __global__ void fitness_kernel(
|
|
|
37
36
|
}
|
|
38
37
|
}
|
|
39
38
|
"""
|
|
40
|
-
_FITNESS_KERNEL_INT = """
|
|
41
|
-
extern "C" __global__ void fitness_kernel(
|
|
42
|
-
const long long* coefficients,
|
|
43
|
-
int num_coefficients,
|
|
44
|
-
const double* x_vals,
|
|
45
|
-
double* ranks,
|
|
46
|
-
int size,
|
|
47
|
-
double y_val)
|
|
48
|
-
{
|
|
49
|
-
int idx = threadIdx.x + blockIdx.x * blockDim.x;
|
|
50
|
-
if (idx < size)
|
|
51
|
-
{
|
|
52
|
-
double ans = 0;
|
|
53
|
-
int lrgst_expo = num_coefficients - 1;
|
|
54
|
-
for (int i = 0; i < num_coefficients; ++i)
|
|
55
|
-
{
|
|
56
|
-
ans += coefficients[i] * pow(x_vals[idx], (double)(lrgst_expo - i));
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
ans -= y_val;
|
|
60
|
-
ranks[idx] = (ans == 0) ? 1.7976931348623157e+308 : fabs(1.0 / ans);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
"""
|
|
64
|
-
|
|
65
39
|
|
|
66
40
|
@dataclass
|
|
67
41
|
class GA_Options:
|
|
@@ -70,18 +44,51 @@ class GA_Options:
|
|
|
70
44
|
|
|
71
45
|
Attributes:
|
|
72
46
|
min_range (float): The minimum value for the initial random solutions.
|
|
47
|
+
Default: -100.0
|
|
73
48
|
max_range (float): The maximum value for the initial random solutions.
|
|
49
|
+
Default: 100.0
|
|
74
50
|
num_of_generations (int): The number of iterations the algorithm will run.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
51
|
+
Default: 10
|
|
52
|
+
sample_size (int): The number of top solutions to *return* at the end.
|
|
53
|
+
Default: 1000
|
|
54
|
+
data_size (int): The total number of solutions (population size)
|
|
55
|
+
generated in each generation. Default: 100000
|
|
56
|
+
mutation_strength (float): The percentage (e.g., 0.01 for 1%) by which
|
|
57
|
+
a solution is mutated. Default: 0.01
|
|
58
|
+
elite_ratio (float): The percentage (e.g., 0.05 for 5%) of the *best*
|
|
59
|
+
solutions to carry over to the next generation
|
|
60
|
+
unchanged (elitism). Default: 0.05
|
|
61
|
+
crossover_ratio (float): The percentage (e.g., 0.45 for 45%) of the next
|
|
62
|
+
generation to be created by "breeding" two
|
|
63
|
+
solutions from the parent pool. Default: 0.45
|
|
64
|
+
mutation_ratio (float): The percentage (e.g., 0.40 for 40%) of the next
|
|
65
|
+
generation to be created by mutating solutions
|
|
66
|
+
from the parent pool. Default: 0.40
|
|
78
67
|
"""
|
|
79
68
|
min_range: float = -100.0
|
|
80
69
|
max_range: float = 100.0
|
|
81
70
|
num_of_generations: int = 10
|
|
82
71
|
sample_size: int = 1000
|
|
83
72
|
data_size: int = 100000
|
|
84
|
-
|
|
73
|
+
mutation_strength: float = 0.01
|
|
74
|
+
elite_ratio: float = 0.05
|
|
75
|
+
crossover_ratio: float = 0.45
|
|
76
|
+
mutation_ratio: float = 0.40
|
|
77
|
+
|
|
78
|
+
def __post_init__(self):
|
|
79
|
+
"""Validates the GA options after initialization."""
|
|
80
|
+
total_ratio = self.elite_ratio + self.crossover_ratio + self.mutation_ratio
|
|
81
|
+
if total_ratio > 1.0:
|
|
82
|
+
raise ValueError(
|
|
83
|
+
f"The sum of elite_ratio, crossover_ratio, and mutation_ratio must be <= 1.0, but got {total_ratio}"
|
|
84
|
+
)
|
|
85
|
+
if any(r < 0 for r in [self.elite_ratio, self.crossover_ratio, self.mutation_ratio]):
|
|
86
|
+
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
|
+
)
|
|
85
92
|
|
|
86
93
|
class Function:
|
|
87
94
|
"""
|
|
@@ -176,7 +183,7 @@ class Function:
|
|
|
176
183
|
if self._largest_exponent == 0:
|
|
177
184
|
raise ValueError("Cannot differentiate a constant (Function of degree 0).")
|
|
178
185
|
|
|
179
|
-
return self.
|
|
186
|
+
return self.derivative()
|
|
180
187
|
|
|
181
188
|
|
|
182
189
|
def derivative(self) -> 'Function':
|
|
@@ -269,8 +276,19 @@ class Function:
|
|
|
269
276
|
|
|
270
277
|
def _solve_x_numpy(self, y_val: float, options: GA_Options) -> np.ndarray:
|
|
271
278
|
"""Genetic algorithm implementation using NumPy (CPU)."""
|
|
279
|
+
elite_ratio = options.elite_ratio
|
|
280
|
+
crossover_ratio = options.crossover_ratio
|
|
281
|
+
mutation_ratio = options.mutation_ratio
|
|
282
|
+
|
|
283
|
+
data_size = options.data_size
|
|
284
|
+
|
|
285
|
+
elite_size = int(data_size * elite_ratio)
|
|
286
|
+
crossover_size = int(data_size * crossover_ratio)
|
|
287
|
+
mutation_size = int(data_size * mutation_ratio)
|
|
288
|
+
random_size = data_size - elite_size - crossover_size - mutation_size
|
|
289
|
+
|
|
272
290
|
# Create initial random solutions
|
|
273
|
-
solutions = np.random.uniform(options.min_range, options.max_range,
|
|
291
|
+
solutions = np.random.uniform(options.min_range, options.max_range, data_size)
|
|
274
292
|
|
|
275
293
|
for _ in range(options.num_of_generations):
|
|
276
294
|
# Calculate fitness for all solutions (vectorized)
|
|
@@ -283,40 +301,75 @@ class Function:
|
|
|
283
301
|
sorted_indices = np.argsort(-ranks)
|
|
284
302
|
solutions = solutions[sorted_indices]
|
|
285
303
|
|
|
286
|
-
#
|
|
287
|
-
|
|
304
|
+
# --- Create the next generation ---
|
|
305
|
+
|
|
306
|
+
# 1. Elitism: Keep the best solutions as-is
|
|
307
|
+
elite_solutions = solutions[:elite_size]
|
|
308
|
+
|
|
309
|
+
# Define a "parent pool" of the top 50% of solutions to breed from
|
|
310
|
+
parent_pool = solutions[:data_size // 2]
|
|
311
|
+
|
|
312
|
+
# 2. Crossover: Breed two parents to create a child
|
|
313
|
+
# Select from the full list (indices 0 to data_size-1)
|
|
314
|
+
parent1_indices = np.random.randint(0, data_size, crossover_size)
|
|
315
|
+
parent2_indices = np.random.randint(0, data_size, crossover_size)
|
|
316
|
+
parents1 = solutions[parent1_indices]
|
|
317
|
+
parents2 = solutions[parent2_indices]
|
|
318
|
+
# Simple "average" crossover
|
|
319
|
+
crossover_solutions = (parents1 + parents2) / 2.0
|
|
320
|
+
|
|
321
|
+
# 3. Mutation:
|
|
322
|
+
# Select from the full list (indices 0 to data_size-1)
|
|
323
|
+
mutation_candidates = solutions[np.random.randint(0, data_size, mutation_size)]
|
|
288
324
|
|
|
289
|
-
#
|
|
290
|
-
# and fill the rest with new random values.
|
|
325
|
+
# Use mutation_strength (the new name)
|
|
291
326
|
mutation_factors = np.random.uniform(
|
|
292
|
-
1 - options.
|
|
293
|
-
1 + options.
|
|
294
|
-
|
|
327
|
+
1 - options.mutation_strength,
|
|
328
|
+
1 + options.mutation_strength,
|
|
329
|
+
mutation_size
|
|
295
330
|
)
|
|
296
|
-
mutated_solutions =
|
|
297
|
-
|
|
298
|
-
new_random_solutions = np.random.uniform(
|
|
299
|
-
options.min_range, options.max_range, options.data_size - options.sample_size
|
|
300
|
-
)
|
|
301
|
-
|
|
302
|
-
solutions = np.concatenate([mutated_solutions, new_random_solutions])
|
|
331
|
+
mutated_solutions = mutation_candidates * mutation_factors
|
|
303
332
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
333
|
+
# 4. New Randoms: Add new blood to prevent getting stuck
|
|
334
|
+
random_solutions = np.random.uniform(options.min_range, options.max_range, random_size)
|
|
335
|
+
|
|
336
|
+
# Assemble the new generation
|
|
337
|
+
solutions = np.concatenate([
|
|
338
|
+
elite_solutions,
|
|
339
|
+
crossover_solutions,
|
|
340
|
+
mutated_solutions,
|
|
341
|
+
random_solutions
|
|
342
|
+
])
|
|
343
|
+
|
|
344
|
+
# --- Final Step: Return the best results ---
|
|
345
|
+
# After all generations, do one last ranking to find the best solutions
|
|
346
|
+
y_calculated = np.polyval(self.coefficients, solutions)
|
|
347
|
+
error = y_calculated - y_val
|
|
348
|
+
ranks = np.where(error == 0, np.finfo(float).max, np.abs(1.0 / error))
|
|
349
|
+
sorted_indices = np.argsort(-ranks)
|
|
350
|
+
|
|
351
|
+
# Get the top 'sample_size' solutions the user asked for
|
|
352
|
+
best_solutions = solutions[sorted_indices][:options.sample_size]
|
|
353
|
+
|
|
354
|
+
return np.sort(best_solutions)
|
|
307
355
|
|
|
308
356
|
def _solve_x_cuda(self, y_val: float, options: GA_Options) -> np.ndarray:
|
|
309
357
|
"""Genetic algorithm implementation using CuPy (GPU/CUDA)."""
|
|
358
|
+
|
|
359
|
+
elite_ratio = options.elite_ratio
|
|
360
|
+
crossover_ratio = options.crossover_ratio
|
|
361
|
+
mutation_ratio = options.mutation_ratio
|
|
310
362
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
363
|
+
data_size = options.data_size
|
|
364
|
+
|
|
365
|
+
elite_size = int(data_size * elite_ratio)
|
|
366
|
+
crossover_size = int(data_size * crossover_ratio)
|
|
367
|
+
mutation_size = int(data_size * mutation_ratio)
|
|
368
|
+
random_size = data_size - elite_size - crossover_size - mutation_size
|
|
369
|
+
|
|
370
|
+
# ALWAYS cast coefficients to float64 for the kernel.
|
|
371
|
+
fitness_gpu = cupy.RawKernel(_FITNESS_KERNEL_FLOAT, 'fitness_kernel')
|
|
372
|
+
d_coefficients = cupy.array(self.coefficients, dtype=cupy.float64)
|
|
320
373
|
|
|
321
374
|
# Create initial random solutions on the GPU
|
|
322
375
|
d_solutions = cupy.random.uniform(
|
|
@@ -339,27 +392,62 @@ class Function:
|
|
|
339
392
|
sorted_indices = cupy.argsort(-d_ranks)
|
|
340
393
|
d_solutions = d_solutions[sorted_indices]
|
|
341
394
|
|
|
342
|
-
|
|
343
|
-
break
|
|
344
|
-
|
|
345
|
-
# Get top solutions
|
|
346
|
-
d_top_solutions = d_solutions[:options.sample_size]
|
|
347
|
-
|
|
348
|
-
# Mutate top solutions on the GPU
|
|
349
|
-
mutation_factors = cupy.random.uniform(
|
|
350
|
-
1 - options.mutation_percentage, 1 + options.mutation_percentage, options.sample_size
|
|
351
|
-
)
|
|
352
|
-
d_mutated = d_top_solutions * mutation_factors
|
|
395
|
+
# --- Create the next generation ---
|
|
353
396
|
|
|
354
|
-
#
|
|
355
|
-
|
|
356
|
-
|
|
397
|
+
# 1. Elitism
|
|
398
|
+
d_elite_solutions = d_solutions[:elite_size]
|
|
399
|
+
|
|
400
|
+
# Define a "parent pool" of the top 50% of solutions to breed from
|
|
401
|
+
d_parent_pool = d_solutions[:data_size // 2]
|
|
402
|
+
parent_pool_size = d_parent_pool.size
|
|
403
|
+
|
|
404
|
+
# 2. Crossover
|
|
405
|
+
# Select from the full list (indices 0 to data_size-1)
|
|
406
|
+
parent1_indices = cupy.random.randint(0, data_size, crossover_size)
|
|
407
|
+
parent2_indices = cupy.random.randint(0, data_size, crossover_size)
|
|
408
|
+
d_parents1 = d_solutions[parent1_indices]
|
|
409
|
+
d_parents2 = d_solutions[parent2_indices]
|
|
410
|
+
d_crossover_solutions = (d_parents1 + d_parents2) / 2.0
|
|
411
|
+
|
|
412
|
+
# 3. Mutation
|
|
413
|
+
# Select from the full list (indices 0 to data_size-1)
|
|
414
|
+
mutation_indices = cupy.random.randint(0, data_size, mutation_size)
|
|
415
|
+
d_mutation_candidates = d_solutions[mutation_indices]
|
|
416
|
+
|
|
417
|
+
# Use mutation_strength (the new name)
|
|
418
|
+
d_mutation_factors = cupy.random.uniform(
|
|
419
|
+
1 - options.mutation_strength,
|
|
420
|
+
1 + options.mutation_strength,
|
|
421
|
+
mutation_size
|
|
422
|
+
)
|
|
423
|
+
d_mutated_solutions = d_mutation_candidates * d_mutation_factors
|
|
424
|
+
|
|
425
|
+
# 4. New Randoms
|
|
426
|
+
d_random_solutions = cupy.random.uniform(
|
|
427
|
+
options.min_range, options.max_range, random_size, dtype=cupy.float64
|
|
357
428
|
)
|
|
358
429
|
|
|
359
|
-
|
|
430
|
+
# Assemble the new generation
|
|
431
|
+
d_solutions = cupy.concatenate([
|
|
432
|
+
d_elite_solutions,
|
|
433
|
+
d_crossover_solutions,
|
|
434
|
+
d_mutated_solutions,
|
|
435
|
+
d_random_solutions
|
|
436
|
+
])
|
|
437
|
+
|
|
438
|
+
# --- Final Step: Return the best results ---
|
|
439
|
+
# After all generations, do one last ranking to find the best solutions
|
|
440
|
+
fitness_gpu(
|
|
441
|
+
(blocks_per_grid,), (threads_per_block,),
|
|
442
|
+
(d_coefficients, d_coefficients.size, d_solutions, d_ranks, d_solutions.size, y_val)
|
|
443
|
+
)
|
|
444
|
+
sorted_indices = cupy.argsort(-d_ranks)
|
|
445
|
+
|
|
446
|
+
# Get the top 'sample_size' solutions
|
|
447
|
+
d_best_solutions = d_solutions[sorted_indices][:options.sample_size]
|
|
360
448
|
|
|
361
449
|
# Get the final sample, sort it, and copy back to CPU
|
|
362
|
-
final_solutions_gpu = cupy.sort(
|
|
450
|
+
final_solutions_gpu = cupy.sort(d_best_solutions)
|
|
363
451
|
return final_solutions_gpu.get()
|
|
364
452
|
|
|
365
453
|
|
|
@@ -481,36 +569,52 @@ class Function:
|
|
|
481
569
|
def __imul__(self, other: Union['Function', int, float]) -> 'Function':
|
|
482
570
|
"""Performs in-place multiplication by a scalar (func *= 3)."""
|
|
483
571
|
|
|
484
|
-
self.
|
|
572
|
+
self._check_initialized()
|
|
573
|
+
|
|
574
|
+
if isinstance(other, (int, float)):
|
|
575
|
+
if other == 0:
|
|
576
|
+
self.coefficients = np.array([0], dtype=self.coefficients.dtype)
|
|
577
|
+
self._largest_exponent = 0
|
|
578
|
+
else:
|
|
579
|
+
self.coefficients *= other
|
|
580
|
+
|
|
581
|
+
elif isinstance(other, self.__class__):
|
|
582
|
+
other._check_initialized()
|
|
583
|
+
self.coefficients = np.polymul(self.coefficients, other.coefficients)
|
|
584
|
+
self._largest_exponent = len(self.coefficients) - 1
|
|
585
|
+
|
|
586
|
+
else:
|
|
587
|
+
return NotImplemented
|
|
588
|
+
|
|
485
589
|
return self
|
|
486
590
|
|
|
487
591
|
|
|
488
|
-
def quadratic_solve(
|
|
489
|
-
|
|
490
|
-
|
|
592
|
+
def quadratic_solve(self) -> Optional[List[float]]:
|
|
593
|
+
"""
|
|
594
|
+
Calculates the real roots of a quadratic function using the quadratic formula.
|
|
491
595
|
|
|
492
|
-
|
|
493
|
-
|
|
596
|
+
Args:
|
|
597
|
+
f (Function): A Function object of degree 2.
|
|
494
598
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
599
|
+
Returns:
|
|
600
|
+
Optional[List[float]]: A list containing the two real roots, or None if there are no real roots.
|
|
601
|
+
"""
|
|
602
|
+
self._check_initialized()
|
|
603
|
+
if self.largest_exponent != 2:
|
|
604
|
+
raise ValueError("Input function must be quadratic (degree 2) to use quadratic_solve.")
|
|
501
605
|
|
|
502
|
-
|
|
606
|
+
a, b, c = self.coefficients
|
|
503
607
|
|
|
504
|
-
|
|
608
|
+
discriminant = (b**2) - (4*a*c)
|
|
505
609
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
610
|
+
if discriminant < 0:
|
|
611
|
+
return None # No real roots
|
|
612
|
+
|
|
613
|
+
sqrt_discriminant = math.sqrt(discriminant)
|
|
614
|
+
root1 = (-b + sqrt_discriminant) / (2 * a)
|
|
615
|
+
root2 = (-b - sqrt_discriminant) / (2 * a)
|
|
512
616
|
|
|
513
|
-
|
|
617
|
+
return [root1, root2]
|
|
514
618
|
|
|
515
619
|
# Example Usage
|
|
516
620
|
if __name__ == '__main__':
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: polysolve
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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
|
|
@@ -94,12 +94,12 @@ pip install polysolve[cuda12]
|
|
|
94
94
|
Here is a simple example of how to define a quadratic function, find its properties, and solve for its roots.
|
|
95
95
|
|
|
96
96
|
```python
|
|
97
|
-
from polysolve import Function, GA_Options
|
|
97
|
+
from polysolve import Function, GA_Options
|
|
98
98
|
|
|
99
99
|
# 1. Define the function f(x) = 2x^2 - 3x - 5
|
|
100
100
|
# Coefficients can be integers or floats.
|
|
101
101
|
f1 = Function(largest_exponent=2)
|
|
102
|
-
f1.
|
|
102
|
+
f1.set_coeffs([2, -3, -5])
|
|
103
103
|
|
|
104
104
|
print(f"Function f1: {f1}")
|
|
105
105
|
# > Function f1: 2x^2 - 3x - 5
|
|
@@ -121,7 +121,7 @@ print(f"2nd Derivative of f1: {ddf1}")
|
|
|
121
121
|
|
|
122
122
|
# 5. Find roots analytically using the quadratic formula
|
|
123
123
|
# This is exact and fast for degree-2 polynomials.
|
|
124
|
-
roots_analytic = quadratic_solve(
|
|
124
|
+
roots_analytic = f1.quadratic_solve()
|
|
125
125
|
print(f"Analytic roots: {sorted(roots_analytic)}")
|
|
126
126
|
# > Analytic roots: [-1.0, 2.5]
|
|
127
127
|
|
|
@@ -140,6 +140,32 @@ print(f"Approximate roots from GA: {roots_ga[:2]}")
|
|
|
140
140
|
|
|
141
141
|
---
|
|
142
142
|
|
|
143
|
+
## Tuning the Genetic Algorithm
|
|
144
|
+
|
|
145
|
+
The `GA_Options` class gives you fine-grained control over the genetic algorithm's performance, letting you trade speed for accuracy.
|
|
146
|
+
|
|
147
|
+
The default options are balanced, but for very complex polynomials, you may want a more exhaustive search.
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
from polysolve import GA_Options
|
|
151
|
+
|
|
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(
|
|
155
|
+
num_of_generations=50, # Run for more generations
|
|
156
|
+
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
|
+
|
|
161
|
+
# Pass the custom options to the solver
|
|
162
|
+
roots = f1.get_real_roots(ga_accurate)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
For a full breakdown of all parameters, including crossover_ratio, mutation_strength, and more, please see [the full GA_Options API Documentation](https://polysolve.jono-rams.work/docs/ga-options-api).
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
143
169
|
## Development & Testing Environment
|
|
144
170
|
|
|
145
171
|
This project is automatically tested against a specific set of dependencies to ensure stability. Our Continuous Integration (CI) pipeline runs on an environment using **CUDA 12.5** on **Ubuntu 24.04**.
|
|
@@ -164,7 +190,7 @@ Please read our `CONTRIBUTING.md` file for details on our code of conduct and th
|
|
|
164
190
|
<table>
|
|
165
191
|
<tbody>
|
|
166
192
|
<tr>
|
|
167
|
-
<td align="center" valign="top" width="14.28%"><a href="https://jono-rams.work"><img src="https://avatars.githubusercontent.com/u/29872001?v=4?s=100" width="100px;" alt="Jonathan Rampersad"/><br /><sub><b>Jonathan Rampersad</b></sub></a><br /><a href="https://github.com/jono-rams/PolySolve/commits?author=jono-rams" title="Code">💻</a> <a href="https://github.com/jono-rams/PolySolve/commits?author=jono-rams" title="Documentation">📖</a> <a href="#infra-jono-rams" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
|
193
|
+
<td align="center" valign="top" width="14.28%"><a href="https://jono-rams.work"><img src="https://avatars.githubusercontent.com/u/29872001?v=4?s=100" width="100px;" alt="Jonathan Rampersad"/><br /><sub><b>Jonathan Rampersad</b></sub></a><br /><a href="https://github.com/jono-rams/PolySolve/commits?author=jono-rams" title="Maintenance">🚧</a> <a href="https://github.com/jono-rams/PolySolve/commits?author=jono-rams" title="Code">💻</a> <a href="https://github.com/jono-rams/PolySolve/commits?author=jono-rams" title="Documentation">📖</a> <a href="#infra-jono-rams" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
|
168
194
|
</tr>
|
|
169
195
|
</tbody>
|
|
170
196
|
<tfoot>
|
|
@@ -8,7 +8,7 @@ try:
|
|
|
8
8
|
except ImportError:
|
|
9
9
|
_CUPY_AVAILABLE = False
|
|
10
10
|
|
|
11
|
-
from polysolve import Function, GA_Options
|
|
11
|
+
from polysolve import Function, GA_Options
|
|
12
12
|
|
|
13
13
|
@pytest.fixture
|
|
14
14
|
def quadratic_func() -> Function:
|
|
@@ -60,7 +60,7 @@ def test_nth_derivative(quadratic_func):
|
|
|
60
60
|
|
|
61
61
|
def test_quadratic_solve(quadratic_func):
|
|
62
62
|
"""Tests the analytical quadratic solver for exact roots."""
|
|
63
|
-
roots = quadratic_solve(
|
|
63
|
+
roots = quadratic_func.quadratic_solve()
|
|
64
64
|
# Sorting ensures consistent order for comparison
|
|
65
65
|
assert sorted(roots) == [-1.0, 2.5]
|
|
66
66
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|