Compare commits
23 Commits
3aad9efb61
...
v0.7.0
| Author | SHA1 | Date | |
|---|---|---|---|
| b87ea5011c | |||
| 117e43a984 | |||
| b415df2983 | |||
| 602269889b | |||
| dca1d66346 | |||
| 1aa2e8875a | |||
| 94723dcb88 | |||
| f4c5d245e4 | |||
| b7ea6c2e23 | |||
| 9d967210fa | |||
| 1318006959 | |||
| 2d8c8a09e3 | |||
| 4e46c11f83 | |||
| 962eab5af7 | |||
| 7c75000637 | |||
| c3b3513e79 | |||
| 0536003dce | |||
| bb89149930 | |||
| 6596c2df99 | |||
| 24337cea48 | |||
| ee18cc9e59 | |||
| ce464cffd4 | |||
| c94d08498d |
@@ -19,6 +19,7 @@
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/29872001?v=4",
|
||||
"profile": "https://jono-rams.work",
|
||||
"contributions": [
|
||||
"maintenance",
|
||||
"code",
|
||||
"doc",
|
||||
"infra"
|
||||
|
||||
BIN
PolySolve_Technical_Paper.pdf
Normal file
BIN
PolySolve_Technical_Paper.pdf
Normal file
Binary file not shown.
59
README.md
59
README.md
@@ -1,17 +1,20 @@
|
||||
# polysolve
|
||||
<p align="center">
|
||||
<img src="https://i.ibb.co/N22Gx6xq/Poly-Solve-Logo.png" alt="polysolve Logo" width="256">
|
||||
</p>
|
||||
|
||||
[](https://pypi.org/project/polysolve/)
|
||||
[](https://pypi.org/project/polysolve/)
|
||||
|
||||
A Python library for representing, manipulating, and solving polynomial equations using a high-performance genetic algorithm, with optional CUDA/GPU acceleration.
|
||||
A Python library for representing, manipulating, and solving polynomial equations. Features a high-performance, Numba-accelerated genetic algorithm for CPU, with an optional CUDA/GPU backend for massive-scale parallel solving.
|
||||
|
||||
---
|
||||
|
||||
## Key Features
|
||||
|
||||
* **Create and Manipulate Polynomials**: Easily define polynomials of any degree and perform arithmetic operations like addition, subtraction, and scaling.
|
||||
* **Genetic Algorithm Solver**: Find approximate real roots for complex polynomials where analytical solutions are difficult or impossible.
|
||||
* **Numerically Stable Solver**: Makes complex calculations **practical**. Leverage your GPU to power the robust genetic algorithm, solving high-degree polynomials accurately in a reasonable timeframe.
|
||||
* **Numba Accelerated CPU Solver**: The default genetic algorithm is JIT-compiled with Numba for high-speed CPU performance, right out of the box.
|
||||
* **CUDA Accelerated**: Leverage NVIDIA GPUs for a massive performance boost when finding roots in large solution spaces.
|
||||
* **Create and Manipulate Polynomials**: Easily define polynomials of any degree using integer or float coefficients, and perform arithmetic operations like addition, subtraction, multiplication, and scaling.
|
||||
* **Analytical Solvers**: Includes standard, exact solvers for simple cases (e.g., `quadratic_solve`).
|
||||
* **Simple API**: Designed to be intuitive and easy to integrate into any project.
|
||||
|
||||
@@ -41,11 +44,12 @@ pip install polysolve[cuda12]
|
||||
Here is a simple example of how to define a quadratic function, find its properties, and solve for its roots.
|
||||
|
||||
```python
|
||||
from polysolve import Function, GA_Options, quadratic_solve
|
||||
from polysolve import Function, GA_Options
|
||||
|
||||
# 1. Define the function f(x) = 2x^2 - 3x - 5
|
||||
# Coefficients can be integers or floats.
|
||||
f1 = Function(largest_exponent=2)
|
||||
f1.set_constants([2, -3, -5])
|
||||
f1.set_coeffs([2, -3, -5])
|
||||
|
||||
print(f"Function f1: {f1}")
|
||||
# > Function f1: 2x^2 - 3x - 5
|
||||
@@ -67,12 +71,12 @@ print(f"2nd Derivative of f1: {ddf1}")
|
||||
|
||||
# 5. Find roots analytically using the quadratic formula
|
||||
# This is exact and fast for degree-2 polynomials.
|
||||
roots_analytic = quadratic_solve(f1)
|
||||
roots_analytic = f1.quadratic_solve()
|
||||
print(f"Analytic roots: {sorted(roots_analytic)}")
|
||||
# > Analytic roots: [-1.0, 2.5]
|
||||
|
||||
# 6. Find roots with the genetic algorithm (CPU)
|
||||
# This can solve polynomials of any degree.
|
||||
# 6. Find roots with the genetic algorithm (Numba CPU)
|
||||
# This is the default, JIT-compiled CPU solver.
|
||||
ga_opts = GA_Options(num_of_generations=20)
|
||||
roots_ga = f1.get_real_roots(ga_opts, use_cuda=False)
|
||||
print(f"Approximate roots from GA: {roots_ga[:2]}")
|
||||
@@ -86,6 +90,41 @@ print(f"Approximate roots from GA: {roots_ga[:2]}")
|
||||
|
||||
---
|
||||
|
||||
## Tuning the Genetic Algorithm
|
||||
|
||||
The `GA_Options` class gives you fine-grained control over the genetic algorithm's performance, letting you trade speed for accuracy.
|
||||
|
||||
The default options are balanced, but for very complex polynomials, you may want a more exhaustive search.
|
||||
|
||||
```python
|
||||
from polysolve import GA_Options
|
||||
|
||||
# Create a config for a deep search, optimized for finding
|
||||
# *all* real roots (even if they are far apart).
|
||||
ga_robust_search = GA_Options(
|
||||
num_of_generations=50, # Run for more generations
|
||||
data_size=500000, # Use a larger population
|
||||
|
||||
# --- Key Tuning Parameters for Multi-Root Finding ---
|
||||
|
||||
# Widen the parent pool to 75% to keep more "niches"
|
||||
# (solution-clouds around different roots) alive.
|
||||
selection_percentile=0.75,
|
||||
|
||||
# Increase the crossover blend factor to 0.75.
|
||||
# This allows new solutions to be created further
|
||||
# away from their parents, increasing exploration.
|
||||
blend_alpha=0.75
|
||||
)
|
||||
|
||||
# Pass the custom options to the solver
|
||||
roots = f1.get_real_roots(ga_accurate)
|
||||
```
|
||||
|
||||
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).
|
||||
|
||||
---
|
||||
|
||||
## Development & Testing Environment
|
||||
|
||||
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**.
|
||||
@@ -110,7 +149,7 @@ Please read our `CONTRIBUTING.md` file for details on our code of conduct and th
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<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>
|
||||
<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>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
|
||||
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
|
||||
[project]
|
||||
# --- Core Metadata ---
|
||||
name = "polysolve"
|
||||
version = "0.2.1"
|
||||
version = "0.7.0"
|
||||
authors = [
|
||||
{ name="Jonathan Rampersad", email="jonathan@jono-rams.work" },
|
||||
]
|
||||
@@ -33,7 +33,8 @@ classifiers = [
|
||||
|
||||
# --- Dependencies ---
|
||||
dependencies = [
|
||||
"numpy>=1.21"
|
||||
"numpy>=1.21",
|
||||
"numba"
|
||||
]
|
||||
|
||||
# --- Optional Dependencies (Extras) ---
|
||||
@@ -42,6 +43,7 @@ cuda12 = ["cupy-cuda12x"]
|
||||
dev = ["pytest"]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/jono-rams/PolySolve"
|
||||
"Source Code" = "https://github.com/jono-rams/PolySolve"
|
||||
Homepage = "https://polysolve.jono-rams.work"
|
||||
Documentation = "https://polysolve.jono-rams.work/docs"
|
||||
Repository = "https://github.com/jono-rams/PolySolve"
|
||||
"Bug Tracker" = "https://github.com/jono-rams/PolySolve/issues"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
import pytest
|
||||
import numpy as np
|
||||
import numpy.testing as npt
|
||||
|
||||
# Try to import cupy to check for CUDA availability
|
||||
try:
|
||||
@@ -8,7 +9,7 @@ try:
|
||||
except ImportError:
|
||||
_CUPY_AVAILABLE = False
|
||||
|
||||
from polysolve import Function, GA_Options, quadratic_solve
|
||||
from polysolve import Function, GA_Options
|
||||
|
||||
@pytest.fixture
|
||||
def quadratic_func() -> Function:
|
||||
@@ -36,6 +37,17 @@ def m_func_2() -> Function:
|
||||
f.set_coeffs([5, -4])
|
||||
return f
|
||||
|
||||
@pytest.fixture
|
||||
def base_func():
|
||||
f = Function(2)
|
||||
f.set_coeffs([1, 2, 3])
|
||||
return f
|
||||
|
||||
@pytest.fixture
|
||||
def complex_func():
|
||||
f = Function(2, [1, 2, 2])
|
||||
return f
|
||||
|
||||
# --- Core Functionality Tests ---
|
||||
|
||||
def test_solve_y(quadratic_func):
|
||||
@@ -60,7 +72,7 @@ def test_nth_derivative(quadratic_func):
|
||||
|
||||
def test_quadratic_solve(quadratic_func):
|
||||
"""Tests the analytical quadratic solver for exact roots."""
|
||||
roots = quadratic_solve(quadratic_func)
|
||||
roots = quadratic_func.quadratic_solve()
|
||||
# Sorting ensures consistent order for comparison
|
||||
assert sorted(roots) == [-1.0, 2.5]
|
||||
|
||||
@@ -94,6 +106,32 @@ def test_function_multiplication(m_func_1, m_func_2):
|
||||
assert result.largest_exponent == 3
|
||||
assert np.array_equal(result.coefficients, [10, 7, -7, -4])
|
||||
|
||||
def test_equality(base_func):
|
||||
"""Tests the __eq__ method for the Function class."""
|
||||
|
||||
# 1. Test for equality with a new, identical object
|
||||
f_identical = Function(2)
|
||||
f_identical.set_coeffs([1, 2, 3])
|
||||
assert base_func == f_identical
|
||||
|
||||
# 2. Test for inequality (different coefficients)
|
||||
f_different = Function(2)
|
||||
f_different.set_coeffs([1, 9, 3])
|
||||
assert base_func != f_different
|
||||
|
||||
# 3. Test for inequality (different degree)
|
||||
f_diff_degree = Function(1)
|
||||
f_diff_degree.set_coeffs([1, 2])
|
||||
assert base_func != f_diff_degree
|
||||
|
||||
# 4. Test against a different type
|
||||
assert base_func != "some_string"
|
||||
assert base_func != 123
|
||||
|
||||
# 5. Test against an uninitialized Function
|
||||
f_uninitialized = Function(2)
|
||||
assert base_func != f_uninitialized
|
||||
|
||||
# --- Genetic Algorithm Root-Finding Tests ---
|
||||
|
||||
def test_get_real_roots_numpy(quadratic_func):
|
||||
@@ -101,7 +139,7 @@ def test_get_real_roots_numpy(quadratic_func):
|
||||
Tests that the NumPy-based genetic algorithm approximates the roots correctly.
|
||||
"""
|
||||
# Using more generations for higher accuracy in testing
|
||||
ga_opts = GA_Options(num_of_generations=25, data_size=50000)
|
||||
ga_opts = GA_Options(num_of_generations=50, data_size=200000, selection_percentile=0.66, root_precision=3)
|
||||
|
||||
roots = quadratic_func.get_real_roots(ga_opts, use_cuda=False)
|
||||
|
||||
@@ -109,11 +147,7 @@ def test_get_real_roots_numpy(quadratic_func):
|
||||
# We don't know which order they'll be in, so we check for presence.
|
||||
expected_roots = np.array([-1.0, 2.5])
|
||||
|
||||
# Check that at least one found root is close to -1.0
|
||||
assert np.any(np.isclose(roots, expected_roots[0], atol=1e-2))
|
||||
|
||||
# Check that at least one found root is close to 2.5
|
||||
assert np.any(np.isclose(roots, expected_roots[1], atol=1e-2))
|
||||
npt.assert_allclose(np.sort(roots), np.sort(expected_roots), atol=1e-2)
|
||||
|
||||
|
||||
@pytest.mark.skipif(not _CUPY_AVAILABLE, reason="CuPy is not installed, skipping CUDA test.")
|
||||
@@ -124,13 +158,45 @@ def test_get_real_roots_cuda(quadratic_func):
|
||||
It will be skipped automatically if CuPy is not available.
|
||||
"""
|
||||
|
||||
ga_opts = GA_Options(num_of_generations=25, data_size=50000)
|
||||
ga_opts = GA_Options(num_of_generations=50, data_size=200000, selection_percentile=0.66, root_precision=3)
|
||||
|
||||
roots = quadratic_func.get_real_roots(ga_opts, use_cuda=True)
|
||||
|
||||
expected_roots = np.array([-1.0, 2.5])
|
||||
|
||||
# Verify that the CUDA implementation also finds the correct roots within tolerance.
|
||||
assert np.any(np.isclose(roots, expected_roots[0], atol=1e-2))
|
||||
assert np.any(np.isclose(roots, expected_roots[1], atol=1e-2))
|
||||
npt.assert_allclose(np.sort(roots), np.sort(expected_roots), atol=1e-2)
|
||||
|
||||
def test_get_roots_numpy(complex_func):
|
||||
"""
|
||||
Tests that the NumPy-based genetic algorithm approximates the roots correctly.
|
||||
"""
|
||||
# Using more generations for higher accuracy in testing
|
||||
ga_opts = GA_Options(num_of_generations=50, data_size=200000, selection_percentile=0.66, root_precision=3)
|
||||
|
||||
roots = complex_func.get_roots(ga_opts, use_cuda=False)
|
||||
|
||||
# Check if the algorithm found values close to the two known roots.
|
||||
# We don't know which order they'll be in, so we check for presence.
|
||||
expected_roots = np.array([-1.0-1.j, -1.0+1.j])
|
||||
|
||||
npt.assert_allclose(np.sort(roots), np.sort(expected_roots), atol=1e-2)
|
||||
|
||||
|
||||
@pytest.mark.skipif(not _CUPY_AVAILABLE, reason="CuPy is not installed, skipping CUDA test.")
|
||||
def test_get_roots_cuda(complex_func):
|
||||
"""
|
||||
Tests that the CUDA-based genetic algorithm approximates the roots correctly.
|
||||
This test implicitly verifies that the CUDA kernel is functioning.
|
||||
It will be skipped automatically if CuPy is not available.
|
||||
"""
|
||||
|
||||
ga_opts = GA_Options(num_of_generations=50, data_size=200000, selection_percentile=0.66, root_precision=3)
|
||||
|
||||
roots = complex_func.get_roots(ga_opts, use_cuda=True)
|
||||
|
||||
expected_roots = np.array([-1.0-1.j, -1+1.j])
|
||||
|
||||
# Verify that the CUDA implementation also finds the correct roots within tolerance.
|
||||
npt.assert_allclose(np.sort(roots), np.sort(expected_roots), atol=1e-2)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user