refactor!: migrate to modern numpy API, patch root solving, and bump dependencies #27

Merged
jono merged 2 commits from v0.8.0 into main 2026-09-26 16:31:18 +00:00
3 changed files with 67 additions and 91 deletions
Showing only changes of commit 563fb440a3 - Show all commits
+1 -1
View File
@@ -6,7 +6,7 @@ jobs:
runs-on: nvidia-cuda runs-on: nvidia-cuda
strategy: strategy:
matrix: matrix:
python-version: ["3.8", "3.10", "3.12", "3.14"] python-version: ["3.10", "3.12", "3.14"]
steps: steps:
- name: Checkout repository - name: Checkout repository
+4 -6
View File
@@ -5,13 +5,13 @@ build-backend = "setuptools.build_meta"
[project] [project]
# --- Core Metadata --- # --- Core Metadata ---
name = "polysolve" name = "polysolve"
version = "0.7.1" version = "0.8.0"
authors = [ authors = [
{ name="Jonathan Rampersad", email="[email protected]" }, { name="Jonathan Rampersad", email="[email protected]" },
] ]
description = "A Python library for representing, manipulating, and solving exponential functions using analytical methods and genetic algorithms, with optional CUDA acceleration." description = "A Python library for representing, manipulating, and solving exponential functions using analytical methods and genetic algorithms, with optional CUDA acceleration."
readme = "README.md" readme = "README.md"
requires-python = ">=3.8" requires-python = ">=3.10"
license = { file = "LICENSE" } license = { file = "LICENSE" }
keywords = ["math", "polynomial", "genetic algorithm", "cuda", "equation solver"] keywords = ["math", "polynomial", "genetic algorithm", "cuda", "equation solver"]
@@ -23,8 +23,6 @@ classifiers = [
"License :: OSI Approved :: MIT License", "License :: OSI Approved :: MIT License",
"Operating System :: OS Independent", "Operating System :: OS Independent",
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.12",
@@ -35,8 +33,8 @@ classifiers = [
# --- Dependencies --- # --- Dependencies ---
dependencies = [ dependencies = [
"numpy>=1.21", "numpy>=2.0",
"numba" "numba>=0.60"
] ]
# --- Optional Dependencies (Extras) --- # --- Optional Dependencies (Extras) ---
+60 -82
View File
@@ -1,6 +1,7 @@
import math import math
import cmath import cmath
import numpy as np import numpy as np
from numpy.polynomial import Polynomial
import numba import numba
from dataclasses import dataclass from dataclasses import dataclass
from typing import List, Optional, Union from typing import List, Optional, Union
@@ -383,6 +384,9 @@ class Function:
largest_exponent (int): The largest exponent (n) in the function. largest_exponent (int): The largest exponent (n) in the function.
""" """
self._largest_exponent = largest_exponent self._largest_exponent = largest_exponent
self.poly_obj = None
self._initialized = False
if coefficients is not None: if coefficients is not None:
self.set_coeffs(coefficients) self.set_coeffs(coefficients)
# Verify user provided exponent matches if they provided both # Verify user provided exponent matches if they provided both
@@ -390,7 +394,6 @@ class Function:
raise ValueError("Provided largest_exponent does not match coefficient list length.") raise ValueError("Provided largest_exponent does not match coefficient list length.")
elif largest_exponent is not None: elif largest_exponent is not None:
self.coefficients = None self.coefficients = None
self._initialized = False
else: else:
raise ValueError("Must provide either coefficients or largest_exponent.") raise ValueError("Must provide either coefficients or largest_exponent.")
@@ -425,6 +428,9 @@ class Function:
target_dtype = np.float64 target_dtype = np.float64
self.coefficients = np.array(coefficients, dtype=target_dtype) self.coefficients = np.array(coefficients, dtype=target_dtype)
reversed_coeffs = coefficients[::-1]
self.poly_obj = Polynomial(reversed_coeffs)
self._largest_exponent = len(coefficients) - 1 self._largest_exponent = len(coefficients) - 1
self._initialized = True self._initialized = True
@@ -454,7 +460,7 @@ class Function:
float: The resulting y-value. float: The resulting y-value.
""" """
self._check_initialized() self._check_initialized()
return np.polyval(self.coefficients, x_val) return self.poly_obj(x_val)
def differential(self) -> 'Function': def differential(self) -> 'Function':
""" """
@@ -489,10 +495,11 @@ class Function:
diff_func.set_coeffs([0]) diff_func.set_coeffs([0])
return diff_func return diff_func
derivative_coefficients = np.polyder(self.coefficients) deriv_poly = self.poly_obj.deriv(1)
new_coeffs = deriv_poly.coef[::-1].tolist()
diff_func = Function(self._largest_exponent - 1) diff_func = Function(self._largest_exponent - 1)
diff_func.set_coeffs(derivative_coefficients.tolist()) diff_func.set_coeffs(new_coeffs)
return diff_func return diff_func
@@ -512,18 +519,30 @@ class Function:
raise ValueError("Derivative order 'n' must be a positive integer.") raise ValueError("Derivative order 'n' must be a positive integer.")
if n > self.largest_exponent: if n > self.largest_exponent:
function = Function(0) diff_func = Function(0)
function.set_coeffs([0]) diff_func.set_coeffs([0])
return function return diff_func
if n == 1: deriv_poly = self.poly_obj.deriv(n)
return self.derivative() new_coeffs = deriv_poly.coef[::-1].tolist()
function = self new_degree = self._largest_exponent - n
for _ in range(n): diff_func = Function(new_degree)
function = function.derivative() diff_func.set_coeffs(new_coeffs)
return diff_func
return function
def integral(self, constant_of_integration: float = 0.0) -> 'Function':
"""Calculates the indefinite integral (antiderivative)."""
self._check_initialized()
# Integration naturally increases the degree by 1
integ_poly = self.poly_obj.integ(m=1, k=[constant_of_integration])
new_coeffs = integ_poly.coef[::-1].tolist()
integral_func = Function(self._largest_exponent + 1)
integral_func.set_coeffs(new_coeffs)
return integral_func
def get_real_roots(self, options: Optional[GA_Options] = None, use_cuda: bool = False) -> np.ndarray: def get_real_roots(self, options: Optional[GA_Options] = None, use_cuda: bool = False) -> np.ndarray:
@@ -1139,49 +1158,10 @@ class Function:
def __str__(self) -> str: def __str__(self) -> str:
"""Returns a human-readable string representation of the function.""" """Returns a human-readable string representation of the function."""
self._check_initialized() if not self._initialized:
parts = [] return "Uninitialized Function"
for i, c in enumerate(self.coefficients):
if c == 0:
continue
power = self._largest_exponent - i return str(self.poly_obj)
# Coefficient part
coeff_val = c
if c == int(c):
coeff_val = int(c)
if coeff_val == 1 and power != 0:
coeff = ""
elif coeff_val == -1 and power != 0:
coeff = "-"
else:
coeff = str(coeff_val)
# 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(coeff_val))
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: def __repr__(self) -> str:
return f"Function(str='{self}')" return f"Function(str='{self}')"
@@ -1191,11 +1171,12 @@ class Function:
self._check_initialized() self._check_initialized()
other._check_initialized() other._check_initialized()
new_coefficients = np.polyadd(self.coefficients, other.coefficients) # The Polynomial objects handle degree alignment and zero-trimming natively
new_coefficients = self._strip_leading_zeros(new_coefficients) new_poly = self.poly_obj + other.poly_obj
new_coeffs = new_poly.coef[::-1].tolist()
result_func = Function(len(new_coefficients) - 1) result_func = Function(len(new_coeffs) - 1)
result_func.set_coeffs(new_coefficients.tolist()) result_func.set_coeffs(new_coeffs)
return result_func return result_func
def _strip_leading_zeros(self, coeffs: np.ndarray) -> np.ndarray: def _strip_leading_zeros(self, coeffs: np.ndarray) -> np.ndarray:
@@ -1209,11 +1190,11 @@ class Function:
self._check_initialized() self._check_initialized()
other._check_initialized() other._check_initialized()
new_coefficients = np.polysub(self.coefficients, other.coefficients) new_poly = self.poly_obj - other.poly_obj
new_coefficients = self._strip_leading_zeros(new_coefficients) new_coeffs = new_poly.coef[::-1].tolist()
result_func = Function(len(new_coefficients) - 1) result_func = Function(len(new_coeffs) - 1)
result_func.set_coeffs(new_coefficients.tolist()) result_func.set_coeffs(new_coeffs)
return result_func return result_func
def _multiply_by_scalar(self, scalar: Union[int, float, complex]) -> 'Function': def _multiply_by_scalar(self, scalar: Union[int, float, complex]) -> 'Function':
@@ -1237,15 +1218,11 @@ class Function:
self._check_initialized() self._check_initialized()
other._check_initialized() other._check_initialized()
# np.polymul performs convolution of coefficients to multiply polynomials new_poly = self.poly_obj * other.poly_obj
new_coefficients = np.polymul(self.coefficients, other.coefficients) new_coeffs = new_poly.coef[::-1].tolist()
new_coefficients = self._strip_leading_zeros(new_coefficients)
# The degree of the resulting polynomial is derived from the new coefficients result_func = Function(len(new_coeffs) - 1)
new_degree = len(new_coefficients) - 1 result_func.set_coeffs(new_coeffs)
result_func = Function(new_degree)
result_func.set_coeffs(new_coefficients.tolist())
return result_func return result_func
def __mul__(self, other: Union['Function', int, float, complex]) -> 'Function': def __mul__(self, other: Union['Function', int, float, complex]) -> 'Function':
@@ -1269,16 +1246,18 @@ class Function:
if isinstance(other, (int, float, complex)): if isinstance(other, (int, float, complex)):
if other == 0: if other == 0:
self.coefficients = np.array([0], dtype=self.coefficients.dtype) self.set_coeffs([0])
self._largest_exponent = 0
else: else:
self.coefficients *= other # Update the underlying poly_obj, then sync the legacy array
self.poly_obj *= other
self.coefficients = self.poly_obj.coef[::-1]
self._largest_exponent = len(self.coefficients) - 1
elif isinstance(other, self.__class__): elif isinstance(other, self.__class__):
other._check_initialized() other._check_initialized()
self.coefficients = np.polymul(self.coefficients, other.coefficients) self.poly_obj *= other.poly_obj
self.coefficients = self.poly_obj.coef[::-1]
self._largest_exponent = len(self.coefficients) - 1 self._largest_exponent = len(self.coefficients) - 1
else: else:
return NotImplemented return NotImplemented
@@ -1289,20 +1268,20 @@ class Function:
Checks if two Function objects are equal by comparing Checks if two Function objects are equal by comparing
their coefficients. their coefficients.
""" """
# Check if the 'other' object is even a Function
if not isinstance(other, Function): if not isinstance(other, Function):
return NotImplemented return NotImplemented
# Ensure both are initialized before trying to access .coefficients
if not self._initialized or not other._initialized: if not self._initialized or not other._initialized:
return False return False
c1 = self._strip_leading_zeros(self.coefficients) # Extract the auto-trimmed arrays from the modern API
c2 = self._strip_leading_zeros(other.coefficients) c1 = self.poly_obj.coef
c2 = other.poly_obj.coef
if c1.shape != c2.shape: if c1.shape != c2.shape:
return False return False
# Retain np.allclose to protect against floating-point inaccuracies
return np.allclose(c1, c2) return np.allclose(c1, c2)
@@ -1320,10 +1299,9 @@ class Function:
a, b, c = self.coefficients a, b, c = self.coefficients
discriminant = (b**2) - (4*a*c) discriminant = (b**2) - (4*a*c)
sqrt_discriminant = cmath.sqrt(discriminant) sqrt_discriminant = cmath.sqrt(discriminant)
if b >= 0: if b.real >= 0:
sign_b = 1 sign_b = 1
else: else:
sign_b = -1 sign_b = -1