DynamisLab/scripts/train_ppo.py
2026-02-20 11:57:01 +08:00

317 lines
10 KiB
Python

#!/usr/bin/env python3
"""
Train PPO agent for CFD flow control.
This script trains a Proximal Policy Optimization (PPO) agent to control
flow around a cylinder using the CFD environment.
"""
import argparse
import os
import pickle
from pathlib import Path
import sys
# Set threading layers
os.environ['MKL_THREADING_LAYER'] = 'GNU'
os.environ["OMP_NUM_THREADS"] = "8"
os.environ["MKL_NUM_THREADS"] = "8"
import numpy as np
import torch
from torch.nn import Module
from stable_baselines3 import PPO
from stable_baselines3.common.callbacks import BaseCallback
from torch.utils.tensorboard import SummaryWriter
# Add src to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent / 'src'))
from environments import CFDFlowControlEnv
from config import (
load_celeris_configs,
get_model_path,
get_tensorboard_logdir,
get_output_path,
)
class SinActivation(Module):
"""Sine activation function for neural networks."""
def __init__(self):
super().__init__()
def forward(self, x):
return torch.sin(x)
class TensorboardCallback(BaseCallback):
"""
Custom callback for logging additional metrics to TensorBoard.
"""
def __init__(self, check_freq: int = 360, verbose: int = 0):
super().__init__(verbose)
self.check_freq = check_freq
self.episode_rewards = []
self.episode_cd = []
self.episode_cl = []
def _on_step(self) -> bool:
if self.n_calls % self.check_freq == 0:
# Extract episode info
if len(self.locals.get('infos', [])) > 0:
info = self.locals['infos'][0]
if 'cd' in info:
self.logger.record('flow/cd', info['cd'])
if 'cl' in info:
self.logger.record('flow/cl', info['cl'])
if 'reward_cd' in info:
self.logger.record('reward/cd', info['reward_cd'])
if 'reward_cl' in info:
self.logger.record('reward/cl', info['reward_cl'])
if 'reward_sim' in info:
self.logger.record('reward/sim', info['reward_sim'])
return True
def parse_args():
"""Parse command line arguments."""
parser = argparse.ArgumentParser(description='Train PPO for CFD control')
# Environment settings
parser.add_argument('--device-id', type=int, default=0,
help='CUDA device ID for simulation')
# Training hyperparameters
parser.add_argument('--total-timesteps', type=int, default=100,
help='Number of training iterations (each = n_steps)')
parser.add_argument('--n-steps', type=int, default=3600,
help='Steps to collect per training iteration')
parser.add_argument('--batch-size', type=int, default=360,
help='Batch size for PPO updates')
parser.add_argument('--learning-rate', type=float, default=3e-4,
help='Learning rate')
parser.add_argument('--gamma', type=float, default=0.99,
help='Discount factor')
# Model settings
parser.add_argument('--activation', choices=['tanh', 'relu', 'sin'], default='sin',
help='Activation function for policy network')
parser.add_argument('--cuda-device', type=int, default=0,
help='CUDA device for PyTorch training')
# Experiment settings
parser.add_argument('--run-name', type=str, default='ppo_cfd_control',
help='Name for this training run')
parser.add_argument('--save-freq', type=int, default=10,
help='Save model every N iterations')
parser.add_argument('--eval-episodes', type=int, default=1,
help='Number of episodes to evaluate')
# Resume training
parser.add_argument('--resume', type=str, default=None,
help='Path to model checkpoint to resume from')
return parser.parse_args()
def create_env(device_id: int):
"""Create the CFD environment."""
config_cuda, config_field = load_celeris_configs()
env = CFDFlowControlEnv(
device_id=device_id,
config_cuda=config_cuda,
config_field=config_field,
)
return env
def get_activation_fn(name: str):
"""Get activation function by name."""
if name == 'sin':
return SinActivation
elif name == 'tanh':
return torch.nn.Tanh
elif name == 'relu':
return torch.nn.ReLU
else:
raise ValueError(f"Unknown activation: {name}")
def evaluate_policy(model, env, n_episodes: int = 1):
"""
Evaluate the trained policy.
Returns:
Dictionary of evaluation metrics
"""
episode_rewards = []
episode_data = []
for episode in range(n_episodes):
obs, info = env.reset()
done = False
episode_reward = 0
steps = 0
ep_data = {
'actions': [],
'observations': [],
'rewards': [],
'cd': [],
'cl': [],
}
while not done:
action, _states = model.predict(obs, deterministic=True)
obs, reward, terminated, truncated, info = env.step(action)
done = terminated or truncated
episode_reward += reward
steps += 1
# Record data
ep_data['actions'].append(action)
ep_data['observations'].append(obs)
ep_data['rewards'].append(reward)
ep_data['cd'].append(info.get('cd', 0))
ep_data['cl'].append(info.get('cl', 0))
episode_rewards.append(episode_reward)
episode_data.append(ep_data)
print(f" Episode {episode + 1}/{n_episodes}: "
f"Reward = {episode_reward:.2f}, "
f"Steps = {steps}, "
f"Avg CD = {np.mean(ep_data['cd']):.4f}")
return {
'mean_reward': np.mean(episode_rewards),
'std_reward': np.std(episode_rewards),
'episodes': episode_data,
}
def main():
"""Main training loop."""
args = parse_args()
print("=" * 70)
print(f"DynamisLab - CFD Flow Control Training")
print(f"Run: {args.run_name}")
print("=" * 70)
# Create environment
print(f"\n[1/4] Creating CFD environment on GPU:{args.device_id}...")
env = create_env(args.device_id)
print(f" Action space: {env.action_space}")
print(f" Observation space: {env.observation_space}")
# Create or load model
print(f"\n[2/4] Setting up PPO model...")
device = torch.device(f"cuda:{args.cuda_device}")
if args.resume:
print(f" Resuming from: {args.resume}")
model = PPO.load(args.resume, env=env, device=device)
else:
activation_fn = get_activation_fn(args.activation)
model = PPO(
"MlpPolicy",
env=env,
learning_rate=args.learning_rate,
n_steps=args.n_steps,
batch_size=args.batch_size,
gamma=args.gamma,
policy_kwargs=dict(activation_fn=activation_fn),
device=device,
verbose=1,
)
print(f" Activation: {args.activation}")
print(f" Device: {device}")
print(f" Learning rate: {args.learning_rate}")
print(f" Steps per iteration: {args.n_steps}")
print(f" Batch size: {args.batch_size}")
# Setup logging
tensorboard_dir = get_tensorboard_logdir(args.run_name)
writer = SummaryWriter(log_dir=str(tensorboard_dir))
print(f" TensorBoard: {tensorboard_dir}")
# Training loop
print(f"\n[3/4] Training for {args.total_timesteps} iterations...")
best_reward = -np.inf
history_data = []
for iteration in range(args.total_timesteps):
# Train
model.learn(total_timesteps=args.n_steps, reset_num_timesteps=False)
# Evaluate
print(f"\n--- Iteration {iteration + 1}/{args.total_timesteps} ---")
eval_results = evaluate_policy(model, env, n_episodes=args.eval_episodes)
mean_reward = eval_results['mean_reward']
std_reward = eval_results['std_reward']
# Log to TensorBoard
writer.add_scalar('eval/mean_reward', mean_reward, iteration)
writer.add_scalar('eval/std_reward', std_reward, iteration)
# Extract CD/CL from last episode
if len(eval_results['episodes']) > 0:
last_ep = eval_results['episodes'][-1]
avg_cd = np.mean(last_ep['cd'])
avg_cl = np.mean(last_ep['cl'])
writer.add_scalar('eval/avg_cd', avg_cd, iteration)
writer.add_scalar('eval/avg_cl', avg_cl, iteration)
# Save best model
if mean_reward > best_reward:
best_reward = mean_reward
model_path = get_model_path(f"{args.run_name}_best")
model.save(str(model_path))
print(f" ✓ New best model saved: {model_path} (reward: {mean_reward:.2f})")
# Periodic save
if (iteration + 1) % args.save_freq == 0:
model_path = get_model_path(f"{args.run_name}_iter{iteration + 1}")
model.save(str(model_path))
print(f" Checkpoint saved: {model_path}")
# Store history
history_data.append(eval_results['episodes'])
# Final evaluation
print(f"\n[4/4] Final evaluation...")
final_results = evaluate_policy(model, env, n_episodes=5)
print(f" Final mean reward: {final_results['mean_reward']:.2f} ± {final_results['std_reward']:.2f}")
# Save final model and history
final_model_path = get_model_path(f"{args.run_name}_final")
model.save(str(final_model_path))
print(f" Final model saved: {final_model_path}")
history_path = get_output_path(f"{args.run_name}_history.pkl")
with open(history_path, 'wb') as f:
pickle.dump(history_data, f)
print(f" Training history saved: {history_path}")
# Cleanup
writer.close()
env.close()
print("\n" + "=" * 70)
print("Training complete!")
print("=" * 70)
if __name__ == '__main__':
main()