""" ============================================================================ AIRCRAFT ROUTING - ESQUADRAO ARARA (Transporte C-95/C-105) ============================================================================ Modelo MIP compacto que ATRIBUI aeronaves a missoes de transporte, maximizando o numero (ponderado por prioridade) de missoes cumpridas, respeitando: - rota viavel (continuidade espacial e temporal por aeronave) - turnaround minimo entre missoes - limite de horas de celula ate a proxima inspecao (LRT) - base inicial e horas acumuladas reais de cada aeronave Diferenca chave vs. modelo civil (Bazargan): aqui a cobertura e' "<= 1" (missao OPCIONAL, selecionada se compensar) e nao "= 1". O objetivo e' prontidao/cumprimento de missao, nao custo. Solver: CBC (vem junto com o PuLP, zero instalacao). Para instancias maiores troque por HiGHS (ver nota no fim do arquivo). Autor: gerado como ponto de partida. pt-BR nos comentarios, codigo limpo. ============================================================================ """ import csv from datetime import datetime, timezone from pathlib import Path import pulp # =========================================================================== # 1) DADOS DE ENTRADA (substitua pelos dados reais do ARARA) # =========================================================================== # Bases (codigos curtos ficticios). 'S' marca bases HABILITADAS a inspecao. # Na FAB o conjunto de bases habilitadas e' restrito -> isso aperta o routing. # MN representa Manaus, base operacional do Esquadrao Arara nesta simulacao. BASES_INSPECAO = {"MN"} # Manaus TURNAROUND_MIN_H = 1.0 # tempo minimo em solo entre 2 missoes (h) LIMITE_HORAS_F = 100.0 # horas de celula ate inspecao obrigatoria # (AJUSTE ao intervalo real da sua frota) # ----- Aeronaves (tail number, base inicial, horas acumuladas desde a ult. insp.) # 'horas_acum' = f_k. LRT inicial de cada aeronave = LIMITE_HORAS_F - f_k. AERONAVES = [ # id base horas_acum ("FAB-2301", "MN", 12.0), ("FAB-2302", "MN", 71.0), # ja gastou muito -> pouco LRT disponivel ("FAB-2303", "MN", 30.0), ("FAB-2304", "MN", 95.0), # quase no limite -> mal consegue voar ] # ----- Missoes de transporte # Entrada recomendada: data/hora UTC, como viria de uma planilha. # Para usar uma planilha, exporte para CSV com as colunas: # id,orig,dest,partida_utc,chegada_utc,prioridade # e preencha CSV_MISSOES_PATH com o caminho do arquivo. CSV_MISSOES_PATH = None MISSOES_UTC = [ # id orig dest partida UTC chegada UTC prio ("M01", "MN", "BR", "2026-07-01T06:00Z", "2026-07-01T10:00Z", 5), ("M02", "BR", "MN", "2026-07-01T12:00Z", "2026-07-01T16:00Z", 5), ("M03", "MN", "BE", "2026-07-01T06:30Z", "2026-07-01T09:00Z", 4), ("M04", "BE", "MN", "2026-07-01T11:00Z", "2026-07-01T13:30Z", 4), ("M05", "MN", "PV", "2026-07-01T07:00Z", "2026-07-01T09:00Z", 3), ("M06", "PV", "MN", "2026-07-01T10:30Z", "2026-07-01T12:30Z", 3), ("M07", "MN", "BV", "2026-07-01T08:00Z", "2026-07-01T09:30Z", 3), ("M08", "BV", "MN", "2026-07-01T11:00Z", "2026-07-01T12:30Z", 3), ("M09", "MN", "GL", "2026-07-01T18:00Z", "2026-07-01T23:00Z", 4), ("M10", "GL", "MN", "2026-07-02T01:00Z", "2026-07-02T06:00Z", 4), ("M11", "MN", "CG", "2026-07-01T20:00Z", "2026-07-01T23:00Z", 2), ("M12", "CG", "MN", "2026-07-02T01:00Z", "2026-07-02T04:00Z", 2), ("M13", "MN", "RF", "2026-07-02T06:00Z", "2026-07-02T11:00Z", 4), ("M14", "RF", "MN", "2026-07-02T13:00Z", "2026-07-02T18:00Z", 4), ("M15", "MN", "NT", "2026-07-02T08:00Z", "2026-07-02T12:30Z", 3), ("M16", "NT", "MN", "2026-07-02T14:00Z", "2026-07-02T18:30Z", 3), ("M17", "MN", "BR", "2026-07-03T00:00Z", "2026-07-03T04:00Z", 5), ("M18", "BR", "MN", "2026-07-03T06:00Z", "2026-07-03T10:00Z", 5), ("M19", "MN", "BE", "2026-07-03T02:00Z", "2026-07-03T04:30Z", 4), ("M20", "BE", "MN", "2026-07-03T06:00Z", "2026-07-03T08:30Z", 4), ] def parse_utc(valor): """Converte texto ISO UTC da planilha para datetime com timezone.""" texto = str(valor).strip() if texto.endswith("Z"): texto = texto[:-1] + "+00:00" dt = datetime.fromisoformat(texto) if dt.tzinfo is None: dt = dt.replace(tzinfo=timezone.utc) return dt.astimezone(timezone.utc) def carregar_missoes_csv(caminho): with Path(caminho).open(newline="", encoding="utf-8-sig") as arq: leitor = csv.DictReader(arq) return [ ( linha["id"], linha["orig"], linha["dest"], linha["partida_utc"], linha["chegada_utc"], int(linha["prioridade"]), ) for linha in leitor ] def converter_missoes_para_horas(missoes_utc): datas_partida = [parse_utc(m[3]) for m in missoes_utc] horizonte_inicio = min(datas_partida) missoes_horas = [] for mid, orig, dest, partida_utc, chegada_utc, prio in missoes_utc: partida = parse_utc(partida_utc) chegada = parse_utc(chegada_utc) if chegada <= partida: raise ValueError(f"Missao {mid}: chegada_utc deve ser depois de partida_utc") part_h = (partida - horizonte_inicio).total_seconds() / 3600 cheg_h = (chegada - horizonte_inicio).total_seconds() / 3600 missoes_horas.append((mid, orig, dest, part_h, cheg_h, prio)) return missoes_horas, horizonte_inicio MISSOES_FONTE = carregar_missoes_csv(CSV_MISSOES_PATH) if CSV_MISSOES_PATH else MISSOES_UTC MISSOES, HORIZONTE_INICIO_UTC = converter_missoes_para_horas(MISSOES_FONTE) # =========================================================================== # 2) PRE-PROCESSAMENTO (gera conexoes viaveis) # =========================================================================== miss = {m[0]: dict(orig=m[1], dest=m[2], part=m[3], cheg=m[4], prio=m[5], dur=m[4] - m[3]) for m in MISSOES} acft = {a[0]: dict(base=a[1], f=a[2]) for a in AERONAVES} IDS = list(miss.keys()) KS = list(acft.keys()) # Conexao i->j valida: destino de i == origem de j e ha turnaround suficiente conex = [(i, j) for i in IDS for j in IDS if i != j and miss[i]["dest"] == miss[j]["orig"] and miss[j]["part"] >= miss[i]["cheg"] + TURNAROUND_MIN_H] # Aeronave k pode INICIAR pela missao i se i parte da base inicial de k def pode_iniciar(k, i): return miss[i]["orig"] == acft[k]["base"] # =========================================================================== # 3) MODELO MIP # =========================================================================== mdl = pulp.LpProblem("ARARA_Aircraft_Routing", pulp.LpMaximize) # --- Variaveis x = {(k, i): pulp.LpVariable(f"x_{k}_{i}", cat="Binary") for k in KS for i in IDS} s = {(k, i): pulp.LpVariable(f"s_{k}_{i}", cat="Binary") # k inicia por i for k in KS for i in IDS if pode_iniciar(k, i)} c = {(k, i, j): pulp.LpVariable(f"c_{k}_{i}_{j}", cat="Binary") # k voa i depois j for k in KS for (i, j) in conex} z = {i: pulp.LpVariable(f"z_{i}", cat="Binary") for i in IDS} # missao coberta # --- Objetivo primario: maximizar prioridade total das missoes cumpridas mdl += pulp.lpSum(miss[i]["prio"] * z[i] for i in IDS), "Missoes_cumpridas_ponderadas" # --- (R1) Cobertura: cada missao por NO MAXIMO uma aeronave for i in IDS: mdl += z[i] == pulp.lpSum(x[(k, i)] for k in KS), f"def_z_{i}" # z[i] ja e' binaria, entao automaticamente <= 1 # --- (R2) Fluxo de entrada: se k voa i, ele iniciou por i OU chegou de outra missao for k in KS: for i in IDS: entradas = pulp.lpSum(c[(k, jj, i)] for (jj, ii) in conex if ii == i) ini = s[(k, i)] if (k, i) in s else 0 mdl += ini + entradas == x[(k, i)], f"in_{k}_{i}" # --- (R3) Fluxo de saida: depois de i, k segue para no max uma proxima missao for k in KS: for i in IDS: saidas = pulp.lpSum(c[(k, i, jj)] for (ii, jj) in conex if ii == i) mdl += saidas <= x[(k, i)], f"out_{k}_{i}" # --- (R4) Cada aeronave inicia no maximo uma rota for k in KS: inis = [s[(k, i)] for i in IDS if (k, i) in s] if inis: mdl += pulp.lpSum(inis) <= 1, f"start_{k}" # --- (R5) Limite de horas de celula (LRT): total voado por k <= F - f_k # Esta e' a restricao CENTRAL de manutencao (versao "orcamento de horas"). # -> A inspecao com RESET de horas em base habilitada e' a extensao da Fase 3. for k in KS: mdl += pulp.lpSum(miss[i]["dur"] * x[(k, i)] for i in IDS) <= \ LIMITE_HORAS_F - acft[k]["f"], f"horas_{k}" # =========================================================================== # 4) RESOLVER # =========================================================================== solver = pulp.PULP_CBC_CMD(msg=False) # troque por HiGHS se instalado # Etapa 1: maximiza o cumprimento ponderado das missoes. mdl.solve(solver) prio_otima = int(pulp.value(mdl.objective)) # Etapa 2: mantendo a prioridade otima, minimiza o LRT restante ponderado. # Como as horas totais voadas podem ser constantes quando todas as missoes sao # cumpridas, o peso prioriza consumir LRT das aeronaves mais proximas da # manutencao, sem sacrificar nenhuma missao prioritaria da primeira etapa. lrt_restante = { k: (LIMITE_HORAS_F - acft[k]["f"]) - pulp.lpSum(miss[i]["dur"] * x[(k, i)] for i in IDS) for k in KS } pesos_lrt = {k: 1 / (LIMITE_HORAS_F - acft[k]["f"]) for k in KS} mdl += pulp.lpSum(miss[i]["prio"] * z[i] for i in IDS) == prio_otima, "fixa_prioridade_otima" mdl.sense = pulp.LpMinimize mdl.setObjective(pulp.lpSum(pesos_lrt[k] * lrt_restante[k] for k in KS)) mdl.solve(solver) # =========================================================================== # 5) RELATORIO # =========================================================================== print("=" * 70) print(f" STATUS: {pulp.LpStatus[mdl.status]}") print(f" Horizonte inicia em UTC: {HORIZONTE_INICIO_UTC.isoformat().replace('+00:00', 'Z')}") prio_tot = sum(miss[i]["prio"] for i in IDS) prio_obt = prio_otima cob = [i for i in IDS if z[i].value() and z[i].value() > 0.5] print(f" Missoes cumpridas: {len(cob)}/{len(IDS)} " f"| Prioridade obtida: {prio_obt}/{prio_tot}") lrt_total = sum((LIMITE_HORAS_F - acft[k]["f"]) - sum(miss[i]["dur"] * x[(k, i)].value() for i in IDS) for k in KS) print(f" LRT restante total: {lrt_total:g}h") print(" Criterio secundario: menor LRT restante ponderado p/ manutencao") print("=" * 70) def rota_da_aeronave(k): """Reconstroi a sequencia de missoes da aeronave k seguindo as conexoes.""" inicio = [i for i in IDS if (k, i) in s and s[(k, i)].value() and s[(k, i)].value() > 0.5] if not inicio: return [] seq = [inicio[0]] while True: atual = seq[-1] prox = [j for (ii, jj) in conex if ii == atual for j in [jj] if c[(k, atual, j)].value() and c[(k, atual, j)].value() > 0.5] if not prox: break seq.append(prox[0]) return seq for k in KS: seq = rota_da_aeronave(k) horas = sum(miss[i]["dur"] for i in seq) lrt_ini = LIMITE_HORAS_F - acft[k]["f"] if seq: trechos = " -> ".join( f"{i}({miss[i]['orig']}-{miss[i]['dest']} {miss[i]['part']:g}h)" for i in seq) print(f"\n {k} [base {acft[k]['base']}, f={acft[k]['f']:g}h, LRT={lrt_ini:g}h]") print(f" {trechos}") print(f" horas voadas: {horas:g}h | LRT restante: {lrt_ini - horas:g}h") else: print(f"\n {k} [base {acft[k]['base']}, LRT={lrt_ini:g}h] -> sem missoes") descobertas = [i for i in IDS if i not in cob] if descobertas: print("\n " + "-" * 60) print(" Missoes NAO cumpridas:", ", ".join(f"{i}(prio {miss[i]['prio']})" for i in descobertas)) # --------------------------------------------------------------------------- # NOTA - usar HiGHS (recomendado p/ instancias maiores, sem licenca): # pip install highspy # solver = pulp.HiGHS(msg=False) # PuLP 3.x expoe HiGHS nativo # ---------------------------------------------------------------------------