Skip to content

Commit

Permalink
Adding some tests
Browse files Browse the repository at this point in the history
Tereso del Rio committed Sep 23, 2023
1 parent f96c66f commit cc21e87
Showing 4 changed files with 477 additions and 0 deletions.
26 changes: 26 additions & 0 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Run Tests

on: [push]

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v2

- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.8

- name: Install dependencies
run: |
pip install -r requirements.txt
working-directory: ./UsingDorianFeatures

- name: Run tests
run: |
pytest
working-directory: ./UsingDorianFeatures
222 changes: 222 additions & 0 deletions utils/lucas_heuristic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
import sympy as sp
import numpy as np
from typing import List, Dict


def newton_raphson(equations: List[sp.Expr],
variables: List[sp.Symbol],
initial_guess: List[float],
tol: float = 1e-6,
max_iter: int = 100,
perturb_factor: float = 1e-5) -> List[float]:
"""
Approximate the root of a set of multivariate polynomials
using the Newton-Raphson method.

Args:
- equations (list of sympy expressions): The system of equations to solve.
- variables (list of sympy symbols): The variables in the equations.
- initial_guess (list of float): Initial guess for the root.
- tol (float): Tolerance for convergence.
- max_iter (int): Maximum number of iterations.
- perturb_factor (float): Factor to perturb the root when the
Jacobian determinant is close to zero.

Returns:
- list of float: Approximated root.
"""

# print(equations, variables, initial_guess)
m_vars = sp.Matrix(variables)
f = sp.Matrix(equations)
J = f.jacobian(m_vars)
root = sp.Matrix(initial_guess)

for i in range(max_iter):
f_eval = [float(expr.subs(list(zip(m_vars, root)))) for expr in f]

if all(abs(val) < tol for val in f_eval):
# print('max_iter', i)
return [float(val) for val in root]

J_eval = J.subs(list(zip(m_vars, root)))
determinant = J_eval.det()

# Set a counter
tries = 0
while abs(determinant) < tol and tries < max_iter:
tries += 1
# print(tries, max_iter)
# Perturb the root slightly
perturbation = np.random.rand(len(variables)) * perturb_factor
root += sp.Matrix(perturbation)
J_eval = J.subs(list(zip(m_vars, root)))
determinant = J_eval.det()

# Exit if the determinant is still to low
if tries >= max_iter:
break

delta_m_vars = -J_eval.inv() * sp.Matrix(f_eval)
root += delta_m_vars

raise ValueError("Newton-Raphson method did not converge within "
"the specified maximum number of iterations.")


def find_roots(equations: List[sp.Expr],
variables: List[sp.Symbol],
tol: float = 1e-6,
trials_factor: int = 5,
min_trials: int = 10) -> List[List[float]]:
"""
Find roots of a set of multivariate polynomials
using the Newton-Raphson method
with automatic initial guess generation and
stopping criteria based on equation characteristics.

Args:
- equations (list of sympy expressions): The system of equations to solve.
- variables (list of sympy symbols): The variables in the equations.
- tol (float): Tolerance for convergence.
- trials_factor (int): The higher, the more times we look for new roots
- min_trials (int): Minimum number of times we look for new roots

Returns:
- list of lists of float: Approximated roots.
"""

if len(equations) != len(variables):
raise ValueError("The number of independent equations should be equal"
"to the number of variables to have finite roots.")

roots = []
attempts = 0

# Choose the range for the initial guesses
guess_range = 0
# Choose maximum number of attempts to find new roots
max_attempts = min_trials
for polynomial in equations:
# Compute the total degree of the polynomial
total_degree = polynomial.as_poly().total_degree()
if total_degree != 0:
abs_indep_coeff = abs(polynomial.as_coefficients_dict()[1])
guess_range = max(guess_range,
2 * abs_indep_coeff ** (1 / total_degree))
max_attempts = total_degree * trials_factor

while attempts < max_attempts:
# Generate a random initial guess
initial_guess = [np.random.uniform(-guess_range, guess_range)
for _ in variables]
# print('initial_guess', initial_guess)

try:
root = newton_raphson(equations, variables, initial_guess, tol)
# print('root', root)

# Check if the root is new (within tolerance)
is_new = all(
np.linalg.norm(np.array(root) - np.array(existing_root)) > tol
for existing_root in roots
)

if is_new:
roots.append(root)
attempts = 0 # Reset attempts if a new root is found
else:
# print('not new')
attempts += 1

except ValueError:
# Count the attempt if convergence error for this initial guess
attempts += 1

return roots


def critical_points(polynomial: sp.Expr,
trials_factor: int = 5) -> Dict[sp.Symbol, int]:
'''
Compute the critical points of the given polynomial with respect
to each of its variables.

Args:
- polynomial (sympy expression): The polynomial expression.
- trials_factor (int): The higher, the more times we look for new roots

Returns:
- dict: A dictionary containing the number of critical points
for each variable in the polynomial.
'''

# Find its variables
variables = list(polynomial.free_symbols)

# Store derivatives
derivatives = {}
for var in variables:
derivatives[var] = sp.diff(polynomial, var)

# Dictionary to store number of critical points
critical_points = dict()
for var in variables:
# print('var', var)
# List of equations containing:
# derivatives with respect to other variables
# and the original polynomial
equations = [derivatives[aux_var] for aux_var in variables
if aux_var != var] + [polynomial]

# Use a root-finding function to find the critical points,
# that are the common roots of the equations
roots = find_roots(equations, variables, trials_factor=trials_factor)
# print('roots', roots)
critical_points[var] = len(roots)

return critical_points


def count_critical_points(polynomials: List[sp.Expr]) -> Dict[sp.Symbol, int]:
"""
Compute the total count of critical points
for each variable in a set of polynomials.

Args:
- polynomials (list of sympy expressions):
The set of multivariate polynomials to analyze.

Returns:
- dict: A dictionary where keys are variables
and values are the count of critical points.
"""

# Find all unique variables present in the set of polynomials
unique_variables = set()
for polynomial in polynomials:
unique_variables.update(polynomial.free_symbols)

# Initialize a dictionary to store the total count of critical points
# for each variable
total_critical_points = {variable: 0 for variable in unique_variables}

# Iterate through each polynomial to find critical points
# and update the counts
for polynomial in polynomials:
for variable, count in critical_points(polynomial).items():
total_critical_points[variable] += count

return total_critical_points


# Example usage:
if __name__ == "__main__":
# Define a set of multivariate polynomials
x, y = sp.symbols('x y')
polynomials = [x**2 + y**2 - 4, x**3 * y - 4 * y**3]
polynomials = []

# Compute the total count of critical values for each variable
critical_values = count_critical_points(polynomials)
print(critical_values) # Example output: {y: 3, x: 3}
184 changes: 184 additions & 0 deletions utils/test_lucas_heuristic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import sympy as sp
import unittest
import time

# Import the critical_points function from your module
from lucas_heuristic import critical_points
from lucas_heuristic import find_roots
from lucas_heuristic import newton_raphson
from lucas_heuristic import count_critical_points


class TestCountCriticalPoints(unittest.TestCase):

def test_empty_input(self):
# Test with an empty list of polynomials
result = count_critical_points([])
expected = dict()
self.assertEqual(result, expected)

def test_single_polynomial(self):
# Test with a single polynomial with known critical points
x, y = sp.symbols('x y')
polynomial = x**2 + y**2 - 4
result = count_critical_points([polynomial])
expected = {x: 2, y: 2} # Expected number of critical points
self.assertEqual(result, expected)

def test_multiple_polynomials(self):
# Test with multiple polynomials with known critical points
x, y = sp.symbols('x y')
polynomials = [x**2 + y**2 - 4,
x**3 * y - 4 * y**3]
result = count_critical_points(polynomials)
expected = {x: 3, y: 3} # Expected number of critical points
self.assertEqual(result, expected)


class TestNewtonRaphson(unittest.TestCase):

def test_valid_input(self):
# Test with a valid set of equations, variables, and initial guess
x, y = sp.symbols('x y')
equations = [x**2 - 4,
y**2 - 9] # Two simple equations with roots (+-2, +-3)
variables = [x, y]
initial_guess = [1, 1]
root = newton_raphson(equations, variables, initial_guess)

# Ensure the root is one of the possibilities
self.assertAlmostEqual(root[0], 2, msg="The root given is not a root")
self.assertAlmostEqual(root[1], 3, msg="The root given is not a root")

def test_invalid_input(self):
# Test with invalid input where the number of equations
# and variables don't match
x, y, z = sp.symbols('x y z')
equations = [x**2 - 4, y**2 - 9] # Two equations, but three variables
variables = [x, y, z]
initial_guess = [1, 1]

with self.assertRaises(ValueError):
newton_raphson(equations, variables, initial_guess)

def test_non_converging_case(self):
# Test a case where Newton-Raphson method
# doesn't converge within max_iter
x = sp.symbols('x')
equations = [x**2 + 1] # No real roots
variables = [x]
initial_guess = [1]

with self.assertRaises(ValueError):
newton_raphson(equations, variables, initial_guess, max_iter=10)

def test_not_infinite_loop(self):
# It was observed that there can be an infinite loop
# this was fixed but example added here
# this test takes more than 10s
# if it enters the almost infinite loop

# Start measuring time
start_time = time.time()

x, y = sp.symbols('x y')
equations = [x**3, x**3*y - 4]
variables = [x, y]
initial_guess = [-1, 0]

with self.assertRaises(ValueError):
newton_raphson(equations, variables, initial_guess)

# Total time taken
execution_time = time.time() - start_time
# Assert that the execution time is less than 1 second
self.assertLess(execution_time, 1.0, "Test took longer than 1 second")


class TestFindRoots(unittest.TestCase):

def test_valid_equations(self):
# Test with valid equations and variables
x, y = sp.symbols('x y')
equations = [x**2 + y**2 - 1, # Circle equation
x - y] # diagonal line
variables = [x, y]
tol = 1e-6
roots = find_roots(equations, variables, tol=tol)

# Ensure the number of roots is correct (should be 2)
self.assertEqual(len(roots), 2)

# Ensure that they are the correct roots
sqrt2div2 = 0.7071068
for root in roots:
for coordinate in root:
self.assertAlmostEqual(abs(coordinate), sqrt2div2)

def test_invalid_input(self):
# Test with invalid input; number of equations
# and variables dont match
x, y, z = sp.symbols('x y z')
equations = [x**2 - 4, y**2 - 9] # Two equations
variables = [x, y, z] # but three variables

with self.assertRaises(ValueError):
find_roots(equations, variables)

def test_no_roots(self):
# Test with an invalid equation that cannot be solved
x, y = sp.symbols('x y')
equations = [x**2 + y**2 + 1, # No real roots
x - y]
variables = [x, y]
roots = find_roots(equations, variables)

# Ensure no roots are found (empty list)
self.assertEqual(len(roots), 0)


class TestCriticalPoints(unittest.TestCase):

def test_single_variable(self):
# Test a polynomial with a single variable (e.g., x)
x = sp.symbols('x')
poly = x**2 + 3*x + 2
result = critical_points(poly)
expected = {x: 2}
self.assertDictEqual(result, expected)

def test_two_variables(self):
# Test a polynomial with two variables
x, y = sp.symbols('x y')
poly = x**2 + y**2 - 4
result = critical_points(poly)
expected = {x: 2, y: 2}
self.assertDictEqual(result, expected)

def test_no_independent_coefficient(self):
# Test a polynomial with no critical points
x, y = sp.symbols('x y')
poly = x**2 + y**2 * x
result = critical_points(poly)
expected = {x: 1, y: 1}
self.assertDictEqual(result, expected)

def test_no_critical_points(self):
# Test a polynomial with no critical points
x, y = sp.symbols('x y')
poly = x**2 + y**2 + 1
result = critical_points(poly)
expected = {x: 0, y: 0}
self.assertDictEqual(result, expected)

def test_complex_critical_points(self):
# Test a polynomial with complex critical points
x, y, z, w = sp.symbols('x y z w')
poly = x**4 + y**4 + z**4 + w**4 - 6*x**2 - 6*y**2 - 6*z**2 - 6*w**2
result = critical_points(poly, 10)
expected = {x: 1, y: 1, z: 1, w: 1}
self.assertDictEqual(result, expected)


if __name__ == '__main__':
unittest.main()
45 changes: 45 additions & 0 deletions utils/trial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import sympy as sp


def create_polynomial(monomials):
"""
Create a polynomial from a list of monomials, where each monomial is represented as [degrees, coefficient].

Args:
- monomials (list of lists): List of monomials, where each monomial is represented as [degrees, coefficient].

Returns:
- sympy expression: The polynomial expression.
"""

# Make sure the list of monomials is not empty
if monomials == []:
return 0

# Compute the number of variables
num_variables = len(monomials[0]) - 1

# Create symbolic variables in a loop and store them in a list
variables = [sp.symbols(f'x{i}') for i in range(num_variables)]

polynomial = sum(coefficient * sp.prod(var**degree
for var, degree in zip(variables, degrees))
for *degrees, coefficient in monomials)

return polynomial

# Example usage:
# if __name__ == "__main__":

# # List of monomials, each represented as [degrees, coefficient]
# monomials = [[1, 2, 0, 1], [1, 2, 3, 4]]

# # Create the polynomial
# polynomial = create_polynomial(monomials)

# # Print the polynomial
# print("Polynomial:", polynomial)

dict1 = {'a': 2, 'b': 1, 'c': 3}
dict2 = {'b': 1, 'a': 2, 'c': 3}
assertDictEqual(dict1, dict2)

0 comments on commit cc21e87

Please sign in to comment.