Reescrita do zero em software/oamrp_v3.py: abstracao No/Arco, variavel unificada y[k,arco], C1-C6 (Fase 1) e C7-C11 com relogio de horas, downtime e disjuntivas de slot unico (Fase 2). TAT corrigido para 1,5h; top-10 bases; EVAM obrigatoria; L2 = maximizar horas voadas. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1196 lines
41 KiB
Python
1196 lines
41 KiB
Python
"""
|
||
============================================================================
|
||
AIRCRAFT ROUTING - ESQUADRAO ARARA - V3
|
||
============================================================================
|
||
Modelo OAMRP compacto (Al-Thani et al., 2016) em rede espaço-tempo.
|
||
NAO usa Set Partitioning / Column Generation.
|
||
|
||
Arquitetura:
|
||
A rede é composta por Nos (origem, missao, inspecao, sumidouro) e Arcos
|
||
tipados. A variavel de decisao y[k, arco] unifica o que era x/s/c no v2.
|
||
Para adicionar inspeções na Fase 2, basta estender construir_rede() —
|
||
o modelo de otimização e a conservação de fluxo não mudam.
|
||
|
||
Escopo desta versão — Fase 2 (C1–C9, C11):
|
||
C1 Cobertura: cada missão é servida por no máximo 1 aeronave (EVAM = hard)
|
||
C2 Conservação de fluxo por nó e por aeronave
|
||
C3 Continuidade espacial (dest(m) == orig(m') no arco)
|
||
C4 TAT = 1,5 h entre chegada e próxima decolagem
|
||
C5 Janela de decolagem derivada do CSV
|
||
C6 Orçamento de horas por relógio de inspeção com reset (H_{k,i,n} ≤ LRT_i)
|
||
C9 Downtime: próxima missão só começa após s_{k,i} + DT_i + TAT
|
||
C11 Slot único: no máximo 1 aeronave em inspeção em qualquer instante
|
||
|
||
Fases futuras:
|
||
Fase 3 — arcos de ferry, C7 (limite calendárico), objetivo L2 real
|
||
Fase 4 — replanejamento e visualização
|
||
|
||
Inspeções válidas (Q1 confirmado):
|
||
seq {2, 3, 4, 6, 7, 19, 20, 22}
|
||
Removidas: seq 18 (INSP 100FH) e seq 23 (INSP 50FH)
|
||
|
||
Horas iniciais f0_{k,i}:
|
||
--horas-iniciais <arquivo.json ou string JSON>
|
||
--aleatorio [--seed N]
|
||
padrão: 0,0 para todas (aeronave recém-inspecionada — conservador)
|
||
============================================================================
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import csv
|
||
import json
|
||
import random
|
||
from dataclasses import dataclass, field
|
||
from datetime import datetime, timezone
|
||
from pathlib import Path
|
||
from typing import Literal, Optional
|
||
|
||
import pulp
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Caminhos padrão
|
||
# ---------------------------------------------------------------------------
|
||
|
||
BASE_DIR = Path(__file__).resolve().parents[1]
|
||
|
||
CSV_REGISTRO_PADRAO = (
|
||
BASE_DIR / "db" / "processed" / "registro_voo_2025_consolidado.csv"
|
||
)
|
||
CSV_RESULTADO_PADRAO = (
|
||
BASE_DIR / "db" / "processed" / "resultado_oamrp_v3.csv"
|
||
)
|
||
JSON_INSPECOES_PADRAO = (
|
||
BASE_DIR
|
||
/ "db"
|
||
/ "pre_process"
|
||
/ "relatorio_ciclo_inspecoes_c105_2805_2026-06-15"
|
||
/ "relatorio_ciclo_inspecoes_c105_2805_2026-06-15_inspecoes.json"
|
||
)
|
||
|
||
DATA_INICIO_PADRAO = "2025-01-01"
|
||
DATA_FIM_PADRAO = "2025-02-01"
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Constantes operacionais
|
||
# ---------------------------------------------------------------------------
|
||
|
||
BASE_MANUTENCAO = "SBMN"
|
||
TAT_H = 1.5
|
||
|
||
AERONAVES_PADRAO = ["2800", "2803", "2809", "2811"]
|
||
BASE_INICIAL_PADRAO = BASE_MANUTENCAO
|
||
|
||
SEQS_VALIDOS = {2, 3, 4, 6, 7, 19, 20, 22} # Q1 confirmado
|
||
NIVEIS_PERMITIDOS = {"Base", "Orgânico"}
|
||
|
||
N_BASES_PADRAO = 10 # Q4: top-N bases por frequência de uso
|
||
HARD_MISSAO_PADRAO = "EVAM" # Q2: padrão para identificar missões obrigatórias
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Domínio
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class Missao:
|
||
id: str
|
||
orig: str
|
||
dest: str
|
||
t_dep: float # horas desde o início do horizonte
|
||
t_arr: float
|
||
dur_h: float
|
||
prioridade: int
|
||
obrigatoria: bool # True = M_obr (EVAM/hard); cobertura == 1
|
||
partida_utc: datetime
|
||
chegada_utc: datetime
|
||
periodo: str
|
||
linhas_origem: str
|
||
aeronave_real: str
|
||
om: str
|
||
codigo_missao: str # campo 'missao' do CSV (ex: "69TV01", "EVAM001")
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class InspecaoParam:
|
||
seq: int
|
||
sigla: str
|
||
descricao: str
|
||
nivel: str
|
||
lrt_h: float
|
||
cal_dias: Optional[float]
|
||
dt_h: float
|
||
|
||
|
||
@dataclass
|
||
class Aeronave:
|
||
id: str
|
||
base_inicial: str
|
||
horas_iniciais: dict[int, float] = field(default_factory=dict)
|
||
|
||
def orcamento_h(self, catalogo: dict[int, InspecaoParam]) -> float:
|
||
"""
|
||
Orçamento de horas disponíveis (C6 — Fase 1 simplificado).
|
||
Retorna min(LRT_i − f0_{k,i}) sobre todas as inspeções ativas.
|
||
"""
|
||
if not catalogo:
|
||
return 0.0
|
||
margens = [
|
||
insp.lrt_h - self.horas_iniciais.get(seq, 0.0)
|
||
for seq, insp in catalogo.items()
|
||
]
|
||
return max(0.0, min(margens))
|
||
|
||
def insp_mais_restritiva(
|
||
self, catalogo: dict[int, InspecaoParam]
|
||
) -> Optional[tuple[int, float, InspecaoParam]]:
|
||
if not catalogo:
|
||
return None
|
||
seq_min = min(
|
||
catalogo,
|
||
key=lambda s: catalogo[s].lrt_h - self.horas_iniciais.get(s, 0.0),
|
||
)
|
||
return seq_min, self.horas_iniciais.get(seq_min, 0.0), catalogo[seq_min]
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Rede espaço-tempo
|
||
# ---------------------------------------------------------------------------
|
||
|
||
TipoNo = Literal["origem", "missao", "inspecao", "sumidouro"]
|
||
TipoArco = Literal[
|
||
"inicio", # origem_k → missao
|
||
"missao_missao", # missao → missao
|
||
"missao_insp", # missao → inspecao (Fase 2)
|
||
"insp_missao", # inspecao → missao (Fase 2)
|
||
"ferry", # qualquer → qualquer com reposicionamento (Fase 3)
|
||
"fim", # missao → sumidouro_k
|
||
]
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class No:
|
||
"""
|
||
Nó genérico da rede espaço-tempo.
|
||
|
||
base_in — base onde a aeronave deve estar ao *entrar* neste nó.
|
||
base_out — base onde a aeronave estará ao *sair* deste nó.
|
||
t_ini — instante de início (decolagem para missão; início downtime para inspeção).
|
||
t_fim — instante de fim (chegada para missão; fim downtime para inspeção).
|
||
carga_h — horas de célula consumidas ao atravessar este nó.
|
||
|
||
Nós de inspeção (Fase 2) terão aeronave_id preenchido — são nós por aeronave.
|
||
Nós de missão são compartilhados entre aeronaves.
|
||
"""
|
||
|
||
id: str
|
||
tipo: TipoNo
|
||
base_in: str
|
||
base_out: str
|
||
t_ini: float
|
||
t_fim: float
|
||
carga_h: float = 0.0
|
||
missao: Optional[Missao] = None
|
||
insp_seq: Optional[int] = None
|
||
aeronave_id: Optional[str] = None
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class Arco:
|
||
"""
|
||
Arco da rede espaço-tempo.
|
||
|
||
aeronave_id — se preenchido, o arco pertence exclusivamente a esta aeronave
|
||
(arcos de início e fim). None indica arco compartilhado
|
||
(missao_missao, ferry), utilizável por qualquer aeronave.
|
||
|
||
Restrições de viabilidade (continuidade espacial e temporal) são
|
||
verificadas em construir_rede() — não são restrições do modelo PuLP.
|
||
"""
|
||
|
||
id: str
|
||
orig_id: str
|
||
dest_id: str
|
||
tipo: TipoArco
|
||
aeronave_id: Optional[str] = None # None = arco compartilhado
|
||
horas_ferry: float = 0.0 # horas de célula de ferry (Fase 3)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Construção da rede (Fase 1 — sem nós de inspeção)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def construir_rede(
|
||
missoes: list[Missao],
|
||
aeronaves: list[Aeronave],
|
||
catalogo: dict[int, InspecaoParam],
|
||
tat_h: float = TAT_H,
|
||
) -> tuple[dict[str, No], list[Arco]]:
|
||
"""
|
||
Constrói os nós e arcos da rede espaço-tempo (Fase 2).
|
||
|
||
Tipos de nós:
|
||
- origem_k : ponto de partida de cada aeronave k
|
||
- missao_m : cada missão candidata
|
||
- inspecao_k_i : nó de inspeção por (aeronave k, tipo i) — base SBMN
|
||
- sumidouro_k : ponto de chegada de cada aeronave k
|
||
|
||
Tipos de arcos:
|
||
- inicio : origem_k → missao_m (m.orig == base_k)
|
||
- missao_missao : missao_m → missao_m' (dest==orig e gap ≥ TAT)
|
||
- missao_insp : missao_m → inspecao_k_i (m.dest == SBMN)
|
||
- insp_missao : inspecao_k_i → missao_m' (m'.orig == SBMN, t_dep ≥ DT_i+TAT)
|
||
- fim : missao_m / inspecao_k_i → sumidouro_k
|
||
|
||
Fase 3 adicionará arcos de ferry entre bases distintas.
|
||
"""
|
||
nos: dict[str, No] = {}
|
||
arcos: list[Arco] = []
|
||
|
||
# --- Nós de missão ---
|
||
for m in missoes:
|
||
no_id = f"M_{m.id}"
|
||
nos[no_id] = No(
|
||
id=no_id,
|
||
tipo="missao",
|
||
base_in=m.orig,
|
||
base_out=m.dest,
|
||
t_ini=m.t_dep,
|
||
t_fim=m.t_arr,
|
||
carga_h=m.dur_h,
|
||
missao=m,
|
||
)
|
||
|
||
# --- Nós de origem e sumidouro por aeronave ---
|
||
for a in aeronaves:
|
||
src_id = f"SRC_{a.id}"
|
||
snk_id = f"SNK_{a.id}"
|
||
nos[src_id] = No(
|
||
id=src_id,
|
||
tipo="origem",
|
||
base_in=a.base_inicial,
|
||
base_out=a.base_inicial,
|
||
t_ini=0.0,
|
||
t_fim=0.0,
|
||
aeronave_id=a.id,
|
||
)
|
||
nos[snk_id] = No(
|
||
id=snk_id,
|
||
tipo="sumidouro",
|
||
base_in="", # sumidouro aceita qualquer base
|
||
base_out="",
|
||
t_ini=float("inf"),
|
||
t_fim=float("inf"),
|
||
aeronave_id=a.id,
|
||
)
|
||
|
||
# --- Arcos início: origem_k → missao_m ---
|
||
for a in aeronaves:
|
||
src_id = f"SRC_{a.id}"
|
||
for m in missoes:
|
||
if m.orig != a.base_inicial:
|
||
continue
|
||
arc_id = f"INI_{a.id}_{m.id}"
|
||
arcos.append(
|
||
Arco(
|
||
id=arc_id,
|
||
orig_id=src_id,
|
||
dest_id=f"M_{m.id}",
|
||
tipo="inicio",
|
||
aeronave_id=a.id,
|
||
)
|
||
)
|
||
|
||
# --- Arcos missao → missao ---
|
||
for mi in missoes:
|
||
for mj in missoes:
|
||
if mi.id == mj.id:
|
||
continue
|
||
if mi.dest != mj.orig:
|
||
continue
|
||
if mj.t_dep < mi.t_arr + tat_h:
|
||
continue
|
||
arc_id = f"MM_{mi.id}_{mj.id}"
|
||
arcos.append(
|
||
Arco(
|
||
id=arc_id,
|
||
orig_id=f"M_{mi.id}",
|
||
dest_id=f"M_{mj.id}",
|
||
tipo="missao_missao",
|
||
)
|
||
)
|
||
|
||
# --- Arcos fim: missao_m → sumidouro_k ---
|
||
for a in aeronaves:
|
||
snk_id = f"SNK_{a.id}"
|
||
for m in missoes:
|
||
arc_id = f"FIM_{m.id}_{a.id}"
|
||
arcos.append(
|
||
Arco(
|
||
id=arc_id,
|
||
orig_id=f"M_{m.id}",
|
||
dest_id=snk_id,
|
||
tipo="fim",
|
||
aeronave_id=a.id,
|
||
)
|
||
)
|
||
|
||
# --- Fase 2: Nós e arcos de inspeção ---
|
||
for a in aeronaves:
|
||
for seq, insp in catalogo.items():
|
||
insp_nid = f"INSP_{a.id}_{seq}"
|
||
nos[insp_nid] = No(
|
||
id=insp_nid,
|
||
tipo="inspecao",
|
||
base_in=BASE_MANUTENCAO,
|
||
base_out=BASE_MANUTENCAO,
|
||
t_ini=0.0, # início flexível: determinado por s[k,i] no modelo
|
||
t_fim=insp.dt_h, # duração do downtime (s[k,i] + dt_h = fim)
|
||
carga_h=0.0, # inspeção não acumula horas de célula
|
||
insp_seq=seq,
|
||
aeronave_id=a.id,
|
||
)
|
||
|
||
# SRC_k → INSP_k_i (aeronave começa o horizonte em SBMN e vai direto)
|
||
if a.base_inicial == BASE_MANUTENCAO:
|
||
arcos.append(Arco(
|
||
id=f"SRC_INSP_{a.id}_{seq}",
|
||
orig_id=f"SRC_{a.id}",
|
||
dest_id=insp_nid,
|
||
tipo="inicio",
|
||
aeronave_id=a.id,
|
||
))
|
||
|
||
# M_m → INSP_k_i: missões que chegam em SBMN
|
||
for m in missoes:
|
||
if m.dest != BASE_MANUTENCAO:
|
||
continue
|
||
arcos.append(Arco(
|
||
id=f"MINSP_{a.id}_{seq}_{m.id}",
|
||
orig_id=f"M_{m.id}",
|
||
dest_id=insp_nid,
|
||
tipo="missao_insp",
|
||
aeronave_id=a.id,
|
||
))
|
||
|
||
# INSP_k_i → M_m': missões que partem de SBMN após o downtime mínimo
|
||
for m in missoes:
|
||
if m.orig != BASE_MANUTENCAO:
|
||
continue
|
||
if m.t_dep < insp.dt_h + tat_h: # muito cedo mesmo no melhor caso
|
||
continue
|
||
arcos.append(Arco(
|
||
id=f"INSPM_{a.id}_{seq}_{m.id}",
|
||
orig_id=insp_nid,
|
||
dest_id=f"M_{m.id}",
|
||
tipo="insp_missao",
|
||
aeronave_id=a.id,
|
||
))
|
||
|
||
# INSP_k_i → SNK_k (aeronave encerra o horizonte após inspeção)
|
||
arcos.append(Arco(
|
||
id=f"INSP_SNK_{a.id}_{seq}",
|
||
orig_id=insp_nid,
|
||
dest_id=f"SNK_{a.id}",
|
||
tipo="fim",
|
||
aeronave_id=a.id,
|
||
))
|
||
|
||
return nos, arcos
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Modelo de otimização
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def resolver(
|
||
nos: dict[str, No],
|
||
arcos: list[Arco],
|
||
aeronaves: list[Aeronave],
|
||
catalogo: dict[int, InspecaoParam],
|
||
) -> dict:
|
||
ks = [a.id for a in aeronaves]
|
||
acft = {a.id: a for a in aeronaves}
|
||
missoes_nos = {nid: n for nid, n in nos.items() if n.tipo == "missao"}
|
||
|
||
# Índices de adjacência para conservação de fluxo
|
||
arcos_saindo: dict[str, list[Arco]] = {nid: [] for nid in nos}
|
||
arcos_entrando: dict[str, list[Arco]] = {nid: [] for nid in nos}
|
||
for a in arcos:
|
||
arcos_saindo[a.orig_id].append(a)
|
||
arcos_entrando[a.dest_id].append(a)
|
||
|
||
# --- Variáveis ---
|
||
# y[k, arco_id] = 1 se aeronave k usa este arco.
|
||
# Arcos exclusivos de outra aeronave (INI/FIM) não geram variável para k.
|
||
def arco_valido_para(k: str, a: Arco) -> bool:
|
||
return a.aeronave_id is None or a.aeronave_id == k
|
||
|
||
y: dict[tuple[str, str], pulp.LpVariable] = {
|
||
(k, a.id): pulp.LpVariable(f"y_{k}_{a.id}", cat="Binary")
|
||
for k in ks
|
||
for a in arcos
|
||
if arco_valido_para(k, a)
|
||
}
|
||
|
||
# z[missao_no_id] = 1 se a missão é coberta (por qualquer aeronave)
|
||
z: dict[str, pulp.LpVariable] = {
|
||
nid: pulp.LpVariable(f"z_{nid}", cat="Binary")
|
||
for nid in missoes_nos
|
||
}
|
||
|
||
mdl = pulp.LpProblem("ARARA_OAMRP_v3", pulp.LpMaximize)
|
||
|
||
# L1: max Σ_m p_m * z_m
|
||
mdl += (
|
||
pulp.lpSum(n.missao.prioridade * z[nid] for nid, n in missoes_nos.items()),
|
||
"obj_L1",
|
||
)
|
||
|
||
def y_sum(k: str, arcs: list[Arco]) -> pulp.LpAffineExpression:
|
||
"""Soma y[(k, a.id)] apenas para arcos válidos para a aeronave k."""
|
||
return pulp.lpSum(
|
||
y[(k, a.id)] for a in arcs if (k, a.id) in y
|
||
)
|
||
|
||
# --- C1: Cobertura e definição de z ---
|
||
# Missões normais: Σ_k y_entrada ≤ 1 (cobertura opcional)
|
||
# Missões obrigatórias (M_obr / EVAM): Σ_k y_entrada == 1
|
||
for nid, n in missoes_nos.items():
|
||
fluxo_entrada = pulp.lpSum(
|
||
y[(k, a.id)]
|
||
for k in ks
|
||
for a in arcos_entrando[nid]
|
||
if (k, a.id) in y
|
||
)
|
||
mdl += fluxo_entrada == z[nid], f"def_z_{nid}"
|
||
if n.missao.obrigatoria:
|
||
mdl += z[nid] == 1, f"hard_{nid}"
|
||
else:
|
||
mdl += z[nid] <= 1, f"cob_{nid}"
|
||
|
||
# --- C2: Conservação de fluxo por (aeronave, nó) ---
|
||
for k in ks:
|
||
src_id = f"SRC_{k}"
|
||
snk_id = f"SNK_{k}"
|
||
for nid, n in nos.items():
|
||
if n.tipo in ("origem", "sumidouro") and n.aeronave_id != k:
|
||
continue # nó de outra aeronave — irrelevante
|
||
|
||
entrada_k = y_sum(k, arcos_entrando[nid])
|
||
saida_k = y_sum(k, arcos_saindo[nid])
|
||
|
||
if nid == src_id:
|
||
# Origem: no máximo 1 arco de saída (aeronave pode não voar)
|
||
mdl += saida_k <= 1, f"src_{k}"
|
||
elif nid == snk_id:
|
||
# Sumidouro: entrada = saída da origem (fluxo fechado)
|
||
src_saida = y_sum(k, arcos_saindo[src_id])
|
||
mdl += entrada_k == src_saida, f"snk_{k}"
|
||
elif n.tipo == "missao":
|
||
# Missão: fluxo de entrada = fluxo de saída para esta aeronave
|
||
mdl += entrada_k == saida_k, f"fluxo_{k}_{nid}"
|
||
|
||
# --- Fase 2: variáveis e restrições C6-reset, C9, C11 ---
|
||
|
||
insp_nos = {nid: n for nid, n in nos.items() if n.tipo == "inspecao"}
|
||
|
||
# Big-M: usa LRT máximo (para horas) e horizonte temporal (para tempo)
|
||
M_h = max((insp.lrt_h for insp in catalogo.values()), default=1.0) + 100.0
|
||
M_t = (
|
||
max((n.t_fim for n in missoes_nos.values()), default=0.0) + 200.0
|
||
)
|
||
|
||
# s[k,i]: instante de início da inspeção i pela aeronave k
|
||
s_insp: dict[tuple[str, int], pulp.LpVariable] = {
|
||
(k, seq): pulp.LpVariable(f"s_{k}_{seq}", lowBound=0, upBound=M_t)
|
||
for k in ks
|
||
for seq in catalogo
|
||
}
|
||
|
||
# H[k, i, nid]: horas acumuladas no relógio i da aeronave k ao sair do nó nid.
|
||
# Definido para todos os nós de missão e de inspeção (exceto o nó de reset
|
||
# de clock i, onde H = 0 por definição).
|
||
def _h_valido(k: str, seq: int, nid: str) -> bool:
|
||
n = nos[nid]
|
||
if n.tipo in ("origem", "sumidouro"):
|
||
return False
|
||
# Nó de reset do próprio clock i: H = 0, não precisa de variável
|
||
if n.tipo == "inspecao" and n.insp_seq == seq and n.aeronave_id == k:
|
||
return False
|
||
# Nó de inspeção de outra aeronave: irrelevante
|
||
if n.tipo == "inspecao" and n.aeronave_id != k:
|
||
return False
|
||
return True
|
||
|
||
H: dict[tuple[str, int, str], pulp.LpVariable] = {
|
||
(k, seq, nid): pulp.LpVariable(
|
||
f"H_{k}_{seq}_{nid}",
|
||
lowBound=0,
|
||
upBound=catalogo[seq].lrt_h,
|
||
)
|
||
for k in ks
|
||
for seq in catalogo
|
||
for nid in nos
|
||
if _h_valido(k, seq, nid)
|
||
}
|
||
|
||
def _H_prev(k: str, seq: int, u_nid: str) -> object:
|
||
"""Retorna o valor/variável de H_{k,seq} ao sair do nó u."""
|
||
n = nos[u_nid]
|
||
if n.tipo == "origem":
|
||
return acft[k].horas_iniciais.get(seq, 0.0)
|
||
# Nó de reset para este clock: saída é 0
|
||
if n.tipo == "inspecao" and n.insp_seq == seq and n.aeronave_id == k:
|
||
return 0.0
|
||
return H.get((k, seq, u_nid))
|
||
|
||
# Propagação de H ao longo dos arcos (big-M)
|
||
for k in ks:
|
||
for seq in catalogo:
|
||
reset_nid = f"INSP_{k}_{seq}"
|
||
for a in arcos:
|
||
if (k, a.id) not in y:
|
||
continue
|
||
v_nid = a.dest_id
|
||
if v_nid == reset_nid:
|
||
continue # reset: H = 0, sem propagação
|
||
H_v = H.get((k, seq, v_nid))
|
||
if H_v is None:
|
||
continue
|
||
h_prev = _H_prev(k, seq, a.orig_id)
|
||
if h_prev is None:
|
||
continue
|
||
carga = nos[v_nid].carga_h # dur_h para missões, 0 para inspeções
|
||
y_var = y[(k, a.id)]
|
||
mdl += H_v >= h_prev + carga - M_h * (1 - y_var), f"Hlo_{k}_{seq}_{a.id}"
|
||
mdl += H_v <= h_prev + carga + M_h * (1 - y_var), f"Hhi_{k}_{seq}_{a.id}"
|
||
|
||
# --- C6 (reset): H[k,i,M_m] ≤ LRT_i quando aeronave k visita M_m ---
|
||
# (já aplicado como upBound na variável; confirma aqui explicitamente)
|
||
# Nota: se a missão não for visitada, H fica ≤ LRT por bound.
|
||
|
||
# Horizonte temporal em horas (última chegada registrada)
|
||
horizonte_h = max((n.t_fim for n in missoes_nos.values()), default=744.0)
|
||
|
||
# Inspeção só pode começar dentro do horizonte de planejamento
|
||
for k in ks:
|
||
for seq in catalogo:
|
||
insp_nid = f"INSP_{k}_{seq}"
|
||
if insp_nid not in nos:
|
||
continue
|
||
w_ki = pulp.lpSum(
|
||
y[(k, a.id)] for a in arcos_entrando[insp_nid] if (k, a.id) in y
|
||
)
|
||
mdl += (
|
||
s_insp[(k, seq)] <= horizonte_h * w_ki,
|
||
f"s_horizonte_{k}_{seq}",
|
||
)
|
||
|
||
# --- C9: downtime — próxima missão só parte após s[k,i] + DT_i + TAT ---
|
||
for k in ks:
|
||
for seq, insp in catalogo.items():
|
||
insp_nid = f"INSP_{k}_{seq}"
|
||
# s[k,i] ≥ t_arr(m_antes) para cada arco M_m → INSP
|
||
for a in arcos_entrando[insp_nid]:
|
||
if (k, a.id) not in y:
|
||
continue
|
||
u = nos[a.orig_id]
|
||
if u.tipo == "missao":
|
||
mdl += (
|
||
s_insp[(k, seq)]
|
||
>= u.t_fim - M_t * (1 - y[(k, a.id)]),
|
||
f"c9a_{k}_{seq}_{a.id}",
|
||
)
|
||
# t_dep(m') ≥ s[k,i] + DT_i + TAT para cada arco INSP → M_m'
|
||
for a in arcos_saindo[insp_nid]:
|
||
if (k, a.id) not in y:
|
||
continue
|
||
v = nos[a.dest_id]
|
||
if v.tipo == "missao":
|
||
mdl += (
|
||
v.t_ini + M_t * (1 - y[(k, a.id)])
|
||
>= s_insp[(k, seq)] + insp.dt_h + TAT_H,
|
||
f"c9b_{k}_{seq}_{a.id}",
|
||
)
|
||
|
||
# --- C11: slot único — no máximo 1 aeronave em inspeção em qualquer instante ---
|
||
# Para cada par distinto de inspeções de aeronaves diferentes,
|
||
# variável oo = 1 se (k1,i1) termina antes de (k2,i2) começar.
|
||
insp_events = [
|
||
(k, seq)
|
||
for k in ks
|
||
for seq in catalogo
|
||
if f"INSP_{k}_{seq}" in nos
|
||
]
|
||
for idx1, (k1, seq1) in enumerate(insp_events):
|
||
for k2, seq2 in insp_events[idx1 + 1:]:
|
||
if k1 == k2:
|
||
continue # mesma aeronave: C10 já garantido pelo fluxo
|
||
insp1, insp2 = catalogo[seq1], catalogo[seq2]
|
||
nid1, nid2 = f"INSP_{k1}_{seq1}", f"INSP_{k2}_{seq2}"
|
||
w1 = pulp.lpSum(y[(k1, a.id)] for a in arcos_entrando[nid1] if (k1, a.id) in y)
|
||
w2 = pulp.lpSum(y[(k2, a.id)] for a in arcos_entrando[nid2] if (k2, a.id) in y)
|
||
oo = pulp.LpVariable(f"oo_{k1}_{seq1}_{k2}_{seq2}", cat="Binary")
|
||
# (k1,i1) termina antes de (k2,i2): s2 ≥ s1 + DT1
|
||
mdl += (
|
||
s_insp[(k2, seq2)]
|
||
>= s_insp[(k1, seq1)] + insp1.dt_h
|
||
- M_t * (1 - oo) - M_t * (1 - w1) - M_t * (1 - w2),
|
||
f"c11a_{k1}_{seq1}_{k2}_{seq2}",
|
||
)
|
||
# (k2,i2) termina antes de (k1,i1): s1 ≥ s2 + DT2
|
||
mdl += (
|
||
s_insp[(k1, seq1)]
|
||
>= s_insp[(k2, seq2)] + insp2.dt_h
|
||
- M_t * oo - M_t * (1 - w1) - M_t * (1 - w2),
|
||
f"c11b_{k1}_{seq1}_{k2}_{seq2}",
|
||
)
|
||
|
||
# --- C6 (Fase 1 — mantido como redundância explícita para verificação) ---
|
||
for k in ks:
|
||
orcamento = acft[k].orcamento_h(catalogo)
|
||
mdl += (
|
||
pulp.lpSum(
|
||
n.carga_h * y[(k, a.id)]
|
||
for nid, n in missoes_nos.items()
|
||
for a in arcos_entrando[nid]
|
||
if (k, a.id) in y
|
||
)
|
||
<= orcamento,
|
||
f"orcamento_{k}",
|
||
)
|
||
|
||
solver = pulp.PULP_CBC_CMD(msg=False)
|
||
mdl.solve(solver)
|
||
prio_otima = int(round(pulp.value(mdl.objective) or 0))
|
||
|
||
# L2: com prioridade fixada, minimiza folga de horas ao fim do horizonte.
|
||
# folga_k = orcamento_k − horas_voadas_k
|
||
# Minimizar Σ_k folga_k ⟺ maximizar Σ_k horas_voadas_k (orcamento é constante).
|
||
mdl += (
|
||
pulp.lpSum(n.missao.prioridade * z[nid] for nid, n in missoes_nos.items())
|
||
== prio_otima,
|
||
"fixa_L1",
|
||
)
|
||
horas_voadas = pulp.lpSum(
|
||
n.carga_h * y[(k, a.id)]
|
||
for nid, n in missoes_nos.items()
|
||
for k in ks
|
||
for a in arcos_entrando[nid]
|
||
if (k, a.id) in y
|
||
)
|
||
mdl.sense = pulp.LpMaximize
|
||
mdl.setObjective(horas_voadas)
|
||
mdl.solve(solver)
|
||
|
||
return {
|
||
"modelo": mdl,
|
||
"nos": nos,
|
||
"arcos": arcos,
|
||
"arcos_entrando": arcos_entrando,
|
||
"arcos_saindo": arcos_saindo,
|
||
"missoes_nos": missoes_nos,
|
||
"insp_nos": insp_nos,
|
||
"acft": acft,
|
||
"catalogo": catalogo,
|
||
"ks": ks,
|
||
"y": y,
|
||
"z": z,
|
||
"s_insp": s_insp,
|
||
"H": H,
|
||
"prio_otima": prio_otima,
|
||
}
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Extração de rota
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def rota_da_aeronave(sol: dict, k: str) -> list[No]:
|
||
"""
|
||
Reconstrói a sequência de nós (missão e inspeção) percorrida pela aeronave k.
|
||
"""
|
||
nos = sol["nos"]
|
||
arcos_saindo = sol["arcos_saindo"]
|
||
y = sol["y"]
|
||
|
||
src_id = f"SRC_{k}"
|
||
seq: list[No] = []
|
||
atual_id = src_id
|
||
|
||
while True:
|
||
prox = [
|
||
a
|
||
for a in arcos_saindo[atual_id]
|
||
if y.get((k, a.id)) is not None
|
||
and y[(k, a.id)].value() is not None
|
||
and y[(k, a.id)].value() > 0.5
|
||
]
|
||
if not prox:
|
||
break
|
||
arco = prox[0]
|
||
no_dest = nos[arco.dest_id]
|
||
if no_dest.tipo == "sumidouro":
|
||
break
|
||
seq.append(no_dest)
|
||
atual_id = arco.dest_id
|
||
|
||
return seq
|
||
|
||
|
||
def inspecoes_da_aeronave(sol: dict, k: str) -> list[tuple[int, float, InspecaoParam]]:
|
||
"""Retorna lista de (seq, s_inicio, InspecaoParam) para inspeções executadas por k."""
|
||
s_insp = sol["s_insp"]
|
||
catalogo = sol["catalogo"]
|
||
nos = sol["nos"]
|
||
arcos_entrando = sol["arcos_entrando"]
|
||
y = sol["y"]
|
||
resultado = []
|
||
for seq, insp in catalogo.items():
|
||
insp_nid = f"INSP_{k}_{seq}"
|
||
if insp_nid not in nos:
|
||
continue
|
||
if any(
|
||
y.get((k, a.id)) is not None and y[(k, a.id)].value() is not None
|
||
and y[(k, a.id)].value() > 0.5
|
||
for a in arcos_entrando[insp_nid]
|
||
):
|
||
s_val = s_insp[(k, seq)].value() or 0.0
|
||
resultado.append((seq, s_val, insp))
|
||
return sorted(resultado, key=lambda t: t[1])
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Saída
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def escrever_resultado(sol: dict, caminho: Path) -> None:
|
||
caminho.parent.mkdir(parents=True, exist_ok=True)
|
||
z = sol["z"]
|
||
missoes_nos = sol["missoes_nos"]
|
||
linhas = []
|
||
|
||
cobertas_ids: set[str] = set()
|
||
for k in sol["ks"]:
|
||
missoes_rota = [n for n in rota_da_aeronave(sol, k) if n.tipo == "missao"]
|
||
for ordem, no in enumerate(missoes_rota, start=1):
|
||
m = no.missao
|
||
cobertas_ids.add(no.id)
|
||
linhas.append({
|
||
"status": "cumprida",
|
||
"aeronave": k,
|
||
"ordem": ordem,
|
||
"id": m.id,
|
||
"om": m.om,
|
||
"orig": m.orig,
|
||
"dest": m.dest,
|
||
"partida_utc": m.partida_utc.isoformat().replace("+00:00", "Z"),
|
||
"chegada_utc": m.chegada_utc.isoformat().replace("+00:00", "Z"),
|
||
"dur_h": f"{m.dur_h:.2f}",
|
||
"prioridade": m.prioridade,
|
||
"periodo": m.periodo,
|
||
"aeronave_real_2025": m.aeronave_real,
|
||
"linhas_origem": m.linhas_origem,
|
||
})
|
||
|
||
for nid, n in missoes_nos.items():
|
||
if nid in cobertas_ids:
|
||
continue
|
||
m = n.missao
|
||
linhas.append({
|
||
"status": "nao_cumprida",
|
||
"aeronave": "",
|
||
"ordem": "",
|
||
"id": m.id,
|
||
"om": m.om,
|
||
"orig": m.orig,
|
||
"dest": m.dest,
|
||
"partida_utc": m.partida_utc.isoformat().replace("+00:00", "Z"),
|
||
"chegada_utc": m.chegada_utc.isoformat().replace("+00:00", "Z"),
|
||
"dur_h": f"{m.dur_h:.2f}",
|
||
"prioridade": m.prioridade,
|
||
"periodo": m.periodo,
|
||
"aeronave_real_2025": m.aeronave_real,
|
||
"linhas_origem": m.linhas_origem,
|
||
})
|
||
|
||
colunas = [
|
||
"status", "aeronave", "ordem", "id", "om",
|
||
"orig", "dest", "partida_utc", "chegada_utc",
|
||
"dur_h", "prioridade", "periodo", "aeronave_real_2025", "linhas_origem",
|
||
]
|
||
with caminho.open("w", newline="", encoding="utf-8-sig") as f:
|
||
w = csv.DictWriter(f, fieldnames=colunas)
|
||
w.writeheader()
|
||
w.writerows(linhas)
|
||
|
||
|
||
def imprimir_relatorio(
|
||
sol: dict, bases: list[str], resultado_csv: Path
|
||
) -> None:
|
||
z = sol["z"]
|
||
missoes_nos = sol["missoes_nos"]
|
||
catalogo: dict[int, InspecaoParam] = sol["catalogo"]
|
||
acft: dict[str, Aeronave] = sol["acft"]
|
||
|
||
cobertas = [nid for nid, n in missoes_nos.items() if z[nid].value() and z[nid].value() > 0.5]
|
||
prio_total = sum(n.missao.prioridade for n in missoes_nos.values())
|
||
prio_obtida = sum(missoes_nos[nid].missao.prioridade for nid in cobertas)
|
||
total = len(missoes_nos)
|
||
|
||
print("=" * 72)
|
||
print(" OAMRP V3 — Fase 1 (C1–C6) | Esquadrao Arara C-105")
|
||
print("=" * 72)
|
||
print(f" Status solver : {pulp.LpStatus[sol['modelo'].status]}")
|
||
print(f" Missoes : {len(cobertas)}/{total} cumpridas")
|
||
print(f" Prioridade : {prio_obtida}/{prio_total}")
|
||
print(f" Resultado CSV : {resultado_csv}")
|
||
|
||
print(f"\n Bases observadas ({len(bases)}): {', '.join(bases)}")
|
||
|
||
print("\n Catalogo de inspecoes (Fase 1 — orcamento por aeronave):")
|
||
for seq in sorted(catalogo):
|
||
insp = catalogo[seq]
|
||
cal = f"{insp.cal_dias:.0f}d" if insp.cal_dias else "—"
|
||
print(
|
||
f" seq {seq:2d} {insp.sigla:<18} LRT={insp.lrt_h:>6.0f}h "
|
||
f"CAL={cal:>5} DT={insp.dt_h:.1f}h [{insp.nivel}]"
|
||
)
|
||
|
||
print("\n Orcamento por aeronave:")
|
||
for k in sol["ks"]:
|
||
a = acft[k]
|
||
oc = a.orcamento_h(catalogo)
|
||
info = a.insp_mais_restritiva(catalogo)
|
||
if info:
|
||
seq_min, f0, insp = info
|
||
print(
|
||
f" {k}: {oc:.1f}h "
|
||
f"(limitado por seq {seq_min} {insp.sigla}, "
|
||
f"f0={f0:.1f}h, LRT={insp.lrt_h:.0f}h)"
|
||
)
|
||
else:
|
||
print(f" {k}: {oc:.1f}h (sem restricao de inspecao)")
|
||
|
||
print()
|
||
for k in sol["ks"]:
|
||
rota = rota_da_aeronave(sol, k)
|
||
insps = inspecoes_da_aeronave(sol, k)
|
||
missoes_rota = [n for n in rota if n.tipo == "missao"]
|
||
horas = sum(n.carga_h for n in missoes_rota)
|
||
orcamento = acft[k].orcamento_h(catalogo)
|
||
folga = orcamento - horas
|
||
print(
|
||
f" Aeronave {k}: {len(missoes_rota)} missoes | {horas:.1f}h voadas | "
|
||
f"folga {folga:.1f}h | {len(insps)} inspecao(oes)"
|
||
)
|
||
# Linha do tempo unificada: missões e inspeções ordenadas por t_ini
|
||
eventos: list[tuple[float, str]] = []
|
||
for no in rota:
|
||
if no.tipo == "missao":
|
||
m = no.missao
|
||
eventos.append((
|
||
no.t_ini,
|
||
f" {m.partida_utc:%d/%m %H:%M}Z "
|
||
f"{m.orig}->{m.dest} OM {m.om} prio {m.prioridade} ({m.id})"
|
||
))
|
||
elif no.tipo == "inspecao":
|
||
seq = no.insp_seq
|
||
s_val = sol["s_insp"][(k, seq)].value() or 0.0
|
||
insp = catalogo[seq]
|
||
eventos.append((
|
||
s_val,
|
||
f" [INSP seq {seq} {insp.sigla}] "
|
||
f"s={s_val:.1f}h DT={insp.dt_h:.1f}h fim={s_val + insp.dt_h:.1f}h"
|
||
))
|
||
for _, linha in sorted(eventos)[:15]:
|
||
print(linha)
|
||
if len(eventos) > 15:
|
||
print(f" ... mais {len(eventos) - 15} eventos")
|
||
print()
|
||
|
||
nao = [nid for nid in missoes_nos if nid not in cobertas]
|
||
if nao:
|
||
print(f" Missoes nao cumpridas ({len(nao)}, primeiras 20):")
|
||
for nid in nao[:20]:
|
||
m = missoes_nos[nid].missao
|
||
print(
|
||
f" {m.partida_utc:%d/%m %H:%M}Z "
|
||
f"{m.orig}->{m.dest} OM {m.om} ({m.id})"
|
||
)
|
||
if len(nao) > 20:
|
||
print(f" ... mais {len(nao) - 20}")
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Carregamento de dados
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def _parse_utc(valor: str) -> datetime:
|
||
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(
|
||
caminho: Path,
|
||
inicio: datetime,
|
||
fim: datetime,
|
||
bases_validas: Optional[set[str]] = None,
|
||
hard_pattern: str = HARD_MISSAO_PADRAO,
|
||
prioridade_padrao: int = 1,
|
||
) -> tuple[list[Missao], datetime]:
|
||
"""
|
||
Lê o CSV consolidado e retorna missões na janela [inicio, fim).
|
||
|
||
bases_validas — se fornecido, descarta missões fora deste conjunto de bases (Q4).
|
||
hard_pattern — substring no campo 'missao' que marca M_obr (Q2, ex: 'EVAM').
|
||
"""
|
||
import re
|
||
hard_re = re.compile(hard_pattern, re.IGNORECASE)
|
||
|
||
brutos: list[tuple[datetime, datetime, dict]] = []
|
||
with caminho.open(newline="", encoding="utf-8-sig") as f:
|
||
for linha in csv.DictReader(f):
|
||
partida = _parse_utc(linha["partida_utc"])
|
||
chegada = _parse_utc(linha["chegada_utc"])
|
||
if not (inicio <= partida < fim):
|
||
continue
|
||
if chegada <= partida:
|
||
raise ValueError(
|
||
f"Missao {linha['id_registro']}: chegada antes da partida"
|
||
)
|
||
orig = linha["localidade_dep"]
|
||
dest = linha["localidade_arr"]
|
||
if bases_validas and (orig not in bases_validas or dest not in bases_validas):
|
||
continue
|
||
brutos.append((partida, chegada, linha))
|
||
|
||
if not brutos:
|
||
raise ValueError("Nenhuma missao encontrada na janela/bases informadas.")
|
||
|
||
brutos.sort(key=lambda t: (t[0], t[2]["id_registro"]))
|
||
horizonte = brutos[0][0]
|
||
|
||
missoes = []
|
||
for partida, chegada, linha in brutos:
|
||
t_dep = (partida - horizonte).total_seconds() / 3600
|
||
t_arr = (chegada - horizonte).total_seconds() / 3600
|
||
dur_h = t_arr - t_dep
|
||
prio = int(linha.get("prioridade", prioridade_padrao) or prioridade_padrao)
|
||
cod_missao = linha.get("missao", "")
|
||
obrigatoria = bool(hard_re.search(cod_missao))
|
||
missoes.append(
|
||
Missao(
|
||
id=linha["id_registro"],
|
||
orig=orig,
|
||
dest=dest,
|
||
t_dep=t_dep,
|
||
t_arr=t_arr,
|
||
dur_h=dur_h,
|
||
prioridade=prio,
|
||
obrigatoria=obrigatoria,
|
||
partida_utc=partida,
|
||
chegada_utc=chegada,
|
||
periodo=linha.get("periodo", ""),
|
||
linhas_origem=linha.get("linhas_origem", ""),
|
||
aeronave_real=linha.get("aeronave", ""),
|
||
om=linha.get("om", ""),
|
||
codigo_missao=cod_missao,
|
||
)
|
||
)
|
||
return missoes, horizonte
|
||
|
||
|
||
def carregar_inspecoes(caminho: Path) -> dict[int, InspecaoParam]:
|
||
with caminho.open(encoding="utf-8") as f:
|
||
dados = json.load(f)
|
||
|
||
catalogo: dict[int, InspecaoParam] = {}
|
||
for item in dados["inspecoes"]:
|
||
seq = item["seq"]
|
||
if seq not in SEQS_VALIDOS:
|
||
continue
|
||
nivel_desc = item.get("nivel_descricao", "")
|
||
if nivel_desc not in NIVEIS_PERMITIDOS:
|
||
continue
|
||
lrt = item.get("intervalo_horas_voo_valor")
|
||
if not lrt:
|
||
continue # sem intervalo em horas — só calendárica; entra na Fase 2 via CAL
|
||
|
||
cal_meses = item.get("intervalo_meses_continuos_valor")
|
||
cal_dias = float(cal_meses) * 30.0 if cal_meses else None
|
||
|
||
dur_valor = item.get("duracao_valor", 0)
|
||
dur_unidade = item.get("duracao_unidade", "horas")
|
||
dt_h = float(dur_valor) * 24.0 if dur_unidade == "dias" else float(dur_valor)
|
||
|
||
catalogo[seq] = InspecaoParam(
|
||
seq=seq,
|
||
sigla=item["sigla_mnt"],
|
||
descricao=item["descricao_mnt"],
|
||
nivel=nivel_desc,
|
||
lrt_h=float(lrt),
|
||
cal_dias=cal_dias,
|
||
dt_h=dt_h,
|
||
)
|
||
return catalogo
|
||
|
||
|
||
def derivar_bases(
|
||
caminho: Path,
|
||
inicio: datetime,
|
||
fim: datetime,
|
||
top_n: Optional[int] = None,
|
||
) -> list[str]:
|
||
"""
|
||
Deriva bases ICAO observadas na janela.
|
||
top_n — se fornecido, retorna apenas as N bases mais frequentes (Q4).
|
||
A base de manutenção SBMN é sempre incluída.
|
||
"""
|
||
from collections import Counter
|
||
contagem: Counter = Counter()
|
||
with caminho.open(newline="", encoding="utf-8-sig") as f:
|
||
for linha in csv.DictReader(f):
|
||
try:
|
||
partida = _parse_utc(linha["partida_utc"])
|
||
except Exception:
|
||
continue
|
||
if not (inicio <= partida < fim):
|
||
continue
|
||
contagem[linha["localidade_dep"]] += 1
|
||
contagem[linha["localidade_arr"]] += 1
|
||
|
||
if top_n is None:
|
||
return sorted(contagem)
|
||
|
||
# Garante que SBMN entra independente do ranking
|
||
top = {b for b, _ in contagem.most_common(top_n)}
|
||
top.add(BASE_MANUTENCAO)
|
||
if len(top) > top_n:
|
||
# SBMN foi adicionada extra; remove a menos frequente que não seja SBMN
|
||
extras = sorted(top - {BASE_MANUTENCAO}, key=lambda b: contagem[b])
|
||
top.discard(extras[0])
|
||
return sorted(top)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Horas iniciais
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def horas_iniciais_aleatorias(
|
||
aeronaves: list[str],
|
||
catalogo: dict[int, InspecaoParam],
|
||
seed: Optional[int] = None,
|
||
) -> dict[str, dict[int, float]]:
|
||
rng = random.Random(seed)
|
||
return {
|
||
k: {
|
||
seq: round(rng.uniform(0.1 * insp.lrt_h, 0.9 * insp.lrt_h), 1)
|
||
for seq, insp in catalogo.items()
|
||
}
|
||
for k in aeronaves
|
||
}
|
||
|
||
|
||
def carregar_horas_iniciais(
|
||
fonte: Optional[str],
|
||
aleatorio: bool,
|
||
aeronaves: list[str],
|
||
catalogo: dict[int, InspecaoParam],
|
||
seed: Optional[int] = None,
|
||
) -> dict[str, dict[int, float]]:
|
||
if fonte:
|
||
p = Path(fonte)
|
||
raw = json.loads(p.read_text(encoding="utf-8")) if p.exists() else json.loads(fonte)
|
||
return {k: {int(s): float(v) for s, v in d.items()} for k, d in raw.items()}
|
||
if aleatorio:
|
||
return horas_iniciais_aleatorias(aeronaves, catalogo, seed=seed)
|
||
return {k: {seq: 0.0 for seq in catalogo} for k in aeronaves}
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# CLI
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def main() -> None:
|
||
parser = argparse.ArgumentParser(
|
||
description="OAMRP v3 — Fase 1: rede espaço-tempo + cobertura + orçamento."
|
||
)
|
||
parser.add_argument("--csv", type=Path, default=CSV_REGISTRO_PADRAO)
|
||
parser.add_argument("--inspecoes", type=Path, default=JSON_INSPECOES_PADRAO)
|
||
parser.add_argument("--inicio", default=DATA_INICIO_PADRAO)
|
||
parser.add_argument("--fim", default=DATA_FIM_PADRAO)
|
||
parser.add_argument("--resultado", type=Path, default=CSV_RESULTADO_PADRAO)
|
||
parser.add_argument(
|
||
"--horas-iniciais", metavar="JSON",
|
||
help='f0_{k,i}: arquivo ou string JSON. Ex: {"2800": {"23": 35.0}}',
|
||
)
|
||
parser.add_argument(
|
||
"--aleatorio", action="store_true",
|
||
help="Gera f0_{k,i} aleatório (10%–90% do LRT).",
|
||
)
|
||
parser.add_argument("--seed", type=int, default=None)
|
||
parser.add_argument(
|
||
"--aeronaves", nargs="+", default=AERONAVES_PADRAO, metavar="CAUDA"
|
||
)
|
||
parser.add_argument("--base-inicial", default=BASE_INICIAL_PADRAO)
|
||
parser.add_argument(
|
||
"--n-bases", type=int, default=N_BASES_PADRAO,
|
||
help="Top-N bases por frequência de uso (Q4). 0 = todas.",
|
||
)
|
||
parser.add_argument(
|
||
"--hard-pattern", default=HARD_MISSAO_PADRAO,
|
||
help="Regex no campo 'missao' do CSV que marca missões obrigatórias (Q2).",
|
||
)
|
||
args = parser.parse_args()
|
||
|
||
inicio = datetime.fromisoformat(args.inicio).replace(tzinfo=timezone.utc)
|
||
fim = datetime.fromisoformat(args.fim).replace(tzinfo=timezone.utc)
|
||
|
||
catalogo = carregar_inspecoes(args.inspecoes)
|
||
top_n = args.n_bases if args.n_bases > 0 else None
|
||
bases = derivar_bases(args.csv, inicio, fim, top_n=top_n)
|
||
bases_validas = set(bases) if top_n else None
|
||
missoes, _ = carregar_missoes(
|
||
args.csv, inicio, fim,
|
||
bases_validas=bases_validas,
|
||
hard_pattern=args.hard_pattern,
|
||
)
|
||
|
||
f0 = carregar_horas_iniciais(
|
||
fonte=args.horas_iniciais,
|
||
aleatorio=args.aleatorio,
|
||
aeronaves=args.aeronaves,
|
||
catalogo=catalogo,
|
||
seed=args.seed,
|
||
)
|
||
aeronaves = [
|
||
Aeronave(id=k, base_inicial=args.base_inicial, horas_iniciais=f0.get(k, {}))
|
||
for k in args.aeronaves
|
||
]
|
||
|
||
nos, arcos = construir_rede(missoes, aeronaves, catalogo)
|
||
sol = resolver(nos, arcos, aeronaves, catalogo)
|
||
escrever_resultado(sol, args.resultado)
|
||
imprimir_relatorio(sol, bases, args.resultado)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|