Merge pull request 'v0.2.0' (#10) from v0.2.0-dev into main
All checks were successful
Publish Python Package to PyPI / deploy (push) Successful in 13s

Reviewed-on: #10
This commit is contained in:
2025-06-17 18:36:25 +00:00
5 changed files with 154 additions and 38 deletions

View File

@ -1,10 +0,0 @@
## Contributors
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
[![All Contributors](https://img.shields.io/github/all-contributors/jono-rams/PolySolve?color=ee8449&style=flat-square)](#contributors)
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->

View File

@ -56,17 +56,22 @@ print(f"Value of f1 at x=5 is: {y_val}")
# > Value of f1 at x=5 is: 30.0 # > Value of f1 at x=5 is: 30.0
# 3. Get the derivative: 4x - 3 # 3. Get the derivative: 4x - 3
df1 = f1.differential() df1 = f1.derivative()
print(f"Derivative of f1: {df1}") print(f"Derivative of f1: {df1}")
# > Derivative of f1: 4x - 3 # > Derivative of f1: 4x - 3
# 4. Find roots analytically using the quadratic formula # 4. Get the 2nd derivative: 4
df1 = f1.nth_derivative(2)
print(f"2nd Derivative of f1: {df1}")
# > Derivative of f1: 4
# 5. Find roots analytically using the quadratic formula
# This is exact and fast for degree-2 polynomials. # This is exact and fast for degree-2 polynomials.
roots_analytic = quadratic_solve(f1) roots_analytic = quadratic_solve(f1)
print(f"Analytic roots: {sorted(roots_analytic)}") print(f"Analytic roots: {sorted(roots_analytic)}")
# > Analytic roots: [-1.0, 2.5] # > Analytic roots: [-1.0, 2.5]
# 5. Find roots with the genetic algorithm (CPU) # 6. Find roots with the genetic algorithm (CPU)
# This can solve polynomials of any degree. # This can solve polynomials of any degree.
ga_opts = GA_Options(num_of_generations=20) ga_opts = GA_Options(num_of_generations=20)
roots_ga = f1.get_real_roots(ga_opts, use_cuda=False) roots_ga = f1.get_real_roots(ga_opts, use_cuda=False)

View File

@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
# --- Core Metadata --- # --- Core Metadata ---
name = "polysolve" name = "polysolve"
version = "0.1.1" version = "0.2.0"
authors = [ authors = [
{ name="Jonathan Rampersad", email="jonathan@jono-rams.work" }, { name="Jonathan Rampersad", email="jonathan@jono-rams.work" },
] ]

View File

@ -1,7 +1,7 @@
import math import math
import numpy as np import numpy as np
from dataclasses import dataclass from dataclasses import dataclass
from typing import List, Optional from typing import List, Optional, Union
import warnings import warnings
# Attempt to import CuPy for CUDA acceleration. # Attempt to import CuPy for CUDA acceleration.
@ -109,6 +109,11 @@ class Function:
"""Returns the largest exponent of the function.""" """Returns the largest exponent of the function."""
return self._largest_exponent return self._largest_exponent
@property
def degree(self) -> int:
"""Returns the largest exponent of the function."""
return self._largest_exponent
def solve_y(self, x_val: float) -> float: def solve_y(self, x_val: float) -> float:
""" """
Solves for y given an x value. (i.e., evaluates the polynomial at x). Solves for y given an x value. (i.e., evaluates the polynomial at x).
@ -126,6 +131,26 @@ class Function:
""" """
Calculates the derivative of the function. Calculates the derivative of the function.
Returns:
Function: A new Function object representing the derivative.
"""
warnings.warn(
"The 'differential' function has been renamed. Please use 'derivative' instead.",
DeprecationWarning,
stacklevel=2
)
self._check_initialized()
if self._largest_exponent == 0:
raise ValueError("Cannot differentiate a constant (Function of degree 0).")
return self.derivitive()
def derivative(self) -> 'Function':
"""
Calculates the derivative of the function.
Returns: Returns:
Function: A new Function object representing the derivative. Function: A new Function object representing the derivative.
""" """
@ -139,6 +164,37 @@ class Function:
diff_func.set_coeffs(derivative_coefficients.tolist()) diff_func.set_coeffs(derivative_coefficients.tolist())
return diff_func return diff_func
def nth_derivative(self, n: int) -> 'Function':
"""
Calculates the nth derivative of the function.
Args:
n (int): The order of the derivative to calculate.
Returns:
Function: A new Function object representing the nth derivative.
"""
self._check_initialized()
if not isinstance(n, int) or n < 1:
raise ValueError("Derivative order 'n' must be a positive integer.")
if n > self.largest_exponent:
function = Function(0)
function.set_coeffs([0])
return function
if n == 1:
return self.derivative()
function = self
for _ in range(n):
function = function.derivative()
return function
def get_real_roots(self, options: GA_Options = GA_Options(), use_cuda: bool = False) -> np.ndarray: 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). Uses a genetic algorithm to find the approximate real roots of the function (where y=0).
@ -337,13 +393,14 @@ class Function:
result_func.set_coeffs(new_coefficients.tolist()) result_func.set_coeffs(new_coefficients.tolist())
return result_func return result_func
def __mul__(self, scalar: int) -> 'Function': def _multiply_by_scalar(self, scalar: Union[int, float]) -> 'Function':
"""Multiplies the function by a scalar constant.""" """Helper method to multiply the function by a scalar constant."""
self._check_initialized() self._check_initialized() # It's good practice to check here too
if not isinstance(scalar, (int, float)):
return NotImplemented
if scalar == 0: if scalar == 0:
raise ValueError("Cannot multiply a function by 0.") result_func = Function(0)
result_func.set_coeffs([0])
return result_func
new_coefficients = self.coefficients * scalar new_coefficients = self.coefficients * scalar
@ -351,19 +408,39 @@ class Function:
result_func.set_coeffs(new_coefficients.tolist()) result_func.set_coeffs(new_coefficients.tolist())
return result_func return result_func
def __rmul__(self, scalar: int) -> 'Function': def _multiply_by_function(self, other: 'Function') -> 'Function':
"""Helper method for polynomial multiplication (Function * Function)."""
self._check_initialized()
other._check_initialized()
# np.polymul performs convolution of coefficients to multiply polynomials
new_coefficients = np.polymul(self.coefficients, other.coefficients)
# The degree of the resulting polynomial is derived from the new coefficients
new_degree = len(new_coefficients) - 1
result_func = Function(new_degree)
result_func.set_coeffs(new_coefficients.tolist())
return result_func
def __mul__(self, other: Union['Function', int, float]) -> 'Function':
"""Multiplies the function by a scalar constant."""
if isinstance(other, (int, float)):
return self._multiply_by_scalar(other)
elif isinstance(other, self.__class__):
return self._multiply_by_function(other)
else:
return NotImplemented
def __rmul__(self, scalar: Union[int, float]) -> 'Function':
"""Handles scalar multiplication from the right (e.g., 3 * func).""" """Handles scalar multiplication from the right (e.g., 3 * func)."""
return self.__mul__(scalar) return self.__mul__(scalar)
def __imul__(self, scalar: int) -> 'Function': def __imul__(self, other: Union['Function', int, float]) -> 'Function':
"""Performs in-place multiplication by a scalar (func *= 3).""" """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 self.coefficients *= other
return self return self
@ -408,9 +485,13 @@ if __name__ == '__main__':
print(f"Value of f1 at x=5 is: {y}") # Expected: 2*(25) - 3*(5) - 5 = 50 - 15 - 5 = 30 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 # Find the derivative: 4x - 3
df1 = f1.differential() df1 = f1.derivative()
print(f"Derivative of f1: {df1}") print(f"Derivative of f1: {df1}")
# Find the second derivative: 4
ddf1 = f1.nth_derivative(2)
print(f"Second derivative of f1: {ddf1}")
# --- Root Finding --- # --- Root Finding ---
# 1. Analytical solution for quadratic # 1. Analytical solution for quadratic
roots_analytic = quadratic_solve(f1) roots_analytic = quadratic_solve(f1)
@ -449,3 +530,17 @@ if __name__ == '__main__':
# Multiplication: (x + 10) * 3 = 3x + 30 # Multiplication: (x + 10) * 3 = 3x + 30
f_mul = f2 * 3 f_mul = f2 * 3
print(f"f2 * 3 = {f_mul}") print(f"f2 * 3 = {f_mul}")
# f3 represents 2x^2 + 3x + 1
f3 = Function(2)
f3.set_coeffs([2, 3, 1])
print(f"Function f3: {f3}")
# f4 represents 5x - 4
f4 = Function(1)
f4.set_coeffs([5, -4])
print(f"Function f4: {f4}")
# Multiply the two functions
product_func = f3 * f4
print(f"f3 * f4 = {product_func}")

View File

@ -24,6 +24,18 @@ def linear_func() -> Function:
f.set_coeffs([1, 10]) f.set_coeffs([1, 10])
return f return f
@pytest.fixture
def m_func_1() -> Function:
f = Function(2)
f.set_coeffs([2, 3, 1])
return f
@pytest.fixture
def m_func_2() -> Function:
f = Function(1)
f.set_coeffs([5, -4])
return f
# --- Core Functionality Tests --- # --- Core Functionality Tests ---
def test_solve_y(quadratic_func): def test_solve_y(quadratic_func):
@ -32,13 +44,20 @@ def test_solve_y(quadratic_func):
assert quadratic_func.solve_y(0) == -5.0 assert quadratic_func.solve_y(0) == -5.0
assert quadratic_func.solve_y(-1) == 0.0 assert quadratic_func.solve_y(-1) == 0.0
def test_differential(quadratic_func): def test_derivative(quadratic_func):
"""Tests the calculation of the function's derivative.""" """Tests the calculation of the function's derivative."""
derivative = quadratic_func.differential() derivative = quadratic_func.derivative()
assert derivative.largest_exponent == 1 assert derivative.largest_exponent == 1
# The derivative of 2x^2 - 3x - 5 is 4x - 3 # The derivative of 2x^2 - 3x - 5 is 4x - 3
assert np.array_equal(derivative.coefficients, [4, -3]) assert np.array_equal(derivative.coefficients, [4, -3])
def test_nth_derivative(quadratic_func):
"""Tests the calculation of the function's 2nd derivative."""
derivative = quadratic_func.nth_derivative(2)
assert derivative.largest_exponent == 0
# The derivative of 2x^2 - 3x - 5 is 4x - 3
assert np.array_equal(derivative.coefficients, [4])
def test_quadratic_solve(quadratic_func): def test_quadratic_solve(quadratic_func):
"""Tests the analytical quadratic solver for exact roots.""" """Tests the analytical quadratic solver for exact roots."""
roots = quadratic_solve(quadratic_func) roots = quadratic_solve(quadratic_func)
@ -61,13 +80,20 @@ def test_subtraction(quadratic_func, linear_func):
assert result.largest_exponent == 2 assert result.largest_exponent == 2
assert np.array_equal(result.coefficients, [2, -4, -15]) assert np.array_equal(result.coefficients, [2, -4, -15])
def test_multiplication(linear_func): def test_scalar_multiplication(linear_func):
"""Tests the multiplication of a Function object by a scalar.""" """Tests the multiplication of a Function object by a scalar."""
# (x + 10) * 3 = 3x + 30 # (x + 10) * 3 = 3x + 30
result = linear_func * 3 result = linear_func * 3
assert result.largest_exponent == 1 assert result.largest_exponent == 1
assert np.array_equal(result.coefficients, [3, 30]) assert np.array_equal(result.coefficients, [3, 30])
def test_function_multiplication(m_func_1, m_func_2):
"""Tests the multiplication of two Function objects."""
# (2x^2 + 3x + 1) * (5x -4) = 10x^3 + 7x^2 - 7x -4
result = m_func_1 * m_func_2
assert result.largest_exponent == 3
assert np.array_equal(result.coefficients, [10, 7, -7, -4])
# --- Genetic Algorithm Root-Finding Tests --- # --- Genetic Algorithm Root-Finding Tests ---
def test_get_real_roots_numpy(quadratic_func): def test_get_real_roots_numpy(quadratic_func):