"""Validacao retrospectiva com voos reais de 2025. Entradas: dados/Quadro de Voo 2025 (2).xlsx, aba VOOS, ou o CSV ja importado em dados/validacao/voos_2025.csv. Saidas: resultados/validacao/validacao_2025_resumo.xlsx resultados/validacao/validacao_2025_detalhada.xlsx resultados/validacao/validacao_2025_metricas.csv resultados/validacao/validacao_2025_barras.png Papel no pipeline: Mantém a demanda histórica de 2025, gera candidatos compatíveis para cada slot histórico e usa MILP para redistribuir tripulantes preservando horas. """ from __future__ import annotations import importlib import re import struct import zlib from datetime import time, timedelta from pathlib import Path import pandas as pd config = importlib.import_module("01_config") io_utils = importlib.import_module("02_io_utils") COLUNAS_VALIDACAO = ["slot_id", "data", "aeronave", "tipo_escala", "tripulante", "funcao", "oi", "horas_voadas", "sbv"] def _normalizar_texto(valor: object) -> str: from src.planejador_missao.utils import normalizar_texto return normalizar_texto(valor) def _horas(valor: object) -> float: """Converte TEV da planilha historica para horas decimais.""" if pd.isna(valor): return 0.0 if isinstance(valor, timedelta): return round(valor.total_seconds() / 3600, 2) if isinstance(valor, time): return round(valor.hour + valor.minute / 60 + valor.second / 3600, 2) if isinstance(valor, str): texto = valor.strip() if not texto or texto == "-": return 0.0 partes = texto.split(":") if len(partes) >= 2: return round(float(partes[0]) + float(partes[1]) / 60, 2) return float(texto.replace(",", ".")) if isinstance(valor, (int, float)): return round(float(valor) * 24, 2) if 0 < float(valor) < 1 else float(valor) return 0.0 def _aeronave(valor: object) -> str: """Traduz matricula FAB para familia operacional C95/C97/C98.""" texto = _normalizar_texto(valor) match = re.search(r"(\d{4})", texto) if not match: return "" numero = match.group(1) if numero.startswith("20"): return "C97" if numero.startswith("23"): return "C95" if numero.startswith("27"): return "C98" return "" def _tripulantes(valor: object) -> list[str]: """Extrai a dupla principal da tripulacao historica.""" partes = re.split(r"\s*/\s*", str(valor)) trips = [] for parte in partes: texto = _normalizar_texto(parte) texto = re.sub(r"\([^)]*\)", " ", texto) texto = re.sub(r"[^A-Z0-9]+", " ", texto).strip() if texto and texto != "NAN": trips.append(texto) return trips[:2] def importar_voos_2025(origem: Path = config.ARQUIVO_QUADRO_VOO_2025, destino: Path = config.ARQUIVO_VOOS_2025) -> pd.DataFrame: """Importa voos realizados de 2025 da aba VOOS para o CSV de validação.""" io_utils.exigir_arquivos([origem]) raw = pd.read_excel(origem, sheet_name=config.ABAS["voos_2025"], header=None) linhas = [] slot_id = 0 for _, row in raw.iterrows(): data = pd.to_datetime(row[2], errors="coerce") if pd.isna(data) or data.year != 2025: continue if _normalizar_texto(row[41]) != "REALIZADO": continue aeronave = _aeronave(row[37]) if not aeronave: continue horas_voadas = _horas(row[30]) if horas_voadas <= 0: continue trips = _tripulantes(row[8]) if len(trips) < 2: continue slot_id += 1 tipo_escala = _normalizar_texto(row[1]) oi = _normalizar_texto(row[35]) if not oi or oi == "-": oi = _normalizar_texto(row[32]) for idx, tripulante in enumerate(trips, start=1): linhas.append( { "slot_id": slot_id, "data": data.date().isoformat(), "aeronave": aeronave, "tipo_escala": tipo_escala, "tripulante": tripulante, "funcao": "PILOTO" if idx == 1 else "COPILOTO", "oi": oi, "horas_voadas": horas_voadas, "sbv": 0, } ) df = pd.DataFrame(linhas, columns=COLUNAS_VALIDACAO) io_utils.exportar_csv(destino, df) return df def _png_barras(path: Path, metricas: pd.DataFrame) -> Path: """Gera PNG simples sem depender de bibliotecas graficas externas.""" width, height = 760, 420 canvas = bytearray([255, 255, 255] * width * height) def rect(x0: int, y0: int, x1: int, y1: int, color: tuple[int, int, int]) -> None: x0, x1 = max(0, x0), min(width, x1) y0, y1 = max(0, y0), min(height, y1) for y in range(y0, y1): for x in range(x0, x1): i = (y * width + x) * 3 canvas[i : i + 3] = bytes(color) rect(55, 45, 60, 365, (38, 57, 77)) rect(55, 360, 705, 365, (38, 57, 77)) valores = metricas.set_index("cenario")["desvio_padrao"].to_dict() max_v = max(valores.values()) if valores else 1 cores = {"real_2025": (154, 52, 18), "otimizado_meta_50": (38, 115, 77)} xs = {"real_2025": 190, "otimizado_meta_50": 430} for cenario, valor in valores.items(): h = int((valor / max_v) * 270) rect(xs[cenario], 360 - h, xs[cenario] + 110, 360, cores[cenario]) raw = b"".join(b"\x00" + canvas[y * width * 3 : (y + 1) * width * 3] for y in range(height)) png = b"\x89PNG\r\n\x1a\n" for chunk_type, data in [ (b"IHDR", struct.pack(">IIBBBBB", width, height, 8, 2, 0, 0, 0)), (b"IDAT", zlib.compress(raw, 9)), (b"IEND", b""), ]: png += struct.pack(">I", len(data)) + chunk_type + data + struct.pack(">I", zlib.crc32(chunk_type + data) & 0xFFFFFFFF) path.parent.mkdir(parents=True, exist_ok=True) path.write_bytes(png) return path def executar_validacao_2025(importar: bool = True, logger=None) -> dict: """Executa a validação completa e exporta os artefatos finais.""" io_utils.configurar_ambiente() logger = logger or io_utils.configurar_logger("validacao_2025") inicio = pd.Timestamp.now() if importar or not config.ARQUIVO_VOOS_2025.exists(): voos_importados = importar_voos_2025() logger.info("Voos 2025 importados: %s registros", len(voos_importados)) from src.planejador_missao.validacao import carregar_voos_2025, otimizar_equalizacao_50 voos = carregar_voos_2025(config.ARQUIVO_VOOS_2025) if voos.empty: raise RuntimeError("Erro: base de validação 2025 vazia. Verifique dados/validacao/voos_2025.csv.") if (voos["horas_voadas"] < 0).any(): raise RuntimeError("Erro: foram encontradas horas voadas negativas na validação 2025.") resultado = otimizar_equalizacao_50( config.PROJECT_ROOT, voos, meta_horas=config.META_HORAS_PADRAO, tempo_limite_segundos=config.VALIDACAO_TEMPO_LIMITE_SEGUNDOS, gap_relativo=config.VALIDACAO_GAP_RELATIVO, ) metricas = resultado["metricas"] total_real = float(metricas.loc[metricas["cenario"] == "real_2025", "horas_total"].iloc[0]) total_otim = float(metricas.loc[metricas["cenario"] == "otimizado_meta_50", "horas_total"].iloc[0]) if abs(total_real - total_otim) > 0.01: logger.warning("Horas não preservadas: real=%s otimizado=%s", total_real, total_otim) resumo_path = config.VALIDACAO_DIR / "validacao_2025_resumo.xlsx" detalhada_path = config.VALIDACAO_DIR / "validacao_2025_detalhada.xlsx" metricas_path = config.VALIDACAO_DIR / "validacao_2025_metricas.csv" grafico_path = config.VALIDACAO_DIR / "validacao_2025_barras.png" solver_df = pd.DataFrame([resultado.get("status_solver", {})]) io_utils.exportar_excel(resumo_path, {"solver": solver_df, "metricas": metricas}) io_utils.exportar_excel( detalhada_path, { "solver": solver_df, "metricas": metricas, "comparativo_trips": resultado["comparativo"], "voos_2025_slots": resultado["slots"], "escala_otimizada": resultado["otimizados"], }, ) io_utils.exportar_csv(metricas_path, metricas) _png_barras(grafico_path, metricas) duracao = (pd.Timestamp.now() - inicio).total_seconds() logger.info("Slots historicos avaliados: %s", len(resultado["slots"])) logger.info("Alocacoes otimizadas: %s", len(resultado["otimizados"])) logger.info("Tripulantes comparados: %s", len(resultado["comparativo"])) logger.info("Status solver: %s", resultado["status_solver"]["mensagem"]) logger.info("Tempo de validação: %.1f s", duracao) logger.info("Arquivos exportados: %s | %s | %s | %s", resumo_path, detalhada_path, metricas_path, grafico_path) return { "resultado": resultado, "arquivos": [resumo_path, detalhada_path, metricas_path, grafico_path], "duracao_segundos": duracao, } def main() -> None: """Ponto de entrada da validação 2025.""" logger = io_utils.configurar_logger("validacao_2025") saida = executar_validacao_2025(importar=True, logger=logger) metricas = saida["resultado"]["metricas"] print("\n=== Validação 2025 concluída ===") print(metricas.to_string(index=False)) print("\nArquivos gerados:") for path in saida["arquivos"]: print(f"- {path}") if __name__ == "__main__": main()