Files
arara_oarmp/software/visualizar_resultado.py
Cesa-V f0d72f8189 Corrige modelo OAMRP v3 e alinha nomenclatura com Al-Thani (2016)
Correções ao modelo:
- Adiciona conservação de fluxo em nós de inspeção (bug que permitia
  criação/destruição de unidades de fluxo, inflando cobertura para 50/50
  sem atribuição real de rotas)
- Remove restrição C6 simplificada que somava horas totais sem respeitar
  resets de inspeção, tornando 2809/2811 artificialmente ociosos

Nomenclatura alinhada ao Al-Thani (2016):
- InspecaoParam.lrt_h → f_max  (F do artigo: batente máximo legal)
- Aeronave.horas_iniciais → f0  (f_k: horas acumuladas no início)
- orcamento_h() → lrt_inicial() (LRT = F_max − f0)
- horas_iniciais_aleatorias() → gerar_f0_aleatorio()
- carregar_horas_iniciais() → carregar_f0()
- CLI --horas-iniciais → --f0

Novos arquivos:
- software/gerar_ofrag.py: gera tabela de OFRAGs com prioridades 1-5
- software/visualizar_resultado.py: mapa Folium interativo de rotas
- db/processed/ofrag.csv: 50 OFRAGs sintéticas (seed 42)
- db/processed/mapa_rotas.html: mapa gerado da última rodada

Resultado com --sintetico --aleatorio --ofrag: 50/50 missões cumpridas
(antes: 30/50 sem ferry, 7/50 com bug de fluxo)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 00:50:53 -03:00

233 lines
7.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Visualização interativa do resultado OAMRP — Esquadrão Arara C-105.
Gera um mapa HTML (Folium) com:
- Marcadores para cada base operacional
- Rotas por aeronave (cor distinta por EVAM)
- Missões não cumpridas em cinza tracejado
- Popup com detalhes de cada missão
Uso:
python software/visualizar_resultado.py \
--resultado db/processed/resultado_oamrp_sintetico.csv \
--saida db/processed/mapa_rotas.html
"""
import argparse
import csv
from pathlib import Path
import folium
from folium import plugins
BASE_DIR = Path(__file__).resolve().parents[1]
RESULTADO_PADRAO = BASE_DIR / "db" / "processed" / "resultado_oamrp_sintetico.csv"
MAPA_PADRAO = BASE_DIR / "db" / "processed" / "mapa_rotas.html"
# Coordenadas das bases (lat, lon)
BASES_COORDS: dict[str, tuple[float, float]] = {
"SBMN": (-3.15, -59.99),
"SBBE": (-1.38, -48.48),
"SBBV": ( 2.84, -60.69),
"SBSN": (-2.42, -54.79),
"SBPV": (-8.71, -63.90),
"SBTS": (-4.25, -69.94),
"SBTT": (-3.38, -64.72),
"SBUA": (-0.15, -67.05),
"SWBC": (-0.98, -62.92),
"SBMY": (-5.81, -61.28),
}
# Nomes legíveis das bases
BASES_NOMES = {
"SBMN": "Manaus / Ponta Pelada (Hub)",
"SBBE": "Belém",
"SBBV": "Boa Vista",
"SBSN": "Santarém",
"SBPV": "Porto Velho",
"SBTS": "Tabatinga",
"SBTT": "Tefé",
"SBUA": "São Gabriel da Cachoeira",
"SWBC": "Barcelos",
"SBMY": "Manicoré",
}
# Paleta de cores por aeronave
CORES_AERONAVE = {
"2800": "#1f77b4", # azul
"2803": "#2ca02c", # verde
"2809": "#d62728", # vermelho
"2811": "#ff7f0e", # laranja
}
COR_NAO_CUMPRIDA = "#aaaaaa"
COR_HUB = "#d62728"
COR_BASE = "#1f77b4"
def _cor_prio(prio: str) -> str:
mapa = {"1": "red", "2": "orange", "3": "yellow", "4": "lightblue", "5": "lightgray"}
return mapa.get(str(prio), "white")
def carregar_resultado(caminho: Path) -> list[dict]:
with caminho.open(newline="", encoding="utf-8-sig") as f:
return list(csv.DictReader(f))
def adicionar_bases(mapa: folium.Map) -> None:
for icao, (lat, lon) in BASES_COORDS.items():
cor = COR_HUB if icao == "SBMN" else COR_BASE
icone = folium.Icon(color="red" if icao == "SBMN" else "blue", icon="plane", prefix="fa")
folium.Marker(
location=[lat, lon],
tooltip=f"<b>{icao}</b><br>{BASES_NOMES.get(icao, '')}",
popup=folium.Popup(f"<b>{icao}</b><br>{BASES_NOMES.get(icao, '')}", max_width=200),
icon=icone,
).add_to(mapa)
def gerar_mapa(resultado: Path, saida: Path) -> None:
linhas = carregar_resultado(resultado)
mapa = folium.Map(
location=[-3.5, -62.0],
zoom_start=5,
tiles="CartoDB positron",
)
# Grupos por aeronave + grupo para não cumpridas
aeronaves_vistas = sorted({r["aeronave"] for r in linhas if r["aeronave"]})
grupos: dict[str, folium.FeatureGroup] = {}
for k in aeronaves_vistas:
grupos[k] = folium.FeatureGroup(name=f"EVAM {k}", show=True)
grupos["nao_cumprida"] = folium.FeatureGroup(name="Não cumpridas", show=True)
cumpridas = [r for r in linhas if r["status"] == "cumprida"]
nao_cumpridas = [r for r in linhas if r["status"] != "cumprida"]
# Rotas cumpridas
for r in cumpridas:
orig = r["orig"]
dest = r["dest"]
if orig not in BASES_COORDS or dest not in BASES_COORDS:
continue
lat_o, lon_o = BASES_COORDS[orig]
lat_d, lon_d = BASES_COORDS[dest]
k = r["aeronave"]
cor = CORES_AERONAVE.get(k, "#333333")
# Linha de rota
folium.PolyLine(
locations=[[lat_o, lon_o], [lat_d, lon_d]],
color=cor,
weight=2.5,
opacity=0.8,
tooltip=f"EVAM {k} | {orig}{dest} | OM {r['om']} | prio {r['prioridade']}",
popup=folium.Popup(
f"<b>EVAM {k}</b> — Missão #{r['ordem']}<br>"
f"{orig}{dest}<br>"
f"OM: {r['om']} | Prio: {r['prioridade']}<br>"
f"Partida: {r['partida_utc'][:16].replace('T',' ')}Z<br>"
f"Chegada: {r['chegada_utc'][:16].replace('T',' ')}Z<br>"
f"Duração: {r['dur_h']}h",
max_width=250,
),
).add_to(grupos[k])
# Marcador de origem (pequeno círculo)
folium.CircleMarker(
location=[lat_o, lon_o],
radius=4,
color=cor,
fill=True,
fill_color=_cor_prio(r["prioridade"]),
fill_opacity=0.9,
tooltip=f"{orig} | prio {r['prioridade']}",
).add_to(grupos[k])
# Missões não cumpridas (linha tracejada cinza)
for r in nao_cumpridas:
orig = r["orig"]
dest = r["dest"]
if orig not in BASES_COORDS or dest not in BASES_COORDS:
continue
lat_o, lon_o = BASES_COORDS[orig]
lat_d, lon_d = BASES_COORDS[dest]
folium.PolyLine(
locations=[[lat_o, lon_o], [lat_d, lon_d]],
color=COR_NAO_CUMPRIDA,
weight=1.5,
opacity=0.5,
dash_array="6 4",
tooltip=f"NÃO CUMPRIDA | {orig}{dest} | OM {r['om']} | prio {r['prioridade']}",
).add_to(grupos["nao_cumprida"])
# Adiciona grupos ao mapa
for g in grupos.values():
g.add_to(mapa)
adicionar_bases(mapa)
folium.LayerControl(collapsed=False).add_to(mapa)
# Legenda HTML
legenda_html = """
<div style="position:fixed;bottom:30px;left:30px;z-index:1000;background:white;
padding:12px 16px;border-radius:8px;box-shadow:2px 2px 6px rgba(0,0,0,0.3);
font-family:sans-serif;font-size:12px;">
<b>Esquadrão Arara — OAMRP v3</b><br><br>
<b>Aeronaves:</b><br>
"""
for k, cor in CORES_AERONAVE.items():
legenda_html += (
f' <span style="display:inline-block;width:16px;height:4px;'
f'background:{cor};margin-right:6px;vertical-align:middle;"></span>'
f'EVAM {k}<br>'
)
legenda_html += """
<br><b>Prioridade (preenchimento):</b><br>
<span style="display:inline-block;width:12px;height:12px;border-radius:50%;
background:red;margin-right:6px;"></span>Prio 1 (urgente)<br>
<span style="display:inline-block;width:12px;height:12px;border-radius:50%;
background:orange;margin-right:6px;"></span>Prio 2<br>
<span style="display:inline-block;width:12px;height:12px;border-radius:50%;
background:yellow;margin-right:6px;"></span>Prio 3<br>
<span style="display:inline-block;width:12px;height:12px;border-radius:50%;
background:lightblue;margin-right:6px;"></span>Prio 45<br>
<br>
<span style="border-bottom:2px dashed #aaa;display:inline-block;width:20px;
margin-right:6px;"></span>Não cumprida
</div>
"""
mapa.get_root().html.add_child(folium.Element(legenda_html))
saida.parent.mkdir(parents=True, exist_ok=True)
mapa.save(str(saida))
print(f" Mapa salvo: {saida}")
_resumir_resultado(linhas)
def _resumir_resultado(linhas: list[dict]) -> None:
total = len(linhas)
cumpridas = sum(1 for r in linhas if r["status"] == "cumprida")
print(f" Missões: {cumpridas}/{total} cumpridas")
aeronaves = {}
for r in linhas:
if r["aeronave"]:
aeronaves.setdefault(r["aeronave"], 0)
aeronaves[r["aeronave"]] += 1
for k, c in sorted(aeronaves.items()):
print(f" EVAM {k}: {c} missões")
def main() -> None:
parser = argparse.ArgumentParser(description="Visualização OAMRP — Arara C-105")
parser.add_argument("--resultado", type=Path, default=RESULTADO_PADRAO)
parser.add_argument("--saida", type=Path, default=MAPA_PADRAO)
args = parser.parse_args()
gerar_mapa(resultado=args.resultado, saida=args.saida)
if __name__ == "__main__":
main()