-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Tereso del Rio
committed
Sep 23, 2023
1 parent
f96c66f
commit cc21e87
Showing
4 changed files
with
477 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |