Initial project import
This commit is contained in:
103
scripts/00_main.py
Normal file
103
scripts/00_main.py
Normal file
@@ -0,0 +1,103 @@
|
||||
"""Orquestrador principal do Planejador Missao.
|
||||
|
||||
Entradas:
|
||||
Arquivos em dados/, argumentos opcionais de linha de comando e configuracoes
|
||||
centralizadas em scripts/01_config.py.
|
||||
|
||||
Saidas:
|
||||
Planejamento diario em resultados/, historico operacional atualizado,
|
||||
relatorios de validacao quando solicitado e logs em logs/execucao.log.
|
||||
|
||||
Papel no pipeline:
|
||||
Executa o fluxo completo: leitura de dados, preparacao, geracao de colunas
|
||||
candidatas, montagem/solucao do MILP, exportacao de resultados e validacao.
|
||||
|
||||
Exemplos:
|
||||
python scripts/00_main.py --data 2026-01-02 --modo diario
|
||||
python scripts/00_main.py --validacao 2025
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import importlib
|
||||
import time
|
||||
|
||||
import pandas as pd
|
||||
|
||||
config = importlib.import_module("01_config")
|
||||
io_utils = importlib.import_module("02_io_utils")
|
||||
|
||||
|
||||
def atualizar_data_planejamento(data: str) -> None:
|
||||
"""Atualiza dados/parametros_missao.csv com a data solicitada."""
|
||||
path = config.ARQUIVO_PARAMETROS
|
||||
df = pd.read_csv(path) if path.exists() else pd.DataFrame(columns=["parametro", "valor"])
|
||||
if "parametro" not in df.columns or "valor" not in df.columns:
|
||||
raise ValueError("Erro: parametros_missao.csv deve conter as colunas parametro,valor.")
|
||||
if (df["parametro"] == "data_planejamento").any():
|
||||
df.loc[df["parametro"] == "data_planejamento", "valor"] = data
|
||||
else:
|
||||
df = pd.concat([df, pd.DataFrame([{"parametro": "data_planejamento", "valor": data}])], ignore_index=True)
|
||||
df.to_csv(path, index=False)
|
||||
|
||||
|
||||
def executar_diario(args: argparse.Namespace, logger) -> dict:
|
||||
"""Executa o planejamento diario oficial em Python."""
|
||||
io_utils.configurar_ambiente()
|
||||
if args.data:
|
||||
atualizar_data_planejamento(args.data)
|
||||
logger.info("Data de planejamento ajustada para %s", args.data)
|
||||
if args.aeronave:
|
||||
logger.info("Filtro --aeronave recebido: %s. O modelo diario usa a disponibilidade em dados/aeronaves_disponiveis.csv.", args.aeronave)
|
||||
|
||||
from src.planejador_missao.main import executar_planejamento
|
||||
|
||||
inicio = time.perf_counter()
|
||||
resultado = executar_planejamento(config.PROJECT_ROOT)
|
||||
duracao = time.perf_counter() - inicio
|
||||
logger.info("Data planejada: %s", resultado["data_planejamento"])
|
||||
logger.info("Colunas candidatas: %s", resultado["total_candidatas"])
|
||||
logger.info("Escalas selecionadas: %s", resultado["total_selecionadas"])
|
||||
logger.info("Arquivo exportado: %s", resultado["arquivo_saida"])
|
||||
logger.info("Tempo de execucao: %.1f s", duracao)
|
||||
return resultado
|
||||
|
||||
|
||||
def executar_validacao(ano: str, logger) -> dict:
|
||||
"""Executa validacao retrospectiva suportada pelo projeto."""
|
||||
if str(ano) != "2025":
|
||||
raise ValueError("Erro: no momento a validacao disponivel e --validacao 2025.")
|
||||
validacao = importlib.import_module("07_validacao_2025")
|
||||
return validacao.executar_validacao_2025(importar=True, logger=logger)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Executa o Planejador Missao em Python.")
|
||||
parser.add_argument("--data", help="Data do planejamento diario no formato AAAA-MM-DD.")
|
||||
parser.add_argument("--aeronave", choices=config.AERONAVES, help="Aeronave de referencia para diagnostico.")
|
||||
parser.add_argument("--modo", default="diario", choices=["diario"], help="Modo de execucao do planejamento.")
|
||||
parser.add_argument("--validacao", help="Ano da validacao retrospectiva. Exemplo: --validacao 2025.")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
logger = io_utils.configurar_logger("main")
|
||||
if args.validacao:
|
||||
saida = executar_validacao(args.validacao, logger)
|
||||
print("\n=== Validacao concluida ===")
|
||||
for path in saida["arquivos"]:
|
||||
print(f"- {path}")
|
||||
return
|
||||
|
||||
resultado = executar_diario(args, logger)
|
||||
print("\n=== Planejamento diario concluido ===")
|
||||
print(f"Data: {resultado['data_planejamento']}")
|
||||
print(f"Arquivo: {resultado['arquivo_saida']}")
|
||||
print(f"Colunas candidatas: {resultado['total_candidatas']}")
|
||||
print(f"Escalas selecionadas: {resultado['total_selecionadas']}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
72
scripts/01_config.py
Normal file
72
scripts/01_config.py
Normal file
@@ -0,0 +1,72 @@
|
||||
"""Configuracao central dos scripts do Planejador Missao.
|
||||
|
||||
Entradas:
|
||||
Estrutura padrao do projeto, com pastas dados/, src/ e resultados/.
|
||||
|
||||
Saidas:
|
||||
Constantes de caminho, parametros operacionais, nomes de arquivos e pesos
|
||||
usados pelos scripts de execucao, validacao e teste.
|
||||
|
||||
Papel no pipeline:
|
||||
Evita caminhos absolutos e concentra convencoes para que o projeto rode em
|
||||
qualquer computador mantendo a mesma estrutura de diretorios.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
SCRIPTS_DIR = PROJECT_ROOT / "scripts"
|
||||
SRC_DIR = PROJECT_ROOT / "src"
|
||||
DADOS_DIR = PROJECT_ROOT / "dados"
|
||||
RESULTADOS_DIR = PROJECT_ROOT / "resultados"
|
||||
VALIDACAO_DIR = RESULTADOS_DIR / "validacao"
|
||||
LOGS_DIR = PROJECT_ROOT / "logs"
|
||||
TEMP_DIR = PROJECT_ROOT / "temp"
|
||||
|
||||
ARQUIVO_PARAMETROS = DADOS_DIR / "parametros_missao.csv"
|
||||
ARQUIVO_CADASTRO_LOCAL = DADOS_DIR / "Modelagem_C98_ETA2_local.xlsx"
|
||||
ARQUIVO_CADASTRO_ORIGINAL = DADOS_DIR / "Modelagem C98 ETA2.xlsx"
|
||||
ARQUIVO_CATALOGO_OIS = DADOS_DIR / "catalogo_ois.xlsx"
|
||||
ARQUIVO_INDISPONIBILIDADES = DADOS_DIR / "indisponibilidades_2026.xlsx"
|
||||
ARQUIVO_HISTORICO = DADOS_DIR / "historico_horas_voadas.csv"
|
||||
ARQUIVO_QUADRO_VOO_2025 = DADOS_DIR / "Quadro de Voo 2025 (2).xlsx"
|
||||
ARQUIVO_VOOS_2025 = DADOS_DIR / "validacao" / "voos_2025.csv"
|
||||
|
||||
LOG_EXECUCAO = LOGS_DIR / "execucao.log"
|
||||
|
||||
AERONAVES = ["C98", "C97", "C95"]
|
||||
META_HORAS_PADRAO = 50.0
|
||||
VALIDACAO_TEMPO_LIMITE_SEGUNDOS = 180.0
|
||||
VALIDACAO_GAP_RELATIVO = 0.05
|
||||
|
||||
PESOS_OBJETIVO = {
|
||||
"rota_acionada": 100000,
|
||||
"missao_local": 1000,
|
||||
"sobreaviso": 0,
|
||||
"meta_50": 100,
|
||||
"meta_110": 100,
|
||||
"beneficio_paop": 10,
|
||||
"custo_financeiro": -0.05,
|
||||
}
|
||||
|
||||
ABAS = {
|
||||
"cadastro": "BANCO DE DADOS 2026",
|
||||
"catalogo_ois": "catalogo_ois",
|
||||
"voos_2025": "VOOS",
|
||||
"relatorio_diario": "ESCALA DIARIA",
|
||||
}
|
||||
|
||||
ARQUIVOS_ENTRADA_OBRIGATORIOS = [
|
||||
ARQUIVO_CATALOGO_OIS,
|
||||
ARQUIVO_INDISPONIBILIDADES,
|
||||
ARQUIVO_PARAMETROS,
|
||||
]
|
||||
|
||||
|
||||
def garantir_diretorios() -> None:
|
||||
"""Cria diretorios de saida e apoio usados pelos scripts."""
|
||||
for path in [RESULTADOS_DIR, VALIDACAO_DIR, LOGS_DIR, TEMP_DIR, ARQUIVO_VOOS_2025.parent]:
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
85
scripts/02_io_utils.py
Normal file
85
scripts/02_io_utils.py
Normal file
@@ -0,0 +1,85 @@
|
||||
"""Utilitarios de entrada, saida e log dos scripts.
|
||||
|
||||
Entradas:
|
||||
Caminhos definidos em 01_config.py.
|
||||
|
||||
Saidas:
|
||||
Validacoes de arquivos, diretorios criados, logs em logs/execucao.log e
|
||||
funcoes pequenas para exportacao.
|
||||
|
||||
Papel no pipeline:
|
||||
Padroniza mensagens de erro e evita repeticao de codigo de filesystem.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import logging
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Iterable
|
||||
|
||||
import pandas as pd
|
||||
|
||||
config = importlib.import_module("01_config")
|
||||
|
||||
|
||||
def configurar_ambiente() -> None:
|
||||
"""Garante diretorios e deixa src/ importavel para execucoes pelo VS Code."""
|
||||
config.garantir_diretorios()
|
||||
project_root = str(config.PROJECT_ROOT)
|
||||
if project_root not in sys.path:
|
||||
sys.path.insert(0, project_root)
|
||||
|
||||
|
||||
def configurar_logger(nome: str = "planejador_missao") -> logging.Logger:
|
||||
"""Cria logger simples em arquivo e terminal."""
|
||||
configurar_ambiente()
|
||||
logger = logging.getLogger(nome)
|
||||
logger.setLevel(logging.INFO)
|
||||
logger.handlers.clear()
|
||||
|
||||
formato = logging.Formatter("%(asctime)s | %(levelname)s | %(message)s")
|
||||
file_handler = logging.FileHandler(config.LOG_EXECUCAO, encoding="utf-8")
|
||||
file_handler.setFormatter(formato)
|
||||
stream_handler = logging.StreamHandler()
|
||||
stream_handler.setFormatter(logging.Formatter("%(message)s"))
|
||||
logger.addHandler(file_handler)
|
||||
logger.addHandler(stream_handler)
|
||||
logger.info("Execucao iniciada em %s", datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
|
||||
return logger
|
||||
|
||||
|
||||
def exigir_arquivos(paths: Iterable[Path]) -> None:
|
||||
"""Interrompe a execucao com mensagem clara se algum arquivo faltar."""
|
||||
ausentes = [path for path in paths if not path.exists()]
|
||||
if ausentes:
|
||||
lista = "\n".join(f"- {path}" for path in ausentes)
|
||||
raise FileNotFoundError(f"Erro: arquivos obrigatorios ausentes:\n{lista}")
|
||||
|
||||
|
||||
def exigir_colunas(df: pd.DataFrame, colunas: Iterable[str], origem: str) -> None:
|
||||
"""Valida colunas obrigatorias de uma tabela carregada."""
|
||||
faltantes = [col for col in colunas if col not in df.columns]
|
||||
if faltantes:
|
||||
raise ValueError(
|
||||
f"Erro: coluna obrigatoria '{faltantes[0]}' nao encontrada em {origem}. "
|
||||
"Verifique o arquivo de entrada."
|
||||
)
|
||||
|
||||
|
||||
def exportar_excel(path: Path, abas: dict[str, pd.DataFrame]) -> Path:
|
||||
"""Exporta um workbook Excel com uma aba por DataFrame."""
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with pd.ExcelWriter(path, engine="openpyxl") as writer:
|
||||
for nome, df in abas.items():
|
||||
df.to_excel(writer, sheet_name=nome[:31], index=False)
|
||||
return path
|
||||
|
||||
|
||||
def exportar_csv(path: Path, df: pd.DataFrame) -> Path:
|
||||
"""Exporta CSV garantindo existencia do diretorio."""
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
df.to_csv(path, index=False)
|
||||
return path
|
||||
73
scripts/03_preparar_dados.py
Normal file
73
scripts/03_preparar_dados.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""Preparacao das bases operacionais do Planejador Missao.
|
||||
|
||||
Entradas:
|
||||
Planilhas e CSVs em dados/: cadastro, catalogo de OIs, indisponibilidades,
|
||||
progresso, historico, aeronaves, tripulantes disponiveis e rotas acionadas.
|
||||
|
||||
Saidas:
|
||||
Dicionario com DataFrames normalizados para geracao de colunas candidatas.
|
||||
|
||||
Papel no pipeline:
|
||||
Concentra a leitura e a preparacao antes do MILP, mantendo rastreavel quais
|
||||
arquivos alimentam cada execucao.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
from pathlib import Path
|
||||
|
||||
config = importlib.import_module("01_config")
|
||||
io_utils = importlib.import_module("02_io_utils")
|
||||
|
||||
|
||||
def carregar_bases(base_dir: Path = config.PROJECT_ROOT) -> dict:
|
||||
"""Le e normaliza todas as bases necessarias ao planejamento diario."""
|
||||
io_utils.configurar_ambiente()
|
||||
io_utils.exigir_arquivos(config.ARQUIVOS_ENTRADA_OBRIGATORIOS)
|
||||
|
||||
from src.planejador_missao.data_io import (
|
||||
carregar_aeronaves,
|
||||
carregar_cadastro,
|
||||
carregar_catalogo_ois,
|
||||
carregar_historico,
|
||||
carregar_indisponibilidades,
|
||||
carregar_parametros,
|
||||
carregar_progresso_ois,
|
||||
carregar_rotas,
|
||||
carregar_tripulantes_disponiveis,
|
||||
)
|
||||
from src.planejador_missao.rules import combinar_progresso_com_historico
|
||||
|
||||
parametros = carregar_parametros(base_dir)
|
||||
historico = carregar_historico(base_dir)
|
||||
cadastro = carregar_cadastro(base_dir, historico)
|
||||
catalogo = carregar_catalogo_ois(base_dir)
|
||||
indisponibilidades = carregar_indisponibilidades(base_dir)
|
||||
progresso = combinar_progresso_com_historico(carregar_progresso_ois(base_dir), historico, catalogo)
|
||||
|
||||
return {
|
||||
"parametros": parametros,
|
||||
"historico": historico,
|
||||
"cadastro": cadastro,
|
||||
"catalogo": catalogo,
|
||||
"indisponibilidades": indisponibilidades,
|
||||
"progresso": progresso,
|
||||
"aeronaves": carregar_aeronaves(base_dir),
|
||||
"disponiveis": carregar_tripulantes_disponiveis(base_dir),
|
||||
"rotas": carregar_rotas(base_dir),
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Executa uma leitura de diagnostico das bases."""
|
||||
logger = io_utils.configurar_logger("preparar_dados")
|
||||
bases = carregar_bases()
|
||||
logger.info("Tripulantes no cadastro: %s", len(bases["cadastro"]))
|
||||
logger.info("Tripulantes disponiveis: %s", len(bases["disponiveis"]))
|
||||
logger.info("Aeronaves informadas: %s", len(bases["aeronaves"]))
|
||||
logger.info("Rotas acionadas: %s", len(bases["rotas"]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
56
scripts/04_gerar_colunas_candidatas.py
Normal file
56
scripts/04_gerar_colunas_candidatas.py
Normal file
@@ -0,0 +1,56 @@
|
||||
"""Geracao das colunas candidatas viaveis.
|
||||
|
||||
Entradas:
|
||||
Bases preparadas por 03_preparar_dados.py.
|
||||
|
||||
Saidas:
|
||||
DataFrame em que cada linha representa uma escala possivel: rota acionada,
|
||||
missao local ou sobreaviso, com dupla, aeronave, score, custo e metadados.
|
||||
|
||||
Papel no pipeline:
|
||||
Materializa o espaco de decisoes do MILP. A compatibilidade de aeronave,
|
||||
indisponibilidade, qualificacao e regra de instrucao ja sao filtradas aqui.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
|
||||
preparar = importlib.import_module("03_preparar_dados")
|
||||
io_utils = importlib.import_module("02_io_utils")
|
||||
|
||||
|
||||
def gerar_colunas_do_dia(bases: dict):
|
||||
"""Gera colunas candidatas para a data e parametros informados nas bases."""
|
||||
from src.planejador_missao.candidates import gerar_colunas
|
||||
|
||||
parametros = bases["parametros"]
|
||||
# Cada coluna candidata equivale a uma variavel x_j do MILP.
|
||||
# As regras de dupla, qualificacao, instrucao e indisponibilidade sao
|
||||
# aplicadas antes do solver para reduzir o modelo a alternativas viaveis.
|
||||
return gerar_colunas(
|
||||
parametros["data_planejamento"],
|
||||
bases["rotas"],
|
||||
bases["aeronaves"],
|
||||
bases["cadastro"],
|
||||
bases["indisponibilidades"],
|
||||
bases["progresso"],
|
||||
bases["catalogo"],
|
||||
bases["disponiveis"],
|
||||
parametros["criterio_missao"],
|
||||
parametros["criterio_sbv"],
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Executa diagnostico da geracao de colunas."""
|
||||
logger = io_utils.configurar_logger("gerar_colunas")
|
||||
bases = preparar.carregar_bases()
|
||||
colunas = gerar_colunas_do_dia(bases)
|
||||
logger.info("Colunas candidatas geradas: %s", len(colunas))
|
||||
if colunas.empty:
|
||||
logger.warning("Nenhuma coluna candidata viavel foi gerada.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
58
scripts/05_modelo_milp.py
Normal file
58
scripts/05_modelo_milp.py
Normal file
@@ -0,0 +1,58 @@
|
||||
"""Modelo MILP do Planejador Missao usando scipy.optimize.milp/HiGHS.
|
||||
|
||||
Entradas:
|
||||
Colunas candidatas viaveis geradas por 04_gerar_colunas_candidatas.py.
|
||||
|
||||
Saidas:
|
||||
Subconjunto de colunas selecionadas pelo solver, representando a escala
|
||||
diaria otimizada.
|
||||
|
||||
Metodologia:
|
||||
Variavel de decisao: x_j em {0,1}, onde cada j representa uma coluna
|
||||
candidata. Como scipy.optimize.milp minimiza, o modelo minimiza -score_milp,
|
||||
equivalente a maximizar o score operacional total.
|
||||
|
||||
Restricoes implementadas no motor src.planejador_missao.optimizer:
|
||||
a) rota acionada obrigatoria coberta exatamente uma vez;
|
||||
b) cada tripulante aparece no maximo uma vez no dia;
|
||||
c) cada aeronave recebe cobertura principal por rota acionada ou SBV;
|
||||
d) aeronave nao executa escalas conflitantes de rota/local no mesmo dia;
|
||||
e) qualificacao, indisponibilidade e regras de instrucao entram no filtro de
|
||||
colunas candidatas, logo colunas inviaveis nao chegam ao MILP;
|
||||
f) regras especificas de rota, missao local e SBV sao preservadas por tipo.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
|
||||
gerador = importlib.import_module("04_gerar_colunas_candidatas")
|
||||
io_utils = importlib.import_module("02_io_utils")
|
||||
preparar = importlib.import_module("03_preparar_dados")
|
||||
|
||||
|
||||
def resolver_escala(colunas, rotas, aeronaves):
|
||||
"""Resolve o MILP e retorna as colunas escolhidas."""
|
||||
from src.planejador_missao.optimizer import resolver_milp
|
||||
|
||||
if colunas.empty:
|
||||
raise RuntimeError("Erro: nao ha colunas candidatas viaveis para montar o MILP.")
|
||||
|
||||
# O resolvedor cria x_j binario para cada linha de colunas.
|
||||
# A funcao objetivo usa -score_milp porque o HiGHS via SciPy resolve
|
||||
# minimizacao; isso preserva a interpretacao de maximizar score.
|
||||
return resolver_milp(colunas, rotas, aeronaves)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Executa o MILP em modo diagnostico."""
|
||||
logger = io_utils.configurar_logger("modelo_milp")
|
||||
bases = preparar.carregar_bases()
|
||||
colunas = gerador.gerar_colunas_do_dia(bases)
|
||||
solucao = resolver_escala(colunas, bases["rotas"], bases["aeronaves"])
|
||||
logger.info("Colunas candidatas: %s", len(colunas))
|
||||
logger.info("Escalas selecionadas: %s", len(solucao))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
38
scripts/06_exportar_resultados.py
Normal file
38
scripts/06_exportar_resultados.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""Exportacao dos resultados do planejamento diario.
|
||||
|
||||
Entradas:
|
||||
Solucao do MILP, catalogo de OIs e data de planejamento.
|
||||
|
||||
Saidas:
|
||||
Planilha em resultados/planejamento_diario_YYYY-MM-DD.xlsx e registros de
|
||||
historico operacional para atualizacao do acumulado.
|
||||
|
||||
Papel no pipeline:
|
||||
Converte a solucao matematica em artefatos utilizaveis pelo esquadrao.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
|
||||
config = importlib.import_module("01_config")
|
||||
io_utils = importlib.import_module("02_io_utils")
|
||||
|
||||
|
||||
def exportar_planejamento(data_planejamento, solucao, catalogo):
|
||||
"""Gera registros operacionais e planilha Excel da escala."""
|
||||
from src.planejador_missao.report import gerar_excel, gerar_registros
|
||||
|
||||
registros = gerar_registros(solucao, catalogo)
|
||||
arquivo = gerar_excel(config.PROJECT_ROOT, data_planejamento, solucao)
|
||||
return {"arquivo": arquivo, "registros": registros}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Mostra instrucao de uso deste modulo dentro do pipeline."""
|
||||
logger = io_utils.configurar_logger("exportar_resultados")
|
||||
logger.info("Use python scripts/00_main.py para executar o fluxo completo.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
257
scripts/07_validacao_2025.py
Normal file
257
scripts/07_validacao_2025.py
Normal file
@@ -0,0 +1,257 @@
|
||||
"""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:
|
||||
Mantem a demanda historica de 2025, gera candidatos compativeis para cada
|
||||
slot historico 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 validacao."""
|
||||
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 validacao 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 validacao 2025 vazia. Verifique dados/validacao/voos_2025.csv.")
|
||||
if (voos["horas_voadas"] < 0).any():
|
||||
raise RuntimeError("Erro: foram encontradas horas voadas negativas na validacao 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 nao 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 validacao: %.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 validacao 2025."""
|
||||
logger = io_utils.configurar_logger("validacao_2025")
|
||||
saida = executar_validacao_2025(importar=True, logger=logger)
|
||||
metricas = saida["resultado"]["metricas"]
|
||||
print("\n=== Validacao 2025 concluida ===")
|
||||
print(metricas.to_string(index=False))
|
||||
print("\nArquivos gerados:")
|
||||
for path in saida["arquivos"]:
|
||||
print(f"- {path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
44
scripts/08_app_utils.py
Normal file
44
scripts/08_app_utils.py
Normal file
@@ -0,0 +1,44 @@
|
||||
"""Funcoes auxiliares da interface web local.
|
||||
|
||||
Entradas:
|
||||
Estrutura padrao do projeto e web_app.py.
|
||||
|
||||
Saidas:
|
||||
Inicializacao local da interface em http://127.0.0.1:8050 quando chamado
|
||||
diretamente.
|
||||
|
||||
Papel no pipeline:
|
||||
Facilita acesso ao app Python com interface web local.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
config = importlib.import_module("01_config")
|
||||
io_utils = importlib.import_module("02_io_utils")
|
||||
|
||||
|
||||
def comando_python_app() -> list[str]:
|
||||
"""Retorna comando portavel para iniciar a interface web local."""
|
||||
pythonw = config.PROJECT_ROOT / ".venv" / "Scripts" / "pythonw.exe"
|
||||
python = config.PROJECT_ROOT / ".venv" / "Scripts" / "python.exe"
|
||||
exe = pythonw if pythonw.exists() else python if python.exists() else sys.executable
|
||||
return [str(exe), str(config.PROJECT_ROOT / "web_app.py")]
|
||||
|
||||
|
||||
def abrir_app() -> None:
|
||||
"""Inicia o app em segundo plano sem janela de console quando possivel."""
|
||||
io_utils.configurar_ambiente()
|
||||
subprocess.Popen(comando_python_app(), cwd=config.PROJECT_ROOT)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
abrir_app()
|
||||
print("Interface web local iniciada em http://127.0.0.1:8050")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
63
scripts/09_testar_instalacao.py
Normal file
63
scripts/09_testar_instalacao.py
Normal file
@@ -0,0 +1,63 @@
|
||||
"""Teste rapido de instalacao do Planejador Missao.
|
||||
|
||||
Entradas:
|
||||
Ambiente Python atual, dependencias instaladas e arquivos em dados/.
|
||||
|
||||
Saidas:
|
||||
Diagnostico amigavel no terminal e log em logs/execucao.log.
|
||||
|
||||
Papel no pipeline:
|
||||
Permite verificar rapidamente se o computador esta pronto para executar o
|
||||
planejador e o solver HiGHS/scipy.optimize.milp.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
from scipy.optimize import Bounds, LinearConstraint, milp
|
||||
|
||||
config = importlib.import_module("01_config")
|
||||
io_utils = importlib.import_module("02_io_utils")
|
||||
|
||||
|
||||
def testar_solver_minimo() -> None:
|
||||
"""Resolve um MILP minimo para comprovar que scipy.optimize.milp funciona."""
|
||||
c = np.array([-1.0, -2.0])
|
||||
integrality = np.ones(2)
|
||||
bounds = Bounds([0, 0], [1, 1])
|
||||
constraints = [LinearConstraint([[1, 1]], -np.inf, 1)]
|
||||
result = milp(c=c, integrality=integrality, bounds=bounds, constraints=constraints)
|
||||
if not result.success:
|
||||
raise RuntimeError(f"Erro: teste minimo do solver falhou: {result.message}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Executa todas as verificacoes de ambiente."""
|
||||
logger = io_utils.configurar_logger("teste_instalacao")
|
||||
print(f"Python encontrado: {sys.version.split()[0]}")
|
||||
|
||||
for pacote in ["pandas", "numpy", "openpyxl", "scipy"]:
|
||||
importlib.import_module(pacote)
|
||||
print(f"Biblioteca OK: {pacote}")
|
||||
|
||||
config.garantir_diretorios()
|
||||
for diretorio in [config.DADOS_DIR, config.RESULTADOS_DIR, config.SRC_DIR, config.LOGS_DIR, config.TEMP_DIR]:
|
||||
if not diretorio.exists():
|
||||
raise RuntimeError(f"Erro: diretorio obrigatorio ausente: {diretorio}")
|
||||
print(f"Diretorio OK: {diretorio.name}")
|
||||
|
||||
cadastro = config.ARQUIVO_CADASTRO_LOCAL if config.ARQUIVO_CADASTRO_LOCAL.exists() else config.ARQUIVO_CADASTRO_ORIGINAL
|
||||
io_utils.exigir_arquivos([cadastro, *config.ARQUIVOS_ENTRADA_OBRIGATORIOS])
|
||||
print("Arquivos de entrada OK")
|
||||
|
||||
testar_solver_minimo()
|
||||
print("Solver scipy.optimize.milp/HiGHS OK")
|
||||
logger.info("Teste de instalacao concluido com sucesso.")
|
||||
print("Ambiente OK para executar o Planejador Missao.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
136
scripts/_arquivados/importar_quadro_voo_2025.py
Normal file
136
scripts/_arquivados/importar_quadro_voo_2025.py
Normal file
@@ -0,0 +1,136 @@
|
||||
"""Importa a aba VOOS do Quadro de Voo 2025 para a base de validacao."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
from datetime import time, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
import pandas as pd
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(BASE_DIR))
|
||||
|
||||
from src.planejador_missao.utils import normalizar_texto
|
||||
|
||||
|
||||
COLUNAS_VALIDACAO = ["slot_id", "data", "aeronave", "tipo_escala", "tripulante", "funcao", "oi", "horas_voadas", "sbv"]
|
||||
|
||||
|
||||
def _horas(valor: object) -> float:
|
||||
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:
|
||||
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]:
|
||||
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(origem: Path, destino: Path) -> pd.DataFrame:
|
||||
raw = pd.read_excel(origem, sheet_name="VOOS", 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
|
||||
|
||||
slot_id += 1
|
||||
tipo_escala = normalizar_texto(row[1])
|
||||
horas_voadas = _horas(row[30])
|
||||
oi = normalizar_texto(row[35])
|
||||
if not oi or oi == "-":
|
||||
oi = normalizar_texto(row[32])
|
||||
|
||||
for idx, tripulante in enumerate(_tripulantes(row[8]), 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)
|
||||
destino.parent.mkdir(parents=True, exist_ok=True)
|
||||
df.to_csv(destino, index=False)
|
||||
return df
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Importa voos realizados de 2025 para a validacao.")
|
||||
parser.add_argument(
|
||||
"--origem",
|
||||
type=Path,
|
||||
default=BASE_DIR / "dados" / "Quadro de Voo 2025 (2).xlsx",
|
||||
help="Planilha com aba VOOS.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--destino",
|
||||
type=Path,
|
||||
default=BASE_DIR / "dados" / "validacao" / "voos_2025.csv",
|
||||
help="CSV de validacao gerado.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
df = importar(args.origem, args.destino)
|
||||
print(f"Arquivo gerado: {args.destino}")
|
||||
print(f"Registros exportados: {len(df)}")
|
||||
print(f"Voos/slots exportados: {df['slot_id'].nunique() if not df.empty else 0}")
|
||||
print(f"Tripulantes exportados: {df['tripulante'].nunique() if not df.empty else 0}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
57
scripts/_arquivados/validar_2025.py
Normal file
57
scripts/_arquivados/validar_2025.py
Normal file
@@ -0,0 +1,57 @@
|
||||
"""Executa a validacao retrospectiva com voos de 2025."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(BASE_DIR))
|
||||
|
||||
from src.planejador_missao.validacao import (
|
||||
carregar_voos_2025,
|
||||
criar_modelo_voos_2025,
|
||||
otimizar_equalizacao_50,
|
||||
salvar_relatorio_validacao,
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Valida o planejador com voos historicos de 2025.")
|
||||
parser.add_argument("--meta", type=float, default=50.0, help="Meta de horas usada na equalizacao.")
|
||||
parser.add_argument("--tempo-limite", type=float, default=180.0, help="Tempo limite do resolvedor MILP, em segundos.")
|
||||
parser.add_argument("--gap", type=float, default=0.05, help="Gap relativo aceitavel para encerrar o MILP.")
|
||||
args = parser.parse_args()
|
||||
|
||||
entrada = BASE_DIR / "dados" / "validacao" / "voos_2025.csv"
|
||||
criar_modelo_voos_2025(entrada)
|
||||
|
||||
voos = carregar_voos_2025(entrada)
|
||||
if voos.empty:
|
||||
print("Base de validacao ainda vazia.")
|
||||
print(f"Preencha o arquivo: {entrada}")
|
||||
print("Colunas esperadas: data,aeronave,tipo_escala,tripulante,funcao,oi,horas_voadas,sbv")
|
||||
return
|
||||
|
||||
resultado = otimizar_equalizacao_50(
|
||||
BASE_DIR,
|
||||
voos,
|
||||
meta_horas=args.meta,
|
||||
tempo_limite_segundos=args.tempo_limite,
|
||||
gap_relativo=args.gap,
|
||||
)
|
||||
arquivo = salvar_relatorio_validacao(BASE_DIR, resultado, meta_horas=args.meta)
|
||||
|
||||
print("\n=== Validacao 2025 concluida ===")
|
||||
print(f"Meta de equalizacao: {args.meta:g} horas")
|
||||
print(f"Voos/slots avaliados: {len(resultado['slots'])}")
|
||||
print(f"Tripulantes comparados: {len(resultado['comparativo'])}")
|
||||
print(f"Relatorio: {arquivo}")
|
||||
print(f"Solver: {resultado['status_solver']['mensagem']}")
|
||||
print("\nMetricas:")
|
||||
print(resultado["metricas"].to_string(index=False))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user