import numpy as np

# Define inputs for all truth tables
inputs = np.array([
    [0, 0],
    [0, 1],
    [1, 0],
    [1, 1]
])

# Define all possible truth tables as target outputs
truth_tables = [
    [0, 0, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0], [0, 0, 1, 1],
    [0, 1, 0, 0], [0, 1, 0, 1], [0, 1, 1, 0], [0, 1, 1, 1],
    [1, 0, 0, 0], [1, 0, 0, 1], [1, 0, 1, 0], [1, 0, 1, 1],
    [1, 1, 0, 0], [1, 1, 0, 1], [1, 1, 1, 0], [1, 1, 1, 1]
]

# Define the perceptron learning function
def perceptron_learning(inputs, targets, learning_rate=0.1, max_epochs=100):
    for attempt in range(10):  # Allow up to 10 restarts
        # Initialize weights and bias with random values (can be negative)
        weights = np.random.uniform(-1, 1, inputs.shape[1])
        bias = np.random.uniform(-1, 1)
        
        for epoch in range(max_epochs):
            error_count = 0
            for i, input_vector in enumerate(inputs):
                # Compute the perceptron output
                linear_combination = np.dot(input_vector, weights) + bias
                output = 1 if linear_combination >= 0 else 0
                
                # Calculate the error
                error = targets[i] - output
                
                # Update weights and bias if there is an error
                if error != 0:
                    weights += learning_rate * error * input_vector
                    bias += learning_rate * error
                    error_count += 1
            
            # If no errors, learning is complete
            if error_count == 0:
                return weights, bias, epoch + 1  # Successful learning
        
        # Reset weights and bias for a new attempt
        print(f"Failed to learn after {max_epochs} epochs. Restarting with new weights and bias...")
    
    # If unable to learn after all attempts, return failure
    print("Perceptron failed to learn this truth table.")
    return None, None, None

# Test perceptron for all truth tables
results = []
for idx, table in enumerate(truth_tables):
    print(f"\n=== Truth Table {idx + 1}: Targets = {table} ===")
    weights, bias, epochs = perceptron_learning(inputs, table)
    
    if weights is not None:
        # Test the final perceptron on inputs
        outputs = [1 if np.dot(input_vector, weights) + bias >= 0 else 0 for input_vector in inputs]
        
        # Display results
        print(f"Final Weights: {weights}")
        print(f"Final Bias: {bias}")
        print(f"Epochs to Learn: {epochs}")
        print(f"Final Test Output: {outputs}")
        
        # Store the results
        results.append({
            "truth_table": table,
            "weights": weights.tolist(),
            "bias": bias,
            "epochs": epochs,
            "outputs": outputs
        })
    else:
        print("Learning failed for this truth table.")
        results.append({
            "truth_table": table,
            "weights": None,
            "bias": None,
            "epochs": None,
            "outputs": None
        })
