agrobot_base/Python/trajetoria-dinamica/treinamento2.py

541 lines
21 KiB
Python

import os
import time
import gymnasium as gym
import numpy as np
import json
import logging
from stable_baselines3 import PPO
from stable_baselines3.common.callbacks import CheckpointCallback
from geopy.distance import geodesic
import paho.mqtt.client as mqtt
# Configurações gerais
CHECKPOINT_DIR = "./checkpoints/"
MODEL_NAME = "robot_corridor_model"
BROKER = "localhost"
PORT = 1883
STATE_TOPIC = "robot/state"
ACTION_TOPIC = "robot/actions"
aprendizado_centralizacao_corredor = True
aprendizado_desvio_obstaculos = False
aprendizado_aproximacao_proximo_ponto = True
aprendizado_alinhamento_com_rua = True
# Configuração do logger
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger("RobotEnv")
# Variáveis globais
current_state = None
new_state_received = False
no_corredor = False
fator_aproximacao = 0
first_distance = 0
max_distance = 8.2
max_angle = 45.0
# Funções utilitárias
def calculate_distance(lat1, lon1, lat2, lon2):
return geodesic((lat1, lon1), (lat2, lon2)).meters
def calculate_angular_difference(angle1, angle2):
diff = (angle2 - angle1 + 360) % 360
return diff if diff <= 180 else 360 - diff
def normalize_keys(data):
if isinstance(data, dict):
return {k.lower(): normalize_keys(v) for k, v in data.items()}
elif isinstance(data, list):
return [normalize_keys(i) for i in data]
else:
return data
def on_message(client, userdata, message):
global current_state, new_state_received
payload = message.payload.decode("utf-8")
current_state = normalize_keys(json.loads(payload))
new_state_received = True
# Configuração do cliente MQTT
client = mqtt.Client()
client.on_message = on_message
client.connect(BROKER, PORT)
client.subscribe(STATE_TOPIC)
client.loop_start()
# Ambiente do robô
class RobotEnv(gym.Env):
def __init__(self):
super(RobotEnv, self).__init__()
# Espaço de ação
self.action_limits = {
"angle": (-30, 30),
"movement_type": (0, 1)
}
self.action_space = gym.spaces.Box(low=-1, high=1, shape=(2,), dtype=np.float32)
# Espaço de observação
self.observation_space = gym.spaces.Box(
low=-np.inf, high=np.inf, shape=(55,), dtype=np.float32
)
# Variáveis internas
self.state = None
self.max_episode_time = 60
self.previous_distance_to_next_point = None
self.reward = 0
def reset(self, seed=None):
global new_state_received, current_state, first_distance
self.previous_distance_to_next_point = None
self.reward = 0
new_state_received = False
first_distance = 0
# Capturar o timestamp inicial do episódio
self.episode_start_time = time.time()
logger.info("Resetando o ambiente...")
while not new_state_received:
time.sleep(0.01)
self.state = self._process_state(current_state)
return self.state, {}
def step(self, action):
global new_state_received, current_state
if not self.action_space.contains(action):
logger.warning(f"Ação inválida: {action}. Clamping para limites válidos.")
action = np.clip(action, self.action_space.low, self.action_space.high)
angle = self._denormalize_action(action[0], *self.action_limits["angle"])
movement_type = int(self._denormalize_action(action[1], *self.action_limits["movement_type"]))
action_payload = json.dumps({
"angle": float(angle),
"movementtype": movement_type,
"reward": float(self.reward)
})
client.publish(ACTION_TOPIC, action_payload)
new_state_received = False
while not new_state_received:
time.sleep(0.01)
self.state = self._process_state(current_state)
reward = self._calculate_reward()
terminated, truncated, add_reward = self._check_termination_conditions()
reward += add_reward
self.reward = reward
logger.info(f"Ação: {action_payload}, Recompensa: {reward}, Terminou: {terminated}, Truncado: {truncated}")
if (terminated or truncated):
action_payload = json.dumps({
"reset": True
})
client.publish(ACTION_TOPIC, action_payload)
self.state = self._validate_tensor(self.state, name="state")
return self.state, reward, terminated, truncated, {}
def _process_state(self, state):
# Verifica se há obstáculos no estado recebido
obstacles = state["current_state"].get("obstacles", [])
if obstacles: # Caso existam obstáculos, processa os dados
processed_obstacles = np.full((10, 3), -1, dtype=np.float32) # Inicializa com -1
for i, obs in enumerate(obstacles[:10]): # Considere até 10 obstáculos
processed_obstacles[i] = [
obs.get("distance", -1), # Distância do obstáculo
obs.get("width", -1), # Largura do obstáculo
obs.get("sidepercentual", -1) # Posição percentual (0-100%)
]
else: # Caso a lista esteja vazia, inicializa com -1
processed_obstacles = np.full((10, 3), -1, dtype=np.float32)
# Processa o restante do estado
processed_state = {
"current_state": {
"position": np.array([
state["current_state"]["position"]["latitude"],
state["current_state"]["position"]["longitude"],
state["current_state"]["position"]["orientation"]
], dtype=np.float32),
"speed": np.array([state["current_state"]["control"]["speed"]], dtype=np.float32),
"distancetoedges": np.array([
state["current_state"]["distancetoedges"]["left"],
state["current_state"]["distancetoedges"]["right"]
], dtype=np.float32),
"obstacles": processed_obstacles,
"next_point": np.array([
state["current_state"]["nextpoints"][0]["latitude"],
state["current_state"]["nextpoints"][0]["longitude"],
state["current_state"]["nextpoints"][0]["distance"],
state["current_state"]["nextpoints"][0]["orientation"],
state["current_state"]["nextpoints"][0]["estimatedtime"],
state["current_state"]["nextpoints"][0]["approaching"]
], dtype=np.float32),
"street_end_position": np.array([
state["current_state"]["street"]["street_end_position"]["latitude"],
state["current_state"]["street"]["street_end_position"]["longitude"]
], dtype=np.float32),
"street_status": np.array([
state["current_state"]["street"]["insidestreet"],
state["current_state"]["street"]["edgestreet"],
state["current_state"]["street"]["orientation"]
], dtype=np.float32)
},
"deltas": {
"position_delta": np.array([
state["deltas"]["positiondelta"]["deltalatitude"],
state["deltas"]["positiondelta"]["deltalongitude"],
state["deltas"]["positiondelta"]["deltapositiondistance"]
], dtype=np.float32),
"delta_speed": np.array([state["deltas"]["deltaspeed"]], dtype=np.float32),
"delta_angle": np.array([state["deltas"]["deltaangle"]], dtype=np.float32),
"delta_distancetoedges": np.array([
state["deltas"]["deltadistancetoedgesleft"],
state["deltas"]["deltadistancetoedgesright"]
], dtype=np.float32)
},
"time_elapsed": np.array([state["deltas"]["deltatimestamp"]], dtype=np.float32)
}
# Combine os dados para retornar como vetor achatado
combined_state = np.concatenate([
processed_state["current_state"]["position"],
processed_state["current_state"]["speed"],
processed_state["current_state"]["distancetoedges"],
processed_state["current_state"]["obstacles"].flatten(),
processed_state["current_state"]["next_point"],
processed_state["current_state"]["street_end_position"],
processed_state["current_state"]["street_status"],
processed_state["deltas"]["position_delta"],
processed_state["deltas"]["delta_speed"],
processed_state["deltas"]["delta_angle"],
processed_state["deltas"]["delta_distancetoedges"],
processed_state["time_elapsed"]
]).astype(np.float32) # Converte explicitamente para float32
combined_state = self._validate_tensor(combined_state, name="combined_state")
return combined_state
def _denormalize_action(self, value, min_val, max_val):
return (value + 1) * 0.5 * (max_val - min_val) + min_val
def _validate_tensor(self, tensor, name="tensor"):
"""
Valida o tensor para garantir que ele não contenha valores inválidos (NaN, inf, -inf).
Substitui valores inválidos por zeros e avisa sobre o problema.
"""
if not np.isfinite(tensor).all(): # Verifica se há valores inválidos
print(f"Tensor inválido detectado no {name}: {tensor}")
tensor = np.nan_to_num(tensor, nan=0.0, posinf=0.0, neginf=0.0) # Substitui NaN/Inf
return tensor
def _calculate_reward(self):
global fator_aproximacao, no_corredor, max_distance
reward = 0
car_angle = self.state[2] # Orientação do robô (em graus)
edges = self.state[4:6] # Distância às bordas esquerda e direita
obstacles = self.state[6:36].reshape(-1, 3) # Lista de obstáculos
current_distance = self.state[38] # Distância ao próximo ponto
current_orientation = self.state[39] # Orientacao em relacao ao próximo ponto
current_approaching = self.state[40] # Se aproximando em relacao ao próximo ponto
inside_street = self.state[44] # Indicador "InsideStreet"
edge_street = self.state[45] # Indicador "EdgeStreet"
street_angle = self.state[46] # Orientação da rua (em graus)
# Validação dos vetores
edges = self._validate_tensor(edges, name="edges")
obstacles = self._validate_tensor(obstacles, name="obstacles")
no_corredor = inside_street or edge_street
### 1. Centralização no corredor ###
if (aprendizado_centralizacao_corredor):
if no_corredor: # Apenas quando no corredor
left_dist, right_dist = edges
if left_dist > 0 and right_dist > 0: # Dentro dos limites
difference = abs(left_dist - right_dist)
reward += max(0, 5 - (difference ** 2) * 0.5)
print(f"Centralizado no corredor: reward +{max(0, 5 - difference * 2)}, edges={edges}")
else:
reward -= 5 # Penalidade forte para sair do corredor
print(f"Descentralizado ou fora do corredor: reward -5, edges={edges}")
### 2. Obstáculos ###
if (aprendizado_desvio_obstaculos):
if obstacles.size > 0: # Apenas se houver obstáculos
for obs in obstacles:
if obs[0] >= 0 and obs[0] < 1.0: # Obstáculo muito próximo
reward -= 5
print(f"Obstáculo muito próximo: reward -5, obs={obs}")
elif obs[0] >= 0 and (obs[2] < 30 or obs[2] > 70): # Fora da faixa central
reward -= 2
print(f"Obstáculo fora da faixa central: reward -2, obs={obs}")
### 3. Aproximação ao próximo ponto ###
if (aprendizado_aproximacao_proximo_ponto):
# Verifica se estamos se aproximando ou se afastando
if self.previous_distance_to_next_point is not None and np.isfinite(self.previous_distance_to_next_point):
delta_distance = self.previous_distance_to_next_point - current_distance
delta_distance = self._validate_tensor(delta_distance, name="delta_distance")
fator_aproximacao = delta_distance
# Define o peso com base no contexto (no corredor ou fora)
weight = 2 if not no_corredor else 1 # Peso maior fora do corredor
rwd = 0
if delta_distance <= 0: # Se afastando do ponto
# Penalidade: distância maior ou igual a 5 -> penalidade máxima (-5)
# distância menor ou igual a 0 -> penalidade mínima (-1)
min_penalty = -1
max_penalty = -5
normalized_distance = min(current_distance, max_distance) / max_distance
rwd = weight * (max_penalty + (max_penalty - min_penalty) * np.log1p(normalized_distance))
else: # Se aproximando do ponto
# Recompensa: distância igual a 0 -> recompensa máxima (5)
# distância maior ou igual a 5 -> recompensa mínima (1)
min_reward = 1
max_reward = 5
normalized_distance = min(current_distance, max_distance) / max_distance
rwd = weight * (min_reward + (max_reward - min_reward) * (1 - np.log1p(normalized_distance)))
# Atualiza a recompensa
reward += rwd
else:
print("Primeiro ponto, sem delta_distance.")
# Atualiza a distância anterior
self.previous_distance_to_next_point = current_distance
### 4. Alinhamento Angular com a Rua ###
if aprendizado_alinhamento_com_rua:
compare_angle = street_angle if no_corredor else current_orientation
car_angle = self._validate_tensor(car_angle, name="car_angle")
compare_angle = self._validate_tensor(compare_angle, name="compare_angle")
# Calcula a diferença absoluta entre os ângulos
angular_difference = np.abs(car_angle - compare_angle)
# Ajusta para lidar com o ciclo angular (360 graus)
if angular_difference > 180:
angular_difference = 360 - angular_difference
# Recompensa logarítmica com base no alinhamento angular
max_angular_difference = 180.0
normalized_angular_difference = angular_difference / max_angular_difference
angular_reward = 5 * (1 - np.log1p(normalized_angular_difference))
reward += angular_reward
### 5. Validação final da recompensa ###
reward = float(self._validate_tensor(np.array([reward], dtype=np.float32), name="reward")[0])
return reward
def _check_termination_conditions(self):
global max_distance, max_angle, first_distance
terminated = False
truncated = False
reward = 0
# Verificar se o tempo real do episódio excedeu o limite
elapsed_time = time.time() - self.episode_start_time
if self._reached_end_of_street():
print("Episódio concluído: Final da rua!")
efficiency = self.max_episode_time - elapsed_time
reward += 50 + (efficiency / self.max_episode_time) * 10 # Recompensa extra por eficiência
terminated = True
elif self._out_of_bounds():
print("Episódio concluído: Saiu do corredor!")
reward -= 20
terminated = True
elif self._collided_with_obstacle():
print("Episódio concluído: Colisão!")
reward -= 30
terminated = True
elif self._out_of_next_point_range(range=max_distance):
print("Episódio concluído: Fora do alcance do próximo ponto!")
reward -= 8
terminated = True
elif self._angle_misalignment(threshold=max_angle):
print("Episódio concluído: Desalinhamento angular excessivo!")
reward -= 9
terminated = True
elif not np.isfinite(reward):
print("Recompensa inválida detectada! Abortando episódio.")
reward += 0
truncated = True
# Calcular recompensa extra de acordo com a distancia que o robo parou do ultimo ponto
distance_to_end = self._distance_end_of_street()
normalized_distance = distance_to_end / first_distance
# Recompensa baseada na distância normalizada, escalada para 0 a 10
reward_distance = 10 * np.exp(-normalized_distance * 5.0)
reward += reward_distance
if elapsed_time >= self.max_episode_time:
reward -= 50
truncated = True
print("Episódio truncado: Tempo máximo atingido.")
return terminated, truncated, reward
def _distance_end_of_street(self):
"""
Verifica a distancia do robo ate o ultimo ponto da rua.
"""
global first_distance
street_end_lat = self.state[41]
street_end_lon = self.state[42]
current_lat = self.state[0]
current_lon = self.state[1]
distance_to_end = calculate_distance(current_lat, current_lon, street_end_lat, street_end_lon)
if first_distance == 0:
first_distance = distance_to_end
return distance_to_end
def _reached_end_of_street(self):
"""
Verifica se o robô chegou no último ponto da rua.
"""
distance_to_end = self._distance_end_of_street()
return distance_to_end < 1.0
def _out_of_bounds(self):
"""
Verifica se o robô invadiu alguma margem de rua.
"""
global no_corredor
if (not aprendizado_centralizacao_corredor):
return False
edges = self.state[4:6]
if no_corredor:
return edges[0] < 0 or edges[1] < 0
return False
def _collided_with_obstacle(self):
"""
Verifica se o robô colidiu com algum obstáculo.
"""
if (not aprendizado_desvio_obstaculos):
return False
# Extrai os obstáculos do vetor de estado achatado
obstacles = self.state[13:43].reshape(-1, 3) # 10 obstáculos, 3 valores cada (distância, largura, lado)
# Itera sobre os obstáculos e verifica colisões
for obstacle in obstacles:
if obstacle[0] > 0 and obstacle[0] < 0.5: # Distância menor que 0.5 indica colisão
return True
return False # Nenhuma colisão detectada
def _out_of_next_point_range(self, range = 5.0):
"""
Verifica se o robô se afastou muito do próximo ponto.
"""
current_approaching = self.state[40] # Se aproximando em relacao ao próximo ponto
#print(fator_aproximacao)
if (not aprendizado_aproximacao_proximo_ponto):
return False
current_distance = self.state[38] # Distância ao próximo ponto
if (current_distance > range and current_approaching == False): # Robo esta a mais de 5 metros do proximo ponto e esta se afastabdo
return True
return False
def _angle_misalignment(self, threshold=30.0):
"""
Verifica se a diferença angular entre o robô e a rua excede um limite.
"""
if not aprendizado_alinhamento_com_rua:
return False
car_angle = self.state[2] # Orientação do robô (em graus)
street_angle = self.state[46] # Orientação da rua (em graus)
current_orientation = self.state[39] # Orientacao ate o proximo ponto
inside_street = self.state[44] # Indicador "InsideStreet"
edge_street = self.state[45] # Indicador "EdgeStreet"
if not inside_street:
threshold = threshold / 2
compare_angle = street_angle if inside_street else current_orientation
# Calcula a diferença absoluta entre os ângulo
angular_difference = np.abs(car_angle - compare_angle)
# Ajusta para o ciclo de 360 graus
if angular_difference > 180:
angular_difference = 360 - angular_difference
# Verifica se a diferença excede o limite
if angular_difference > threshold:
return True
return False
# Configuração do ambiente e treinamento
env = RobotEnv()
latest_checkpoint = max(
[os.path.join(CHECKPOINT_DIR, f) for f in os.listdir(CHECKPOINT_DIR) if f.endswith(".zip")],
key=os.path.getctime,
default=None
)
if latest_checkpoint:
logger.info(f"Carregando checkpoint mais recente: {latest_checkpoint}")
model = PPO.load(latest_checkpoint, env=env, verbose=1, device="cpu")
elif os.path.exists(MODEL_NAME + ".zip"):
logger.info("Carregando modelo salvo anteriormente...")
model = PPO.load(MODEL_NAME + ".zip", env=env, verbose=1, device="cpu")
else:
logger.info("Criando novo modelo...")
model = PPO("MlpPolicy", env, verbose=1, device="cpu")
checkpoint_callback = CheckpointCallback(save_freq=5000, save_path=CHECKPOINT_DIR, name_prefix=MODEL_NAME)
logger.info("Iniciando o treinamento...")
model.learn(total_timesteps=100000, callback=[checkpoint_callback])
model.save(f"final_{MODEL_NAME}")
logger.info(f"Treinamento concluído e modelo salvo como final_{MODEL_NAME}.")