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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polysolve
3
- Version: 0.3.2
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, 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.1"
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,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, 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)
277
295
  y_calculated = np.polyval(self.coefficients, solutions)
278
296
  error = y_calculated - y_val
279
297
 
280
- ranks = np.where(error == 0, np.finfo(float).max, np.abs(1.0 / error))
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
- # Keep only the top solutions
287
- top_solutions = solutions[:options.sample_size]
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
- # For the next generation, start with the mutated top solutions
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.mutation_percentage,
293
- 1 + options.mutation_percentage,
294
- options.sample_size
328
+ 1 - options.mutation_strength,
329
+ 1 + options.mutation_strength,
330
+ mutation_size
295
331
  )
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])
332
+ mutated_solutions = mutation_candidates * mutation_factors
303
333
 
304
- # Final sort of the best solutions from the last generation
305
- final_solutions = np.sort(solutions[:options.sample_size])
306
- return final_solutions
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
- # 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}")
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
- 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
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
- # 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
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
- d_solutions = cupy.concatenate([d_mutated, d_new_random])
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(d_solutions[:options.sample_size])
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.coefficients *= other
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(f: Function) -> Optional[List[float]]:
489
- """
490
- Calculates the real roots of a quadratic function using the quadratic formula.
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
- Args:
493
- f (Function): A Function object of degree 2.
598
+ Args:
599
+ f (Function): A Function object of degree 2.
494
600
 
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).")
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
- a, b, c = f.coefficients
608
+ a, b, c = self.coefficients
503
609
 
504
- discriminant = (b**2) - (4*a*c)
610
+ discriminant = (b**2) - (4*a*c)
505
611
 
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)
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
- return [root1, root2]
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.2
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, 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