Files
aircraftrouting_arara/app/dashboard_backup.py
Eduardo Carlos 3607965c88 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>
2026-06-17 11:52:34 -03:00

233 lines
9.8 KiB
Python
Raw Permalink 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.

"""
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)