File size: 23,693 Bytes
ba7b9c7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 | import pygame
import math
import random
import csv
import torch
from collections import deque
from Agent import *
from Class_Def import *
from utils import *
from Const import *
device = DEVICE
pygame.init()
WIN = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Planet_Sim")
FONT = pygame.font.SysFont("Times", 16)
def SatSim(load_weights=False):
print(device)
Scale = 50/AU
Step_p_frame = 1
shift_x, shift_y = 0, 0
run = True
clock = pygame.time.Clock()
planets = Solar_System()
Target = 6 #random.randint(0,len(planets)-1)
rockets = []
rockets.append(Launch(planets[3]))
# --- Initialize Distance Recording ---
all_distance_acquisitions = []
current_acquisition = []
# --- Instantiate the Agent ---
agent = Att_Agent(n_features=NUM_FEATURES, # Use n_features
seq_len=SEQ_LENGTH, # Use seq_len
action_size=ACTION_SIZE,
replay_memory_capacity=MEMORY_CAPACITY,
batch_size=BATCH_SIZE,
gamma=GAMMA,
eps_start=EPS_START, eps_end=EPS_END, eps_decay=EPS_DECAY,
tau=TAU, lr=LR, target_update_freq=TARGET_UPDATE_FREQ,
# Add Attention Network specific params if needed (using defaults here)
# embed_dim=64, num_heads=4
)
# Load weights if demanded
try:
agent.load_weights()
except Exception as e:
print(f"Error loading weights: {e}")
# --- Setup Agent Logging ---
# The agent will create its own timestamped directory within Metrics/MODEL_NAME/
agent.setup_logging(model_name=MODEL_NAME) # Use the defined MODEL_NAME
# --- Initialize Episode Tracking Variables ---
episode_number = 0
episode_reward = 0.0
episode_steps = 0
episode_goal_achieved = False
# History deques initialization...
action_hist = deque(maxlen=MAX_HISTORY_LEN)
Flag = 0
agent_controlled_rocket_index = 0
rocket = rockets[agent_controlled_rocket_index] if rockets else None
target_planet = planets[Target] if Target < len(planets) else None
# --- Initialize History Deques ---
action_hist = deque(maxlen=MAX_HISTORY_LEN)
rel_x_hist = deque(maxlen=MAX_HISTORY_LEN)
rel_y_hist = deque(maxlen=MAX_HISTORY_LEN)
rel_vx_hist = deque(maxlen=MAX_HISTORY_LEN)
rel_vy_hist = deque(maxlen=MAX_HISTORY_LEN)
# Fill initial history with padding values (optional, but helps)
# Use padding values defined in utils or Agent
initial_pad_value = 0.0
initial_action_pad = 0
for _ in range(SEQ_LENGTH): # Start with enough history for the first state
action_hist.append(initial_action_pad)
rel_x_hist.append(initial_pad_value)
rel_y_hist.append(initial_pad_value)
rel_vx_hist.append(initial_pad_value)
rel_vy_hist.append(initial_pad_value)
# Initialize state tuple for the loop start
state_tuple = None
# --- Steps count ---
n_step = 0
while run:
# --- Initialize state ---
# For simplicity, let's always control the first rocket if it exists
done = False # Reset done flag each iteration
agent_controlled_rocket_index = 0
rocket = rockets[agent_controlled_rocket_index] if rockets else None
target_planet = planets[Target]
clock.tick(FPS)
WIN.fill((0, 0, 0))
# --- Pygame Event Handling ---
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
#Actions done once per pressure
if event.type == pygame.KEYDOWN:
if(event.key == pygame.K_1): Step_p_frame += 10 # Speed up time fast >>
if(event.key == pygame.K_2): Step_p_frame += 1 # Speed up time slow >
if(event.key == pygame.K_3): Step_p_frame -= 1 # Slow down time slow <
if(event.key == pygame.K_4): Step_p_frame -= 10 # Slow down time fast <<
if(event.key == pygame.K_a): Flag -=1 # Show previous object
if(event.key == pygame.K_z): Flag +=1 # Show next object
if(event.key == pygame.K_t): Flag = Target # Show target object
if(event.key == pygame.K_r): Target = random.randint(0,len(planets)-1) # Set a new target
if(event.key == pygame.K_SPACE):
rockets.append(Launch(planets[3]))
if(event.key == pygame.K_BACKSPACE):
if(len(rockets)> 0):
rockets.pop()
if event.key == pygame.K_x:
if rocket:
done = True
# rocket = reset_rocket_state(rocket, planets) Now reset is in the done condition True
# --- Clear History Deques on Reset ---
action_hist.clear()
rel_x_hist.clear()
rel_y_hist.clear()
rel_vx_hist.clear()
rel_vy_hist.clear()
# Fill with padding again? Or let the loop handle it?
# Re-filling ensures immediate valid sequence length.
for _ in range(SEQ_LENGTH):
action_hist.append(initial_action_pad)
rel_x_hist.append(initial_pad_value)
rel_y_hist.append(initial_pad_value)
rel_vx_hist.append(initial_pad_value)
rel_vy_hist.append(initial_pad_value)
# state = get_state(rocket, target_planet) # Remove old state logic
state_tuple = None # Indicate state needs recalculation
# --- Log previous episode before reset (if it ran at least one step) ---
# if episode_steps > 0:
# agent.log_episode_data(episode_number, episode_steps, episode_reward, episode_goal_achieved)
# --- Reset Episode Trackers ---
episode_reward = 0.0
episode_steps = 0
episode_goal_achieved = False
print("Targeted planet is", Core_position[int(Target)])
# Save distance track and reset
# all_distance_acquisitions.append(list(current_acquisition)) # Use list() to copy
# current_acquisition.clear() # Use clear() for deque/list
if(event.key == pygame.K_q):
run = False
all_distance_acquisitions.append(current_acquisition) # Save the track of distances bwt Rckt & Tgt
current_acquisition = []
if(Flag>(len(planets)+len(rockets)-1)): Flag = 0
if(Flag<0): Flag = len(planets)+len(rockets)-1
# --- RL Agent Step ---
reward = 0 # Initialize reward
if rocket and target_planet: # Ensure we have a rocket and target
# --- 1. Calculate Current Relative State ---
# This provides the LATEST snapshot before action selection
# Uses the state *before* physics update for this step
current_rel_x, current_rel_y, current_rel_vx, current_rel_vy = calculate_current_relative_state(rocket, target_planet)
# Handle potential NaN from calculate_current_relative_state if rocket/target missing
if any(math.isnan(v) for v in [current_rel_x, current_rel_y, current_rel_vx, current_rel_vy]):
print("Warning: Invalid current state values (NaN). Skipping agent step.")
# Potentially try to recover or skip agent logic for this frame
# You might need error handling here depending on how NaNs occur
else:
# --- 2. Get State Sequence for Agent ---
# This uses the history *leading up to* the current point
state_tuple = get_state_sequence(
action_hist, rel_x_hist, rel_y_hist, rel_vx_hist, rel_vy_hist, seq_length=SEQ_LENGTH
)
# --- 3. Agent Selects Action ---
action_tensor = agent.select_action(state_tuple) # Pass tuple (sequence, mask)
action = action_tensor.item()
# --- 4. Apply Action to Rocket ---
rocket.apply_action(action)
# --- Store state before physics update (for reward calc based on distance change) ---
prev_dist = get_distance(rocket, target_planet)
if prev_dist == 0: prev_dist = 1e-6 # Avoid division by zero
# --- 5. Physics Update ---
planets, rockets = update_position(planets, rockets) # This updates rocket.x, .y, .vx, .vy
# Find the controlled rocket again after update (it might have been removed)
# This assumes the agent always controls the rocket at index 0 if it exists
controlled_rocket_index = 0
if controlled_rocket_index < len(rockets):
rocket = rockets[controlled_rocket_index] # Update rocket reference
else:
rocket = None # Rocket was destroyed/removed
print("Controlled rocket destroyed during physics update.")
done = True
reward = -GOAL_REWARD * 2 # Heavy penalty for destruction
next_state_tuple = None # No next state if destroyed
# --- 6. Calculate Reward & Next State (if rocket exists) ---
if rocket:
# --- Increment episode step counter ---
episode_steps += 1
try:
current_distance = get_distance(rocket, target_planet)
current_acquisition.append(current_distance) # Save distance for logging
if current_distance == 0: current_distance = 1e-6
# --- 6a. Calculate Reward Components ---
# Distance Reward :
reward_distance = float(AU/abs(current_distance - 10*target_planet.radius))
if current_distance>50*target_planet.radius:
reward_distance *= np.sign(prev_dist - current_distance)
# Orbital speed reward & goal check
target_mass = target_planet.mass
ideal_orbital_speed = math.sqrt(G * target_mass / max(current_distance, 1e-6)) # Avoid div by zero
rel_vx_now = rocket.vx - target_planet.vx # Use updated velocities
rel_vy_now = rocket.vy - target_planet.vy
current_speed_relative = math.sqrt(rel_vx_now**2 + rel_vy_now**2)
speed_diff = abs((current_speed_relative - ideal_orbital_speed) / current_speed_relative)
reward_speed = REWARD_SPEED_SCALE * (1.0 - speed_diff)
reward_action = 0
if current_distance<20*target_planet.radius:
reward_action = NO_THRUST_REWARD if action == 0 else 0 # Penalize any thrust
# Time Penalty
reward_time = 0 #-TIME_PENALTY (Far too soon) Probably useless w/ fuel consuption notion.
# Goal Reward Check (use updated distance/speed)
is_orbiting_goal = (speed_diff * ideal_orbital_speed < ORBIT_SPEED_TOLERANCE) and \
(5*target_planet.radius <= current_distance <= 20*target_planet.radius)
# print(current_distance, target_planet.radius)
reactors_off = rocket.motor_off()
if is_orbiting_goal and reactors_off:
episode_goal_achieved = True # Mark true if goal met anytime in episode
reward_goal = GOAL_REWARD
else:
reward_goal = 0
reward = reward_distance + reward_speed + reward_action + reward_time + reward_goal
step_reward = reward
# --- Accumulate Episode Reward ---
episode_reward += step_reward
# --- 6b. Check "Done" Conditions ---
# Crash into target planet
crash_dist_m = (target_planet.radius) #+ CRASH_DISTANCE_THRESHOLD
if current_distance < crash_dist_m :
print("Crashed into target! Resetting rocket.")
print(current_distance, target_planet.radius)
done = True
reward -= GOAL_REWARD * 0.5 # Penalty for crash
# Resetting logic now handles history clearing (see point 5)
# Out of bounds (relative to target planet)
if current_distance > OUT_OF_BOUNDS_DISTANCE:
print("Went out of bounds! Resetting rocket.")
done = True
reward -= GOAL_REWARD # Penalty for OOB (adjust as needed)
# Resetting logic handles history clearing
# --- 6c. Update History & Get Next State ---
# Calculate the relative state AFTER the physics update
next_rel_x, next_rel_y, next_rel_vx, next_rel_vy = calculate_current_relative_state(rocket, target_planet)
# Append the state values LEADING TO the action, and the action itself, to history
# Use the values calculated in step 1
if not math.isnan(current_rel_x): # Check if state was valid before appending
action_hist.append(action)
rel_x_hist.append(current_rel_x)
rel_y_hist.append(current_rel_y)
rel_vx_hist.append(current_rel_vx)
rel_vy_hist.append(current_rel_vy)
# Now generate the next_state sequence using the updated history
if not done and not any(math.isnan(v) for v in [next_rel_x, next_rel_y, next_rel_vx, next_rel_vy]):
next_state_tuple = get_state_sequence(
action_hist, rel_x_hist, rel_y_hist, rel_vx_hist, rel_vy_hist, seq_length=SEQ_LENGTH
)
else:
next_state_tuple = None # Terminal state or invalid next state
except (NameError, AttributeError, IndexError, TypeError, ValueError) as e:
print(f"Warning: Error during RL step calculation: {e}")
import traceback
traceback.print_exc() # Print full traceback for debugging
reward = 0 # Default reward on error
next_state_tuple = None # Cannot determine next state
# Decide if this error should terminate the episode (done=True)
# done = True # Optional: Terminate on calculation error
# --- 7. Store Experience ---
# Ensure state_tuple was calculated in this iteration before storing
if state_tuple is not None:
reward_tensor = torch.tensor([reward], dtype=torch.float32, device=device)
# Store the state tuple (history before action) and next_state tuple (history after action)
agent.store_experience(state_tuple, action_tensor, next_state_tuple, reward_tensor, done)
else:
# This case might happen on the very first step if state isn't pre-initialized
# or after an error/reset where state_tuple became None.
# Avoid storing experience if the initial state wasn't valid.
print("Warning: state_tuple is None, skipping experience storage.") # Optional log
pass
# --- 8. Optimize Agent Model ---
agent.optimize_model()
# --- 9. Update Target Network ---
if agent.tau > 0:
agent.update_target_net(soft_update=True)
# --- Or Hard update less frequently ---
# elif step_count % agent.target_update_freq == 0:
# agent.update_target_net(soft_update=False)
# --- Handle End of Episode ---
if done:
print(f"Episode finished. Reason: {'Crash' if current_distance < crash_dist_m else 'OOB' if current_distance > OUT_OF_BOUNDS_DISTANCE else 'User Reset'}. Final Reward: {reward}")
# Clear History Deques on Reset
action_hist.clear(); rel_x_hist.clear(); rel_y_hist.clear(); rel_vx_hist.clear(); rel_vy_hist.clear()
# Re-fill padding
for _ in range(SEQ_LENGTH):
action_hist.append(initial_action_pad)
rel_x_hist.append(initial_pad_value)
rel_y_hist.append(initial_pad_value)
rel_vx_hist.append(initial_pad_value)
rel_vy_hist.append(initial_pad_value)
# Save distance track and reset
all_distance_acquisitions.append(list(current_acquisition))
current_acquisition.clear()
episode_number += 1
episode_reward /= episode_steps # Average reward per step
print(f"--- Episode {episode_number} Finished --- Steps: {episode_steps}, Average Reward: {episode_reward:.2f}, Goal: {episode_goal_achieved} ---")
# Log data for the completed episode
agent.log_episode_data(episode_number, episode_steps, episode_reward, episode_goal_achieved)
# Reset rocket state
rocket = reset_rocket_state(rocket, planets)
# Clear and refill history deques
action_hist.clear(); rel_x_hist.clear(); rel_y_hist.clear(); rel_vx_hist.clear(); rel_vy_hist.clear()
for _ in range(SEQ_LENGTH):
action_hist.append(initial_action_pad); rel_x_hist.append(initial_pad_value); rel_y_hist.append(initial_pad_value); rel_vx_hist.append(initial_pad_value); rel_vy_hist.append(initial_pad_value)
# Reset episode trackers
episode_reward = 0.0
episode_steps = 0
episode_goal_achieved = False
state_tuple = None # Invalidate state tuple until next loop iteration
# --- End Reward Calculation ---
# Add a pannel to show commands when pressing 'h':
keys = pygame.key.get_pressed()
if keys[pygame.K_o]: Scale *= 1.05 # Zoom in
if keys[pygame.K_p]: Scale /= 1.05 # Zoom out
if keys[pygame.K_h]:
help_lines = [
"--- Help ---",
"1: Speed up time (fast)",
"2: Speed up time (slow)",
"3: Slow down time (slow)",
"4: Slow down time (fast)",
"a: Focus previous object",
"z: Focus next object",
"t: Focus target planet",
"r: Set random target planet",
"space: Launch new rocket (from Earth)",
"backspace: Remove last rocket",
"o: Zoom in",
"p: Zoom out",
"left/right/up: Activate rocket reactors (when focusing rocket)"
]
line_height = FONT.get_height()
start_y = HEIGHT - (len(help_lines) * line_height) - 30 # Start Y position near bottom
for i, line in enumerate(help_lines):
line_surface = FONT.render(line, 1, WHITE)
# Position each line below the previous one
WIN.blit(line_surface, (10, start_y + i * line_height))
speed_text = FONT.render(f"Step per Frame: {int(Step_p_frame)}", 1, WHITE)
WIN.blit(speed_text, (WIDTH - speed_text.get_width() - 10, 25))
follow_core = Core_position[int(Flag)] if int(Flag)<len(planets) else f"Rocket {int(Flag-len(planets))}"
Flag_text = FONT.render(f"Followed Planet: {follow_core}", 1, WHITE)
WIN.blit(Flag_text, (10, 10))
Target_text = FONT.render(f"Target Planet: {Core_position[int(Target)]}", 1, WHITE)
WIN.blit(Target_text, (10, 25))
reward_text_line1 = f"Total Reward: {round(reward, 3)}"
reward_text_line2 = f" D: {round(reward_distance, 3)}, S: {round(reward_speed, 3)}, A: {round(reward_action, 3)}, T: {round(reward_time, 3)}, G: {round(reward_goal, 3)}"
reward_text1 = FONT.render(reward_text_line1, 1, WHITE)
reward_text2 = FONT.render(reward_text_line2, 1, WHITE)
WIN.blit(reward_text1, (10, 40))
WIN.blit(reward_text2, (10, 55)) # Position below the first line
Dist_text = FONT.render(f"Rocket-Tgt Distancce:{round(get_distance(planets[Target],rockets[0])/AU,5)}", 1, WHITE)
WIN.blit(Dist_text, (WIDTH-Dist_text.get_width()-10, HEIGHT-Dist_text.get_height()-25))
if(Flag<len(planets)):
Orbit_text = FONT.render(f"Is Orbiting:{is_orbiting(planets[3],planets[Flag])}", 1, WHITE)
else:
Orbit_text = FONT.render(f"Is Orbiting:{is_orbiting(rockets[Flag-len(planets)],planets[Target])}", 1, WHITE)
WIN.blit(Orbit_text, (WIDTH-Orbit_text.get_width()-10, HEIGHT-Dist_text.get_height()-10))
# in the bottom right corner : show press h to show help
help_text = FONT.render("Press h to show help-commands", 1, WHITE)
WIN.blit(help_text, (10, HEIGHT - help_text.get_height() - 10))
# --- Show fps ---
fps = clock.get_fps()
fps_text = FONT.render(f"FPS: {int(fps)}", 1, WHITE)
WIN.blit(fps_text, (WIDTH - fps_text.get_width() - 10, HEIGHT-Dist_text.get_height()-40))
# --- Update Display ---
if n_step%Step_p_frame == 0:
# --- Draw Planets and Rockets ---
if Flag<len(planets):
shift_x = - planets[Flag].x
shift_y = - planets[Flag].y
else:
shift_x = - rockets[Flag-len(planets)].x
shift_y = - rockets[Flag-len(planets)].y
for core in planets+rockets:
core.draw(WIN, shift_x, shift_y, Scale)
pygame.display.update()
n_step += 1
pygame.quit()
print("Simulation ended.")
# Optional: Save the trained model
agent.save_weights_and_distances(all_distance_acquisitions)
# Close the log file
agent.close_log()
if __name__ == '__main__':
SatSim(load_weights=True) |