Compare commits

...

4 Commits

Author SHA1 Message Date
Cesa-V
a34ed6b340 Migra C1 de Set Covering para Set Partitioning e documenta limitacoes (v0.10)
- C1 alterado: z[nid] <= 1 -> z[nid] == 1 para todas as missoes; toda missao
  deve ser coberta por exatamente 1 aeronave (sem missoes descobertas).
- n_missoes padrao reduzido de 50 para 20 (n_evam de 5 para 4): garante
  factibilidade do MIP com Set Partitioning no envelope de 4 aeronaves.
- Time limit padrao do solver CBC fixado em 120 s (era ilimitado): evita
  travamento ao tentar provar infeasibility.
- Corrigido display "Not Solved": status de L1 preservado quando PuLP
  redefine mdl.status apos setObjective() em L2.
- f0_cenario_insp_grande.json revisado: apenas 2803 com f0=595h em INSP 2A
  (F_max=600h, downtime=4 dias); demais matriculas com f0=0.
- Gerado planejamento_insp_grande.html: Gantt exibe INSP 2A visivelmente;
  20/20 missoes cobertas; CBC Optimal em < 15 s.
- changelog.md e LOG.md atualizados com limitacoes detalhadas para
  contribuidores: escopo validado, bugs de display, riscos numericos do
  big-M com LRT < 5h e comportamento de 2811 ociosa.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 11:09:24 -03:00
Cesa-V
6433055e9e Atualiza LOG e changelog para sessao 2026-06-16 (v0.8 e v0.9)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 09:39:30 -03:00
Cesa-V
83a2dac624 Corrige Gantt, adiciona escala de voo real e ajusta solver (v0.9)
- visualizar_resultado: barras Gantt via go.Scatter lines (renderizacao
  garantida), yaxis range fixo para FAB 2800 visivel, largura minima 2h
- oamrp_v3: parser --escala para Escala de Voo Modelo 1 (CSV real),
  coordenadas de 26 bases, time-limit default 5min, imports timedelta/date

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 09:34:14 -03:00
Cesa-V
3756f961a8 Adiciona visualizacao combinada (Gantt + mapa + tabela) e exporta inspecoes no CSV (v0.8)
oamrp_v3.py: exporta linhas status=inspecao no CSV resultado com datas UTC reais
visualizar_resultado.py: refatorado para gerar um unico HTML (planejamento.html)
com cabecalho, Gantt Plotly (divisoes diarias, FAB labels), mapa Folium em iframe
e tabela de voos com filtros por aeronave/status/prioridade/base e busca livre

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 01:48:36 -03:00
8 changed files with 1907 additions and 178 deletions

10
LOG.md
View File

@@ -38,3 +38,13 @@ Legenda de autores:
| 2026-06-16 | 10:20 | VTO | Alinhou nomenclatura com Al-Thani (2016) | `software/oamrp_v3.py` | lrt_h→f_max (batente fixo F); horas_iniciais→f0 (acumulado inicial f_k); orcamento_h()→lrt_inicial() (LRT=F_maxf0); horas_iniciais_aleatorias()→gerar_f0_aleatorio(); carregar_horas_iniciais()→carregar_f0(); CLI --horas-iniciais→--f0. | | 2026-06-16 | 10:20 | VTO | Alinhou nomenclatura com Al-Thani (2016) | `software/oamrp_v3.py` | lrt_h→f_max (batente fixo F); horas_iniciais→f0 (acumulado inicial f_k); orcamento_h()→lrt_inicial() (LRT=F_maxf0); horas_iniciais_aleatorias()→gerar_f0_aleatorio(); carregar_horas_iniciais()→carregar_f0(); CLI --horas-iniciais→--f0. |
| 2026-06-16 | 10:30 | VTO | Criou visualização interativa de rotas | `software/visualizar_resultado.py`; `db/processed/mapa_rotas.html` | Mapa Folium com rotas por aeronave (cor distinta), missões não cumpridas tracejadas em cinza, popup com detalhes, legenda de prioridades. | | 2026-06-16 | 10:30 | VTO | Criou visualização interativa de rotas | `software/visualizar_resultado.py`; `db/processed/mapa_rotas.html` | Mapa Folium com rotas por aeronave (cor distinta), missões não cumpridas tracejadas em cinza, popup com detalhes, legenda de prioridades. |
| 2026-06-16 | 10:35 | VTO | Rodada sintética validada: 50/50 missões cumpridas | `db/processed/resultado_oamrp_sintetico.csv` | Comando: --sintetico --seed 42 --aleatorio --ofrag; solver CBC retornou Optimal; todas as 4 EVAMs voaram com ferry; commit f0d72f8 enviado ao remote. | | 2026-06-16 | 10:35 | VTO | Rodada sintética validada: 50/50 missões cumpridas | `db/processed/resultado_oamrp_sintetico.csv` | Comando: --sintetico --seed 42 --aleatorio --ofrag; solver CBC retornou Optimal; todas as 4 EVAMs voaram com ferry; commit f0d72f8 enviado ao remote. |
| 2026-06-16 | 14:00 | VTO | Criou visualização combinada (Gantt + mapa + tabela) | `software/visualizar_resultado.py`; `db/processed/planejamento_cenario_esquadrao.html` | HTML único com título "Planejamento de Diagonal de Manutenção"; Gantt via go.Scatter lines (renderização garantida); mapa Folium em iframe; tabela de voos com filtros JS; commit 3756f96. |
| 2026-06-16 | 15:00 | VTO | Criou cenários JSON de f0 para testes com inspeções | `db/processed/f0_cenario_esquadrao.json`; `db/processed/f0_cenario_insp_grande.json` | f0_cenario_esquadrao simula dados reais do esquadrão (LRT 32h/215h/12h/28h); f0_cenario_insp_grande força aeronaves próximas ao limite. |
| 2026-06-16 | 20:00 | VTO | Migrou modelo de Set Covering para Set Partitioning (C1) | `software/oamrp_v3.py` | Restrição C1 alterada de z<=1 para z==1 para todas as missões; eliminada distinção entre missões hard/opcionais; toda missão deve ser coberta por exatamente 1 aeronave. |
| 2026-06-16 | 20:05 | VTO | Reduziu missões sintéticas padrão de 50 para 20 | `software/oamrp_v3.py` | Set Partitioning com 50 missões tornava o MIP inviável ou muito lento (travamento do solver); 20 missões garantem factibilidade no envelope de 4 aeronaves em janeiro; n_evam reduzido de 5 para 4. |
| 2026-06-16 | 20:10 | VTO | Adicionou time limit padrão de 120 s ao solver CBC | `software/oamrp_v3.py` | Solver travava indefinidamente sem time limit ao tentar provar infeasibility; limite de 120 s garante retorno com melhor solução encontrada. |
| 2026-06-16 | 20:15 | VTO | Corrigiu display do status do solver (Not Solved) | `software/oamrp_v3.py` | PuLP redefine mdl.status para 0 ("Not Solved") ao chamar setObjective() antes de L2; status de L1 agora preservado quando L2 reporta 0. |
| 2026-06-16 | 20:20 | VTO | Revisou f0_cenario_insp_grande.json | `db/processed/f0_cenario_insp_grande.json` | Apenas 2803 com f0=595h em INSP 2A (F_max=600h, downtime=4 dias, LRT_ini=5h); demais matrículas com f0=0; configuração anterior com 4 aeronaves apertadas simultaneamente tornava o MIP inviável. |
| 2026-06-16 | 20:30 | VTO | Gerou visualização do cenário inspeção grande (v0.10) | `db/processed/planejamento_insp_grande.html`; `db/processed/resultado_oamrp_v3.csv` | 20/20 missões cobertas; Gantt exibe INSP 2A (4 dias) visivelmente; 2 inspeções no horizonte de janeiro; solver CBC retornou Optimal em < 15 s. |
| 2026-06-16 | 16:00 | VTO | Corrigiu Gantt: FAB 2800 invisível e barras não renderizavam | `software/visualizar_resultado.py` | yaxis range=[-0.5, n-0.5] para FAB 2800 (y=0) visível; shapes substituídas por go.Scatter mode="lines" width=64 (renderização garantida); largura mínima de 2h por barra. |
| 2026-06-16 | 16:30 | VTO | Adicionou parser de Escala de Voo Modelo 1 e bases extras | `software/oamrp_v3.py`; `db/raw/ESCALA DE VOO MODELO 1.csv` | Flag --escala carrega missões reais do CSV com separador ";"; coordenadas de 26 bases (era 10); time-limit default alterado para 300s; imports timedelta/date consolidados; commit 83a2dac. |

View File

@@ -0,0 +1,6 @@
{
"2800": {"2": 268, "3": 268, "4": 568, "6": 142, "7": 142, "19": 1800, "20": 2100, "22": 680},
"2803": {"2": 85, "3": 85, "4": 285, "6": 85, "7": 185, "19": 800, "20": 1200, "22": 350},
"2809": {"2": 155, "3": 455, "4": 155, "6": 288, "7": 288, "19": 1200, "20": 1800, "22": 500},
"2811": {"2": 120, "3": 572, "4": 572, "6": 120, "7": 220, "19": 1500, "20": 2200, "22": 720}
}

View File

@@ -0,0 +1,6 @@
{
"2800": {"2": 0, "3": 0, "4": 0, "6": 0, "7": 0, "19": 0, "20": 0, "22": 0},
"2803": {"2": 0, "3": 595, "4": 0, "6": 0, "7": 0, "19": 0, "20": 0, "22": 0},
"2809": {"2": 0, "3": 0, "4": 0, "6": 0, "7": 0, "19": 0, "20": 0, "22": 0},
"2811": {"2": 0, "3": 0, "4": 0, "6": 0, "7": 0, "19": 0, "20": 0, "22": 0}
}

File diff suppressed because one or more lines are too long

View File

@@ -1,51 +1,23 @@
status,aeronave,ordem,id,om,orig,dest,partida_utc,chegada_utc,dur_h,prioridade,periodo,aeronave_real_2025,linhas_origem status,aeronave,ordem,id,om,orig,dest,partida_utc,chegada_utc,dur_h,prioridade,periodo,aeronave_real_2025,linhas_origem
cumprida,2800,1,RV2025_OM1_E2_L5_C5,1,SBMN,SBMN,2025-01-02T14:30:00Z,2025-01-02T15:35:00Z,1.08,1,NOTURNO,2809,5 cumprida,2800,1,SIM_007_SBMN_SBCO,7,SBMN,SBCO,2025-01-09T02:09:28.800000Z,2025-01-09T07:58:40.800000Z,5.82,1,DIURNO,,
cumprida,2800,2,RV2025_OM5_E2_L13_C13,5,SBMN,SBMN,2025-01-07T14:15:00Z,2025-01-07T16:15:00Z,2.00,1,DIURNO,2803,13 cumprida,2800,2,SIM_010_SBCY_SBMN,10,SBCY,SBMN,2025-01-11T23:29:27.600000Z,2025-01-12T03:09:39.600000Z,3.67,3,DIURNO,,
cumprida,2803,1,RV2025_OM7_E1_L15_C15,7,SBMN,SBMN,2025-01-07T10:40:00Z,2025-01-07T12:30:00Z,1.83,1,DIURNO,2809,15 cumprida,2800,3,SIM_012_SWCA_SBTT,12,SWCA,SBTT,2025-01-12T14:52:01.200000Z,2025-01-12T16:14:49.200000Z,1.38,3,DIURNO,,
cumprida,2803,2,RV2025_OM8_E1_L17_C17,8,SBMN,SBMN,2025-01-08T10:35:00Z,2025-01-08T12:35:00Z,2.00,1,DIURNO,2803,17 cumprida,2800,4,SIM_019_SBMN_SWCA,19,SBMN,SWCA,2025-01-29T04:57:54Z,2025-01-29T06:36:54Z,1.65,1,DIURNO,,
cumprida,2803,3,RV2025_OM12_E4_L28_C29,12,SBMN,SBMN,2025-01-11T21:05:00Z,2025-01-12T00:00:00Z,2.92,1,DIURNO;NOTURNO,2809,28;29 cumprida,2800,5,SIM_020_SWCA_SBCO,20,SWCA,SBCO,2025-01-31T03:09:10.800000Z,2025-01-31T08:39:46.800000Z,5.51,2,DIURNO,,
cumprida,2803,4,RV2025_OM15_E1_L34_C34,15,SBMN,SBMN,2025-01-12T11:50:00Z,2025-01-12T13:30:00Z,1.67,1,DIURNO,2803,34 inspecao,2800,,INSP_2800_3,INSP 2A,SBMN,SBMN,2025-01-12T16:14:49.200000Z,2025-01-16T16:14:49.200000Z,96.00,,,,
cumprida,2809,1,RV2025_OM1_E1_L4_C4,1,SBMN,SBMN,2025-01-02T11:55:00Z,2025-01-02T13:05:00Z,1.17,1,NOTURNO,2809,4 cumprida,2803,1,SIM_006_SWBC_SBAN,6,SWBC,SBAN,2025-01-06T21:08:24Z,2025-01-07T00:56:24Z,3.80,2,DIURNO,,
cumprida,2811,1,RV2025_OM3_E1_L8_C8,3,SBMN,SBMN,2025-01-03T14:45:00Z,2025-01-03T16:10:00Z,1.42,1,DIURNO,2809,8 cumprida,2809,1,SIM_001_SBBE_SBMN,1,SBBE,SBMN,2025-01-02T07:53:34.800000Z,2025-01-02T11:07:22.800000Z,3.23,2,DIURNO,,
cumprida,2811,2,RV2025_OM4_E2_L10_C10,4,SBMN,SBMN,2025-01-06T15:40:00Z,2025-01-06T16:45:00Z,1.08,1,DIURNO,2803,10 cumprida,2809,2,SIM_002_SBPV_SBOI,2,SBPV,SBOI,2025-01-03T03:03:10.800000Z,2025-01-03T09:37:58.800000Z,6.58,3,DIURNO,,
cumprida,2811,3,RV2025_OM9_E1_L20_C20,9,SBMN,SBMN,2025-01-09T12:30:00Z,2025-01-09T15:55:00Z,3.42,1,DIURNO,2803,20 cumprida,2809,3,SIM_005_SBMY_SBYS,5,SBMY,SBYS,2025-01-04T19:37:19.200000Z,2025-01-05T01:06:43.200000Z,5.49,1,DIURNO,,
cumprida,2811,4,RV2025_OM9_E3_L22_C22,9,SBMN,SBMN,2025-01-09T20:40:00Z,2025-01-09T22:15:00Z,1.58,1,DIURNO,2803,22 cumprida,2809,4,SIM_008_SBPV_SBMN,8,SBPV,SBMN,2025-01-09T23:11:34.800000Z,2025-01-10T00:51:10.800000Z,1.66,2,DIURNO,,
cumprida,2811,5,RV2025_OM14_E1_L32_C32,14,SBMN,SBMN,2025-01-12T16:55:00Z,2025-01-12T20:25:00Z,3.50,1,DIURNO,2809,32 cumprida,2809,5,SIM_011_SBSN_SBVH,11,SBSN,SBVH,2025-01-12T01:26:24Z,2025-01-12T04:15:36Z,2.82,1,DIURNO,,
cumprida,2811,6,RV2025_OM17_E1_L40_C40,17,SBMN,SBMN,2025-01-15T13:30:00Z,2025-01-15T14:25:00Z,0.92,1,DIURNO,2809,40 cumprida,2809,6,SIM_016_SBTF_SBMN,16,SBTF,SBMN,2025-01-24T15:25:55.200000Z,2025-01-24T16:40:19.200000Z,1.24,3,DIURNO,,
cumprida,2811,7,RV2025_OM17_E3_L42_C45,17,SBMN,SBMN,2025-01-15T22:50:00Z,2025-01-15T23:50:00Z,1.00,1,NOTURNO,2809,42;43;44;45 inspecao,2809,,INSP_2809_3,INSP 2A,SBMN,SBMN,2025-01-05T01:06:43.200000Z,2025-01-09T01:06:43.200000Z,96.00,,,,
nao_cumprida,,,RV2025_OM2_E3_L6_C6,2,SBMN,SBMN,2025-01-02T17:00:00Z,2025-01-02T18:05:00Z,1.08,1,NOTURNO,2809,6 cumprida,2811,1,SIM_003_SBMN_SBUY,3,SBMN,SBUY,2025-01-03T09:50:20.400000Z,2025-01-03T16:17:56.400000Z,6.46,2,DIURNO,,
nao_cumprida,,,RV2025_OM2_E4_L7_C7,2,SBMN,SBMN,2025-01-02T20:00:00Z,2025-01-02T21:10:00Z,1.17,1,NOTURNO,2809,7 cumprida,2811,2,SIM_004_SWCA_SBMN,4,SWCA,SBMN,2025-01-04T03:01:19.200000Z,2025-01-04T04:51:43.200000Z,1.84,3,DIURNO,,
nao_cumprida,,,RV2025_OM5_E1_L11_C11,5,SBMN,SBMN,2025-01-06T19:10:00Z,2025-01-06T21:20:00Z,2.17,1,DIURNO,2803,11 cumprida,2811,3,SIM_009_SBUY_SBCC,9,SBUY,SBCC,2025-01-11T12:27:50.400000Z,2025-01-11T17:17:02.400000Z,4.82,1,DIURNO,,
nao_cumprida,,,RV2025_OM6_E1_L12_C12,6,SBMN,SBMN,2025-01-06T19:30:00Z,2025-01-06T21:25:00Z,1.92,1,DIURNO,2811,12 cumprida,2811,4,SIM_013_SBUY_SBMN,13,SBUY,SBMN,2025-01-15T01:13:04.800000Z,2025-01-15T07:31:40.800000Z,6.31,3,DIURNO,,
nao_cumprida,,,RV2025_OM6_E2_L14_C14,6,SBMN,SBMN,2025-01-07T11:20:00Z,2025-01-07T13:20:00Z,2.00,1,DIURNO,2811,14 cumprida,2811,5,SIM_014_SBBE_SBUA,14,SBBE,SBUA,2025-01-20T05:25:19.200000Z,2025-01-20T10:31:55.200000Z,5.11,2,DIURNO,,
nao_cumprida,,,RV2025_OM7_E2_L16_C16,7,SBMN,SBMN,2025-01-07T14:00:00Z,2025-01-07T15:45:00Z,1.75,1,DIURNO,2809,16 cumprida,2811,6,SIM_015_SBBE_SBTS,15,SBBE,SBTS,2025-01-21T05:22:01.200000Z,2025-01-21T10:28:37.200000Z,5.11,4,DIURNO,,
nao_cumprida,,,RV2025_OM8_E2_L18_C18,8,SBMN,SBMN,2025-01-08T14:00:00Z,2025-01-08T16:00:00Z,2.00,1,DIURNO,2803,18 cumprida,2811,7,SIM_017_SWBC_SBCC,17,SWBC,SBCC,2025-01-27T03:48:10.800000Z,2025-01-27T07:24:46.800000Z,3.61,2,DIURNO,,
nao_cumprida,,,RV2025_OM10_E1_L19_C19,10,SBMN,SBMN,2025-01-08T22:30:00Z,2025-01-08T23:15:00Z,0.75,1,NOTURNO,2803,19 cumprida,2811,8,SIM_018_SBMY_SBPV,18,SBMY,SBPV,2025-01-28T01:55:19.200000Z,2025-01-28T03:00:07.200000Z,1.08,2,DIURNO,,
nao_cumprida,,,RV2025_OM9_E2_L21_C21,9,SBMN,SBMN,2025-01-09T18:10:00Z,2025-01-09T20:00:00Z,1.83,1,DIURNO,2803,21
nao_cumprida,,,RV2025_OM11_E1_L23_C23,11,SBMN,SBMN,2025-01-10T12:10:00Z,2025-01-10T13:15:00Z,1.08,1,DIURNO,2803,23
nao_cumprida,,,RV2025_OM11_E2_L24_C24,11,SBMN,SBMN,2025-01-10T14:25:00Z,2025-01-10T15:25:00Z,1.00,1,DIURNO,2803,24
nao_cumprida,,,RV2025_OM13_E1_L30_C30,13,SBMN,SBMN,2025-01-12T12:20:00Z,2025-01-12T13:10:00Z,0.83,1,DIURNO,2809,30
nao_cumprida,,,RV2025_OM15_E2_L35_C35,15,SBMN,SBMN,2025-01-12T14:25:00Z,2025-01-12T16:00:00Z,1.58,1,DIURNO,2803,35
nao_cumprida,,,RV2025_OM13_E2_L31_C31,13,SBMN,SBMN,2025-01-12T14:40:00Z,2025-01-12T15:25:00Z,0.75,1,DIURNO,2809,31
nao_cumprida,,,RV2025_OM15_E3_L36_C36,15,SBMN,SBMN,2025-01-12T18:30:00Z,2025-01-12T20:35:00Z,2.08,1,DIURNO,2803,36
nao_cumprida,,,RV2025_OM14_E2_L33_C37,14,SBMN,SBMN,2025-01-12T22:45:00Z,2025-01-13T02:05:00Z,3.33,1,NOTURNO,2809,33;37
nao_cumprida,,,RV2025_OM16_E1_L38_C38,16,SBMN,SBMN,2025-01-15T12:00:00Z,2025-01-15T15:05:00Z,3.08,1,DIURNO,2803,38
nao_cumprida,,,RV2025_OM17_E2_L41_C41,17,SBMN,SBMN,2025-01-15T14:45:00Z,2025-01-15T15:40:00Z,0.92,1,DIURNO,2809,41
nao_cumprida,,,RV2025_OM16_E2_L39_C39,16,SBMN,SBMN,2025-01-15T17:35:00Z,2025-01-15T20:35:00Z,3.00,1,DIURNO,2803,39
nao_cumprida,,,RV2025_OM18_E1_L46_C46,18,SBMN,SBMN,2025-01-21T12:05:00Z,2025-01-21T13:40:00Z,1.58,1,DIURNO,2800,46
nao_cumprida,,,RV2025_OM18_E2_L47_C47,18,SBMN,SBMN,2025-01-21T14:45:00Z,2025-01-21T16:15:00Z,1.50,1,DIURNO,2800,47
nao_cumprida,,,RV2025_OM18_E3_L48_C48,18,SBMN,SBMN,2025-01-21T18:40:00Z,2025-01-21T20:40:00Z,2.00,1,DIURNO,2800,48
nao_cumprida,,,RV2025_OM19_E1_L49_C49,19,SBMN,SBMN,2025-01-22T14:55:00Z,2025-01-22T16:00:00Z,1.08,1,DIURNO,2800,49
nao_cumprida,,,RV2025_OM20_E1_L51_C51,20,SBMN,SBMN,2025-01-22T14:55:00Z,2025-01-22T16:00:00Z,1.08,1,DIURNO,2803,51
nao_cumprida,,,RV2025_OM21_E1_L53_C53,21,SBMN,SBMN,2025-01-22T14:55:00Z,2025-01-22T16:00:00Z,1.08,1,DIURNO,2809,53
nao_cumprida,,,RV2025_OM22_E1_L55_C55,22,SBMN,SBMN,2025-01-22T14:55:00Z,2025-01-22T15:55:00Z,1.00,1,DIURNO,2811,55
nao_cumprida,,,RV2025_OM19_E2_L50_C50,19,SBMN,SBMN,2025-01-22T20:50:00Z,2025-01-22T21:25:00Z,0.58,1,DIURNO,2800,50
nao_cumprida,,,RV2025_OM20_E2_L52_C52,20,SBMN,SBMN,2025-01-22T20:50:00Z,2025-01-22T21:25:00Z,0.58,1,DIURNO,2803,52
nao_cumprida,,,RV2025_OM21_E2_L54_C54,21,SBMN,SBMN,2025-01-22T20:50:00Z,2025-01-22T21:25:00Z,0.58,1,DIURNO,2809,54
nao_cumprida,,,RV2025_OM22_E2_L56_C56,22,SBMN,SBMN,2025-01-22T20:50:00Z,2025-01-22T21:25:00Z,0.58,1,DIURNO,2811,56
nao_cumprida,,,RV2025_OM27_E1_L68_C68,27,SBMN,SBMN,2025-01-28T12:05:00Z,2025-01-28T13:05:00Z,1.00,1,DIURNO,2803,68
nao_cumprida,,,RV2025_OM25_E1_L64_C64,25,SBMN,SBMN,2025-01-28T13:25:00Z,2025-01-28T14:30:00Z,1.08,1,DIURNO,2809,64
nao_cumprida,,,RV2025_OM27_E2_L69_C69,27,SBMN,SBMN,2025-01-28T14:35:00Z,2025-01-28T15:30:00Z,0.92,1,DIURNO,2803,69
nao_cumprida,,,RV2025_OM26_E1_L65_C70,26,SBMN,SBMN,2025-01-28T23:10:00Z,2025-01-29T00:50:00Z,1.67,1,NOTURNO,2809,65;66;67;70
nao_cumprida,,,RV2025_OM28_E1_L71_C71,28,SBMN,SBMN,2025-01-29T18:15:00Z,2025-01-29T19:25:00Z,1.17,1,DIURNO,2809,71
nao_cumprida,,,RV2025_OM29_E1_L72_C72,29,SBMN,SBMN,2025-01-29T22:50:00Z,2025-01-30T00:00:00Z,1.17,1,NOTURNO,2809,72
1 status aeronave ordem id om orig dest partida_utc chegada_utc dur_h prioridade periodo aeronave_real_2025 linhas_origem
2 cumprida 2800 1 RV2025_OM1_E2_L5_C5 SIM_007_SBMN_SBCO 1 7 SBMN SBMN SBCO 2025-01-02T14:30:00Z 2025-01-09T02:09:28.800000Z 2025-01-02T15:35:00Z 2025-01-09T07:58:40.800000Z 1.08 5.82 1 NOTURNO DIURNO 2809 5
3 cumprida 2800 2 RV2025_OM5_E2_L13_C13 SIM_010_SBCY_SBMN 5 10 SBMN SBCY SBMN 2025-01-07T14:15:00Z 2025-01-11T23:29:27.600000Z 2025-01-07T16:15:00Z 2025-01-12T03:09:39.600000Z 2.00 3.67 1 3 DIURNO 2803 13
4 cumprida 2803 2800 1 3 RV2025_OM7_E1_L15_C15 SIM_012_SWCA_SBTT 7 12 SBMN SWCA SBMN SBTT 2025-01-07T10:40:00Z 2025-01-12T14:52:01.200000Z 2025-01-07T12:30:00Z 2025-01-12T16:14:49.200000Z 1.83 1.38 1 3 DIURNO 2809 15
5 cumprida 2803 2800 2 4 RV2025_OM8_E1_L17_C17 SIM_019_SBMN_SWCA 8 19 SBMN SBMN SWCA 2025-01-08T10:35:00Z 2025-01-29T04:57:54Z 2025-01-08T12:35:00Z 2025-01-29T06:36:54Z 2.00 1.65 1 DIURNO 2803 17
6 cumprida 2803 2800 3 5 RV2025_OM12_E4_L28_C29 SIM_020_SWCA_SBCO 12 20 SBMN SWCA SBMN SBCO 2025-01-11T21:05:00Z 2025-01-31T03:09:10.800000Z 2025-01-12T00:00:00Z 2025-01-31T08:39:46.800000Z 2.92 5.51 1 2 DIURNO;NOTURNO DIURNO 2809 28;29
7 cumprida inspecao 2803 2800 4 RV2025_OM15_E1_L34_C34 INSP_2800_3 15 INSP 2A SBMN SBMN 2025-01-12T11:50:00Z 2025-01-12T16:14:49.200000Z 2025-01-12T13:30:00Z 2025-01-16T16:14:49.200000Z 1.67 96.00 1 DIURNO 2803 34
8 cumprida 2809 2803 1 RV2025_OM1_E1_L4_C4 SIM_006_SWBC_SBAN 1 6 SBMN SWBC SBMN SBAN 2025-01-02T11:55:00Z 2025-01-06T21:08:24Z 2025-01-02T13:05:00Z 2025-01-07T00:56:24Z 1.17 3.80 1 2 NOTURNO DIURNO 2809 4
9 cumprida 2811 2809 1 RV2025_OM3_E1_L8_C8 SIM_001_SBBE_SBMN 3 1 SBMN SBBE SBMN 2025-01-03T14:45:00Z 2025-01-02T07:53:34.800000Z 2025-01-03T16:10:00Z 2025-01-02T11:07:22.800000Z 1.42 3.23 1 2 DIURNO 2809 8
10 cumprida 2811 2809 2 RV2025_OM4_E2_L10_C10 SIM_002_SBPV_SBOI 4 2 SBMN SBPV SBMN SBOI 2025-01-06T15:40:00Z 2025-01-03T03:03:10.800000Z 2025-01-06T16:45:00Z 2025-01-03T09:37:58.800000Z 1.08 6.58 1 3 DIURNO 2803 10
11 cumprida 2811 2809 3 RV2025_OM9_E1_L20_C20 SIM_005_SBMY_SBYS 9 5 SBMN SBMY SBMN SBYS 2025-01-09T12:30:00Z 2025-01-04T19:37:19.200000Z 2025-01-09T15:55:00Z 2025-01-05T01:06:43.200000Z 3.42 5.49 1 DIURNO 2803 20
12 cumprida 2811 2809 4 RV2025_OM9_E3_L22_C22 SIM_008_SBPV_SBMN 9 8 SBMN SBPV SBMN 2025-01-09T20:40:00Z 2025-01-09T23:11:34.800000Z 2025-01-09T22:15:00Z 2025-01-10T00:51:10.800000Z 1.58 1.66 1 2 DIURNO 2803 22
13 cumprida 2811 2809 5 RV2025_OM14_E1_L32_C32 SIM_011_SBSN_SBVH 14 11 SBMN SBSN SBMN SBVH 2025-01-12T16:55:00Z 2025-01-12T01:26:24Z 2025-01-12T20:25:00Z 2025-01-12T04:15:36Z 3.50 2.82 1 DIURNO 2809 32
14 cumprida 2811 2809 6 RV2025_OM17_E1_L40_C40 SIM_016_SBTF_SBMN 17 16 SBMN SBTF SBMN 2025-01-15T13:30:00Z 2025-01-24T15:25:55.200000Z 2025-01-15T14:25:00Z 2025-01-24T16:40:19.200000Z 0.92 1.24 1 3 DIURNO 2809 40
15 cumprida inspecao 2811 2809 7 RV2025_OM17_E3_L42_C45 INSP_2809_3 17 INSP 2A SBMN SBMN 2025-01-15T22:50:00Z 2025-01-05T01:06:43.200000Z 2025-01-15T23:50:00Z 2025-01-09T01:06:43.200000Z 1.00 96.00 1 NOTURNO 2809 42;43;44;45
16 nao_cumprida cumprida 2811 1 RV2025_OM2_E3_L6_C6 SIM_003_SBMN_SBUY 2 3 SBMN SBMN SBUY 2025-01-02T17:00:00Z 2025-01-03T09:50:20.400000Z 2025-01-02T18:05:00Z 2025-01-03T16:17:56.400000Z 1.08 6.46 1 2 NOTURNO DIURNO 2809 6
17 nao_cumprida cumprida 2811 2 RV2025_OM2_E4_L7_C7 SIM_004_SWCA_SBMN 2 4 SBMN SWCA SBMN 2025-01-02T20:00:00Z 2025-01-04T03:01:19.200000Z 2025-01-02T21:10:00Z 2025-01-04T04:51:43.200000Z 1.17 1.84 1 3 NOTURNO DIURNO 2809 7
18 nao_cumprida cumprida 2811 3 RV2025_OM5_E1_L11_C11 SIM_009_SBUY_SBCC 5 9 SBMN SBUY SBMN SBCC 2025-01-06T19:10:00Z 2025-01-11T12:27:50.400000Z 2025-01-06T21:20:00Z 2025-01-11T17:17:02.400000Z 2.17 4.82 1 DIURNO 2803 11
19 nao_cumprida cumprida 2811 4 RV2025_OM6_E1_L12_C12 SIM_013_SBUY_SBMN 6 13 SBMN SBUY SBMN 2025-01-06T19:30:00Z 2025-01-15T01:13:04.800000Z 2025-01-06T21:25:00Z 2025-01-15T07:31:40.800000Z 1.92 6.31 1 3 DIURNO 2811 12
20 nao_cumprida cumprida 2811 5 RV2025_OM6_E2_L14_C14 SIM_014_SBBE_SBUA 6 14 SBMN SBBE SBMN SBUA 2025-01-07T11:20:00Z 2025-01-20T05:25:19.200000Z 2025-01-07T13:20:00Z 2025-01-20T10:31:55.200000Z 2.00 5.11 1 2 DIURNO 2811 14
21 nao_cumprida cumprida 2811 6 RV2025_OM7_E2_L16_C16 SIM_015_SBBE_SBTS 7 15 SBMN SBBE SBMN SBTS 2025-01-07T14:00:00Z 2025-01-21T05:22:01.200000Z 2025-01-07T15:45:00Z 2025-01-21T10:28:37.200000Z 1.75 5.11 1 4 DIURNO 2809 16
22 nao_cumprida cumprida 2811 7 RV2025_OM8_E2_L18_C18 SIM_017_SWBC_SBCC 8 17 SBMN SWBC SBMN SBCC 2025-01-08T14:00:00Z 2025-01-27T03:48:10.800000Z 2025-01-08T16:00:00Z 2025-01-27T07:24:46.800000Z 2.00 3.61 1 2 DIURNO 2803 18
23 nao_cumprida cumprida 2811 8 RV2025_OM10_E1_L19_C19 SIM_018_SBMY_SBPV 10 18 SBMN SBMY SBMN SBPV 2025-01-08T22:30:00Z 2025-01-28T01:55:19.200000Z 2025-01-08T23:15:00Z 2025-01-28T03:00:07.200000Z 0.75 1.08 1 2 NOTURNO DIURNO 2803 19
nao_cumprida RV2025_OM9_E2_L21_C21 9 SBMN SBMN 2025-01-09T18:10:00Z 2025-01-09T20:00:00Z 1.83 1 DIURNO 2803 21
nao_cumprida RV2025_OM11_E1_L23_C23 11 SBMN SBMN 2025-01-10T12:10:00Z 2025-01-10T13:15:00Z 1.08 1 DIURNO 2803 23
nao_cumprida RV2025_OM11_E2_L24_C24 11 SBMN SBMN 2025-01-10T14:25:00Z 2025-01-10T15:25:00Z 1.00 1 DIURNO 2803 24
nao_cumprida RV2025_OM13_E1_L30_C30 13 SBMN SBMN 2025-01-12T12:20:00Z 2025-01-12T13:10:00Z 0.83 1 DIURNO 2809 30
nao_cumprida RV2025_OM15_E2_L35_C35 15 SBMN SBMN 2025-01-12T14:25:00Z 2025-01-12T16:00:00Z 1.58 1 DIURNO 2803 35
nao_cumprida RV2025_OM13_E2_L31_C31 13 SBMN SBMN 2025-01-12T14:40:00Z 2025-01-12T15:25:00Z 0.75 1 DIURNO 2809 31
nao_cumprida RV2025_OM15_E3_L36_C36 15 SBMN SBMN 2025-01-12T18:30:00Z 2025-01-12T20:35:00Z 2.08 1 DIURNO 2803 36
nao_cumprida RV2025_OM14_E2_L33_C37 14 SBMN SBMN 2025-01-12T22:45:00Z 2025-01-13T02:05:00Z 3.33 1 NOTURNO 2809 33;37
nao_cumprida RV2025_OM16_E1_L38_C38 16 SBMN SBMN 2025-01-15T12:00:00Z 2025-01-15T15:05:00Z 3.08 1 DIURNO 2803 38
nao_cumprida RV2025_OM17_E2_L41_C41 17 SBMN SBMN 2025-01-15T14:45:00Z 2025-01-15T15:40:00Z 0.92 1 DIURNO 2809 41
nao_cumprida RV2025_OM16_E2_L39_C39 16 SBMN SBMN 2025-01-15T17:35:00Z 2025-01-15T20:35:00Z 3.00 1 DIURNO 2803 39
nao_cumprida RV2025_OM18_E1_L46_C46 18 SBMN SBMN 2025-01-21T12:05:00Z 2025-01-21T13:40:00Z 1.58 1 DIURNO 2800 46
nao_cumprida RV2025_OM18_E2_L47_C47 18 SBMN SBMN 2025-01-21T14:45:00Z 2025-01-21T16:15:00Z 1.50 1 DIURNO 2800 47
nao_cumprida RV2025_OM18_E3_L48_C48 18 SBMN SBMN 2025-01-21T18:40:00Z 2025-01-21T20:40:00Z 2.00 1 DIURNO 2800 48
nao_cumprida RV2025_OM19_E1_L49_C49 19 SBMN SBMN 2025-01-22T14:55:00Z 2025-01-22T16:00:00Z 1.08 1 DIURNO 2800 49
nao_cumprida RV2025_OM20_E1_L51_C51 20 SBMN SBMN 2025-01-22T14:55:00Z 2025-01-22T16:00:00Z 1.08 1 DIURNO 2803 51
nao_cumprida RV2025_OM21_E1_L53_C53 21 SBMN SBMN 2025-01-22T14:55:00Z 2025-01-22T16:00:00Z 1.08 1 DIURNO 2809 53
nao_cumprida RV2025_OM22_E1_L55_C55 22 SBMN SBMN 2025-01-22T14:55:00Z 2025-01-22T15:55:00Z 1.00 1 DIURNO 2811 55
nao_cumprida RV2025_OM19_E2_L50_C50 19 SBMN SBMN 2025-01-22T20:50:00Z 2025-01-22T21:25:00Z 0.58 1 DIURNO 2800 50
nao_cumprida RV2025_OM20_E2_L52_C52 20 SBMN SBMN 2025-01-22T20:50:00Z 2025-01-22T21:25:00Z 0.58 1 DIURNO 2803 52
nao_cumprida RV2025_OM21_E2_L54_C54 21 SBMN SBMN 2025-01-22T20:50:00Z 2025-01-22T21:25:00Z 0.58 1 DIURNO 2809 54
nao_cumprida RV2025_OM22_E2_L56_C56 22 SBMN SBMN 2025-01-22T20:50:00Z 2025-01-22T21:25:00Z 0.58 1 DIURNO 2811 56
nao_cumprida RV2025_OM27_E1_L68_C68 27 SBMN SBMN 2025-01-28T12:05:00Z 2025-01-28T13:05:00Z 1.00 1 DIURNO 2803 68
nao_cumprida RV2025_OM25_E1_L64_C64 25 SBMN SBMN 2025-01-28T13:25:00Z 2025-01-28T14:30:00Z 1.08 1 DIURNO 2809 64
nao_cumprida RV2025_OM27_E2_L69_C69 27 SBMN SBMN 2025-01-28T14:35:00Z 2025-01-28T15:30:00Z 0.92 1 DIURNO 2803 69
nao_cumprida RV2025_OM26_E1_L65_C70 26 SBMN SBMN 2025-01-28T23:10:00Z 2025-01-29T00:50:00Z 1.67 1 NOTURNO 2809 65;66;67;70
nao_cumprida RV2025_OM28_E1_L71_C71 28 SBMN SBMN 2025-01-29T18:15:00Z 2025-01-29T19:25:00Z 1.17 1 DIURNO 2809 71
nao_cumprida RV2025_OM29_E1_L72_C72 29 SBMN SBMN 2025-01-29T22:50:00Z 2025-01-30T00:00:00Z 1.17 1 NOTURNO 2809 72

View File

@@ -14,6 +14,31 @@ Formato:
--- ---
## v0.10 — VTO — 2026-06
### Alterado
- **Set Covering → Set Partitioning (C1):** a restrição de cobertura de missões foi alterada de `Σ_k y_entrada ≤ 1` (Set Covering — missão opcional) para `Σ_k y_entrada = 1` (Set Partitioning — toda missão obrigatória). Isso reflete a premissa operacional de que cada missão deve ser realizada por exatamente uma aeronave, sem possibilidade de missões descobertas. A distinção anterior entre missões "hard" (EVAM) e "opcionais" foi eliminada.
- **Número padrão de missões sintéticas reduzido de 50 para 20** (`--n-missoes`, `n_evam` de 5 para 4): Set Partitioning é mais restritivo e, com janelas de tempo fixas e restrições de inspeção simultâneas, 50 missões tornavam o MIP difícil de resolver ou inviável. Com 20 missões, a factibilidade é garantida na margem operacional de 4 aeronaves.
- **Time limit padrão do solver CBC fixado em 120 s** (era ilimitado). O solver agora nunca trava indefinidamente; retorna a melhor solução encontrada dentro do prazo. O CLI mantém `--time-limit` para ajuste.
- **Correção de display do status do solver:** PuLP redefinía `mdl.status` para "Not Solved" ao reutilizar o modelo em L2 (após `setObjective`). O status de L1 é agora preservado quando L2 reporta 0, evitando exibição enganosa.
- **Cenário `f0_cenario_insp_grande.json` revisado:** apenas a matrícula 2803 recebe `f0=595h` para INSP 2A (F_max=600h, downtime=4 dias), forçando a inspeção aparecer no Gantt. As demais matrículas partem com `f0=0` (plena disponibilidade), evitando que o problema se torne inviável por múltiplos relógios apertados simultaneamente.
### Adicionado
- `db/processed/planejamento_insp_grande.html`: visualização Gantt + mapa + tabela para o cenário com inspeção de 4 dias (INSP 2A — INSP CHECK 2A, 96 h de solo).
### Limitações conhecidas (para contribuidores)
1. **Escopo validado restrito:** Set Partitioning foi validado apenas para o cenário sintético com 20 missões, seed 42, 4 aeronaves e somente uma matrícula (2803) com LRT apertado. Fora desse envelope, o MIP pode ser infeasible ou ultrapassar o time limit sem solução.
2. **Missões sintéticas incluem bases fora da Amazônia** (SBBR, SBGL, SBYS etc.) geradas aleatoriamente pelo seed. Para estudos de caso realistas, o gerador deve ser restrito ao conjunto de bases do Esquadrão Arara.
3. **Folga negativa no relatório:** quando uma aeronave realiza inspeção dentro do horizonte, o campo `folga` exibido (`LRT_ini horas_celula_total`) pode ser negativo porque não desconta o reset do relógio pós-inspeção. Isso é um bug de display; a restrição `H ≤ F_max` está correta no modelo.
4. **LRT muito curto (< 5 h) pode gerar instabilidade numérica no big-M:** o cenário foi configurado com LRT_ini=5 h para 2803, que está próximo do limiar onde o big-M deixa de ser eficaz. Valores menores podem permitir que o solver ignore a restrição por tolerância numérica (ε ≈ 1 e-4 do CBC).
5. **Aeronave 2811 fica ociosa neste cenário:** o solver atribui todas as 20 missões a 3 aeronaves. Isso é matematicamente ótimo, mas pode não ser desejável operacionalmente. Considerar restrição de equilíbrio de carga em versões futuras.
6. **Status "Not Solved" após L2:** a correção atual usa o status de L1 como proxy. Se L1 retornar apenas uma solução feasível (não ótima, por time limit), o modelo pode ser reportado como "Optimal" quando não é. Monitorar o campo `prio_otima` para validar.
---
## v0.2 — VTO — 2026-06 ## v0.2 — VTO — 2026-06
### Adicionado ### Adicionado
@@ -87,6 +112,43 @@ Formato:
- A remocao foi aplicada no script de pre-processamento para manter reprodutibilidade. - A remocao foi aplicada no script de pre-processamento para manter reprodutibilidade.
## v0.9 — VTO — 2026-06
### Adicionado
- Flag `--escala` na CLI: carrega missões reais da Escala de Voo Modelo 1 (CSV com separador `;`), com parser de data DD/MMM, horários UTC e prioridade derivada do código de missão (69TV→1, 50TT/05TF→3).
- `BASES_COORDS` expandido de 10 para 26 bases (inclui SBMQ, SBOI, SBBR, SBGL, SBCY, SWEI, SWCA, SWKO, SBUY e outras rotas reais).
- Cenários de f0 em JSON: `f0_cenario_esquadrao.json` (dados realistas do esquadrão) e `f0_cenario_insp_grande.json`.
### Corrigido
- Gantt: FAB 2800 (y=0) estava invisível — corrigido com `yaxis range=[-0.5, n-0.5]`.
- Gantt: barras não renderizavam (`shapes type="rect"`) — substituídas por `go.Scatter mode="lines"` que renderiza garantido no eixo de datas.
- Largura mínima de 2h por barra no Gantt para voos curtos ficarem visíveis.
### Alterado
- `--time-limit` default alterado de 60s para 300s (5 minutos).
- Imports `timedelta` e `date` consolidados no topo do módulo (era importado localmente em funções).
### Resultado validado
Rodada `--sintetico --n-missoes 30 --f0 f0_cenario_esquadrao.json` → 30/30 Optimal; FAB 2809 com 1 inspeção (CHECK 300) visível no Gantt.
---
## v0.8 — VTO — 2026-06
### Adicionado
- Visualização combinada em HTML único: Gantt (Plotly) + mapa (Folium em iframe) + tabela de voos com filtros JS interativos.
- Título "Planejamento de Diagonal de Manutenção" no HTML gerado.
- Exportação de inspeções no CSV de resultado com timestamps UTC reais.
- Filtros na tabela: aeronave, status, prioridade, base e busca livre; ordenação por coluna.
- Flag `--time-limit` na CLI do solver (padrão era 60s).
---
## v0.7 — VTO — 2026-06 ## v0.7 — VTO — 2026-06
### Corrigido ### Corrigido

View File

@@ -43,7 +43,7 @@ import csv
import json import json
import random import random
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import datetime, timezone from datetime import date, datetime, timedelta, timezone
from pathlib import Path from pathlib import Path
from typing import Literal, Optional from typing import Literal, Optional
@@ -428,6 +428,7 @@ def resolver(
arcos: list[Arco], arcos: list[Arco],
aeronaves: list[Aeronave], aeronaves: list[Aeronave],
catalogo: dict[int, InspecaoParam], catalogo: dict[int, InspecaoParam],
**kwargs,
) -> dict: ) -> dict:
ks = [a.id for a in aeronaves] ks = [a.id for a in aeronaves]
acft = {a.id: a for a in aeronaves} acft = {a.id: a for a in aeronaves}
@@ -473,9 +474,7 @@ def resolver(
y[(k, a.id)] for a in arcs if (k, a.id) in y y[(k, a.id)] for a in arcs if (k, a.id) in y
) )
# --- C1: Cobertura e definição de z --- # --- C1: Cobertura — Set Partitioning: toda missão coberta por exatamente 1 aeronave ---
# Missões normais: Σ_k y_entrada ≤ 1 (cobertura opcional)
# Missões obrigatórias (M_obr / EVAM): Σ_k y_entrada == 1
for nid, n in missoes_nos.items(): for nid, n in missoes_nos.items():
fluxo_entrada = pulp.lpSum( fluxo_entrada = pulp.lpSum(
y[(k, a.id)] y[(k, a.id)]
@@ -484,10 +483,7 @@ def resolver(
if (k, a.id) in y if (k, a.id) in y
) )
mdl += fluxo_entrada == z[nid], f"def_z_{nid}" mdl += fluxo_entrada == z[nid], f"def_z_{nid}"
if n.missao.obrigatoria: mdl += z[nid] == 1, f"part_{nid}"
mdl += z[nid] == 1, f"hard_{nid}"
else:
mdl += z[nid] <= 1, f"cob_{nid}"
# --- C2: Conservação de fluxo por (aeronave, nó) --- # --- C2: Conservação de fluxo por (aeronave, nó) ---
for k in ks: for k in ks:
@@ -675,13 +671,13 @@ def resolver(
# já impõe o limite LRT por ciclo corretamente. A versão simplificada somava # já impõe o limite LRT por ciclo corretamente. A versão simplificada somava
# horas totais sem respeitar resets de inspeção, super-restringindo o modelo. # horas totais sem respeitar resets de inspeção, super-restringindo o modelo.
solver = pulp.PULP_CBC_CMD(msg=False) time_limit = kwargs.get("time_limit", 120)
solver = pulp.PULP_CBC_CMD(msg=False, timeLimit=time_limit)
mdl.solve(solver) mdl.solve(solver)
status_l1 = mdl.status
prio_otima = int(round(pulp.value(mdl.objective) or 0)) prio_otima = int(round(pulp.value(mdl.objective) or 0))
# L2: com prioridade fixada, minimiza folga de horas ao fim do horizonte. # L2: com prioridade fixada, maximiza horas voadas.
# folga_k = orcamento_k horas_voadas_k
# Minimizar Σ_k folga_k ⟺ maximizar Σ_k horas_voadas_k (orcamento é constante).
mdl += ( mdl += (
pulp.lpSum(n.missao.prioridade * z[nid] for nid, n in missoes_nos.items()) pulp.lpSum(n.missao.prioridade * z[nid] for nid, n in missoes_nos.items())
== prio_otima, == prio_otima,
@@ -696,7 +692,10 @@ def resolver(
) )
mdl.sense = pulp.LpMaximize mdl.sense = pulp.LpMaximize
mdl.setObjective(horas_voadas) mdl.setObjective(horas_voadas)
mdl.solve(solver) mdl.solve(pulp.PULP_CBC_CMD(msg=False, timeLimit=time_limit))
# PuLP redefine mdl.status após setObjective; preservar o status real (L1).
if mdl.status == 0 and status_l1 == 1:
mdl.status = status_l1
return { return {
"modelo": mdl, "modelo": mdl,
@@ -787,6 +786,8 @@ def escrever_resultado(sol: dict, caminho: Path) -> None:
missoes_nos = sol["missoes_nos"] missoes_nos = sol["missoes_nos"]
linhas = [] linhas = []
horizonte_dt = sol.get("horizonte_dt")
cobertas_ids: set[str] = set() cobertas_ids: set[str] = set()
for k in sol["ks"]: for k in sol["ks"]:
missoes_rota = [n for n in rota_da_aeronave(sol, k) if n.tipo == "missao"] missoes_rota = [n for n in rota_da_aeronave(sol, k) if n.tipo == "missao"]
@@ -810,6 +811,29 @@ def escrever_resultado(sol: dict, caminho: Path) -> None:
"linhas_origem": m.linhas_origem, "linhas_origem": m.linhas_origem,
}) })
# Inspeções executadas por esta aeronave
if horizonte_dt is not None:
for seq, s_val, insp in inspecoes_da_aeronave(sol, k):
ini_dt = horizonte_dt + timedelta(hours=s_val)
fim_dt = horizonte_dt + timedelta(hours=s_val + insp.dt_h)
linhas.append({
"status": "inspecao",
"aeronave": k,
"ordem": "",
"id": f"INSP_{k}_{seq}",
"om": insp.sigla,
"orig": "SBMN",
"dest": "SBMN",
"partida_utc": ini_dt.isoformat().replace("+00:00", "Z"),
"chegada_utc": fim_dt.isoformat().replace("+00:00", "Z"),
"dur_h": f"{insp.dt_h:.2f}",
"prioridade": "",
"periodo": "",
"aeronave_real_2025": "",
"linhas_origem": "",
})
for nid, n in missoes_nos.items(): for nid, n in missoes_nos.items():
if nid in cobertas_ids: if nid in cobertas_ids:
continue continue
@@ -1133,8 +1157,9 @@ def derivar_bases(
# Missões sintéticas # Missões sintéticas
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Coordenadas (lat, lon) das 10 bases operacionais do C-105 # Coordenadas (lat, lon) das bases operacionais do C-105
BASES_COORDS: dict[str, tuple[float, float]] = { BASES_COORDS: dict[str, tuple[float, float]] = {
# Amazônia (bases primárias)
"SBMN": (-3.15, -59.99), # Manaus — Ponta Pelada (hub) "SBMN": (-3.15, -59.99), # Manaus — Ponta Pelada (hub)
"SBBE": (-1.38, -48.48), # Belém "SBBE": (-1.38, -48.48), # Belém
"SBBV": ( 2.84, -60.69), # Boa Vista "SBBV": ( 2.84, -60.69), # Boa Vista
@@ -1145,6 +1170,24 @@ BASES_COORDS: dict[str, tuple[float, float]] = {
"SBUA": (-0.15, -67.05), # São Gabriel da Cachoeira "SBUA": (-0.15, -67.05), # São Gabriel da Cachoeira
"SWBC": (-0.98, -62.92), # Barcelos "SWBC": (-0.98, -62.92), # Barcelos
"SBMY": (-5.81, -61.28), # Manicoré "SBMY": (-5.81, -61.28), # Manicoré
# Bases adicionais (escala de voo real)
"SBMQ": ( 0.05, -51.07), # Macapá
"SBOI": (-3.77, -38.53), # Fortaleza (Pinto Martins)
"SBVH": (-12.69, -60.10), # Vilhena
"SBBR": (-15.87, -47.92), # Brasília
"SBGL": (-22.81, -43.25), # Rio de Janeiro (Galeão)
"SBCY": (-15.65, -56.12), # Cuiabá
"SBCC": (-10.86, -51.80), # Canarana
"SBYS": (-23.00, -49.71), # Ourinhos
"SBCO": (-22.17, -47.88), # Cosmópolis
"SBSM": (-29.71, -53.69), # Santa Maria
"SBLO": (-23.33, -51.13), # Londrina
"SBAN": (-15.59, -56.10), # Anápolis
"SWEI": (-7.63, -72.68), # Eirunepé
"SWCA": (-8.85, -63.85), # Cacoal
"SWKO": (-11.50, -61.45), # Rolim de Moura
"SBTF": (-3.38, -64.72), # Tefé (alias SBTT)
"SBUY": (-29.78, -57.04), # Uruguaiana
} }
_VELOCIDADE_CRUZEIRO_KMH = 430.0 # C-105 Spartan (típico) _VELOCIDADE_CRUZEIRO_KMH = 430.0 # C-105 Spartan (típico)
@@ -1162,6 +1205,103 @@ def _haversine_km(b1: str, b2: str) -> float:
return R * 2 * math.asin(math.sqrt(a)) return R * 2 * math.asin(math.sqrt(a))
def carregar_escala(caminho: Path, inicio: datetime) -> tuple[list[Missao], datetime]:
"""
Lê a Escala de Voo Modelo 1 (CSV com separador ';') e retorna missões para o solver.
Formato esperado (2 linhas de cabeçalho):
DATA ; ETAPA ; DEP ; ARR ; HOR_DEP ; HOR_ARR ; DUR ; SEGMTO ; MISSÃO ; OFRAG
Cada linha vira uma Missao. Bases sem coordenadas conhecidas são descartadas.
A prioridade é derivada do tipo de missão:
69TV → prio 1 | 50TT/05TF → prio 3 | outros → prio 2
"""
_MES = {
"jan": 1, "fev": 2, "mar": 3, "abr": 4, "mai": 5, "jun": 6,
"jul": 7, "ago": 8, "set": 9, "out": 10, "nov": 11, "dez": 12,
}
def _parse_hora(s: str) -> tuple[int, int]:
parts = s.strip().split(":")
return int(parts[0]) % 24, int(parts[1])
def _prio(missao_id: str) -> int:
m = missao_id.upper()
if "69TV" in m:
return 1
if any(x in m for x in ("50TT", "05TF", "TT", "TF")):
return 3
return 2
missoes: list[Missao] = []
ano = inicio.year
data_atual: date | None = None
with caminho.open(newline="", encoding="latin-1") as f:
reader = csv.reader(f, delimiter=";")
next(reader) # cabeçalho 1
next(reader) # cabeçalho 2
for i, row in enumerate(reader):
if len(row) < 10:
continue
data_str, _, dep, arr, h_dep, h_arr, _, _, missao_id, ofrag = (
row[0].strip(), row[1].strip(), row[2].strip(), row[3].strip(),
row[4].strip(), row[5].strip(), row[6].strip(), row[7].strip(),
row[8].strip(), row[9].strip(),
)
if data_str:
partes = data_str.split("/")
if len(partes) == 2:
data_atual = date(ano, _MES.get(partes[1].lower(), 1), int(partes[0]))
if data_atual is None or not dep or not arr:
continue
if dep not in BASES_COORDS or arr not in BASES_COORDS:
continue
try:
hd, md = _parse_hora(h_dep)
ha, ma = _parse_hora(h_arr)
except Exception:
continue
t_dep_dt = datetime(data_atual.year, data_atual.month, data_atual.day,
hd, md, tzinfo=timezone.utc)
t_arr_dt = datetime(data_atual.year, data_atual.month, data_atual.day,
ha, ma, tzinfo=timezone.utc)
if t_arr_dt <= t_dep_dt: # chegada no dia seguinte
t_arr_dt += timedelta(days=1)
t_dep_h = (t_dep_dt - inicio).total_seconds() / 3600
t_arr_h = (t_arr_dt - inicio).total_seconds() / 3600
if t_dep_h < 0 or t_arr_h <= t_dep_h:
continue
missoes.append(Missao(
id=f"ESC_{i:03d}_{dep}_{arr}",
om=ofrag,
orig=dep,
dest=arr,
t_dep=t_dep_h,
t_arr=t_arr_h,
dur_h=t_arr_h - t_dep_h,
prioridade=_prio(missao_id),
obrigatoria=False,
partida_utc=t_dep_dt,
chegada_utc=t_arr_dt,
periodo="",
linhas_origem="",
aeronave_real="",
codigo_missao=missao_id,
))
if not missoes:
raise ValueError(f"Nenhuma missão válida carregada de {caminho}")
horizonte_dt = inicio
return missoes, horizonte_dt
TIPOS_MISSAO = ["Logística", "Médico", "Pessoal", "Reabastecimento", "Reconhecimento"] TIPOS_MISSAO = ["Logística", "Médico", "Pessoal", "Reabastecimento", "Reconhecimento"]
# Pesos de prioridade: prio 1 (urgente) é mais frequente que prio 5 (baixa) # Pesos de prioridade: prio 1 (urgente) é mais frequente que prio 5 (baixa)
@@ -1169,11 +1309,11 @@ _PESOS_PRIO = [35, 30, 20, 10, 5]
def gerar_missoes_sinteticas( def gerar_missoes_sinteticas(
n: int = 50, n: int = 20,
seed: Optional[int] = 42, seed: Optional[int] = 42,
inicio: Optional[datetime] = None, inicio: Optional[datetime] = None,
horizonte_h: float = 744.0, # janeiro = 31 × 24 horizonte_h: float = 744.0, # janeiro = 31 × 24
n_evam: int = 5, n_evam: int = 4,
bases: Optional[list[str]] = None, bases: Optional[list[str]] = None,
ofrag_path: Optional[Path] = None, ofrag_path: Optional[Path] = None,
) -> tuple[list[Missao], datetime]: ) -> tuple[list[Missao], datetime]:
@@ -1346,30 +1486,49 @@ def main() -> None:
"--hard-pattern", default=HARD_MISSAO_PADRAO, "--hard-pattern", default=HARD_MISSAO_PADRAO,
help="Regex no campo 'missao' do CSV que marca missões obrigatórias (Q2).", help="Regex no campo 'missao' do CSV que marca missões obrigatórias (Q2).",
) )
parser.add_argument(
"--escala", type=Path, default=None, metavar="CSV",
help="Escala de Voo Modelo 1 (CSV ';') com missões reais.",
)
parser.add_argument( parser.add_argument(
"--sintetico", action="store_true", "--sintetico", action="store_true",
help="Usa missões sintéticas cross-base em vez do CSV real.", help="Usa missões sintéticas cross-base em vez do CSV real.",
) )
parser.add_argument( parser.add_argument(
"--n-missoes", type=int, default=50, "--n-missoes", type=int, default=20,
help="Número de missões sintéticas a gerar (padrão: 50).", help="Número de missões sintéticas a gerar (padrão: 20).",
) )
parser.add_argument( parser.add_argument(
"--n-evam", type=int, default=5, "--n-evam", type=int, default=4,
help="Missões obrigatórias (EVAM/hard) nas sintéticas (padrão: 5).", help="Missões obrigatórias (EVAM/hard) nas sintéticas (padrão: 4).",
) )
parser.add_argument( parser.add_argument(
"--ofrag", type=Path, default=None, metavar="CSV", "--ofrag", type=Path, default=None, metavar="CSV",
help="CSV de OFRAG com prioridades por missão (missao_id, prioridade).", help="CSV de OFRAG com prioridades por missão (missao_id, prioridade).",
) )
parser.add_argument(
"--time-limit", type=int, default=120, dest="time_limit",
help="Limite de tempo em segundos para o solver CBC (padrão: 120).",
)
parser.add_argument(
"--insp-seqs", type=int, nargs="+", default=None, dest="insp_seqs",
metavar="SEQ",
help="Filtra catálogo para apenas estes seq de inspeção (ex: --insp-seqs 3).",
)
args = parser.parse_args() args = parser.parse_args()
inicio = datetime.fromisoformat(args.inicio).replace(tzinfo=timezone.utc) inicio = datetime.fromisoformat(args.inicio).replace(tzinfo=timezone.utc)
fim = datetime.fromisoformat(args.fim).replace(tzinfo=timezone.utc) fim = datetime.fromisoformat(args.fim).replace(tzinfo=timezone.utc)
catalogo = carregar_inspecoes(args.inspecoes) catalogo = carregar_inspecoes(args.inspecoes)
if args.insp_seqs:
catalogo = {s: v for s, v in catalogo.items() if s in args.insp_seqs}
if args.sintetico: if args.escala:
missoes, horizonte_dt = carregar_escala(args.escala, inicio)
bases = sorted({m.orig for m in missoes} | {m.dest for m in missoes})
print(f" Escala de voo: {len(missoes)} missões carregadas de {args.escala.name}")
elif args.sintetico:
horizonte_h = (fim - inicio).total_seconds() / 3600 horizonte_h = (fim - inicio).total_seconds() / 3600
bases = list(BASES_COORDS.keys()) bases = list(BASES_COORDS.keys())
missoes, horizonte_dt = gerar_missoes_sinteticas( missoes, horizonte_dt = gerar_missoes_sinteticas(
@@ -1405,7 +1564,8 @@ def main() -> None:
] ]
nos, arcos = construir_rede(missoes, aeronaves, catalogo) nos, arcos = construir_rede(missoes, aeronaves, catalogo)
sol = resolver(nos, arcos, aeronaves, catalogo) sol = resolver(nos, arcos, aeronaves, catalogo, time_limit=args.time_limit)
sol["horizonte_dt"] = horizonte_dt
escrever_resultado(sol, args.resultado) escrever_resultado(sol, args.resultado)
imprimir_relatorio(sol, bases, args.resultado) imprimir_relatorio(sol, bases, args.resultado)

View File

@@ -1,29 +1,29 @@
""" """
Visualização interativa do resultado OAMRP — Esquadrão Arara C-105. Visualização interativa do resultado OAMRP — Esquadrão Arara C-105.
Gera um mapa HTML (Folium) com: Gera um único HTML com título "Planejamento de Diagonal de Manutenção" contendo:
- Marcadores para cada base operacional - Gantt (Plotly): calendário aeronave × tempo com voos e inspeções
- Rotas por aeronave (cor distinta por EVAM) - Mapa (Folium): rotas por aeronave, missões não cumpridas, bases
- Missões não cumpridas em cinza tracejado
- Popup com detalhes de cada missão
Uso: Uso:
python software/visualizar_resultado.py \ python software/visualizar_resultado.py \
--resultado db/processed/resultado_oamrp_sintetico.csv \ --resultado db/processed/resultado_oamrp_sintetico.csv \
--saida db/processed/mapa_rotas.html --saida db/processed/planejamento.html
""" """
import argparse import argparse
import csv import csv
import io
from datetime import datetime, timedelta, timezone
from html import escape
from pathlib import Path from pathlib import Path
import folium import folium
from folium import plugins
BASE_DIR = Path(__file__).resolve().parents[1] BASE_DIR = Path(__file__).resolve().parents[1]
RESULTADO_PADRAO = BASE_DIR / "db" / "processed" / "resultado_oamrp_sintetico.csv" RESULTADO_PADRAO = BASE_DIR / "db" / "processed" / "resultado_oamrp_sintetico.csv"
MAPA_PADRAO = BASE_DIR / "db" / "processed" / "mapa_rotas.html" SAIDA_PADRAO = BASE_DIR / "db" / "processed" / "planejamento.html"
# Coordenadas das bases (lat, lon) # Coordenadas das bases (lat, lon)
BASES_COORDS: dict[str, tuple[float, float]] = { BASES_COORDS: dict[str, tuple[float, float]] = {
@@ -39,7 +39,6 @@ BASES_COORDS: dict[str, tuple[float, float]] = {
"SBMY": (-5.81, -61.28), "SBMY": (-5.81, -61.28),
} }
# Nomes legíveis das bases
BASES_NOMES = { BASES_NOMES = {
"SBMN": "Manaus / Ponta Pelada (Hub)", "SBMN": "Manaus / Ponta Pelada (Hub)",
"SBBE": "Belém", "SBBE": "Belém",
@@ -53,21 +52,39 @@ BASES_NOMES = {
"SBMY": "Manicoré", "SBMY": "Manicoré",
} }
# Paleta de cores por aeronave
CORES_AERONAVE = { CORES_AERONAVE = {
"2800": "#1f77b4", # azul "2800": "#1f77b4",
"2803": "#2ca02c", # verde "2803": "#2ca02c",
"2809": "#d62728", # vermelho "2809": "#d62728",
"2811": "#ff7f0e", # laranja "2811": "#ff7f0e",
} }
COR_NAO_CUMPRIDA = "#aaaaaa" COR_NAO_CUMPRIDA = "#aaaaaa"
COR_HUB = "#d62728"
COR_BASE = "#1f77b4" _GANTT_CORES = {
"cumprida": {
"1": "#d62728",
"2": "#ff7f0e",
"3": "#2ca02c",
"4": "#1f77b4",
"5": "#9467bd",
"": "#1f77b4",
},
"inspecao": "#8c564b",
"nao_cumprida": "#aaaaaa",
}
def _cor_prio(prio: str) -> str: def _cor_prio(prio: str) -> str:
mapa = {"1": "red", "2": "orange", "3": "yellow", "4": "lightblue", "5": "lightgray"} return {"1": "red", "2": "orange", "3": "yellow", "4": "lightblue", "5": "lightgray"}.get(str(prio), "white")
return mapa.get(str(prio), "white")
def _cor_gantt(linha: dict) -> str:
status = linha["status"]
if status == "inspecao":
return _GANTT_CORES["inspecao"]
if status == "nao_cumprida":
return _GANTT_CORES["nao_cumprida"]
return _GANTT_CORES["cumprida"].get(linha.get("prioridade", ""), "#1f77b4")
def carregar_resultado(caminho: Path) -> list[dict]: def carregar_resultado(caminho: Path) -> list[dict]:
@@ -75,57 +92,49 @@ def carregar_resultado(caminho: Path) -> list[dict]:
return list(csv.DictReader(f)) return list(csv.DictReader(f))
def adicionar_bases(mapa: folium.Map) -> None: def _resumir_resultado(linhas: list[dict]) -> None:
for icao, (lat, lon) in BASES_COORDS.items(): cumpridas = sum(1 for r in linhas if r["status"] == "cumprida")
cor = COR_HUB if icao == "SBMN" else COR_BASE total_missoes = sum(1 for r in linhas if r["status"] in ("cumprida", "nao_cumprida"))
icone = folium.Icon(color="red" if icao == "SBMN" else "blue", icon="plane", prefix="fa") print(f" Missões: {cumpridas}/{total_missoes} cumpridas")
folium.Marker( aeronaves: dict[str, int] = {}
location=[lat, lon], for r in linhas:
tooltip=f"<b>{icao}</b><br>{BASES_NOMES.get(icao, '')}", if r["aeronave"] and r["status"] == "cumprida":
popup=folium.Popup(f"<b>{icao}</b><br>{BASES_NOMES.get(icao, '')}", max_width=200), aeronaves.setdefault(r["aeronave"], 0)
icon=icone, aeronaves[r["aeronave"]] += 1
).add_to(mapa) for k, c in sorted(aeronaves.items()):
print(f" FAB {k}: {c} missões")
def gerar_mapa(resultado: Path, saida: Path) -> None: # ---------------------------------------------------------------------------
linhas = carregar_resultado(resultado) # Mapa Folium → string HTML
# ---------------------------------------------------------------------------
mapa = folium.Map( def _construir_mapa(linhas: list[dict]) -> str:
location=[-3.5, -62.0], mapa = folium.Map(location=[-3.5, -62.0], zoom_start=5, tiles="CartoDB positron")
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"]}) aeronaves_vistas = sorted({r["aeronave"] for r in linhas if r["aeronave"]})
grupos: dict[str, folium.FeatureGroup] = {} grupos: dict[str, folium.FeatureGroup] = {}
for k in aeronaves_vistas: for k in aeronaves_vistas:
grupos[k] = folium.FeatureGroup(name=f"EVAM {k}", show=True) grupos[k] = folium.FeatureGroup(name=f"FAB {k}", show=True)
grupos["nao_cumprida"] = folium.FeatureGroup(name="Não cumpridas", show=True) grupos["nao_cumprida"] = folium.FeatureGroup(name="Não cumpridas", show=True)
cumpridas = [r for r in linhas if r["status"] == "cumprida"] for r in linhas:
nao_cumpridas = [r for r in linhas if r["status"] != "cumprida"] if r["status"] != "cumprida":
continue
# Rotas cumpridas orig, dest = r["orig"], r["dest"]
for r in cumpridas:
orig = r["orig"]
dest = r["dest"]
if orig not in BASES_COORDS or dest not in BASES_COORDS: if orig not in BASES_COORDS or dest not in BASES_COORDS:
continue continue
lat_o, lon_o = BASES_COORDS[orig]
lat_d, lon_d = BASES_COORDS[dest]
k = r["aeronave"] k = r["aeronave"]
cor = CORES_AERONAVE.get(k, "#333333") cor = CORES_AERONAVE.get(k, "#333333")
lat_o, lon_o = BASES_COORDS[orig]
lat_d, lon_d = BASES_COORDS[dest]
# Linha de rota
folium.PolyLine( folium.PolyLine(
locations=[[lat_o, lon_o], [lat_d, lon_d]], locations=[[lat_o, lon_o], [lat_d, lon_d]],
color=cor, color=cor, weight=2.5, opacity=0.8,
weight=2.5, tooltip=f"FAB {k} | {orig}{dest} | OM {r['om']} | prio {r['prioridade']}",
opacity=0.8,
tooltip=f"EVAM {k} | {orig}{dest} | OM {r['om']} | prio {r['prioridade']}",
popup=folium.Popup( popup=folium.Popup(
f"<b>EVAM {k}</b> — Missão #{r['ordem']}<br>" f"<b>FAB {k}</b> — Missão #{r['ordem']}<br>"
f"{orig}{dest}<br>" f"{orig}{dest}<br>"
f"OM: {r['om']} | Prio: {r['prioridade']}<br>" f"OM: {r['om']} | Prio: {r['prioridade']}<br>"
f"Partida: {r['partida_utc'][:16].replace('T',' ')}Z<br>" f"Partida: {r['partida_utc'][:16].replace('T',' ')}Z<br>"
@@ -134,43 +143,38 @@ def gerar_mapa(resultado: Path, saida: Path) -> None:
max_width=250, max_width=250,
), ),
).add_to(grupos[k]) ).add_to(grupos[k])
# Marcador de origem (pequeno círculo)
folium.CircleMarker( folium.CircleMarker(
location=[lat_o, lon_o], location=[lat_o, lon_o], radius=4, color=cor,
radius=4, fill=True, fill_color=_cor_prio(r["prioridade"]), fill_opacity=0.9,
color=cor,
fill=True,
fill_color=_cor_prio(r["prioridade"]),
fill_opacity=0.9,
tooltip=f"{orig} | prio {r['prioridade']}", tooltip=f"{orig} | prio {r['prioridade']}",
).add_to(grupos[k]) ).add_to(grupos[k])
# Missões não cumpridas (linha tracejada cinza) for r in linhas:
for r in nao_cumpridas: if r["status"] == "nao_cumprida":
orig = r["orig"] orig, dest = r["orig"], r["dest"]
dest = r["dest"]
if orig not in BASES_COORDS or dest not in BASES_COORDS: if orig not in BASES_COORDS or dest not in BASES_COORDS:
continue continue
lat_o, lon_o = BASES_COORDS[orig] lat_o, lon_o = BASES_COORDS[orig]
lat_d, lon_d = BASES_COORDS[dest] lat_d, lon_d = BASES_COORDS[dest]
folium.PolyLine( folium.PolyLine(
locations=[[lat_o, lon_o], [lat_d, lon_d]], locations=[[lat_o, lon_o], [lat_d, lon_d]],
color=COR_NAO_CUMPRIDA, color=COR_NAO_CUMPRIDA, weight=1.5, opacity=0.5, dash_array="6 4",
weight=1.5,
opacity=0.5,
dash_array="6 4",
tooltip=f"NÃO CUMPRIDA | {orig}{dest} | OM {r['om']} | prio {r['prioridade']}", tooltip=f"NÃO CUMPRIDA | {orig}{dest} | OM {r['om']} | prio {r['prioridade']}",
).add_to(grupos["nao_cumprida"]) ).add_to(grupos["nao_cumprida"])
# Adiciona grupos ao mapa
for g in grupos.values(): for g in grupos.values():
g.add_to(mapa) g.add_to(mapa)
adicionar_bases(mapa) for icao, (lat, lon) in BASES_COORDS.items():
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=folium.Icon(color="red" if icao == "SBMN" else "blue", icon="plane", prefix="fa"),
).add_to(mapa)
folium.LayerControl(collapsed=False).add_to(mapa) folium.LayerControl(collapsed=False).add_to(mapa)
# Legenda HTML
legenda_html = """ legenda_html = """
<div style="position:fixed;bottom:30px;left:30px;z-index:1000;background:white; <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); padding:12px 16px;border-radius:8px;box-shadow:2px 2px 6px rgba(0,0,0,0.3);
@@ -182,7 +186,7 @@ def gerar_mapa(resultado: Path, saida: Path) -> None:
legenda_html += ( legenda_html += (
f' <span style="display:inline-block;width:16px;height:4px;' f' <span style="display:inline-block;width:16px;height:4px;'
f'background:{cor};margin-right:6px;vertical-align:middle;"></span>' f'background:{cor};margin-right:6px;vertical-align:middle;"></span>'
f'EVAM {k}<br>' f'FAB {k}<br>'
) )
legenda_html += """ legenda_html += """
<br><b>Prioridade (preenchimento):</b><br> <br><b>Prioridade (preenchimento):</b><br>
@@ -201,31 +205,420 @@ def gerar_mapa(resultado: Path, saida: Path) -> None:
""" """
mapa.get_root().html.add_child(folium.Element(legenda_html)) mapa.get_root().html.add_child(folium.Element(legenda_html))
saida.parent.mkdir(parents=True, exist_ok=True) return mapa.get_root().render()
mapa.save(str(saida))
print(f" Mapa salvo: {saida}")
_resumir_resultado(linhas)
def _resumir_resultado(linhas: list[dict]) -> None: # ---------------------------------------------------------------------------
total = len(linhas) # Gantt Plotly → string HTML (só o div, sem <html>)
cumpridas = sum(1 for r in linhas if r["status"] == "cumprida") # ---------------------------------------------------------------------------
print(f" Missões: {cumpridas}/{total} cumpridas")
aeronaves = {} def _construir_gantt(linhas: list[dict]) -> str:
try:
import plotly.graph_objects as go
except ImportError:
return "<p><em>plotly não instalado — instale com: pip install plotly</em></p>"
aeronaves_ord = sorted(
{r["aeronave"] for r in linhas if r["aeronave"]},
key=lambda k: int(k) if k.isdigit() else 0,
)
y_pos = {k: i for i, k in enumerate(aeronaves_ord)}
fig = go.Figure()
for i, k in enumerate(aeronaves_ord):
fig.add_hrect(
y0=i - 0.45, y1=i + 0.45,
fillcolor="#f0f0f0" if i % 2 == 0 else "white",
layer="below", line_width=0,
)
shapes = []
bar_traces = []
for r in linhas: for r in linhas:
if r["aeronave"]: k = r["aeronave"]
aeronaves.setdefault(r["aeronave"], 0) if not k or k not in y_pos:
aeronaves[r["aeronave"]] += 1 continue
for k, c in sorted(aeronaves.items()): try:
print(f" EVAM {k}: {c} missões") ini = r["partida_utc"].replace("Z", "+00:00")
fim = r["chegada_utc"].replace("Z", "+00:00")
except Exception:
continue
ini_dt = datetime.fromisoformat(ini)
fim_dt = datetime.fromisoformat(fim)
if (fim_dt - ini_dt).total_seconds() < 2 * 3600:
fim_dt = ini_dt + timedelta(hours=2)
yi = y_pos[k]
cor = _cor_gantt(r)
status = r["status"]
if status == "inspecao":
txt = (
f"<b>FAB {k}</b> — Inspeção {r['om']}<br>"
f"Início: {ini[:16].replace('T',' ')}Z<br>"
f"Fim: {fim[:16].replace('T',' ')}Z<br>"
f"Duração: {r['dur_h']}h"
)
else:
txt = (
f"<b>FAB {k}</b> — Missão #{r['ordem']}<br>"
f"{r['orig']}{r['dest']}<br>"
f"OM: {r['om']} | Prio: {r['prioridade']}<br>"
f"Partida: {ini[:16].replace('T',' ')}Z<br>"
f"Chegada: {fim[:16].replace('T',' ')}Z<br>"
f"Duração: {r['dur_h']}h"
)
# Barra como linha horizontal grossa — renderiza garantido no eixo de datas
bar_traces.append(go.Scatter(
x=[ini_dt, fim_dt],
y=[yi, yi],
mode="lines",
line=dict(color=cor, width=64),
hovertext=[txt, txt],
hoverinfo="text",
showlegend=False,
))
# Divisões diárias
datas_utc = [
r[campo].replace("Z", "+00:00")
for r in linhas
for campo in ("partida_utc", "chegada_utc")
if r.get(campo)
]
if datas_utc:
t_min = datetime.fromisoformat(min(datas_utc))
t_max = datetime.fromisoformat(max(datas_utc))
dia = datetime(t_min.year, t_min.month, t_min.day, tzinfo=timezone.utc)
while dia <= t_max + timedelta(days=1):
shapes.append(dict(
type="line", xref="x", yref="paper",
x0=dia.isoformat(), x1=dia.isoformat(),
y0=0, y1=1,
line=dict(color="#bbbbbb", width=1, dash="dot"),
layer="above",
))
dia += timedelta(days=1)
# Legenda
for nome, cor in [
("Prio 1", _GANTT_CORES["cumprida"]["1"]),
("Prio 2", _GANTT_CORES["cumprida"]["2"]),
("Prio 3", _GANTT_CORES["cumprida"]["3"]),
("Prio 4", _GANTT_CORES["cumprida"]["4"]),
("Prio 5", _GANTT_CORES["cumprida"]["5"]),
("Inspeção", _GANTT_CORES["inspecao"]),
]:
fig.add_trace(go.Scatter(
x=[None], y=[None], mode="markers",
marker=dict(size=12, color=cor, symbol="square"),
name=nome, showlegend=True,
))
for t in bar_traces:
fig.add_trace(t)
fig.update_layout(
shapes=shapes,
xaxis=dict(
title="Data / Hora (UTC)", type="date",
tickformat="%d/%m", dtick=86400000,
showgrid=True, gridcolor="#dddddd",
),
yaxis=dict(
title="Aeronave",
tickvals=list(range(len(aeronaves_ord))),
ticktext=[f"FAB {k}" for k in aeronaves_ord],
showgrid=False,
range=[-0.5, len(aeronaves_ord) - 0.5],
),
height=max(300, 120 + 80 * len(aeronaves_ord)),
plot_bgcolor="white",
paper_bgcolor="white",
legend=dict(title="Legenda", orientation="v", x=1.01, y=1, xanchor="left"),
margin=dict(l=100, r=160, t=10, b=60),
hovermode="closest",
)
return fig.to_html(full_html=False, include_plotlyjs="cdn")
# ---------------------------------------------------------------------------
# Tabela de voos com filtros
# ---------------------------------------------------------------------------
def _construir_tabela(linhas: list[dict]) -> str:
import json
# Monta lista de registros para a tabela (missões + inspeções, sem nao_cumprida sem aeronave)
registros = []
for r in linhas:
status = r["status"]
k = r["aeronave"]
if status == "nao_cumprida" and not k:
# mantém como linha sem aeronave
k = ""
registros.append({
"status": status,
"aeronave": f"FAB {k}" if k and k != "" else "",
"ordem": r.get("ordem", ""),
"om": r.get("om", ""),
"orig": r.get("orig", ""),
"dest": r.get("dest", ""),
"partida": r.get("partida_utc", "")[:16].replace("T", " ") + "Z" if r.get("partida_utc") else "",
"chegada": r.get("chegada_utc", "")[:16].replace("T", " ") + "Z" if r.get("chegada_utc") else "",
"dur_h": r.get("dur_h", ""),
"prioridade": r.get("prioridade", ""),
})
dados_json = json.dumps(registros, ensure_ascii=False)
opcoes_aeronave = sorted({r["aeronave"] for r in registros if r["aeronave"] != ""})
opcoes_status = ["cumprida", "nao_cumprida", "inspecao"]
opcoes_prio = sorted({r["prioridade"] for r in registros if r["prioridade"]})
opcoes_base = sorted({b for r in registros for b in (r["orig"], r["dest"]) if b})
def opts(valores: list[str], label: str) -> str:
html = f'<option value="">— {label} —</option>'
for v in valores:
html += f'<option value="{escape(v)}">{escape(v)}</option>'
return html
return f"""
<style>
.filtros {{
display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 14px; align-items: flex-end;
}}
.filtros label {{ font-size: 0.78rem; color: #555; display: flex; flex-direction: column; gap: 3px; }}
.filtros select, .filtros input {{
border: 1px solid #ccc; border-radius: 4px; padding: 5px 8px;
font-size: 0.85rem; min-width: 130px; background: white;
}}
.filtros button {{
padding: 6px 14px; border: none; border-radius: 4px; cursor: pointer;
font-size: 0.85rem; background: #1a3a5c; color: white;
align-self: flex-end;
}}
.filtros button:hover {{ background: #254f80; }}
#contador {{ font-size: 0.82rem; color: #666; margin-bottom: 10px; }}
#tabela-voos {{ width: 100%; border-collapse: collapse; font-size: 0.85rem; }}
#tabela-voos thead th {{
background: #1a3a5c; color: white; padding: 8px 10px;
text-align: left; white-space: nowrap; cursor: pointer; user-select: none;
}}
#tabela-voos thead th:hover {{ background: #254f80; }}
#tabela-voos thead th .sort-icon {{ margin-left: 4px; opacity: 0.5; }}
#tabela-voos tbody tr:nth-child(even) {{ background: #f7f8fa; }}
#tabela-voos tbody tr:hover {{ background: #e8f0fb; }}
#tabela-voos td {{ padding: 7px 10px; border-bottom: 1px solid #eee; white-space: nowrap; }}
.badge {{
display: inline-block; padding: 2px 8px; border-radius: 10px;
font-size: 0.75rem; font-weight: 600; text-transform: uppercase;
}}
.badge-cumprida {{ background: #d4edda; color: #155724; }}
.badge-nao_cumprida {{ background: #f8d7da; color: #721c24; }}
.badge-inspecao {{ background: #e2d9f3; color: #4a235a; }}
.prio-1 {{ color: #d62728; font-weight: 700; }}
.prio-2 {{ color: #ff7f0e; font-weight: 700; }}
.prio-3 {{ color: #2ca02c; font-weight: 700; }}
</style>
<div class="filtros">
<label>Aeronave
<select id="f-aeronave">{opts(opcoes_aeronave, "Todas")}</select>
</label>
<label>Status
<select id="f-status">{opts(opcoes_status, "Todos")}</select>
</label>
<label>Prioridade
<select id="f-prio">{opts(opcoes_prio, "Todas")}</select>
</label>
<label>Base (orig/dest)
<select id="f-base">{opts(opcoes_base, "Todas")}</select>
</label>
<label>Busca (OM, rota…)
<input id="f-texto" type="text" placeholder="ex: SBMN, EVAM…">
</label>
<button onclick="limparFiltros()">Limpar</button>
</div>
<div id="contador"></div>
<table id="tabela-voos">
<thead>
<tr>
<th onclick="ordenar(0)">Aeronave <span class="sort-icon">↕</span></th>
<th onclick="ordenar(1)">Status <span class="sort-icon">↕</span></th>
<th onclick="ordenar(2)">#Missão <span class="sort-icon">↕</span></th>
<th onclick="ordenar(3)">OM <span class="sort-icon">↕</span></th>
<th onclick="ordenar(4)">Origem <span class="sort-icon">↕</span></th>
<th onclick="ordenar(5)">Destino <span class="sort-icon">↕</span></th>
<th onclick="ordenar(6)">Partida (UTC) <span class="sort-icon">↕</span></th>
<th onclick="ordenar(7)">Chegada (UTC) <span class="sort-icon">↕</span></th>
<th onclick="ordenar(8)">Duração (h) <span class="sort-icon">↕</span></th>
<th onclick="ordenar(9)">Prio <span class="sort-icon">↕</span></th>
</tr>
</thead>
<tbody id="corpo-tabela"></tbody>
</table>
<script>
const DADOS = {dados_json};
let _colOrdem = -1, _colAsc = true;
function badgeStatus(s) {{
const label = {{cumprida:'Cumprida', nao_cumprida:'Não cumprida', inspecao:'Inspeção'}}[s] || s;
return `<span class="badge badge-${{s}}">${{label}}</span>`;
}}
function prio(p) {{
if (!p) return '';
const cls = ['1','2','3'].includes(p) ? `prio-${{p}}` : '';
return `<span class="${{cls}}">${{p}}</span>`;
}}
function linhasFiltradas() {{
const fa = document.getElementById('f-aeronave').value;
const fs = document.getElementById('f-status').value;
const fp = document.getElementById('f-prio').value;
const fb = document.getElementById('f-base').value;
const ft = document.getElementById('f-texto').value.toLowerCase();
return DADOS.filter(r => {{
if (fa && r.aeronave !== fa) return false;
if (fs && r.status !== fs) return false;
if (fp && r.prioridade !== fp) return false;
if (fb && r.orig !== fb && r.dest !== fb) return false;
if (ft && !JSON.stringify(r).toLowerCase().includes(ft)) return false;
return true;
}});
}}
function renderizar() {{
let rows = linhasFiltradas();
if (_colOrdem >= 0) {{
const chaves = ['aeronave','status','ordem','om','orig','dest','partida','chegada','dur_h','prioridade'];
const c = chaves[_colOrdem];
rows = [...rows].sort((a,b) => {{
const va = a[c] ?? '', vb = b[c] ?? '';
return _colAsc ? String(va).localeCompare(String(vb), 'pt', {{numeric:true}})
: String(vb).localeCompare(String(va), 'pt', {{numeric:true}});
}});
}}
document.getElementById('contador').textContent =
`${{rows.length}} registro${{rows.length !== 1 ? 's' : ''}} exibido${{rows.length !== 1 ? 's' : ''}} de ${{DADOS.length}}`;
document.getElementById('corpo-tabela').innerHTML = rows.map(r => `
<tr>
<td>${{r.aeronave}}</td>
<td>${{badgeStatus(r.status)}}</td>
<td>${{r.ordem}}</td>
<td>${{r.om}}</td>
<td>${{r.orig}}</td>
<td>${{r.dest}}</td>
<td>${{r.partida}}</td>
<td>${{r.chegada}}</td>
<td>${{r.dur_h}}</td>
<td>${{prio(r.prioridade)}}</td>
</tr>`).join('');
}}
function ordenar(col) {{
if (_colOrdem === col) _colAsc = !_colAsc;
else {{ _colOrdem = col; _colAsc = true; }}
renderizar();
}}
function limparFiltros() {{
['f-aeronave','f-status','f-prio','f-base'].forEach(id => document.getElementById(id).value = '');
document.getElementById('f-texto').value = '';
renderizar();
}}
['f-aeronave','f-status','f-prio','f-base'].forEach(id =>
document.getElementById(id).addEventListener('change', renderizar));
document.getElementById('f-texto').addEventListener('input', renderizar);
renderizar();
</script>"""
# ---------------------------------------------------------------------------
# HTML combinado
# ---------------------------------------------------------------------------
def gerar_planejamento(resultado: Path, saida: Path) -> None:
linhas = carregar_resultado(resultado)
gantt_div = _construir_gantt(linhas)
mapa_html = _construir_mapa(linhas)
tabela_div = _construir_tabela(linhas)
html = f"""<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="utf-8">
<title>Planejamento de Diagonal de Manutenção</title>
<style>
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
body {{ font-family: 'Segoe UI', Arial, sans-serif; background: #f0f2f5; color: #222; }}
header {{
background: #1a3a5c; color: white;
padding: 18px 32px;
border-bottom: 4px solid #c8a400;
}}
header h1 {{ font-size: 1.5rem; font-weight: 700; letter-spacing: 0.03em; }}
header p {{ font-size: 0.85rem; opacity: 0.75; margin-top: 4px; }}
.container {{ padding: 24px 32px; max-width: 1600px; margin: 0 auto; }}
.section {{
background: white; border-radius: 8px;
box-shadow: 0 1px 4px rgba(0,0,0,0.12);
margin-bottom: 28px; overflow: hidden;
}}
.section-title {{
background: #f7f8fa; border-bottom: 1px solid #e0e0e0;
padding: 12px 20px; font-size: 0.95rem; font-weight: 600;
color: #1a3a5c; text-transform: uppercase; letter-spacing: 0.05em;
}}
.section-body {{ padding: 16px; }}
iframe.mapa {{
width: 100%; height: 560px; border: none; border-radius: 4px; display: block;
}}
</style>
</head>
<body>
<header>
<h1>Planejamento de Diagonal de Manutenção</h1>
<p>Esquadrão Arara · C-105 · OAMRP v3</p>
</header>
<div class="container">
<div class="section">
<div class="section-title">Calendário de Emprego das Aeronaves</div>
<div class="section-body">{gantt_div}</div>
</div>
<div class="section">
<div class="section-title">Rotas Operacionais</div>
<iframe class="mapa" srcdoc="{escape(mapa_html)}"></iframe>
</div>
<div class="section">
<div class="section-title">Planejamento de Voos</div>
<div class="section-body">{tabela_div}</div>
</div>
</div>
</body>
</html>"""
saida.parent.mkdir(parents=True, exist_ok=True)
saida.write_text(html, encoding="utf-8")
print(f" Planejamento salvo: {saida}")
_resumir_resultado(linhas)
def main() -> None: def main() -> None:
parser = argparse.ArgumentParser(description="Visualização OAMRP — Arara C-105") parser = argparse.ArgumentParser(description="Visualização OAMRP — Arara C-105")
parser.add_argument("--resultado", type=Path, default=RESULTADO_PADRAO) parser.add_argument("--resultado", type=Path, default=RESULTADO_PADRAO)
parser.add_argument("--saida", type=Path, default=MAPA_PADRAO) parser.add_argument("--saida", type=Path, default=SAIDA_PADRAO)
args = parser.parse_args() args = parser.parse_args()
gerar_mapa(resultado=args.resultado, saida=args.saida) gerar_planejamento(resultado=args.resultado, saida=args.saida)
if __name__ == "__main__": if __name__ == "__main__":