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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polysolve
3
- Version: 0.3.2
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, quadratic_solve
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.set_constants([2, -3, -5])
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(f1)
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, quadratic_solve
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.set_constants([2, -3, -5])
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(f1)
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>
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  # --- Core Metadata ---
7
7
  name = "polysolve"
8
- version = "0.3.2"
8
+ version = "0.4.0"
9
9
  authors = [
10
10
  { name="Jonathan Rampersad", email="jonathan@jono-rams.work" },
11
11
  ]
@@ -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 lrgst_expo = num_coefficients - 1;
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 += coefficients[i] * pow(x_vals[idx], (double)(lrgst_expo - i));
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
- sample_size (int): The number of top solutions to keep and return.
76
- data_size (int): The total number of solutions generated in each generation.
77
- mutation_percentage (float): The amount by which top solutions are mutated each generation.
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
- mutation_percentage: float = 0.01
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.derivitive()
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, options.data_size)
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
- # Keep only the top solutions
287
- top_solutions = solutions[:options.sample_size]
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
- # For the next generation, start with the mutated top solutions
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.mutation_percentage,
293
- 1 + options.mutation_percentage,
294
- options.sample_size
327
+ 1 - options.mutation_strength,
328
+ 1 + options.mutation_strength,
329
+ mutation_size
295
330
  )
296
- mutated_solutions = top_solutions * mutation_factors
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
- # Final sort of the best solutions from the last generation
305
- final_solutions = np.sort(solutions[:options.sample_size])
306
- return final_solutions
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
- # Check the dtype of our coefficients array
312
- if self.coefficients.dtype == np.float64:
313
- fitness_gpu = cupy.RawKernel(_FITNESS_KERNEL_FLOAT, 'fitness_kernel')
314
- d_coefficients = cupy.array(self.coefficients, dtype=cupy.float64)
315
- elif self.coefficients.dtype == np.int64:
316
- fitness_gpu = cupy.RawKernel(_FITNESS_KERNEL_INT, 'fitness_kernel')
317
- d_coefficients = cupy.array(self.coefficients, dtype=cupy.int64)
318
- else:
319
- raise TypeError(f"Unsupported dtype for CUDA solver: {self.coefficients.dtype}")
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
- if i + 1 == options.num_of_generations:
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
- # Create new random solutions for the rest
355
- d_new_random = cupy.random.uniform(
356
- options.min_range, options.max_range, options.data_size - options.sample_size
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
- d_solutions = cupy.concatenate([d_mutated, d_new_random])
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(d_solutions[:options.sample_size])
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.coefficients *= other
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(f: Function) -> Optional[List[float]]:
489
- """
490
- Calculates the real roots of a quadratic function using the quadratic formula.
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
- Args:
493
- f (Function): A Function object of degree 2.
596
+ Args:
597
+ f (Function): A Function object of degree 2.
494
598
 
495
- Returns:
496
- Optional[List[float]]: A list containing the two real roots, or None if there are no real roots.
497
- """
498
- f._check_initialized()
499
- if f.largest_exponent != 2:
500
- raise ValueError("Input function must be quadratic (degree 2).")
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
- a, b, c = f.coefficients
606
+ a, b, c = self.coefficients
503
607
 
504
- discriminant = (b**2) - (4*a*c)
608
+ discriminant = (b**2) - (4*a*c)
505
609
 
506
- if discriminant < 0:
507
- return None # No real roots
508
-
509
- sqrt_discriminant = math.sqrt(discriminant)
510
- root1 = (-b + sqrt_discriminant) / (2 * a)
511
- root2 = (-b - sqrt_discriminant) / (2 * a)
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
- return [root1, root2]
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.2
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, quadratic_solve
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.set_constants([2, -3, -5])
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(f1)
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, quadratic_solve
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(quadratic_func)
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