import numpy as np
from itertools import product

# Parameters
lower_threshold = 0.8
upper_threshold = 1.2
learning_rate = 0.1
max_iterations = 1000

# Inputs for the two-variable logic tables
inputs = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])

# Generate all 16 possible truth tables for 2 inputs
all_truth_tables = list(product([0, 1], repeat=4))  # 16 combinations of [0, 1, 1, 0] etc.

# Activation function
def activation_function(weighted_sum):
    return 1 if lower_threshold < weighted_sum < upper_threshold else 0

# Train the network for each truth table
for truth_table_idx, targets in enumerate(all_truth_tables):
    print(f"\nTraining for Truth Table {truth_table_idx + 1}: {targets}")
    
    # Initialize weights
    weights_input_hidden = np.random.rand(2, 2)  # 2 input neurons to 2 hidden neurons
    weights_hidden_output = np.random.rand(2)  # 2 hidden neurons to 1 output neuron

    # Track initial weights
    start_weights_input_hidden = weights_input_hidden.copy()
    start_weights_hidden_output = weights_hidden_output.copy()

    epoch = 0
    network_trained = False

    while epoch < max_iterations:
        epoch += 1
        all_correct = True  # Flag to track if all outputs are correct

        for input_vector, target in zip(inputs, targets):
            # Forward pass
            hidden_input = np.dot(input_vector, weights_input_hidden)  # Input -> Hidden
            hidden_output = np.array([activation_function(h) for h in hidden_input])  # Hidden activations
            
            final_input = np.dot(hidden_output, weights_hidden_output)  # Hidden -> Output
            output = activation_function(final_input)  # Output activation

            # Error calculation
            error = target - output

            # Backpropagation and weight update if error exists
            if error != 0:
                all_correct = False

                # Update weights for Hidden -> Output
                weights_hidden_output += learning_rate * error * hidden_output

                # Update weights for Input -> Hidden
                for i in range(2):  # Loop over hidden neurons
                    if hidden_output[i] > 0:  # Only update weights if neuron is active
                        weights_input_hidden[:, i] += learning_rate * error * input_vector

        if all_correct:
            network_trained = True
            break  # Stop training if all outputs are correct

    # Print results for the truth table
    if network_trained:
        print(f"The network learned the truth table correctly after {epoch} iterations.")
    else:
        print(f"The network failed to learn the truth table after {epoch} iterations.")

    # Test the trained network
    print("\nTesting the trained network:")
    for input_vector, target in zip(inputs, targets):
        hidden_input = np.dot(input_vector, weights_input_hidden)  # Input -> Hidden
        hidden_output = np.array([activation_function(h) for h in hidden_input])  # Hidden neuron activations
        
        final_input = np.dot(hidden_output, weights_hidden_output)  # Hidden -> Output
        output = activation_function(final_input)  # Activation of output neuron
        print(f"Input: {input_vector}, Target: {target}, Output: {output}")

    # Print weights for reference
    print("\nInitial Weights (Input -> Hidden):")
    print(start_weights_input_hidden)

    print("\nInitial Weights (Hidden -> Output):")
    print(start_weights_hidden_output)

    print("\nFinal Weights (Input -> Hidden):")
    print(weights_input_hidden)

    print("\nFinal Weights (Hidden -> Output):")
    print(weights_hidden_output)
    print("-----------------------------------------------------------")
