Compare commits
9 Commits
7cd7458565
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe2ba12e49 | ||
|
|
b368201d5d | ||
|
|
e71a0f2518 | ||
|
|
a34ed6b340 | ||
|
|
6433055e9e | ||
|
|
83a2dac624 | ||
|
|
3756f961a8 | ||
|
|
4d8554df38 | ||
|
|
f0d72f8189 |
16
LOG.md
16
LOG.md
@@ -32,3 +32,19 @@ Legenda de autores:
|
||||
| 2026-06-15 | 22:30 | VTO | Implementou Fase 2: inspeções na rede espaço-tempo | `software/oamrp_v3.py` | Adicionados nós INSP_{k}_{seq} e arcos missao_insp/insp_missao; variáveis H_{k,i,nid} para rastreamento de horas com reset; C9 downtime; C11 slot único via disjuntivas. |
|
||||
| 2026-06-15 | 23:00 | VTO | Substituiu dados reais por missões sintéticas cross-base | `software/oamrp_v3.py`; `db/processed/resultado_oamrp_sintetico.csv` | Dados reais de jan/2025 eram todos SBMN→SBMN (voos locais), não exercitando roteamento; gerador sintético cria 50 missões cross-base entre 10 bases com distâncias reais (haversine) e velocidade C-105; flag --sintetico na CLI. |
|
||||
| 2026-06-15 | 23:30 | VTO | Implementou Fase 3: arcos de ferry (C3 relaxado) | `software/oamrp_v3.py` | Arcos inicio/missao_missao/missao_insp/insp_missao agora permitem bases diferentes com tempo de ferry (haversine/430 km/h); ferry consome horas de célula no relógio de inspeção (C6); relatório imprime segmentos de ferry na linha do tempo. |
|
||||
| 2026-06-16 | 10:00 | VTO | Corrigiu bug de conservação de fluxo em nós de inspeção | `software/oamrp_v3.py` | Nós INSP_{k}_{seq} não tinham restrição entrada_k==saida_k; solver criava/destruía unidades de fluxo nesses nós, inflando cobertura para 50/50 no relatório com apenas 16 missões reais nas rotas; corrigido adicionando C2 para nós de inspeção. |
|
||||
| 2026-06-16 | 10:05 | VTO | Removeu restrição C6 Fase 1 simplificada | `software/oamrp_v3.py` | Restrição somava horas totais sem respeitar resets de inspeção, tornando EVAMs 2809/2811 artificialmente ociosas; variáveis H[k,seq,nid] já cobrem isso corretamente com reset nos nós de inspeção. |
|
||||
| 2026-06-16 | 10:10 | VTO | Adicionou horas iniciais aleatórias e prioridades via OFRAG | `software/oamrp_v3.py`; `software/gerar_ofrag.py`; `db/processed/ofrag.csv` | Flag --aleatorio sorteia f0 entre 10%–90% de F_max por aeronave; gerar_ofrag.py cria tabela com id_ofrag, missao_id, prioridade (1–5), tipo_missao; --ofrag na CLI carrega prioridades ao gerar missões sintéticas. |
|
||||
| 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_max−f0); 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. |
|
||||
|
||||
120
README.md
120
README.md
@@ -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,57 +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)
|
||||
|
||||
Já existe um pré-processamento do relatório de ciclo de inspeções da aeronave C-105 matrícula 2805, com data de relatório `2026-06-15`.
|
||||
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.
|
||||
|
||||
Também existe um primeiro protótipo em `software/oamrp_v1.py`, que formula um modelo MIP compacto em PuLP para atribuir aeronaves a missões de transporte, respeitando continuidade espacial/temporal, turnaround mínimo e limite de horas até inspeção.
|
||||
**Ú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:
|
||||
### 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
|
||||
db/raw/relatorio_ciclo_inspecoes_c105_2805_2026-06-15.pdf
|
||||
db/pre_process/relatorio_ciclo_inspecoes_c105_2805_2026-06-15/preprocess_pdf.py
|
||||
db/pre_process/relatorio_ciclo_inspecoes_c105_2805_2026-06-15/relatorio_ciclo_inspecoes_c105_2805_2026-06-15_texto.txt
|
||||
db/pre_process/relatorio_ciclo_inspecoes_c105_2805_2026-06-15/relatorio_ciclo_inspecoes_c105_2805_2026-06-15_inspecoes.json
|
||||
db/pre_process/relatorio_ciclo_inspecoes_c105_2805_2026-06-15/relatorio_ciclo_inspecoes_c105_2805_2026-06-15_inspecoes.csv
|
||||
software/oamrp_v1.py
|
||||
software/oamrp_v3.py — modelo MIP principal (Fases 1–3 + 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 1–5
|
||||
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
|
||||
```
|
||||
|
||||
Esse processamento extraiu 18 inspeções e separou campos como inspeção, referência, vencimento, TSO, letra, nível, duração, intervalo por horas de voo, meses contínuos e pousos.
|
||||
## Atualizacao - registro de voo 2025
|
||||
### Como rodar
|
||||
|
||||
A `Planilha 2025.xlsx` foi pre-processada a partir de `db/raw/` sem alterar o arquivo original.
|
||||
Cenário padrão (inspeção grande visível no Gantt):
|
||||
|
||||
Arquivos gerados:
|
||||
|
||||
```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
|
||||
```powershell
|
||||
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
|
||||
```
|
||||
|
||||
O consolidado trata `ROTA` como marcador de transicao, nao como localidade operacional, e remove a linha original 793 da aeronave 2806 dos artefatos processados.
|
||||
Parâmetros relevantes do modelo:
|
||||
|
||||
## Modelo OAMRP v2
|
||||
| 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 |
|
||||
|
||||
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
|
||||
```
|
||||
## Histórico dos modelos
|
||||
|
||||
Para executar:
|
||||
### v1 — Protótipo inicial (`software/oamrp_v1.py`)
|
||||
|
||||
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.
|
||||
|
||||
- **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
|
||||
|
||||
---
|
||||
|
||||
### v2 — Dados reais de 2025 (`software/oamrp_v2.py`)
|
||||
|
||||
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 SBMN→SBMN, 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 | C1–C6 | Rede espaço-tempo, cobertura (Set Partitioning), conservação de fluxo, TAT |
|
||||
| 2 | C7–C11 | 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
|
||||
```
|
||||
|
||||
6
db/processed/f0_cenario_esquadrao.json
Normal file
6
db/processed/f0_cenario_esquadrao.json
Normal 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}
|
||||
}
|
||||
6
db/processed/f0_cenario_insp_grande.json
Normal file
6
db/processed/f0_cenario_insp_grande.json
Normal 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}
|
||||
}
|
||||
1999
db/processed/mapa_rotas.html
Normal file
1999
db/processed/mapa_rotas.html
Normal file
File diff suppressed because it is too large
Load Diff
51
db/processed/ofrag.csv
Normal file
51
db/processed/ofrag.csv
Normal file
@@ -0,0 +1,51 @@
|
||||
id_ofrag,missao_id,om,orig,dest,prioridade,tipo_missao,observacao
|
||||
OFRAG-2025-001,SIM_001_SBMN_SBMY,1,SBMN,SBMY,1,Reabastecimento,
|
||||
OFRAG-2025-002,SIM_002_SBBE_SBTT,2,SBBE,SBTT,1,Pessoal,
|
||||
OFRAG-2025-003,SIM_003_SBMN_SBPV,3,SBMN,SBPV,1,Médico,EVAM — execução obrigatória
|
||||
OFRAG-2025-004,SIM_004_SBMN_SBUA,4,SBMN,SBUA,1,Logística,EVAM — execução obrigatória
|
||||
OFRAG-2025-005,SIM_005_SBSN_SBTS,5,SBSN,SBTS,2,Reabastecimento,
|
||||
OFRAG-2025-006,SIM_006_SBSN_SBBE,6,SBSN,SBBE,2,Pessoal,
|
||||
OFRAG-2025-007,SIM_007_SBTS_SBMN,7,SBTS,SBMN,2,Reconhecimento,
|
||||
OFRAG-2025-008,SIM_008_SBBE_SBTS,8,SBBE,SBTS,4,Reconhecimento,
|
||||
OFRAG-2025-009,SIM_009_SBMY_SBTS,9,SBMY,SBTS,2,Logística,
|
||||
OFRAG-2025-010,SIM_010_SBTT_SBMN,10,SBTT,SBMN,1,Logística,
|
||||
OFRAG-2025-011,SIM_011_SBBE_SBSN,11,SBBE,SBSN,1,Logística,
|
||||
OFRAG-2025-012,SIM_012_SWBC_SBSN,12,SWBC,SBSN,3,Logística,
|
||||
OFRAG-2025-013,SIM_013_SBMN_SBBV,13,SBMN,SBBV,2,Reconhecimento,
|
||||
OFRAG-2025-014,SIM_014_SBMN_SBTS,14,SBMN,SBTS,1,Logística,
|
||||
OFRAG-2025-015,SIM_015_SBMN_SBPV,15,SBMN,SBPV,1,Médico,EVAM — execução obrigatória
|
||||
OFRAG-2025-016,SIM_016_SBPV_SBMN,16,SBPV,SBMN,1,Logística,
|
||||
OFRAG-2025-017,SIM_017_SBSN_SBMN,17,SBSN,SBMN,4,Reabastecimento,
|
||||
OFRAG-2025-018,SIM_018_SBSN_SBBV,18,SBSN,SBBV,1,Reconhecimento,
|
||||
OFRAG-2025-019,SIM_019_SBBV_SBMN,19,SBBV,SBMN,2,Logística,
|
||||
OFRAG-2025-020,SIM_020_SWBC_SBTS,20,SWBC,SBTS,4,Pessoal,
|
||||
OFRAG-2025-021,SIM_021_SBMN_SBPV,21,SBMN,SBPV,1,Médico,EVAM — execução obrigatória
|
||||
OFRAG-2025-022,SIM_022_SBMN_SBBV,22,SBMN,SBBV,1,Reabastecimento,
|
||||
OFRAG-2025-023,SIM_023_SBBV_SWBC,23,SBBV,SWBC,3,Reconhecimento,
|
||||
OFRAG-2025-024,SIM_024_SBMY_SBBV,24,SBMY,SBBV,1,Pessoal,
|
||||
OFRAG-2025-025,SIM_025_SWBC_SBMN,25,SWBC,SBMN,3,Logística,
|
||||
OFRAG-2025-026,SIM_026_SBUA_SBTT,26,SBUA,SBTT,3,Reabastecimento,
|
||||
OFRAG-2025-027,SIM_027_SBTS_SBMY,27,SBTS,SBMY,1,Pessoal,
|
||||
OFRAG-2025-028,SIM_028_SBMN_SBTT,28,SBMN,SBTT,2,Logística,
|
||||
OFRAG-2025-029,SIM_029_SBTT_SBSN,29,SBTT,SBSN,2,Reconhecimento,
|
||||
OFRAG-2025-030,SIM_030_SWBC_SBSN,30,SWBC,SBSN,2,Logística,
|
||||
OFRAG-2025-031,SIM_031_SBMN_SBTT,31,SBMN,SBTT,2,Reabastecimento,
|
||||
OFRAG-2025-032,SIM_032_SBMN_SBBE,32,SBMN,SBBE,5,Reabastecimento,
|
||||
OFRAG-2025-033,SIM_033_SBBV_SBSN,33,SBBV,SBSN,1,Reconhecimento,
|
||||
OFRAG-2025-034,SIM_034_SBTS_SBMN,34,SBTS,SBMN,2,Logística,
|
||||
OFRAG-2025-035,SIM_035_SBTT_SBMN,35,SBTT,SBMN,1,Logística,
|
||||
OFRAG-2025-036,SIM_036_SBPV_SWBC,36,SBPV,SWBC,3,Logística,
|
||||
OFRAG-2025-037,SIM_037_SBBV_SBMN,37,SBBV,SBMN,3,Logística,
|
||||
OFRAG-2025-038,SIM_038_SBBV_SBBE,38,SBBV,SBBE,2,Pessoal,
|
||||
OFRAG-2025-039,SIM_039_SBSN_SBMN,39,SBSN,SBMN,1,Logística,
|
||||
OFRAG-2025-040,SIM_040_SBSN_SBMN,40,SBSN,SBMN,4,Reconhecimento,
|
||||
OFRAG-2025-041,SIM_041_SBBE_SBTS,41,SBBE,SBTS,1,Reabastecimento,
|
||||
OFRAG-2025-042,SIM_042_SBBE_SBSN,42,SBBE,SBSN,5,Reconhecimento,
|
||||
OFRAG-2025-043,SIM_043_SBMN_SBPV,43,SBMN,SBPV,4,Médico,
|
||||
OFRAG-2025-044,SIM_044_SBBE_SBPV,44,SBBE,SBPV,3,Pessoal,
|
||||
OFRAG-2025-045,SIM_045_SBTS_SBMN,45,SBTS,SBMN,4,Reabastecimento,
|
||||
OFRAG-2025-046,SIM_046_SBMN_SBUA,46,SBMN,SBUA,3,Reconhecimento,
|
||||
OFRAG-2025-047,SIM_047_SWBC_SBTS,47,SWBC,SBTS,2,Reabastecimento,
|
||||
OFRAG-2025-048,SIM_048_SBSN_SBUA,48,SBSN,SBUA,1,Médico,
|
||||
OFRAG-2025-049,SIM_049_SBMN_SBMY,49,SBMN,SBMY,4,Pessoal,
|
||||
OFRAG-2025-050,SIM_050_SBTT_SBPV,50,SBTT,SBPV,1,Logística,EVAM — execução obrigatória
|
||||
|
1120
db/processed/planejamento_insp_grande.html
Normal file
1120
db/processed/planejamento_insp_grande.html
Normal file
File diff suppressed because one or more lines are too long
@@ -1,51 +1,51 @@
|
||||
status,aeronave,ordem,id,om,orig,dest,partida_utc,chegada_utc,dur_h,prioridade,periodo,aeronave_real_2025,linhas_origem
|
||||
cumprida,2800,1,SIM_014_SBMN_SBTS,14,SBMN,SBTS,2025-01-11T09:41:20.400000Z,2025-01-11T12:08:20.400000Z,2.45,1,DIURNO,,
|
||||
cumprida,2800,2,SIM_034_SBTS_SBMN,34,SBTS,SBMN,2025-01-23T00:14:45.600000Z,2025-01-23T02:54:57.600000Z,2.67,1,DIURNO,,
|
||||
cumprida,2803,1,SIM_001_SBMN_SBMY,1,SBMN,SBMY,2025-01-01T02:22:55.200000Z,2025-01-01T03:09:07.200000Z,0.77,1,DIURNO,,
|
||||
cumprida,2803,2,SIM_024_SBMY_SBBV,24,SBMY,SBBV,2025-01-17T22:04:04.800000Z,2025-01-18T00:30:28.800000Z,2.44,1,DIURNO,,
|
||||
cumprida,2803,3,SIM_038_SBBV_SBBE,38,SBBV,SBBE,2025-01-23T23:08:38.400000Z,2025-01-24T02:18:50.400000Z,3.17,1,DIURNO,,
|
||||
cumprida,2803,4,SIM_041_SBBE_SBTS,41,SBBE,SBTS,2025-01-24T12:09:03.600000Z,2025-01-24T17:48:03.600000Z,5.65,1,DIURNO,,
|
||||
cumprida,2803,5,SIM_045_SBTS_SBMN,45,SBTS,SBMN,2025-01-28T01:21:25.200000Z,2025-01-28T03:43:01.200000Z,2.36,1,DIURNO,,
|
||||
cumprida,2800,1,SIM_007_SBTS_SBMN,7,SBTS,SBMN,2025-01-05T08:52:04.800000Z,2025-01-05T11:40:04.800000Z,2.80,2,DIURNO,,
|
||||
cumprida,2800,2,SIM_008_SBBE_SBTS,8,SBBE,SBTS,2025-01-05T16:11:09.600000Z,2025-01-05T21:53:09.600000Z,5.70,4,DIURNO,,
|
||||
cumprida,2800,3,SIM_015_SBMN_SBPV,15,SBMN,SBPV,2025-01-12T01:42:18Z,2025-01-12T03:21:54Z,1.66,1,DIURNO,,
|
||||
cumprida,2800,4,SIM_021_SBMN_SBPV,21,SBMN,SBPV,2025-01-16T06:26:09.600000Z,2025-01-16T08:12:21.600000Z,1.77,1,DIURNO,,
|
||||
cumprida,2800,5,SIM_037_SBBV_SBMN,37,SBBV,SBMN,2025-01-23T20:18:28.800000Z,2025-01-23T21:50:52.800000Z,1.54,3,DIURNO,,
|
||||
cumprida,2800,6,SIM_042_SBBE_SBSN,42,SBBE,SBSN,2025-01-25T14:54:14.400000Z,2025-01-25T16:35:02.400000Z,1.68,5,DIURNO,,
|
||||
cumprida,2800,7,SIM_045_SBTS_SBMN,45,SBTS,SBMN,2025-01-28T01:21:25.200000Z,2025-01-28T03:43:01.200000Z,2.36,4,DIURNO,,
|
||||
cumprida,2803,1,SIM_006_SBSN_SBBE,6,SBSN,SBBE,2025-01-05T06:29:20.400000Z,2025-01-05T08:16:44.400000Z,1.79,2,DIURNO,,
|
||||
cumprida,2803,2,SIM_023_SBBV_SWBC,23,SBBV,SWBC,2025-01-17T11:36:32.400000Z,2025-01-17T12:47:20.400000Z,1.18,3,DIURNO,,
|
||||
cumprida,2803,3,SIM_028_SBMN_SBTT,28,SBMN,SBTT,2025-01-19T05:45:50.400000Z,2025-01-19T07:06:14.400000Z,1.34,2,DIURNO,,
|
||||
cumprida,2809,1,SIM_036_SBPV_SWBC,36,SBPV,SWBC,2025-01-23T18:49:19.200000Z,2025-01-23T21:00:07.200000Z,2.18,3,DIURNO,,
|
||||
cumprida,2811,1,SIM_001_SBMN_SBMY,1,SBMN,SBMY,2025-01-01T02:22:55.200000Z,2025-01-01T03:09:07.200000Z,0.77,1,DIURNO,,
|
||||
cumprida,2811,2,SIM_003_SBMN_SBPV,3,SBMN,SBPV,2025-01-04T06:06:21.600000Z,2025-01-04T07:48:57.600000Z,1.71,1,DIURNO,,
|
||||
cumprida,2811,3,SIM_005_SBSN_SBTS,5,SBSN,SBTS,2025-01-05T05:32:09.600000Z,2025-01-05T09:50:09.600000Z,4.30,2,DIURNO,,
|
||||
cumprida,2811,4,SIM_025_SWBC_SBMN,25,SWBC,SBMN,2025-01-18T08:18:36Z,2025-01-18T09:11:24Z,0.88,3,DIURNO,,
|
||||
cumprida,2811,5,SIM_041_SBBE_SBTS,41,SBBE,SBTS,2025-01-24T12:09:03.600000Z,2025-01-24T17:48:03.600000Z,5.65,1,DIURNO,,
|
||||
nao_cumprida,,,SIM_002_SBBE_SBTT,2,SBBE,SBTT,2025-01-03T00:34:55.200000Z,2025-01-03T04:33:07.200000Z,3.97,1,DIURNO,,
|
||||
nao_cumprida,,,SIM_003_SBMN_SBPV,3,SBMN,SBPV,2025-01-04T06:06:21.600000Z,2025-01-04T07:48:57.600000Z,1.71,1,DIURNO,,
|
||||
nao_cumprida,,,SIM_004_SBMN_SBUA,4,SBMN,SBUA,2025-01-04T12:04:51.600000Z,2025-01-04T13:55:51.600000Z,1.85,1,DIURNO,,
|
||||
nao_cumprida,,,SIM_005_SBSN_SBTS,5,SBSN,SBTS,2025-01-05T05:32:09.600000Z,2025-01-05T09:50:09.600000Z,4.30,1,DIURNO,,
|
||||
nao_cumprida,,,SIM_006_SBSN_SBBE,6,SBSN,SBBE,2025-01-05T06:29:20.400000Z,2025-01-05T08:16:44.400000Z,1.79,1,DIURNO,,
|
||||
nao_cumprida,,,SIM_007_SBTS_SBMN,7,SBTS,SBMN,2025-01-05T08:52:04.800000Z,2025-01-05T11:40:04.800000Z,2.80,1,DIURNO,,
|
||||
nao_cumprida,,,SIM_008_SBBE_SBTS,8,SBBE,SBTS,2025-01-05T16:11:09.600000Z,2025-01-05T21:53:09.600000Z,5.70,1,DIURNO,,
|
||||
nao_cumprida,,,SIM_009_SBMY_SBTS,9,SBMY,SBTS,2025-01-05T20:05:16.800000Z,2025-01-05T22:20:52.800000Z,2.26,1,DIURNO,,
|
||||
nao_cumprida,,,SIM_009_SBMY_SBTS,9,SBMY,SBTS,2025-01-05T20:05:16.800000Z,2025-01-05T22:20:52.800000Z,2.26,2,DIURNO,,
|
||||
nao_cumprida,,,SIM_010_SBTT_SBMN,10,SBTT,SBMN,2025-01-05T21:25:37.200000Z,2025-01-05T22:33:25.200000Z,1.13,1,DIURNO,,
|
||||
nao_cumprida,,,SIM_011_SBBE_SBSN,11,SBBE,SBSN,2025-01-07T12:04:04.800000Z,2025-01-07T13:40:40.800000Z,1.61,1,DIURNO,,
|
||||
nao_cumprida,,,SIM_012_SWBC_SBSN,12,SWBC,SBSN,2025-01-08T16:32:16.800000Z,2025-01-08T18:52:40.800000Z,2.34,1,DIURNO,,
|
||||
nao_cumprida,,,SIM_013_SBMN_SBBV,13,SBMN,SBBV,2025-01-09T23:25:44.400000Z,2025-01-10T01:05:20.400000Z,1.66,1,DIURNO,,
|
||||
nao_cumprida,,,SIM_015_SBMN_SBPV,15,SBMN,SBPV,2025-01-12T01:42:18Z,2025-01-12T03:21:54Z,1.66,1,DIURNO,,
|
||||
nao_cumprida,,,SIM_012_SWBC_SBSN,12,SWBC,SBSN,2025-01-08T16:32:16.800000Z,2025-01-08T18:52:40.800000Z,2.34,3,DIURNO,,
|
||||
nao_cumprida,,,SIM_013_SBMN_SBBV,13,SBMN,SBBV,2025-01-09T23:25:44.400000Z,2025-01-10T01:05:20.400000Z,1.66,2,DIURNO,,
|
||||
nao_cumprida,,,SIM_014_SBMN_SBTS,14,SBMN,SBTS,2025-01-11T09:41:20.400000Z,2025-01-11T12:08:20.400000Z,2.45,1,DIURNO,,
|
||||
nao_cumprida,,,SIM_016_SBPV_SBMN,16,SBPV,SBMN,2025-01-12T18:25:08.400000Z,2025-01-12T20:11:56.400000Z,1.78,1,DIURNO,,
|
||||
nao_cumprida,,,SIM_017_SBSN_SBMN,17,SBSN,SBMN,2025-01-13T06:27:18Z,2025-01-13T07:41:42Z,1.24,1,DIURNO,,
|
||||
nao_cumprida,,,SIM_017_SBSN_SBMN,17,SBSN,SBMN,2025-01-13T06:27:18Z,2025-01-13T07:41:42Z,1.24,4,DIURNO,,
|
||||
nao_cumprida,,,SIM_018_SBSN_SBBV,18,SBSN,SBBV,2025-01-14T02:28:12Z,2025-01-14T04:36:36Z,2.14,1,DIURNO,,
|
||||
nao_cumprida,,,SIM_019_SBBV_SBMN,19,SBBV,SBMN,2025-01-15T07:32:56.400000Z,2025-01-15T09:12:32.400000Z,1.66,1,DIURNO,,
|
||||
nao_cumprida,,,SIM_020_SWBC_SBTS,20,SWBC,SBTS,2025-01-16T02:14:27.600000Z,2025-01-16T04:25:51.600000Z,2.19,1,DIURNO,,
|
||||
nao_cumprida,,,SIM_021_SBMN_SBPV,21,SBMN,SBPV,2025-01-16T06:26:09.600000Z,2025-01-16T08:12:21.600000Z,1.77,1,DIURNO,,
|
||||
nao_cumprida,,,SIM_019_SBBV_SBMN,19,SBBV,SBMN,2025-01-15T07:32:56.400000Z,2025-01-15T09:12:32.400000Z,1.66,2,DIURNO,,
|
||||
nao_cumprida,,,SIM_020_SWBC_SBTS,20,SWBC,SBTS,2025-01-16T02:14:27.600000Z,2025-01-16T04:25:51.600000Z,2.19,4,DIURNO,,
|
||||
nao_cumprida,,,SIM_022_SBMN_SBBV,22,SBMN,SBBV,2025-01-16T12:38:16.800000Z,2025-01-16T14:04:40.800000Z,1.44,1,DIURNO,,
|
||||
nao_cumprida,,,SIM_023_SBBV_SWBC,23,SBBV,SWBC,2025-01-17T11:36:32.400000Z,2025-01-17T12:47:20.400000Z,1.18,1,DIURNO,,
|
||||
nao_cumprida,,,SIM_025_SWBC_SBMN,25,SWBC,SBMN,2025-01-18T08:18:36Z,2025-01-18T09:11:24Z,0.88,1,DIURNO,,
|
||||
nao_cumprida,,,SIM_026_SBUA_SBTT,26,SBUA,SBTT,2025-01-18T20:18:46.800000Z,2025-01-18T21:21:10.800000Z,1.04,1,DIURNO,,
|
||||
nao_cumprida,,,SIM_024_SBMY_SBBV,24,SBMY,SBBV,2025-01-17T22:04:04.800000Z,2025-01-18T00:30:28.800000Z,2.44,1,DIURNO,,
|
||||
nao_cumprida,,,SIM_026_SBUA_SBTT,26,SBUA,SBTT,2025-01-18T20:18:46.800000Z,2025-01-18T21:21:10.800000Z,1.04,3,DIURNO,,
|
||||
nao_cumprida,,,SIM_027_SBTS_SBMY,27,SBTS,SBMY,2025-01-19T05:22:55.200000Z,2025-01-19T07:42:07.200000Z,2.32,1,DIURNO,,
|
||||
nao_cumprida,,,SIM_028_SBMN_SBTT,28,SBMN,SBTT,2025-01-19T05:45:50.400000Z,2025-01-19T07:06:14.400000Z,1.34,1,DIURNO,,
|
||||
nao_cumprida,,,SIM_029_SBTT_SBSN,29,SBTT,SBSN,2025-01-20T02:58:12Z,2025-01-20T05:46:12Z,2.80,1,DIURNO,,
|
||||
nao_cumprida,,,SIM_030_SWBC_SBSN,30,SWBC,SBSN,2025-01-20T15:51:10.800000Z,2025-01-20T18:05:34.800000Z,2.24,1,DIURNO,,
|
||||
nao_cumprida,,,SIM_031_SBMN_SBTT,31,SBMN,SBTT,2025-01-21T19:19:40.800000Z,2025-01-21T20:35:52.800000Z,1.27,1,DIURNO,,
|
||||
nao_cumprida,,,SIM_032_SBMN_SBBE,32,SBMN,SBBE,2025-01-21T19:28:22.800000Z,2025-01-21T22:36:46.800000Z,3.14,1,DIURNO,,
|
||||
nao_cumprida,,,SIM_029_SBTT_SBSN,29,SBTT,SBSN,2025-01-20T02:58:12Z,2025-01-20T05:46:12Z,2.80,2,DIURNO,,
|
||||
nao_cumprida,,,SIM_030_SWBC_SBSN,30,SWBC,SBSN,2025-01-20T15:51:10.800000Z,2025-01-20T18:05:34.800000Z,2.24,2,DIURNO,,
|
||||
nao_cumprida,,,SIM_031_SBMN_SBTT,31,SBMN,SBTT,2025-01-21T19:19:40.800000Z,2025-01-21T20:35:52.800000Z,1.27,2,DIURNO,,
|
||||
nao_cumprida,,,SIM_032_SBMN_SBBE,32,SBMN,SBBE,2025-01-21T19:28:22.800000Z,2025-01-21T22:36:46.800000Z,3.14,5,DIURNO,,
|
||||
nao_cumprida,,,SIM_033_SBBV_SBSN,33,SBBV,SBSN,2025-01-22T22:56:56.400000Z,2025-01-23T00:56:56.400000Z,2.00,1,DIURNO,,
|
||||
nao_cumprida,,,SIM_034_SBTS_SBMN,34,SBTS,SBMN,2025-01-23T00:14:45.600000Z,2025-01-23T02:54:57.600000Z,2.67,2,DIURNO,,
|
||||
nao_cumprida,,,SIM_035_SBTT_SBMN,35,SBTT,SBMN,2025-01-23T02:00:57.600000Z,2025-01-23T03:12:21.600000Z,1.19,1,DIURNO,,
|
||||
nao_cumprida,,,SIM_036_SBPV_SWBC,36,SBPV,SWBC,2025-01-23T18:49:19.200000Z,2025-01-23T21:00:07.200000Z,2.18,1,DIURNO,,
|
||||
nao_cumprida,,,SIM_037_SBBV_SBMN,37,SBBV,SBMN,2025-01-23T20:18:28.800000Z,2025-01-23T21:50:52.800000Z,1.54,1,DIURNO,,
|
||||
nao_cumprida,,,SIM_038_SBBV_SBBE,38,SBBV,SBBE,2025-01-23T23:08:38.400000Z,2025-01-24T02:18:50.400000Z,3.17,2,DIURNO,,
|
||||
nao_cumprida,,,SIM_039_SBSN_SBMN,39,SBSN,SBMN,2025-01-24T02:44:38.400000Z,2025-01-24T03:59:02.400000Z,1.24,1,DIURNO,,
|
||||
nao_cumprida,,,SIM_040_SBSN_SBMN,40,SBSN,SBMN,2025-01-24T07:16:48Z,2025-01-24T08:30:36Z,1.23,1,DIURNO,,
|
||||
nao_cumprida,,,SIM_042_SBBE_SBSN,42,SBBE,SBSN,2025-01-25T14:54:14.400000Z,2025-01-25T16:35:02.400000Z,1.68,1,DIURNO,,
|
||||
nao_cumprida,,,SIM_043_SBMN_SBPV,43,SBMN,SBPV,2025-01-26T17:47:06Z,2025-01-26T19:21:54Z,1.58,1,DIURNO,,
|
||||
nao_cumprida,,,SIM_044_SBBE_SBPV,44,SBBE,SBPV,2025-01-27T08:03:00Z,2025-01-27T12:37:48Z,4.58,1,DIURNO,,
|
||||
nao_cumprida,,,SIM_046_SBMN_SBUA,46,SBMN,SBUA,2025-01-28T01:26:06Z,2025-01-28T03:14:42Z,1.81,1,DIURNO,,
|
||||
nao_cumprida,,,SIM_047_SWBC_SBTS,47,SWBC,SBTS,2025-01-28T22:14:31.200000Z,2025-01-29T00:03:07.200000Z,1.81,1,DIURNO,,
|
||||
nao_cumprida,,,SIM_040_SBSN_SBMN,40,SBSN,SBMN,2025-01-24T07:16:48Z,2025-01-24T08:30:36Z,1.23,4,DIURNO,,
|
||||
nao_cumprida,,,SIM_043_SBMN_SBPV,43,SBMN,SBPV,2025-01-26T17:47:06Z,2025-01-26T19:21:54Z,1.58,4,DIURNO,,
|
||||
nao_cumprida,,,SIM_044_SBBE_SBPV,44,SBBE,SBPV,2025-01-27T08:03:00Z,2025-01-27T12:37:48Z,4.58,3,DIURNO,,
|
||||
nao_cumprida,,,SIM_046_SBMN_SBUA,46,SBMN,SBUA,2025-01-28T01:26:06Z,2025-01-28T03:14:42Z,1.81,3,DIURNO,,
|
||||
nao_cumprida,,,SIM_047_SWBC_SBTS,47,SWBC,SBTS,2025-01-28T22:14:31.200000Z,2025-01-29T00:03:07.200000Z,1.81,2,DIURNO,,
|
||||
nao_cumprida,,,SIM_048_SBSN_SBUA,48,SBSN,SBUA,2025-01-29T19:44:38.400000Z,2025-01-29T22:51:14.400000Z,3.11,1,DIURNO,,
|
||||
nao_cumprida,,,SIM_049_SBMN_SBMY,49,SBMN,SBMY,2025-01-30T17:21:57.600000Z,2025-01-30T18:08:09.600000Z,0.77,1,DIURNO,,
|
||||
nao_cumprida,,,SIM_049_SBMN_SBMY,49,SBMN,SBMY,2025-01-30T17:21:57.600000Z,2025-01-30T18:08:09.600000Z,0.77,4,DIURNO,,
|
||||
nao_cumprida,,,SIM_050_SBTT_SBPV,50,SBTT,SBPV,2025-01-31T12:02:13.200000Z,2025-01-31T13:22:01.200000Z,1.33,1,DIURNO,,
|
||||
|
||||
|
@@ -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,,
|
||||
|
||||
|
@@ -49,16 +49,72 @@ db/pre_process/relatorio_ciclo_inspecoes_c105_2805_2026-06-15/relatorio_ciclo_in
|
||||
db/pre_process/relatorio_ciclo_inspecoes_c105_2805_2026-06-15/relatorio_ciclo_inspecoes_c105_2805_2026-06-15_inspecoes.csv
|
||||
```
|
||||
|
||||
## Modelo OAMRP inicial
|
||||
## Modelo OAMRP v3 (modelo atual)
|
||||
|
||||
O arquivo `software/oamrp_v1.py` contém um protótipo de modelo MIP em PuLP para o Aircraft Routing do Esquadrão Arara. O modelo usa dados sintéticos/substituíveis de aeronaves e missões e busca maximizar o cumprimento ponderado de missões opcionais.
|
||||
O arquivo `software/oamrp_v3.py` é o modelo principal. Implementa o *Operational Aircraft Maintenance Routing Problem* (Al-Thani, Ben Ahmed, Haouari, 2016, Transportation Research Part C, vol. 72, pp. 29–44) sobre uma rede espaço-tempo com arcos de ferry.
|
||||
|
||||
Restrições representadas no protótipo:
|
||||
### Rede espaço-tempo
|
||||
|
||||
- continuidade espacial e temporal por aeronave;
|
||||
- turnaround mínimo entre missões;
|
||||
- limite de horas de célula até a próxima inspeção;
|
||||
- base inicial e horas acumuladas reais de cada aeronave como parâmetros de entrada.
|
||||
A rede tem quatro tipos de nó:
|
||||
|
||||
| Tipo | Descrição |
|
||||
| --- | --- |
|
||||
| `origem` | Nó de partida de cada aeronave (base inicial, t=0) |
|
||||
| `missao` | Uma missão específica; `aeronave_id=None` (qualquer aeronave pode voar) |
|
||||
| `inspecao` | Nó de manutenção; exclusivo de uma aeronave (`aeronave_id=k`) |
|
||||
| `sumidouro` | Nó de chegada ao fim do horizonte |
|
||||
|
||||
Os arcos são: `inicio`, `missao_missao`, `missao_insp`, `insp_missao`, `missao_fim` e `ferry` (quando bases forem distintas). Ferry usa velocidade 430 km/h e distância haversine.
|
||||
|
||||
### Nomenclatura (Al-Thani, 2016)
|
||||
|
||||
| Símbolo | Significado | Campo no código |
|
||||
| --- | --- | --- |
|
||||
| F | Intervalo máximo legal entre manutenções | `InspecaoParam.f_max` |
|
||||
| f_k | Horas acumuladas no início | `Aeronave.f0` |
|
||||
| LRT = F − f_k | Tempo legal restante (decresce) | `lrt_inicial()` |
|
||||
| H[k,i,nid] | Horas acumuladas em tempo real (cresce, reseta na inspeção) | `H[k,seq,nid]` |
|
||||
|
||||
### Restrições implementadas
|
||||
|
||||
- **C1**: cada missão coberta por no máximo uma aeronave.
|
||||
- **C2**: conservação de fluxo em nós de missão e inspeção.
|
||||
- **C3**: continuidade espacial (base de chegada = base de partida do próximo arco; ferry gerado automaticamente).
|
||||
- **C4**: continuidade temporal (partida ≥ chegada + TAT 1,5h).
|
||||
- **C5**: missões EVAM (`obrigatoria=True`) devem ser cumpridas.
|
||||
- **C6**: `H[k,i,nid] ≤ f_max` para cada tipo de inspeção, com reset após nó de inspeção.
|
||||
- **C9**: downtime de inspeção (aeronave fica indisponível durante o slot de manutenção).
|
||||
- **C11**: slot único de inspeção por base — no máximo uma aeronave em manutenção a cada instante.
|
||||
|
||||
### Objetivos
|
||||
|
||||
- **L1**: maximizar cobertura ponderada `Σ prioridade_m × z_m` (missões cobertas).
|
||||
- **L2**: maximizar horas voadas, dado o L1 fixado.
|
||||
|
||||
### Execução típica
|
||||
|
||||
```powershell
|
||||
python software/oamrp_v3.py --sintetico --seed 42 --aleatorio --ofrag db/processed/ofrag.csv
|
||||
```
|
||||
|
||||
Gera missões sintéticas cross-base, sorteia `f0` aleatório e carrega prioridades do OFRAG.
|
||||
|
||||
### Scripts auxiliares
|
||||
|
||||
| Script | Função |
|
||||
| --- | --- |
|
||||
| `software/gerar_ofrag.py` | Gera `db/processed/ofrag.csv` com ordens de missão (OFRAG), prioridades 1–5 e tipo |
|
||||
| `software/visualizar_resultado.py` | Lê o CSV de resultado e gera `db/processed/mapa_rotas.html` (mapa Folium interativo) |
|
||||
|
||||
### Resultado validado (2026-06-16)
|
||||
|
||||
Rodada sintética com 50 missões, seed 42, horas iniciais aleatórias e OFRAG: 50/50 missões cumpridas, solver CBC Optimal, todas as 4 EVAMs operacionais.
|
||||
|
||||
---
|
||||
|
||||
## Modelo OAMRP v1 (protótipo inicial)
|
||||
|
||||
O arquivo `software/oamrp_v1.py` contém o protótipo inicial, substituído pelo v3. Mantido no repositório para referência histórica.
|
||||
|
||||
Campos extraídos e estruturados:
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,70 @@ 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
|
||||
|
||||
- Bug crítico de conservação de fluxo em nós de inspeção (`oamrp_v3.py`): o solver criava unidades de fluxo fantasma nesses nós, fazendo o relatório exibir 50/50 enquanto o CSV real tinha apenas 16 missões cobertas. Corrigido adicionando C2 para nós `tipo == "inspecao"`.
|
||||
- Bug de restrição C6 Fase 1 simplificada removida: somava horas totais sem respeitar resets de inspeção, tornando EVAMs 2809 e 2811 artificialmente ociosas. As variáveis `H[k,seq,nid]` com reset já impõem o limite corretamente.
|
||||
|
||||
### Adicionado
|
||||
|
||||
- Flag `--aleatorio` na CLI: sorteia `f0` entre 10% e 90% de `F_max` por aeronave e tipo de inspeção.
|
||||
- Tabela OFRAG em `db/processed/ofrag.csv` com 50 ordens sintéticas (prioridades 1–5, tipo de missão, OM).
|
||||
- Script `software/gerar_ofrag.py` para gerar ou regen a tabela OFRAG de forma reprodutível.
|
||||
- Flag `--ofrag` na CLI: carrega prioridades do CSV ao gerar missões sintéticas.
|
||||
- Visualização interativa `db/processed/mapa_rotas.html` via Folium, com uma camada por aeronave, missões não cumpridas tracejadas e popup com detalhes.
|
||||
- Script `software/visualizar_resultado.py` para gerar o mapa a partir do CSV de resultado.
|
||||
|
||||
### Alterado
|
||||
|
||||
- Nomenclatura alinhada com Al-Thani (2016): `lrt_h` → `f_max`; `horas_iniciais` → `f0`; `orcamento_h()` → `lrt_inicial()`; `horas_iniciais_aleatorias()` → `gerar_f0_aleatorio()`; `carregar_horas_iniciais()` → `carregar_f0()`; CLI `--horas-iniciais` → `--f0`.
|
||||
- Relatório imprime `F_max=`, `f0=` e `LRT_ini=` para cada aeronave, alinhado com a formulação do artigo de referência.
|
||||
|
||||
### Resultado validado
|
||||
|
||||
Rodada `--sintetico --seed 42 --aleatorio --ofrag db/processed/ofrag.csv` → 50/50 missões cumpridas, solver CBC Optimal, todas as 4 EVAMs com voos e ferry.
|
||||
|
||||
---
|
||||
|
||||
## v0.6 - VTO - 2026-06
|
||||
|
||||
### Adicionado
|
||||
|
||||
108
software/gerar_ofrag.py
Normal file
108
software/gerar_ofrag.py
Normal file
@@ -0,0 +1,108 @@
|
||||
"""
|
||||
Gera o arquivo de OFRAG (Ordem de Fragmentação) sintético para o Esquadrão Arara.
|
||||
|
||||
Cada OFRAG autoriza uma missão específica e define:
|
||||
- Prioridade operacional (1=urgente … 5=baixa)
|
||||
- Tipo de missão (Logística, Médico, Pessoal, Reabastecimento, Reconhecimento)
|
||||
- Base de origem e destino
|
||||
|
||||
Uso:
|
||||
python software/gerar_ofrag.py --seed 42 --n 50 --saida db/processed/ofrag.csv
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import random
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Importa constantes e função de geração do modelo principal
|
||||
# ---------------------------------------------------------------------------
|
||||
import sys
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
|
||||
from oamrp_v3 import (
|
||||
BASES_COORDS,
|
||||
TIPOS_MISSAO,
|
||||
_PESOS_PRIO,
|
||||
gerar_missoes_sinteticas,
|
||||
)
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parents[1]
|
||||
OFRAG_PADRAO = BASE_DIR / "db" / "processed" / "ofrag.csv"
|
||||
|
||||
_TIPOS_EVAM = ["Médico", "Logística"] # EVAM normalmente carrega carga urgente
|
||||
|
||||
|
||||
def gerar_ofrag(
|
||||
n: int = 50,
|
||||
seed: int = 42,
|
||||
saida: Path = OFRAG_PADRAO,
|
||||
) -> None:
|
||||
inicio = datetime(2025, 1, 1, tzinfo=timezone.utc)
|
||||
missoes, _ = gerar_missoes_sinteticas(n=n, seed=seed, inicio=inicio, n_evam=5)
|
||||
|
||||
rng = random.Random(seed + 1) # semente diferente para tipos
|
||||
|
||||
saida.parent.mkdir(parents=True, exist_ok=True)
|
||||
colunas = [
|
||||
"id_ofrag", "missao_id", "om", "orig", "dest",
|
||||
"prioridade", "tipo_missao", "observacao",
|
||||
]
|
||||
|
||||
with saida.open("w", newline="", encoding="utf-8-sig") as f:
|
||||
w = csv.DictWriter(f, fieldnames=colunas)
|
||||
w.writeheader()
|
||||
for idx, m in enumerate(missoes, start=1):
|
||||
if m.obrigatoria:
|
||||
prio = 1
|
||||
tipo = rng.choice(_TIPOS_EVAM)
|
||||
obs = "EVAM — execução obrigatória"
|
||||
else:
|
||||
prio = rng.choices(range(1, 6), weights=_PESOS_PRIO)[0]
|
||||
tipo = rng.choices(TIPOS_MISSAO, weights=[20, 15, 25, 25, 15])[0]
|
||||
obs = ""
|
||||
w.writerow({
|
||||
"id_ofrag": f"OFRAG-2025-{idx:03d}",
|
||||
"missao_id": m.id,
|
||||
"om": m.om,
|
||||
"orig": m.orig,
|
||||
"dest": m.dest,
|
||||
"prioridade": prio,
|
||||
"tipo_missao": tipo,
|
||||
"observacao": obs,
|
||||
})
|
||||
|
||||
print(f" OFRAG gerado: {saida} ({len(missoes)} ordens)")
|
||||
_resumir(saida)
|
||||
|
||||
|
||||
def _resumir(saida: Path) -> None:
|
||||
from collections import Counter
|
||||
prios: Counter = Counter()
|
||||
tipos: Counter = Counter()
|
||||
with saida.open(newline="", encoding="utf-8-sig") as f:
|
||||
for row in csv.DictReader(f):
|
||||
prios[int(row["prioridade"])] += 1
|
||||
tipos[row["tipo_missao"]] += 1
|
||||
print("\n Distribuição de prioridades:")
|
||||
for p in sorted(prios):
|
||||
bar = "#" * prios[p]
|
||||
print(f" Prio {p}: {prios[p]:3d} {bar}")
|
||||
print("\n Distribuição de tipos:")
|
||||
for t, c in tipos.most_common():
|
||||
print(f" {t:<20} {c:3d}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Gerador de OFRAG sintético — Arara C-105")
|
||||
parser.add_argument("--seed", type=int, default=42)
|
||||
parser.add_argument("--n", type=int, default=50, help="Número de missões")
|
||||
parser.add_argument("--saida", type=Path, default=OFRAG_PADRAO)
|
||||
args = parser.parse_args()
|
||||
gerar_ofrag(n=args.n, seed=args.seed, saida=args.saida)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -30,7 +30,7 @@
|
||||
Removidas: seq 18 (INSP 100FH) e seq 23 (INSP 50FH)
|
||||
|
||||
Horas iniciais f0_{k,i}:
|
||||
--horas-iniciais <arquivo.json ou string JSON>
|
||||
--f0 <arquivo.json ou string JSON>
|
||||
--aleatorio [--seed N]
|
||||
padrão: 0,0 para todas (aeronave recém-inspecionada — conservador)
|
||||
============================================================================
|
||||
@@ -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
|
||||
|
||||
@@ -120,7 +120,7 @@ class InspecaoParam:
|
||||
sigla: str
|
||||
descricao: str
|
||||
nivel: str
|
||||
lrt_h: float
|
||||
f_max: float # F do Al-Thani: intervalo máximo legal entre manutenções
|
||||
cal_dias: Optional[float]
|
||||
dt_h: float
|
||||
|
||||
@@ -129,20 +129,16 @@ class InspecaoParam:
|
||||
class Aeronave:
|
||||
id: str
|
||||
base_inicial: str
|
||||
horas_iniciais: dict[int, float] = field(default_factory=dict)
|
||||
f0: dict[int, float] = field(default_factory=dict) # f_k do Al-Thani: horas acumuladas no início
|
||||
|
||||
def orcamento_h(self, catalogo: dict[int, InspecaoParam]) -> float:
|
||||
"""
|
||||
Orçamento de horas disponíveis (C6 — Fase 1 simplificado).
|
||||
Retorna min(LRT_i − f0_{k,i}) sobre todas as inspeções ativas.
|
||||
"""
|
||||
def lrt_inicial(self, catalogo: dict[int, InspecaoParam]) -> float:
|
||||
"""Retorna min(F_max_i − f0_{k,i}): LRT disponível no início do horizonte."""
|
||||
if not catalogo:
|
||||
return 0.0
|
||||
margens = [
|
||||
insp.lrt_h - self.horas_iniciais.get(seq, 0.0)
|
||||
return max(0.0, min(
|
||||
insp.f_max - self.f0.get(seq, 0.0)
|
||||
for seq, insp in catalogo.items()
|
||||
]
|
||||
return max(0.0, min(margens))
|
||||
))
|
||||
|
||||
def insp_mais_restritiva(
|
||||
self, catalogo: dict[int, InspecaoParam]
|
||||
@@ -151,9 +147,9 @@ class Aeronave:
|
||||
return None
|
||||
seq_min = min(
|
||||
catalogo,
|
||||
key=lambda s: catalogo[s].lrt_h - self.horas_iniciais.get(s, 0.0),
|
||||
key=lambda s: catalogo[s].f_max - self.f0.get(s, 0.0),
|
||||
)
|
||||
return seq_min, self.horas_iniciais.get(seq_min, 0.0), catalogo[seq_min]
|
||||
return seq_min, self.f0.get(seq_min, 0.0), catalogo[seq_min]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -432,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}
|
||||
@@ -477,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)]
|
||||
@@ -488,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:
|
||||
@@ -514,13 +506,16 @@ def resolver(
|
||||
elif n.tipo == "missao":
|
||||
# Missão: fluxo de entrada = fluxo de saída para esta aeronave
|
||||
mdl += entrada_k == saida_k, f"fluxo_{k}_{nid}"
|
||||
elif n.tipo == "inspecao" and n.aeronave_id == k:
|
||||
# Inspeção exclusiva de k: conservação de fluxo (evita criação/destruição)
|
||||
mdl += entrada_k == saida_k, f"fluxo_{k}_{nid}"
|
||||
|
||||
# --- Fase 2: variáveis e restrições C6-reset, C9, C11 ---
|
||||
|
||||
insp_nos = {nid: n for nid, n in nos.items() if n.tipo == "inspecao"}
|
||||
|
||||
# Big-M: usa LRT máximo (para horas) e horizonte temporal (para tempo)
|
||||
M_h = max((insp.lrt_h for insp in catalogo.values()), default=1.0) + 100.0
|
||||
M_h = max((insp.f_max for insp in catalogo.values()), default=1.0) + 100.0
|
||||
M_t = (
|
||||
max((n.t_fim for n in missoes_nos.values()), default=0.0) + 200.0
|
||||
)
|
||||
@@ -551,7 +546,7 @@ def resolver(
|
||||
(k, seq, nid): pulp.LpVariable(
|
||||
f"H_{k}_{seq}_{nid}",
|
||||
lowBound=0,
|
||||
upBound=catalogo[seq].lrt_h,
|
||||
upBound=catalogo[seq].f_max,
|
||||
)
|
||||
for k in ks
|
||||
for seq in catalogo
|
||||
@@ -563,7 +558,7 @@ def resolver(
|
||||
"""Retorna o valor/variável de H_{k,seq} ao sair do nó u."""
|
||||
n = nos[u_nid]
|
||||
if n.tipo == "origem":
|
||||
return acft[k].horas_iniciais.get(seq, 0.0)
|
||||
return acft[k].f0.get(seq, 0.0)
|
||||
# Nó de reset para este clock: saída é 0
|
||||
if n.tipo == "inspecao" and n.insp_seq == seq and n.aeronave_id == k:
|
||||
return 0.0
|
||||
@@ -672,27 +667,17 @@ def resolver(
|
||||
f"c11b_{k1}_{seq1}_{k2}_{seq2}",
|
||||
)
|
||||
|
||||
# --- C6 (Fase 1 — mantido como redundância explícita para verificação) ---
|
||||
for k in ks:
|
||||
orcamento = acft[k].orcamento_h(catalogo)
|
||||
mdl += (
|
||||
pulp.lpSum(
|
||||
n.carga_h * y[(k, a.id)]
|
||||
for nid, n in missoes_nos.items()
|
||||
for a in arcos_entrando[nid]
|
||||
if (k, a.id) in y
|
||||
)
|
||||
<= orcamento,
|
||||
f"orcamento_{k}",
|
||||
)
|
||||
# C6 Fase 1 simplificada foi removida: a propagação de H[k,seq,nid] com reset
|
||||
# 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,
|
||||
@@ -707,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,
|
||||
@@ -798,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"]
|
||||
@@ -821,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
|
||||
@@ -881,24 +894,24 @@ def imprimir_relatorio(
|
||||
insp = catalogo[seq]
|
||||
cal = f"{insp.cal_dias:.0f}d" if insp.cal_dias else "—"
|
||||
print(
|
||||
f" seq {seq:2d} {insp.sigla:<18} LRT={insp.lrt_h:>6.0f}h "
|
||||
f" seq {seq:2d} {insp.sigla:<18} F_max={insp.f_max:>6.0f}h "
|
||||
f"CAL={cal:>5} DT={insp.dt_h:.1f}h [{insp.nivel}]"
|
||||
)
|
||||
|
||||
print("\n Orcamento por aeronave:")
|
||||
for k in sol["ks"]:
|
||||
a = acft[k]
|
||||
oc = a.orcamento_h(catalogo)
|
||||
lrt_ini = a.lrt_inicial(catalogo)
|
||||
info = a.insp_mais_restritiva(catalogo)
|
||||
if info:
|
||||
seq_min, f0, insp = info
|
||||
seq_min, f0_val, insp = info
|
||||
print(
|
||||
f" {k}: {oc:.1f}h "
|
||||
f" {k}: LRT_ini={lrt_ini:.1f}h "
|
||||
f"(limitado por seq {seq_min} {insp.sigla}, "
|
||||
f"f0={f0:.1f}h, LRT={insp.lrt_h:.0f}h)"
|
||||
f"f0={f0_val:.1f}h, F_max={insp.f_max:.0f}h)"
|
||||
)
|
||||
else:
|
||||
print(f" {k}: {oc:.1f}h (sem restricao de inspecao)")
|
||||
print(f" {k}: LRT_ini={lrt_ini:.1f}h (sem restricao de inspecao)")
|
||||
|
||||
print()
|
||||
arcos_por_id = {a.id: a for a in sol["arcos"]}
|
||||
@@ -919,12 +932,12 @@ def imprimir_relatorio(
|
||||
and a.horas_ferry > 0.0
|
||||
)
|
||||
horas_celula = horas_missao + horas_ferry_total
|
||||
orcamento = acft[k].orcamento_h(catalogo)
|
||||
folga = orcamento - horas_celula
|
||||
lrt_ini = acft[k].lrt_inicial(catalogo)
|
||||
folga = lrt_ini - horas_celula
|
||||
print(
|
||||
f" Aeronave {k}: {len(missoes_rota)} missoes | "
|
||||
f"{horas_missao:.1f}h voadas + {horas_ferry_total:.1f}h ferry = {horas_celula:.1f}h celula | "
|
||||
f"folga {folga:.1f}h | {len(insps)} inspecao(oes)"
|
||||
f"LRT_ini={lrt_ini:.1f}h folga={folga:.1f}h | {len(insps)} inspecao(oes)"
|
||||
)
|
||||
# Linha do tempo: missões, inspeções e ferries ordenados por t_ini
|
||||
eventos: list[tuple[float, str]] = []
|
||||
@@ -1096,7 +1109,7 @@ def carregar_inspecoes(caminho: Path) -> dict[int, InspecaoParam]:
|
||||
sigla=item["sigla_mnt"],
|
||||
descricao=item["descricao_mnt"],
|
||||
nivel=nivel_desc,
|
||||
lrt_h=float(lrt),
|
||||
f_max=float(lrt),
|
||||
cal_dias=cal_dias,
|
||||
dt_h=dt_h,
|
||||
)
|
||||
@@ -1144,8 +1157,9 @@ 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]] = {
|
||||
# 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
|
||||
@@ -1156,6 +1170,24 @@ BASES_COORDS: dict[str, tuple[float, float]] = {
|
||||
"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)
|
||||
@@ -1173,13 +1205,117 @@ 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)
|
||||
_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]:
|
||||
"""
|
||||
Gera n missões aleatórias cross-base (orig ≠ dest) entre as bases listadas.
|
||||
@@ -1187,6 +1323,7 @@ def gerar_missoes_sinteticas(
|
||||
Duração estimada via distância haversine ÷ velocidade de cruzeiro do C-105,
|
||||
com variação aleatória de ± VARIACAO_DUR.
|
||||
n_evam missões são marcadas como obrigatórias (hard).
|
||||
Se ofrag_path for fornecido, prioridades são lidas do CSV de OFRAG.
|
||||
"""
|
||||
import math
|
||||
|
||||
@@ -1196,6 +1333,15 @@ def gerar_missoes_sinteticas(
|
||||
bases = list(bases or BASES_COORDS.keys())
|
||||
rng = random.Random(seed)
|
||||
|
||||
# Carrega prioridades do CSV de OFRAG, se fornecido
|
||||
ofrag_prios: dict[str, int] = {}
|
||||
ofrag_tipos: dict[str, str] = {}
|
||||
if ofrag_path and ofrag_path.exists():
|
||||
with ofrag_path.open(newline="", encoding="utf-8-sig") as f:
|
||||
for row in csv.DictReader(f):
|
||||
ofrag_prios[row["missao_id"]] = int(row["prioridade"])
|
||||
ofrag_tipos[row["missao_id"]] = row.get("tipo_missao", "")
|
||||
|
||||
pares = [(o, d) for o in bases for d in bases if o != d]
|
||||
missoes: list[Missao] = []
|
||||
|
||||
@@ -1236,15 +1382,24 @@ def gerar_missoes_sinteticas(
|
||||
obrigatoria = idx in evam_indices
|
||||
cod = f"EVAM{idx+1:03d}" if obrigatoria else f"SIM{idx+1:03d}"
|
||||
om_str = str(idx + 1)
|
||||
missao_id = f"SIM_{idx+1:03d}_{orig}_{dest}"
|
||||
|
||||
# Prioridade: EVAM sempre prio 1 (urgente); demais lidas do OFRAG ou geradas
|
||||
if obrigatoria:
|
||||
prio = 1
|
||||
elif missao_id in ofrag_prios:
|
||||
prio = ofrag_prios[missao_id]
|
||||
else:
|
||||
prio = rng.choices(range(1, 6), weights=_PESOS_PRIO)[0]
|
||||
|
||||
missoes.append(Missao(
|
||||
id=f"SIM_{idx+1:03d}_{orig}_{dest}",
|
||||
id=missao_id,
|
||||
orig=orig,
|
||||
dest=dest,
|
||||
t_dep=t_dep_h,
|
||||
t_arr=t_arr_h,
|
||||
dur_h=dur_h,
|
||||
prioridade=1,
|
||||
prioridade=prio,
|
||||
obrigatoria=obrigatoria,
|
||||
partida_utc=partida,
|
||||
chegada_utc=chegada,
|
||||
@@ -1263,34 +1418,36 @@ def gerar_missoes_sinteticas(
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def horas_iniciais_aleatorias(
|
||||
def gerar_f0_aleatorio(
|
||||
aeronaves: list[str],
|
||||
catalogo: dict[int, InspecaoParam],
|
||||
seed: Optional[int] = None,
|
||||
) -> dict[str, dict[int, float]]:
|
||||
"""Sorteia f0_{k,i} entre 10% e 90% de F_max_i para cada aeronave e inspeção."""
|
||||
rng = random.Random(seed)
|
||||
return {
|
||||
k: {
|
||||
seq: round(rng.uniform(0.1 * insp.lrt_h, 0.9 * insp.lrt_h), 1)
|
||||
seq: round(rng.uniform(0.1 * insp.f_max, 0.9 * insp.f_max), 1)
|
||||
for seq, insp in catalogo.items()
|
||||
}
|
||||
for k in aeronaves
|
||||
}
|
||||
|
||||
|
||||
def carregar_horas_iniciais(
|
||||
def carregar_f0(
|
||||
fonte: Optional[str],
|
||||
aleatorio: bool,
|
||||
aeronaves: list[str],
|
||||
catalogo: dict[int, InspecaoParam],
|
||||
seed: Optional[int] = None,
|
||||
) -> dict[str, dict[int, float]]:
|
||||
"""Retorna dict[aeronave_id -> dict[seq -> f0]] com horas iniciais acumuladas."""
|
||||
if fonte:
|
||||
p = Path(fonte)
|
||||
raw = json.loads(p.read_text(encoding="utf-8")) if p.exists() else json.loads(fonte)
|
||||
return {k: {int(s): float(v) for s, v in d.items()} for k, d in raw.items()}
|
||||
if aleatorio:
|
||||
return horas_iniciais_aleatorias(aeronaves, catalogo, seed=seed)
|
||||
return gerar_f0_aleatorio(aeronaves, catalogo, seed=seed)
|
||||
return {k: {seq: 0.0 for seq in catalogo} for k in aeronaves}
|
||||
|
||||
|
||||
@@ -1309,8 +1466,8 @@ def main() -> None:
|
||||
parser.add_argument("--fim", default=DATA_FIM_PADRAO)
|
||||
parser.add_argument("--resultado", type=Path, default=CSV_RESULTADO_PADRAO)
|
||||
parser.add_argument(
|
||||
"--horas-iniciais", metavar="JSON",
|
||||
help='f0_{k,i}: arquivo ou string JSON. Ex: {"2800": {"23": 35.0}}',
|
||||
"--f0", metavar="JSON", dest="f0",
|
||||
help='f0_{k,i} (horas acumuladas iniciais): arquivo ou string JSON. Ex: {"2800": {"2": 150.0}}',
|
||||
)
|
||||
parser.add_argument(
|
||||
"--aleatorio", action="store_true",
|
||||
@@ -1329,17 +1486,34 @@ 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()
|
||||
|
||||
@@ -1347,8 +1521,14 @@ def main() -> None:
|
||||
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(
|
||||
@@ -1358,6 +1538,7 @@ def main() -> None:
|
||||
horizonte_h=horizonte_h,
|
||||
n_evam=args.n_evam,
|
||||
bases=bases,
|
||||
ofrag_path=args.ofrag,
|
||||
)
|
||||
print(f" Modo sintético: {len(missoes)} missões geradas entre {len(bases)} bases.")
|
||||
else:
|
||||
@@ -1370,20 +1551,21 @@ def main() -> None:
|
||||
hard_pattern=args.hard_pattern,
|
||||
)
|
||||
|
||||
f0 = carregar_horas_iniciais(
|
||||
fonte=args.horas_iniciais,
|
||||
f0_map = carregar_f0(
|
||||
fonte=args.f0,
|
||||
aleatorio=args.aleatorio,
|
||||
aeronaves=args.aeronaves,
|
||||
catalogo=catalogo,
|
||||
seed=args.seed,
|
||||
)
|
||||
aeronaves = [
|
||||
Aeronave(id=k, base_inicial=args.base_inicial, horas_iniciais=f0.get(k, {}))
|
||||
Aeronave(id=k, base_inicial=args.base_inicial, f0=f0_map.get(k, {}))
|
||||
for k in args.aeronaves
|
||||
]
|
||||
|
||||
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)
|
||||
|
||||
|
||||
625
software/visualizar_resultado.py
Normal file
625
software/visualizar_resultado.py
Normal file
@@ -0,0 +1,625 @@
|
||||
"""
|
||||
Visualização interativa do resultado OAMRP — Esquadrão Arara C-105.
|
||||
|
||||
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/planejamento.html
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import io
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from html import escape
|
||||
from pathlib import Path
|
||||
|
||||
import folium
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parents[1]
|
||||
|
||||
RESULTADO_PADRAO = BASE_DIR / "db" / "processed" / "resultado_oamrp_sintetico.csv"
|
||||
SAIDA_PADRAO = BASE_DIR / "db" / "processed" / "planejamento.html"
|
||||
|
||||
# Coordenadas das bases (lat, lon)
|
||||
BASES_COORDS: dict[str, tuple[float, float]] = {
|
||||
"SBMN": (-3.15, -59.99),
|
||||
"SBBE": (-1.38, -48.48),
|
||||
"SBBV": ( 2.84, -60.69),
|
||||
"SBSN": (-2.42, -54.79),
|
||||
"SBPV": (-8.71, -63.90),
|
||||
"SBTS": (-4.25, -69.94),
|
||||
"SBTT": (-3.38, -64.72),
|
||||
"SBUA": (-0.15, -67.05),
|
||||
"SWBC": (-0.98, -62.92),
|
||||
"SBMY": (-5.81, -61.28),
|
||||
}
|
||||
|
||||
BASES_NOMES = {
|
||||
"SBMN": "Manaus / Ponta Pelada (Hub)",
|
||||
"SBBE": "Belém",
|
||||
"SBBV": "Boa Vista",
|
||||
"SBSN": "Santarém",
|
||||
"SBPV": "Porto Velho",
|
||||
"SBTS": "Tabatinga",
|
||||
"SBTT": "Tefé",
|
||||
"SBUA": "São Gabriel da Cachoeira",
|
||||
"SWBC": "Barcelos",
|
||||
"SBMY": "Manicoré",
|
||||
}
|
||||
|
||||
CORES_AERONAVE = {
|
||||
"2800": "#1f77b4",
|
||||
"2803": "#2ca02c",
|
||||
"2809": "#d62728",
|
||||
"2811": "#ff7f0e",
|
||||
}
|
||||
COR_NAO_CUMPRIDA = "#aaaaaa"
|
||||
|
||||
_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:
|
||||
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]:
|
||||
with caminho.open(newline="", encoding="utf-8-sig") as f:
|
||||
return list(csv.DictReader(f))
|
||||
|
||||
|
||||
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")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Mapa Folium → string HTML
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _construir_mapa(linhas: list[dict]) -> str:
|
||||
mapa = folium.Map(location=[-3.5, -62.0], zoom_start=5, tiles="CartoDB positron")
|
||||
|
||||
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"FAB {k}", show=True)
|
||||
grupos["nao_cumprida"] = folium.FeatureGroup(name="Não cumpridas", show=True)
|
||||
|
||||
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
|
||||
k = r["aeronave"]
|
||||
cor = CORES_AERONAVE.get(k, "#333333")
|
||||
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, weight=2.5, opacity=0.8,
|
||||
tooltip=f"FAB {k} | {orig}→{dest} | OM {r['om']} | prio {r['prioridade']}",
|
||||
popup=folium.Popup(
|
||||
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>"
|
||||
f"Chegada: {r['chegada_utc'][:16].replace('T',' ')}Z<br>"
|
||||
f"Duração: {r['dur_h']}h",
|
||||
max_width=250,
|
||||
),
|
||||
).add_to(grupos[k])
|
||||
folium.CircleMarker(
|
||||
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])
|
||||
|
||||
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"])
|
||||
|
||||
for g in grupos.values():
|
||||
g.add_to(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 = """
|
||||
<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);
|
||||
font-family:sans-serif;font-size:12px;">
|
||||
<b>Esquadrão Arara — OAMRP v3</b><br><br>
|
||||
<b>Aeronaves:</b><br>
|
||||
"""
|
||||
for k, cor in CORES_AERONAVE.items():
|
||||
legenda_html += (
|
||||
f' <span style="display:inline-block;width:16px;height:4px;'
|
||||
f'background:{cor};margin-right:6px;vertical-align:middle;"></span>'
|
||||
f'FAB {k}<br>'
|
||||
)
|
||||
legenda_html += """
|
||||
<br><b>Prioridade (preenchimento):</b><br>
|
||||
<span style="display:inline-block;width:12px;height:12px;border-radius:50%;
|
||||
background:red;margin-right:6px;"></span>Prio 1 (urgente)<br>
|
||||
<span style="display:inline-block;width:12px;height:12px;border-radius:50%;
|
||||
background:orange;margin-right:6px;"></span>Prio 2<br>
|
||||
<span style="display:inline-block;width:12px;height:12px;border-radius:50%;
|
||||
background:yellow;margin-right:6px;"></span>Prio 3<br>
|
||||
<span style="display:inline-block;width:12px;height:12px;border-radius:50%;
|
||||
background:lightblue;margin-right:6px;"></span>Prio 4–5<br>
|
||||
<br>
|
||||
<span style="border-bottom:2px dashed #aaa;display:inline-block;width:20px;
|
||||
margin-right:6px;"></span>Não cumprida
|
||||
</div>
|
||||
"""
|
||||
mapa.get_root().html.add_child(folium.Element(legenda_html))
|
||||
|
||||
return mapa.get_root().render()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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:
|
||||
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=SAIDA_PADRAO)
|
||||
args = parser.parse_args()
|
||||
gerar_planejamento(resultado=args.resultado, saida=args.saida)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user