Compare commits

..

7 Commits

Author SHA1 Message Date
Cesa-V
fe2ba12e49 Inclui @vitorcesavc nos autores (README e docs/authors.md)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 18:03:31 -03:00
Cesa-V
b368201d5d Documenta historico v1/v2/v3 no README com fases, limitacoes e nomenclatura
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 11:14:57 -03:00
Cesa-V
e71a0f2518 Atualiza README para v0.10: Set Partitioning, cenario insp grande e tabela de parametros
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 11:11:23 -03:00
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
10 changed files with 1992 additions and 226 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: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 | 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. |

123
README.md
View File

@@ -45,7 +45,7 @@ arara_oarmp/
Usamos tags curtas para identificar quem fez cada alteração relevante:
- `VTO`: Vitor Cesa.
- `VTO`: Vitor Cesa ([@vitorcesavc](https://git.ppgao.ita.br/vitorcesavc)).
- `GNR`: Generoso.
- `JOM`: João Marcos.
@@ -144,70 +144,107 @@ git push
- Preferir nomes de arquivo descritivos, com tipo do documento, aeronave, matrícula e data quando possível.
- Registrar no `LOG.md` qualquer decisão que afete os dados.
## Estado atual
## Estado atual (v0.10)
O modelo ativo é `software/oamrp_v3.py`, que implementa o OAMRP sobre uma rede espaço-tempo com arcos de ferry, múltiplos tipos de inspeção e prioridades de missão via OFRAG.
O modelo ativo é `software/oamrp_v3.py`, que implementa o OAMRP sobre uma rede espaço-tempo com arcos de ferry, múltiplos tipos de inspeção e Set Partitioning para cobertura de missões.
Última rodada validada (2026-06-16): 50/50 missões sintéticas cumpridas, solver CBC Optimal, 4 EVAMs operacionais.
**Última rodada validada (2026-06-16, commit `a34ed6b`):**
- 20/20 missões sintéticas cobertas, solver CBC Optimal em < 15 s
- Cenário com INSP 2A (4 dias em solo) visível no Gantt da matrícula 2803
- Set Partitioning: toda missão coberta por exatamente 1 aeronave
Arquivos principais do modelo:
### Formulação de cobertura
O modelo usa **Set Partitioning** (`z[m] = 1` para toda missão `m`): cada missão deve ser atribuída a exatamente uma aeronave. Versões anteriores usavam Set Covering (`z[m] ≤ 1`), que permitia missões descobertas. A transição foi documentada em `docs/changelog.md` (v0.10).
> **Atenção para contribuidores:** Set Partitioning foi validado apenas para o cenário sintético padrão (20 missões, seed 42, 4 aeronaves, somente 2803 com LRT apertado). Com mais missões ou múltiplas aeronaves com LRT < 5 h simultâneas, o MIP pode ser infeasível ou ultrapassar o time limit. Consulte as limitações detalhadas em `docs/changelog.md`.
### Arquivos principais
```text
software/oamrp_v3.py — modelo MIP principal
software/gerar_ofrag.py — gerador da tabela OFRAG
software/visualizar_resultado.py — mapa Folium interativo
db/processed/ofrag.csv — ordens de missão com prioridades 15
db/processed/resultado_oamrp_sintetico.csv — resultado da última rodada
db/processed/mapa_rotas.html — mapa interativo de rotas
software/oamrp_v3.py — modelo MIP principal (Fases 13 + Set Partitioning)
software/gerar_ofrag.py — gerador da tabela OFRAG
software/visualizar_resultado.py Gantt + mapa + tabela interativa
db/processed/ofrag.csv — ordens de missão com prioridades 15
db/processed/f0_cenario_insp_grande.json — f0 calibrado para forçar INSP 2A em 2803
db/processed/resultado_oamrp_v3.csv — resultado da última rodada
db/processed/planejamento_insp_grande.html — Gantt com inspeção de 4 dias visível
```
Para rodar:
### Como rodar
Cenário padrão (inspeção grande visível no Gantt):
```powershell
python software/oamrp_v3.py --sintetico --seed 42 --aleatorio --ofrag db/processed/ofrag.csv
python software/visualizar_resultado.py
python software/oamrp_v3.py --sintetico --seed 42 --f0 db/processed/f0_cenario_insp_grande.json --time-limit 60
python software/visualizar_resultado.py --resultado db/processed/resultado_oamrp_v3.csv --saida db/processed/planejamento_insp_grande.html
```
Pré-processamentos disponíveis:
Parâmetros relevantes do modelo:
```text
db/raw/relatorio_ciclo_inspecoes_c105_2805_2026-06-15.pdf — ciclo de inspeções C-105/2805
db/processed/registro_voo_2025.csv — registro de voo 2025 limpo (1420 trechos)
db/processed/registro_voo_2025_consolidado.csv — consolidado para roteamento
```
## Atualizacao - registro de voo 2025
| Parâmetro | Padrão | Descrição |
|---|---|---|
| `--sintetico` | | Usa missões sintéticas cross-base em vez do CSV real |
| `--n-missoes` | 20 | Número de missões sintéticas |
| `--seed` | 42 | Semente aleatória (reprodutibilidade) |
| `--f0` | | JSON com horas acumuladas iniciais por aeronave/inspeção |
| `--time-limit` | 300 | Limite de tempo do solver CBC (segundos) |
| `--aleatorio` | | Sorteia f0 aleatório entre 10%90% de F_max |
A `Planilha 2025.xlsx` foi pre-processada a partir de `db/raw/` sem alterar o arquivo original.
---
Arquivos gerados:
## Histórico dos modelos
```text
db/pre_process/registro_voo_2025/preprocess_planilha_2025.py
db/pre_process/registro_voo_2025/registro_voo_2025_limpo.csv
db/pre_process/registro_voo_2025/registro_voo_2025_limpo.xlsx
db/pre_process/registro_voo_2025/registro_voo_2025_consolidado.csv
db/pre_process/registro_voo_2025/missoes_oamrp_2025.csv
db/pre_process/registro_voo_2025/missoes_oamrp_2025_consolidado.csv
db/pre_process/registro_voo_2025/resumo_registro_voo_2025.json
db/pre_process/registro_voo_2025/validacao_registro_voo_2025.md
db/processed/registro_voo_2025.csv
db/processed/registro_voo_2025_consolidado.csv
```
### v1 — Protótipo inicial (`software/oamrp_v1.py`)
O consolidado trata `ROTA` como marcador de transicao, nao como localidade operacional, e remove a linha original 793 da aeronave 2806 dos artefatos processados.
Primeiro protótipo do modelo MIP em PuLP. Estrutura simples de atribuição aeronave-missão sem rede espaço-tempo, sem inspeções e sem ferry. Serviu para validar a configuração do ambiente e a integração com o solver CBC.
## Modelo OAMRP v2
- **Formulação:** atribuição direta, variáveis binárias por (aeronave, missão)
- **Restrições:** cobertura básica e continuidade de base
- **Sem:** janelas de tempo, inspeções, ferry, prioridades
- **Status:** protótipo histórico, não usar em produção
O arquivo `software/oamrp_v2.py` executa a primeira rodada do modelo com dados reais consolidados de 2025. Por padrao, ele usa janeiro/2025, as aeronaves `2800`, `2803`, `2809` e `2811`, e grava:
---
```text
db/processed/resultado_oamrp_jan_2025.csv
```
### v2 — Dados reais de 2025 (`software/oamrp_v2.py`)
Para executar:
Primeira rodada com dados reais do registro de voo de janeiro/2025, pré-processados a partir da planilha original. Introduziu janelas de tempo (decolagem e chegada do CSV), TAT entre missões e prioridade uniforme.
- **Dados de entrada:** `db/processed/registro_voo_2025_consolidado.csv` (1 267 trechos, 1 420 linhas originais)
- **Resultado:** 50 de 61 missões cumpridas em janeiro/2025 (`db/processed/resultado_oamrp_jan_2025.csv`)
- **Limitação identificada:** os dados reais de jan/2025 eram predominantemente voos locais SBMNSBMN, não exercitando roteamento cross-base nem inspeções
```powershell
python software/oamrp_v2.py
```
A rodada inicial de janeiro/2025 foi resolvida como otima pelo CBC, com 50 de 61 missoes cumpridas.
---
### v3 — Rede espaço-tempo completa (`software/oamrp_v3.py`) ← **modelo ativo**
Reescrita completa baseada em Al-Thani, Ben Ahmed & Haouari (2016). Introduziu a formulação por rede espaço-tempo com nós No e arcos Arco, inspeções com relógio de horas acumuladas e reset, arcos de ferry entre bases e missões sintéticas cross-base para validação de roteamento.
**Fases implementadas:**
| Fase | Restrições | Descrição |
|---|---|---|
| 1 | C1C6 | Rede espaço-tempo, cobertura (Set Partitioning), conservação de fluxo, TAT |
| 2 | C7C11 | Nós de inspeção, relógio H[k,i,n] com reset, downtime, slot único |
| 3 | Ferry | Arcos de ferry entre quaisquer bases (haversine ÷ 430 km/h), consome horas de célula |
**Nomenclatura alinhada com Al-Thani (2016):**
- `F_max` batente fixo (intervalo máximo legal entre manutenções, ex: 300 h)
- `f0` horas acumuladas no início do horizonte (estado inicial da aeronave)
- `LRT = F_max f0` tempo legal restante (decresce com o voo)
- `H[k, seq, nid]` acumulado em tempo real, rastreado na rede (equivale a u_j do artigo)
**Missões sintéticas:** como os dados reais de 2025 não exercitavam roteamento, o modelo usa um gerador sintético cross-base (`--sintetico`) com distâncias reais (haversine) e velocidade de cruzeiro do C-105 (430 km/h).
**Pré-processamentos disponíveis:**
```text
db/raw/relatorio_ciclo_inspecoes_c105_2805_2026-06-15.pdf — ciclo de inspeções C-105/2805
db/processed/registro_voo_2025.csv — registro de voo 2025 limpo (1 420 trechos)
db/processed/registro_voo_2025_consolidado.csv — consolidado para roteamento
```

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
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,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,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,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,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,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
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,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,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,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,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,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,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,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
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
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
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
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
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
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
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
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
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
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,SIM_010_SBCY_SBMN,10,SBCY,SBMN,2025-01-11T23:29:27.600000Z,2025-01-12T03:09:39.600000Z,3.67,3,DIURNO,,
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,2800,4,SIM_019_SBMN_SWCA,19,SBMN,SWCA,2025-01-29T04:57:54Z,2025-01-29T06:36:54Z,1.65,1,DIURNO,,
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,,
inspecao,2800,,INSP_2800_3,INSP 2A,SBMN,SBMN,2025-01-12T16:14:49.200000Z,2025-01-16T16:14:49.200000Z,96.00,,,,
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,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,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,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,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,2809,5,SIM_011_SBSN_SBVH,11,SBSN,SBVH,2025-01-12T01:26:24Z,2025-01-12T04:15:36Z,2.82,1,DIURNO,,
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,,
inspecao,2809,,INSP_2809_3,INSP 2A,SBMN,SBMN,2025-01-05T01:06:43.200000Z,2025-01-09T01:06:43.200000Z,96.00,,,,
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,,
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,,
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,,
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,,
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,,
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,,
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,,
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,,
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

@@ -2,11 +2,11 @@
## Colaboradores
| Tag | Nome | Papel inicial |
| --- | --- | --- |
| `VTO` | Vitor Cesa | Organização do repositório, estrutura inicial, pré-processamento e documentação |
| `GNR` | Generoso | Colaborador |
| `JOM` | João Marcos | Colaborador e origem inicial do relatório de inspeções |
| Tag | Nome | Gitea | Papel inicial |
| --- | --- | --- | --- |
| `VTO` | Vitor Cesa | [@vitorcesavc](https://git.ppgao.ita.br/vitorcesavc) | Organização do repositório, estrutura inicial, pré-processamento e documentação |
| `GNR` | Generoso | — | Colaborador |
| `JOM` | João Marcos | — | Colaborador e origem inicial do relatório de inspeções |
## Instituição e contexto

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
### Adicionado
@@ -87,6 +112,43 @@ Formato:
- 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
### Corrigido

View File

@@ -43,7 +43,7 @@ import csv
import json
import random
from dataclasses import dataclass, field
from datetime import datetime, timezone
from datetime import date, datetime, timedelta, timezone
from pathlib import Path
from typing import Literal, Optional
@@ -428,6 +428,7 @@ def resolver(
arcos: list[Arco],
aeronaves: list[Aeronave],
catalogo: dict[int, InspecaoParam],
**kwargs,
) -> dict:
ks = [a.id 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
)
# --- C1: Cobertura e definição de z ---
# Missões normais: Σ_k y_entrada ≤ 1 (cobertura opcional)
# Missões obrigatórias (M_obr / EVAM): Σ_k y_entrada == 1
# --- C1: Cobertura — Set Partitioning: toda missão coberta por exatamente 1 aeronave ---
for nid, n in missoes_nos.items():
fluxo_entrada = pulp.lpSum(
y[(k, a.id)]
@@ -484,10 +483,7 @@ def resolver(
if (k, a.id) in y
)
mdl += fluxo_entrada == z[nid], f"def_z_{nid}"
if n.missao.obrigatoria:
mdl += z[nid] == 1, f"hard_{nid}"
else:
mdl += z[nid] <= 1, f"cob_{nid}"
mdl += z[nid] == 1, f"part_{nid}"
# --- C2: Conservação de fluxo por (aeronave, nó) ---
for k in ks:
@@ -675,13 +671,13 @@ def resolver(
# 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.
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)
status_l1 = mdl.status
prio_otima = int(round(pulp.value(mdl.objective) or 0))
# L2: com prioridade fixada, minimiza folga de horas ao fim do horizonte.
# folga_k = orcamento_k horas_voadas_k
# Minimizar Σ_k folga_k ⟺ maximizar Σ_k horas_voadas_k (orcamento é constante).
# L2: com prioridade fixada, maximiza horas voadas.
mdl += (
pulp.lpSum(n.missao.prioridade * z[nid] for nid, n in missoes_nos.items())
== prio_otima,
@@ -696,7 +692,10 @@ def resolver(
)
mdl.sense = pulp.LpMaximize
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 {
"modelo": mdl,
@@ -787,6 +786,8 @@ def escrever_resultado(sol: dict, caminho: Path) -> None:
missoes_nos = sol["missoes_nos"]
linhas = []
horizonte_dt = sol.get("horizonte_dt")
cobertas_ids: set[str] = set()
for k in sol["ks"]:
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,
})
# 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():
if nid in cobertas_ids:
continue
@@ -1133,18 +1157,37 @@ def derivar_bases(
# 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]] = {
"SBMN": (-3.15, -59.99), # Manaus — Ponta Pelada (hub)
"SBBE": (-1.38, -48.48), # Belém
"SBBV": ( 2.84, -60.69), # Boa Vista
"SBSN": (-2.42, -54.79), # Santarém
"SBPV": (-8.71, -63.90), # Porto Velho
"SBTS": (-4.25, -69.94), # Tabatinga
"SBTT": (-3.38, -64.72), # Tefé
"SBUA": (-0.15, -67.05), # São Gabriel da Cachoeira
"SWBC": (-0.98, -62.92), # Barcelos
"SBMY": (-5.81, -61.28), # Manicoré
# Amazônia (bases primárias)
"SBMN": (-3.15, -59.99), # Manaus — Ponta Pelada (hub)
"SBBE": (-1.38, -48.48), # Belém
"SBBV": ( 2.84, -60.69), # Boa Vista
"SBSN": (-2.42, -54.79), # Santarém
"SBPV": (-8.71, -63.90), # Porto Velho
"SBTS": (-4.25, -69.94), # Tabatinga
"SBTT": (-3.38, -64.72), # Tefé
"SBUA": (-0.15, -67.05), # São Gabriel da Cachoeira
"SWBC": (-0.98, -62.92), # Barcelos
"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)
@@ -1162,6 +1205,103 @@ def _haversine_km(b1: str, b2: str) -> float:
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"]
# 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(
n: int = 50,
n: int = 20,
seed: Optional[int] = 42,
inicio: Optional[datetime] = None,
horizonte_h: float = 744.0, # janeiro = 31 × 24
n_evam: int = 5,
n_evam: int = 4,
bases: Optional[list[str]] = None,
ofrag_path: Optional[Path] = None,
) -> tuple[list[Missao], datetime]:
@@ -1346,30 +1486,49 @@ def main() -> None:
"--hard-pattern", default=HARD_MISSAO_PADRAO,
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(
"--sintetico", action="store_true",
help="Usa missões sintéticas cross-base em vez do CSV real.",
)
parser.add_argument(
"--n-missoes", type=int, default=50,
help="Número de missões sintéticas a gerar (padrão: 50).",
"--n-missoes", type=int, default=20,
help="Número de missões sintéticas a gerar (padrão: 20).",
)
parser.add_argument(
"--n-evam", type=int, default=5,
help="Missões obrigatórias (EVAM/hard) nas sintéticas (padrão: 5).",
"--n-evam", type=int, default=4,
help="Missões obrigatórias (EVAM/hard) nas sintéticas (padrão: 4).",
)
parser.add_argument(
"--ofrag", type=Path, default=None, metavar="CSV",
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()
inicio = datetime.fromisoformat(args.inicio).replace(tzinfo=timezone.utc)
fim = datetime.fromisoformat(args.fim).replace(tzinfo=timezone.utc)
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
bases = list(BASES_COORDS.keys())
missoes, horizonte_dt = gerar_missoes_sinteticas(
@@ -1405,7 +1564,8 @@ def main() -> None:
]
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)
imprimir_relatorio(sol, bases, args.resultado)

View File

@@ -1,29 +1,29 @@
"""
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
Gera um único HTML com título "Planejamento de Diagonal de Manutenção" contendo:
- Gantt (Plotly): calendário aeronave × tempo com voos e inspeções
- Mapa (Folium): rotas por aeronave, missões não cumpridas, bases
Uso:
python software/visualizar_resultado.py \
--resultado db/processed/resultado_oamrp_sintetico.csv \
--saida db/processed/mapa_rotas.html
--saida db/processed/planejamento.html
"""
import argparse
import csv
import io
from datetime import datetime, timedelta, timezone
from html import escape
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"
SAIDA_PADRAO = BASE_DIR / "db" / "processed" / "planejamento.html"
# Coordenadas das bases (lat, lon)
BASES_COORDS: dict[str, tuple[float, float]] = {
@@ -39,7 +39,6 @@ BASES_COORDS: dict[str, tuple[float, float]] = {
"SBMY": (-5.81, -61.28),
}
# Nomes legíveis das bases
BASES_NOMES = {
"SBMN": "Manaus / Ponta Pelada (Hub)",
"SBBE": "Belém",
@@ -53,21 +52,39 @@ BASES_NOMES = {
"SBMY": "Manicoré",
}
# Paleta de cores por aeronave
CORES_AERONAVE = {
"2800": "#1f77b4", # azul
"2803": "#2ca02c", # verde
"2809": "#d62728", # vermelho
"2811": "#ff7f0e", # laranja
"2800": "#1f77b4",
"2803": "#2ca02c",
"2809": "#d62728",
"2811": "#ff7f0e",
}
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:
mapa = {"1": "red", "2": "orange", "3": "yellow", "4": "lightblue", "5": "lightgray"}
return mapa.get(str(prio), "white")
return {"1": "red", "2": "orange", "3": "yellow", "4": "lightblue", "5": "lightgray"}.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]:
@@ -75,57 +92,49 @@ def carregar_resultado(caminho: Path) -> list[dict]:
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 _resumir_resultado(linhas: list[dict]) -> None:
cumpridas = sum(1 for r in linhas if r["status"] == "cumprida")
total_missoes = sum(1 for r in linhas if r["status"] in ("cumprida", "nao_cumprida"))
print(f" Missões: {cumpridas}/{total_missoes} cumpridas")
aeronaves: dict[str, int] = {}
for r in linhas:
if r["aeronave"] and r["status"] == "cumprida":
aeronaves.setdefault(r["aeronave"], 0)
aeronaves[r["aeronave"]] += 1
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(
location=[-3.5, -62.0],
zoom_start=5,
tiles="CartoDB positron",
)
def _construir_mapa(linhas: list[dict]) -> str:
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[k] = folium.FeatureGroup(name=f"FAB {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"]
for r in linhas:
if r["status"] != "cumprida":
continue
orig, dest = r["orig"], 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")
lat_o, lon_o = BASES_COORDS[orig]
lat_d, lon_d = BASES_COORDS[dest]
# 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']}",
color=cor, weight=2.5, opacity=0.8,
tooltip=f"FAB {k} | {orig}{dest} | OM {r['om']} | prio {r['prioridade']}",
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"OM: {r['om']} | Prio: {r['prioridade']}<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,
),
).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,
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"])
for r in linhas:
if r["status"] == "nao_cumprida":
orig, dest = r["orig"], 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)
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)
# 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);
@@ -182,7 +186,7 @@ def gerar_mapa(resultado: Path, saida: Path) -> None:
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>'
f'FAB {k}<br>'
)
legenda_html += """
<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))
saida.parent.mkdir(parents=True, exist_ok=True)
mapa.save(str(saida))
print(f" Mapa salvo: {saida}")
_resumir_resultado(linhas)
return mapa.get_root().render()
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 = {}
# ---------------------------------------------------------------------------
# Gantt Plotly → string HTML (só o div, sem <html>)
# ---------------------------------------------------------------------------
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:
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")
k = r["aeronave"]
if not k or k not in y_pos:
continue
try:
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:
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)
parser.add_argument("--saida", type=Path, default=SAIDA_PADRAO)
args = parser.parse_args()
gerar_mapa(resultado=args.resultado, saida=args.saida)
gerar_planejamento(resultado=args.resultado, saida=args.saida)
if __name__ == "__main__":