polysolve 0.3.2__tar.gz → 0.4.1__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.1}/PKG-INFO +31 -5
- {polysolve-0.3.2 → polysolve-0.4.1}/README.md +30 -4
- {polysolve-0.3.2 → polysolve-0.4.1}/pyproject.toml +1 -1
- {polysolve-0.3.2 → polysolve-0.4.1}/src/polysolve/__init__.py +205 -99
- {polysolve-0.3.2 → polysolve-0.4.1/src/polysolve.egg-info}/PKG-INFO +31 -5
- {polysolve-0.3.2 → polysolve-0.4.1}/tests/test_polysolve.py +2 -2
- {polysolve-0.3.2 → polysolve-0.4.1}/LICENSE +0 -0
- {polysolve-0.3.2 → polysolve-0.4.1}/setup.cfg +0 -0
- {polysolve-0.3.2 → polysolve-0.4.1}/src/polysolve.egg-info/SOURCES.txt +0 -0
- {polysolve-0.3.2 → polysolve-0.4.1}/src/polysolve.egg-info/dependency_links.txt +0 -0
- {polysolve-0.3.2 → polysolve-0.4.1}/src/polysolve.egg-info/requires.txt +0 -0
- {polysolve-0.3.2 → polysolve-0.4.1}/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.1
|
|
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,54 +276,102 @@ 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)
|
|
277
295
|
y_calculated = np.polyval(self.coefficients, solutions)
|
|
278
296
|
error = y_calculated - y_val
|
|
279
297
|
|
|
280
|
-
|
|
298
|
+
with np.errstate(divide='ignore'):
|
|
299
|
+
ranks = np.where(error == 0, np.finfo(float).max, np.abs(1.0 / error))
|
|
281
300
|
|
|
282
301
|
# Sort solutions by fitness (descending)
|
|
283
302
|
sorted_indices = np.argsort(-ranks)
|
|
284
303
|
solutions = solutions[sorted_indices]
|
|
285
304
|
|
|
286
|
-
#
|
|
287
|
-
|
|
305
|
+
# --- Create the next generation ---
|
|
306
|
+
|
|
307
|
+
# 1. Elitism: Keep the best solutions as-is
|
|
308
|
+
elite_solutions = solutions[:elite_size]
|
|
309
|
+
|
|
310
|
+
# Define a "parent pool" of the top 50% of solutions to breed from
|
|
311
|
+
parent_pool = solutions[:data_size // 2]
|
|
312
|
+
|
|
313
|
+
# 2. Crossover: Breed two parents to create a child
|
|
314
|
+
# Select from the full list (indices 0 to data_size-1)
|
|
315
|
+
parent1_indices = np.random.randint(0, data_size, crossover_size)
|
|
316
|
+
parent2_indices = np.random.randint(0, data_size, crossover_size)
|
|
317
|
+
parents1 = solutions[parent1_indices]
|
|
318
|
+
parents2 = solutions[parent2_indices]
|
|
319
|
+
# Simple "average" crossover
|
|
320
|
+
crossover_solutions = (parents1 + parents2) / 2.0
|
|
321
|
+
|
|
322
|
+
# 3. Mutation:
|
|
323
|
+
# Select from the full list (indices 0 to data_size-1)
|
|
324
|
+
mutation_candidates = solutions[np.random.randint(0, data_size, mutation_size)]
|
|
288
325
|
|
|
289
|
-
#
|
|
290
|
-
# and fill the rest with new random values.
|
|
326
|
+
# Use mutation_strength (the new name)
|
|
291
327
|
mutation_factors = np.random.uniform(
|
|
292
|
-
1 - options.
|
|
293
|
-
1 + options.
|
|
294
|
-
|
|
328
|
+
1 - options.mutation_strength,
|
|
329
|
+
1 + options.mutation_strength,
|
|
330
|
+
mutation_size
|
|
295
331
|
)
|
|
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])
|
|
332
|
+
mutated_solutions = mutation_candidates * mutation_factors
|
|
303
333
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
334
|
+
# 4. New Randoms: Add new blood to prevent getting stuck
|
|
335
|
+
random_solutions = np.random.uniform(options.min_range, options.max_range, random_size)
|
|
336
|
+
|
|
337
|
+
# Assemble the new generation
|
|
338
|
+
solutions = np.concatenate([
|
|
339
|
+
elite_solutions,
|
|
340
|
+
crossover_solutions,
|
|
341
|
+
mutated_solutions,
|
|
342
|
+
random_solutions
|
|
343
|
+
])
|
|
344
|
+
|
|
345
|
+
# --- Final Step: Return the best results ---
|
|
346
|
+
# After all generations, do one last ranking to find the best solutions
|
|
347
|
+
y_calculated = np.polyval(self.coefficients, solutions)
|
|
348
|
+
error = y_calculated - y_val
|
|
349
|
+
with np.errstate(divide='ignore'):
|
|
350
|
+
ranks = np.where(error == 0, np.finfo(float).max, np.abs(1.0 / error))
|
|
351
|
+
sorted_indices = np.argsort(-ranks)
|
|
352
|
+
|
|
353
|
+
# Get the top 'sample_size' solutions the user asked for
|
|
354
|
+
best_solutions = solutions[sorted_indices][:options.sample_size]
|
|
355
|
+
|
|
356
|
+
return np.sort(best_solutions)
|
|
307
357
|
|
|
308
358
|
def _solve_x_cuda(self, y_val: float, options: GA_Options) -> np.ndarray:
|
|
309
359
|
"""Genetic algorithm implementation using CuPy (GPU/CUDA)."""
|
|
360
|
+
|
|
361
|
+
elite_ratio = options.elite_ratio
|
|
362
|
+
crossover_ratio = options.crossover_ratio
|
|
363
|
+
mutation_ratio = options.mutation_ratio
|
|
310
364
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
365
|
+
data_size = options.data_size
|
|
366
|
+
|
|
367
|
+
elite_size = int(data_size * elite_ratio)
|
|
368
|
+
crossover_size = int(data_size * crossover_ratio)
|
|
369
|
+
mutation_size = int(data_size * mutation_ratio)
|
|
370
|
+
random_size = data_size - elite_size - crossover_size - mutation_size
|
|
371
|
+
|
|
372
|
+
# ALWAYS cast coefficients to float64 for the kernel.
|
|
373
|
+
fitness_gpu = cupy.RawKernel(_FITNESS_KERNEL_FLOAT, 'fitness_kernel')
|
|
374
|
+
d_coefficients = cupy.array(self.coefficients, dtype=cupy.float64)
|
|
320
375
|
|
|
321
376
|
# Create initial random solutions on the GPU
|
|
322
377
|
d_solutions = cupy.random.uniform(
|
|
@@ -339,27 +394,62 @@ class Function:
|
|
|
339
394
|
sorted_indices = cupy.argsort(-d_ranks)
|
|
340
395
|
d_solutions = d_solutions[sorted_indices]
|
|
341
396
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
397
|
+
# --- Create the next generation ---
|
|
398
|
+
|
|
399
|
+
# 1. Elitism
|
|
400
|
+
d_elite_solutions = d_solutions[:elite_size]
|
|
401
|
+
|
|
402
|
+
# Define a "parent pool" of the top 50% of solutions to breed from
|
|
403
|
+
d_parent_pool = d_solutions[:data_size // 2]
|
|
404
|
+
parent_pool_size = d_parent_pool.size
|
|
405
|
+
|
|
406
|
+
# 2. Crossover
|
|
407
|
+
# Select from the full list (indices 0 to data_size-1)
|
|
408
|
+
parent1_indices = cupy.random.randint(0, data_size, crossover_size)
|
|
409
|
+
parent2_indices = cupy.random.randint(0, data_size, crossover_size)
|
|
410
|
+
d_parents1 = d_solutions[parent1_indices]
|
|
411
|
+
d_parents2 = d_solutions[parent2_indices]
|
|
412
|
+
d_crossover_solutions = (d_parents1 + d_parents2) / 2.0
|
|
413
|
+
|
|
414
|
+
# 3. Mutation
|
|
415
|
+
# Select from the full list (indices 0 to data_size-1)
|
|
416
|
+
mutation_indices = cupy.random.randint(0, data_size, mutation_size)
|
|
417
|
+
d_mutation_candidates = d_solutions[mutation_indices]
|
|
353
418
|
|
|
354
|
-
#
|
|
355
|
-
|
|
356
|
-
|
|
419
|
+
# Use mutation_strength (the new name)
|
|
420
|
+
d_mutation_factors = cupy.random.uniform(
|
|
421
|
+
1 - options.mutation_strength,
|
|
422
|
+
1 + options.mutation_strength,
|
|
423
|
+
mutation_size
|
|
424
|
+
)
|
|
425
|
+
d_mutated_solutions = d_mutation_candidates * d_mutation_factors
|
|
426
|
+
|
|
427
|
+
# 4. New Randoms
|
|
428
|
+
d_random_solutions = cupy.random.uniform(
|
|
429
|
+
options.min_range, options.max_range, random_size, dtype=cupy.float64
|
|
357
430
|
)
|
|
358
431
|
|
|
359
|
-
|
|
432
|
+
# Assemble the new generation
|
|
433
|
+
d_solutions = cupy.concatenate([
|
|
434
|
+
d_elite_solutions,
|
|
435
|
+
d_crossover_solutions,
|
|
436
|
+
d_mutated_solutions,
|
|
437
|
+
d_random_solutions
|
|
438
|
+
])
|
|
439
|
+
|
|
440
|
+
# --- Final Step: Return the best results ---
|
|
441
|
+
# After all generations, do one last ranking to find the best solutions
|
|
442
|
+
fitness_gpu(
|
|
443
|
+
(blocks_per_grid,), (threads_per_block,),
|
|
444
|
+
(d_coefficients, d_coefficients.size, d_solutions, d_ranks, d_solutions.size, y_val)
|
|
445
|
+
)
|
|
446
|
+
sorted_indices = cupy.argsort(-d_ranks)
|
|
447
|
+
|
|
448
|
+
# Get the top 'sample_size' solutions
|
|
449
|
+
d_best_solutions = d_solutions[sorted_indices][:options.sample_size]
|
|
360
450
|
|
|
361
451
|
# Get the final sample, sort it, and copy back to CPU
|
|
362
|
-
final_solutions_gpu = cupy.sort(
|
|
452
|
+
final_solutions_gpu = cupy.sort(d_best_solutions)
|
|
363
453
|
return final_solutions_gpu.get()
|
|
364
454
|
|
|
365
455
|
|
|
@@ -481,36 +571,52 @@ class Function:
|
|
|
481
571
|
def __imul__(self, other: Union['Function', int, float]) -> 'Function':
|
|
482
572
|
"""Performs in-place multiplication by a scalar (func *= 3)."""
|
|
483
573
|
|
|
484
|
-
self.
|
|
574
|
+
self._check_initialized()
|
|
575
|
+
|
|
576
|
+
if isinstance(other, (int, float)):
|
|
577
|
+
if other == 0:
|
|
578
|
+
self.coefficients = np.array([0], dtype=self.coefficients.dtype)
|
|
579
|
+
self._largest_exponent = 0
|
|
580
|
+
else:
|
|
581
|
+
self.coefficients *= other
|
|
582
|
+
|
|
583
|
+
elif isinstance(other, self.__class__):
|
|
584
|
+
other._check_initialized()
|
|
585
|
+
self.coefficients = np.polymul(self.coefficients, other.coefficients)
|
|
586
|
+
self._largest_exponent = len(self.coefficients) - 1
|
|
587
|
+
|
|
588
|
+
else:
|
|
589
|
+
return NotImplemented
|
|
590
|
+
|
|
485
591
|
return self
|
|
486
592
|
|
|
487
593
|
|
|
488
|
-
def quadratic_solve(
|
|
489
|
-
|
|
490
|
-
|
|
594
|
+
def quadratic_solve(self) -> Optional[List[float]]:
|
|
595
|
+
"""
|
|
596
|
+
Calculates the real roots of a quadratic function using the quadratic formula.
|
|
491
597
|
|
|
492
|
-
|
|
493
|
-
|
|
598
|
+
Args:
|
|
599
|
+
f (Function): A Function object of degree 2.
|
|
494
600
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
601
|
+
Returns:
|
|
602
|
+
Optional[List[float]]: A list containing the two real roots, or None if there are no real roots.
|
|
603
|
+
"""
|
|
604
|
+
self._check_initialized()
|
|
605
|
+
if self.largest_exponent != 2:
|
|
606
|
+
raise ValueError("Input function must be quadratic (degree 2) to use quadratic_solve.")
|
|
501
607
|
|
|
502
|
-
|
|
608
|
+
a, b, c = self.coefficients
|
|
503
609
|
|
|
504
|
-
|
|
610
|
+
discriminant = (b**2) - (4*a*c)
|
|
505
611
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
612
|
+
if discriminant < 0:
|
|
613
|
+
return None # No real roots
|
|
614
|
+
|
|
615
|
+
sqrt_discriminant = math.sqrt(discriminant)
|
|
616
|
+
root1 = (-b + sqrt_discriminant) / (2 * a)
|
|
617
|
+
root2 = (-b - sqrt_discriminant) / (2 * a)
|
|
512
618
|
|
|
513
|
-
|
|
619
|
+
return [root1, root2]
|
|
514
620
|
|
|
515
621
|
# Example Usage
|
|
516
622
|
if __name__ == '__main__':
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: polysolve
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
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
|