# Übungsblatt 3 - Evolutionary Algorithms (Paul Helstab & Yannik Bretschneider)

## Loading Data

```{r setwd}
# Get the current working directory
current_directory <- getwd()

# Define the folder name to check
folder_name <- "w3"

# Construct the full path to the folder
folder_path <- file.path(current_directory, folder_name)

# Check if the folder exists (means most likely in the repo root folder)
if (dir.exists(folder_path)) {
    setwd(folder_path)
    cat("Changed working directory to:", getwd(), "\n")
} else {
    cat("Folder 'w3' does not exist in the current working directory.
Staying where we are\n")
}
```


```{r read_data}
file_path <- "./data1.txt"

print(paste0("read data from '", file_path, "'"))
data <- read.table(
    file_path,
    header = FALSE,
    sep = " ",
    # colClasses = c("numeric", "numeric", "integer", "integer")
)
colnames(data) <- c("x1", "x2", "x_bias", "label")


evaluation_set_idx <- c(90:109)
```

```r
# possible, but unnecessary here:
# grab first 10 as evaluation set
evaluation_set <- data[evaluation_set_idx, ]

# grab the rest as training set
training_set <- data[-evaluation_set_idx, ]
```

## Aufgabe 1: Perzeptron

```{r peceptron-impl}
# randomized order
random_order <- sample(nrow(data))
vecs <- data[random_order, 1:3]
classes <- data[random_order, 4]

weights <- c(0, 0, 0)

# matrix for logging weights
wlog <- matrix(weights, ncol = 3)

infer <- function(x, wghts) {
    return(sign(x %*% wghts))
}

perceptron_infer <- function(x) {
    return(infer(x, weights))
}

while (TRUE) {
    mispredictions <- 0
    for (i in seq_len(nrow(vecs))) {
        vec <- as.numeric(vecs[i, ])
        class <- classes[i]

        # print(vec %*% weights)

        error <- as.numeric(class - perceptron_infer(vec))
        if (error != 0) {
            mispredictions <- mispredictions + 1

            print("adjusting")
            weights <- weights + error * vec
            wlog <- rbind(wlog, weights)
        }
    }


    if (mispredictions == 0) {
        print("No more mispredictions")
        break
    } else {
        print(paste0("Misclassifications: ", mispredictions))
    }
}

print("NOTE: for more interesting plots, run this chunk multiple times until multiple \"adjusting\" can be seen in the output")
```

```{r apply-to-data}
perceptron_reinfer <- t(sapply(seq_len(nrow(data)), function(i) {
    return(c(as.numeric(data[i, 1:3]), perceptron_infer((as.numeric(data[i, 1:3])))))
}))

# perceptron_reinfer
```


### Plots

The plots show the datapoints on a 2D plot, with the colors showing the classes (`green := 1` and `red := -1`)  
The separation plane defined by the normal of the weight vector is shown in increasing opacities, until the final 
weights are reached.

```{r plot-weights}
perceptron_plot <- function() {
    plot(data$x1, data$x2,
        col = ifelse(data$label == 1, "green", "red"),
        pch = 16, xlab = "x1", ylab = "x2", xlim = c(-5, 5), ylim = c(-5, 5)
    )

    legend("topright",
        legend = c("Class 1", "Class -1"),
        col = c("green", "red"), pch = 16
    )

    # Plot the separation planes
    num_steps <- nrow(wlog)
    alpha_step <- 1 / num_steps

    for (i in 1:num_steps) {
        a <- wlog[i, 1]
        b <- wlog[i, 2]
        c <- wlog[i, 3]

        # Calculate the line coordinates
        x <- seq(-5, 5, length.out = 100)
        y <- -(a * x + c) / b

        # Determine the line color based on the iteration
        alpha <- i * alpha_step
        line_color <- rgb(0, 0, 1, alpha)

        # Plot the line
        lines(x, y, col = line_color)
    }

    # Plot the final separation plane
    a <- weights[1]
    b <- weights[2]
    c <- weights[3]

    x <- seq(-5, 5, length.out = 100)
    y <- -(a * x + c) / b

    lines(x, y, col = "blue", lwd = 2)
}

perceptron_plot()
```

```{r plot-weights-pdf}
pdf("perceptron-plot.pdf", width = 8, height = 6)
perceptron_plot()
dev.off()
```



## Aufgabe 2: Perzeptron Evolutionary Algorithm (EA)


```{r peceptron-ea-impl}
# randomized order
ea_random_order <- sample(nrow(data))
ea_vecs <- data[ea_random_order, 1:3]
ea_classes <- data[ea_random_order, 4]

NUM_INDIVIDUALS <- 3
MUTATE_ALL_GENES <- FALSE
MUTATE_STEP <- function() {
    return(0.5)
}

# MUTATE_STEP <- function() {
#     return(runif(1, min = -1, max = 1)[1])
# }

weights <- matrix(0, ncol = 3, nrow = NUM_INDIVIDUALS)

# matrix for logging weights
ea_wlog <- matrix(0, ncol = 3)

fittest <- c(0, 0, 0)


mutate <- function(genes, step_size_fn, mutate_all) {
    if (mutate_all == TRUE) {
        for (i in seq_along(genes)) {
            step <- step_size_fn() * sample(c(1, -1), size = 1)[1] # mutation in step_size, -step_size or not at all
            genes[i] <- genes[i] + step
        }
    } else {
        idx_to_mutate <- sample(seq_along(genes), size = 1)[1]
        genes[idx_to_mutate] <- genes[idx_to_mutate] + step_size_fn() * sample(c(1, -1), size = 1)[1] # mutation in step_size or not AT all
    }

    return(genes)
}

infer <- function(x, wghts) {
    return(sign(x %*% wghts))
}

fitness <- function(wghts) {
    mispredictions <- 0


    for (i in seq_len(nrow(ea_vecs))) {
        vec <- as.numeric(ea_vecs[i, ])
        class <- ea_classes[i]

        # print(vec %*% weights)

        error <- as.numeric(class - infer(vec, wghts))
        if (error != 0) {
            mispredictions <- mispredictions + 1
        }
    }

    return(mispredictions)
}

while (TRUE) {
    i_fittest <- weights[1, ]
    i_fit <- fitness(weights[1, ])

    for (individual in 2:NUM_INDIVIDUALS) {
        this_fitness <- fitness(weights[individual, ])
        if (this_fitness < i_fit) {
            i_fittest <- weights[individual, ]
            i_fit <- this_fitness
        }
    }

    # add fittest to weights
    ea_wlog <- rbind(ea_wlog, i_fittest)

    if (i_fit == 0) {
        print("No more mispredictions")
        fittest <- i_fittest
        break
    } else {
        print(paste0("Misclassifications: ", i_fit))

        for (individual in 1:NUM_INDIVIDUALS) {
            weights[individual, ] <- mutate(i_fittest, step_size_fn = MUTATE_STEP, mutate_all = MUTATE_ALL_GENES)
        }
    }
}
```


```{r ea-apply-to-data}
ea_reinfer <- t(sapply(seq_len(nrow(data)), function(i) {
    return(c(as.numeric(data[i, 1:3]), infer((as.numeric(data[i, 1:3])), fittest)))
}))

# ea_reinfer
```

### Plots

The plots show the datapoints on a 2D plot, with the colors showing the classes (`green := 1` and `red := -1`)  
The separation plane defined by the normal of the weight vector is shown in increasing opacities, until the final 
weights are reached.

```{r ea-plot-weights}
ea_plot <- function() {
    plot(data$x1, data$x2,
        col = ifelse(data$label == 1, "green", "red"),
        pch = 16, xlab = "x1", ylab = "x2", xlim = c(-5, 5), ylim = c(-5, 5)
    )

    legend("topright",
        legend = c("Class 1", "Class -1"),
        col = c("green", "red"), pch = 16
    )

    # Plot the separation planes
    num_steps <- nrow(ea_wlog)
    alpha_step <- 1 / num_steps

    for (i in 1:num_steps) {
        a <- ea_wlog[i, 1]
        b <- ea_wlog[i, 2]
        c <- ea_wlog[i, 3]

        # Calculate the line coordinates
        x <- seq(-5, 5, length.out = 100)
        y <- -(a * x + c) / b

        # Determine the line color based on the iteration
        alpha <- i * alpha_step
        line_color <- rgb(0, 0, 1, alpha)

        # Plot the line
        lines(x, y, col = line_color)
    }

    # Plot the final separation plane
    a <- fittest[1]
    b <- fittest[2]
    c <- fittest[3]

    x <- seq(-5, 5, length.out = 100)
    y <- -(a * x + c) / b

    lines(x, y, col = "blue", lwd = 2)
}

ea_plot()
```


```{r plot-weights-pdf}
pdf("ea-plot_static-delta_one.pdf", width = 8, height = 6)
ea_plot()
dev.off()
```

### Evaluation

Gaussian (Mutate All) with a high number of individuals generally acheives a correct result first-try. In our very constrained individuum pool tests with 3 individuals, this also seems to be the best choice for parameters for the EA. Its efficacy seems to increase even more with a larger gene pool. 


## Aufgabe 3: Exploration vs Exploitation

```{r setup}
# generate a random binary vector of length 10
generate_individual <- function() {
    sample(c(0, 1), 10, replace = TRUE)
}

# 1-point crossover
one_point_crossover <- function(parent1, parent2) {
    point <- sample(1:9, 1)
    child1 <- c(parent1[1:point], parent2[(point + 1):10])
    child2 <- c(parent2[1:point], parent1[(point + 1):10])
    return(list(child1, child2))
}

# 2-point crossover
two_point_crossover <- function(parent1, parent2) {
    points <- sort(sample(1:9, 2))
    child1 <- c(parent1[1:points[1]], parent2[(points[1] + 1):points[2]], parent1[(points[2] + 1):10])
    child2 <- c(parent2[1:points[1]], parent1[(points[1] + 1):points[2]], parent2[(points[2] + 1):10])
    return(list(child1, child2))
}

# uniform crossover
uniform_crossover <- function(parent1, parent2) {
    mask <- sample(c(0, 1), 10, replace = TRUE)
    child1 <- ifelse(mask == 1, parent1, parent2)
    child2 <- ifelse(mask == 1, parent2, parent1)
    return(list(child1, child2))
}

# Function to convert binary vector to decimal
binary_to_decimal <- function(binary_vector) {
    sum(binary_vector * 2^(rev(seq_along(binary_vector) - 1)))
}
```


```{r initialize_parents}
# init parents
parent1 <- generate_individual()
parent2 <- generate_individual()

print(parent1)
print(parent2)

# Set num gens
num_generations <- 10000

# for tracking unique solutions
unique_solutions <- list()
unique_solutions[[binary_to_decimal(parent1)]] <- TRUE
unique_solutions[[binary_to_decimal(parent2)]] <- TRUE
```


```{r run_genetic_algorithm}
for (generation in 1:num_generations) {
    # Choose crossover method
    crossover_method <- sample(c("one_point", "two_point", "uniform"), 1)

    if (crossover_method == "one_point") {
        children <- one_point_crossover(parent1, parent2)
    } else if (crossover_method == "two_point") {
        children <- two_point_crossover(parent1, parent2)
    } else {
        children <- uniform_crossover(parent1, parent2)
    }



    # Replace
    parent1 <- children[[1]]
    parent2 <- children[[2]]

    # Track sols
    unique_solutions[[binary_to_decimal(parent1)]] <- TRUE
    unique_solutions[[binary_to_decimal(parent2)]] <- TRUE
}

# Calculate number of unique solutions observed
num_unique_solutions <- length(unique_solutions)

# Calculate number of possible solutions covered
num_possible_solutions <- 2^10
num_solutions_covered <- length(unique_solutions)

# Print results
cat("Number of unique solutions observed:", num_unique_solutions, "\n")
cat("Number of possible solutions covered:", num_solutions_covered, "\n")
cat("Percentage of 10-bit numbers covered:", round(num_solutions_covered / num_possible_solutions * 100, 2), "%\n")
```

