import numpy as np
import matplotlib.pyplot as plt
import sys

# Configuration for plotting
plt.style.use('seaborn-v0_8')
plt.rcParams['figure.figsize'] = (18, 6)

def newton_method(f, df, x0, tolerance=1e-12, max_iter=20):
    """
    Implements Newton's Method with overflow protection.
    Returns: list of iteration history [(iter, x, error)]
    """
    history = []
    x = x0
    
    for i in range(max_iter):
        try:
            # Check for divergence first
            if abs(x) > 1e100:
                break

            fx = f(x)
            dfx = df(x)
            
            if abs(dfx) < 1e-15: # Avoid division by zero
                break
                
            history.append(x)
            
            x_new = x - fx / dfx
            
            # Check convergence
            if abs(x_new - x) < tolerance:
                x = x_new
                history.append(x)
                break
                
            x = x_new
        except OverflowError:
            # Catch errors if numbers become too large
            break
        
    return history

def chord_method(f, df_x0, x0, tolerance=1e-12, max_iter=50):
    """
    Implements Chord Method using fixed slope m = f'(x0) with overflow protection.
    Returns: list of iteration history
    """
    history = []
    x = x0
    m = df_x0 # Fixed slope
    
    for i in range(max_iter):
        try:
            if abs(x) > 1e100:
                break
                
            fx = f(x)
            history.append(x)
            
            if abs(m) < 1e-15:
                break
                
            x_new = x - fx / m
            
            if abs(x_new - x) < tolerance:
                x = x_new
                history.append(x)
                break
            x = x_new
        except OverflowError:
            break
            
    return history

def secant_method(f, x0, x_minus_1, tolerance=1e-12, max_iter=20):
    """
    Implements Secant Method with overflow protection.
    Returns: list of iteration history
    """
    history = []
    x_curr = x0
    x_prev = x_minus_1
    
    for i in range(max_iter):
        try:
            if abs(x_curr) > 1e100:
                break

            history.append(x_curr)
            
            fx_curr = f(x_curr)
            fx_prev = f(x_prev)
            
            if abs(fx_curr - fx_prev) < 1e-15:
                break
                
            # Secant update
            x_new = x_curr - fx_curr * (x_curr - x_prev) / (fx_curr - fx_prev)
            
            if abs(x_new - x_curr) < tolerance:
                x_curr = x_new
                history.append(x_curr)
                break
                
            x_prev = x_curr
            x_curr = x_new
        except OverflowError:
            break
        
    return history

def solve_and_save():
    # Definition of problems
    problems = [
        {
            'id': 'a',
            'title': r'$f(x) = \cos(x) - x, x_0 = 0.5$',
            'func_name': 'cos(x) - x',
            'f': lambda x: np.cos(x) - x,
            'df': lambda x: -np.sin(x) - 1,
            'x0': 0.5,
            'true_root': 0.73908513321516064165531208767387340401341175890075746496568063577328465488354759 # From wolframalpha 
        },
        {
            'id': 'b',
            'title': r'$f(x) = \sin(x), x_0 = 3$',
            'func_name': 'sin(x)',
            'f': lambda x: np.sin(x),
            'df': lambda x: np.cos(x),
            'x0': 3.0,
            'true_root': np.pi
        },
        {
            'id': 'c',
            'title': r'$f(x) = x^2 + 1, x_0 = 0.5$',
            'func_name': 'x^2 + 1',
            'f': lambda x: x**2 + 1,
            'df': lambda x: 2*x,
            'x0': 0.5,
            'true_root': None # No real root
        }
    ]
    
    # Open file for writing text results
    output_filename = 'numerical_results.txt'
    image_filename = 'convergence_results.png'
    
    with open(output_filename, 'w', encoding='utf-8') as txt_file:
        
        fig, axes = plt.subplots(1, 3)
        
        for idx, prob in enumerate(problems):
            header_str = f"\n{'='*60}\nProblem ({prob['id']}): {prob['func_name']}, x0={prob['x0']}\n{'='*60}\n"
            print(header_str) # Print to console to show progress
            txt_file.write(header_str)
            
            x0 = prob['x0']
            f = prob['f']
            df = prob['df']
            
            # 1. Run Newton
            hist_newton = newton_method(f, df, x0)
            
            # 2. Run Chord (m = f'(x0))
            hist_chord = chord_method(f, df(x0), x0)
            
            # 3. Run Secant (x_-1 = 0.99 * x0)
            x_minus_1 = 0.99 * x0
            hist_secant = secant_method(f, x0, x_minus_1)
            
            methods_data = {
                "Newton": hist_newton,
                "Chord": hist_chord,
                "Secant": hist_secant
            }
            
            ax = axes[idx]
            
            for m_name, history in methods_data.items():
                iters = list(range(len(history)))
                errors = []
                
                # Calculate errors for plotting
                # Note: Handling overflow for plotting large errors in problem c
                for x in history:
                    try:
                        if prob['true_root'] is not None:
                            val = abs(x - prob['true_root'])
                        else:
                            val = abs(f(x)) # Residual for problem c
                        errors.append(val)
                    except OverflowError:
                        errors.append(float('inf'))

                # Write to TXT file
                txt_file.write(f"\nMethod: {m_name}\n")
                txt_file.write(f"{'Iter':<5} | {'Value (x_k)':<25} | {'Error/Resid':<25}\n")
                txt_file.write("-" * 60 + "\n")
                
                for i, val in enumerate(history):
                    # Only write full history if short, or head/tail if long
                    if len(history) < 20 or i < 5 or i > len(history) - 3:
                        if prob['true_root'] is not None:
                            err_val = abs(val - prob['true_root'])
                        else:
                            try:
                                err_val = abs(f(val))
                            except OverflowError:
                                err_val = float('inf')
                                
                        txt_file.write(f"{i:<5} | {val:<25.10g} | {err_val:<25.6e}\n")
                    elif i == 5:
                        txt_file.write("...\n")

                # Add to plot
                if prob['true_root'] is not None:
                    ylabel_text = "Error |x_k - x*|"
                    log_scale = True
                    ax.plot(iters, errors, marker='o', label=m_name)
                else:
                    ylabel_text = "Residual |f(x_k)|"
                    log_scale = False
                    # For problem C, data explodes, so we limit plotting range to avoid ruining the graph
                    valid_errors = [e for e in errors if e < 1e10]
                    valid_iters = iters[:len(valid_errors)]
                    ax.plot(valid_iters, valid_errors, marker='x', label=m_name)

            ax.set_title(f"Problem ({prob['id']})\n{prob['title']}")
            ax.set_xlabel("Iteration")
            ax.set_ylabel(ylabel_text)
            if log_scale:
                ax.set_yscale('log')
            ax.legend()
            if prob['id'] == 'c':
                ax.set_ylim(0, 20)
                ax.set_xticks(range(0, len(iters), 1))
            ax.grid(True)

    print(f"\nText results saved to: {output_filename}")
    
    plt.tight_layout()
    plt.savefig(image_filename)
    print(f"Chart saved to: {image_filename}")

if __name__ == "__main__":
    solve_and_save()
