From a761efe28e404000b6f344a7884c755f5943b0d1 Mon Sep 17 00:00:00 2001 From: Jonathan Rampersad Date: Tue, 17 Jun 2025 13:45:51 -0400 Subject: [PATCH 01/11] fix: Renamed differential function to derivitive --- src/polysolve/__init__.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/polysolve/__init__.py b/src/polysolve/__init__.py index 5ef267d..ef9c9b1 100644 --- a/src/polysolve/__init__.py +++ b/src/polysolve/__init__.py @@ -129,15 +129,36 @@ class Function: Returns: Function: A new Function object representing the derivative. """ + warnings.warn( + "The 'differential' function has been renamed. Please use 'derivitive' 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 derivitive(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: """ From 3d2c724ad45fe4ee445a43143dfa3507f210772b Mon Sep 17 00:00:00 2001 From: Jonathan Rampersad Date: Tue, 17 Jun 2025 14:06:45 -0400 Subject: [PATCH 02/11] feat: Add nth derivative function and fix: typo derivitive->derivative --- src/polysolve/__init__.py | 36 +++++++++++++++++++++++++++++++++--- tests/test_polysolve.py | 11 +++++++++-- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/src/polysolve/__init__.py b/src/polysolve/__init__.py index ef9c9b1..6f2ae5a 100644 --- a/src/polysolve/__init__.py +++ b/src/polysolve/__init__.py @@ -130,7 +130,7 @@ class Function: Function: A new Function object representing the derivative. """ warnings.warn( - "The 'differential' function has been renamed. Please use 'derivitive' instead.", + "The 'differential' function has been renamed. Please use 'derivative' instead.", DeprecationWarning, stacklevel=2 ) @@ -142,7 +142,7 @@ class Function: return self.derivitive() - def derivitive(self) -> 'Function': + def derivative(self) -> 'Function': """ Calculates the derivative of the function. @@ -160,6 +160,36 @@ class Function: 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: """ Uses a genetic algorithm to find the approximate real roots of the function (where y=0). @@ -429,7 +459,7 @@ if __name__ == '__main__': 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() + df1 = f1.derivative() print(f"Derivative of f1: {df1}") # --- Root Finding --- diff --git a/tests/test_polysolve.py b/tests/test_polysolve.py index ad03ca5..4bf0f11 100644 --- a/tests/test_polysolve.py +++ b/tests/test_polysolve.py @@ -32,13 +32,20 @@ def test_solve_y(quadratic_func): assert quadratic_func.solve_y(0) == -5.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.""" - derivative = quadratic_func.differential() + derivative = quadratic_func.derivative() assert derivative.largest_exponent == 1 # The derivative of 2x^2 - 3x - 5 is 4x - 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): """Tests the analytical quadratic solver for exact roots.""" roots = quadratic_solve(quadratic_func) From 30a5189928d0a1000450e7945ab4ae2e57ab0a36 Mon Sep 17 00:00:00 2001 From: Jonathan Rampersad Date: Tue, 17 Jun 2025 14:08:36 -0400 Subject: [PATCH 03/11] fix: multiplying by 0 returns a function object representing 0 --- src/polysolve/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/polysolve/__init__.py b/src/polysolve/__init__.py index 6f2ae5a..15d9fc3 100644 --- a/src/polysolve/__init__.py +++ b/src/polysolve/__init__.py @@ -394,7 +394,9 @@ class Function: if not isinstance(scalar, (int, float)): return NotImplemented 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 From 8656b558b42c0d32ef80b0256669bb9f0af86f59 Mon Sep 17 00:00:00 2001 From: Jonathan Rampersad Date: Tue, 17 Jun 2025 14:12:19 -0400 Subject: [PATCH 04/11] feat: Added alternative degree property to return largest_exponent --- src/polysolve/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/polysolve/__init__.py b/src/polysolve/__init__.py index 15d9fc3..885d048 100644 --- a/src/polysolve/__init__.py +++ b/src/polysolve/__init__.py @@ -108,6 +108,11 @@ class Function: def largest_exponent(self) -> int: """Returns the largest exponent of the function.""" 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: """ From ee414ea0dc309432aef924d7f6ea115bb53da9c5 Mon Sep 17 00:00:00 2001 From: Jonathan Rampersad Date: Tue, 17 Jun 2025 14:26:26 -0400 Subject: [PATCH 05/11] feat: Added function * function multiplication --- src/polysolve/__init__.py | 71 ++++++++++++++++++++++++++++----------- tests/test_polysolve.py | 21 +++++++++++- 2 files changed, 72 insertions(+), 20 deletions(-) diff --git a/src/polysolve/__init__.py b/src/polysolve/__init__.py index 885d048..a342ed0 100644 --- a/src/polysolve/__init__.py +++ b/src/polysolve/__init__.py @@ -1,7 +1,7 @@ import math import numpy as np from dataclasses import dataclass -from typing import List, Optional +from typing import List, Optional, Union import warnings # Attempt to import CuPy for CUDA acceleration. @@ -392,36 +392,55 @@ class Function: 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 + + def _multiply_by_scalar(self, scalar: Union[int, float]) -> 'Function': + """Helper method to multiply the function by a scalar constant.""" + self._check_initialized() # It's good practice to check here too + if scalar == 0: result_func = Function(0) result_func.set_coeffs([0]) return result_func - + 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': + 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).""" + 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).""" - 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 @@ -506,4 +525,18 @@ if __name__ == '__main__': # Multiplication: (x + 10) * 3 = 3x + 30 f_mul = f2 * 3 - print(f"f2 * 3 = {f_mul}") \ No newline at end of file + 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}") diff --git a/tests/test_polysolve.py b/tests/test_polysolve.py index 4bf0f11..e0a502a 100644 --- a/tests/test_polysolve.py +++ b/tests/test_polysolve.py @@ -24,6 +24,18 @@ def linear_func() -> Function: f.set_coeffs([1, 10]) 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 --- def test_solve_y(quadratic_func): @@ -68,13 +80,20 @@ def test_subtraction(quadratic_func, linear_func): assert result.largest_exponent == 2 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.""" # (x + 10) * 3 = 3x + 30 result = linear_func * 3 assert result.largest_exponent == 1 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, [19, 7, -7, -4]) + # --- Genetic Algorithm Root-Finding Tests --- def test_get_real_roots_numpy(quadratic_func): From 25f20a4db24f3bea3598b97d8c76e33608224ad5 Mon Sep 17 00:00:00 2001 From: Jonathan Rampersad Date: Tue, 17 Jun 2025 14:26:45 -0400 Subject: [PATCH 06/11] v0.2.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b9fd3c4..f46ff93 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] # --- Core Metadata --- name = "polysolve" -version = "0.1.1" +version = "0.2.0" authors = [ { name="Jonathan Rampersad", email="jonathan@jono-rams.work" }, ] From 36f51ca67ebf4737a994e29a61e402c86aca87d1 Mon Sep 17 00:00:00 2001 From: Jonathan Rampersad Date: Tue, 17 Jun 2025 14:27:53 -0400 Subject: [PATCH 07/11] fix: Typo in test --- tests/test_polysolve.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_polysolve.py b/tests/test_polysolve.py index e0a502a..ccf22bb 100644 --- a/tests/test_polysolve.py +++ b/tests/test_polysolve.py @@ -92,7 +92,7 @@ def test_function_multiplication(m_func_1, m_func_2): # (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, [19, 7, -7, -4]) + assert np.array_equal(result.coefficients, [10, 7, -7, -4]) # --- Genetic Algorithm Root-Finding Tests --- From 41daf4f7e0454ae7c3a4b8c2dea2a4ea51b6d2be Mon Sep 17 00:00:00 2001 From: Jonathan Rampersad Date: Tue, 17 Jun 2025 14:30:11 -0400 Subject: [PATCH 08/11] Remove CONTRIBUTORS.md --- CONTRIBUTORS.md | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 CONTRIBUTORS.md diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md deleted file mode 100644 index 7c37bdb..0000000 --- a/CONTRIBUTORS.md +++ /dev/null @@ -1,10 +0,0 @@ -## Contributors - - - - -[![All Contributors](https://img.shields.io/github/all-contributors/jono-rams/PolySolve?color=ee8449&style=flat-square)](#contributors) - - - - From d27497488fc4276eb4e8fb421fbe974ffd0899df Mon Sep 17 00:00:00 2001 From: Jonathan Rampersad Date: Tue, 17 Jun 2025 14:30:40 -0400 Subject: [PATCH 09/11] fix: differential in README.md renamed to derivative --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f85aba5..7f2d943 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ 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() +df1 = f1.derivative() print(f"Derivative of f1: {df1}") # > Derivative of f1: 4x - 3 From ec97aefee16e11de2e67be7d3681b50ac64b5f97 Mon Sep 17 00:00:00 2001 From: Jonathan Rampersad Date: Tue, 17 Jun 2025 14:34:16 -0400 Subject: [PATCH 10/11] feat: Added nth derivative showcase in __main__ --- src/polysolve/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/polysolve/__init__.py b/src/polysolve/__init__.py index a342ed0..260acc9 100644 --- a/src/polysolve/__init__.py +++ b/src/polysolve/__init__.py @@ -488,6 +488,10 @@ if __name__ == '__main__': df1 = f1.derivative() 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 --- # 1. Analytical solution for quadratic roots_analytic = quadratic_solve(f1) From ac591f49ec17908101eab5fdf952bf136adef642 Mon Sep 17 00:00:00 2001 From: Jonathan Rampersad Date: Tue, 17 Jun 2025 14:34:36 -0400 Subject: [PATCH 11/11] docs: Added documentation for nth_derivative function --- README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7f2d943..e9f180a 100644 --- a/README.md +++ b/README.md @@ -60,13 +60,18 @@ df1 = f1.derivative() print(f"Derivative of f1: {df1}") # > 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. 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) +# 6. 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)