Initial Commit
This commit is contained in:
32
.gitea/workflows/test.yml
Normal file
32
.gitea/workflows/test.yml
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
name: Run Python Tests
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: nvidia-cuda
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: ["3.8", "3.10", "3.12"]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
|
- name: Check for GPU
|
||||||
|
run: |
|
||||||
|
echo "Checking for NVIDIA GPU..."
|
||||||
|
nvidia-smi
|
||||||
|
|
||||||
|
- name: Install dependencies with CUDA
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install .[dev,cuda12]
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: |
|
||||||
|
pytest
|
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
.Python
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
.venv/
|
||||||
|
|
||||||
|
# Build artifacts
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
*.egg-info/
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# IDE / Editor specific
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
76
CODE_OF_CONDUCT.md
Normal file
76
CODE_OF_CONDUCT.md
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that are considerate, respectful, and collaborative.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment for our community include:
|
||||||
|
|
||||||
|
* Demonstrating empathy and kindness toward other people
|
||||||
|
* Being respectful of differing opinions, viewpoints, and experiences
|
||||||
|
* Giving and gracefully accepting constructive feedback
|
||||||
|
* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
|
||||||
|
* Focusing on what is best not just for us as individuals, but for the overall community
|
||||||
|
|
||||||
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery, and sexual attention or advances of any kind
|
||||||
|
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or email address, without their explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Community leaders are responsible for clarifying and enforcing our standards and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
|
||||||
|
|
||||||
|
Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at **jonathan@jono-rams.work**. All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
|
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series of actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interaction in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within the community.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
84
CONTRIBUTING.md
Normal file
84
CONTRIBUTING.md
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
# Contributing to polysolve
|
||||||
|
|
||||||
|
First off, thank you for considering contributing! We welcome any contributions, from fixing a typo to implementing a new feature.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
* [Reporting Bugs](#reporting-bugs)
|
||||||
|
* [Suggesting Enhancements](#suggesting-enhancements)
|
||||||
|
* [Setting Up the Development Environment](#setting-up-the-development-environment)
|
||||||
|
* [Running Tests](#running-tests)
|
||||||
|
* [Pull Request Process](#pull-request-process)
|
||||||
|
|
||||||
|
## Reporting Bugs
|
||||||
|
|
||||||
|
If you find a bug, please open an issue on our Gitea issue tracker. Please include as many details as possible, such as your OS, Python version, steps to reproduce, and any error messages.
|
||||||
|
|
||||||
|
## Suggesting Enhancements
|
||||||
|
|
||||||
|
If you have an idea for a new feature or an improvement, please open an issue to discuss it. This allows us to coordinate efforts and ensure the proposed change aligns with the project's goals.
|
||||||
|
|
||||||
|
## Setting Up the Development Environment
|
||||||
|
|
||||||
|
1. **Fork the repository** on Gitea.
|
||||||
|
|
||||||
|
2. **Clone your fork** locally:
|
||||||
|
```bash
|
||||||
|
git clone [https://gitea.example.com/YourUsername/PolySolve.git](https://gitea.example.com/YourUsername/PolySolve.git)
|
||||||
|
cd PolySolve
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Create and activate a virtual environment:**
|
||||||
|
```bash
|
||||||
|
# For Unix/macOS
|
||||||
|
python3 -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
|
||||||
|
# For Windows
|
||||||
|
python -m venv .venv
|
||||||
|
.venv\Scripts\activate
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Install the project in editable mode with development dependencies.** This command installs `polysolve` itself, plus `pytest` for testing.
|
||||||
|
```bash
|
||||||
|
pip install -e '.[dev]'
|
||||||
|
```
|
||||||
|
|
||||||
|
5. If you need to work on the CUDA-specific features and test them, install the appropriate CuPy extra alongside the `dev` dependencies:
|
||||||
|
```bash
|
||||||
|
# For a development setup with CUDA 12.x
|
||||||
|
pip install -e '.[dev,cuda12]'
|
||||||
|
|
||||||
|
# For a development setup with CUDA 11.x
|
||||||
|
pip install -e '.[dev,cuda11]'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
|
||||||
|
We use `pytest` for automated testing. After setting up the development environment, you can run the full test suite with a single command from the root of the repository:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pytest
|
||||||
|
```
|
||||||
|
|
||||||
|
This will automatically discover and run all tests located in the `tests/` directory.
|
||||||
|
|
||||||
|
All tests should pass before you submit your changes. If you are adding a new feature or fixing a bug, please add a corresponding test case to ensure the code is working correctly and to prevent future regressions.
|
||||||
|
|
||||||
|
## Pull Request Process
|
||||||
|
|
||||||
|
1. Create a new branch for your feature or bugfix from the `main` branch:
|
||||||
|
```bash
|
||||||
|
git checkout -b your-feature-name
|
||||||
|
```
|
||||||
|
2. Make your changes to the code in the `src/` directory.
|
||||||
|
3. Add or update tests in the `tests/` directory to cover your changes.
|
||||||
|
4. Run the test suite to ensure everything passes:
|
||||||
|
```bash
|
||||||
|
pytest
|
||||||
|
```
|
||||||
|
5. Commit your changes with a clear and descriptive commit message.
|
||||||
|
6. Push your branch to your fork on Gitea.
|
||||||
|
7. Open a pull request to the `main` branch of the original `PolySolve` repository. Please provide a clear title and description for your pull request.
|
||||||
|
|
||||||
|
Once you submit your pull request, our automated CI tests will run. We will review your contribution and provide feedback as soon as possible. Thank you for your contribution!
|
98
README.md
98
README.md
@ -1,3 +1,97 @@
|
|||||||
# PolySolve
|
# polysolve
|
||||||
|
|
||||||
A Python polynomial solver using a genetic algorithm with optional CUDA/GPU acceleration.
|
[](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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
* **CUDA Accelerated**: Leverage NVIDIA GPUs for a massive performance boost when finding roots in large solution spaces.
|
||||||
|
* **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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Install the base package from PyPI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install polysolve
|
||||||
|
```
|
||||||
|
|
||||||
|
### CUDA Acceleration
|
||||||
|
|
||||||
|
To enable GPU acceleration, install the extra that matches your installed NVIDIA CUDA Toolkit version. This provides a significant speedup for the genetic algorithm.
|
||||||
|
|
||||||
|
**For CUDA 12.x users:**
|
||||||
|
```bash
|
||||||
|
pip install polysolve[cuda12]
|
||||||
|
```
|
||||||
|
|
||||||
|
**For CUDA 11.x users:**
|
||||||
|
```bash
|
||||||
|
pip install polysolve[cuda11]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# 1. Define the function f(x) = 2x^2 - 3x - 5
|
||||||
|
f1 = Function(largest_exponent=2)
|
||||||
|
f1.set_constants([2, -3, -5])
|
||||||
|
|
||||||
|
print(f"Function f1: {f1}")
|
||||||
|
# > Function f1: 2x^2 - 3x - 5
|
||||||
|
|
||||||
|
# 2. Solve for y at a given x
|
||||||
|
y_val = f1.solve_y(5)
|
||||||
|
print(f"Value of f1 at x=5 is: {y_val}")
|
||||||
|
# > Value of f1 at x=5 is: 30.0
|
||||||
|
|
||||||
|
# 3. Get the derivative: 4x - 3
|
||||||
|
df1 = f1.differential()
|
||||||
|
print(f"Derivative of f1: {df1}")
|
||||||
|
# > Derivative of f1: 4x - 3
|
||||||
|
|
||||||
|
# 4. Find roots analytically using the quadratic formula
|
||||||
|
# This is exact and fast for degree-2 polynomials.
|
||||||
|
roots_analytic = quadratic_solve(f1)
|
||||||
|
print(f"Analytic roots: {sorted(roots_analytic)}")
|
||||||
|
# > Analytic roots: [-1.0, 2.5]
|
||||||
|
|
||||||
|
# 5. Find roots with the genetic algorithm (CPU)
|
||||||
|
# This can solve polynomials of any degree.
|
||||||
|
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]}")
|
||||||
|
# > Approximate roots from GA: [-1.000..., 2.500...]
|
||||||
|
|
||||||
|
# If you installed a CUDA extra, you can run it on the GPU:
|
||||||
|
# roots_ga_gpu = f1.get_real_roots(ga_opts, use_cuda=True)
|
||||||
|
# print(f"Approximate roots from GA (GPU): {roots_ga_gpu[:2]}")
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions are welcome! Whether it's a bug report, a feature request, or a pull request, please feel free to get involved.
|
||||||
|
|
||||||
|
Please read our `CONTRIBUTING.md` file for details on our code of conduct and the process for submitting pull requests.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License - see the `LICENSE` file for details.
|
||||||
|
47
pyproject.toml
Normal file
47
pyproject.toml
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=61.0"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
# --- Core Metadata ---
|
||||||
|
name = "polysolve"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = [
|
||||||
|
{ name="Jonathan Rampersad", email="jonathan@jono-rams.work" },
|
||||||
|
]
|
||||||
|
description = "A Python library for representing, manipulating, and solving exponential functions using analytical methods and genetic algorithms, with optional CUDA acceleration."
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.8"
|
||||||
|
license = { file="LICENSE" }
|
||||||
|
keywords = ["math", "polynomial", "genetic algorithm", "cuda", "equation solver"]
|
||||||
|
|
||||||
|
# --- Classifiers ---
|
||||||
|
classifiers = [
|
||||||
|
"Development Status :: 4 - Beta",
|
||||||
|
"Intended Audience :: Developers",
|
||||||
|
"Intended Audience :: Science/Research",
|
||||||
|
"License :: OSI Approved :: MIT License",
|
||||||
|
"Operating System :: OS Independent",
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.8",
|
||||||
|
"Programming Language :: Python :: 3.9",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
|
"Programming Language :: Python :: 3.11",
|
||||||
|
"Programming Language :: Python :: 3.12",
|
||||||
|
"Topic :: Scientific/Engineering :: Mathematics",
|
||||||
|
]
|
||||||
|
|
||||||
|
# --- Dependencies ---
|
||||||
|
dependencies = [
|
||||||
|
"numpy>=1.21"
|
||||||
|
]
|
||||||
|
|
||||||
|
# --- Optional Dependencies (Extras) ---
|
||||||
|
[project.optional-dependencies]
|
||||||
|
cuda11 = ["cupy-cuda11x"]
|
||||||
|
cuda12 = ["cupy-cuda12x"]
|
||||||
|
dev = ["pytest"]
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Homepage = "https://gitea.jono-rams.work/jono/PolySolve"
|
||||||
|
"Bug Tracker" = "https://gitea.jono-rams.work/jono/PolySolve/issues"
|
451
src/polysolve/__init__.py
Normal file
451
src/polysolve/__init__.py
Normal file
@ -0,0 +1,451 @@
|
|||||||
|
import math
|
||||||
|
import numpy as np
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import List, Optional
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
# Attempt to import CuPy for CUDA acceleration.
|
||||||
|
# If CuPy is not installed, the CUDA functionality will not be available.
|
||||||
|
try:
|
||||||
|
import cupy
|
||||||
|
_CUPY_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
_CUPY_AVAILABLE = False
|
||||||
|
|
||||||
|
# The CUDA kernel for the fitness function
|
||||||
|
_FITNESS_KERNEL = """
|
||||||
|
extern "C" __global__ void fitness_kernel(
|
||||||
|
const long long* coefficients,
|
||||||
|
int num_coefficients,
|
||||||
|
const double* x_vals,
|
||||||
|
double* ranks,
|
||||||
|
int size,
|
||||||
|
double y_val)
|
||||||
|
{
|
||||||
|
int idx = threadIdx.x + blockIdx.x * blockDim.x;
|
||||||
|
if (idx < size)
|
||||||
|
{
|
||||||
|
double ans = 0;
|
||||||
|
int lrgst_expo = num_coefficients - 1;
|
||||||
|
for (int i = 0; i < num_coefficients; ++i)
|
||||||
|
{
|
||||||
|
ans += coefficients[i] * pow(x_vals[idx], (double)(lrgst_expo - i));
|
||||||
|
}
|
||||||
|
|
||||||
|
ans -= y_val;
|
||||||
|
ranks[idx] = (ans == 0) ? 1.7976931348623157e+308 : fabs(1.0 / ans);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class GA_Options:
|
||||||
|
"""
|
||||||
|
Configuration options for the genetic algorithm used to find function roots.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
min_range (float): The minimum value for the initial random solutions.
|
||||||
|
max_range (float): The maximum value for the initial random solutions.
|
||||||
|
num_of_generations (int): The number of iterations the algorithm will run.
|
||||||
|
sample_size (int): The number of top solutions to keep and return.
|
||||||
|
data_size (int): The total number of solutions generated in each generation.
|
||||||
|
mutation_percentage (float): The amount by which top solutions are mutated each generation.
|
||||||
|
"""
|
||||||
|
min_range: float = -100.0
|
||||||
|
max_range: float = 100.0
|
||||||
|
num_of_generations: int = 10
|
||||||
|
sample_size: int = 1000
|
||||||
|
data_size: int = 100000
|
||||||
|
mutation_percentage: float = 0.01
|
||||||
|
|
||||||
|
class Function:
|
||||||
|
"""
|
||||||
|
Represents an exponential function (polynomial) of the form:
|
||||||
|
c_0*x^n + c_1*x^(n-1) + ... + c_n
|
||||||
|
"""
|
||||||
|
def __init__(self, largest_exponent: int):
|
||||||
|
"""
|
||||||
|
Initializes a function with its highest degree.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
largest_exponent (int): The largest exponent (n) in the function.
|
||||||
|
"""
|
||||||
|
if not isinstance(largest_exponent, int) or largest_exponent < 0:
|
||||||
|
raise ValueError("largest_exponent must be a non-negative integer.")
|
||||||
|
self._largest_exponent = largest_exponent
|
||||||
|
self.coefficients: Optional[np.ndarray] = None
|
||||||
|
self._initialized = False
|
||||||
|
|
||||||
|
def set_coeffs(self, coefficients: List[int]):
|
||||||
|
"""
|
||||||
|
Sets the coefficients of the polynomial.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
coefficients (List[int]): A list of integer coefficients. The list size
|
||||||
|
must be largest_exponent + 1.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If the input is invalid.
|
||||||
|
"""
|
||||||
|
expected_size = self._largest_exponent + 1
|
||||||
|
if len(coefficients) != expected_size:
|
||||||
|
raise ValueError(
|
||||||
|
f"Function with exponent {self._largest_exponent} requires {expected_size} coefficients, "
|
||||||
|
f"but {len(coefficients)} were given."
|
||||||
|
)
|
||||||
|
if coefficients[0] == 0 and self._largest_exponent > 0:
|
||||||
|
raise ValueError("The first constant (for the largest exponent) cannot be 0.")
|
||||||
|
|
||||||
|
self.coefficients = np.array(coefficients, dtype=np.int64)
|
||||||
|
self._initialized = True
|
||||||
|
|
||||||
|
def _check_initialized(self):
|
||||||
|
"""Raises a RuntimeError if the function coefficients have not been set."""
|
||||||
|
if not self._initialized:
|
||||||
|
raise RuntimeError("Function is not fully initialized. Call .set_coeffs() first.")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def largest_exponent(self) -> int:
|
||||||
|
"""Returns the largest exponent of the function."""
|
||||||
|
return self._largest_exponent
|
||||||
|
|
||||||
|
def solve_y(self, x_val: float) -> float:
|
||||||
|
"""
|
||||||
|
Solves for y given an x value. (i.e., evaluates the polynomial at x).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
x_val (float): The x-value to evaluate.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
float: The resulting y-value.
|
||||||
|
"""
|
||||||
|
self._check_initialized()
|
||||||
|
return np.polyval(self.coefficients, x_val)
|
||||||
|
|
||||||
|
def differential(self) -> 'Function':
|
||||||
|
"""
|
||||||
|
Calculates the derivative of the function.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Function: A new Function object representing the derivative.
|
||||||
|
"""
|
||||||
|
self._check_initialized()
|
||||||
|
if self._largest_exponent == 0:
|
||||||
|
raise ValueError("Cannot differentiate a constant (Function of degree 0).")
|
||||||
|
|
||||||
|
derivative_coefficients = np.polyder(self.coefficients)
|
||||||
|
|
||||||
|
diff_func = Function(self._largest_exponent - 1)
|
||||||
|
diff_func.set_coeffs(derivative_coefficients.tolist())
|
||||||
|
return diff_func
|
||||||
|
|
||||||
|
def get_real_roots(self, options: GA_Options = GA_Options(), use_cuda: bool = False) -> np.ndarray:
|
||||||
|
"""
|
||||||
|
Uses a genetic algorithm to find the approximate real roots of the function (where y=0).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
options (GA_Options): Configuration for the genetic algorithm.
|
||||||
|
use_cuda (bool): If True, attempts to use CUDA for acceleration.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.ndarray: An array of approximate root values.
|
||||||
|
"""
|
||||||
|
self._check_initialized()
|
||||||
|
return self.solve_x(0.0, options, use_cuda)
|
||||||
|
|
||||||
|
def solve_x(self, y_val: float, options: GA_Options = GA_Options(), use_cuda: bool = False) -> np.ndarray:
|
||||||
|
"""
|
||||||
|
Uses a genetic algorithm to find x-values for a given y-value.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
y_val (float): The target y-value.
|
||||||
|
options (GA_Options): Configuration for the genetic algorithm.
|
||||||
|
use_cuda (bool): If True, attempts to use CUDA for acceleration.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.ndarray: An array of approximate x-values.
|
||||||
|
"""
|
||||||
|
self._check_initialized()
|
||||||
|
if use_cuda and _CUPY_AVAILABLE:
|
||||||
|
return self._solve_x_cuda(y_val, options)
|
||||||
|
else:
|
||||||
|
if use_cuda:
|
||||||
|
warnings.warn(
|
||||||
|
"use_cuda=True was specified, but CuPy is not installed. "
|
||||||
|
"Falling back to NumPy (CPU). For GPU acceleration, "
|
||||||
|
"install with 'pip install polysolve[cuda]'.",
|
||||||
|
UserWarning
|
||||||
|
)
|
||||||
|
|
||||||
|
return self._solve_x_numpy(y_val, options)
|
||||||
|
|
||||||
|
def _solve_x_numpy(self, y_val: float, options: GA_Options) -> np.ndarray:
|
||||||
|
"""Genetic algorithm implementation using NumPy (CPU)."""
|
||||||
|
# Create initial random solutions
|
||||||
|
solutions = np.random.uniform(options.min_range, options.max_range, options.data_size)
|
||||||
|
|
||||||
|
for _ in range(options.num_of_generations):
|
||||||
|
# Calculate fitness for all solutions (vectorized)
|
||||||
|
y_calculated = np.polyval(self.coefficients, solutions)
|
||||||
|
error = y_calculated - y_val
|
||||||
|
|
||||||
|
ranks = np.where(error == 0, np.finfo(float).max, np.abs(1.0 / error))
|
||||||
|
|
||||||
|
# Sort solutions by fitness (descending)
|
||||||
|
sorted_indices = np.argsort(-ranks)
|
||||||
|
solutions = solutions[sorted_indices]
|
||||||
|
|
||||||
|
# Keep only the top solutions
|
||||||
|
top_solutions = solutions[:options.sample_size]
|
||||||
|
|
||||||
|
# For the next generation, start with the mutated top solutions
|
||||||
|
# and fill the rest with new random values.
|
||||||
|
mutation_factors = np.random.uniform(
|
||||||
|
1 - options.mutation_percentage,
|
||||||
|
1 + options.mutation_percentage,
|
||||||
|
options.sample_size
|
||||||
|
)
|
||||||
|
mutated_solutions = top_solutions * mutation_factors
|
||||||
|
|
||||||
|
new_random_solutions = np.random.uniform(
|
||||||
|
options.min_range, options.max_range, options.data_size - options.sample_size
|
||||||
|
)
|
||||||
|
|
||||||
|
solutions = np.concatenate([mutated_solutions, new_random_solutions])
|
||||||
|
|
||||||
|
# Final sort of the best solutions from the last generation
|
||||||
|
final_solutions = np.sort(solutions[:options.sample_size])
|
||||||
|
return final_solutions
|
||||||
|
|
||||||
|
def _solve_x_cuda(self, y_val: float, options: GA_Options) -> np.ndarray:
|
||||||
|
"""Genetic algorithm implementation using CuPy (GPU/CUDA)."""
|
||||||
|
# Load the raw CUDA kernel
|
||||||
|
fitness_gpu = cupy.RawKernel(_FITNESS_KERNEL, 'fitness_kernel')
|
||||||
|
|
||||||
|
# Move coefficients to GPU
|
||||||
|
d_coefficients = cupy.array(self.coefficients, dtype=cupy.int64)
|
||||||
|
|
||||||
|
# Create initial random solutions on the GPU
|
||||||
|
d_solutions = cupy.random.uniform(
|
||||||
|
options.min_range, options.max_range, options.data_size, dtype=cupy.float64
|
||||||
|
)
|
||||||
|
d_ranks = cupy.empty(options.data_size, dtype=cupy.float64)
|
||||||
|
|
||||||
|
# Configure kernel launch parameters
|
||||||
|
threads_per_block = 512
|
||||||
|
blocks_per_grid = (options.data_size + threads_per_block - 1) // threads_per_block
|
||||||
|
|
||||||
|
for i in range(options.num_of_generations):
|
||||||
|
# Run the fitness kernel on the GPU
|
||||||
|
fitness_gpu(
|
||||||
|
(blocks_per_grid,), (threads_per_block,),
|
||||||
|
(d_coefficients, d_coefficients.size, d_solutions, d_ranks, d_solutions.size, y_val)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Sort solutions by rank on the GPU
|
||||||
|
sorted_indices = cupy.argsort(-d_ranks)
|
||||||
|
d_solutions = d_solutions[sorted_indices]
|
||||||
|
|
||||||
|
if i + 1 == options.num_of_generations:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Get top solutions
|
||||||
|
d_top_solutions = d_solutions[:options.sample_size]
|
||||||
|
|
||||||
|
# Mutate top solutions on the GPU
|
||||||
|
mutation_factors = cupy.random.uniform(
|
||||||
|
1 - options.mutation_percentage, 1 + options.mutation_percentage, options.sample_size
|
||||||
|
)
|
||||||
|
d_mutated = d_top_solutions * mutation_factors
|
||||||
|
|
||||||
|
# Create new random solutions for the rest
|
||||||
|
d_new_random = cupy.random.uniform(
|
||||||
|
options.min_range, options.max_range, options.data_size - options.sample_size
|
||||||
|
)
|
||||||
|
|
||||||
|
d_solutions = cupy.concatenate([d_mutated, d_new_random])
|
||||||
|
|
||||||
|
# Get the final sample, sort it, and copy back to CPU
|
||||||
|
final_solutions_gpu = cupy.sort(d_solutions[:options.sample_size])
|
||||||
|
return final_solutions_gpu.get()
|
||||||
|
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
"""Returns a human-readable string representation of the function."""
|
||||||
|
self._check_initialized()
|
||||||
|
parts = []
|
||||||
|
for i, c in enumerate(self.coefficients):
|
||||||
|
if c == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
power = self._largest_exponent - i
|
||||||
|
|
||||||
|
# Coefficient part
|
||||||
|
if c == 1 and power != 0:
|
||||||
|
coeff = ""
|
||||||
|
elif c == -1 and power != 0:
|
||||||
|
coeff = "-"
|
||||||
|
else:
|
||||||
|
coeff = str(c)
|
||||||
|
|
||||||
|
# Variable part
|
||||||
|
if power == 0:
|
||||||
|
var = ""
|
||||||
|
elif power == 1:
|
||||||
|
var = "x"
|
||||||
|
else:
|
||||||
|
var = f"x^{power}"
|
||||||
|
|
||||||
|
# Add sign for non-leading terms
|
||||||
|
sign = ""
|
||||||
|
if i > 0:
|
||||||
|
sign = " + " if c > 0 else " - "
|
||||||
|
coeff = str(abs(c))
|
||||||
|
if abs(c) == 1 and power != 0:
|
||||||
|
coeff = "" # Don't show 1 for non-constant terms
|
||||||
|
|
||||||
|
parts.append(f"{sign}{coeff}{var}")
|
||||||
|
|
||||||
|
# Join parts and clean up
|
||||||
|
result = "".join(parts)
|
||||||
|
if result.startswith(" + "):
|
||||||
|
result = result[3:]
|
||||||
|
return result if result else "0"
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"Function(str='{self}')"
|
||||||
|
|
||||||
|
def __add__(self, other: 'Function') -> 'Function':
|
||||||
|
"""Adds two Function objects."""
|
||||||
|
self._check_initialized()
|
||||||
|
other._check_initialized()
|
||||||
|
|
||||||
|
new_coefficients = np.polyadd(self.coefficients, other.coefficients)
|
||||||
|
|
||||||
|
result_func = Function(len(new_coefficients) - 1)
|
||||||
|
result_func.set_coeffs(new_coefficients.tolist())
|
||||||
|
return result_func
|
||||||
|
|
||||||
|
def __sub__(self, other: 'Function') -> 'Function':
|
||||||
|
"""Subtracts another Function object from this one."""
|
||||||
|
self._check_initialized()
|
||||||
|
other._check_initialized()
|
||||||
|
|
||||||
|
new_coefficients = np.polysub(self.coefficients, other.coefficients)
|
||||||
|
|
||||||
|
result_func = Function(len(new_coefficients) - 1)
|
||||||
|
result_func.set_coeffs(new_coefficients.tolist())
|
||||||
|
return result_func
|
||||||
|
|
||||||
|
def __mul__(self, scalar: int) -> 'Function':
|
||||||
|
"""Multiplies the function by a scalar constant."""
|
||||||
|
self._check_initialized()
|
||||||
|
if not isinstance(scalar, (int, float)):
|
||||||
|
return NotImplemented
|
||||||
|
if scalar == 0:
|
||||||
|
raise ValueError("Cannot multiply a function by 0.")
|
||||||
|
|
||||||
|
new_coefficients = self.coefficients * scalar
|
||||||
|
|
||||||
|
result_func = Function(self._largest_exponent)
|
||||||
|
result_func.set_coeffs(new_coefficients.tolist())
|
||||||
|
return result_func
|
||||||
|
|
||||||
|
def __rmul__(self, scalar: int) -> 'Function':
|
||||||
|
"""Handles scalar multiplication from the right (e.g., 3 * func)."""
|
||||||
|
return self.__mul__(scalar)
|
||||||
|
|
||||||
|
def __imul__(self, scalar: int) -> 'Function':
|
||||||
|
"""Performs in-place multiplication by a scalar (func *= 3)."""
|
||||||
|
self._check_initialized()
|
||||||
|
if not isinstance(scalar, (int, float)):
|
||||||
|
return NotImplemented
|
||||||
|
if scalar == 0:
|
||||||
|
raise ValueError("Cannot multiply a function by 0.")
|
||||||
|
|
||||||
|
self.coefficients *= scalar
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
def quadratic_solve(f: Function) -> Optional[List[float]]:
|
||||||
|
"""
|
||||||
|
Calculates the real roots of a quadratic function using the quadratic formula.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
f (Function): A Function object of degree 2.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optional[List[float]]: A list containing the two real roots, or None if there are no real roots.
|
||||||
|
"""
|
||||||
|
f._check_initialized()
|
||||||
|
if f.largest_exponent != 2:
|
||||||
|
raise ValueError("Input function must be quadratic (degree 2).")
|
||||||
|
|
||||||
|
a, b, c = f.coefficients
|
||||||
|
|
||||||
|
discriminant = (b**2) - (4*a*c)
|
||||||
|
|
||||||
|
if discriminant < 0:
|
||||||
|
return None # No real roots
|
||||||
|
|
||||||
|
sqrt_discriminant = math.sqrt(discriminant)
|
||||||
|
root1 = (-b + sqrt_discriminant) / (2 * a)
|
||||||
|
root2 = (-b - sqrt_discriminant) / (2 * a)
|
||||||
|
|
||||||
|
return [root1, root2]
|
||||||
|
|
||||||
|
# Example Usage
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print("--- Demonstrating Functionality ---")
|
||||||
|
|
||||||
|
# Create a quadratic function: 2x^2 - 3x - 5
|
||||||
|
f1 = Function(2)
|
||||||
|
f1.set_coeffs([2, -3, -5])
|
||||||
|
print(f"Function f1: {f1}")
|
||||||
|
|
||||||
|
# Solve for y
|
||||||
|
y = f1.solve_y(5)
|
||||||
|
print(f"Value of f1 at x=5 is: {y}") # Expected: 2*(25) - 3*(5) - 5 = 50 - 15 - 5 = 30
|
||||||
|
|
||||||
|
# Find the derivative: 4x - 3
|
||||||
|
df1 = f1.differential()
|
||||||
|
print(f"Derivative of f1: {df1}")
|
||||||
|
|
||||||
|
# --- Root Finding ---
|
||||||
|
# 1. Analytical solution for quadratic
|
||||||
|
roots_analytic = quadratic_solve(f1)
|
||||||
|
print(f"Analytic roots of f1: {roots_analytic}") # Expected: -1, 2.5
|
||||||
|
|
||||||
|
# 2. Genetic algorithm solution
|
||||||
|
ga_opts = GA_Options(num_of_generations=20, data_size=50000, sample_size=10)
|
||||||
|
print("\nFinding roots with Genetic Algorithm (CPU)...")
|
||||||
|
roots_ga_cpu = f1.get_real_roots(ga_opts)
|
||||||
|
print(f"Approximate roots from GA (CPU): {roots_ga_cpu}")
|
||||||
|
print("(Note: GA provides approximations around the true roots)")
|
||||||
|
|
||||||
|
# 3. CUDA accelerated genetic algorithm
|
||||||
|
if _CUPY_AVAILABLE:
|
||||||
|
print("\nFinding roots with Genetic Algorithm (CUDA)...")
|
||||||
|
# Since this PC has an RTX 4060 Ti, we can use the CUDA version.
|
||||||
|
roots_ga_gpu = f1.get_real_roots(ga_opts, use_cuda=True)
|
||||||
|
print(f"Approximate roots from GA (GPU): {roots_ga_gpu}")
|
||||||
|
else:
|
||||||
|
print("\nSkipping CUDA example: CuPy library not found or no compatible GPU.")
|
||||||
|
|
||||||
|
# --- Function Arithmetic ---
|
||||||
|
print("\n--- Function Arithmetic ---")
|
||||||
|
f2 = Function(1)
|
||||||
|
f2.set_coeffs([1, 10]) # x + 10
|
||||||
|
print(f"Function f2: {f2}")
|
||||||
|
|
||||||
|
# Addition: (2x^2 - 3x - 5) + (x + 10) = 2x^2 - 2x + 5
|
||||||
|
f_add = f1 + f2
|
||||||
|
print(f"f1 + f2 = {f_add}")
|
||||||
|
|
||||||
|
# Subtraction: (2x^2 - 3x - 5) - (x + 10) = 2x^2 - 4x - 15
|
||||||
|
f_sub = f1 - f2
|
||||||
|
print(f"f1 - f2 = {f_sub}")
|
||||||
|
|
||||||
|
# Multiplication: (x + 10) * 3 = 3x + 30
|
||||||
|
f_mul = f2 * 3
|
||||||
|
print(f"f2 * 3 = {f_mul}")
|
110
tests/test_polysolve.py
Normal file
110
tests/test_polysolve.py
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import pytest
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
# Try to import cupy to check for CUDA availability
|
||||||
|
try:
|
||||||
|
import cupy
|
||||||
|
_CUPY_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
_CUPY_AVAILABLE = False
|
||||||
|
|
||||||
|
from polysolve import Function, GA_Options, quadratic_solve
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def quadratic_func() -> Function:
|
||||||
|
"""Provides a standard quadratic function: 2x^2 - 3x - 5."""
|
||||||
|
f = Function(largest_exponent=2)
|
||||||
|
f.set_constants([2, -3, -5])
|
||||||
|
return f
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def linear_func() -> Function:
|
||||||
|
"""Provides a standard linear function: x + 10."""
|
||||||
|
f = Function(largest_exponent=1)
|
||||||
|
f.set_constants([1, 10])
|
||||||
|
return f
|
||||||
|
|
||||||
|
# --- Core Functionality Tests ---
|
||||||
|
|
||||||
|
def test_solve_y(quadratic_func):
|
||||||
|
"""Tests if the function correctly evaluates y for a given x."""
|
||||||
|
assert quadratic_func.solve_y(5) == 30.0
|
||||||
|
assert quadratic_func.solve_y(0) == -5.0
|
||||||
|
assert quadratic_func.solve_y(-1) == 0.0
|
||||||
|
|
||||||
|
def test_differential(quadratic_func):
|
||||||
|
"""Tests the calculation of the function's derivative."""
|
||||||
|
derivative = quadratic_func.differential()
|
||||||
|
assert derivative.largest_exponent == 1
|
||||||
|
# The derivative of 2x^2 - 3x - 5 is 4x - 3
|
||||||
|
assert np.array_equal(derivative.constants, [4, -3])
|
||||||
|
|
||||||
|
def test_quadratic_solve(quadratic_func):
|
||||||
|
"""Tests the analytical quadratic solver for exact roots."""
|
||||||
|
roots = quadratic_solve(quadratic_func)
|
||||||
|
# Sorting ensures consistent order for comparison
|
||||||
|
assert sorted(roots) == [-1.0, 2.5]
|
||||||
|
|
||||||
|
# --- Arithmetic Operation Tests ---
|
||||||
|
|
||||||
|
def test_addition(quadratic_func, linear_func):
|
||||||
|
"""Tests the addition of two Function objects."""
|
||||||
|
# (2x^2 - 3x - 5) + (x + 10) = 2x^2 - 2x + 5
|
||||||
|
result = quadratic_func + linear_func
|
||||||
|
assert result.largest_exponent == 2
|
||||||
|
assert np.array_equal(result.constants, [2, -2, 5])
|
||||||
|
|
||||||
|
def test_subtraction(quadratic_func, linear_func):
|
||||||
|
"""Tests the subtraction of two Function objects."""
|
||||||
|
# (2x^2 - 3x - 5) - (x + 10) = 2x^2 - 4x - 15
|
||||||
|
result = quadratic_func - linear_func
|
||||||
|
assert result.largest_exponent == 2
|
||||||
|
assert np.array_equal(result.constants, [2, -4, -15])
|
||||||
|
|
||||||
|
def test_multiplication(linear_func):
|
||||||
|
"""Tests the multiplication of a Function object by a scalar."""
|
||||||
|
# (x + 10) * 3 = 3x + 30
|
||||||
|
result = linear_func * 3
|
||||||
|
assert result.largest_exponent == 1
|
||||||
|
assert np.array_equal(result.constants, [3, 30])
|
||||||
|
|
||||||
|
# --- Genetic Algorithm Root-Finding Tests ---
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
roots = quadratic_func.get_real_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, 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))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not _CUPY_AVAILABLE, reason="CuPy is not installed, skipping CUDA test.")
|
||||||
|
def test_get_real_roots_cuda(quadratic_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=25, data_size=50000)
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
Reference in New Issue
Block a user