Add OARMP routing engine, dashboard and documentation

- Complete routing engine: ingest, optimizer (CG+B&B), maintenance
  monitor, metrics, pipeline, quality checks
- Streamlit dashboard with Input/Output tab structure, editable data
  editors, interactive Folium map with satellite layer and maintenance
  base highlights, FH stacked bar chart with TTM availability
- CSV data files: AERONAVES, CHECKS, AIRPORTS, ESCALA DE VOO
- README, CONTEXTO and CHANGELOG added
- Remove legacy pre_process scripts and raw binary files (PDFs/xlsx)
- Update .gitignore to exclude outputs/, data/, raw/*.pdf, raw/*.xlsx

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Eduardo Carlos
2026-06-17 11:52:34 -03:00
parent 32ad7c9f23
commit 3607965c88
43 changed files with 3419 additions and 1936 deletions

232
app/dashboard_backup.py Normal file
View File

@@ -0,0 +1,232 @@
"""
Aircraft Routing Dashboard Streamlit application.
Run with: streamlit run app/dashboard.py (from project root)
"""
from __future__ import annotations
import sys
from pathlib import Path
# Allow imports from src/ when running from project root
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import streamlit as st
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
from datetime import timedelta
from src.routing_engine import RoutingPipeline, DEFAULT_CONFIG
# ── Page config ───────────────────────────────────────────────────────────────
st.set_page_config(
page_title="OARMP Aircraft Routing",
page_icon="",
layout="wide",
)
st.title("✈ Aircraft Routing & Maintenance Planning (OARMP)")
st.caption("Set Partitioning + Column Generation + Branch & Bound")
# ── Sidebar parameters ──────────────────────────────────────────────────────
with st.sidebar:
st.header("⚙ Parameters")
tat = st.number_input("TAT mínimo (min)", min_value=0, max_value=240, value=60, step=10)
year = st.number_input("Ano do planejamento", min_value=2024, max_value=2030, value=2026)
time_limit = st.number_input("Limite tempo solver (s)", min_value=30, max_value=600, value=120)
run_btn = st.button("▶ Executar Otimização", type="primary")
# ── Session state ─────────────────────────────────────────────────────────────
if "result" not in st.session_state:
st.session_state["result"] = None
# ── Run pipeline ──────────────────────────────────────────────────────────────
if run_btn:
cfg = DEFAULT_CONFIG
cfg.tat_minutes = tat
cfg.planning_year = int(year)
cfg.mip_time_limit_seconds = int(time_limit)
with st.spinner("Executando otimização…"):
try:
pipe = RoutingPipeline(cfg)
st.session_state["result"] = pipe.run(save_outputs=True)
st.success("Otimização concluída!")
except Exception as exc:
st.error(f"Erro: {exc}")
st.exception(exc)
result = st.session_state.get("result")
# ── Tabs ──────────────────────────────────────────────────────────────────────
tab_summary, tab_gantt, tab_maint, tab_fleet, tab_ofrags = st.tabs(
["📊 Resumo", "📅 Gantt", "🔧 Manutenção", "✈ Frota", "📋 OFRAGs"]
)
# ── Tab: Summary ──────────────────────────────────────────────────────────────
with tab_summary:
if result is None:
st.info("Execute a otimização para ver os resultados.")
else:
s = result["summary"]
c1, c2, c3, c4 = st.columns(4)
c1.metric("Status", s["status"])
c2.metric("Objetivo (FH perdidas)", f"{s['total_ttm_loss_hours']:.2f} h")
c3.metric("OFRAGs cobertas", f"{s['covered_ofrags']} / {s['total_ofrags']}")
c4.metric("Eventos de manutenção", s["n_maintenance_events"])
if s["uncovered_ofrags"]:
st.warning(f"OFRAGs NÃO cobertas: {s['uncovered_ofrags']}")
st.subheader("Escala de voo otimizada")
sched = result["schedule"]
if not sched.empty:
st.dataframe(
sched.style.apply(
lambda r: ["background-color:#fff3cd" if r.get("maintenance_before") else "" for _ in r],
axis=1,
),
use_container_width=True,
)
st.caption(f"Colunas geradas no CG: {s['columns_generated']}")
# ── Tab: Gantt ────────────────────────────────────────────────────────────────
with tab_gantt:
if result is None:
st.info("Execute a otimização para ver o diagrama de Gantt.")
else:
sched = result["schedule"]
maint = result["maintenance"]
gantt_rows = []
if not sched.empty:
for _, row in sched.iterrows():
if pd.isna(row.get("departure")) or pd.isna(row.get("arrival")):
continue
gantt_rows.append(
dict(
Task=row["aircraft"],
Start=row["departure"],
Finish=row["arrival"],
Resource=row["ofrag_id"],
Type="OFRAG",
Label=row["ofrag_id"],
)
)
if not maint.empty:
for _, row in maint.iterrows():
if pd.isna(row.get("maint_start")) or pd.isna(row.get("maint_end")):
continue
gantt_rows.append(
dict(
Task=row["aircraft"],
Start=row["maint_start"],
Finish=row["maint_end"],
Resource="Manutenção",
Type="Maintenance",
Label=f"CHECK (perde {row['ttm_loss_hours']:.1f}h)",
)
)
if gantt_rows:
df_gantt = pd.DataFrame(gantt_rows)
color_map = {"Manutenção": "#636EFA"}
fig = px.timeline(
df_gantt,
x_start="Start",
x_end="Finish",
y="Task",
color="Type",
text="Label",
title="Escala de Voo e Manutenção por Aeronave",
color_discrete_map={"OFRAG": "#00CC96", "Maintenance": "#EF553B"},
)
fig.update_yaxes(categoryorder="category ascending")
fig.update_traces(textposition="inside")
fig.update_layout(height=400 + len(sched["aircraft"].unique()) * 40)
st.plotly_chart(fig, use_container_width=True)
cfg_obj = DEFAULT_CONFIG
try:
fig.write_html(str(cfg_obj.figures_dir / "gantt.html"))
except Exception:
pass
else:
st.info("Nenhum dado de Gantt disponível.")
# ── Tab: Maintenance ──────────────────────────────────────────────────────────
with tab_maint:
if result is None:
st.info("Execute a otimização para ver eventos de manutenção.")
else:
maint = result["maintenance"]
if maint.empty:
st.success("Nenhum evento de manutenção forçado.")
else:
st.dataframe(maint, use_container_width=True)
fig_loss = px.bar(
maint,
x="aircraft",
y="ttm_loss_hours",
color="check_cycle_index",
title="Horas de TTM perdidas por aeronave / ciclo de check",
labels={"ttm_loss_hours": "TTM perdido (h)", "aircraft": "Aeronave"},
text_auto=".2f",
)
st.plotly_chart(fig_loss, use_container_width=True)
# ── Tab: Fleet ────────────────────────────────────────────────────────────────
with tab_fleet:
if result is None:
st.info("Execute a otimização.")
else:
fleet_df = result["fleet"]
st.dataframe(fleet_df, use_container_width=True)
fig_util = px.bar(
fleet_df,
x="aircraft",
y="ttm_utilisation_pct",
title="Utilização do TTM por aeronave (%)",
labels={"ttm_utilisation_pct": "Utilização TTM (%)", "aircraft": "Aeronave"},
text_auto=".1f",
color="ttm_utilisation_pct",
color_continuous_scale="RdYlGn",
range_color=[0, 100],
)
st.plotly_chart(fig_util, use_container_width=True)
fig_fh = px.bar(
fleet_df,
x="aircraft",
y=["initial_ttm_h", "flight_hours_scheduled", "total_ttm_loss_h"],
barmode="group",
title="TTM inicial vs FH planejadas vs FH perdidas",
labels={"value": "Horas de voo", "aircraft": "Aeronave"},
)
st.plotly_chart(fig_fh, use_container_width=True)
# ── Tab: OFRAGs ───────────────────────────────────────────────────────────────
with tab_ofrags:
if result is None:
st.info("Execute a otimização.")
else:
ofrags = result["ofrags"]
st.write(f"**{len(ofrags)} OFRAGs** elegíveis (iniciam e terminam na base de manutenção).")
cols_show = [c for c in ["ofrag_id", "departure", "arrival", "flight_hours", "n_legs", "missions", "origin", "destination"] if c in ofrags.columns]
st.dataframe(ofrags[cols_show], use_container_width=True)
fig_fh = px.bar(
ofrags,
x="ofrag_id",
y="flight_hours",
title="Horas de voo por OFRAG",
labels={"flight_hours": "FH total", "ofrag_id": "OFRAG"},
text_auto=".2f",
)
st.plotly_chart(fig_fh, use_container_width=True)