neuron.py

#
import pygame
import sys
import random
#

Define constants

WINDOW_SIZE = (800, 600)
NUM_NEURONS = 25
INITIAL_PROBABILITY = 0.1
MAX_CONNECTION_STRENGTH = 1.0
MIN_CONNECTION_STRENGTH = 0.0
CONNECTION_STRENGTH_DELTA = 0.1
PROBABILITY_THRESHOLD = 0.5
PROBABILITY_INCREASE = 0.1
PROBABILITY_DECREASE = 0.05
#

Update the probabilities of neurons being activated based on the activity of their neighbors. If the average activity level of a neuron’s neighbors is above the threshold, its probability of being activated is increased by the increase amount. If it is below the threshold, its probability is decreased by the decrease amount.

def update_probabilities(
    neurons, connections, threshold=0.5, increase=0.1, decrease=0.05
):
#

Compute the activity level of each neuron

    activity_levels = [
        sum(
            [connections[i][j] * neurons[j]["probability"] for j in range(len(neurons))]
        )
        for i in range(len(neurons))
    ]
#

Compute the average activity level of each neuron’s neighbors

    neighbor_activity_levels = [
        sum(
            [
                connections[i][j] * activity_levels[j]
                for j in range(len(neurons))
                if j != i
            ]
        )
        / (len(neurons) - 1)
        for i in range(len(neurons))
    ]
#

Update the probabilities of each neuron being activated

    for i in range(len(neurons)):
        if neighbor_activity_levels[i] > threshold:
            neurons[i]["probability"] += increase
        else:
            neurons[i]["probability"] -= decrease
#

Ensure the probability stays within the valid range

        neurons[i]["probability"] = max(0, min(1, neurons[i]["probability"]))
#

Mutate the connection strengths between neurons. With probability mutation_rate, each connection is mutated by a factor of mutation_size.

def mutate_connections(connections, mutation_rate=0.1, mutation_size=0.1):
#
    for i in range(len(connections)):
        for j in range(len(connections)):
            if random.uniform(0, 1) < mutation_rate:
                connections[i][j] *= random.uniform(
                    1 - mutation_size, 1 + mutation_size
                )
                connections[i][j] = max(0, min(1, connections[i][j]))
#

Apply external input to a random subset of neurons. Each neuron in the subset has its probability of being activated increased by strength, with probability rate.

def apply_external_input(neurons, strength=0.1, rate=0.1):
#
    subset = random.sample(neurons, int(len(neurons) * rate))
    for neuron in subset:
        neuron["probability"] = min(1, neuron["probability"] + strength)
#

Apply feedback to the network by updating the connection strengths between neurons. For each neuron in the network, its feedback weight is calculated as the sum of the connection strengths from all other neurons to that neuron, multiplied by the feedback_strength. The connection strengths are then updated by adding the feedback weight to each connection.

def apply_feedback(neurons, connections, feedback_strength=0.1):
#
    for i in range(len(neurons)):
        feedback_weight = (
            sum([connections[j][i] for j in range(len(neurons)) if j != i])
            * feedback_strength
        )
        for j in range(len(neurons)):
            connections[j][i] += feedback_weight
            connections[j][i] = max(0, min(1, connections[j][i]))
#

Apply global inhibition to the network by reducing the probabilities of all neurons. A random subset of neurons is chosen, with size proportional to inhibition_rate. The probabilities of these neurons are reduced by inhibition_strength. The connection strengths from these neurons to all other neurons are also reduced by a factor of 0.5.

def apply_inhibition(
    neurons, connections, inhibition_strength=0.1, inhibition_rate=0.1
):
#
    subset = random.sample(neurons, int(len(neurons) * inhibition_rate))
    for neuron in subset:
        neuron["probability"] = max(0, neuron["probability"] - inhibition_strength)
        for i in range(len(neurons)):
            connections[neurons.index(neuron)][i] *= 0.5
            connections[neurons.index(neuron)][i] = max(
                0, min(1, connections[neurons.index(neuron)][i])
            )
#

Apply synaptic plasticity to the network by updating the connection strengths between neurons. For each neuron in the network, a sliding window of the last window activations is computed. If the fraction of activations that were positive is above the threshold, the connection strengths from that neuron to all other neurons are increased by a factor of factor. If the fraction of activations that were negative is above the threshold, the connection strengths from that neuron to all other neurons are decreased by a factor of factor.

def apply_synaptic_plasticity(
    neurons, connections, window=10, threshold=0.1, factor=0.1
):
#
    for i in range(len(neurons)):
        window_start = max(0, i - window)
        window_end = min(len(neurons), i + window + 1)
        window_activities = [
            neurons[j]["probability"] > 0.5 for j in range(window_start, window_end)
        ]
        positive_fraction = sum(window_activities) / len(window_activities)
        negative_fraction = 1 - positive_fraction
        for j in range(len(neurons)):
            if positive_fraction > threshold:
                connections[i][j] *= 1 + factor
                connections[i][j] = max(0, min(1, connections[i][j]))
            elif negative_fraction > threshold:
                connections[i][j] *= 1 - factor
                connections[i][j] = max(0, min(1, connections[i][j]))
#

Apply Hebbian learning to the network by updating the connection strengths between neurons. For each pair of neurons that are activated together, the connection strength between them is increased by a factor of learning_rate. The connection strengths are clipped to the range [0, 1].

def apply_learning(neurons, connections, learning_rate=0.1):
#
    for i in range(len(neurons)):
        for j in range(len(neurons)):
            if (
                i != j
                and neurons[i]["probability"] > 0.5
                and neurons[j]["probability"] > 0.5
            ):
                connections[i][j] += learning_rate
                connections[i][j] = max(0, min(1, connections[i][j]))
#

Apply modulatory signals to the network by updating the connection strengths between neurons. A random subset of neurons is chosen, with size proportional to modulatory_rate. For each neuron in the subset, the connection strengths from that neuron to all other neurons are increased or decreased by a random amount in the range [-modulatory_strength, modulatory_strength].

def apply_modulatory_signals(
    neurons, connections, modulatory_strength=0.1, modulatory_rate=0.1
):
#
    subset = random.sample(neurons, int(len(neurons) * modulatory_rate))
    for neuron in subset:
        for i in range(len(neurons)):
            connections[neurons.index(neuron)][i] += random.uniform(
                -modulatory_strength, modulatory_strength
            )
            connections[neurons.index(neuron)][i] = max(
                0, min(1, connections[neurons.index(neuron)][i])
            )
#

Apply homeostasis to the network by adjusting the probabilities of neurons based on their activity. For each neuron, its probability of activation is increased or decreased by a factor proportional to the difference between its current activity and the target activity. The adjustment factor is determined by the homeostasis rate parameter.

def apply_homeostasis(neurons, target_prob=0.1, homeostasis_rate=0.1):
#
    for neuron in neurons:
        delta_prob = (target_prob - neuron["probability"]) * homeostasis_rate
        neuron["probability"] = max(0, min(1, neuron["probability"] + delta_prob))
#

Apply a refractory period to the network by temporarily decreasing the probability of activated neurons. For each neuron, if its probability of activation is above 0.5, it is considered activated, and its probability is set to 0.0 for the next refractory_period time steps.

def apply_refractory_period(neurons, refractory_period=10):
#
    for neuron in neurons:
        if neuron["probability"] > 0.5:
            neuron["probability"] = 0.0
            neuron["refractory"] = refractory_period
        elif "refractory" in neuron and neuron["refractory"] > 0:
            neuron["refractory"] -= 1
#

Apply noise to the network by randomly changing the probability of a subset of neurons. A random subset of neurons is chosen, with size proportional to noise_rate. For each neuron in the subset, its probability is increased or decreased by a random amount in the range [-noise_strength, noise_strength].

def apply_noise(neurons, noise_strength=0.05, noise_rate=0.1):
#
    subset = random.sample(neurons, int(len(neurons) * noise_rate))
    for neuron in subset:
        neuron["probability"] += random.uniform(-noise_strength, noise_strength)
        neuron["probability"] = max(0, min(1, neuron["probability"]))
#

Draw a checkerboard pattern with a gradient as the background of the screen. The pattern alternates between two colors, and each square has size size pixels.

def draw_background(screen, color1=(30, 30, 30), color2=(50, 50, 50), size=50):
#
    for x in range(0, screen.get_width(), size):
        for y in range(0, screen.get_height(), size):
            rect = pygame.Rect(x, y, size, size)
            color = [0, 0, 0]
            for i in range(3):
                color[i] = int(
                    color1[i] * (1 - x / screen.get_width())
                    + color2[i] * (x / screen.get_width())
                )
            if (x // size + y // size) % 2 == 0:
                pygame.draw.rect(screen, color, rect)
            else:
                pygame.draw.rect(screen, color[::-1], rect)
#

Draw a visual effect around activated neurons. For each neuron that is currently activated, draw a circle with a gradient effect that fades out towards the edge.

def draw_activation_effect(screen, neurons):
#
    for neuron in neurons:
        if neuron["probability"] > 0.5:
#

Determine the size of the circle based on the neuron’s activation probability

            size = int(neuron["probability"] * 20)
#

Create a surface with a radial gradient

            circle_surface = pygame.Surface((size * 2, size * 2), pygame.SRCALPHA)
            alpha_values = [
                min(255, int((1 - (float(i) / size)) * 255)) for i in range(size + 1)
            ]
            for i in range(size + 1):
                pygame.draw.circle(
                    circle_surface,
                    (255, 255, 0, alpha_values[i]),
                    (size, size),
                    size - i,
                )
#

Draw the circle on the screen

            screen.blit(circle_surface, (neuron["x"] - size, neuron["y"] - size))
#

Draw the connections between neurons on the screen. The color of the lines represents the strength of the connection.

def draw_connections(screen, neurons, connections):
#
    for i in range(len(neurons)):
        for j in range(len(neurons)):
            if connections[i][j] > 0:
                strength = int(connections[i][j] * 255)
                color = (strength, strength, strength)
                pygame.draw.line(
                    screen,
                    color,
                    (neurons[i]["x"], neurons[i]["y"]),
                    (neurons[j]["x"], neurons[j]["y"]),
                    1,
                )
#

Draw random sparks on the screen with random trajectories and velocities.

def draw_sparks(
    screen, spark_color=(255, 255, 0), spark_size=2, num_sparks=10, max_speed=5
):
#
    for i in range(num_sparks):
        x = random.randint(0, screen.get_width())
        y = random.randint(0, screen.get_height())
        vx = random.uniform(-max_speed, max_speed)
        vy = random.uniform(-max_speed, max_speed)
        pygame.draw.circle(screen, spark_color, (x, y), spark_size)
#

Update the position of the spark based on its velocity

        x += vx
        y += vy
#

Bounce the spark off the edges of the screen

        if x < 0 or x > screen.get_width():
            vx *= -1
        if y < 0 or y > screen.get_height():
            vy *= -1
#

Draw circles to represent the neurons on the screen.

def draw_neurons(screen, neurons):
#
    for neuron in neurons:
        pygame.draw.circle(screen, (255, 255, 0), (neuron["x"], neuron["y"]), 3)
#

Apply various complex effects to the network of neurons and connections.

def apply_complexities(neurons, connections):
#
    mutate_connections(connections)
    apply_external_input(neurons)
    apply_feedback(neurons, connections)
    apply_inhibition(neurons, connections)
    apply_synaptic_plasticity(neurons, connections)
    apply_learning(neurons, connections)
    apply_modulatory_signals(neurons, connections)
    apply_homeostasis(neurons)
    apply_refractory_period(neurons)
    apply_noise(neurons)
#
def initialize_network():
#

Initialize neurons

    neurons = [
        {
            "x": random.randint(0, WINDOW_SIZE[0]),
            "y": random.randint(0, WINDOW_SIZE[1]),
            "probability": INITIAL_PROBABILITY,
        }
        for _ in range(NUM_NEURONS)
    ]
#

Initialize connections

    connections = [
        [
            random.uniform(MIN_CONNECTION_STRENGTH, MAX_CONNECTION_STRENGTH)
            for _ in range(NUM_NEURONS)
        ]
        for _ in range(NUM_NEURONS)
    ]

    return neurons, connections
#
def update_network(screen, neurons, connections):
#

Handle events

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()
#

Draw the background

    screen.fill((255, 255, 255))
    draw_background(screen, color1=(30, 30, 30), color2=(50, 50, 50), size=50)
#

Draw the connections between neurons

    draw_connections(screen, neurons, connections)
#

Update the network

    for i, activating_neuron in enumerate(neurons):
#

Determine the target neuron based on connection strengths

        target_neuron = random.choices(neurons, weights=connections[i])[0]
#

Activate or inhibit the target neuron based on connection strength and probability

        if (
            random.uniform(0, 1)
            < connections[i][neurons.index(target_neuron)]
            * activating_neuron["probability"]
        ):
            color = (0, 0, 0)
            delta = CONNECTION_STRENGTH_DELTA
        elif (
            random.uniform(0, 1)
            < connections[neurons.index(target_neuron)][i]
            * activating_neuron["probability"]
        ):
            color = (255, 0, 0)
            delta = -CONNECTION_STRENGTH_DELTA
        else:
            continue
#

Draw a line between the two neurons, using color based on connection strength

        line_width = int(3 * connections[i][neurons.index(target_neuron)])
        pygame.draw.line(
            screen,
            color,
            (activating_neuron["x"], activating_neuron["y"]),
            (target_neuron["x"], target_neuron["y"]),
            line_width,
        )
#

Update the connection strength between the neurons

        connections[i][neurons.index(target_neuron)] += delta
#

Update neuron probabilities and apply complexities

    update_probabilities(
        neurons,
        connections,
        threshold=PROBABILITY_THRESHOLD,
        increase=PROBABILITY_INCREASE,
        decrease=PROBABILITY_DECREASE,
    )
    apply_complexities(neurons, connections)
#

Draw the neurons and activation effect

    draw_neurons(screen, neurons)
    draw_activation_effect(screen, neurons)
#

Draw some sparks

    draw_sparks(screen)
#

Update the screen

    pygame.display.flip()
#
def main():
#

Initialize Pygame

    pygame.init()
    screen = pygame.display.set_mode(WINDOW_SIZE)
#

Initialize the network

    neurons, connections = initialize_network()
#

Main loop of the game

    while True:
        update_network(screen, neurons, connections)


if __name__ == "__main__":
    main()