import numpy as np

# Parameters
learning_rate = 0.1
max_iterations = 1000
lower_threshold = 0.8  # Lower threshold for activation
upper_threshold = 1.2  # Upper threshold for activation

# Threshold activation function with two thresholds
def activation_function(weighted_sum):
    if lower_threshold < weighted_sum < upper_threshold:
        return 1
    else:
        return 0

# Generate all 16 possible truth tables for 2 inputs
inputs = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
all_targets = [
    [0, 0, 0, 0],  # 0
    [0, 0, 0, 1],  # 1
    [0, 0, 1, 0],  # 2
    [0, 0, 1, 1],  # 3
    [0, 1, 0, 0],  # 4
    [0, 1, 0, 1],  # 5
    [0, 1, 1, 0],  # 6
    [0, 1, 1, 1],  # 7
    [1, 0, 0, 0],  # 8
    [1, 0, 0, 1],  # 9
    [1, 0, 1, 0],  # 10
    [1, 0, 1, 1],  # 11
    [1, 1, 0, 0],  # 12
    [1, 1, 0, 1],  # 13
    [1, 1, 1, 0],  # 14
    [1, 1, 1, 1],  # 15
]

# Initialize weights for the neurons
weights_input_to_hidden = np.random.rand(3)  # 2 inputs + 1 bias → 1 hidden neuron
weights_hidden_to_output = np.random.rand(2)  # 1 hidden neuron + 1 bias → 1 output neuron

# Training loop
network_trained = False
for target in all_targets:  # Train for all possible tables
    epoch = 0
    while epoch < max_iterations:
        epoch += 1
        all_correct = True  # Flag to track if all outputs are correct

        for input_vector, expected_target in zip(inputs, target):
            N1, N2 = input_vector
            bias = 1  # Bias input

            # Forward pass (hidden layer)
            N3_input = np.dot(np.append(input_vector, bias), weights_input_to_hidden)  # Hidden layer sum
            N3 = activation_function(N3_input)  # Output of the hidden neuron

            # Output layer
            N4_input = (N3 * weights_hidden_to_output[0] + bias * weights_hidden_to_output[1])  # Weighted sum to output
            N4 = activation_function(N4_input)  # Output neuron decision

            # Error calculation
            error = expected_target - N4

            # If there is an error, update the weights
            if error != 0:
                all_correct = False

                # Update weights for hidden to output neuron
                weights_hidden_to_output[0] += learning_rate * error * N3
                weights_hidden_to_output[1] += learning_rate * error * bias

                # Update weights for input to hidden neuron
                weights_input_to_hidden += learning_rate * error * N3 * np.append(input_vector, bias)

        if all_correct:
            network_trained = True
            break

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

# Testing the trained network
print("\nTesting the trained network:")
for target_index, target in enumerate(all_targets):
    print(f"Testing Table {target_index}:")
    for input_vector, expected_target in zip(inputs, target):
        N1, N2 = input_vector
        bias = 1

        # Forward pass (hidden layer)
        N3_input = np.dot(np.append(input_vector, bias), weights_input_to_hidden)
        N3 = activation_function(N3_input)

        # Output layer
        N4_input = (N3 * weights_hidden_to_output[0] + bias * weights_hidden_to_output[1])
        N4 = activation_function(N4_input)

        print(f"Input: {input_vector}, Target: {expected_target}, Output: {N4}")
