Adiciona missoes sinteticas, ferry (Fase 3) e consolida versoes anteriores

- Gerador sintetico cross-base (50 missoes, 10 bases, haversine/C-105)
- Fase 3: arcos de ferry em construir_rede(); ferry consome horas de celula
- oamrp_v2.py e resultados CSV anteriores adicionados ao repositorio
- README, docs/about.md e changelog atualizados ate v0.6

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Cesa-V
2026-06-15 23:51:31 -03:00
parent 328a066849
commit 7cd7458565
9 changed files with 782 additions and 37 deletions

View File

@@ -240,13 +240,12 @@ def construir_rede(
- sumidouro_k : ponto de chegada de cada aeronave k
Tipos de arcos:
- inicio : origem_k → missao_m (m.orig == base_k)
- inicio : origem_k → missao_m (com ou sem ferry inicial)
- 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)
- ferry : missao_m → missao_m' (dest≠orig, gap ≥ TAT + tempo_ferry)
- missao_insp : missao_m → inspecao_k_i (m.dest == SBMN, ou com ferry)
- insp_missao : inspecao_k_i → missao_m' (m'.orig == SBMN ou com ferry)
- fim : missao_m / inspecao_k_i → sumidouro_k
Fase 3 adicionará arcos de ferry entre bases distintas.
"""
nos: dict[str, No] = {}
arcos: list[Arco] = []
@@ -288,11 +287,22 @@ def construir_rede(
aeronave_id=a.id,
)
# --- Arcos início: origem_k → missao_m ---
# Helper: tempo de ferry entre bases (0 se mesma base)
def _ferry_h(b1: str, b2: str) -> float:
if b1 == b2:
return 0.0
if b1 not in BASES_COORDS or b2 not in BASES_COORDS:
return float("inf")
return _haversine_km(b1, b2) / _VELOCIDADE_CRUZEIRO_KMH
# --- Arcos início: origem_k → missao_m (com ou sem ferry) ---
for a in aeronaves:
src_id = f"SRC_{a.id}"
for m in missoes:
if m.orig != a.base_inicial:
fh = _ferry_h(a.base_inicial, m.orig)
if fh == float("inf"):
continue
if m.t_dep < fh: # missão começa antes do ferry terminar
continue
arc_id = f"INI_{a.id}_{m.id}"
arcos.append(
@@ -300,19 +310,21 @@ def construir_rede(
id=arc_id,
orig_id=src_id,
dest_id=f"M_{m.id}",
tipo="inicio",
tipo="inicio" if fh == 0.0 else "ferry",
aeronave_id=a.id,
horas_ferry=fh,
)
)
# --- Arcos missao → missao ---
# --- Arcos missao → missao (direto ou com ferry) ---
for mi in missoes:
for mj in missoes:
if mi.id == mj.id:
continue
if mi.dest != mj.orig:
fh = _ferry_h(mi.dest, mj.orig)
if fh == float("inf"):
continue
if mj.t_dep < mi.t_arr + tat_h:
if mj.t_dep < mi.t_arr + tat_h + fh:
continue
arc_id = f"MM_{mi.id}_{mj.id}"
arcos.append(
@@ -320,7 +332,8 @@ def construir_rede(
id=arc_id,
orig_id=f"M_{mi.id}",
dest_id=f"M_{mj.id}",
tipo="missao_missao",
tipo="missao_missao" if fh == 0.0 else "ferry",
horas_ferry=fh,
)
)
@@ -355,19 +368,22 @@ def construir_rede(
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:
# SRC_k → INSP_k_i (com ou sem ferry até SBMN)
fh_src = _ferry_h(a.base_inicial, BASE_MANUTENCAO)
if fh_src != float("inf"):
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,
horas_ferry=fh_src,
))
# M_m → INSP_k_i: missões que chegam em SBMN
# M_m → INSP_k_i: missões de qualquer base (ferry até SBMN)
for m in missoes:
if m.dest != BASE_MANUTENCAO:
fh = _ferry_h(m.dest, BASE_MANUTENCAO)
if fh == float("inf"):
continue
arcos.append(Arco(
id=f"MINSP_{a.id}_{seq}_{m.id}",
@@ -375,13 +391,15 @@ def construir_rede(
dest_id=insp_nid,
tipo="missao_insp",
aeronave_id=a.id,
horas_ferry=fh,
))
# INSP_k_i → M_m': missões que partem de SBMN após o downtime mínimo
# INSP_k_i → M_m': missões de qualquer base após downtime (ferry de SBMN)
for m in missoes:
if m.orig != BASE_MANUTENCAO:
fh = _ferry_h(BASE_MANUTENCAO, m.orig)
if fh == float("inf"):
continue
if m.t_dep < insp.dt_h + tat_h: # muito cedo mesmo no melhor caso
if m.t_dep < insp.dt_h + tat_h + fh:
continue
arcos.append(Arco(
id=f"INSPM_{a.id}_{seq}_{m.id}",
@@ -389,6 +407,7 @@ def construir_rede(
dest_id=f"M_{m.id}",
tipo="insp_missao",
aeronave_id=a.id,
horas_ferry=fh,
))
# INSP_k_i → SNK_k (aeronave encerra o horizonte após inspeção)
@@ -566,7 +585,8 @@ def resolver(
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
# horas acumuladas = missão + ferry (C6: traslado consome horas de célula)
carga = nos[v_nid].carga_h + a.horas_ferry
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}"
@@ -847,7 +867,7 @@ def imprimir_relatorio(
total = len(missoes_nos)
print("=" * 72)
print(" OAMRP V3 — Fase 1 (C1C6) | Esquadrao Arara C-105")
print(" OAMRP V3 — Fase 3 (C1C11 + ferry) | Esquadrao Arara C-105")
print("=" * 72)
print(f" Status solver : {pulp.LpStatus[sol['modelo'].status]}")
print(f" Missoes : {len(cobertas)}/{total} cumpridas")
@@ -881,22 +901,47 @@ def imprimir_relatorio(
print(f" {k}: {oc:.1f}h (sem restricao de inspecao)")
print()
arcos_por_id = {a.id: a for a in sol["arcos"]}
y = sol["y"]
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)
horas_missao = sum(n.carga_h for n in missoes_rota)
# horas de ferry: soma horas_ferry dos arcos ativos para este k
horas_ferry_total = sum(
a.horas_ferry
for a in sol["arcos"]
if (k, a.id) in y
and y[(k, a.id)].value() is not None
and y[(k, a.id)].value() > 0.5
and a.horas_ferry > 0.0
)
horas_celula = horas_missao + horas_ferry_total
orcamento = acft[k].orcamento_h(catalogo)
folga = orcamento - horas
folga = orcamento - horas_celula
print(
f" Aeronave {k}: {len(missoes_rota)} missoes | {horas:.1f}h voadas | "
f" Aeronave {k}: {len(missoes_rota)} missoes | "
f"{horas_missao:.1f}h voadas + {horas_ferry_total:.1f}h ferry = {horas_celula:.1f}h celula | "
f"folga {folga:.1f}h | {len(insps)} inspecao(oes)"
)
# Linha do tempo unificada: missões e inspeções ordenadas por t_ini
# Linha do tempo: missões, inspeções e ferries ordenados por t_ini
eventos: list[tuple[float, str]] = []
for no in rota:
# rastrear nó anterior para identificar ferry
nos_rota_ids = [n.id for n in rota]
for idx, no in enumerate(rota):
if no.tipo == "missao":
m = no.missao
# detecta ferry antes desta missão
if idx > 0:
prev = rota[idx - 1]
if prev.tipo in ("missao", "origem") and prev.base_out != no.base_in:
fh = _haversine_km(prev.base_out, no.base_in) / _VELOCIDADE_CRUZEIRO_KMH
eventos.append((
no.t_ini - fh,
f" [FERRY {prev.base_out}->{no.base_in} {fh:.1f}h]"
))
eventos.append((
no.t_ini,
f" {m.partida_utc:%d/%m %H:%M}Z "
@@ -906,15 +951,23 @@ def imprimir_relatorio(
seq = no.insp_seq
s_val = sol["s_insp"][(k, seq)].value() or 0.0
insp = catalogo[seq]
if idx > 0:
prev = rota[idx - 1]
if prev.tipo == "missao" and prev.base_out != BASE_MANUTENCAO:
fh = _haversine_km(prev.base_out, BASE_MANUTENCAO) / _VELOCIDADE_CRUZEIRO_KMH
eventos.append((
s_val - fh,
f" [FERRY {prev.base_out}->{BASE_MANUTENCAO} {fh:.1f}h (retorno insp)]"
))
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]:
for _, linha in sorted(eventos)[:20]:
print(linha)
if len(eventos) > 15:
print(f" ... mais {len(eventos) - 15} eventos")
if len(eventos) > 20:
print(f" ... mais {len(eventos) - 20} eventos")
print()
nao = [nid for nid in missoes_nos if nid not in cobertas]
@@ -1087,6 +1140,124 @@ def derivar_bases(
return sorted(top)
# ---------------------------------------------------------------------------
# Missões sintéticas
# ---------------------------------------------------------------------------
# Coordenadas (lat, lon) das 10 bases operacionais do C-105
BASES_COORDS: dict[str, tuple[float, float]] = {
"SBMN": (-3.15, -59.99), # Manaus — Ponta Pelada (hub)
"SBBE": (-1.38, -48.48), # Belém
"SBBV": ( 2.84, -60.69), # Boa Vista
"SBSN": (-2.42, -54.79), # Santarém
"SBPV": (-8.71, -63.90), # Porto Velho
"SBTS": (-4.25, -69.94), # Tabatinga
"SBTT": (-3.38, -64.72), # Tefé
"SBUA": (-0.15, -67.05), # São Gabriel da Cachoeira
"SWBC": (-0.98, -62.92), # Barcelos
"SBMY": (-5.81, -61.28), # Manicoré
}
_VELOCIDADE_CRUZEIRO_KMH = 430.0 # C-105 Spartan (típico)
_VARIACAO_DUR = 0.10 # ± 10 % de variação aleatória na duração
def _haversine_km(b1: str, b2: str) -> float:
import math
lat1, lon1 = BASES_COORDS[b1]
lat2, lon2 = BASES_COORDS[b2]
R = 6371.0
dlat = math.radians(lat2 - lat1)
dlon = math.radians(lon2 - lon1)
a = math.sin(dlat/2)**2 + math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) * math.sin(dlon/2)**2
return R * 2 * math.asin(math.sqrt(a))
def gerar_missoes_sinteticas(
n: int = 50,
seed: Optional[int] = 42,
inicio: Optional[datetime] = None,
horizonte_h: float = 744.0, # janeiro = 31 × 24
n_evam: int = 5,
bases: Optional[list[str]] = None,
) -> tuple[list[Missao], datetime]:
"""
Gera n missões aleatórias cross-base (orig ≠ dest) entre as bases listadas.
Duração estimada via distância haversine ÷ velocidade de cruzeiro do C-105,
com variação aleatória de ± VARIACAO_DUR.
n_evam missões são marcadas como obrigatórias (hard).
"""
import math
if inicio is None:
inicio = datetime(2025, 1, 1, tzinfo=timezone.utc)
bases = list(bases or BASES_COORDS.keys())
rng = random.Random(seed)
pares = [(o, d) for o in bases for d in bases if o != d]
missoes: list[Missao] = []
# sorteia pares com reposição para garantir n missões
escolhidos = [pares[rng.randrange(len(pares))] for _ in range(n)]
# garante que SBMN aparece em orig ou dest com frequência (hub real)
for i in range(0, n, 3):
outro = rng.choice([b for b in bases if b != "SBMN"])
if rng.random() < 0.5:
escolhidos[i] = ("SBMN", outro)
else:
escolhidos[i] = (outro, "SBMN")
evam_indices = set(rng.sample(range(n), min(n_evam, n)))
# distribui decolagens uniformemente ao longo do horizonte com gap mínimo de 2h
t_deps = sorted(rng.uniform(0, horizonte_h - 10) for _ in range(n))
for idx, (t_dep_h, (orig, dest)) in enumerate(zip(t_deps, escolhidos)):
dist_km = _haversine_km(orig, dest)
dur_base = dist_km / _VELOCIDADE_CRUZEIRO_KMH
dur_h = round(dur_base * (1 + rng.uniform(-_VARIACAO_DUR, _VARIACAO_DUR)), 2)
dur_h = max(0.5, dur_h)
t_dep_h = round(t_dep_h, 3)
t_arr_h = round(t_dep_h + dur_h, 3)
if t_arr_h > horizonte_h:
t_arr_h = horizonte_h
t_dep_h = max(0.0, t_arr_h - dur_h)
partida = datetime.fromtimestamp(
inicio.timestamp() + t_dep_h * 3600, tz=timezone.utc
)
chegada = datetime.fromtimestamp(
inicio.timestamp() + t_arr_h * 3600, tz=timezone.utc
)
obrigatoria = idx in evam_indices
cod = f"EVAM{idx+1:03d}" if obrigatoria else f"SIM{idx+1:03d}"
om_str = str(idx + 1)
missoes.append(Missao(
id=f"SIM_{idx+1:03d}_{orig}_{dest}",
orig=orig,
dest=dest,
t_dep=t_dep_h,
t_arr=t_arr_h,
dur_h=dur_h,
prioridade=1,
obrigatoria=obrigatoria,
partida_utc=partida,
chegada_utc=chegada,
periodo="DIURNO",
linhas_origem="",
aeronave_real="",
om=om_str,
codigo_missao=cod,
))
return missoes, inicio
# ---------------------------------------------------------------------------
# Horas iniciais
# ---------------------------------------------------------------------------
@@ -1158,20 +1329,46 @@ def main() -> None:
"--hard-pattern", default=HARD_MISSAO_PADRAO,
help="Regex no campo 'missao' do CSV que marca missões obrigatórias (Q2).",
)
parser.add_argument(
"--sintetico", action="store_true",
help="Usa missões sintéticas cross-base em vez do CSV real.",
)
parser.add_argument(
"--n-missoes", type=int, default=50,
help="Número de missões sintéticas a gerar (padrão: 50).",
)
parser.add_argument(
"--n-evam", type=int, default=5,
help="Missões obrigatórias (EVAM/hard) nas sintéticas (padrão: 5).",
)
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,
)
if args.sintetico:
horizonte_h = (fim - inicio).total_seconds() / 3600
bases = list(BASES_COORDS.keys())
missoes, horizonte_dt = gerar_missoes_sinteticas(
n=args.n_missoes,
seed=args.seed,
inicio=inicio,
horizonte_h=horizonte_h,
n_evam=args.n_evam,
bases=bases,
)
print(f" Modo sintético: {len(missoes)} missões geradas entre {len(bases)} bases.")
else:
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, horizonte_dt = carregar_missoes(
args.csv, inicio, fim,
bases_validas=bases_validas,
hard_pattern=args.hard_pattern,
)
f0 = carregar_horas_iniciais(
fonte=args.horas_iniciais,