Skip to content

Commit

Permalink
feat: saves actions and makes it possible to play genome back in all …
Browse files Browse the repository at this point in the history
…environments
  • Loading branch information
Vetlets05 committed Nov 5, 2024
2 parents 55085aa + a17405c commit cc4731c
Show file tree
Hide file tree
Showing 7 changed files with 1,968 additions and 27 deletions.
11 changes: 10 additions & 1 deletion main.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,17 @@ def main(args):

if neat.config.SHOULD_PROFILE:
profiler.disable()
stats = pstats.Stats(profiler).sort_stats('cumtime') # Create a stats object to print out profiling results

# Create a stats object to format the profiling results
stats = pstats.Stats(profiler).sort_stats('cumtime')

# Print stats to terminal (optional, you may remove if too verbose)
stats.print_stats()

# Write human-readable stats to a text file
with open("profile_output.txt", "w") as f:
stats = pstats.Stats(profiler, stream=f)
stats.sort_stats('cumtime').print_stats()

return neat.genomes

Expand Down
1,875 changes: 1,875 additions & 0 deletions profile_output.txt

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/environments/debug_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def run_game_debug(env: MarioJoypadSpace, initial_state: np.ndarray, genome: Gen
sr = env.step(action) # State, Reward, Done, Info

# timeout = 600 + sr.info["x_pos"]
if visualize and i % 1 == 0:
if visualize and i % 10 == 0:
save_state_as_png(i, sr.state, neat_name)
visualize_genome(genome, neat_name, 0)

Expand Down
46 changes: 36 additions & 10 deletions src/environments/mario_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,24 +58,50 @@ def step(self, action: int) -> StepResult:
state = self.interpret_state(state)
return StepResult(state, reward, done, info)

# def interpret_state(self, state: np.ndarray) -> np.ndarray:
# """
# Preprocessing of state:\n
# Input:
# - ndarray with shape (240, 256, 3)\n
# Output:
# - ndarray with shape (20, 40)
# """
# MAX_COLOR = 255
# state = state[80:216] # Cut the picture

# # Example: Limit to 4 colors
# state = np.dot(state[..., :3], [0.2989, 0.5870, 0.1140]) # Convert to grayscale
# state = state.astype(np.uint8) # Ensure valid grayscale value (can't be float)

# state = resize(state, (self.config.input_shape[0], self.config.input_shape[1]), anti_aliasing=True, preserve_range=True).astype(np.uint8) # Reduce pixel count.

# state = np.array(state)
# grayscale_pixel_values = state / MAX_COLOR
# return grayscale_pixel_values

def interpret_state(self, state: np.ndarray) -> np.ndarray:
"""
Preprocessing of state:\n
Preprocessing of state:
Input:
- ndarray with shape (240, 256, 3)\n
- ndarray with shape (240, 256, 3)
Output:
- ndarray with shape (10, 20)
- ndarray with shape (20, 40, 3)
"""
MAX_COLOR = 255
state = state[80:216] # Cut the picture
state = np.dot(state[..., :3], [0.2989, 0.5870, 0.1140]) # Convert to grayscale
state = state.astype(np.uint8) # Ensure valid grayscale value (can't be float)

state = resize(state, (self.config.input_shape[0], self.config.input_shape[1]), anti_aliasing=True, preserve_range=True).astype(np.uint8) # Reduce pixel count.
# Cut the picture
state = state[80:216]

# Resize while preserving RGB channels
state = resize(
state,
(self.config.input_shape[0], self.config.input_shape[1], 3),
anti_aliasing=True,
preserve_range=True
).astype(np.uint8)

state = np.array(state)
grayscale_pixel_values = state / MAX_COLOR
return grayscale_pixel_values
# Normalize to [0,1] range
return state / MAX_COLOR

class ResizeEnv(ObservationWrapper):
def __init__(self, env, size):
Expand Down
9 changes: 6 additions & 3 deletions src/utils/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class Config:
c2: float = 1.5
c3: float = 0.4
genomic_distance_threshold: float = 2.69
population_size: int = 4 # 56 cores on IDUN
population_size: int = 40 # 56 cores on IDUN
generations: int = 2 # A bunch of iterations

connection_weight_mutation_chance: float = 0.8
Expand All @@ -24,16 +24,19 @@ class Config:
# Connections should be added way more often than nodes

num_output_nodes: int = 7
num_input_nodes: int = 800
input_shape: Tuple[int, int] = (20, 40)

input_channels: int = 3
"""If using RGB, value should be 3, if using grayscale, value should be 1"""
num_input_nodes: int = input_shape[0] * input_shape[1] * input_channels

# Activation function
# Paper: 1/(1+exp(-0.49*x))
activation_func: str = "sigmoid"

elitism_rate: float = 0.02 # percentage of the best genomes are copied to the next generation
remove_worst_percentage: float = 0.4 # percentage of the worst genomes are removed from the population when breeding

SHOULD_PROFILE: bool = False
SHOULD_PROFILE: bool = True

cores: int = -1 # -1 means all cores
46 changes: 37 additions & 9 deletions src/utils/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,44 @@ def normalize_negative_values(negative_vals: np.ndarray) -> None:
negative_vals *= -1
normalize_positive_values(negative_vals)

def insert_input(genome:Genome, state: np.ndarray) -> None:
"""Insert the state of the game into the input nodes of the genome."""
config = Config()
start_idx_input_node = config.num_output_nodes
num_input_nodes = config.num_input_nodes
num_columns = config.input_shape[-1]
# def insert_input(genome:Genome, state: np.ndarray) -> None:
# """Insert the state of the game into the input nodes of the genome."""
# config = Config()
# start_idx_input_node = config.num_output_nodes
# num_input_nodes = config.num_input_nodes
# num_columns = config.input_shape[-1]

for i, node in enumerate(genome.nodes[start_idx_input_node:start_idx_input_node+num_input_nodes]): # get all input nodes
node.value = state[i//num_columns][i % num_columns]
# print(f"node value: {node.value} node id: {node.id}")
# for i, node in enumerate(genome.nodes[start_idx_input_node:start_idx_input_node+num_input_nodes]): # get all input nodes
# node.value = state[i//num_columns][i % num_columns]
# # print(f"node value: {node.value} node id: {node.id}")

def insert_input(genome: Genome, state: np.ndarray) -> None:
"""
Insert the RGB state of the game into the input nodes of the genome.
Each pixel is represented by 3 consecutive nodes (R,G,B values).
Args:
genome: The genome to update
state: numpy array of shape (height, width, 3) containing RGB values
"""
config = Config()
start_idx = config.num_output_nodes
expected_inputs = config.num_input_nodes

# Pre-validate array shape to avoid unnecessary operations
if state.size != expected_inputs:
raise ValueError(f"State shape mismatch. Expected {expected_inputs} values, got {state.size}")

if start_idx + expected_inputs > len(genome.nodes):
raise IndexError(f"Genome has insufficient nodes. Need {start_idx + expected_inputs} but length is {len(genome.nodes)}")

# Use direct array assignment instead of individual updates
# This avoids the Python loop overhead
flattened_state = state.ravel() # ravel() is faster than flatten() as it returns a view when possible

# Update all nodes at once using array slicing
for node, value in zip(genome.nodes[start_idx:start_idx + expected_inputs], flattened_state):
node.value = value

def save_fitness(best: list, avg: list, min: list, name: str):
os.makedirs(f'data/{name}/fitness', exist_ok=True)
Expand Down
6 changes: 3 additions & 3 deletions src/visualization/viz_config.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
GRAPH_XMIN = 0
GRAPH_XMIN = -30
GRAPH_XMAX = 17
GRAPH_YMIN = 0
GRAPH_YMAX = 21
Expand All @@ -7,9 +7,9 @@
VERTICAL_GAP = 0.7

NUM_INPUT_ROWS = 20
NUM_INPUT_COLS = 40
NUM_INPUT_COLS = 120

XSTART_INPUT = 1.5
XSTART_INPUT = -28.5
XSTART_OUTPUT = 15
YSTART = 3.0

Expand Down

0 comments on commit cc4731c

Please sign in to comment.