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