Initial project import
This commit is contained in:
21
.gitignore
vendored
Normal file
21
.gitignore
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
__pycache__/
|
||||||
|
**/__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
.pytest_cache/
|
||||||
|
.mypy_cache/
|
||||||
|
.ruff_cache/
|
||||||
|
|
||||||
|
resultados/
|
||||||
|
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
temp/
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
~$*
|
||||||
|
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
7
.vscode/Escalante Pastor V6.code-workspace
vendored
Normal file
7
.vscode/Escalante Pastor V6.code-workspace
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": ".."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
7
.vscode/planejador_missao_limpo_milp.code-workspace
vendored
Normal file
7
.vscode/planejador_missao_limpo_milp.code-workspace
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": ".."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
18
Abrir Planejador.vbs
Normal file
18
Abrir Planejador.vbs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
Set shell = CreateObject("WScript.Shell")
|
||||||
|
Set fso = CreateObject("Scripting.FileSystemObject")
|
||||||
|
|
||||||
|
baseDir = fso.GetParentFolderName(WScript.ScriptFullName)
|
||||||
|
pythonw = baseDir & "\.venv\Scripts\pythonw.exe"
|
||||||
|
python = baseDir & "\.venv\Scripts\python.exe"
|
||||||
|
app = baseDir & "\web_app.py"
|
||||||
|
|
||||||
|
If fso.FileExists(pythonw) Then
|
||||||
|
exe = pythonw
|
||||||
|
ElseIf fso.FileExists(python) Then
|
||||||
|
exe = python
|
||||||
|
Else
|
||||||
|
exe = "pythonw"
|
||||||
|
End If
|
||||||
|
|
||||||
|
shell.CurrentDirectory = baseDir
|
||||||
|
shell.Run """" & exe & """ """ & app & """", 0, False
|
||||||
216
README.md
Normal file
216
README.md
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
# Planejador Diario de Missoes e Sobreaviso
|
||||||
|
|
||||||
|
Projeto em Python para gerar a escala diaria de missao acionada, missao local e sobreaviso usando Programacao Linear Inteira Mista (MILP).
|
||||||
|
|
||||||
|
## 1. Estrutura do projeto
|
||||||
|
|
||||||
|
```text
|
||||||
|
dados/ Arquivos de entrada do planejamento
|
||||||
|
resultados/ Planilhas geradas pelo planejador
|
||||||
|
src/planejador_missao/ Codigo-fonte Python do modelo
|
||||||
|
scripts/ Scripts numerados de execucao, validacao e teste
|
||||||
|
.vscode/ Arquivos de workspace do VS Code
|
||||||
|
web_app.py Interface web HTML para abrir no navegador
|
||||||
|
run_planner.py Ponto de entrada para executar o planejamento
|
||||||
|
requirements.txt Dependencias Python
|
||||||
|
Abrir Planejador.vbs Atalho para abrir a interface sem prompt
|
||||||
|
```
|
||||||
|
|
||||||
|
A execucao oficial do projeto e feita somente em Python.
|
||||||
|
|
||||||
|
## 2. Preparacao do ambiente
|
||||||
|
|
||||||
|
No PowerShell, a partir da raiz do projeto:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd "C:\caminho\para\planejador_missao_limpo_milp"
|
||||||
|
python -m venv .venv
|
||||||
|
.\.venv\Scripts\activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
A pasta `.venv` nao acompanha a entrega oficial. Ela deve ser recriada pelo usuario com os comandos acima.
|
||||||
|
|
||||||
|
## 3. Como executar pelo app no navegador
|
||||||
|
|
||||||
|
Forma recomendada:
|
||||||
|
|
||||||
|
1. De dois cliques em `Abrir Planejador.vbs`.
|
||||||
|
2. Aguarde o navegador abrir em `http://127.0.0.1:8050`.
|
||||||
|
|
||||||
|
Se preferir executar manualmente pelo PowerShell, use:
|
||||||
|
|
||||||
|
Na raiz do projeto:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python web_app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
O navegador abre automaticamente em `http://127.0.0.1:8050`. Se nao abrir, copie esse endereco no Google Chrome.
|
||||||
|
|
||||||
|
O app permite:
|
||||||
|
|
||||||
|
- editar data e criterios de otimizacao;
|
||||||
|
- informar a missao de rota acionada;
|
||||||
|
- marcar condicao das aeronaves;
|
||||||
|
- selecionar tripulantes disponiveis;
|
||||||
|
- consultar a aba `Quadrinhos`, com proxima OI, horas do ano, SBV e falta para 50 horas;
|
||||||
|
- salvar os arquivos de entrada em `dados/`;
|
||||||
|
- gerar a escala e abrir a ultima planilha.
|
||||||
|
|
||||||
|
A versao final usa Python e interface web local.
|
||||||
|
|
||||||
|
## 4. Como executar pelo terminal
|
||||||
|
|
||||||
|
Na raiz do projeto:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python scripts\00_main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Ao final, o programa informa:
|
||||||
|
|
||||||
|
- data de planejamento usada;
|
||||||
|
- caminho da planilha Excel gerada;
|
||||||
|
- quantidade de colunas candidatas criadas;
|
||||||
|
- quantidade de escalas selecionadas pelo MILP.
|
||||||
|
|
||||||
|
## 5. Arquivos de entrada
|
||||||
|
|
||||||
|
Os arquivos de entrada ficam em `dados/`.
|
||||||
|
|
||||||
|
| Arquivo | Finalidade |
|
||||||
|
| --- | --- |
|
||||||
|
| `Modelagem_C98_ETA2_local.xlsx` ou `Modelagem C98 ETA2.xlsx` | Cadastro base de tripulantes, qualificacoes, projetos, soldo e metas de horas. |
|
||||||
|
| `catalogo_ois.xlsx` | Catalogo de OIs por aeronave, subprograma, ordem e tipo de missao. |
|
||||||
|
| `indisponibilidades_2026.xlsx` | Periodos em que cada tripulante nao pode ser escalado. |
|
||||||
|
| `parametros_missao.csv` | Data de planejamento e criterios de otimizacao. |
|
||||||
|
| `aeronaves_disponiveis.csv` | Condicao diaria de cada aeronave. |
|
||||||
|
| `rotas_acionadas.csv` | Missao de rota acionada para atendimento obrigatorio. |
|
||||||
|
| `tripulantes_disponiveis.csv` | Selecao manual dos tripulantes disponiveis no dia. |
|
||||||
|
| `progresso_ois_2026.xlsx` | OIs ja concluidas manualmente. |
|
||||||
|
| `historico_horas_voadas.csv` | Historico acumulado gerado automaticamente pelo planejador. |
|
||||||
|
|
||||||
|
## 6. Saida gerada
|
||||||
|
|
||||||
|
O planejador cria uma planilha em:
|
||||||
|
|
||||||
|
```text
|
||||||
|
resultados/planejamento_diario_YYYY-MM-DD.xlsx
|
||||||
|
```
|
||||||
|
|
||||||
|
Quando ja existe arquivo para a mesma data, o sistema acrescenta o horario ao nome para preservar a saida anterior.
|
||||||
|
|
||||||
|
A aba `ESCALA DIARIA` contem tres blocos:
|
||||||
|
|
||||||
|
- `MISSAO ACIONADA`;
|
||||||
|
- `VOOS LOCAIS`;
|
||||||
|
- `SOBREAVISO`.
|
||||||
|
|
||||||
|
Apos a solucao, o arquivo `dados/historico_horas_voadas.csv` tambem e atualizado com horas voadas e sobreavisos registrados.
|
||||||
|
|
||||||
|
## 7. Scripts e validacao com voos de 2025
|
||||||
|
|
||||||
|
O fluxo pelo VS Code pode ser executado com:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python scripts\00_main.py --modo diario
|
||||||
|
```
|
||||||
|
|
||||||
|
Para conferir rapidamente o ambiente:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python scripts\09_testar_instalacao.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Para validar o modelo com dados historicos, coloque os voos reais de 2025 em:
|
||||||
|
|
||||||
|
```text
|
||||||
|
dados/validacao/voos_2025.csv
|
||||||
|
```
|
||||||
|
|
||||||
|
O arquivo deve ter as colunas:
|
||||||
|
|
||||||
|
```text
|
||||||
|
data,aeronave,tipo_escala,tripulante,funcao,oi,horas_voadas,sbv
|
||||||
|
```
|
||||||
|
|
||||||
|
Se a planilha `Quadro de Voo 2025 (2).xlsx` estiver em `dados/`, a validacao importa a aba `VOOS` automaticamente:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python scripts\00_main.py --validacao 2025
|
||||||
|
```
|
||||||
|
|
||||||
|
Tambem e possivel chamar diretamente:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python scripts\07_validacao_2025.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Os relatorios sao salvos em:
|
||||||
|
|
||||||
|
```text
|
||||||
|
resultados/validacao/validacao_2025_resumo.xlsx
|
||||||
|
resultados/validacao/validacao_2025_detalhada.xlsx
|
||||||
|
resultados/validacao/validacao_2025_metricas.csv
|
||||||
|
resultados/validacao/validacao_2025_barras.png
|
||||||
|
```
|
||||||
|
|
||||||
|
Ele contem:
|
||||||
|
|
||||||
|
- `metricas`: comparacao entre escala real 2025 e escala otimizada;
|
||||||
|
- `comparativo_trips`: horas reais, horas otimizadas e delta por tripulante;
|
||||||
|
- `voos_2025_slots`: voos historicos agregados em slots;
|
||||||
|
- `escala_otimizada`: redistribuicao proposta pelo MILP.
|
||||||
|
|
||||||
|
Os scripts antigos de importacao/validacao foram preservados em `scripts/_arquivados/`.
|
||||||
|
|
||||||
|
## 8. Metodo de otimizacao
|
||||||
|
|
||||||
|
O modelo monta previamente todas as colunas candidatas viaveis. Cada coluna representa uma possivel escala de dupla para:
|
||||||
|
|
||||||
|
- rota acionada;
|
||||||
|
- missao local;
|
||||||
|
- sobreaviso.
|
||||||
|
|
||||||
|
Cada coluna recebe uma variavel binaria no MILP:
|
||||||
|
|
||||||
|
```text
|
||||||
|
x[i] = 1 se a coluna candidata i for escolhida
|
||||||
|
x[i] = 0 caso contrario
|
||||||
|
```
|
||||||
|
|
||||||
|
A funcao objetivo maximiza o score operacional, com prioridade para:
|
||||||
|
|
||||||
|
1. atender rotas acionadas;
|
||||||
|
2. aproveitar missoes locais para progressao operacional;
|
||||||
|
3. escolher sobreavisos conforme o criterio definido.
|
||||||
|
|
||||||
|
Restricoes principais:
|
||||||
|
|
||||||
|
- cada tripulante aparece no maximo uma vez na escala;
|
||||||
|
- cada aeronave livre recebe exatamente uma cobertura principal, por rota acionada ou sobreaviso;
|
||||||
|
- cada aeronave executa no maximo uma rota acionada;
|
||||||
|
- cada aeronave executa no maximo uma missao local;
|
||||||
|
- rota acionada e missao local nao podem usar a mesma aeronave no mesmo dia;
|
||||||
|
- toda rota acionada informada deve ser atendida exatamente uma vez.
|
||||||
|
|
||||||
|
O resolvedor usado e `scipy.optimize.milp`, um resolvedor exato de programacao inteira disponivel diretamente em Python.
|
||||||
|
|
||||||
|
## 9. Organizacao do codigo
|
||||||
|
|
||||||
|
O codigo em `src/planejador_missao/` esta dividido por blocos funcionais:
|
||||||
|
|
||||||
|
| Arquivo | Responsabilidade |
|
||||||
|
| --- | --- |
|
||||||
|
| `data_io.py` | Leitura das planilhas/CSVs e escrita do historico. |
|
||||||
|
| `rules.py` | Regras de disponibilidade, qualificacao e progresso operacional. |
|
||||||
|
| `candidates.py` | Geracao das colunas candidatas para o MILP. |
|
||||||
|
| `optimizer.py` | Formulacao e solucao do modelo MILP. |
|
||||||
|
| `report.py` | Registro de horas e criacao da planilha Excel. |
|
||||||
|
| `quadrinhos.py` | Resumo operacional da aba Quadrinhos. |
|
||||||
|
| `validacao.py` | Validacao retrospectiva com voos historicos. |
|
||||||
|
| `main.py` | Orquestracao do fluxo completo. |
|
||||||
|
| `utils.py` | Normalizacao de textos, datas, booleanos e horas. |
|
||||||
|
|
||||||
|
Os comentarios no codigo indicam os blocos do fluxo e as restricoes relevantes do modelo, sem repetir conceitos elementares da linguagem.
|
||||||
3
atalhos/Abrir Planejador.cmd
Normal file
3
atalhos/Abrir Planejador.cmd
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
@echo off
|
||||||
|
wscript.exe "%~dp0Abrir Planejador.vbs"
|
||||||
|
exit /b
|
||||||
19
atalhos/Abrir Planejador.vbs
Normal file
19
atalhos/Abrir Planejador.vbs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
Set shell = CreateObject("WScript.Shell")
|
||||||
|
Set fso = CreateObject("Scripting.FileSystemObject")
|
||||||
|
|
||||||
|
atalhosDir = fso.GetParentFolderName(WScript.ScriptFullName)
|
||||||
|
baseDir = fso.GetParentFolderName(atalhosDir)
|
||||||
|
pythonw = baseDir & "\.venv\Scripts\pythonw.exe"
|
||||||
|
python = baseDir & "\.venv\Scripts\python.exe"
|
||||||
|
app = baseDir & "\web_app.py"
|
||||||
|
|
||||||
|
If fso.FileExists(pythonw) Then
|
||||||
|
exe = pythonw
|
||||||
|
ElseIf fso.FileExists(python) Then
|
||||||
|
exe = python
|
||||||
|
Else
|
||||||
|
exe = "pythonw"
|
||||||
|
End If
|
||||||
|
|
||||||
|
shell.CurrentDirectory = baseDir
|
||||||
|
shell.Run """" & exe & """ """ & app & """", 0, False
|
||||||
BIN
dados/Modelagem C98 ETA2.xlsx
Normal file
BIN
dados/Modelagem C98 ETA2.xlsx
Normal file
Binary file not shown.
BIN
dados/Modelagem_C98_ETA2_local.xlsx
Normal file
BIN
dados/Modelagem_C98_ETA2_local.xlsx
Normal file
Binary file not shown.
BIN
dados/Quadro de Voo 2025 (2).xlsx
Normal file
BIN
dados/Quadro de Voo 2025 (2).xlsx
Normal file
Binary file not shown.
4
dados/aeronaves_disponiveis.csv
Normal file
4
dados/aeronaves_disponiveis.csv
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
aeronave,disponivel_sede,em_pane,em_missao_rota,hora_livre
|
||||||
|
C98,True,False,False,50:00
|
||||||
|
C97,True,False,False,50:00
|
||||||
|
C95,True,False,False,50:00
|
||||||
|
BIN
dados/assets/app_logo.png
Normal file
BIN
dados/assets/app_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 MiB |
BIN
dados/assets/favicon.ico
Normal file
BIN
dados/assets/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
BIN
dados/assets/planilha_logo.png
Normal file
BIN
dados/assets/planilha_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 MiB |
BIN
dados/catalogo_ois.xlsx
Normal file
BIN
dados/catalogo_ois.xlsx
Normal file
Binary file not shown.
49
dados/historico_horas_voadas.csv
Normal file
49
dados/historico_horas_voadas.csv
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
data,aeronave,tipo_escala,tripulante,funcao,oi,horas_voadas,sbv,origem_registro,registrado_em
|
||||||
|
2026-06-18,C97,MISSAO_LOCAL,AEU,TRIP2,,1.5,0,escala_diaria,2026-06-18 10:35:24
|
||||||
|
2026-06-18,C97,MISSAO_LOCAL,LPS,TRIP1,17TL17D31,1.5,0,escala_diaria,2026-06-18 10:35:24
|
||||||
|
2026-06-18,C95,ROTA_ACIONADA,ISA,TRIP1,69TF01DN03,6.0,0,escala_diaria,2026-06-18 10:35:24
|
||||||
|
2026-06-18,C95,ROTA_ACIONADA,MES,TRIP2,,6.0,0,escala_diaria,2026-06-18 10:35:24
|
||||||
|
2026-06-18,C97,SBV,MAT,TRIP1,,0.0,1,escala_diaria,2026-06-18 10:35:24
|
||||||
|
2026-06-18,C97,SBV,STS,TRIP2,,0.0,1,escala_diaria,2026-06-18 10:35:24
|
||||||
|
2026-06-22,C97,ROTA_ACIONADA,JVT,TRIP1,00TF86D01,6.0,0,escala_diaria,2026-06-22 08:49:59
|
||||||
|
2026-06-22,C97,ROTA_ACIONADA,AEU,TRIP2,,6.0,0,escala_diaria,2026-06-22 08:49:59
|
||||||
|
2026-06-22,C95,MISSAO_LOCAL,ISA,TRIP1,69TF01D04,1.0,0,escala_diaria,2026-06-22 08:49:59
|
||||||
|
2026-06-22,C95,MISSAO_LOCAL,MES,TRIP2,,1.0,0,escala_diaria,2026-06-22 08:49:59
|
||||||
|
2026-06-22,C95,SBV,BRJ,TRIP1,,0.0,1,escala_diaria,2026-06-22 08:49:59
|
||||||
|
2026-06-22,C95,SBV,KVN,TRIP2,,0.0,1,escala_diaria,2026-06-22 08:49:59
|
||||||
|
2026-06-26,C95,ROTA_ACIONADA,SOL,TRIP1,69TS01D01,6.0,0,escala_diaria,2026-06-26 09:41:10
|
||||||
|
2026-06-26,C95,ROTA_ACIONADA,MES,TRIP2,,6.0,0,escala_diaria,2026-06-26 09:41:10
|
||||||
|
2026-06-26,C97,MISSAO_LOCAL,MHL,TRIP1,01TF01D21,1.5,0,escala_diaria,2026-06-26 09:41:10
|
||||||
|
2026-06-26,C97,MISSAO_LOCAL,MAT,TRIP2,,1.5,0,escala_diaria,2026-06-26 09:41:10
|
||||||
|
2026-06-26,C97,SBV,SEI,TRIP1,,0.0,1,escala_diaria,2026-06-26 09:41:10
|
||||||
|
2026-06-26,C97,SBV,LPS,TRIP2,,0.0,1,escala_diaria,2026-06-26 09:41:10
|
||||||
|
2026-06-26,C95,ROTA_ACIONADA,SOL,TRIP1,69TS01D02,6.0,0,escala_diaria,2026-06-26 12:32:06
|
||||||
|
2026-06-26,C95,ROTA_ACIONADA,MES,TRIP2,,6.0,0,escala_diaria,2026-06-26 12:32:06
|
||||||
|
2026-06-26,C98,MISSAO_LOCAL,MCH,TRIP1,01TL01D31,1.0,0,escala_diaria,2026-06-26 12:32:06
|
||||||
|
2026-06-26,C98,MISSAO_LOCAL,SLS,TRIP2,,1.0,0,escala_diaria,2026-06-26 12:32:06
|
||||||
|
2026-06-26,C98,SBV,GMS,TRIP1,,0.0,1,escala_diaria,2026-06-26 12:32:06
|
||||||
|
2026-06-26,C98,SBV,LRS,TRIP2,,0.0,1,escala_diaria,2026-06-26 12:32:06
|
||||||
|
2026-06-26,C97,MISSAO_LOCAL,LPS,TRIP1,17TL17D32,1.5,0,escala_diaria,2026-06-26 12:32:06
|
||||||
|
2026-06-26,C97,MISSAO_LOCAL,MAT,TRIP2,,1.5,0,escala_diaria,2026-06-26 12:32:06
|
||||||
|
2026-06-26,C97,SBV,FIA,TRIP1,,0.0,1,escala_diaria,2026-06-26 12:32:06
|
||||||
|
2026-06-26,C97,SBV,AEU,TRIP2,,0.0,1,escala_diaria,2026-06-26 12:32:06
|
||||||
|
2026-06-26,C97,ROTA_ACIONADA,LPS,TRIP1,01TL01D41,6.0,0,escala_diaria,2026-06-26 13:35:48
|
||||||
|
2026-06-26,C97,ROTA_ACIONADA,MAT,TRIP2,,6.0,0,escala_diaria,2026-06-26 13:35:48
|
||||||
|
2026-06-26,C98,MISSAO_LOCAL,MCH,TRIP1,04TL01N32,1.0,0,escala_diaria,2026-06-26 13:35:48
|
||||||
|
2026-06-26,C98,MISSAO_LOCAL,SLS,TRIP2,,1.0,0,escala_diaria,2026-06-26 13:35:48
|
||||||
|
2026-06-26,C98,SBV,CFF,TRIP1,,0.0,1,escala_diaria,2026-06-26 13:35:48
|
||||||
|
2026-06-26,C98,SBV,SUG,TRIP2,,0.0,1,escala_diaria,2026-06-26 13:35:48
|
||||||
|
2026-06-26,C95,MISSAO_LOCAL,SOL,TRIP1,06TT36D01,1.0,0,escala_diaria,2026-06-26 13:35:48
|
||||||
|
2026-06-26,C95,MISSAO_LOCAL,MES,TRIP2,,1.0,0,escala_diaria,2026-06-26 13:35:48
|
||||||
|
2026-06-26,C95,SBV,ISA,TRIP1,,0.0,1,escala_diaria,2026-06-26 13:35:48
|
||||||
|
2026-06-26,C95,SBV,ATA,TRIP2,,0.0,1,escala_diaria,2026-06-26 13:35:48
|
||||||
|
2026-06-26,C97,ROTA_ACIONADA,HCK,TRIP1,00TF86D01,6.0,0,escala_diaria,2026-06-26 14:29:27
|
||||||
|
2026-06-26,C97,ROTA_ACIONADA,AEU,TRIP2,,6.0,0,escala_diaria,2026-06-26 14:29:27
|
||||||
|
2026-06-26,C98,MISSAO_LOCAL,LRS,TRIP1,01TT01D01,1.0,0,escala_diaria,2026-06-26 14:29:27
|
||||||
|
2026-06-26,C98,MISSAO_LOCAL,CFF,TRIP2,,1.0,0,escala_diaria,2026-06-26 14:29:27
|
||||||
|
2026-06-26,C98,SBV,SLS,TRIP1,,0.0,1,escala_diaria,2026-06-26 14:29:27
|
||||||
|
2026-06-26,C98,SBV,MCH,TRIP2,,0.0,1,escala_diaria,2026-06-26 14:29:27
|
||||||
|
2026-06-26,C95,MISSAO_LOCAL,SOL,TRIP1,06TT36D02,1.0,0,escala_diaria,2026-06-26 14:29:27
|
||||||
|
2026-06-26,C95,MISSAO_LOCAL,MES,TRIP2,,1.0,0,escala_diaria,2026-06-26 14:29:27
|
||||||
|
2026-06-26,C95,SBV,ISA,TRIP1,,0.0,1,escala_diaria,2026-06-26 14:29:27
|
||||||
|
2026-06-26,C95,SBV,KVN,TRIP2,,0.0,1,escala_diaria,2026-06-26 14:29:27
|
||||||
|
BIN
dados/indisponibilidades_2026.xlsx
Normal file
BIN
dados/indisponibilidades_2026.xlsx
Normal file
Binary file not shown.
4
dados/parametros_missao.csv
Normal file
4
dados/parametros_missao.csv
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
parametro,valor
|
||||||
|
data_planejamento,hoje
|
||||||
|
criterio_missao,meta_50
|
||||||
|
criterio_sbv,equalizar_quadrinhos
|
||||||
|
BIN
dados/progresso_ois_2026.xlsx
Normal file
BIN
dados/progresso_ois_2026.xlsx
Normal file
Binary file not shown.
1
dados/quadrinho_operacional_manual.csv
Normal file
1
dados/quadrinho_operacional_manual.csv
Normal file
@@ -0,0 +1 @@
|
|||||||
|
tripulante,aeronave,qualificacao_manual,horas_voadas_manual,sbv_manual,ultima_data_voo_manual,observacao
|
||||||
|
2
dados/rotas_acionadas.csv
Normal file
2
dados/rotas_acionadas.csv
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
origem,destino,inicio,fim,tev_horas,pernoite_dias,rota
|
||||||
|
NT,NT,2026-06-14 08:00,2026-06-17 13:00,6,3,NT-RF-*FZ*-RF-NT
|
||||||
|
33
dados/tripulantes_disponiveis.csv
Normal file
33
dados/tripulantes_disponiveis.csv
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
tripulante,disponivel
|
||||||
|
AEU,True
|
||||||
|
ATA,True
|
||||||
|
BEN,True
|
||||||
|
BMK,True
|
||||||
|
BRI,True
|
||||||
|
BRJ,True
|
||||||
|
CFF,True
|
||||||
|
DIL,True
|
||||||
|
DOG,True
|
||||||
|
FIA,True
|
||||||
|
GMR,True
|
||||||
|
GMS,True
|
||||||
|
HCK,True
|
||||||
|
ISA,True
|
||||||
|
JVT,True
|
||||||
|
KVN,True
|
||||||
|
LPS,True
|
||||||
|
LRS,True
|
||||||
|
MAT,True
|
||||||
|
MCH,True
|
||||||
|
MDO,True
|
||||||
|
MES,True
|
||||||
|
MHL,True
|
||||||
|
NSC,True
|
||||||
|
PFR,True
|
||||||
|
RCH,True
|
||||||
|
SCA,True
|
||||||
|
SEI,True
|
||||||
|
SLS,True
|
||||||
|
SOL,True
|
||||||
|
STS,True
|
||||||
|
SUG,True
|
||||||
|
769
dados/validacao/voos_2025.csv
Normal file
769
dados/validacao/voos_2025.csv
Normal file
@@ -0,0 +1,769 @@
|
|||||||
|
slot_id,data,aeronave,tipo_escala,tripulante,funcao,oi,horas_voadas,sbv
|
||||||
|
1,2025-01-02,C98,ROTA,SLS,PILOTO,001,6.67,0
|
||||||
|
1,2025-01-02,C98,ROTA,DOG,COPILOTO,001,6.67,0
|
||||||
|
2,2025-01-07,C98,ROTA,SLS,PILOTO,002,6.67,0
|
||||||
|
2,2025-01-07,C98,ROTA,CFF,COPILOTO,002,6.67,0
|
||||||
|
3,2025-01-09,C95,LOCAL,JVT,PILOTO,-,0.08,0
|
||||||
|
3,2025-01-09,C95,LOCAL,GMR,COPILOTO,-,0.08,0
|
||||||
|
4,2025-01-09,C95,LOCAL,HCK,PILOTO,-,0.08,0
|
||||||
|
4,2025-01-09,C95,LOCAL,CAR,COPILOTO,-,0.08,0
|
||||||
|
5,2025-01-09,C97,LOCAL,MES,PILOTO,-,0.08,0
|
||||||
|
5,2025-01-09,C97,LOCAL,SEI,COPILOTO,-,0.08,0
|
||||||
|
6,2025-01-09,C97,LOCAL,MAT,PILOTO,-,0.08,0
|
||||||
|
6,2025-01-09,C97,LOCAL,STS,COPILOTO,-,0.08,0
|
||||||
|
7,2025-01-15,C98,LOCAL,HAI,PILOTO,011,0.08,0
|
||||||
|
7,2025-01-15,C98,LOCAL,GMS,COPILOTO,011,0.08,0
|
||||||
|
8,2025-01-15,C95,ROTA,RAY,PILOTO,003,1.33,0
|
||||||
|
8,2025-01-15,C95,ROTA,HCK,COPILOTO,003,1.33,0
|
||||||
|
9,2025-01-20,C97,LOCAL,MES,PILOTO,004,1.0,0
|
||||||
|
9,2025-01-20,C97,LOCAL,AEU,COPILOTO,004,1.0,0
|
||||||
|
10,2025-01-20,C97,ROTA,MAT,PILOTO,005,5.67,0
|
||||||
|
10,2025-01-20,C97,ROTA,STS,COPILOTO,005,5.67,0
|
||||||
|
11,2025-01-21,C98,LOCAL,CNI,PILOTO,006,1.0,0
|
||||||
|
11,2025-01-21,C98,LOCAL,DOG,COPILOTO,006,1.0,0
|
||||||
|
12,2025-01-22,C98,ROTA,SLS,PILOTO,007,13.0,0
|
||||||
|
12,2025-01-22,C98,ROTA,GMS,COPILOTO,007,13.0,0
|
||||||
|
13,2025-01-24,C98,ROTA,CFF,PILOTO,009,13.67,0
|
||||||
|
13,2025-01-24,C98,ROTA,GMS,COPILOTO,009,13.67,0
|
||||||
|
14,2025-01-24,C98,LOCAL,FER,PILOTO,008,1.0,0
|
||||||
|
14,2025-01-24,C98,LOCAL,MCH,COPILOTO,008,1.0,0
|
||||||
|
15,2025-01-25,C98,ROTA,SLS,PILOTO,010,6.67,0
|
||||||
|
15,2025-01-25,C98,ROTA,DOG,COPILOTO,010,6.67,0
|
||||||
|
16,2025-01-29,C98,ROTA,FER,PILOTO,012,12.33,0
|
||||||
|
16,2025-01-29,C98,ROTA,DOG,COPILOTO,012,12.33,0
|
||||||
|
17,2025-02-03,C98,LOCAL,HAI,PILOTO,014,1.0,0
|
||||||
|
17,2025-02-03,C98,LOCAL,MED,COPILOTO,014,1.0,0
|
||||||
|
18,2025-02-03,C98,LOCAL,HAI,PILOTO,015,1.0,0
|
||||||
|
18,2025-02-03,C98,LOCAL,FIA,COPILOTO,015,1.0,0
|
||||||
|
19,2025-02-04,C98,ROTA,MED,PILOTO,016,5.92,0
|
||||||
|
19,2025-02-04,C98,ROTA,CNI,COPILOTO,016,5.92,0
|
||||||
|
20,2025-02-06,C98,ROTA,FER,PILOTO,018,10.5,0
|
||||||
|
20,2025-02-06,C98,ROTA,CNI,COPILOTO,018,10.5,0
|
||||||
|
21,2025-02-07,C98,ROTA,HAI,PILOTO,020,2.0,0
|
||||||
|
21,2025-02-07,C98,ROTA,FIA,COPILOTO,020,2.0,0
|
||||||
|
22,2025-02-07,C97,ROTA,AEU,PILOTO,021,7.58,0
|
||||||
|
22,2025-02-07,C97,ROTA,SEI,COPILOTO,021,7.58,0
|
||||||
|
23,2025-02-08,C98,ROTA,CFF,PILOTO,022,10.5,0
|
||||||
|
23,2025-02-08,C98,ROTA,FER,COPILOTO,022,10.5,0
|
||||||
|
24,2025-02-09,C97,ROTA,AEU,PILOTO,023,9.42,0
|
||||||
|
24,2025-02-09,C97,ROTA,SEI,COPILOTO,023,9.42,0
|
||||||
|
25,2025-02-11,C98,ROTA,FER,PILOTO,026,3.33,0
|
||||||
|
25,2025-02-11,C98,ROTA,DOG,COPILOTO,026,3.33,0
|
||||||
|
26,2025-02-12,C98,LOCAL,FIA,PILOTO,027,1.0,0
|
||||||
|
26,2025-02-12,C98,LOCAL,GMS,COPILOTO,027,1.0,0
|
||||||
|
27,2025-02-12,C98,ROTA,FER,PILOTO,028,1.67,0
|
||||||
|
27,2025-02-12,C98,ROTA,DOG,COPILOTO,028,1.67,0
|
||||||
|
28,2025-02-12,C98,ROTA,MCH,PILOTO,028,1.67,0
|
||||||
|
28,2025-02-12,C98,ROTA,CNI,COPILOTO,028,1.67,0
|
||||||
|
29,2025-02-12,C98,ROTA,MCH,PILOTO,026,3.33,0
|
||||||
|
29,2025-02-12,C98,ROTA,CNI,COPILOTO,026,3.33,0
|
||||||
|
30,2025-02-13,C98,LOCAL,BRI,PILOTO,029,1.0,0
|
||||||
|
30,2025-02-13,C98,LOCAL,HAI,COPILOTO,029,1.0,0
|
||||||
|
31,2025-02-13,C98,LOCAL,SLS,PILOTO,027,0.5,0
|
||||||
|
31,2025-02-13,C98,LOCAL,DOG,COPILOTO,027,0.5,0
|
||||||
|
32,2025-02-14,C97,ROTA,AEU,PILOTO,030,5.83,0
|
||||||
|
32,2025-02-14,C97,ROTA,SEI,COPILOTO,030,5.83,0
|
||||||
|
33,2025-02-14,C98,LOCAL,BRI,PILOTO,031,1.0,0
|
||||||
|
33,2025-02-14,C98,LOCAL,FIA,COPILOTO,031,1.0,0
|
||||||
|
34,2025-02-16,C95,ROTA,RAY,PILOTO,032,4.83,0
|
||||||
|
34,2025-02-16,C95,ROTA,PCN,COPILOTO,032,4.83,0
|
||||||
|
35,2025-02-16,C97,ROTA,AEU,PILOTO,033,10.67,0
|
||||||
|
35,2025-02-16,C97,ROTA,SEI,COPILOTO,033,10.67,0
|
||||||
|
36,2025-02-17,C98,LOCAL,CNI,PILOTO,CPT,2.0,0
|
||||||
|
36,2025-02-17,C98,LOCAL,LOU,COPILOTO,CPT,2.0,0
|
||||||
|
37,2025-02-17,C98,LOCAL,CNI,PILOTO,CPT,2.0,0
|
||||||
|
37,2025-02-17,C98,LOCAL,PFR,COPILOTO,CPT,2.0,0
|
||||||
|
38,2025-02-17,C98,LOCAL,FER,PILOTO,CPT,2.0,0
|
||||||
|
38,2025-02-17,C98,LOCAL,BEN,COPILOTO,CPT,2.0,0
|
||||||
|
39,2025-02-17,C98,LOCAL,FER,PILOTO,CPT,2.0,0
|
||||||
|
39,2025-02-17,C98,LOCAL,RCH,COPILOTO,CPT,2.0,0
|
||||||
|
40,2025-02-18,C98,LOCAL,CNI,PILOTO,CPT,1.0,0
|
||||||
|
40,2025-02-18,C98,LOCAL,LOU,COPILOTO,CPT,1.0,0
|
||||||
|
41,2025-02-18,C98,LOCAL,FER,PILOTO,CPT,2.0,0
|
||||||
|
41,2025-02-18,C98,LOCAL,SUG,COPILOTO,CPT,2.0,0
|
||||||
|
42,2025-02-18,C98,LOCAL,FER,PILOTO,CPT,2.0,0
|
||||||
|
42,2025-02-18,C98,LOCAL,LRS,COPILOTO,CPT,2.0,0
|
||||||
|
43,2025-02-18,C97,ROTA,AEU,PILOTO,034,7.83,0
|
||||||
|
43,2025-02-18,C97,ROTA,SEI,COPILOTO,034,7.83,0
|
||||||
|
44,2025-02-18,C98,LOCAL,MED,PILOTO,036,1.5,0
|
||||||
|
44,2025-02-18,C98,LOCAL,CNI,COPILOTO,036,1.5,0
|
||||||
|
45,2025-02-18,C98,LOCAL,FIA,PILOTO,037,1.0,0
|
||||||
|
45,2025-02-18,C98,LOCAL,CNI,COPILOTO,037,1.0,0
|
||||||
|
46,2025-02-20,C95,LOCAL,PCN,PILOTO,039,0.08,0
|
||||||
|
46,2025-02-20,C95,LOCAL,JVT,COPILOTO,039,0.08,0
|
||||||
|
47,2025-02-20,C98,LOCAL,CNI,PILOTO,040,1.5,0
|
||||||
|
47,2025-02-20,C98,LOCAL,PFR,COPILOTO,040,1.5,0
|
||||||
|
48,2025-02-20,C98,LOCAL,CNI,PILOTO,041,1.5,0
|
||||||
|
48,2025-02-20,C98,LOCAL,BEN,COPILOTO,041,1.5,0
|
||||||
|
49,2025-02-20,C95,LOCAL,MHL,PILOTO,042,1.0,0
|
||||||
|
49,2025-02-20,C95,LOCAL,KVN,COPILOTO,042,1.0,0
|
||||||
|
50,2025-02-21,C97,ROTA,AEU,PILOTO,048,4.0,0
|
||||||
|
50,2025-02-21,C97,ROTA,SEI,COPILOTO,048,4.0,0
|
||||||
|
51,2025-02-21,C95,ROTA,CAR,PILOTO,043,3.58,0
|
||||||
|
51,2025-02-21,C95,ROTA,BRJ,COPILOTO,043,3.58,0
|
||||||
|
52,2025-02-21,C98,LOCAL,MED,PILOTO,044,1.5,0
|
||||||
|
52,2025-02-21,C98,LOCAL,RCH,COPILOTO,044,1.5,0
|
||||||
|
53,2025-02-21,C98,LOCAL,FER,PILOTO,045,1.5,0
|
||||||
|
53,2025-02-21,C98,LOCAL,LRS,COPILOTO,045,1.5,0
|
||||||
|
54,2025-02-21,C95,LOCAL,MHL,PILOTO,046,1.0,0
|
||||||
|
54,2025-02-21,C95,LOCAL,ISA,COPILOTO,046,1.0,0
|
||||||
|
55,2025-02-24,C98,LOCAL,FER,PILOTO,050,1.5,0
|
||||||
|
55,2025-02-24,C98,LOCAL,SUG,COPILOTO,050,1.5,0
|
||||||
|
56,2025-02-24,C98,LOCAL,FIA,PILOTO,051,1.5,0
|
||||||
|
56,2025-02-24,C98,LOCAL,BEN,COPILOTO,051,1.5,0
|
||||||
|
57,2025-02-24,C97,LOCAL,AEU,PILOTO,047,0.08,0
|
||||||
|
57,2025-02-24,C97,LOCAL,MAT,COPILOTO,047,0.08,0
|
||||||
|
58,2025-02-24,C95,LOCAL,MHL,PILOTO,052,1.0,0
|
||||||
|
58,2025-02-24,C95,LOCAL,ISA,COPILOTO,052,1.0,0
|
||||||
|
59,2025-02-24,C95,LOCAL,MHL,PILOTO,053,1.0,0
|
||||||
|
59,2025-02-24,C95,LOCAL,KVN,COPILOTO,053,1.0,0
|
||||||
|
60,2025-02-25,C98,ROTA,CNI,PILOTO,054,12.42,0
|
||||||
|
60,2025-02-25,C98,ROTA,BEN,COPILOTO,054,12.42,0
|
||||||
|
61,2025-02-25,C97,LOCAL,MES,PILOTO,055,2.0,0
|
||||||
|
61,2025-02-25,C97,LOCAL,AEU,COPILOTO,055,2.0,0
|
||||||
|
62,2025-02-25,C97,LOCAL,MES,PILOTO,056,1.5,0
|
||||||
|
62,2025-02-25,C97,LOCAL,HCK,COPILOTO,056,1.5,0
|
||||||
|
63,2025-02-26,C97,LOCAL,MES,PILOTO,057,1.5,0
|
||||||
|
63,2025-02-26,C97,LOCAL,HCK,COPILOTO,057,1.5,0
|
||||||
|
64,2025-02-27,C98,LOCAL,FER,PILOTO,058,1.5,0
|
||||||
|
64,2025-02-27,C98,LOCAL,LOU,COPILOTO,058,1.5,0
|
||||||
|
65,2025-02-27,C98,LOCAL,MED,PILOTO,059,1.5,0
|
||||||
|
65,2025-02-27,C98,LOCAL,PFR,COPILOTO,059,1.5,0
|
||||||
|
66,2025-02-27,C95,LOCAL,MHL,PILOTO,060,1.0,0
|
||||||
|
66,2025-02-27,C95,LOCAL,BRI,COPILOTO,060,1.0,0
|
||||||
|
67,2025-02-28,C98,LOCAL,MED,PILOTO,061,0.67,0
|
||||||
|
67,2025-02-28,C98,LOCAL,PFR,COPILOTO,061,0.67,0
|
||||||
|
68,2025-02-28,C95,LOCAL,JVT,PILOTO,066,0.08,0
|
||||||
|
68,2025-02-28,C95,LOCAL,HCK,COPILOTO,066,0.08,0
|
||||||
|
69,2025-02-28,C95,ROTA,RAY,PILOTO,062,9.25,0
|
||||||
|
69,2025-02-28,C95,ROTA,ISA,COPILOTO,062,9.25,0
|
||||||
|
70,2025-03-01,C95,ROTA,CAR,PILOTO,063,13.58,0
|
||||||
|
70,2025-03-01,C95,ROTA,BMK,COPILOTO,063,13.58,0
|
||||||
|
71,2025-03-01,C95,ROTA,MHL,PILOTO,064,7.42,0
|
||||||
|
71,2025-03-01,C95,ROTA,KVN,COPILOTO,064,7.42,0
|
||||||
|
72,2025-03-05,C97,ROTA,MES,PILOTO,065,2.0,0
|
||||||
|
72,2025-03-05,C97,ROTA,LPS,COPILOTO,065,2.0,0
|
||||||
|
73,2025-03-05,C95,LOCAL,RAY,PILOTO,067,1.5,0
|
||||||
|
73,2025-03-05,C95,LOCAL,BRI,COPILOTO,067,1.5,0
|
||||||
|
74,2025-03-05,C98,LOCAL,FER,PILOTO,068,1.0,0
|
||||||
|
74,2025-03-05,C98,LOCAL,PFR,COPILOTO,068,1.0,0
|
||||||
|
75,2025-03-06,C95,ROTA,PCN,PILOTO,069,3.92,0
|
||||||
|
75,2025-03-06,C95,ROTA,JVT,COPILOTO,069,3.92,0
|
||||||
|
76,2025-03-06,C98,LOCAL,FIA,PILOTO,070,1.5,0
|
||||||
|
76,2025-03-06,C98,LOCAL,LRS,COPILOTO,070,1.5,0
|
||||||
|
77,2025-03-06,C98,LOCAL,CNI,PILOTO,071,1.5,0
|
||||||
|
77,2025-03-06,C98,LOCAL,RCH,COPILOTO,071,1.5,0
|
||||||
|
78,2025-03-06,C97,LOCAL,MES,PILOTO,072,2.0,0
|
||||||
|
78,2025-03-06,C97,LOCAL,AEU,COPILOTO,072,2.0,0
|
||||||
|
79,2025-03-06,C98,LOCAL,CFF,PILOTO,073,0.08,0
|
||||||
|
79,2025-03-06,C98,LOCAL,GMS,COPILOTO,073,0.08,0
|
||||||
|
80,2025-03-06,C98,LOCAL,BRI,PILOTO,074,1.0,0
|
||||||
|
80,2025-03-06,C98,LOCAL,CNI,COPILOTO,074,1.0,0
|
||||||
|
81,2025-03-06,C97,LOCAL,MES,PILOTO,075,1.0,0
|
||||||
|
81,2025-03-06,C97,LOCAL,HCK,COPILOTO,075,1.0,0
|
||||||
|
82,2025-03-08,C97,ROTA,MES,PILOTO,077,19.33,0
|
||||||
|
82,2025-03-08,C97,ROTA,LPS,COPILOTO,077,19.33,0
|
||||||
|
83,2025-03-10,C98,LOCAL,FER,PILOTO,078,1.5,0
|
||||||
|
83,2025-03-10,C98,LOCAL,LRS,COPILOTO,078,1.5,0
|
||||||
|
84,2025-03-10,C98,LOCAL,GMS,PILOTO,079,1.0,0
|
||||||
|
84,2025-03-10,C98,LOCAL,DOG,COPILOTO,079,1.0,0
|
||||||
|
85,2025-03-11,C97,LOCAL,AEU,PILOTO,080,1.5,0
|
||||||
|
85,2025-03-11,C97,LOCAL,JVT,COPILOTO,080,1.5,0
|
||||||
|
86,2025-03-11,C97,LOCAL,AEU,PILOTO,081,1.0,0
|
||||||
|
86,2025-03-11,C97,LOCAL,MAT,COPILOTO,081,1.0,0
|
||||||
|
87,2025-03-12,C97,LOCAL,AEU,PILOTO,082,1.5,0
|
||||||
|
87,2025-03-12,C97,LOCAL,JVT,COPILOTO,082,1.5,0
|
||||||
|
88,2025-03-12,C98,LOCAL,CNI,PILOTO,083,1.5,0
|
||||||
|
88,2025-03-12,C98,LOCAL,SUG,COPILOTO,083,1.5,0
|
||||||
|
89,2025-03-12,C98,LOCAL,HAI,PILOTO,084,1.0,0
|
||||||
|
89,2025-03-12,C98,LOCAL,MED,COPILOTO,084,1.0,0
|
||||||
|
90,2025-03-13,C98,LOCAL,CNI,PILOTO,085,1.0,0
|
||||||
|
90,2025-03-13,C98,LOCAL,FER,COPILOTO,085,1.0,0
|
||||||
|
91,2025-03-14,C95,ROTA,MHL,PILOTO,086,0.75,0
|
||||||
|
91,2025-03-14,C95,ROTA,ISA,COPILOTO,086,0.75,0
|
||||||
|
92,2025-03-15,C97,ROTA,MAT,PILOTO,087,9.75,0
|
||||||
|
92,2025-03-15,C97,ROTA,LPS,COPILOTO,087,9.75,0
|
||||||
|
93,2025-03-16,C95,ROTA,MHL,PILOTO,086,0.75,0
|
||||||
|
93,2025-03-16,C95,ROTA,ISA,COPILOTO,086,0.75,0
|
||||||
|
94,2025-03-17,C97,LOCAL,MES,PILOTO,089,1.0,0
|
||||||
|
94,2025-03-17,C97,LOCAL,JVT,COPILOTO,089,1.0,0
|
||||||
|
95,2025-03-18,C98,ROTA,CNI,PILOTO,088,6.67,0
|
||||||
|
95,2025-03-18,C98,ROTA,PFR,COPILOTO,088,6.67,0
|
||||||
|
96,2025-03-18,C95,ROTA,PCN,PILOTO,090,5.33,0
|
||||||
|
96,2025-03-18,C95,ROTA,KVN,COPILOTO,090,5.33,0
|
||||||
|
97,2025-03-18,C95,LOCAL,MES,PILOTO,091,1.5,0
|
||||||
|
97,2025-03-18,C95,LOCAL,MHL,COPILOTO,091,1.5,0
|
||||||
|
98,2025-03-18,C98,LOCAL,MCH,PILOTO,092,0.08,0
|
||||||
|
98,2025-03-18,C98,LOCAL,DOG,COPILOTO,092,0.08,0
|
||||||
|
99,2025-03-19,C98,ROTA,SLS,PILOTO,094,6.67,0
|
||||||
|
99,2025-03-19,C98,ROTA,CFF,COPILOTO,094,6.67,0
|
||||||
|
100,2025-03-19,C95,ROTA,MES,PILOTO,093,12.83,0
|
||||||
|
100,2025-03-19,C95,ROTA,RAY,COPILOTO,093,12.83,0
|
||||||
|
101,2025-03-20,C98,LOCAL,FIA,PILOTO,095,1.5,0
|
||||||
|
101,2025-03-20,C98,LOCAL,SLS,COPILOTO,095,1.5,0
|
||||||
|
102,2025-03-20,C98,LOCAL,FER,PILOTO,096,1.0,0
|
||||||
|
102,2025-03-20,C98,LOCAL,LRS,COPILOTO,096,1.0,0
|
||||||
|
103,2025-03-20,C98,LOCAL,FER,PILOTO,097,1.0,0
|
||||||
|
103,2025-03-20,C98,LOCAL,BEN,COPILOTO,097,1.0,0
|
||||||
|
104,2025-03-21,C95,ROTA,BRI,PILOTO,098,10.08,0
|
||||||
|
104,2025-03-21,C95,ROTA,RAY,COPILOTO,098,10.08,0
|
||||||
|
105,2025-03-22,C97,ROTA,MES,PILOTO,099,10.5,0
|
||||||
|
105,2025-03-22,C97,ROTA,MAT,COPILOTO,099,10.5,0
|
||||||
|
106,2025-03-23,C95,ROTA,JVT,PILOTO,100,1.5,0
|
||||||
|
106,2025-03-23,C95,ROTA,ISA,COPILOTO,100,1.5,0
|
||||||
|
107,2025-03-23,C97,ROTA,MES,PILOTO,099,4.5,0
|
||||||
|
107,2025-03-23,C97,ROTA,SEI,COPILOTO,099,4.5,0
|
||||||
|
108,2025-03-24,C95,ROTA,HCK,PILOTO,101,8.67,0
|
||||||
|
108,2025-03-24,C95,ROTA,KVN,COPILOTO,101,8.67,0
|
||||||
|
109,2025-03-24,C98,ROTA,FER,PILOTO,102,18.75,0
|
||||||
|
109,2025-03-24,C98,ROTA,LRS,COPILOTO,102,18.75,0
|
||||||
|
110,2025-03-27,C95,ROTA,CAR,PILOTO,104,3.67,0
|
||||||
|
110,2025-03-27,C95,ROTA,BRJ,COPILOTO,104,3.67,0
|
||||||
|
111,2025-03-27,C95,ROTA,PCN,PILOTO,105,8.33,0
|
||||||
|
111,2025-03-27,C95,ROTA,GMR,COPILOTO,105,8.33,0
|
||||||
|
112,2025-03-27,C97,ROTA,MES,PILOTO,106,9.17,0
|
||||||
|
112,2025-03-27,C97,ROTA,JVT,COPILOTO,106,9.17,0
|
||||||
|
113,2025-03-29,C97,ROTA,MAT,PILOTO,107,10.58,0
|
||||||
|
113,2025-03-29,C97,ROTA,SEI,COPILOTO,107,10.58,0
|
||||||
|
114,2025-03-29,C95,ROTA,MHL,PILOTO,108,9.5,0
|
||||||
|
114,2025-03-29,C95,ROTA,BMK,COPILOTO,108,9.5,0
|
||||||
|
115,2025-03-31,C95,ROTA,JVT,PILOTO,110,4.75,0
|
||||||
|
115,2025-03-31,C95,ROTA,KVN,COPILOTO,110,4.75,0
|
||||||
|
116,2025-03-31,C98,LOCAL,HAI,PILOTO,111,0.33,0
|
||||||
|
116,2025-03-31,C98,LOCAL,MCH,COPILOTO,111,0.33,0
|
||||||
|
117,2025-04-01,C95,LOCAL,BRI,PILOTO,112,1.0,0
|
||||||
|
117,2025-04-01,C95,LOCAL,MHL,COPILOTO,112,1.0,0
|
||||||
|
118,2025-04-02,C98,LOCAL,FIA,PILOTO,113,0.67,0
|
||||||
|
118,2025-04-02,C98,LOCAL,LOU,COPILOTO,113,0.67,0
|
||||||
|
119,2025-04-02,C98,LOCAL,AEU,PILOTO,CPT,2.0,0
|
||||||
|
119,2025-04-02,C98,LOCAL,RAY,COPILOTO,CPT,2.0,0
|
||||||
|
120,2025-04-02,C98,LOCAL,MED,PILOTO,114,1.0,0
|
||||||
|
120,2025-04-02,C98,LOCAL,SUG,COPILOTO,114,1.0,0
|
||||||
|
121,2025-04-02,C97,ROTA,MAT,PILOTO,109,2.75,0
|
||||||
|
121,2025-04-02,C97,ROTA,LPS,COPILOTO,109,2.75,0
|
||||||
|
122,2025-04-03,C97,LOCAL,AEU,PILOTO,115,1.5,0
|
||||||
|
122,2025-04-03,C97,LOCAL,MAT,COPILOTO,115,1.5,0
|
||||||
|
123,2025-04-03,C97,LOCAL,AEU,PILOTO,116,1.0,0
|
||||||
|
123,2025-04-03,C97,LOCAL,SEI,COPILOTO,116,1.0,0
|
||||||
|
124,2025-04-03,C98,LOCAL,CNI,PILOTO,117,1.0,0
|
||||||
|
124,2025-04-03,C98,LOCAL,LOU,COPILOTO,117,1.0,0
|
||||||
|
125,2025-04-04,C97,LOCAL,AEU,PILOTO,118,0.5,0
|
||||||
|
125,2025-04-04,C97,LOCAL,MAT,COPILOTO,118,0.5,0
|
||||||
|
126,2025-04-05,C95,ROTA,CAR,PILOTO,119,4.75,0
|
||||||
|
126,2025-04-05,C95,ROTA,GMR,COPILOTO,119,4.75,0
|
||||||
|
127,2025-04-06,C97,ROTA,MES,PILOTO,120,15.42,0
|
||||||
|
127,2025-04-06,C97,ROTA,JVT,COPILOTO,120,15.42,0
|
||||||
|
128,2025-04-07,C98,LOCAL,FER,PILOTO,121,1.5,0
|
||||||
|
128,2025-04-07,C98,LOCAL,RAY,COPILOTO,121,1.5,0
|
||||||
|
129,2025-04-08,C98,ROTA,BRI,PILOTO,122,5.33,0
|
||||||
|
129,2025-04-08,C98,ROTA,FER,COPILOTO,122,5.33,0
|
||||||
|
130,2025-04-11,C97,ROTA,MES,PILOTO,125,13.58,0
|
||||||
|
130,2025-04-11,C97,ROTA,HCK,COPILOTO,125,13.58,0
|
||||||
|
131,2025-04-11,C95,ROTA,PCN,PILOTO,123,3.92,0
|
||||||
|
131,2025-04-11,C95,ROTA,BMK,COPILOTO,123,3.92,0
|
||||||
|
132,2025-04-14,C98,LOCAL,MED,PILOTO,124,1.5,0
|
||||||
|
132,2025-04-14,C98,LOCAL,AEU,COPILOTO,124,1.5,0
|
||||||
|
133,2025-04-14,C95,LOCAL,RAY,PILOTO,126,1.0,0
|
||||||
|
133,2025-04-14,C95,LOCAL,CAR,COPILOTO,126,1.0,0
|
||||||
|
134,2025-04-14,C98,LOCAL,BRI,PILOTO,127,1.0,0
|
||||||
|
134,2025-04-14,C98,LOCAL,SLS,COPILOTO,127,1.0,0
|
||||||
|
135,2025-04-15,C95,ROTA,CAR,PILOTO,128,9.58,0
|
||||||
|
135,2025-04-15,C95,ROTA,ISA,COPILOTO,128,9.58,0
|
||||||
|
136,2025-04-15,C98,LOCAL,MED,PILOTO,129,1.5,0
|
||||||
|
136,2025-04-15,C98,LOCAL,RAY,COPILOTO,129,1.5,0
|
||||||
|
137,2025-04-15,C98,LOCAL,FER,PILOTO,130,1.5,0
|
||||||
|
137,2025-04-15,C98,LOCAL,AEU,COPILOTO,130,1.5,0
|
||||||
|
138,2025-04-15,C98,LOCAL,FIA,PILOTO,131,1.0,0
|
||||||
|
138,2025-04-15,C98,LOCAL,RCH,COPILOTO,131,1.0,0
|
||||||
|
139,2025-04-17,C97,LOCAL,MES,PILOTO,132,1.0,0
|
||||||
|
139,2025-04-17,C97,LOCAL,MAT,COPILOTO,132,1.0,0
|
||||||
|
140,2025-04-18,C95,ROTA,BRJ,PILOTO,133,7.5,0
|
||||||
|
140,2025-04-18,C95,ROTA,BMK,COPILOTO,133,7.5,0
|
||||||
|
141,2025-04-21,C98,ROTA,MED,PILOTO,134,11.92,0
|
||||||
|
141,2025-04-21,C98,ROTA,SUG,COPILOTO,134,11.92,0
|
||||||
|
142,2025-04-22,C95,ROTA,BRI,PILOTO,135,12.67,0
|
||||||
|
142,2025-04-22,C95,ROTA,GMR,COPILOTO,135,12.67,0
|
||||||
|
143,2025-04-22,C98,LOCAL,CFF,PILOTO,136,0.08,0
|
||||||
|
143,2025-04-22,C98,LOCAL,DOG,COPILOTO,136,0.08,0
|
||||||
|
144,2025-04-25,C98,ROTA,MED,PILOTO,137,6.58,0
|
||||||
|
144,2025-04-25,C98,ROTA,MCH,COPILOTO,137,6.58,0
|
||||||
|
145,2025-04-26,C98,ROTA,MCH,PILOTO,139,11.5,0
|
||||||
|
145,2025-04-26,C98,ROTA,DOG,COPILOTO,139,11.5,0
|
||||||
|
146,2025-04-29,C98,LOCAL,SLS,PILOTO,141,1.0,0
|
||||||
|
146,2025-04-29,C98,LOCAL,FER,COPILOTO,141,1.0,0
|
||||||
|
147,2025-04-30,C97,ROTA,SEI,PILOTO,140,11.5,0
|
||||||
|
147,2025-04-30,C97,ROTA,LPS,COPILOTO,140,11.5,0
|
||||||
|
148,2025-05-04,C97,ROTA,MAT,PILOTO,142,5.67,0
|
||||||
|
148,2025-05-04,C97,ROTA,JVT,COPILOTO,142,5.67,0
|
||||||
|
149,2025-05-05,C95,ROTA,BRJ,PILOTO,143,7.25,0
|
||||||
|
149,2025-05-05,C95,ROTA,ISA,COPILOTO,143,7.25,0
|
||||||
|
150,2025-05-05,C98,LOCAL,HAI,PILOTO,144,1.0,0
|
||||||
|
150,2025-05-05,C98,LOCAL,AEU,COPILOTO,144,1.0,0
|
||||||
|
151,2025-05-05,C98,LOCAL,CNI,PILOTO,145,1.0,0
|
||||||
|
151,2025-05-05,C98,LOCAL,RAY,COPILOTO,145,1.0,0
|
||||||
|
152,2025-05-06,C98,ROTA,MED,PILOTO,146,6.33,0
|
||||||
|
152,2025-05-06,C98,ROTA,PFR,COPILOTO,146,6.33,0
|
||||||
|
153,2025-05-07,C95,ROTA,PCN,PILOTO,147,3.67,0
|
||||||
|
153,2025-05-07,C95,ROTA,GMR,COPILOTO,147,3.67,0
|
||||||
|
154,2025-05-07,C98,LOCAL,SLS,PILOTO,148,1.0,0
|
||||||
|
154,2025-05-07,C98,LOCAL,GMS,COPILOTO,148,1.0,0
|
||||||
|
155,2025-05-08,C98,ROTA,FER,PILOTO,150,6.67,0
|
||||||
|
155,2025-05-08,C98,ROTA,MCH,COPILOTO,150,6.67,0
|
||||||
|
156,2025-05-08,C98,ROTA,HAI,PILOTO,149,6.33,0
|
||||||
|
156,2025-05-08,C98,ROTA,LRS,COPILOTO,149,6.33,0
|
||||||
|
157,2025-05-09,C95,ROTA,CAR,PILOTO,151,3.58,0
|
||||||
|
157,2025-05-09,C95,ROTA,KVN,COPILOTO,151,3.58,0
|
||||||
|
158,2025-05-09,C98,ROTA,MED,PILOTO,152,17.75,0
|
||||||
|
158,2025-05-09,C98,ROTA,BEN,COPILOTO,152,17.75,0
|
||||||
|
159,2025-05-09,C95,ROTA,RAY,PILOTO,153,5.33,0
|
||||||
|
159,2025-05-09,C95,ROTA,BMK,COPILOTO,153,5.33,0
|
||||||
|
160,2025-05-10,C95,ROTA,BRJ,PILOTO,154,11.58,0
|
||||||
|
160,2025-05-10,C95,ROTA,ISA,COPILOTO,154,11.58,0
|
||||||
|
161,2025-05-12,C95,ROTA,PCN,PILOTO,155,7.58,0
|
||||||
|
161,2025-05-12,C95,ROTA,GMR,COPILOTO,155,7.58,0
|
||||||
|
162,2025-05-13,C98,ROTA,BRI,PILOTO,156,4.83,0
|
||||||
|
162,2025-05-13,C98,ROTA,RAY,COPILOTO,156,4.83,0
|
||||||
|
163,2025-05-14,C95,ROTA,CAR,PILOTO,157,8.0,0
|
||||||
|
163,2025-05-14,C95,ROTA,KVN,COPILOTO,157,8.0,0
|
||||||
|
164,2025-05-14,C95,ROTA,CAR,PILOTO,157,8.75,0
|
||||||
|
164,2025-05-14,C95,ROTA,KVN,COPILOTO,157,8.75,0
|
||||||
|
165,2025-05-19,C95,ROTA,HCK,PILOTO,158,3.58,0
|
||||||
|
165,2025-05-19,C95,ROTA,BMK,COPILOTO,158,3.58,0
|
||||||
|
166,2025-05-20,C95,ROTA,MHL,PILOTO,159,11.17,0
|
||||||
|
166,2025-05-20,C95,ROTA,GMR,COPILOTO,159,11.17,0
|
||||||
|
167,2025-05-20,C97,LOCAL,MAT,PILOTO,160,1.0,0
|
||||||
|
167,2025-05-20,C97,LOCAL,SEI,COPILOTO,160,1.0,0
|
||||||
|
168,2025-05-21,C98,LOCAL,CNI,PILOTO,161,1.0,0
|
||||||
|
168,2025-05-21,C98,LOCAL,MCH,COPILOTO,161,1.0,0
|
||||||
|
169,2025-05-22,C98,ROTA,AEU,PILOTO,162,9.42,0
|
||||||
|
169,2025-05-22,C98,ROTA,SLS,COPILOTO,162,9.42,0
|
||||||
|
170,2025-05-22,C97,ROTA,MAT,PILOTO,163,11.08,0
|
||||||
|
170,2025-05-22,C97,ROTA,JVT,COPILOTO,163,11.08,0
|
||||||
|
171,2025-05-23,C98,LOCAL,FIA,PILOTO,164,1.0,0
|
||||||
|
171,2025-05-23,C98,LOCAL,DOG,COPILOTO,164,1.0,0
|
||||||
|
172,2025-05-25,C97,ROTA,SEI,PILOTO,165,9.67,0
|
||||||
|
172,2025-05-25,C97,ROTA,LPS,COPILOTO,165,9.67,0
|
||||||
|
173,2025-05-26,C98,ROTA,HAI,PILOTO,166,11.92,0
|
||||||
|
173,2025-05-26,C98,ROTA,PFR,COPILOTO,166,11.92,0
|
||||||
|
174,2025-05-27,C98,ROTA,BRI,PILOTO,166,3.33,0
|
||||||
|
174,2025-05-27,C98,ROTA,LRS,COPILOTO,166,3.33,0
|
||||||
|
175,2025-05-29,C98,ROTA,MED,PILOTO,167,4.92,0
|
||||||
|
175,2025-05-29,C98,ROTA,RCH,COPILOTO,167,4.92,0
|
||||||
|
176,2025-05-29,C98,LOCAL,MCH,PILOTO,168,1.0,0
|
||||||
|
176,2025-05-29,C98,LOCAL,DOG,COPILOTO,168,1.0,0
|
||||||
|
177,2025-05-30,C98,ROTA,SLS,PILOTO,169,14.33,0
|
||||||
|
177,2025-05-30,C98,ROTA,PFR,COPILOTO,169,14.33,0
|
||||||
|
178,2025-06-01,C98,ROTA,MCH,PILOTO,170,3.33,0
|
||||||
|
178,2025-06-01,C98,ROTA,GMS,COPILOTO,170,3.33,0
|
||||||
|
179,2025-06-01,C98,ROTA,MED,PILOTO,170,6.83,0
|
||||||
|
179,2025-06-01,C98,ROTA,LRS,COPILOTO,170,6.83,0
|
||||||
|
180,2025-06-02,C98,ROTA,CNI,PILOTO,171,5.67,0
|
||||||
|
180,2025-06-02,C98,ROTA,RCH,COPILOTO,171,5.67,0
|
||||||
|
181,2025-06-03,C98,ROTA,CNI,PILOTO,171,11.67,0
|
||||||
|
181,2025-06-03,C98,ROTA,RCH,COPILOTO,171,11.67,0
|
||||||
|
182,2025-06-03,C98,ROTA,BRI,PILOTO,172,2.0,0
|
||||||
|
182,2025-06-03,C98,ROTA,LRS,COPILOTO,172,2.0,0
|
||||||
|
183,2025-06-03,C98,ROTA,MCH,PILOTO,172,4.0,0
|
||||||
|
183,2025-06-03,C98,ROTA,DOG,COPILOTO,172,4.0,0
|
||||||
|
184,2025-06-04,C98,ROTA,MCH,PILOTO,173,5.0,0
|
||||||
|
184,2025-06-04,C98,ROTA,GMS,COPILOTO,173,5.0,0
|
||||||
|
185,2025-06-06,C98,ROTA,SLS,PILOTO,174,13.33,0
|
||||||
|
185,2025-06-06,C98,ROTA,LRS,COPILOTO,174,13.33,0
|
||||||
|
186,2025-06-06,C97,LOCAL,MAT,PILOTO,175,1.0,0
|
||||||
|
186,2025-06-06,C97,LOCAL,SEI,COPILOTO,175,1.0,0
|
||||||
|
187,2025-06-07,C97,ROTA,SEI,PILOTO,178,11.92,0
|
||||||
|
187,2025-06-07,C97,ROTA,LPS,COPILOTO,178,11.92,0
|
||||||
|
188,2025-06-07,C95,LOCAL,MES,PILOTO,179,1.0,0
|
||||||
|
188,2025-06-07,C95,LOCAL,BMK,COPILOTO,179,1.0,0
|
||||||
|
189,2025-06-08,C97,ROTA,MES,PILOTO,176,9.5,0
|
||||||
|
189,2025-06-08,C97,ROTA,AEU,COPILOTO,176,9.5,0
|
||||||
|
190,2025-06-08,C98,ROTA,MCH,PILOTO,177,16.33,0
|
||||||
|
190,2025-06-08,C98,ROTA,GMS,COPILOTO,177,16.33,0
|
||||||
|
191,2025-06-10,C95,ROTA,PCN,PILOTO,180,9.33,0
|
||||||
|
191,2025-06-10,C95,ROTA,ISA,COPILOTO,180,9.33,0
|
||||||
|
192,2025-06-12,C95,ROTA,GMR,PILOTO,181,11.5,0
|
||||||
|
192,2025-06-12,C95,ROTA,BMK,COPILOTO,181,11.5,0
|
||||||
|
193,2025-06-12,C98,ROTA,HAI,PILOTO,182,11.08,0
|
||||||
|
193,2025-06-12,C98,ROTA,SUG,COPILOTO,182,11.08,0
|
||||||
|
194,2025-06-12,C97,LOCAL,AEU,PILOTO,183,1.5,0
|
||||||
|
194,2025-06-12,C97,LOCAL,MHL,COPILOTO,183,1.5,0
|
||||||
|
195,2025-06-14,C95,ROTA,MHL,PILOTO,184,1.5,0
|
||||||
|
195,2025-06-14,C95,ROTA,JVT,COPILOTO,184,1.5,0
|
||||||
|
196,2025-06-15,C95,ROTA,BRJ,PILOTO,185,9.33,0
|
||||||
|
196,2025-06-15,C95,ROTA,ISA,COPILOTO,185,9.33,0
|
||||||
|
197,2025-06-17,C95,LOCAL,HCK,PILOTO,186,0.67,0
|
||||||
|
197,2025-06-17,C95,LOCAL,KVN,COPILOTO,186,0.67,0
|
||||||
|
198,2025-06-18,C95,ROTA,BMK,PILOTO,187,9.5,0
|
||||||
|
198,2025-06-18,C95,ROTA,ISA,COPILOTO,187,9.5,0
|
||||||
|
199,2025-06-18,C98,LOCAL,MCH,PILOTO,189,1.0,0
|
||||||
|
199,2025-06-18,C98,LOCAL,GMS,COPILOTO,189,1.0,0
|
||||||
|
200,2025-06-18,C95,ROTA,GMR,PILOTO,191,8.0,0
|
||||||
|
200,2025-06-18,C95,ROTA,KVN,COPILOTO,191,8.0,0
|
||||||
|
201,2025-06-21,C98,ROTA,SLS,PILOTO,192,2.0,0
|
||||||
|
201,2025-06-21,C98,ROTA,AEU,COPILOTO,192,2.0,0
|
||||||
|
202,2025-06-21,C97,ROTA,MAT,PILOTO,193,10.0,0
|
||||||
|
202,2025-06-21,C97,ROTA,JVT,COPILOTO,193,10.0,0
|
||||||
|
203,2025-06-23,C97,LOCAL,MAT,PILOTO,190,1.5,0
|
||||||
|
203,2025-06-23,C97,LOCAL,MHL,COPILOTO,190,1.5,0
|
||||||
|
204,2025-06-23,C98,LOCAL,MCH,PILOTO,194,1.0,0
|
||||||
|
204,2025-06-23,C98,LOCAL,DOG,COPILOTO,194,1.0,0
|
||||||
|
205,2025-06-24,C97,LOCAL,AEU,PILOTO,195,1.0,0
|
||||||
|
205,2025-06-24,C97,LOCAL,MHL,COPILOTO,195,1.0,0
|
||||||
|
206,2025-06-25,C97,LOCAL,AEU,PILOTO,198,1.0,0
|
||||||
|
206,2025-06-25,C97,LOCAL,MES,COPILOTO,198,1.0,0
|
||||||
|
207,2025-06-25,C98,LOCAL,SLS,PILOTO,199,1.0,0
|
||||||
|
207,2025-06-25,C98,LOCAL,CFF,COPILOTO,199,1.0,0
|
||||||
|
208,2025-06-25,C98,LOCAL,CNI,PILOTO,200,1.0,0
|
||||||
|
208,2025-06-25,C98,LOCAL,FER,COPILOTO,200,1.0,0
|
||||||
|
209,2025-06-26,C97,ROTA,MAT,PILOTO,202,9.92,0
|
||||||
|
209,2025-06-26,C97,ROTA,MHL,COPILOTO,202,9.92,0
|
||||||
|
210,2025-06-26,C98,LOCAL,MED,PILOTO,201,0.67,0
|
||||||
|
210,2025-06-26,C98,LOCAL,CFF,COPILOTO,201,0.67,0
|
||||||
|
211,2025-06-26,C98,LOCAL,FER,PILOTO,203,0.25,0
|
||||||
|
211,2025-06-26,C98,LOCAL,MCH,COPILOTO,203,0.25,0
|
||||||
|
212,2025-06-26,C98,LOCAL,FER,PILOTO,204,0.25,0
|
||||||
|
212,2025-06-26,C98,LOCAL,DOG,COPILOTO,204,0.25,0
|
||||||
|
213,2025-06-27,C95,LOCAL,BRI,PILOTO,205,1.0,0
|
||||||
|
213,2025-06-27,C95,LOCAL,RAY,COPILOTO,205,1.0,0
|
||||||
|
214,2025-06-29,C97,ROTA,MAT,PILOTO,207,10.67,0
|
||||||
|
214,2025-06-29,C97,ROTA,MHL,COPILOTO,207,10.67,0
|
||||||
|
215,2025-06-30,C98,ROTA,CNI,PILOTO,208,6.67,0
|
||||||
|
215,2025-06-30,C98,ROTA,PFR,COPILOTO,208,6.67,0
|
||||||
|
216,2025-07-01,C98,ROTA,SLS,PILOTO,211,10.92,0
|
||||||
|
216,2025-07-01,C98,ROTA,SUG,COPILOTO,211,10.92,0
|
||||||
|
217,2025-07-01,C95,LOCAL,RAY,PILOTO,206,1.0,0
|
||||||
|
217,2025-07-01,C95,LOCAL,BMK,COPILOTO,206,1.0,0
|
||||||
|
218,2025-07-01,C95,ROTA,PCN,PILOTO,209,5.92,0
|
||||||
|
218,2025-07-01,C95,ROTA,KVN,COPILOTO,209,5.92,0
|
||||||
|
219,2025-07-02,C98,ROTA,RAY,PILOTO,212,3.17,0
|
||||||
|
219,2025-07-02,C98,ROTA,CNI,COPILOTO,212,3.17,0
|
||||||
|
220,2025-07-04,C98,ROTA,AEU,PILOTO,214,5.0,0
|
||||||
|
220,2025-07-04,C98,ROTA,SLS,COPILOTO,214,5.0,0
|
||||||
|
221,2025-07-04,C98,ROTA,MED,PILOTO,213,6.92,0
|
||||||
|
221,2025-07-04,C98,ROTA,RAY,COPILOTO,213,6.92,0
|
||||||
|
222,2025-07-04,C97,LOCAL,MES,PILOTO,210,1.5,0
|
||||||
|
222,2025-07-04,C97,LOCAL,FIA,COPILOTO,210,1.5,0
|
||||||
|
223,2025-07-05,C97,LOCAL,MAT,PILOTO,216,1.5,0
|
||||||
|
223,2025-07-05,C97,LOCAL,FIA,COPILOTO,216,1.5,0
|
||||||
|
224,2025-07-07,C98,ROTA,FER,PILOTO,215,5.92,0
|
||||||
|
224,2025-07-07,C98,ROTA,RCH,COPILOTO,215,5.92,0
|
||||||
|
225,2025-07-07,C97,LOCAL,AEU,PILOTO,217,1.0,0
|
||||||
|
225,2025-07-07,C97,LOCAL,FIA,COPILOTO,217,1.0,0
|
||||||
|
226,2025-07-10,C98,ROTA,CNI,PILOTO,218,12.17,0
|
||||||
|
226,2025-07-10,C98,ROTA,SUG,COPILOTO,218,12.17,0
|
||||||
|
227,2025-07-11,C97,ROTA,MAT,PILOTO,219,6.83,0
|
||||||
|
227,2025-07-11,C97,ROTA,FIA,COPILOTO,219,6.83,0
|
||||||
|
228,2025-07-11,C98,ROTA,FER,PILOTO,220,13.67,0
|
||||||
|
228,2025-07-11,C98,ROTA,LRS,COPILOTO,220,13.67,0
|
||||||
|
229,2025-07-12,C97,ROTA,FIA,PILOTO,221,10.25,0
|
||||||
|
229,2025-07-12,C97,ROTA,LPS,COPILOTO,221,10.25,0
|
||||||
|
230,2025-07-12,C98,ROTA,CFF,PILOTO,223,8.0,0
|
||||||
|
230,2025-07-12,C98,ROTA,MCH,COPILOTO,223,8.0,0
|
||||||
|
231,2025-07-14,C98,ROTA,GMS,PILOTO,224,4.83,0
|
||||||
|
231,2025-07-14,C98,ROTA,DOG,COPILOTO,224,4.83,0
|
||||||
|
232,2025-07-14,C98,ROTA,MCH,PILOTO,222,9.75,0
|
||||||
|
232,2025-07-14,C98,ROTA,CFF,COPILOTO,222,9.75,0
|
||||||
|
233,2025-07-15,C98,ROTA,CNI,PILOTO,225,4.83,0
|
||||||
|
233,2025-07-15,C98,ROTA,PFR,COPILOTO,225,4.83,0
|
||||||
|
234,2025-07-16,C97,ROTA,FIA,PILOTO,226,6.17,0
|
||||||
|
234,2025-07-16,C97,ROTA,LPS,COPILOTO,226,6.17,0
|
||||||
|
235,2025-07-19,C98,ROTA,GMS,PILOTO,228,17.83,0
|
||||||
|
235,2025-07-19,C98,ROTA,DOG,COPILOTO,228,17.83,0
|
||||||
|
236,2025-07-21,C98,ROTA,SLS,PILOTO,228,3.33,0
|
||||||
|
236,2025-07-21,C98,ROTA,AEU,COPILOTO,228,3.33,0
|
||||||
|
237,2025-08-01,C98,LOCAL,CFF,PILOTO,188,1.0,0
|
||||||
|
237,2025-08-01,C98,LOCAL,MCH,COPILOTO,188,1.0,0
|
||||||
|
238,2025-08-04,C95,LOCAL,BMK,PILOTO,229,0.08,0
|
||||||
|
238,2025-08-04,C95,LOCAL,KVN,COPILOTO,229,0.08,0
|
||||||
|
239,2025-08-06,C98,ROTA,MCH,PILOTO,231,6.67,0
|
||||||
|
239,2025-08-06,C98,ROTA,GMS,COPILOTO,231,6.67,0
|
||||||
|
240,2025-08-12,C95,LOCAL,BRI,PILOTO,232,1.0,0
|
||||||
|
240,2025-08-12,C95,LOCAL,GMR,COPILOTO,232,1.0,0
|
||||||
|
241,2025-08-16,C97,LOCAL,MES,PILOTO,233,0.92,0
|
||||||
|
241,2025-08-16,C97,LOCAL,SEI,COPILOTO,233,0.92,0
|
||||||
|
242,2025-08-16,C97,LOCAL,FIA,PILOTO,234,0.08,0
|
||||||
|
242,2025-08-16,C97,LOCAL,LPS,COPILOTO,234,0.08,0
|
||||||
|
243,2025-08-18,C98,ROTA,RAY,PILOTO,235,6.67,0
|
||||||
|
243,2025-08-18,C98,ROTA,SLS,COPILOTO,235,6.67,0
|
||||||
|
244,2025-08-21,C95,LOCAL,BRI,PILOTO,236,1.0,0
|
||||||
|
244,2025-08-21,C95,LOCAL,HCK,COPILOTO,236,1.0,0
|
||||||
|
245,2025-08-25,C98,LOCAL,FIA,PILOTO,237,1.0,0
|
||||||
|
245,2025-08-25,C98,LOCAL,SLS,COPILOTO,237,1.0,0
|
||||||
|
246,2025-08-25,C98,LOCAL,FIA,PILOTO,238,0.08,0
|
||||||
|
246,2025-08-25,C98,LOCAL,DOG,COPILOTO,238,0.08,0
|
||||||
|
247,2025-08-26,C98,LOCAL,FIA,PILOTO,239,1.0,0
|
||||||
|
247,2025-08-26,C98,LOCAL,LRS,COPILOTO,239,1.0,0
|
||||||
|
248,2025-08-26,C97,LOCAL,MES,PILOTO,240,1.0,0
|
||||||
|
248,2025-08-26,C97,LOCAL,AEU,COPILOTO,240,1.0,0
|
||||||
|
249,2025-08-26,C98,LOCAL,FIA,PILOTO,241,1.0,0
|
||||||
|
249,2025-08-26,C98,LOCAL,MED,COPILOTO,241,1.0,0
|
||||||
|
250,2025-08-26,C95,LOCAL,BRI,PILOTO,242,1.0,0
|
||||||
|
250,2025-08-26,C95,LOCAL,PCN,COPILOTO,242,1.0,0
|
||||||
|
251,2025-08-27,C97,ROTA,FIA,PILOTO,243,10.5,0
|
||||||
|
251,2025-08-27,C97,ROTA,LPS,COPILOTO,243,10.5,0
|
||||||
|
252,2025-08-27,C98,LOCAL,MED,PILOTO,244,1.0,0
|
||||||
|
252,2025-08-27,C98,LOCAL,RCH,COPILOTO,244,1.0,0
|
||||||
|
253,2025-08-27,C98,LOCAL,MED,PILOTO,245,1.0,0
|
||||||
|
253,2025-08-27,C98,LOCAL,PFR,COPILOTO,245,1.0,0
|
||||||
|
254,2025-09-01,C97,LOCAL,AEU,PILOTO,246,1.0,0
|
||||||
|
254,2025-09-01,C97,LOCAL,LPS,COPILOTO,246,1.0,0
|
||||||
|
255,2025-09-02,C98,LOCAL,FIA,PILOTO,247,1.0,0
|
||||||
|
255,2025-09-02,C98,LOCAL,SLS,COPILOTO,247,1.0,0
|
||||||
|
256,2025-09-02,C95,LOCAL,BRI,PILOTO,248,1.0,0
|
||||||
|
256,2025-09-02,C95,LOCAL,CAR,COPILOTO,248,1.0,0
|
||||||
|
257,2025-09-02,C98,LOCAL,MED,PILOTO,249,0.08,0
|
||||||
|
257,2025-09-02,C98,LOCAL,SLS,COPILOTO,249,0.08,0
|
||||||
|
258,2025-09-03,C95,LOCAL,CAR,PILOTO,250,0.08,0
|
||||||
|
258,2025-09-03,C95,LOCAL,KVN,COPILOTO,250,0.08,0
|
||||||
|
259,2025-09-03,C95,LOCAL,CAR,PILOTO,251,0.08,0
|
||||||
|
259,2025-09-03,C95,LOCAL,ISA,COPILOTO,251,0.08,0
|
||||||
|
260,2025-09-05,C98,ROTA,MCH,PILOTO,252,4.17,0
|
||||||
|
260,2025-09-05,C98,ROTA,LRS,COPILOTO,252,4.17,0
|
||||||
|
261,2025-09-10,C95,ROTA,BRI,PILOTO,253,2.17,0
|
||||||
|
261,2025-09-10,C95,ROTA,BRJ,COPILOTO,253,2.17,0
|
||||||
|
262,2025-09-10,C98,LOCAL,GMS,PILOTO,254,0.08,0
|
||||||
|
262,2025-09-10,C98,LOCAL,LRS,COPILOTO,254,0.08,0
|
||||||
|
263,2025-09-11,C98,LOCAL,DOG,PILOTO,255,0.08,0
|
||||||
|
263,2025-09-11,C98,LOCAL,PFR,COPILOTO,255,0.08,0
|
||||||
|
264,2025-09-11,C98,LOCAL,FIA,PILOTO,256,1.0,0
|
||||||
|
264,2025-09-11,C98,LOCAL,CFF,COPILOTO,256,1.0,0
|
||||||
|
265,2025-09-12,C97,LOCAL,MES,PILOTO,257,0.08,0
|
||||||
|
265,2025-09-12,C97,LOCAL,SEI,COPILOTO,257,0.08,0
|
||||||
|
266,2025-09-15,C98,LOCAL,MED,PILOTO,258,1.0,0
|
||||||
|
266,2025-09-15,C98,LOCAL,SUG,COPILOTO,258,1.0,0
|
||||||
|
267,2025-09-16,C97,ROTA,FIA,PILOTO,260,8.67,0
|
||||||
|
267,2025-09-16,C97,ROTA,SEI,COPILOTO,260,8.67,0
|
||||||
|
268,2025-09-16,C98,ROTA,DOG,PILOTO,259,2.0,0
|
||||||
|
268,2025-09-16,C98,ROTA,SUG,COPILOTO,259,2.0,0
|
||||||
|
269,2025-09-17,C98,ROTA,CFF,PILOTO,261,0.67,0
|
||||||
|
269,2025-09-17,C98,ROTA,SUG,COPILOTO,261,0.67,0
|
||||||
|
270,2025-09-18,C97,ROTA,FIA,PILOTO,262,5.0,0
|
||||||
|
270,2025-09-18,C97,ROTA,AEU,COPILOTO,262,5.0,0
|
||||||
|
271,2025-09-22,C97,LOCAL,AEU,PILOTO,263,1.5,0
|
||||||
|
271,2025-09-22,C97,LOCAL,LPS,COPILOTO,263,1.5,0
|
||||||
|
272,2025-09-24,C95,LOCAL,HCK,PILOTO,264,0.08,0
|
||||||
|
272,2025-09-24,C95,LOCAL,CAR,COPILOTO,264,0.08,0
|
||||||
|
273,2025-09-25,C98,LOCAL,CFF,PILOTO,265,1.0,0
|
||||||
|
273,2025-09-25,C98,LOCAL,MCH,COPILOTO,265,1.0,0
|
||||||
|
274,2025-10-01,C98,LOCAL,MCH,PILOTO,268,0.08,0
|
||||||
|
274,2025-10-01,C98,LOCAL,GMS,COPILOTO,268,0.08,0
|
||||||
|
275,2025-09-27,C98,ROTA,CFF,PILOTO,266,20.58,0
|
||||||
|
275,2025-09-27,C98,ROTA,DOG,COPILOTO,266,20.58,0
|
||||||
|
276,2025-09-29,C97,ROTA,AEU,PILOTO,267,6.0,0
|
||||||
|
276,2025-09-29,C97,ROTA,LPS,COPILOTO,267,6.0,0
|
||||||
|
277,2025-10-02,C98,ROTA,CFF,PILOTO,269,8.5,0
|
||||||
|
277,2025-10-02,C98,ROTA,DOG,COPILOTO,269,8.5,0
|
||||||
|
278,2025-10-05,C95,LOCAL,BRI,PILOTO,270,3.0,0
|
||||||
|
278,2025-10-05,C95,LOCAL,CAR,COPILOTO,270,3.0,0
|
||||||
|
279,2025-10-05,C95,ROTA,PCN,PILOTO,271,3.5,0
|
||||||
|
279,2025-10-05,C95,ROTA,GMR,COPILOTO,271,3.5,0
|
||||||
|
280,2025-10-08,C95,ROTA,CAR,PILOTO,272,3.58,0
|
||||||
|
280,2025-10-08,C95,ROTA,KVN,COPILOTO,272,3.58,0
|
||||||
|
281,2025-10-11,C95,LOCAL,BRI,PILOTO,273,1.0,0
|
||||||
|
281,2025-10-11,C95,LOCAL,BRJ,COPILOTO,273,1.0,0
|
||||||
|
282,2025-10-11,C95,LOCAL,BRI,PILOTO,274,1.0,0
|
||||||
|
282,2025-10-11,C95,LOCAL,BMK,COPILOTO,274,1.0,0
|
||||||
|
283,2025-10-13,C97,LOCAL,MES,PILOTO,275,0.25,0
|
||||||
|
283,2025-10-13,C97,LOCAL,SEI,COPILOTO,275,0.25,0
|
||||||
|
284,2025-10-13,C98,LOCAL,MED,PILOTO,276,0.08,0
|
||||||
|
284,2025-10-13,C98,LOCAL,SLS,COPILOTO,276,0.08,0
|
||||||
|
285,2025-10-21,C98,LOCAL,FIA,PILOTO,277,0.67,0
|
||||||
|
285,2025-10-21,C98,LOCAL,BEN,COPILOTO,277,0.67,0
|
||||||
|
286,2025-10-21,C95,LOCAL,HCK,PILOTO,278,0.08,0
|
||||||
|
286,2025-10-21,C95,LOCAL,ISA,COPILOTO,278,0.08,0
|
||||||
|
287,2025-10-22,C98,LOCAL,GMS,PILOTO,279,1.0,0
|
||||||
|
287,2025-10-22,C98,LOCAL,DOG,COPILOTO,279,1.0,0
|
||||||
|
288,2025-10-23,C95,LOCAL,BRI,PILOTO,280,1.0,0
|
||||||
|
288,2025-10-23,C95,LOCAL,BRJ,COPILOTO,280,1.0,0
|
||||||
|
289,2025-10-23,C95,LOCAL,BRI,PILOTO,281,1.5,0
|
||||||
|
289,2025-10-23,C95,LOCAL,JVT,COPILOTO,281,1.5,0
|
||||||
|
290,2025-10-28,C95,LOCAL,BRI,PILOTO,282,1.5,0
|
||||||
|
290,2025-10-28,C95,LOCAL,PCN,COPILOTO,282,1.5,0
|
||||||
|
291,2025-10-29,C98,LOCAL,GMS,PILOTO,283,0.08,0
|
||||||
|
291,2025-10-29,C98,LOCAL,PFR,COPILOTO,283,0.08,0
|
||||||
|
292,2025-10-30,C98,LOCAL,MED,PILOTO,284,0.58,0
|
||||||
|
292,2025-10-30,C98,LOCAL,BEN,COPILOTO,284,0.58,0
|
||||||
|
293,2025-11-04,C98,LOCAL,MCH,PILOTO,285,0.08,0
|
||||||
|
293,2025-11-04,C98,LOCAL,DOG,COPILOTO,285,0.08,0
|
||||||
|
294,2025-11-08,C98,ROTA,CFF,PILOTO,286,10.83,0
|
||||||
|
294,2025-11-08,C98,ROTA,BEN,COPILOTO,286,10.83,0
|
||||||
|
295,2025-11-08,C95,LOCAL,BRI,PILOTO,288,2.0,0
|
||||||
|
295,2025-11-08,C95,LOCAL,PCN,COPILOTO,288,2.0,0
|
||||||
|
296,2025-11-11,C98,ROTA,MCH,PILOTO,287,5.92,0
|
||||||
|
296,2025-11-11,C98,ROTA,PFR,COPILOTO,287,5.92,0
|
||||||
|
297,2025-11-11,C95,LOCAL,PCN,PILOTO,289,0.08,0
|
||||||
|
297,2025-11-11,C95,LOCAL,BMK,COPILOTO,289,0.08,0
|
||||||
|
298,2025-11-11,C95,LOCAL,PCN,PILOTO,290,1.0,0
|
||||||
|
298,2025-11-11,C95,LOCAL,GMR,COPILOTO,290,1.0,0
|
||||||
|
299,2025-11-13,C98,ROTA,DOG,PILOTO,287,5.92,0
|
||||||
|
299,2025-11-13,C98,ROTA,LRS,COPILOTO,287,5.92,0
|
||||||
|
300,2025-11-24,C98,LOCAL,SLS,PILOTO,292,0.08,0
|
||||||
|
300,2025-11-24,C98,LOCAL,RCH,COPILOTO,292,0.08,0
|
||||||
|
301,2025-11-24,C98,ROTA,GMS,PILOTO,291,5.0,0
|
||||||
|
301,2025-11-24,C98,ROTA,BEN,COPILOTO,291,5.0,0
|
||||||
|
302,2025-11-24,C95,LOCAL,BRJ,PILOTO,293,0.08,0
|
||||||
|
302,2025-11-24,C95,LOCAL,ISA,COPILOTO,293,0.08,0
|
||||||
|
303,2025-11-27,C95,LOCAL,MHL,PILOTO,294,1.0,0
|
||||||
|
303,2025-11-27,C95,LOCAL,BRJ,COPILOTO,294,1.0,0
|
||||||
|
304,2025-12-01,C95,ROTA,BRI,PILOTO,295,1.5,0
|
||||||
|
304,2025-12-01,C95,ROTA,ISA,COPILOTO,295,1.5,0
|
||||||
|
305,2025-12-02,C95,ROTA,CAR,PILOTO,296,6.58,0
|
||||||
|
305,2025-12-02,C95,ROTA,KVN,COPILOTO,296,6.58,0
|
||||||
|
306,2025-12-03,C97,ROTA,MES,PILOTO,297,9.08,0
|
||||||
|
306,2025-12-03,C97,ROTA,SEI,COPILOTO,297,9.08,0
|
||||||
|
307,2025-12-03,C98,LOCAL,SLS,PILOTO,298,0.08,0
|
||||||
|
307,2025-12-03,C98,LOCAL,RCH,COPILOTO,298,0.08,0
|
||||||
|
308,2025-12-03,C95,LOCAL,JVT,PILOTO,299,0.67,0
|
||||||
|
308,2025-12-03,C95,LOCAL,GMR,COPILOTO,299,0.67,0
|
||||||
|
309,2025-12-03,C95,LOCAL,CAR,PILOTO,300,1.0,0
|
||||||
|
309,2025-12-03,C95,LOCAL,BMK,COPILOTO,300,1.0,0
|
||||||
|
310,2025-12-03,C95,LOCAL,CAR,PILOTO,300,1.0,0
|
||||||
|
310,2025-12-03,C95,LOCAL,BMK,COPILOTO,300,1.0,0
|
||||||
|
311,2025-12-03,C95,LOCAL,CAR,PILOTO,300,1.0,0
|
||||||
|
311,2025-12-03,C95,LOCAL,BMK,COPILOTO,300,1.0,0
|
||||||
|
312,2025-12-03,C95,LOCAL,JVT,PILOTO,299,0.67,0
|
||||||
|
312,2025-12-03,C95,LOCAL,GMR,COPILOTO,299,0.67,0
|
||||||
|
313,2025-12-03,C95,ROTA,BRJ,PILOTO,304,12.0,0
|
||||||
|
313,2025-12-03,C95,ROTA,ISA,COPILOTO,304,12.0,0
|
||||||
|
314,2025-12-04,C95,ROTA,GMR,PILOTO,302,2.67,0
|
||||||
|
314,2025-12-04,C95,ROTA,KVN,COPILOTO,302,2.67,0
|
||||||
|
315,2025-12-04,C95,ROTA,GMR,PILOTO,303,14.0,0
|
||||||
|
315,2025-12-04,C95,ROTA,KVN,COPILOTO,303,14.0,0
|
||||||
|
316,2025-12-05,C95,LOCAL,BRI,PILOTO,301,1.5,0
|
||||||
|
316,2025-12-05,C95,LOCAL,CAR,COPILOTO,301,1.5,0
|
||||||
|
317,2025-12-05,C98,LOCAL,SLS,PILOTO,305,1.0,0
|
||||||
|
317,2025-12-05,C98,LOCAL,MCH,COPILOTO,305,1.0,0
|
||||||
|
318,2025-12-05,C98,LOCAL,SLS,PILOTO,306,1.0,0
|
||||||
|
318,2025-12-05,C98,LOCAL,SUG,COPILOTO,306,1.0,0
|
||||||
|
319,2025-12-05,C98,ROTA,DOG,PILOTO,308,5.0,0
|
||||||
|
319,2025-12-05,C98,ROTA,BEN,COPILOTO,308,5.0,0
|
||||||
|
320,2025-12-06,C98,ROTA,GMS,PILOTO,309,3.5,0
|
||||||
|
320,2025-12-06,C98,ROTA,LRS,COPILOTO,309,3.5,0
|
||||||
|
321,2025-12-06,C98,ROTA,MCH,PILOTO,310,2.08,0
|
||||||
|
321,2025-12-06,C98,ROTA,RCH,COPILOTO,310,2.08,0
|
||||||
|
322,2025-12-06,C95,LOCAL,BRI,PILOTO,307,2.5,0
|
||||||
|
322,2025-12-06,C95,LOCAL,CAR,COPILOTO,307,2.5,0
|
||||||
|
323,2025-12-07,C98,ROTA,CFF,PILOTO,313,3.5,0
|
||||||
|
323,2025-12-07,C98,ROTA,RCH,COPILOTO,313,3.5,0
|
||||||
|
324,2025-12-07,C98,ROTA,DOG,PILOTO,314,2.08,0
|
||||||
|
324,2025-12-07,C98,ROTA,BEN,COPILOTO,314,2.08,0
|
||||||
|
325,2025-12-08,C95,LOCAL,CAR,PILOTO,311,0.67,0
|
||||||
|
325,2025-12-08,C95,LOCAL,KVN,COPILOTO,311,0.67,0
|
||||||
|
326,2025-12-08,C95,LOCAL,JVT,PILOTO,312,1.0,0
|
||||||
|
326,2025-12-08,C95,LOCAL,GMR,COPILOTO,312,1.0,0
|
||||||
|
327,2025-12-08,C95,LOCAL,JVT,PILOTO,312,1.0,0
|
||||||
|
327,2025-12-08,C95,LOCAL,GMR,COPILOTO,312,1.0,0
|
||||||
|
328,2025-12-08,C95,LOCAL,JVT,PILOTO,312,1.0,0
|
||||||
|
328,2025-12-08,C95,LOCAL,GMR,COPILOTO,312,1.0,0
|
||||||
|
329,2025-12-08,C95,LOCAL,CAR,PILOTO,311,0.67,0
|
||||||
|
329,2025-12-08,C95,LOCAL,KVN,COPILOTO,311,0.67,0
|
||||||
|
330,2025-12-08,C97,LOCAL,MES,PILOTO,315,1.0,0
|
||||||
|
330,2025-12-08,C97,LOCAL,AEU,COPILOTO,315,1.0,0
|
||||||
|
331,2025-12-08,C98,LOCAL,CNI,PILOTO,316,1.0,0
|
||||||
|
331,2025-12-08,C98,LOCAL,FIA,COPILOTO,316,1.0,0
|
||||||
|
332,2025-12-09,C95,ROTA,HCK,PILOTO,317,6.75,0
|
||||||
|
332,2025-12-09,C95,ROTA,BRJ,COPILOTO,317,6.75,0
|
||||||
|
333,2025-12-09,C98,LOCAL,FIA,PILOTO,318,1.5,0
|
||||||
|
333,2025-12-09,C98,LOCAL,CFF,COPILOTO,318,1.5,0
|
||||||
|
334,2025-12-09,C95,LOCAL,CAR,PILOTO,319,1.0,0
|
||||||
|
334,2025-12-09,C95,LOCAL,KVN,COPILOTO,319,1.0,0
|
||||||
|
335,2025-12-09,C98,ROTA,GMS,PILOTO,320,6.0,0
|
||||||
|
335,2025-12-09,C98,ROTA,PFR,COPILOTO,320,6.0,0
|
||||||
|
336,2025-12-09,C98,LOCAL,MED,PILOTO,321,0.08,0
|
||||||
|
336,2025-12-09,C98,LOCAL,RCH,COPILOTO,321,0.08,0
|
||||||
|
337,2025-12-10,C95,ROTA,BMK,PILOTO,322,6.75,0
|
||||||
|
337,2025-12-10,C95,ROTA,BRJ,COPILOTO,322,6.75,0
|
||||||
|
338,2025-12-10,C98,LOCAL,FIA,PILOTO,323,1.5,0
|
||||||
|
338,2025-12-10,C98,LOCAL,GMS,COPILOTO,323,1.5,0
|
||||||
|
339,2025-12-10,C98,LOCAL,MED,PILOTO,324,1.0,0
|
||||||
|
339,2025-12-10,C98,LOCAL,CFF,COPILOTO,324,1.0,0
|
||||||
|
340,2025-12-11,C95,ROTA,CAR,PILOTO,325,4.83,0
|
||||||
|
340,2025-12-11,C95,ROTA,KVN,COPILOTO,325,4.83,0
|
||||||
|
341,2025-12-12,C97,LOCAL,FIA,PILOTO,326,1.0,0
|
||||||
|
341,2025-12-12,C97,LOCAL,AEU,COPILOTO,326,1.0,0
|
||||||
|
342,2025-12-12,C95,LOCAL,JVT,PILOTO,327,0.67,0
|
||||||
|
342,2025-12-12,C95,LOCAL,BMK,COPILOTO,327,0.67,0
|
||||||
|
343,2025-12-12,C95,LOCAL,BRI,PILOTO,328,1.0,0
|
||||||
|
343,2025-12-12,C95,LOCAL,HCK,COPILOTO,328,1.0,0
|
||||||
|
344,2025-12-12,C95,LOCAL,BRI,PILOTO,328,1.0,0
|
||||||
|
344,2025-12-12,C95,LOCAL,HCK,COPILOTO,328,1.0,0
|
||||||
|
345,2025-12-12,C95,LOCAL,BRI,PILOTO,328,1.0,0
|
||||||
|
345,2025-12-12,C95,LOCAL,HCK,COPILOTO,328,1.0,0
|
||||||
|
346,2025-12-12,C95,LOCAL,GMR,PILOTO,327,0.67,0
|
||||||
|
346,2025-12-12,C95,LOCAL,HCK,COPILOTO,327,0.67,0
|
||||||
|
347,2025-12-12,C95,LOCAL,RAY,PILOTO,329,1.0,0
|
||||||
|
347,2025-12-12,C95,LOCAL,KVN,COPILOTO,329,1.0,0
|
||||||
|
348,2025-12-12,C95,LOCAL,MHL,PILOTO,329,1.0,0
|
||||||
|
348,2025-12-12,C95,LOCAL,ISA,COPILOTO,329,1.0,0
|
||||||
|
349,2025-12-12,C95,LOCAL,MHL,PILOTO,330,1.0,0
|
||||||
|
349,2025-12-12,C95,LOCAL,ISA,COPILOTO,330,1.0,0
|
||||||
|
350,2025-12-12,C95,LOCAL,RAY,PILOTO,330,1.0,0
|
||||||
|
350,2025-12-12,C95,LOCAL,KVN,COPILOTO,330,1.0,0
|
||||||
|
351,2025-12-12,C98,LOCAL,MED,PILOTO,331,1.0,0
|
||||||
|
351,2025-12-12,C98,LOCAL,GMS,COPILOTO,331,1.0,0
|
||||||
|
352,2025-12-13,C95,LOCAL,RAY,PILOTO,333,1.0,0
|
||||||
|
352,2025-12-13,C95,LOCAL,CAR,COPILOTO,333,1.0,0
|
||||||
|
353,2025-12-13,C95,LOCAL,HCK,PILOTO,334,1.0,0
|
||||||
|
353,2025-12-13,C95,LOCAL,ISA,COPILOTO,334,1.0,0
|
||||||
|
354,2025-12-13,C95,LOCAL,RAY,PILOTO,335,1.0,0
|
||||||
|
354,2025-12-13,C95,LOCAL,CAR,COPILOTO,335,1.0,0
|
||||||
|
355,2025-12-14,C95,ROTA,JVT,PILOTO,332,11.33,0
|
||||||
|
355,2025-12-14,C95,ROTA,BMK,COPILOTO,332,11.33,0
|
||||||
|
356,2025-12-15,C95,LOCAL,MHL,PILOTO,336,1.0,0
|
||||||
|
356,2025-12-15,C95,LOCAL,MES,COPILOTO,336,1.0,0
|
||||||
|
357,2025-12-15,C95,LOCAL,BRJ,PILOTO,336,1.0,0
|
||||||
|
357,2025-12-15,C95,LOCAL,KVN,COPILOTO,336,1.0,0
|
||||||
|
358,2025-12-15,C95,LOCAL,MES,PILOTO,337,1.0,0
|
||||||
|
358,2025-12-15,C95,LOCAL,MHL,COPILOTO,337,1.0,0
|
||||||
|
359,2025-12-15,C95,LOCAL,BRJ,PILOTO,337,1.0,0
|
||||||
|
359,2025-12-15,C95,LOCAL,KVN,COPILOTO,337,1.0,0
|
||||||
|
360,2025-12-15,C98,LOCAL,CFF,PILOTO,338,1.0,0
|
||||||
|
360,2025-12-15,C98,LOCAL,BEN,COPILOTO,338,1.0,0
|
||||||
|
361,2025-12-16,C95,ROTA,BRJ,PILOTO,339,5.33,0
|
||||||
|
361,2025-12-16,C95,ROTA,KVN,COPILOTO,339,5.33,0
|
||||||
|
362,2025-12-16,C98,ROTA,CFF,PILOTO,340,2.08,0
|
||||||
|
362,2025-12-16,C98,ROTA,BEN,COPILOTO,340,2.08,0
|
||||||
|
363,2025-12-17,C95,LOCAL,CAR,PILOTO,342,1.0,0
|
||||||
|
363,2025-12-17,C95,LOCAL,MES,COPILOTO,342,1.0,0
|
||||||
|
364,2025-12-17,C95,LOCAL,GMR,PILOTO,342,1.0,0
|
||||||
|
364,2025-12-17,C95,LOCAL,BMK,COPILOTO,342,1.0,0
|
||||||
|
365,2025-12-17,C95,LOCAL,MES,PILOTO,343,1.0,0
|
||||||
|
365,2025-12-17,C95,LOCAL,CAR,COPILOTO,343,1.0,0
|
||||||
|
366,2025-12-17,C95,LOCAL,BMK,PILOTO,343,1.0,0
|
||||||
|
366,2025-12-17,C95,LOCAL,GMR,COPILOTO,343,1.0,0
|
||||||
|
367,2025-12-18,C97,ROTA,AEU,PILOTO,341,10.17,0
|
||||||
|
367,2025-12-18,C97,ROTA,MHL,COPILOTO,341,10.17,0
|
||||||
|
368,2025-12-18,C98,LOCAL,FIA,PILOTO,344,1.0,0
|
||||||
|
368,2025-12-18,C98,LOCAL,DOG,COPILOTO,344,1.0,0
|
||||||
|
369,2025-12-19,C95,ROTA,BRI,PILOTO,345,5.67,0
|
||||||
|
369,2025-12-19,C95,ROTA,HCK,COPILOTO,345,5.67,0
|
||||||
|
370,2025-12-20,C98,ROTA,MCH,PILOTO,347,1.17,0
|
||||||
|
370,2025-12-20,C98,ROTA,SUG,COPILOTO,347,1.17,0
|
||||||
|
371,2025-12-23,C95,ROTA,GMR,PILOTO,346,6.42,0
|
||||||
|
371,2025-12-23,C95,ROTA,ISA,COPILOTO,346,6.42,0
|
||||||
|
372,2025-12-23,C97,LOCAL,MES,PILOTO,349,0.5,0
|
||||||
|
372,2025-12-23,C97,LOCAL,MHL,COPILOTO,349,0.5,0
|
||||||
|
373,2025-12-23,C98,ROTA,MCH,PILOTO,350,0.92,0
|
||||||
|
373,2025-12-23,C98,ROTA,RCH,COPILOTO,350,0.92,0
|
||||||
|
374,2025-12-28,C95,ROTA,CAR,PILOTO,352,3.92,0
|
||||||
|
374,2025-12-28,C95,ROTA,HCK,COPILOTO,352,3.92,0
|
||||||
|
375,2025-12-28,C95,ROTA,JVT,PILOTO,353,10.0,0
|
||||||
|
375,2025-12-28,C95,ROTA,BRJ,COPILOTO,353,10.0,0
|
||||||
|
376,2025-12-29,C95,ROTA,BMK,PILOTO,354,3.92,0
|
||||||
|
376,2025-12-29,C95,ROTA,KVN,COPILOTO,354,3.92,0
|
||||||
|
377,2025-12-29,C97,LOCAL,AEU,PILOTO,348,0.75,0
|
||||||
|
377,2025-12-29,C97,LOCAL,LPS,COPILOTO,348,0.75,0
|
||||||
|
378,2025-12-29,C97,LOCAL,AEU,PILOTO,351,0.75,0
|
||||||
|
378,2025-12-29,C97,LOCAL,SEI,COPILOTO,351,0.75,0
|
||||||
|
379,2025-12-29,C95,LOCAL,MES,PILOTO,355,1.0,0
|
||||||
|
379,2025-12-29,C95,LOCAL,CAR,COPILOTO,355,1.0,0
|
||||||
|
380,2025-12-29,C95,LOCAL,ISA,PILOTO,355,1.0,0
|
||||||
|
380,2025-12-29,C95,LOCAL,MHL,COPILOTO,355,1.0,0
|
||||||
|
381,2025-12-29,C95,LOCAL,MHL,PILOTO,356,1.0,0
|
||||||
|
381,2025-12-29,C95,LOCAL,RAY,COPILOTO,356,1.0,0
|
||||||
|
382,2025-12-29,C95,LOCAL,CAR,PILOTO,356,1.0,0
|
||||||
|
382,2025-12-29,C95,LOCAL,HCK,COPILOTO,356,1.0,0
|
||||||
|
383,2025-12-30,C95,ROTA,BRI,PILOTO,357,3.42,0
|
||||||
|
383,2025-12-30,C95,ROTA,KVN,COPILOTO,357,3.42,0
|
||||||
|
384,2025-12-30,C97,LOCAL,FIA,PILOTO,358,0.75,0
|
||||||
|
384,2025-12-30,C97,LOCAL,SEI,COPILOTO,358,0.75,0
|
||||||
|
4
requirements.txt
Normal file
4
requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
pandas>=3.0,<4
|
||||||
|
numpy>=2.4,<3
|
||||||
|
scipy>=1.18,<2
|
||||||
|
openpyxl>=3.1,<4
|
||||||
14
run_planner.py
Normal file
14
run_planner.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
"""Ponto de entrada do planejador diario de missoes."""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from src.planejador_missao.main import executar_planejamento
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
resultado = executar_planejamento(Path(__file__).resolve().parent)
|
||||||
|
print("\n=== Planejamento concluido ===")
|
||||||
|
print(f"Data: {resultado['data_planejamento']}")
|
||||||
|
print(f"Arquivo de saida: {resultado['arquivo_saida']}")
|
||||||
|
print(f"Colunas candidatas: {resultado['total_candidatas']}")
|
||||||
|
print(f"Escalas selecionadas: {resultado['total_selecionadas']}")
|
||||||
103
scripts/00_main.py
Normal file
103
scripts/00_main.py
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
"""Orquestrador principal do Planejador Missao.
|
||||||
|
|
||||||
|
Entradas:
|
||||||
|
Arquivos em dados/, argumentos opcionais de linha de comando e configuracoes
|
||||||
|
centralizadas em scripts/01_config.py.
|
||||||
|
|
||||||
|
Saidas:
|
||||||
|
Planejamento diario em resultados/, historico operacional atualizado,
|
||||||
|
relatorios de validacao quando solicitado e logs em logs/execucao.log.
|
||||||
|
|
||||||
|
Papel no pipeline:
|
||||||
|
Executa o fluxo completo: leitura de dados, preparacao, geracao de colunas
|
||||||
|
candidatas, montagem/solucao do MILP, exportacao de resultados e validacao.
|
||||||
|
|
||||||
|
Exemplos:
|
||||||
|
python scripts/00_main.py --data 2026-01-02 --modo diario
|
||||||
|
python scripts/00_main.py --validacao 2025
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import importlib
|
||||||
|
import time
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
config = importlib.import_module("01_config")
|
||||||
|
io_utils = importlib.import_module("02_io_utils")
|
||||||
|
|
||||||
|
|
||||||
|
def atualizar_data_planejamento(data: str) -> None:
|
||||||
|
"""Atualiza dados/parametros_missao.csv com a data solicitada."""
|
||||||
|
path = config.ARQUIVO_PARAMETROS
|
||||||
|
df = pd.read_csv(path) if path.exists() else pd.DataFrame(columns=["parametro", "valor"])
|
||||||
|
if "parametro" not in df.columns or "valor" not in df.columns:
|
||||||
|
raise ValueError("Erro: parametros_missao.csv deve conter as colunas parametro,valor.")
|
||||||
|
if (df["parametro"] == "data_planejamento").any():
|
||||||
|
df.loc[df["parametro"] == "data_planejamento", "valor"] = data
|
||||||
|
else:
|
||||||
|
df = pd.concat([df, pd.DataFrame([{"parametro": "data_planejamento", "valor": data}])], ignore_index=True)
|
||||||
|
df.to_csv(path, index=False)
|
||||||
|
|
||||||
|
|
||||||
|
def executar_diario(args: argparse.Namespace, logger) -> dict:
|
||||||
|
"""Executa o planejamento diario oficial em Python."""
|
||||||
|
io_utils.configurar_ambiente()
|
||||||
|
if args.data:
|
||||||
|
atualizar_data_planejamento(args.data)
|
||||||
|
logger.info("Data de planejamento ajustada para %s", args.data)
|
||||||
|
if args.aeronave:
|
||||||
|
logger.info("Filtro --aeronave recebido: %s. O modelo diario usa a disponibilidade em dados/aeronaves_disponiveis.csv.", args.aeronave)
|
||||||
|
|
||||||
|
from src.planejador_missao.main import executar_planejamento
|
||||||
|
|
||||||
|
inicio = time.perf_counter()
|
||||||
|
resultado = executar_planejamento(config.PROJECT_ROOT)
|
||||||
|
duracao = time.perf_counter() - inicio
|
||||||
|
logger.info("Data planejada: %s", resultado["data_planejamento"])
|
||||||
|
logger.info("Colunas candidatas: %s", resultado["total_candidatas"])
|
||||||
|
logger.info("Escalas selecionadas: %s", resultado["total_selecionadas"])
|
||||||
|
logger.info("Arquivo exportado: %s", resultado["arquivo_saida"])
|
||||||
|
logger.info("Tempo de execucao: %.1f s", duracao)
|
||||||
|
return resultado
|
||||||
|
|
||||||
|
|
||||||
|
def executar_validacao(ano: str, logger) -> dict:
|
||||||
|
"""Executa validacao retrospectiva suportada pelo projeto."""
|
||||||
|
if str(ano) != "2025":
|
||||||
|
raise ValueError("Erro: no momento a validacao disponivel e --validacao 2025.")
|
||||||
|
validacao = importlib.import_module("07_validacao_2025")
|
||||||
|
return validacao.executar_validacao_2025(importar=True, logger=logger)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(description="Executa o Planejador Missao em Python.")
|
||||||
|
parser.add_argument("--data", help="Data do planejamento diario no formato AAAA-MM-DD.")
|
||||||
|
parser.add_argument("--aeronave", choices=config.AERONAVES, help="Aeronave de referencia para diagnostico.")
|
||||||
|
parser.add_argument("--modo", default="diario", choices=["diario"], help="Modo de execucao do planejamento.")
|
||||||
|
parser.add_argument("--validacao", help="Ano da validacao retrospectiva. Exemplo: --validacao 2025.")
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
args = parse_args()
|
||||||
|
logger = io_utils.configurar_logger("main")
|
||||||
|
if args.validacao:
|
||||||
|
saida = executar_validacao(args.validacao, logger)
|
||||||
|
print("\n=== Validacao concluida ===")
|
||||||
|
for path in saida["arquivos"]:
|
||||||
|
print(f"- {path}")
|
||||||
|
return
|
||||||
|
|
||||||
|
resultado = executar_diario(args, logger)
|
||||||
|
print("\n=== Planejamento diario concluido ===")
|
||||||
|
print(f"Data: {resultado['data_planejamento']}")
|
||||||
|
print(f"Arquivo: {resultado['arquivo_saida']}")
|
||||||
|
print(f"Colunas candidatas: {resultado['total_candidatas']}")
|
||||||
|
print(f"Escalas selecionadas: {resultado['total_selecionadas']}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
72
scripts/01_config.py
Normal file
72
scripts/01_config.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
"""Configuracao central dos scripts do Planejador Missao.
|
||||||
|
|
||||||
|
Entradas:
|
||||||
|
Estrutura padrao do projeto, com pastas dados/, src/ e resultados/.
|
||||||
|
|
||||||
|
Saidas:
|
||||||
|
Constantes de caminho, parametros operacionais, nomes de arquivos e pesos
|
||||||
|
usados pelos scripts de execucao, validacao e teste.
|
||||||
|
|
||||||
|
Papel no pipeline:
|
||||||
|
Evita caminhos absolutos e concentra convencoes para que o projeto rode em
|
||||||
|
qualquer computador mantendo a mesma estrutura de diretorios.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
SCRIPTS_DIR = PROJECT_ROOT / "scripts"
|
||||||
|
SRC_DIR = PROJECT_ROOT / "src"
|
||||||
|
DADOS_DIR = PROJECT_ROOT / "dados"
|
||||||
|
RESULTADOS_DIR = PROJECT_ROOT / "resultados"
|
||||||
|
VALIDACAO_DIR = RESULTADOS_DIR / "validacao"
|
||||||
|
LOGS_DIR = PROJECT_ROOT / "logs"
|
||||||
|
TEMP_DIR = PROJECT_ROOT / "temp"
|
||||||
|
|
||||||
|
ARQUIVO_PARAMETROS = DADOS_DIR / "parametros_missao.csv"
|
||||||
|
ARQUIVO_CADASTRO_LOCAL = DADOS_DIR / "Modelagem_C98_ETA2_local.xlsx"
|
||||||
|
ARQUIVO_CADASTRO_ORIGINAL = DADOS_DIR / "Modelagem C98 ETA2.xlsx"
|
||||||
|
ARQUIVO_CATALOGO_OIS = DADOS_DIR / "catalogo_ois.xlsx"
|
||||||
|
ARQUIVO_INDISPONIBILIDADES = DADOS_DIR / "indisponibilidades_2026.xlsx"
|
||||||
|
ARQUIVO_HISTORICO = DADOS_DIR / "historico_horas_voadas.csv"
|
||||||
|
ARQUIVO_QUADRO_VOO_2025 = DADOS_DIR / "Quadro de Voo 2025 (2).xlsx"
|
||||||
|
ARQUIVO_VOOS_2025 = DADOS_DIR / "validacao" / "voos_2025.csv"
|
||||||
|
|
||||||
|
LOG_EXECUCAO = LOGS_DIR / "execucao.log"
|
||||||
|
|
||||||
|
AERONAVES = ["C98", "C97", "C95"]
|
||||||
|
META_HORAS_PADRAO = 50.0
|
||||||
|
VALIDACAO_TEMPO_LIMITE_SEGUNDOS = 180.0
|
||||||
|
VALIDACAO_GAP_RELATIVO = 0.05
|
||||||
|
|
||||||
|
PESOS_OBJETIVO = {
|
||||||
|
"rota_acionada": 100000,
|
||||||
|
"missao_local": 1000,
|
||||||
|
"sobreaviso": 0,
|
||||||
|
"meta_50": 100,
|
||||||
|
"meta_110": 100,
|
||||||
|
"beneficio_paop": 10,
|
||||||
|
"custo_financeiro": -0.05,
|
||||||
|
}
|
||||||
|
|
||||||
|
ABAS = {
|
||||||
|
"cadastro": "BANCO DE DADOS 2026",
|
||||||
|
"catalogo_ois": "catalogo_ois",
|
||||||
|
"voos_2025": "VOOS",
|
||||||
|
"relatorio_diario": "ESCALA DIARIA",
|
||||||
|
}
|
||||||
|
|
||||||
|
ARQUIVOS_ENTRADA_OBRIGATORIOS = [
|
||||||
|
ARQUIVO_CATALOGO_OIS,
|
||||||
|
ARQUIVO_INDISPONIBILIDADES,
|
||||||
|
ARQUIVO_PARAMETROS,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def garantir_diretorios() -> None:
|
||||||
|
"""Cria diretorios de saida e apoio usados pelos scripts."""
|
||||||
|
for path in [RESULTADOS_DIR, VALIDACAO_DIR, LOGS_DIR, TEMP_DIR, ARQUIVO_VOOS_2025.parent]:
|
||||||
|
path.mkdir(parents=True, exist_ok=True)
|
||||||
85
scripts/02_io_utils.py
Normal file
85
scripts/02_io_utils.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
"""Utilitarios de entrada, saida e log dos scripts.
|
||||||
|
|
||||||
|
Entradas:
|
||||||
|
Caminhos definidos em 01_config.py.
|
||||||
|
|
||||||
|
Saidas:
|
||||||
|
Validacoes de arquivos, diretorios criados, logs em logs/execucao.log e
|
||||||
|
funcoes pequenas para exportacao.
|
||||||
|
|
||||||
|
Papel no pipeline:
|
||||||
|
Padroniza mensagens de erro e evita repeticao de codigo de filesystem.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Iterable
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
config = importlib.import_module("01_config")
|
||||||
|
|
||||||
|
|
||||||
|
def configurar_ambiente() -> None:
|
||||||
|
"""Garante diretorios e deixa src/ importavel para execucoes pelo VS Code."""
|
||||||
|
config.garantir_diretorios()
|
||||||
|
project_root = str(config.PROJECT_ROOT)
|
||||||
|
if project_root not in sys.path:
|
||||||
|
sys.path.insert(0, project_root)
|
||||||
|
|
||||||
|
|
||||||
|
def configurar_logger(nome: str = "planejador_missao") -> logging.Logger:
|
||||||
|
"""Cria logger simples em arquivo e terminal."""
|
||||||
|
configurar_ambiente()
|
||||||
|
logger = logging.getLogger(nome)
|
||||||
|
logger.setLevel(logging.INFO)
|
||||||
|
logger.handlers.clear()
|
||||||
|
|
||||||
|
formato = logging.Formatter("%(asctime)s | %(levelname)s | %(message)s")
|
||||||
|
file_handler = logging.FileHandler(config.LOG_EXECUCAO, encoding="utf-8")
|
||||||
|
file_handler.setFormatter(formato)
|
||||||
|
stream_handler = logging.StreamHandler()
|
||||||
|
stream_handler.setFormatter(logging.Formatter("%(message)s"))
|
||||||
|
logger.addHandler(file_handler)
|
||||||
|
logger.addHandler(stream_handler)
|
||||||
|
logger.info("Execucao iniciada em %s", datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
|
||||||
|
return logger
|
||||||
|
|
||||||
|
|
||||||
|
def exigir_arquivos(paths: Iterable[Path]) -> None:
|
||||||
|
"""Interrompe a execucao com mensagem clara se algum arquivo faltar."""
|
||||||
|
ausentes = [path for path in paths if not path.exists()]
|
||||||
|
if ausentes:
|
||||||
|
lista = "\n".join(f"- {path}" for path in ausentes)
|
||||||
|
raise FileNotFoundError(f"Erro: arquivos obrigatorios ausentes:\n{lista}")
|
||||||
|
|
||||||
|
|
||||||
|
def exigir_colunas(df: pd.DataFrame, colunas: Iterable[str], origem: str) -> None:
|
||||||
|
"""Valida colunas obrigatorias de uma tabela carregada."""
|
||||||
|
faltantes = [col for col in colunas if col not in df.columns]
|
||||||
|
if faltantes:
|
||||||
|
raise ValueError(
|
||||||
|
f"Erro: coluna obrigatoria '{faltantes[0]}' nao encontrada em {origem}. "
|
||||||
|
"Verifique o arquivo de entrada."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def exportar_excel(path: Path, abas: dict[str, pd.DataFrame]) -> Path:
|
||||||
|
"""Exporta um workbook Excel com uma aba por DataFrame."""
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with pd.ExcelWriter(path, engine="openpyxl") as writer:
|
||||||
|
for nome, df in abas.items():
|
||||||
|
df.to_excel(writer, sheet_name=nome[:31], index=False)
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def exportar_csv(path: Path, df: pd.DataFrame) -> Path:
|
||||||
|
"""Exporta CSV garantindo existencia do diretorio."""
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
df.to_csv(path, index=False)
|
||||||
|
return path
|
||||||
73
scripts/03_preparar_dados.py
Normal file
73
scripts/03_preparar_dados.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
"""Preparacao das bases operacionais do Planejador Missao.
|
||||||
|
|
||||||
|
Entradas:
|
||||||
|
Planilhas e CSVs em dados/: cadastro, catalogo de OIs, indisponibilidades,
|
||||||
|
progresso, historico, aeronaves, tripulantes disponiveis e rotas acionadas.
|
||||||
|
|
||||||
|
Saidas:
|
||||||
|
Dicionario com DataFrames normalizados para geracao de colunas candidatas.
|
||||||
|
|
||||||
|
Papel no pipeline:
|
||||||
|
Concentra a leitura e a preparacao antes do MILP, mantendo rastreavel quais
|
||||||
|
arquivos alimentam cada execucao.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
config = importlib.import_module("01_config")
|
||||||
|
io_utils = importlib.import_module("02_io_utils")
|
||||||
|
|
||||||
|
|
||||||
|
def carregar_bases(base_dir: Path = config.PROJECT_ROOT) -> dict:
|
||||||
|
"""Le e normaliza todas as bases necessarias ao planejamento diario."""
|
||||||
|
io_utils.configurar_ambiente()
|
||||||
|
io_utils.exigir_arquivos(config.ARQUIVOS_ENTRADA_OBRIGATORIOS)
|
||||||
|
|
||||||
|
from src.planejador_missao.data_io import (
|
||||||
|
carregar_aeronaves,
|
||||||
|
carregar_cadastro,
|
||||||
|
carregar_catalogo_ois,
|
||||||
|
carregar_historico,
|
||||||
|
carregar_indisponibilidades,
|
||||||
|
carregar_parametros,
|
||||||
|
carregar_progresso_ois,
|
||||||
|
carregar_rotas,
|
||||||
|
carregar_tripulantes_disponiveis,
|
||||||
|
)
|
||||||
|
from src.planejador_missao.rules import combinar_progresso_com_historico
|
||||||
|
|
||||||
|
parametros = carregar_parametros(base_dir)
|
||||||
|
historico = carregar_historico(base_dir)
|
||||||
|
cadastro = carregar_cadastro(base_dir, historico)
|
||||||
|
catalogo = carregar_catalogo_ois(base_dir)
|
||||||
|
indisponibilidades = carregar_indisponibilidades(base_dir)
|
||||||
|
progresso = combinar_progresso_com_historico(carregar_progresso_ois(base_dir), historico, catalogo)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"parametros": parametros,
|
||||||
|
"historico": historico,
|
||||||
|
"cadastro": cadastro,
|
||||||
|
"catalogo": catalogo,
|
||||||
|
"indisponibilidades": indisponibilidades,
|
||||||
|
"progresso": progresso,
|
||||||
|
"aeronaves": carregar_aeronaves(base_dir),
|
||||||
|
"disponiveis": carregar_tripulantes_disponiveis(base_dir),
|
||||||
|
"rotas": carregar_rotas(base_dir),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""Executa uma leitura de diagnostico das bases."""
|
||||||
|
logger = io_utils.configurar_logger("preparar_dados")
|
||||||
|
bases = carregar_bases()
|
||||||
|
logger.info("Tripulantes no cadastro: %s", len(bases["cadastro"]))
|
||||||
|
logger.info("Tripulantes disponiveis: %s", len(bases["disponiveis"]))
|
||||||
|
logger.info("Aeronaves informadas: %s", len(bases["aeronaves"]))
|
||||||
|
logger.info("Rotas acionadas: %s", len(bases["rotas"]))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
56
scripts/04_gerar_colunas_candidatas.py
Normal file
56
scripts/04_gerar_colunas_candidatas.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
"""Geracao das colunas candidatas viaveis.
|
||||||
|
|
||||||
|
Entradas:
|
||||||
|
Bases preparadas por 03_preparar_dados.py.
|
||||||
|
|
||||||
|
Saidas:
|
||||||
|
DataFrame em que cada linha representa uma escala possivel: rota acionada,
|
||||||
|
missao local ou sobreaviso, com dupla, aeronave, score, custo e metadados.
|
||||||
|
|
||||||
|
Papel no pipeline:
|
||||||
|
Materializa o espaco de decisoes do MILP. A compatibilidade de aeronave,
|
||||||
|
indisponibilidade, qualificacao e regra de instrucao ja sao filtradas aqui.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
|
||||||
|
preparar = importlib.import_module("03_preparar_dados")
|
||||||
|
io_utils = importlib.import_module("02_io_utils")
|
||||||
|
|
||||||
|
|
||||||
|
def gerar_colunas_do_dia(bases: dict):
|
||||||
|
"""Gera colunas candidatas para a data e parametros informados nas bases."""
|
||||||
|
from src.planejador_missao.candidates import gerar_colunas
|
||||||
|
|
||||||
|
parametros = bases["parametros"]
|
||||||
|
# Cada coluna candidata equivale a uma variavel x_j do MILP.
|
||||||
|
# As regras de dupla, qualificacao, instrucao e indisponibilidade sao
|
||||||
|
# aplicadas antes do solver para reduzir o modelo a alternativas viaveis.
|
||||||
|
return gerar_colunas(
|
||||||
|
parametros["data_planejamento"],
|
||||||
|
bases["rotas"],
|
||||||
|
bases["aeronaves"],
|
||||||
|
bases["cadastro"],
|
||||||
|
bases["indisponibilidades"],
|
||||||
|
bases["progresso"],
|
||||||
|
bases["catalogo"],
|
||||||
|
bases["disponiveis"],
|
||||||
|
parametros["criterio_missao"],
|
||||||
|
parametros["criterio_sbv"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""Executa diagnostico da geracao de colunas."""
|
||||||
|
logger = io_utils.configurar_logger("gerar_colunas")
|
||||||
|
bases = preparar.carregar_bases()
|
||||||
|
colunas = gerar_colunas_do_dia(bases)
|
||||||
|
logger.info("Colunas candidatas geradas: %s", len(colunas))
|
||||||
|
if colunas.empty:
|
||||||
|
logger.warning("Nenhuma coluna candidata viavel foi gerada.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
58
scripts/05_modelo_milp.py
Normal file
58
scripts/05_modelo_milp.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
"""Modelo MILP do Planejador Missao usando scipy.optimize.milp/HiGHS.
|
||||||
|
|
||||||
|
Entradas:
|
||||||
|
Colunas candidatas viaveis geradas por 04_gerar_colunas_candidatas.py.
|
||||||
|
|
||||||
|
Saidas:
|
||||||
|
Subconjunto de colunas selecionadas pelo solver, representando a escala
|
||||||
|
diaria otimizada.
|
||||||
|
|
||||||
|
Metodologia:
|
||||||
|
Variavel de decisao: x_j em {0,1}, onde cada j representa uma coluna
|
||||||
|
candidata. Como scipy.optimize.milp minimiza, o modelo minimiza -score_milp,
|
||||||
|
equivalente a maximizar o score operacional total.
|
||||||
|
|
||||||
|
Restricoes implementadas no motor src.planejador_missao.optimizer:
|
||||||
|
a) rota acionada obrigatoria coberta exatamente uma vez;
|
||||||
|
b) cada tripulante aparece no maximo uma vez no dia;
|
||||||
|
c) cada aeronave recebe cobertura principal por rota acionada ou SBV;
|
||||||
|
d) aeronave nao executa escalas conflitantes de rota/local no mesmo dia;
|
||||||
|
e) qualificacao, indisponibilidade e regras de instrucao entram no filtro de
|
||||||
|
colunas candidatas, logo colunas inviaveis nao chegam ao MILP;
|
||||||
|
f) regras especificas de rota, missao local e SBV sao preservadas por tipo.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
|
||||||
|
gerador = importlib.import_module("04_gerar_colunas_candidatas")
|
||||||
|
io_utils = importlib.import_module("02_io_utils")
|
||||||
|
preparar = importlib.import_module("03_preparar_dados")
|
||||||
|
|
||||||
|
|
||||||
|
def resolver_escala(colunas, rotas, aeronaves):
|
||||||
|
"""Resolve o MILP e retorna as colunas escolhidas."""
|
||||||
|
from src.planejador_missao.optimizer import resolver_milp
|
||||||
|
|
||||||
|
if colunas.empty:
|
||||||
|
raise RuntimeError("Erro: nao ha colunas candidatas viaveis para montar o MILP.")
|
||||||
|
|
||||||
|
# O resolvedor cria x_j binario para cada linha de colunas.
|
||||||
|
# A funcao objetivo usa -score_milp porque o HiGHS via SciPy resolve
|
||||||
|
# minimizacao; isso preserva a interpretacao de maximizar score.
|
||||||
|
return resolver_milp(colunas, rotas, aeronaves)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""Executa o MILP em modo diagnostico."""
|
||||||
|
logger = io_utils.configurar_logger("modelo_milp")
|
||||||
|
bases = preparar.carregar_bases()
|
||||||
|
colunas = gerador.gerar_colunas_do_dia(bases)
|
||||||
|
solucao = resolver_escala(colunas, bases["rotas"], bases["aeronaves"])
|
||||||
|
logger.info("Colunas candidatas: %s", len(colunas))
|
||||||
|
logger.info("Escalas selecionadas: %s", len(solucao))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
38
scripts/06_exportar_resultados.py
Normal file
38
scripts/06_exportar_resultados.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
"""Exportacao dos resultados do planejamento diario.
|
||||||
|
|
||||||
|
Entradas:
|
||||||
|
Solucao do MILP, catalogo de OIs e data de planejamento.
|
||||||
|
|
||||||
|
Saidas:
|
||||||
|
Planilha em resultados/planejamento_diario_YYYY-MM-DD.xlsx e registros de
|
||||||
|
historico operacional para atualizacao do acumulado.
|
||||||
|
|
||||||
|
Papel no pipeline:
|
||||||
|
Converte a solucao matematica em artefatos utilizaveis pelo esquadrao.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
|
||||||
|
config = importlib.import_module("01_config")
|
||||||
|
io_utils = importlib.import_module("02_io_utils")
|
||||||
|
|
||||||
|
|
||||||
|
def exportar_planejamento(data_planejamento, solucao, catalogo):
|
||||||
|
"""Gera registros operacionais e planilha Excel da escala."""
|
||||||
|
from src.planejador_missao.report import gerar_excel, gerar_registros
|
||||||
|
|
||||||
|
registros = gerar_registros(solucao, catalogo)
|
||||||
|
arquivo = gerar_excel(config.PROJECT_ROOT, data_planejamento, solucao)
|
||||||
|
return {"arquivo": arquivo, "registros": registros}
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""Mostra instrucao de uso deste modulo dentro do pipeline."""
|
||||||
|
logger = io_utils.configurar_logger("exportar_resultados")
|
||||||
|
logger.info("Use python scripts/00_main.py para executar o fluxo completo.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
257
scripts/07_validacao_2025.py
Normal file
257
scripts/07_validacao_2025.py
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
"""Validacao retrospectiva com voos reais de 2025.
|
||||||
|
|
||||||
|
Entradas:
|
||||||
|
dados/Quadro de Voo 2025 (2).xlsx, aba VOOS, ou o CSV ja importado em
|
||||||
|
dados/validacao/voos_2025.csv.
|
||||||
|
|
||||||
|
Saidas:
|
||||||
|
resultados/validacao/validacao_2025_resumo.xlsx
|
||||||
|
resultados/validacao/validacao_2025_detalhada.xlsx
|
||||||
|
resultados/validacao/validacao_2025_metricas.csv
|
||||||
|
resultados/validacao/validacao_2025_barras.png
|
||||||
|
|
||||||
|
Papel no pipeline:
|
||||||
|
Mantem a demanda historica de 2025, gera candidatos compativeis para cada
|
||||||
|
slot historico e usa MILP para redistribuir tripulantes preservando horas.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
import re
|
||||||
|
import struct
|
||||||
|
import zlib
|
||||||
|
from datetime import time, timedelta
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
config = importlib.import_module("01_config")
|
||||||
|
io_utils = importlib.import_module("02_io_utils")
|
||||||
|
|
||||||
|
COLUNAS_VALIDACAO = ["slot_id", "data", "aeronave", "tipo_escala", "tripulante", "funcao", "oi", "horas_voadas", "sbv"]
|
||||||
|
|
||||||
|
|
||||||
|
def _normalizar_texto(valor: object) -> str:
|
||||||
|
from src.planejador_missao.utils import normalizar_texto
|
||||||
|
|
||||||
|
return normalizar_texto(valor)
|
||||||
|
|
||||||
|
|
||||||
|
def _horas(valor: object) -> float:
|
||||||
|
"""Converte TEV da planilha historica para horas decimais."""
|
||||||
|
if pd.isna(valor):
|
||||||
|
return 0.0
|
||||||
|
if isinstance(valor, timedelta):
|
||||||
|
return round(valor.total_seconds() / 3600, 2)
|
||||||
|
if isinstance(valor, time):
|
||||||
|
return round(valor.hour + valor.minute / 60 + valor.second / 3600, 2)
|
||||||
|
if isinstance(valor, str):
|
||||||
|
texto = valor.strip()
|
||||||
|
if not texto or texto == "-":
|
||||||
|
return 0.0
|
||||||
|
partes = texto.split(":")
|
||||||
|
if len(partes) >= 2:
|
||||||
|
return round(float(partes[0]) + float(partes[1]) / 60, 2)
|
||||||
|
return float(texto.replace(",", "."))
|
||||||
|
if isinstance(valor, (int, float)):
|
||||||
|
return round(float(valor) * 24, 2) if 0 < float(valor) < 1 else float(valor)
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def _aeronave(valor: object) -> str:
|
||||||
|
"""Traduz matricula FAB para familia operacional C95/C97/C98."""
|
||||||
|
texto = _normalizar_texto(valor)
|
||||||
|
match = re.search(r"(\d{4})", texto)
|
||||||
|
if not match:
|
||||||
|
return ""
|
||||||
|
numero = match.group(1)
|
||||||
|
if numero.startswith("20"):
|
||||||
|
return "C97"
|
||||||
|
if numero.startswith("23"):
|
||||||
|
return "C95"
|
||||||
|
if numero.startswith("27"):
|
||||||
|
return "C98"
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _tripulantes(valor: object) -> list[str]:
|
||||||
|
"""Extrai a dupla principal da tripulacao historica."""
|
||||||
|
partes = re.split(r"\s*/\s*", str(valor))
|
||||||
|
trips = []
|
||||||
|
for parte in partes:
|
||||||
|
texto = _normalizar_texto(parte)
|
||||||
|
texto = re.sub(r"\([^)]*\)", " ", texto)
|
||||||
|
texto = re.sub(r"[^A-Z0-9]+", " ", texto).strip()
|
||||||
|
if texto and texto != "NAN":
|
||||||
|
trips.append(texto)
|
||||||
|
return trips[:2]
|
||||||
|
|
||||||
|
|
||||||
|
def importar_voos_2025(origem: Path = config.ARQUIVO_QUADRO_VOO_2025, destino: Path = config.ARQUIVO_VOOS_2025) -> pd.DataFrame:
|
||||||
|
"""Importa voos realizados de 2025 da aba VOOS para o CSV de validacao."""
|
||||||
|
io_utils.exigir_arquivos([origem])
|
||||||
|
raw = pd.read_excel(origem, sheet_name=config.ABAS["voos_2025"], header=None)
|
||||||
|
linhas = []
|
||||||
|
slot_id = 0
|
||||||
|
for _, row in raw.iterrows():
|
||||||
|
data = pd.to_datetime(row[2], errors="coerce")
|
||||||
|
if pd.isna(data) or data.year != 2025:
|
||||||
|
continue
|
||||||
|
if _normalizar_texto(row[41]) != "REALIZADO":
|
||||||
|
continue
|
||||||
|
|
||||||
|
aeronave = _aeronave(row[37])
|
||||||
|
if not aeronave:
|
||||||
|
continue
|
||||||
|
|
||||||
|
horas_voadas = _horas(row[30])
|
||||||
|
if horas_voadas <= 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
trips = _tripulantes(row[8])
|
||||||
|
if len(trips) < 2:
|
||||||
|
continue
|
||||||
|
|
||||||
|
slot_id += 1
|
||||||
|
tipo_escala = _normalizar_texto(row[1])
|
||||||
|
oi = _normalizar_texto(row[35])
|
||||||
|
if not oi or oi == "-":
|
||||||
|
oi = _normalizar_texto(row[32])
|
||||||
|
|
||||||
|
for idx, tripulante in enumerate(trips, start=1):
|
||||||
|
linhas.append(
|
||||||
|
{
|
||||||
|
"slot_id": slot_id,
|
||||||
|
"data": data.date().isoformat(),
|
||||||
|
"aeronave": aeronave,
|
||||||
|
"tipo_escala": tipo_escala,
|
||||||
|
"tripulante": tripulante,
|
||||||
|
"funcao": "PILOTO" if idx == 1 else "COPILOTO",
|
||||||
|
"oi": oi,
|
||||||
|
"horas_voadas": horas_voadas,
|
||||||
|
"sbv": 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
df = pd.DataFrame(linhas, columns=COLUNAS_VALIDACAO)
|
||||||
|
io_utils.exportar_csv(destino, df)
|
||||||
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
def _png_barras(path: Path, metricas: pd.DataFrame) -> Path:
|
||||||
|
"""Gera PNG simples sem depender de bibliotecas graficas externas."""
|
||||||
|
width, height = 760, 420
|
||||||
|
canvas = bytearray([255, 255, 255] * width * height)
|
||||||
|
|
||||||
|
def rect(x0: int, y0: int, x1: int, y1: int, color: tuple[int, int, int]) -> None:
|
||||||
|
x0, x1 = max(0, x0), min(width, x1)
|
||||||
|
y0, y1 = max(0, y0), min(height, y1)
|
||||||
|
for y in range(y0, y1):
|
||||||
|
for x in range(x0, x1):
|
||||||
|
i = (y * width + x) * 3
|
||||||
|
canvas[i : i + 3] = bytes(color)
|
||||||
|
|
||||||
|
rect(55, 45, 60, 365, (38, 57, 77))
|
||||||
|
rect(55, 360, 705, 365, (38, 57, 77))
|
||||||
|
valores = metricas.set_index("cenario")["desvio_padrao"].to_dict()
|
||||||
|
max_v = max(valores.values()) if valores else 1
|
||||||
|
cores = {"real_2025": (154, 52, 18), "otimizado_meta_50": (38, 115, 77)}
|
||||||
|
xs = {"real_2025": 190, "otimizado_meta_50": 430}
|
||||||
|
for cenario, valor in valores.items():
|
||||||
|
h = int((valor / max_v) * 270)
|
||||||
|
rect(xs[cenario], 360 - h, xs[cenario] + 110, 360, cores[cenario])
|
||||||
|
|
||||||
|
raw = b"".join(b"\x00" + canvas[y * width * 3 : (y + 1) * width * 3] for y in range(height))
|
||||||
|
png = b"\x89PNG\r\n\x1a\n"
|
||||||
|
for chunk_type, data in [
|
||||||
|
(b"IHDR", struct.pack(">IIBBBBB", width, height, 8, 2, 0, 0, 0)),
|
||||||
|
(b"IDAT", zlib.compress(raw, 9)),
|
||||||
|
(b"IEND", b""),
|
||||||
|
]:
|
||||||
|
png += struct.pack(">I", len(data)) + chunk_type + data + struct.pack(">I", zlib.crc32(chunk_type + data) & 0xFFFFFFFF)
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_bytes(png)
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def executar_validacao_2025(importar: bool = True, logger=None) -> dict:
|
||||||
|
"""Executa a validacao completa e exporta os artefatos finais."""
|
||||||
|
io_utils.configurar_ambiente()
|
||||||
|
logger = logger or io_utils.configurar_logger("validacao_2025")
|
||||||
|
inicio = pd.Timestamp.now()
|
||||||
|
|
||||||
|
if importar or not config.ARQUIVO_VOOS_2025.exists():
|
||||||
|
voos_importados = importar_voos_2025()
|
||||||
|
logger.info("Voos 2025 importados: %s registros", len(voos_importados))
|
||||||
|
|
||||||
|
from src.planejador_missao.validacao import carregar_voos_2025, otimizar_equalizacao_50
|
||||||
|
|
||||||
|
voos = carregar_voos_2025(config.ARQUIVO_VOOS_2025)
|
||||||
|
if voos.empty:
|
||||||
|
raise RuntimeError("Erro: base de validacao 2025 vazia. Verifique dados/validacao/voos_2025.csv.")
|
||||||
|
if (voos["horas_voadas"] < 0).any():
|
||||||
|
raise RuntimeError("Erro: foram encontradas horas voadas negativas na validacao 2025.")
|
||||||
|
|
||||||
|
resultado = otimizar_equalizacao_50(
|
||||||
|
config.PROJECT_ROOT,
|
||||||
|
voos,
|
||||||
|
meta_horas=config.META_HORAS_PADRAO,
|
||||||
|
tempo_limite_segundos=config.VALIDACAO_TEMPO_LIMITE_SEGUNDOS,
|
||||||
|
gap_relativo=config.VALIDACAO_GAP_RELATIVO,
|
||||||
|
)
|
||||||
|
metricas = resultado["metricas"]
|
||||||
|
total_real = float(metricas.loc[metricas["cenario"] == "real_2025", "horas_total"].iloc[0])
|
||||||
|
total_otim = float(metricas.loc[metricas["cenario"] == "otimizado_meta_50", "horas_total"].iloc[0])
|
||||||
|
if abs(total_real - total_otim) > 0.01:
|
||||||
|
logger.warning("Horas nao preservadas: real=%s otimizado=%s", total_real, total_otim)
|
||||||
|
|
||||||
|
resumo_path = config.VALIDACAO_DIR / "validacao_2025_resumo.xlsx"
|
||||||
|
detalhada_path = config.VALIDACAO_DIR / "validacao_2025_detalhada.xlsx"
|
||||||
|
metricas_path = config.VALIDACAO_DIR / "validacao_2025_metricas.csv"
|
||||||
|
grafico_path = config.VALIDACAO_DIR / "validacao_2025_barras.png"
|
||||||
|
|
||||||
|
solver_df = pd.DataFrame([resultado.get("status_solver", {})])
|
||||||
|
io_utils.exportar_excel(resumo_path, {"solver": solver_df, "metricas": metricas})
|
||||||
|
io_utils.exportar_excel(
|
||||||
|
detalhada_path,
|
||||||
|
{
|
||||||
|
"solver": solver_df,
|
||||||
|
"metricas": metricas,
|
||||||
|
"comparativo_trips": resultado["comparativo"],
|
||||||
|
"voos_2025_slots": resultado["slots"],
|
||||||
|
"escala_otimizada": resultado["otimizados"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
io_utils.exportar_csv(metricas_path, metricas)
|
||||||
|
_png_barras(grafico_path, metricas)
|
||||||
|
|
||||||
|
duracao = (pd.Timestamp.now() - inicio).total_seconds()
|
||||||
|
logger.info("Slots historicos avaliados: %s", len(resultado["slots"]))
|
||||||
|
logger.info("Alocacoes otimizadas: %s", len(resultado["otimizados"]))
|
||||||
|
logger.info("Tripulantes comparados: %s", len(resultado["comparativo"]))
|
||||||
|
logger.info("Status solver: %s", resultado["status_solver"]["mensagem"])
|
||||||
|
logger.info("Tempo de validacao: %.1f s", duracao)
|
||||||
|
logger.info("Arquivos exportados: %s | %s | %s | %s", resumo_path, detalhada_path, metricas_path, grafico_path)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"resultado": resultado,
|
||||||
|
"arquivos": [resumo_path, detalhada_path, metricas_path, grafico_path],
|
||||||
|
"duracao_segundos": duracao,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""Ponto de entrada da validacao 2025."""
|
||||||
|
logger = io_utils.configurar_logger("validacao_2025")
|
||||||
|
saida = executar_validacao_2025(importar=True, logger=logger)
|
||||||
|
metricas = saida["resultado"]["metricas"]
|
||||||
|
print("\n=== Validacao 2025 concluida ===")
|
||||||
|
print(metricas.to_string(index=False))
|
||||||
|
print("\nArquivos gerados:")
|
||||||
|
for path in saida["arquivos"]:
|
||||||
|
print(f"- {path}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
44
scripts/08_app_utils.py
Normal file
44
scripts/08_app_utils.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
"""Funcoes auxiliares da interface web local.
|
||||||
|
|
||||||
|
Entradas:
|
||||||
|
Estrutura padrao do projeto e web_app.py.
|
||||||
|
|
||||||
|
Saidas:
|
||||||
|
Inicializacao local da interface em http://127.0.0.1:8050 quando chamado
|
||||||
|
diretamente.
|
||||||
|
|
||||||
|
Papel no pipeline:
|
||||||
|
Facilita acesso ao app Python com interface web local.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
config = importlib.import_module("01_config")
|
||||||
|
io_utils = importlib.import_module("02_io_utils")
|
||||||
|
|
||||||
|
|
||||||
|
def comando_python_app() -> list[str]:
|
||||||
|
"""Retorna comando portavel para iniciar a interface web local."""
|
||||||
|
pythonw = config.PROJECT_ROOT / ".venv" / "Scripts" / "pythonw.exe"
|
||||||
|
python = config.PROJECT_ROOT / ".venv" / "Scripts" / "python.exe"
|
||||||
|
exe = pythonw if pythonw.exists() else python if python.exists() else sys.executable
|
||||||
|
return [str(exe), str(config.PROJECT_ROOT / "web_app.py")]
|
||||||
|
|
||||||
|
|
||||||
|
def abrir_app() -> None:
|
||||||
|
"""Inicia o app em segundo plano sem janela de console quando possivel."""
|
||||||
|
io_utils.configurar_ambiente()
|
||||||
|
subprocess.Popen(comando_python_app(), cwd=config.PROJECT_ROOT)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
abrir_app()
|
||||||
|
print("Interface web local iniciada em http://127.0.0.1:8050")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
63
scripts/09_testar_instalacao.py
Normal file
63
scripts/09_testar_instalacao.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
"""Teste rapido de instalacao do Planejador Missao.
|
||||||
|
|
||||||
|
Entradas:
|
||||||
|
Ambiente Python atual, dependencias instaladas e arquivos em dados/.
|
||||||
|
|
||||||
|
Saidas:
|
||||||
|
Diagnostico amigavel no terminal e log em logs/execucao.log.
|
||||||
|
|
||||||
|
Papel no pipeline:
|
||||||
|
Permite verificar rapidamente se o computador esta pronto para executar o
|
||||||
|
planejador e o solver HiGHS/scipy.optimize.milp.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from scipy.optimize import Bounds, LinearConstraint, milp
|
||||||
|
|
||||||
|
config = importlib.import_module("01_config")
|
||||||
|
io_utils = importlib.import_module("02_io_utils")
|
||||||
|
|
||||||
|
|
||||||
|
def testar_solver_minimo() -> None:
|
||||||
|
"""Resolve um MILP minimo para comprovar que scipy.optimize.milp funciona."""
|
||||||
|
c = np.array([-1.0, -2.0])
|
||||||
|
integrality = np.ones(2)
|
||||||
|
bounds = Bounds([0, 0], [1, 1])
|
||||||
|
constraints = [LinearConstraint([[1, 1]], -np.inf, 1)]
|
||||||
|
result = milp(c=c, integrality=integrality, bounds=bounds, constraints=constraints)
|
||||||
|
if not result.success:
|
||||||
|
raise RuntimeError(f"Erro: teste minimo do solver falhou: {result.message}")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""Executa todas as verificacoes de ambiente."""
|
||||||
|
logger = io_utils.configurar_logger("teste_instalacao")
|
||||||
|
print(f"Python encontrado: {sys.version.split()[0]}")
|
||||||
|
|
||||||
|
for pacote in ["pandas", "numpy", "openpyxl", "scipy"]:
|
||||||
|
importlib.import_module(pacote)
|
||||||
|
print(f"Biblioteca OK: {pacote}")
|
||||||
|
|
||||||
|
config.garantir_diretorios()
|
||||||
|
for diretorio in [config.DADOS_DIR, config.RESULTADOS_DIR, config.SRC_DIR, config.LOGS_DIR, config.TEMP_DIR]:
|
||||||
|
if not diretorio.exists():
|
||||||
|
raise RuntimeError(f"Erro: diretorio obrigatorio ausente: {diretorio}")
|
||||||
|
print(f"Diretorio OK: {diretorio.name}")
|
||||||
|
|
||||||
|
cadastro = config.ARQUIVO_CADASTRO_LOCAL if config.ARQUIVO_CADASTRO_LOCAL.exists() else config.ARQUIVO_CADASTRO_ORIGINAL
|
||||||
|
io_utils.exigir_arquivos([cadastro, *config.ARQUIVOS_ENTRADA_OBRIGATORIOS])
|
||||||
|
print("Arquivos de entrada OK")
|
||||||
|
|
||||||
|
testar_solver_minimo()
|
||||||
|
print("Solver scipy.optimize.milp/HiGHS OK")
|
||||||
|
logger.info("Teste de instalacao concluido com sucesso.")
|
||||||
|
print("Ambiente OK para executar o Planejador Missao.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
136
scripts/_arquivados/importar_quadro_voo_2025.py
Normal file
136
scripts/_arquivados/importar_quadro_voo_2025.py
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
"""Importa a aba VOOS do Quadro de Voo 2025 para a base de validacao."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from datetime import time, timedelta
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
BASE_DIR = Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.insert(0, str(BASE_DIR))
|
||||||
|
|
||||||
|
from src.planejador_missao.utils import normalizar_texto
|
||||||
|
|
||||||
|
|
||||||
|
COLUNAS_VALIDACAO = ["slot_id", "data", "aeronave", "tipo_escala", "tripulante", "funcao", "oi", "horas_voadas", "sbv"]
|
||||||
|
|
||||||
|
|
||||||
|
def _horas(valor: object) -> float:
|
||||||
|
if pd.isna(valor):
|
||||||
|
return 0.0
|
||||||
|
if isinstance(valor, timedelta):
|
||||||
|
return round(valor.total_seconds() / 3600, 2)
|
||||||
|
if isinstance(valor, time):
|
||||||
|
return round(valor.hour + valor.minute / 60 + valor.second / 3600, 2)
|
||||||
|
if isinstance(valor, str):
|
||||||
|
texto = valor.strip()
|
||||||
|
if not texto or texto == "-":
|
||||||
|
return 0.0
|
||||||
|
partes = texto.split(":")
|
||||||
|
if len(partes) >= 2:
|
||||||
|
return round(float(partes[0]) + float(partes[1]) / 60, 2)
|
||||||
|
return float(texto.replace(",", "."))
|
||||||
|
if isinstance(valor, (int, float)):
|
||||||
|
return round(float(valor) * 24, 2) if 0 < float(valor) < 1 else float(valor)
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def _aeronave(valor: object) -> str:
|
||||||
|
texto = normalizar_texto(valor)
|
||||||
|
match = re.search(r"(\d{4})", texto)
|
||||||
|
if not match:
|
||||||
|
return ""
|
||||||
|
numero = match.group(1)
|
||||||
|
if numero.startswith("20"):
|
||||||
|
return "C97"
|
||||||
|
if numero.startswith("23"):
|
||||||
|
return "C95"
|
||||||
|
if numero.startswith("27"):
|
||||||
|
return "C98"
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _tripulantes(valor: object) -> list[str]:
|
||||||
|
partes = re.split(r"\s*/\s*", str(valor))
|
||||||
|
trips = []
|
||||||
|
for parte in partes:
|
||||||
|
texto = normalizar_texto(parte)
|
||||||
|
texto = re.sub(r"\([^)]*\)", " ", texto)
|
||||||
|
texto = re.sub(r"[^A-Z0-9]+", " ", texto).strip()
|
||||||
|
if texto and texto != "NAN":
|
||||||
|
trips.append(texto)
|
||||||
|
return trips[:2]
|
||||||
|
|
||||||
|
|
||||||
|
def importar(origem: Path, destino: Path) -> pd.DataFrame:
|
||||||
|
raw = pd.read_excel(origem, sheet_name="VOOS", header=None)
|
||||||
|
linhas = []
|
||||||
|
slot_id = 0
|
||||||
|
for _, row in raw.iterrows():
|
||||||
|
data = pd.to_datetime(row[2], errors="coerce")
|
||||||
|
if pd.isna(data) or data.year != 2025:
|
||||||
|
continue
|
||||||
|
if normalizar_texto(row[41]) != "REALIZADO":
|
||||||
|
continue
|
||||||
|
|
||||||
|
aeronave = _aeronave(row[37])
|
||||||
|
if not aeronave:
|
||||||
|
continue
|
||||||
|
|
||||||
|
slot_id += 1
|
||||||
|
tipo_escala = normalizar_texto(row[1])
|
||||||
|
horas_voadas = _horas(row[30])
|
||||||
|
oi = normalizar_texto(row[35])
|
||||||
|
if not oi or oi == "-":
|
||||||
|
oi = normalizar_texto(row[32])
|
||||||
|
|
||||||
|
for idx, tripulante in enumerate(_tripulantes(row[8]), start=1):
|
||||||
|
linhas.append(
|
||||||
|
{
|
||||||
|
"slot_id": slot_id,
|
||||||
|
"data": data.date().isoformat(),
|
||||||
|
"aeronave": aeronave,
|
||||||
|
"tipo_escala": tipo_escala,
|
||||||
|
"tripulante": tripulante,
|
||||||
|
"funcao": "PILOTO" if idx == 1 else "COPILOTO",
|
||||||
|
"oi": oi,
|
||||||
|
"horas_voadas": horas_voadas,
|
||||||
|
"sbv": 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
df = pd.DataFrame(linhas, columns=COLUNAS_VALIDACAO)
|
||||||
|
destino.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
df.to_csv(destino, index=False)
|
||||||
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description="Importa voos realizados de 2025 para a validacao.")
|
||||||
|
parser.add_argument(
|
||||||
|
"--origem",
|
||||||
|
type=Path,
|
||||||
|
default=BASE_DIR / "dados" / "Quadro de Voo 2025 (2).xlsx",
|
||||||
|
help="Planilha com aba VOOS.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--destino",
|
||||||
|
type=Path,
|
||||||
|
default=BASE_DIR / "dados" / "validacao" / "voos_2025.csv",
|
||||||
|
help="CSV de validacao gerado.",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
df = importar(args.origem, args.destino)
|
||||||
|
print(f"Arquivo gerado: {args.destino}")
|
||||||
|
print(f"Registros exportados: {len(df)}")
|
||||||
|
print(f"Voos/slots exportados: {df['slot_id'].nunique() if not df.empty else 0}")
|
||||||
|
print(f"Tripulantes exportados: {df['tripulante'].nunique() if not df.empty else 0}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
57
scripts/_arquivados/validar_2025.py
Normal file
57
scripts/_arquivados/validar_2025.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
"""Executa a validacao retrospectiva com voos de 2025."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
BASE_DIR = Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.insert(0, str(BASE_DIR))
|
||||||
|
|
||||||
|
from src.planejador_missao.validacao import (
|
||||||
|
carregar_voos_2025,
|
||||||
|
criar_modelo_voos_2025,
|
||||||
|
otimizar_equalizacao_50,
|
||||||
|
salvar_relatorio_validacao,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description="Valida o planejador com voos historicos de 2025.")
|
||||||
|
parser.add_argument("--meta", type=float, default=50.0, help="Meta de horas usada na equalizacao.")
|
||||||
|
parser.add_argument("--tempo-limite", type=float, default=180.0, help="Tempo limite do resolvedor MILP, em segundos.")
|
||||||
|
parser.add_argument("--gap", type=float, default=0.05, help="Gap relativo aceitavel para encerrar o MILP.")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
entrada = BASE_DIR / "dados" / "validacao" / "voos_2025.csv"
|
||||||
|
criar_modelo_voos_2025(entrada)
|
||||||
|
|
||||||
|
voos = carregar_voos_2025(entrada)
|
||||||
|
if voos.empty:
|
||||||
|
print("Base de validacao ainda vazia.")
|
||||||
|
print(f"Preencha o arquivo: {entrada}")
|
||||||
|
print("Colunas esperadas: data,aeronave,tipo_escala,tripulante,funcao,oi,horas_voadas,sbv")
|
||||||
|
return
|
||||||
|
|
||||||
|
resultado = otimizar_equalizacao_50(
|
||||||
|
BASE_DIR,
|
||||||
|
voos,
|
||||||
|
meta_horas=args.meta,
|
||||||
|
tempo_limite_segundos=args.tempo_limite,
|
||||||
|
gap_relativo=args.gap,
|
||||||
|
)
|
||||||
|
arquivo = salvar_relatorio_validacao(BASE_DIR, resultado, meta_horas=args.meta)
|
||||||
|
|
||||||
|
print("\n=== Validacao 2025 concluida ===")
|
||||||
|
print(f"Meta de equalizacao: {args.meta:g} horas")
|
||||||
|
print(f"Voos/slots avaliados: {len(resultado['slots'])}")
|
||||||
|
print(f"Tripulantes comparados: {len(resultado['comparativo'])}")
|
||||||
|
print(f"Relatorio: {arquivo}")
|
||||||
|
print(f"Solver: {resultado['status_solver']['mensagem']}")
|
||||||
|
print("\nMetricas:")
|
||||||
|
print(resultado["metricas"].to_string(index=False))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
2
src/planejador_missao/__init__.py
Normal file
2
src/planejador_missao/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
"""Planejador de missao e sobreaviso por MILP."""
|
||||||
|
|
||||||
216
src/planejador_missao/candidates.py
Normal file
216
src/planejador_missao/candidates.py
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
"""Geracao de colunas candidatas para o modelo MILP."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import date
|
||||||
|
from itertools import combinations
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from .data_io import AERONAVES
|
||||||
|
from .rules import eh_instrutor_adaptado, piloto_disponivel, qualificacao_operacional
|
||||||
|
from .utils import normalizar_texto, parse_horas
|
||||||
|
|
||||||
|
|
||||||
|
def proxima_oi(tripulante: str, aeronave: str, subprograma: str, progresso: pd.DataFrame, catalogo: pd.DataFrame) -> dict | None:
|
||||||
|
ois = catalogo[(catalogo["aeronave"] == aeronave) & (catalogo["subprograma"] == subprograma)].sort_values("ordem")
|
||||||
|
if ois.empty:
|
||||||
|
return None
|
||||||
|
concluidas = set(
|
||||||
|
progresso[
|
||||||
|
(progresso["tripulante"] == tripulante)
|
||||||
|
& (progresso["aeronave"] == aeronave)
|
||||||
|
& (progresso["subprograma"] == subprograma)
|
||||||
|
& (progresso["concluida"] == True)
|
||||||
|
]["oi"]
|
||||||
|
)
|
||||||
|
pendentes = ois[~ois["oi"].isin(concluidas)]
|
||||||
|
if pendentes.empty:
|
||||||
|
return None
|
||||||
|
row = pendentes.iloc[0].to_dict()
|
||||||
|
row["tripulante"] = tripulante
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
def candidatos_missao(data_missao: date, aeronave: str, tipo: str, cadastro: pd.DataFrame, indisponibilidades: pd.DataFrame, progresso: pd.DataFrame, catalogo: pd.DataFrame, disponiveis: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
qt_col = f"qt_{aeronave.lower()}"
|
||||||
|
permitidos = set(disponiveis["tripulante"])
|
||||||
|
linhas = []
|
||||||
|
for _, trip in cadastro[cadastro[qt_col]].iterrows():
|
||||||
|
trigrama = trip["tripulante"]
|
||||||
|
if trigrama not in permitidos or not piloto_disponivel(trigrama, data_missao, indisponibilidades):
|
||||||
|
continue
|
||||||
|
subprogramas = [sp for sp in str(trip[f"subprograma_{aeronave.lower()}"]).split() if sp.startswith("SP")]
|
||||||
|
for sp in subprogramas:
|
||||||
|
oi = proxima_oi(trigrama, aeronave, sp, progresso, catalogo)
|
||||||
|
if not oi:
|
||||||
|
continue
|
||||||
|
if tipo != "TODAS" and oi["tipo_missao"] != tipo:
|
||||||
|
continue
|
||||||
|
oi.update(
|
||||||
|
patente=trip.get("patente", ""),
|
||||||
|
qt=trip.get("qt", ""),
|
||||||
|
qualificacao=trip.get("qualificacao", ""),
|
||||||
|
horas_totais_2026=trip.get("horas_totais_2026", 0),
|
||||||
|
horas_realizadas_2026=trip.get("horas_realizadas_2026", 0),
|
||||||
|
sbv_realizados_2026=trip.get("sbv_realizados_2026", 0),
|
||||||
|
soldo=trip.get("soldo", 0),
|
||||||
|
)
|
||||||
|
linhas.append(oi)
|
||||||
|
if not linhas:
|
||||||
|
return pd.DataFrame()
|
||||||
|
df = pd.DataFrame(linhas)
|
||||||
|
df["horas_faltantes_paop"] = df["horas_totais_2026"] - df["horas_realizadas_2026"]
|
||||||
|
df["prioridade_tipo"] = df["tipo_missao"].map({"LOCAL": 1, "ROTA": 2}).fillna(3)
|
||||||
|
return df.sort_values(["horas_faltantes_paop", "prioridade_tipo", "aeronave", "subprograma", "ordem"], ascending=[False, True, True, True, True])
|
||||||
|
|
||||||
|
|
||||||
|
def aplicar_prioridades(df: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
df = df.copy()
|
||||||
|
df["bloqueado"] = False
|
||||||
|
df["prioridade_paop"] = 100.0
|
||||||
|
df.loc[(df["aeronave"] == "C98") & (df["tripulante"].isin(["DOG", "MCH"])) & (df["subprograma"] == "SPQE-3"), "prioridade_paop"] = 1
|
||||||
|
df.loc[(df["aeronave"] == "C98") & (df["tripulante"].isin(["DOG", "MCH"])) & (df["subprograma"] != "SPQE-3"), "prioridade_paop"] = 50
|
||||||
|
df.loc[(df["aeronave"] == "C98") & (df["tripulante"].isin(["SCA", "DIL"])) & (df["subprograma"] == "SPMO-1"), "prioridade_paop"] = 1
|
||||||
|
df.loc[(df["aeronave"] == "C98") & (df["tripulante"].isin(["SCA", "DIL"])) & (df["subprograma"] != "SPMO-1"), "prioridade_paop"] = 60
|
||||||
|
df.loc[(df["aeronave"] == "C98") & (df["subprograma"] == "SPFO-2") & (df["horas_realizadas_2026"] < 50), "bloqueado"] = True
|
||||||
|
return df.sort_values(["bloqueado", "prioridade_paop", "horas_faltantes_paop"], ascending=[True, True, False])
|
||||||
|
|
||||||
|
|
||||||
|
def score_dupla(p1, p2, h1, h2, s1, s2, sbv1, sbv2, criterio: str) -> float:
|
||||||
|
beneficio = (0 if pd.isna(p1) else 100 / p1) + (0 if pd.isna(p2) else 100 / p2)
|
||||||
|
horas_50 = (h1 < 50) + (h2 < 50)
|
||||||
|
horas_110 = (h1 < 110) + (h2 < 110)
|
||||||
|
custo = (0 if pd.isna(s1) else s1) + (0 if pd.isna(s2) else s2)
|
||||||
|
sbv = (0 if pd.isna(sbv1) else sbv1) + (0 if pd.isna(sbv2) else sbv2)
|
||||||
|
dif_sbv = abs((0 if pd.isna(sbv1) else sbv1) - (0 if pd.isna(sbv2) else sbv2))
|
||||||
|
if criterio == "menor_custo":
|
||||||
|
return -custo + 0.2 * beneficio + 20 * horas_50 + 10 * horas_110
|
||||||
|
if criterio == "meta_110":
|
||||||
|
return 100 * horas_110 + 5 * beneficio - 0.05 * custo
|
||||||
|
if criterio == "equalizar_quadrinhos":
|
||||||
|
return -sbv - 0.5 * dif_sbv + 0.1 * beneficio
|
||||||
|
return 100 * horas_50 + 10 * beneficio - 0.05 * custo
|
||||||
|
|
||||||
|
|
||||||
|
def instrutores_disponiveis(data_missao: date, aeronave: str, cadastro: pd.DataFrame, indisponibilidades: pd.DataFrame, progresso: pd.DataFrame, catalogo: pd.DataFrame, disponiveis: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
qt_col = f"qt_{aeronave.lower()}"
|
||||||
|
permitidos = set(disponiveis["tripulante"])
|
||||||
|
rows = []
|
||||||
|
for _, row in cadastro[cadastro[qt_col]].iterrows():
|
||||||
|
trip = row["tripulante"]
|
||||||
|
if trip in permitidos and piloto_disponivel(trip, data_missao, indisponibilidades) and eh_instrutor_adaptado(trip, aeronave, cadastro, progresso, catalogo):
|
||||||
|
rows.append(row)
|
||||||
|
return pd.DataFrame(rows)
|
||||||
|
|
||||||
|
|
||||||
|
def gerar_duplas(candidatos: pd.DataFrame, criterio: str, tipo_escala: str, aeronave: str, data_planejamento: date, cadastro: pd.DataFrame, indisponibilidades: pd.DataFrame, progresso: pd.DataFrame, catalogo: pd.DataFrame, disponiveis: pd.DataFrame, rota_id=None, rota=None, observacao="") -> pd.DataFrame:
|
||||||
|
if candidatos.empty:
|
||||||
|
return pd.DataFrame()
|
||||||
|
livres = candidatos[~candidatos["bloqueado"]].drop_duplicates("tripulante")
|
||||||
|
linhas = []
|
||||||
|
if tipo_escala in {"ROTA_ACIONADA", "MISSAO_LOCAL"}:
|
||||||
|
instrutores = instrutores_disponiveis(data_planejamento, aeronave, cadastro, indisponibilidades, progresso, catalogo, disponiveis)
|
||||||
|
for _, aluno in livres.iterrows():
|
||||||
|
for _, inst in instrutores.iterrows():
|
||||||
|
if aluno["tripulante"] == inst["tripulante"]:
|
||||||
|
continue
|
||||||
|
linhas.append(_linha_coluna(data_planejamento, tipo_escala, aeronave, aluno, inst, criterio, rota_id, rota, observacao))
|
||||||
|
return pd.DataFrame(linhas)
|
||||||
|
for t1, t2 in combinations(livres["tripulante"].unique(), 2):
|
||||||
|
info1 = livres[livres["tripulante"] == t1].iloc[0]
|
||||||
|
info2 = livres[livres["tripulante"] == t2].iloc[0]
|
||||||
|
linha = _linha_coluna(data_planejamento, tipo_escala, aeronave, info1, info2, criterio, rota_id, rota, observacao)
|
||||||
|
linha["oi_trip1"] = ""
|
||||||
|
linha["oi_trip2"] = ""
|
||||||
|
linhas.append(linha)
|
||||||
|
return pd.DataFrame(linhas)
|
||||||
|
|
||||||
|
|
||||||
|
def _linha_coluna(data_planejamento, tipo_escala, aeronave, info1, info2, criterio, rota_id, rota, observacao):
|
||||||
|
h2 = info2.get("horas_realizadas_2026", 999)
|
||||||
|
p2 = info2.get("prioridade_paop", pd.NA)
|
||||||
|
score = score_dupla(info1.get("prioridade_paop", pd.NA), p2, info1.get("horas_realizadas_2026", 0), h2, info1.get("soldo", 0), info2.get("soldo", 0), info1.get("sbv_realizados_2026", 0), info2.get("sbv_realizados_2026", 0), criterio)
|
||||||
|
return {
|
||||||
|
"data": data_planejamento,
|
||||||
|
"tipo_escala": tipo_escala,
|
||||||
|
"rota_id": rota_id,
|
||||||
|
"aeronave": aeronave,
|
||||||
|
"origem": "" if rota is None else rota.get("origem", ""),
|
||||||
|
"destino": "" if rota is None else rota.get("destino", ""),
|
||||||
|
"inicio": pd.NaT if rota is None else rota.get("inicio", pd.NaT),
|
||||||
|
"fim": pd.NaT if rota is None else rota.get("fim", pd.NaT),
|
||||||
|
"tev_horas": pd.NA if rota is None else rota.get("tev_horas", pd.NA),
|
||||||
|
"pernoite_dias": pd.NA if rota is None else rota.get("pernoite_dias", pd.NA),
|
||||||
|
"rota": "" if rota is None else rota.get("rota", ""),
|
||||||
|
"trip1": info1["tripulante"],
|
||||||
|
"trip2": info2["tripulante"],
|
||||||
|
"oi_trip1": info1.get("oi", ""),
|
||||||
|
"oi_trip2": "",
|
||||||
|
"prioridade_paop_1": info1.get("prioridade_paop", pd.NA),
|
||||||
|
"prioridade_paop_2": p2,
|
||||||
|
"custo_financeiro": info1.get("soldo", 0) + info2.get("soldo", 0),
|
||||||
|
"score": score,
|
||||||
|
"observacao": observacao,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def gerar_colunas(data_planejamento, rotas, aeronaves, cadastro, indisponibilidades, progresso, catalogo, disponiveis, criterio_missao, criterio_sbv):
|
||||||
|
livres = aeronaves[(aeronaves["disponivel_sede"]) & (~aeronaves["em_pane"]) & (~aeronaves["em_missao_rota"])]["aeronave"].tolist()
|
||||||
|
colunas = []
|
||||||
|
for rota_id, rota in rotas.iterrows():
|
||||||
|
for av in livres:
|
||||||
|
horas_disp = horas_disponiveis(aeronaves, av)
|
||||||
|
horas_rota = parse_horas(rota.get("tev_horas"))
|
||||||
|
if horas_rota is None and pd.notna(rota.get("duracao_horas")):
|
||||||
|
horas_rota = float(rota.get("duracao_horas"))
|
||||||
|
if horas_rota is not None and horas_rota > horas_disp:
|
||||||
|
continue
|
||||||
|
candidatos = candidatos_missao(data_planejamento, av, "ROTA", cadastro, indisponibilidades, progresso, catalogo, disponiveis)
|
||||||
|
if len(candidatos) < 2:
|
||||||
|
candidatos = candidatos_missao(data_planejamento, av, "TODAS", cadastro, indisponibilidades, progresso, catalogo, disponiveis)
|
||||||
|
candidatos = filtrar_por_horas_aeronave(candidatos, horas_disp)
|
||||||
|
if candidatos.empty:
|
||||||
|
continue
|
||||||
|
colunas.append(gerar_duplas(aplicar_prioridades(candidatos), criterio_missao, "ROTA_ACIONADA", av, data_planejamento, cadastro, indisponibilidades, progresso, catalogo, disponiveis, rota_id=rota_id + 1, rota=rota, observacao="Missao de rota acionada"))
|
||||||
|
for av in livres:
|
||||||
|
horas_disp = horas_disponiveis(aeronaves, av)
|
||||||
|
candidatos_local = candidatos_missao(data_planejamento, av, "LOCAL", cadastro, indisponibilidades, progresso, catalogo, disponiveis)
|
||||||
|
candidatos_local = filtrar_por_horas_aeronave(candidatos_local, horas_disp)
|
||||||
|
if not candidatos_local.empty:
|
||||||
|
colunas.append(gerar_duplas(aplicar_prioridades(candidatos_local), criterio_missao, "MISSAO_LOCAL", av, data_planejamento, cadastro, indisponibilidades, progresso, catalogo, disponiveis, observacao="Missao local planejada para progressao operacional"))
|
||||||
|
candidatos_sbv = []
|
||||||
|
for _, row in cadastro[cadastro[f"qt_{av.lower()}"]].iterrows():
|
||||||
|
if row["tripulante"] in set(disponiveis["tripulante"]) and piloto_disponivel(row["tripulante"], data_planejamento, indisponibilidades):
|
||||||
|
if qualificacao_operacional(row["tripulante"], av, cadastro, progresso, catalogo) != "AL":
|
||||||
|
r = row.copy()
|
||||||
|
r["aeronave"] = av
|
||||||
|
r["subprograma"] = ""
|
||||||
|
r["oi"] = ""
|
||||||
|
r["prioridade_paop"] = max(1, 999 - float(r.get("horas_totais_2026", 0) - r.get("horas_realizadas_2026", 0)))
|
||||||
|
r["bloqueado"] = False
|
||||||
|
candidatos_sbv.append(r)
|
||||||
|
if len(candidatos_sbv) >= 2:
|
||||||
|
colunas.append(gerar_duplas(pd.DataFrame(candidatos_sbv), criterio_sbv, "SBV", av, data_planejamento, cadastro, indisponibilidades, progresso, catalogo, disponiveis, observacao="Sobreaviso para missao de rota"))
|
||||||
|
if not colunas:
|
||||||
|
return pd.DataFrame()
|
||||||
|
df = pd.concat(colunas, ignore_index=True)
|
||||||
|
bonus = {"ROTA_ACIONADA": 100000, "MISSAO_LOCAL": 1000, "SBV": 0}
|
||||||
|
df["coluna_id"] = range(1, len(df) + 1)
|
||||||
|
df["score_milp"] = df["score"] + df["tipo_escala"].map(bonus).fillna(0)
|
||||||
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
def horas_disponiveis(aeronaves: pd.DataFrame, aeronave: str) -> float:
|
||||||
|
linha = aeronaves[aeronaves["aeronave"] == aeronave]
|
||||||
|
if linha.empty:
|
||||||
|
return float("inf")
|
||||||
|
horas = parse_horas(linha.iloc[0].get("hora_livre"))
|
||||||
|
return float("inf") if horas is None else horas
|
||||||
|
|
||||||
|
|
||||||
|
def filtrar_por_horas_aeronave(candidatos: pd.DataFrame, horas_disp: float) -> pd.DataFrame:
|
||||||
|
if candidatos.empty or horas_disp == float("inf") or "tev_horas" not in candidatos.columns:
|
||||||
|
return candidatos
|
||||||
|
horas_oi = candidatos["tev_horas"].map(parse_horas)
|
||||||
|
return candidatos[(horas_oi.isna()) | (horas_oi <= horas_disp)]
|
||||||
155
src/planejador_missao/data_io.py
Normal file
155
src/planejador_missao/data_io.py
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
"""Leitura das bases de entrada e escrita do historico operacional."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import date, datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from .utils import clean_columns, normalizar_texto, parse_bool, parse_data, parse_data_hora, read_csv
|
||||||
|
|
||||||
|
|
||||||
|
AERONAVES = ["C98", "C97", "C95"]
|
||||||
|
|
||||||
|
|
||||||
|
def carregar_parametros(base_dir: Path) -> dict:
|
||||||
|
default = {
|
||||||
|
"data_planejamento": "hoje",
|
||||||
|
"criterio_missao": "meta_50",
|
||||||
|
"criterio_sbv": "equalizar_quadrinhos",
|
||||||
|
}
|
||||||
|
path = base_dir / "dados" / "parametros_missao.csv"
|
||||||
|
if path.exists():
|
||||||
|
df = pd.read_csv(path)
|
||||||
|
if {"parametro", "valor"}.issubset(df.columns):
|
||||||
|
for _, row in df.iterrows():
|
||||||
|
chave = str(row["parametro"]).strip().lower()
|
||||||
|
if chave in default:
|
||||||
|
default[chave] = row["valor"]
|
||||||
|
default["data_planejamento"] = parse_data(default["data_planejamento"])
|
||||||
|
if default["criterio_missao"] == "paop":
|
||||||
|
default["criterio_missao"] = "meta_50"
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def carregar_cadastro(base_dir: Path, historico: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
local = base_dir / "dados" / "Modelagem_C98_ETA2_local.xlsx"
|
||||||
|
original = base_dir / "dados" / "Modelagem C98 ETA2.xlsx"
|
||||||
|
arquivo = local if local.exists() else original
|
||||||
|
df = clean_columns(pd.read_excel(arquivo, sheet_name="BANCO DE DADOS 2026"))
|
||||||
|
|
||||||
|
resumo = pd.DataFrame(columns=["tripulante", "horas_historico", "sbv_historico"])
|
||||||
|
if not historico.empty:
|
||||||
|
resumo = (
|
||||||
|
historico.groupby("tripulante", as_index=False)
|
||||||
|
.agg(horas_historico=("horas_voadas", "sum"), sbv_historico=("sbv", "sum"))
|
||||||
|
)
|
||||||
|
|
||||||
|
cadastro = df.rename(
|
||||||
|
columns={
|
||||||
|
"subprograma_c98": "subprograma_c98",
|
||||||
|
"subprograma_c97": "subprograma_c97",
|
||||||
|
"subprograma_c95": "subprograma_c95",
|
||||||
|
}
|
||||||
|
).copy()
|
||||||
|
cadastro["tripulante"] = cadastro["tripulante"].map(normalizar_texto)
|
||||||
|
cadastro["qt"] = cadastro["qt"].map(normalizar_texto)
|
||||||
|
cadastro["qualificacao"] = cadastro["qualificacao"].map(normalizar_texto)
|
||||||
|
for av in AERONAVES:
|
||||||
|
cadastro[f"qt_{av.lower()}"] = cadastro["qt"].str.contains(av, na=False)
|
||||||
|
cadastro[f"subprograma_{av.lower()}"] = cadastro[f"subprograma_{av.lower()}"].map(normalizar_texto)
|
||||||
|
cadastro[f"horas_{av.lower()}"] = pd.to_numeric(cadastro[f"horas_{av.lower()}"], errors="coerce").fillna(0)
|
||||||
|
cadastro["soldo"] = pd.to_numeric(cadastro["soldo"], errors="coerce").fillna(0)
|
||||||
|
cadastro["horas_totais_2026"] = pd.to_numeric(cadastro["horas_totais_2026"], errors="coerce").fillna(0)
|
||||||
|
cadastro = cadastro.merge(resumo, how="left", on="tripulante")
|
||||||
|
cadastro["horas_realizadas_2026"] = cadastro["horas_historico"].fillna(0)
|
||||||
|
cadastro["sbv_realizados_2026"] = cadastro["sbv_historico"].fillna(0)
|
||||||
|
cadastro["disponivel"] = True
|
||||||
|
return cadastro.drop(columns=["horas_historico", "sbv_historico"], errors="ignore")
|
||||||
|
|
||||||
|
|
||||||
|
def carregar_catalogo_ois(base_dir: Path) -> pd.DataFrame:
|
||||||
|
df = pd.read_excel(base_dir / "dados" / "catalogo_ois.xlsx", sheet_name="catalogo_ois", dtype=str)
|
||||||
|
df = df.dropna(subset=["aeronave", "subprograma", "oi"]).copy()
|
||||||
|
for col in ["aeronave", "subprograma", "oi", "esforco", "tipo_missao"]:
|
||||||
|
df[col] = df[col].map(normalizar_texto)
|
||||||
|
df["ordem"] = pd.to_numeric(df["ordem"], errors="coerce")
|
||||||
|
df["eh_piloto"] = df.get("eh_piloto", "VERDADEIRO").fillna("VERDADEIRO").map(parse_bool)
|
||||||
|
df["conta_hora_voo"] = df["tipo_missao"].isin(["LOCAL", "ROTA"])
|
||||||
|
return df[df["eh_piloto"]].sort_values(["aeronave", "subprograma", "ordem"]).reset_index(drop=True)
|
||||||
|
|
||||||
|
|
||||||
|
def carregar_indisponibilidades(base_dir: Path) -> pd.DataFrame:
|
||||||
|
path = base_dir / "dados" / "indisponibilidades_2026.xlsx"
|
||||||
|
df = pd.read_excel(path)
|
||||||
|
df["tripulante"] = df["tripulante"].map(normalizar_texto)
|
||||||
|
df["inicio"] = pd.to_datetime(df["inicio"]).dt.date
|
||||||
|
df["fim"] = pd.to_datetime(df["fim"]).dt.date
|
||||||
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
def carregar_historico(base_dir: Path) -> pd.DataFrame:
|
||||||
|
cols = ["data", "aeronave", "tipo_escala", "tripulante", "funcao", "oi", "horas_voadas", "sbv", "origem_registro", "registrado_em"]
|
||||||
|
df = read_csv(base_dir / "dados" / "historico_horas_voadas.csv", cols)
|
||||||
|
if df.empty:
|
||||||
|
return df
|
||||||
|
df["data"] = pd.to_datetime(df["data"], errors="coerce").dt.date
|
||||||
|
for col in ["aeronave", "tipo_escala", "tripulante", "funcao", "oi"]:
|
||||||
|
df[col] = df[col].map(normalizar_texto)
|
||||||
|
df["horas_voadas"] = pd.to_numeric(df["horas_voadas"], errors="coerce").fillna(0)
|
||||||
|
df["sbv"] = pd.to_numeric(df["sbv"], errors="coerce").fillna(0)
|
||||||
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
def carregar_progresso_ois(base_dir: Path) -> pd.DataFrame:
|
||||||
|
path = base_dir / "dados" / "progresso_ois_2026.xlsx"
|
||||||
|
cols = ["tripulante", "aeronave", "subprograma", "oi", "horas_realizadas", "concluida"]
|
||||||
|
if not path.exists():
|
||||||
|
return pd.DataFrame(columns=cols)
|
||||||
|
df = pd.read_excel(path)
|
||||||
|
for col in ["tripulante", "aeronave", "subprograma", "oi"]:
|
||||||
|
df[col] = df[col].map(normalizar_texto)
|
||||||
|
df["horas_realizadas"] = pd.to_numeric(df["horas_realizadas"], errors="coerce").fillna(0)
|
||||||
|
df["concluida"] = df["concluida"].map(parse_bool)
|
||||||
|
return df[cols]
|
||||||
|
|
||||||
|
|
||||||
|
def carregar_aeronaves(base_dir: Path) -> pd.DataFrame:
|
||||||
|
cols = ["aeronave", "disponivel_sede", "em_pane", "em_missao_rota", "hora_livre"]
|
||||||
|
df = read_csv(base_dir / "dados" / "aeronaves_disponiveis.csv", cols)
|
||||||
|
if df.empty:
|
||||||
|
df = pd.DataFrame({"aeronave": AERONAVES, "disponivel_sede": True, "em_pane": False, "em_missao_rota": False, "hora_livre": "08:00"})
|
||||||
|
df["aeronave"] = df["aeronave"].map(normalizar_texto)
|
||||||
|
for col in ["disponivel_sede", "em_pane", "em_missao_rota"]:
|
||||||
|
df[col] = df[col].map(parse_bool)
|
||||||
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
def carregar_tripulantes_disponiveis(base_dir: Path) -> pd.DataFrame:
|
||||||
|
df = read_csv(base_dir / "dados" / "tripulantes_disponiveis.csv", ["tripulante", "disponivel"])
|
||||||
|
df["tripulante"] = df["tripulante"].map(normalizar_texto)
|
||||||
|
df["disponivel"] = df["disponivel"].map(parse_bool)
|
||||||
|
return df[df["disponivel"]].reset_index(drop=True)
|
||||||
|
|
||||||
|
|
||||||
|
def carregar_rotas(base_dir: Path) -> pd.DataFrame:
|
||||||
|
cols = ["origem", "destino", "inicio", "fim", "tev_horas", "pernoite_dias", "rota"]
|
||||||
|
df = read_csv(base_dir / "dados" / "rotas_acionadas.csv", cols)
|
||||||
|
if df.empty:
|
||||||
|
return df
|
||||||
|
for col in ["origem", "destino", "rota"]:
|
||||||
|
df[col] = df[col].map(normalizar_texto)
|
||||||
|
df["inicio"] = df["inicio"].map(parse_data_hora)
|
||||||
|
df["fim"] = df["fim"].map(parse_data_hora)
|
||||||
|
df["tev_horas"] = pd.to_numeric(df["tev_horas"], errors="coerce")
|
||||||
|
df["pernoite_dias"] = pd.to_numeric(df["pernoite_dias"], errors="coerce").fillna((df["fim"] - df["inicio"]).dt.days).fillna(0)
|
||||||
|
df["duracao_horas"] = (df["fim"] - df["inicio"]).dt.total_seconds() / 3600
|
||||||
|
return df.dropna(subset=["origem", "destino"], how="all").reset_index(drop=True)
|
||||||
|
|
||||||
|
|
||||||
|
def salvar_historico(base_dir: Path, registros: pd.DataFrame) -> None:
|
||||||
|
path = base_dir / "dados" / "historico_horas_voadas.csv"
|
||||||
|
historico = carregar_historico(base_dir)
|
||||||
|
atualizado = pd.concat([historico, registros], ignore_index=True)
|
||||||
|
atualizado.to_csv(path, index=False)
|
||||||
64
src/planejador_missao/main.py
Normal file
64
src/planejador_missao/main.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
"""Fluxo principal do planejador em Python."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from .candidates import gerar_colunas
|
||||||
|
from .data_io import (
|
||||||
|
carregar_aeronaves,
|
||||||
|
carregar_cadastro,
|
||||||
|
carregar_catalogo_ois,
|
||||||
|
carregar_historico,
|
||||||
|
carregar_indisponibilidades,
|
||||||
|
carregar_parametros,
|
||||||
|
carregar_progresso_ois,
|
||||||
|
carregar_rotas,
|
||||||
|
carregar_tripulantes_disponiveis,
|
||||||
|
salvar_historico,
|
||||||
|
)
|
||||||
|
from .optimizer import resolver_milp
|
||||||
|
from .report import gerar_excel, gerar_registros
|
||||||
|
from .rules import combinar_progresso_com_historico
|
||||||
|
|
||||||
|
|
||||||
|
def executar_planejamento(base_dir: Path) -> dict:
|
||||||
|
# Bloco 01 - Entrada e estado operacional.
|
||||||
|
parametros = carregar_parametros(base_dir)
|
||||||
|
historico = carregar_historico(base_dir)
|
||||||
|
cadastro = carregar_cadastro(base_dir, historico)
|
||||||
|
catalogo = carregar_catalogo_ois(base_dir)
|
||||||
|
indisponibilidades = carregar_indisponibilidades(base_dir)
|
||||||
|
progresso = combinar_progresso_com_historico(carregar_progresso_ois(base_dir), historico, catalogo)
|
||||||
|
aeronaves = carregar_aeronaves(base_dir)
|
||||||
|
disponiveis = carregar_tripulantes_disponiveis(base_dir)
|
||||||
|
rotas = carregar_rotas(base_dir)
|
||||||
|
|
||||||
|
# Bloco 02 - Geracao das colunas candidatas.
|
||||||
|
colunas = gerar_colunas(
|
||||||
|
parametros["data_planejamento"],
|
||||||
|
rotas,
|
||||||
|
aeronaves,
|
||||||
|
cadastro,
|
||||||
|
indisponibilidades,
|
||||||
|
progresso,
|
||||||
|
catalogo,
|
||||||
|
disponiveis,
|
||||||
|
parametros["criterio_missao"],
|
||||||
|
parametros["criterio_sbv"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Bloco 03 - Modelo MILP e selecao final.
|
||||||
|
solucao = resolver_milp(colunas, rotas, aeronaves)
|
||||||
|
|
||||||
|
# Bloco 04 - Persistencia operacional e relatorio.
|
||||||
|
registros = gerar_registros(solucao, catalogo)
|
||||||
|
salvar_historico(base_dir, registros)
|
||||||
|
arquivo_saida = gerar_excel(base_dir, parametros["data_planejamento"], solucao)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"data_planejamento": parametros["data_planejamento"],
|
||||||
|
"arquivo_saida": arquivo_saida,
|
||||||
|
"total_candidatas": len(colunas),
|
||||||
|
"total_selecionadas": len(solucao),
|
||||||
|
}
|
||||||
60
src/planejador_missao/optimizer.py
Normal file
60
src/planejador_missao/optimizer.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
"""Modelo MILP para escolher a combinacao final de escalas."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
from scipy.optimize import Bounds, LinearConstraint, milp
|
||||||
|
|
||||||
|
|
||||||
|
def resolver_milp(colunas: pd.DataFrame, rotas: pd.DataFrame, aeronaves: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
if colunas.empty:
|
||||||
|
return colunas
|
||||||
|
|
||||||
|
n = len(colunas)
|
||||||
|
integrality = np.ones(n)
|
||||||
|
bounds = Bounds(np.zeros(n), np.ones(n))
|
||||||
|
restricoes = []
|
||||||
|
|
||||||
|
# R1 - Cada tripulante aparece no maximo uma vez.
|
||||||
|
for trip in sorted(set(colunas["trip1"]).union(set(colunas["trip2"]))):
|
||||||
|
linha = np.zeros(n)
|
||||||
|
linha[(colunas["trip1"] == trip) | (colunas["trip2"] == trip)] = 1
|
||||||
|
restricoes.append(LinearConstraint(linha, -np.inf, 1))
|
||||||
|
|
||||||
|
livres = aeronaves[(aeronaves["disponivel_sede"]) & (~aeronaves["em_pane"]) & (~aeronaves["em_missao_rota"])]["aeronave"].unique()
|
||||||
|
for av in livres:
|
||||||
|
idx_rota = (colunas["aeronave"] == av) & (colunas["tipo_escala"] == "ROTA_ACIONADA")
|
||||||
|
idx_local = (colunas["aeronave"] == av) & (colunas["tipo_escala"] == "MISSAO_LOCAL")
|
||||||
|
idx_sbv = (colunas["aeronave"] == av) & (colunas["tipo_escala"] == "SBV")
|
||||||
|
if not (idx_rota | idx_sbv).any():
|
||||||
|
raise RuntimeError(f"Modelo inviavel: nao ha rota nem SBV candidato para a aeronave {av}.")
|
||||||
|
linha = np.zeros(n)
|
||||||
|
linha[idx_rota | idx_sbv] = 1
|
||||||
|
restricoes.append(LinearConstraint(linha, 1, 1))
|
||||||
|
if idx_rota.any():
|
||||||
|
linha = np.zeros(n)
|
||||||
|
linha[idx_rota] = 1
|
||||||
|
restricoes.append(LinearConstraint(linha, -np.inf, 1))
|
||||||
|
if idx_local.any():
|
||||||
|
linha = np.zeros(n)
|
||||||
|
linha[idx_local] = 1
|
||||||
|
restricoes.append(LinearConstraint(linha, -np.inf, 1))
|
||||||
|
if idx_rota.any() and idx_local.any():
|
||||||
|
linha = np.zeros(n)
|
||||||
|
linha[idx_rota | idx_local] = 1
|
||||||
|
restricoes.append(LinearConstraint(linha, -np.inf, 1))
|
||||||
|
|
||||||
|
for rota_id in range(1, len(rotas) + 1):
|
||||||
|
idx = (colunas["tipo_escala"] == "ROTA_ACIONADA") & (colunas["rota_id"] == rota_id)
|
||||||
|
if not idx.any():
|
||||||
|
raise RuntimeError(f"Modelo inviavel: nao ha coluna candidata para a rota acionada {rota_id}.")
|
||||||
|
linha = np.zeros(n)
|
||||||
|
linha[idx] = 1
|
||||||
|
restricoes.append(LinearConstraint(linha, 1, 1))
|
||||||
|
|
||||||
|
resultado = milp(c=-colunas["score_milp"].to_numpy(dtype=float), integrality=integrality, bounds=bounds, constraints=restricoes)
|
||||||
|
if not resultado.success:
|
||||||
|
raise RuntimeError(f"MILP nao encontrou solucao viavel: {resultado.message}")
|
||||||
|
selecionadas = resultado.x > 0.5
|
||||||
|
return colunas.loc[selecionadas].reset_index(drop=True)
|
||||||
100
src/planejador_missao/quadrinhos.py
Normal file
100
src/planejador_missao/quadrinhos.py
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
"""Resumo operacional por tripulante para a aba de quadrinhos."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from .candidates import proxima_oi
|
||||||
|
from .data_io import AERONAVES, carregar_cadastro, carregar_catalogo_ois, carregar_historico, carregar_progresso_ois
|
||||||
|
from .rules import combinar_progresso_com_historico, qualificacao_operacional
|
||||||
|
from .utils import normalizar_texto
|
||||||
|
|
||||||
|
|
||||||
|
def carregar_quadrinho_manual(base_dir: Path) -> pd.DataFrame:
|
||||||
|
path = base_dir / "dados" / "quadrinho_operacional_manual.csv"
|
||||||
|
cols = [
|
||||||
|
"tripulante",
|
||||||
|
"aeronave",
|
||||||
|
"qualificacao_manual",
|
||||||
|
"horas_voadas_manual",
|
||||||
|
"sbv_manual",
|
||||||
|
"ultima_data_voo_manual",
|
||||||
|
"observacao",
|
||||||
|
]
|
||||||
|
if not path.exists():
|
||||||
|
return pd.DataFrame(columns=cols)
|
||||||
|
df = pd.read_csv(path)
|
||||||
|
for col in cols:
|
||||||
|
if col not in df.columns:
|
||||||
|
df[col] = pd.NA
|
||||||
|
df = df[cols].copy()
|
||||||
|
df["tripulante"] = df["tripulante"].map(normalizar_texto)
|
||||||
|
df["aeronave"] = df["aeronave"].map(normalizar_texto)
|
||||||
|
df["horas_voadas_manual"] = pd.to_numeric(df["horas_voadas_manual"], errors="coerce").fillna(0)
|
||||||
|
df["sbv_manual"] = pd.to_numeric(df["sbv_manual"], errors="coerce").fillna(0)
|
||||||
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
def gerar_quadrinhos(base_dir: Path) -> pd.DataFrame:
|
||||||
|
historico = carregar_historico(base_dir)
|
||||||
|
cadastro = carregar_cadastro(base_dir, historico)
|
||||||
|
catalogo = carregar_catalogo_ois(base_dir)
|
||||||
|
progresso = combinar_progresso_com_historico(carregar_progresso_ois(base_dir), historico, catalogo)
|
||||||
|
manual = carregar_quadrinho_manual(base_dir)
|
||||||
|
|
||||||
|
linhas = []
|
||||||
|
for _, trip in cadastro.sort_values("tripulante").iterrows():
|
||||||
|
trigrama = trip["tripulante"]
|
||||||
|
sbv_hist = float(trip.get("sbv_realizados_2026", 0) or 0)
|
||||||
|
horas_hist = float(trip.get("horas_realizadas_2026", 0) or 0)
|
||||||
|
for av in AERONAVES:
|
||||||
|
if not bool(trip.get(f"qt_{av.lower()}", False)):
|
||||||
|
continue
|
||||||
|
manual_row = manual[(manual["tripulante"] == trigrama) & (manual["aeronave"] == av)]
|
||||||
|
horas_manual = 0.0 if manual_row.empty else float(manual_row.iloc[0].get("horas_voadas_manual", 0) or 0)
|
||||||
|
sbv_manual = 0.0 if manual_row.empty else float(manual_row.iloc[0].get("sbv_manual", 0) or 0)
|
||||||
|
qual_manual = "" if manual_row.empty else normalizar_texto(manual_row.iloc[0].get("qualificacao_manual", ""))
|
||||||
|
obs = "" if manual_row.empty else str(manual_row.iloc[0].get("observacao", "") or "")
|
||||||
|
ultima_manual = "" if manual_row.empty else str(manual_row.iloc[0].get("ultima_data_voo_manual", "") or "")
|
||||||
|
|
||||||
|
proximas = []
|
||||||
|
subprogramas = [sp for sp in str(trip.get(f"subprograma_{av.lower()}", "")).split() if sp.startswith("SP")]
|
||||||
|
for sp in subprogramas:
|
||||||
|
oi = proxima_oi(trigrama, av, sp, progresso, catalogo)
|
||||||
|
if oi:
|
||||||
|
proximas.append(f"{sp}: {oi['oi']}")
|
||||||
|
else:
|
||||||
|
proximas.append(f"{sp}: concluido")
|
||||||
|
|
||||||
|
voos_trip = historico[(historico["tripulante"] == trigrama) & (historico["aeronave"] == av) & (historico["horas_voadas"] > 0)]
|
||||||
|
ultima_hist = "" if voos_trip.empty else max(voos_trip["data"]).isoformat()
|
||||||
|
|
||||||
|
linhas.append(
|
||||||
|
{
|
||||||
|
"tripulante": trigrama,
|
||||||
|
"aeronave": av,
|
||||||
|
"qualificacao": qual_manual or qualificacao_operacional(trigrama, av, cadastro, progresso, catalogo),
|
||||||
|
"proxima_oi": " | ".join(proximas) if proximas else "-",
|
||||||
|
"horas_aeronave_base": float(trip.get(f"horas_{av.lower()}", 0) or 0),
|
||||||
|
"horas_voadas_ano": horas_hist + horas_manual,
|
||||||
|
"sbv_ano": sbv_hist + sbv_manual,
|
||||||
|
"ultima_data_voo": ultima_manual or ultima_hist,
|
||||||
|
"meta_50_faltante": max(0.0, 50.0 - (horas_hist + horas_manual)),
|
||||||
|
"observacao": obs,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return pd.DataFrame(linhas)
|
||||||
|
|
||||||
|
|
||||||
|
def quadrinhos_para_api(base_dir: Path) -> dict:
|
||||||
|
df = gerar_quadrinhos(base_dir)
|
||||||
|
if df.empty:
|
||||||
|
return {"linhas": [], "resumo": {"tripulantes": 0, "abaixo_50": 0, "horas_media": 0}}
|
||||||
|
resumo = {
|
||||||
|
"tripulantes": int(df["tripulante"].nunique()),
|
||||||
|
"abaixo_50": int((df["horas_voadas_ano"] < 50).sum()),
|
||||||
|
"horas_media": round(float(df["horas_voadas_ano"].mean()), 1),
|
||||||
|
}
|
||||||
|
return {"linhas": df.fillna("").to_dict(orient="records"), "resumo": resumo}
|
||||||
117
src/planejador_missao/report.py
Normal file
117
src/planejador_missao/report.py
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
"""Registro de horas e geracao do relatorio Excel final."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
from openpyxl import Workbook
|
||||||
|
from openpyxl.styles import Alignment, Font, PatternFill, Border, Side
|
||||||
|
from openpyxl.utils.dataframe import dataframe_to_rows
|
||||||
|
|
||||||
|
from .utils import parse_horas
|
||||||
|
|
||||||
|
|
||||||
|
def horas_previstas(linha: pd.Series, catalogo: pd.DataFrame) -> float:
|
||||||
|
if linha["tipo_escala"] == "SBV":
|
||||||
|
return 0.0
|
||||||
|
horas = parse_horas(linha.get("tev_horas"))
|
||||||
|
if horas:
|
||||||
|
return horas
|
||||||
|
oi = linha.get("oi_trip1", "")
|
||||||
|
match = catalogo[(catalogo["aeronave"] == linha["aeronave"]) & (catalogo["oi"] == oi)]
|
||||||
|
if not match.empty and bool(match.iloc[0]["conta_hora_voo"]):
|
||||||
|
return parse_horas(match.iloc[0]["tev_horas"]) or 0.0
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def gerar_registros(solucao: pd.DataFrame, catalogo: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
linhas = []
|
||||||
|
agora = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
for _, row in solucao.iterrows():
|
||||||
|
horas = horas_previstas(row, catalogo)
|
||||||
|
sbv = 1 if row["tipo_escala"] == "SBV" else 0
|
||||||
|
for funcao, trip, oi in [("TRIP1", row["trip1"], row.get("oi_trip1", "")), ("TRIP2", row["trip2"], row.get("oi_trip2", ""))]:
|
||||||
|
linhas.append(
|
||||||
|
{
|
||||||
|
"data": row["data"],
|
||||||
|
"aeronave": row["aeronave"],
|
||||||
|
"tipo_escala": row["tipo_escala"],
|
||||||
|
"tripulante": trip,
|
||||||
|
"funcao": funcao,
|
||||||
|
"oi": oi if isinstance(oi, str) else "",
|
||||||
|
"horas_voadas": horas,
|
||||||
|
"sbv": sbv,
|
||||||
|
"origem_registro": "escala_diaria",
|
||||||
|
"registrado_em": agora,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return pd.DataFrame(linhas)
|
||||||
|
|
||||||
|
|
||||||
|
def bloco(solucao: pd.DataFrame, tipo: str) -> pd.DataFrame:
|
||||||
|
dados = solucao[solucao["tipo_escala"] == tipo]
|
||||||
|
if dados.empty:
|
||||||
|
return pd.DataFrame([{"AERONAVE": "-", "TRIP 1": "-", "TRIP 2": "-", "OI/FICHA": "-", "DETALHE": "Sem escala"}])
|
||||||
|
detalhe = dados["observacao"]
|
||||||
|
if tipo == "ROTA_ACIONADA":
|
||||||
|
detalhe = dados["rota"].where(dados["rota"].astype(str) != "", dados["origem"] + " - " + dados["destino"])
|
||||||
|
return pd.DataFrame(
|
||||||
|
{
|
||||||
|
"AERONAVE": dados["aeronave"],
|
||||||
|
"TRIP 1": dados["trip1"],
|
||||||
|
"TRIP 2": dados["trip2"],
|
||||||
|
"OI/FICHA": dados["oi_trip1"].replace("", "-"),
|
||||||
|
"DETALHE": detalhe,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def escrever_tabela(ws, titulo: str, dados: pd.DataFrame, row: int, col: int, cor: str) -> None:
|
||||||
|
fill_titulo = PatternFill("solid", fgColor=cor)
|
||||||
|
fill_header = PatternFill("solid", fgColor="D9EAF7")
|
||||||
|
borda = Border(*(Side(style="thin", color="BFBFBF") for _ in range(4)))
|
||||||
|
ws.merge_cells(start_row=row, start_column=col, end_row=row, end_column=col + 4)
|
||||||
|
cell = ws.cell(row=row, column=col, value=titulo)
|
||||||
|
cell.fill = fill_titulo
|
||||||
|
cell.font = Font(color="FFFFFF", bold=True)
|
||||||
|
cell.alignment = Alignment(horizontal="center")
|
||||||
|
for r_idx, linha in enumerate(dataframe_to_rows(dados, index=False, header=True), row + 1):
|
||||||
|
for c_idx, valor in enumerate(linha, col):
|
||||||
|
c = ws.cell(row=r_idx, column=c_idx, value=valor)
|
||||||
|
c.border = borda
|
||||||
|
c.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
|
||||||
|
if r_idx == row + 1:
|
||||||
|
c.fill = fill_header
|
||||||
|
c.font = Font(bold=True)
|
||||||
|
|
||||||
|
|
||||||
|
def gerar_excel(base_dir: Path, data_planejamento, solucao: pd.DataFrame) -> Path:
|
||||||
|
out_dir = base_dir / "resultados"
|
||||||
|
out_dir.mkdir(exist_ok=True)
|
||||||
|
arquivo = out_dir / f"planejamento_diario_{data_planejamento}.xlsx"
|
||||||
|
if arquivo.exists():
|
||||||
|
arquivo = out_dir / f"planejamento_diario_{data_planejamento}_{datetime.now():%H%M%S}.xlsx"
|
||||||
|
|
||||||
|
wb = Workbook()
|
||||||
|
ws = wb.active
|
||||||
|
ws.title = "ESCALA DIARIA"
|
||||||
|
ws.merge_cells("A1:Q1")
|
||||||
|
ws["A1"] = "Planejador diario de missoes e sobreaviso - 2 ETA"
|
||||||
|
ws["A1"].fill = PatternFill("solid", fgColor="1F4E78")
|
||||||
|
ws["A1"].font = Font(color="FFFFFF", bold=True, size=16)
|
||||||
|
ws["A1"].alignment = Alignment(horizontal="center")
|
||||||
|
ws.merge_cells("A2:Q2")
|
||||||
|
ws["A2"] = f"ESCALA DIARIA - {data_planejamento:%d/%m/%Y}"
|
||||||
|
ws["A2"].font = Font(color="1F4E78", bold=True, size=18)
|
||||||
|
ws["A2"].alignment = Alignment(horizontal="center")
|
||||||
|
|
||||||
|
escrever_tabela(ws, "MISSAO ACIONADA", bloco(solucao, "ROTA_ACIONADA"), 5, 1, "C00000")
|
||||||
|
escrever_tabela(ws, "VOOS LOCAIS", bloco(solucao, "MISSAO_LOCAL"), 5, 7, "548235")
|
||||||
|
escrever_tabela(ws, "SOBREAVISO", bloco(solucao, "SBV"), 5, 13, "8064A2")
|
||||||
|
for col in range(1, 18):
|
||||||
|
ws.column_dimensions[chr(64 + col)].width = 14 if col not in {5, 11, 17} else 24
|
||||||
|
ws.freeze_panes = "A5"
|
||||||
|
wb.save(arquivo)
|
||||||
|
return arquivo
|
||||||
108
src/planejador_missao/rules.py
Normal file
108
src/planejador_missao/rules.py
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
"""Regras operacionais de disponibilidade, qualificacao e progressao."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from .utils import normalizar_texto
|
||||||
|
|
||||||
|
|
||||||
|
def piloto_disponivel(tripulante: str, data_missao: date, indisponibilidades: pd.DataFrame) -> bool:
|
||||||
|
trip = normalizar_texto(tripulante)
|
||||||
|
conflitos = indisponibilidades[
|
||||||
|
(indisponibilidades["tripulante"] == trip)
|
||||||
|
& (indisponibilidades["inicio"] <= data_missao)
|
||||||
|
& (indisponibilidades["fim"] >= data_missao)
|
||||||
|
]
|
||||||
|
return conflitos.empty
|
||||||
|
|
||||||
|
|
||||||
|
def classe_principal(qualificacao: str) -> str:
|
||||||
|
qual = normalizar_texto(qualificacao)
|
||||||
|
for classe in ["IN", "PO", "PB", "AL"]:
|
||||||
|
if classe in qual.split("/"):
|
||||||
|
return classe
|
||||||
|
if classe in qual.split():
|
||||||
|
return classe
|
||||||
|
return "OUTRO"
|
||||||
|
|
||||||
|
|
||||||
|
def qualificacao_base_aeronave(tripulante: str, aeronave: str, cadastro: pd.DataFrame) -> str:
|
||||||
|
linha = cadastro[cadastro["tripulante"] == normalizar_texto(tripulante)]
|
||||||
|
if linha.empty:
|
||||||
|
return "OUTRO"
|
||||||
|
qt = [x for x in normalizar_texto(linha.iloc[0]["qt"]).split("/") if x]
|
||||||
|
quals = [x for x in normalizar_texto(linha.iloc[0]["qualificacao"]).split("/") if x]
|
||||||
|
if len(qt) == len(quals) and aeronave in qt:
|
||||||
|
return classe_principal(quals[qt.index(aeronave)])
|
||||||
|
return classe_principal(linha.iloc[0]["qualificacao"])
|
||||||
|
|
||||||
|
|
||||||
|
def subprograma_concluido(progresso: pd.DataFrame, tripulante: str, aeronave: str, subprograma: str, catalogo: pd.DataFrame) -> bool:
|
||||||
|
necessarias = set(catalogo[(catalogo["aeronave"] == aeronave) & (catalogo["subprograma"] == subprograma)]["oi"])
|
||||||
|
if not necessarias:
|
||||||
|
return False
|
||||||
|
concluidas = set(
|
||||||
|
progresso[
|
||||||
|
(progresso["tripulante"] == normalizar_texto(tripulante))
|
||||||
|
& (progresso["aeronave"] == aeronave)
|
||||||
|
& (progresso["subprograma"] == subprograma)
|
||||||
|
& (progresso["concluida"] == True)
|
||||||
|
]["oi"]
|
||||||
|
)
|
||||||
|
return necessarias.issubset(concluidas)
|
||||||
|
|
||||||
|
|
||||||
|
def qualificacao_operacional(tripulante: str, aeronave: str, cadastro: pd.DataFrame, progresso: pd.DataFrame, catalogo: pd.DataFrame) -> str:
|
||||||
|
rank = {"OUTRO": 0, "AL": 1, "PB": 2, "PO": 3, "IN": 4}[qualificacao_base_aeronave(tripulante, aeronave, cadastro)]
|
||||||
|
if subprograma_concluido(progresso, tripulante, aeronave, "SPFO-1", catalogo):
|
||||||
|
rank = max(rank, 2)
|
||||||
|
if subprograma_concluido(progresso, tripulante, aeronave, "SPFO-2", catalogo):
|
||||||
|
rank = max(rank, 3)
|
||||||
|
if aeronave == "C98" and subprograma_concluido(progresso, tripulante, aeronave, "SPQE-3", catalogo):
|
||||||
|
rank = max(rank, 4)
|
||||||
|
if aeronave == "C97" and subprograma_concluido(progresso, tripulante, aeronave, "SPQE-4", catalogo):
|
||||||
|
rank = max(rank, 4)
|
||||||
|
return {0: "OUTRO", 1: "AL", 2: "PB", 3: "PO", 4: "IN"}[rank]
|
||||||
|
|
||||||
|
|
||||||
|
def concluiu_ois(progresso: pd.DataFrame, tripulante: str, aeronave: str, ois: list[str]) -> bool:
|
||||||
|
concluidas = set(
|
||||||
|
progresso[
|
||||||
|
(progresso["tripulante"] == normalizar_texto(tripulante))
|
||||||
|
& (progresso["aeronave"] == aeronave)
|
||||||
|
& (progresso["concluida"] == True)
|
||||||
|
]["oi"]
|
||||||
|
)
|
||||||
|
return set(ois).issubset(concluidas)
|
||||||
|
|
||||||
|
|
||||||
|
def eh_instrutor_adaptado(tripulante: str, aeronave: str, cadastro: pd.DataFrame, progresso: pd.DataFrame, catalogo: pd.DataFrame) -> bool:
|
||||||
|
trip = normalizar_texto(tripulante)
|
||||||
|
instrutores_base = {
|
||||||
|
"C95": {"MES", "BRI"},
|
||||||
|
"C97": {"AEU", "MAT"},
|
||||||
|
"C98": {"BRI", "FIA", "MDO", "SLS", "CFF"},
|
||||||
|
}
|
||||||
|
if trip in instrutores_base.get(aeronave, set()):
|
||||||
|
return True
|
||||||
|
if aeronave == "C97" and trip in {"SEI", "LPS"}:
|
||||||
|
return concluiu_ois(progresso, trip, aeronave, ["01TL01D41", "04TL01N42"])
|
||||||
|
if aeronave == "C98" and trip in {"MCH", "DOG"}:
|
||||||
|
return concluiu_ois(progresso, trip, aeronave, ["01TL01D31", "04TL01N32"])
|
||||||
|
return qualificacao_operacional(trip, aeronave, cadastro, progresso, catalogo) == "IN"
|
||||||
|
|
||||||
|
|
||||||
|
def combinar_progresso_com_historico(progresso: pd.DataFrame, historico: pd.DataFrame, catalogo: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
if historico.empty:
|
||||||
|
return progresso
|
||||||
|
voadas = historico[(historico["oi"] != "") & (historico["horas_voadas"] > 0)]
|
||||||
|
if voadas.empty:
|
||||||
|
return progresso
|
||||||
|
hist = voadas.merge(catalogo[["aeronave", "subprograma", "oi"]].drop_duplicates(), on=["aeronave", "oi"], how="left")
|
||||||
|
hist = hist.dropna(subset=["subprograma"])
|
||||||
|
hist = hist[["tripulante", "aeronave", "subprograma", "oi", "horas_voadas"]].rename(columns={"horas_voadas": "horas_realizadas"})
|
||||||
|
hist["concluida"] = True
|
||||||
|
return pd.concat([progresso, hist], ignore_index=True).drop_duplicates(["tripulante", "aeronave", "subprograma", "oi"])
|
||||||
84
src/planejador_missao/utils.py
Normal file
84
src/planejador_missao/utils.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
"""Utilitarios pequenos de normalizacao usados em todo o planejador."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import math
|
||||||
|
import re
|
||||||
|
from datetime import date, datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
|
||||||
|
def normalizar_texto(valor: Any) -> str:
|
||||||
|
if valor is None or (isinstance(valor, float) and math.isnan(valor)):
|
||||||
|
return ""
|
||||||
|
return re.sub(r"\s+", " ", str(valor).strip()).upper()
|
||||||
|
|
||||||
|
|
||||||
|
def parse_bool(valor: Any) -> bool:
|
||||||
|
return normalizar_texto(valor) in {"TRUE", "T", "1", "SIM", "S", "YES", "Y", "VERDADEIRO"}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_data(valor: Any) -> date:
|
||||||
|
texto = normalizar_texto(valor).lower()
|
||||||
|
if texto in {"", "hoje", "today", "sysdate"}:
|
||||||
|
return date.today()
|
||||||
|
return pd.to_datetime(valor).date()
|
||||||
|
|
||||||
|
|
||||||
|
def parse_data_hora(valor: Any) -> pd.Timestamp:
|
||||||
|
if valor is None or str(valor).strip() == "":
|
||||||
|
return pd.NaT
|
||||||
|
return pd.to_datetime(valor, errors="coerce")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_horas(valor: Any) -> float | None:
|
||||||
|
if valor is None or (isinstance(valor, float) and math.isnan(valor)):
|
||||||
|
return None
|
||||||
|
if isinstance(valor, (int, float)):
|
||||||
|
return float(valor)
|
||||||
|
texto = str(valor).strip().replace(",", ".")
|
||||||
|
if texto == "":
|
||||||
|
return None
|
||||||
|
if ":" in texto:
|
||||||
|
partes = texto.split(":")
|
||||||
|
try:
|
||||||
|
return float(partes[0]) + float(partes[1]) / 60
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return float(texto)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def clean_columns(df: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
df = df.copy()
|
||||||
|
df.columns = [
|
||||||
|
normalizar_texto(c)
|
||||||
|
.lower()
|
||||||
|
.replace("º", "")
|
||||||
|
.replace("ç", "c")
|
||||||
|
.replace("ã", "a")
|
||||||
|
.replace("á", "a")
|
||||||
|
.replace("é", "e")
|
||||||
|
.replace("í", "i")
|
||||||
|
.replace("ó", "o")
|
||||||
|
.replace("ú", "u")
|
||||||
|
.replace(" ", "_")
|
||||||
|
.replace(".", "")
|
||||||
|
for c in df.columns
|
||||||
|
]
|
||||||
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
def read_csv(path: Path, columns: list[str]) -> pd.DataFrame:
|
||||||
|
if not path.exists():
|
||||||
|
return pd.DataFrame(columns=columns)
|
||||||
|
df = pd.read_csv(path)
|
||||||
|
for col in columns:
|
||||||
|
if col not in df.columns:
|
||||||
|
df[col] = pd.NA
|
||||||
|
return df[columns]
|
||||||
251
src/planejador_missao/validacao.py
Normal file
251
src/planejador_missao/validacao.py
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
"""Validacao retrospectiva usando voos historicos."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
from scipy.optimize import Bounds, LinearConstraint, milp
|
||||||
|
|
||||||
|
from .data_io import carregar_cadastro
|
||||||
|
from .utils import normalizar_texto
|
||||||
|
|
||||||
|
|
||||||
|
COLUNAS_VOOS_2025 = ["data", "aeronave", "tipo_escala", "tripulante", "funcao", "oi", "horas_voadas", "sbv"]
|
||||||
|
COLUNAS_OPCIONAIS_VOOS_2025 = ["slot_id"]
|
||||||
|
|
||||||
|
|
||||||
|
def criar_modelo_voos_2025(path: Path) -> None:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
if path.exists():
|
||||||
|
return
|
||||||
|
pd.DataFrame(columns=COLUNAS_VOOS_2025).to_csv(path, index=False)
|
||||||
|
|
||||||
|
|
||||||
|
def carregar_voos_2025(path: Path) -> pd.DataFrame:
|
||||||
|
df = pd.read_csv(path)
|
||||||
|
for col in COLUNAS_VOOS_2025:
|
||||||
|
if col not in df.columns:
|
||||||
|
df[col] = pd.NA
|
||||||
|
for col in COLUNAS_OPCIONAIS_VOOS_2025:
|
||||||
|
if col not in df.columns:
|
||||||
|
df[col] = pd.NA
|
||||||
|
df = df[COLUNAS_OPCIONAIS_VOOS_2025 + COLUNAS_VOOS_2025].copy()
|
||||||
|
df["data"] = pd.to_datetime(df["data"], errors="coerce").dt.date
|
||||||
|
for col in ["aeronave", "tipo_escala", "tripulante", "funcao", "oi"]:
|
||||||
|
df[col] = df[col].map(normalizar_texto)
|
||||||
|
df["horas_voadas"] = pd.to_numeric(df["horas_voadas"], errors="coerce").fillna(0)
|
||||||
|
df["sbv"] = pd.to_numeric(df["sbv"], errors="coerce").fillna(0)
|
||||||
|
return df.dropna(subset=["data"]).reset_index(drop=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _montar_slots(voos: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
voados = voos[voos["horas_voadas"] > 0].copy()
|
||||||
|
if voados.empty:
|
||||||
|
return pd.DataFrame(columns=["slot_id", "data", "aeronave", "tipo_escala", "horas_voadas"])
|
||||||
|
if "slot_id" in voados.columns and not voados["slot_id"].isna().all():
|
||||||
|
voos_unicos = (
|
||||||
|
voados.groupby(["slot_id", "data", "aeronave", "tipo_escala"], as_index=False)
|
||||||
|
.agg(
|
||||||
|
horas_voadas=("horas_voadas", "max"),
|
||||||
|
tripulantes_reais=("tripulante", lambda s: ", ".join(sorted(set(s)))),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
slots = (
|
||||||
|
voos_unicos.groupby(["data", "aeronave", "tipo_escala"], as_index=False)
|
||||||
|
.agg(
|
||||||
|
horas_voadas=("horas_voadas", "sum"),
|
||||||
|
tripulantes_reais=("tripulantes_reais", lambda s: ", ".join(sorted(set(", ".join(s).split(", "))))),
|
||||||
|
)
|
||||||
|
.sort_values(["data", "aeronave", "tipo_escala"])
|
||||||
|
.reset_index(drop=True)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
slots = (
|
||||||
|
voados.groupby(["data", "aeronave", "tipo_escala"], as_index=False)
|
||||||
|
.agg(horas_voadas=("horas_voadas", "max"), tripulantes_reais=("tripulante", lambda s: ", ".join(sorted(set(s)))))
|
||||||
|
.sort_values(["data", "aeronave", "tipo_escala"])
|
||||||
|
.reset_index(drop=True)
|
||||||
|
)
|
||||||
|
slots["slot_id"] = slots.index + 1
|
||||||
|
return slots[["slot_id", "data", "aeronave", "tipo_escala", "horas_voadas", "tripulantes_reais"]]
|
||||||
|
|
||||||
|
|
||||||
|
def _metricas(horas: pd.Series, meta_horas: float) -> dict:
|
||||||
|
horas = horas.astype(float)
|
||||||
|
return {
|
||||||
|
"tripulantes": int(len(horas)),
|
||||||
|
"horas_total": round(float(horas.sum()), 2),
|
||||||
|
"horas_media": round(float(horas.mean()), 2) if len(horas) else 0,
|
||||||
|
"desvio_padrao": round(float(horas.std(ddof=0)), 2) if len(horas) else 0,
|
||||||
|
"menor_horas": round(float(horas.min()), 2) if len(horas) else 0,
|
||||||
|
"maior_horas": round(float(horas.max()), 2) if len(horas) else 0,
|
||||||
|
"abaixo_meta": int((horas < meta_horas).sum()),
|
||||||
|
"desvio_medio_meta": round(float((horas - meta_horas).abs().mean()), 2) if len(horas) else 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def otimizar_equalizacao_50(
|
||||||
|
base_dir: Path,
|
||||||
|
voos: pd.DataFrame,
|
||||||
|
meta_horas: float = 50.0,
|
||||||
|
tempo_limite_segundos: float = 180.0,
|
||||||
|
gap_relativo: float = 0.05,
|
||||||
|
) -> dict:
|
||||||
|
historico_vazio = pd.DataFrame(columns=["tripulante", "horas_voadas", "sbv"])
|
||||||
|
cadastro = carregar_cadastro(base_dir, historico_vazio)
|
||||||
|
trips_cadastro = set(cadastro["tripulante"])
|
||||||
|
if "slot_id" in voos.columns and not voos["slot_id"].isna().all():
|
||||||
|
slots_validos = (
|
||||||
|
voos.groupby("slot_id")["tripulante"]
|
||||||
|
.apply(lambda s: set(s).issubset(trips_cadastro))
|
||||||
|
.loc[lambda s: s]
|
||||||
|
.index
|
||||||
|
)
|
||||||
|
voos = voos[voos["slot_id"].isin(slots_validos)].copy()
|
||||||
|
else:
|
||||||
|
voos = voos[voos["tripulante"].isin(trips_cadastro)].copy()
|
||||||
|
|
||||||
|
slots = _montar_slots(voos)
|
||||||
|
if slots.empty:
|
||||||
|
raise RuntimeError("A base de 2025 nao possui linhas com horas_voadas maiores que zero.")
|
||||||
|
|
||||||
|
trips = sorted(set(voos["tripulante"]) & trips_cadastro)
|
||||||
|
if not trips:
|
||||||
|
raise RuntimeError("Nao ha tripulantes da base de 2025 presentes no cadastro atual.")
|
||||||
|
|
||||||
|
pares = []
|
||||||
|
for s_idx, slot in slots.iterrows():
|
||||||
|
qt_col = f"qt_{slot['aeronave'].lower()}"
|
||||||
|
for t_idx, trip in enumerate(trips):
|
||||||
|
linha = cadastro[cadastro["tripulante"] == trip]
|
||||||
|
if not linha.empty and bool(linha.iloc[0].get(qt_col, False)):
|
||||||
|
pares.append((s_idx, t_idx, float(slot["horas_voadas"])))
|
||||||
|
if not pares:
|
||||||
|
raise RuntimeError("Nao ha candidatos compativeis entre aeronaves dos voos 2025 e cadastro.")
|
||||||
|
|
||||||
|
n_x = len(pares)
|
||||||
|
n_t = len(trips)
|
||||||
|
hmax_idx = n_x + 2 * n_t
|
||||||
|
hmin_idx = hmax_idx + 1
|
||||||
|
n_vars = hmin_idx + 1
|
||||||
|
c = np.zeros(n_vars)
|
||||||
|
c[n_x : n_x + n_t] = 1
|
||||||
|
c[n_x + n_t :] = 1
|
||||||
|
c[hmax_idx] = 100
|
||||||
|
c[hmin_idx] = -100
|
||||||
|
integrality = np.zeros(n_vars)
|
||||||
|
integrality[:n_x] = 1
|
||||||
|
bounds = Bounds(np.zeros(n_vars), np.r_[np.ones(n_x), np.full(2 * n_t + 2, np.inf)])
|
||||||
|
|
||||||
|
constraints = []
|
||||||
|
for s_idx in range(len(slots)):
|
||||||
|
row = np.zeros(n_vars)
|
||||||
|
for var_idx, (slot_i, _, _) in enumerate(pares):
|
||||||
|
if slot_i == s_idx:
|
||||||
|
row[var_idx] = 1
|
||||||
|
constraints.append(LinearConstraint(row, 2, 2))
|
||||||
|
|
||||||
|
for data in sorted(slots["data"].unique()):
|
||||||
|
slots_dia = set(slots.index[slots["data"] == data])
|
||||||
|
for t_idx in range(n_t):
|
||||||
|
row = np.zeros(n_vars)
|
||||||
|
for var_idx, (slot_i, trip_i, _) in enumerate(pares):
|
||||||
|
if slot_i in slots_dia and trip_i == t_idx:
|
||||||
|
row[var_idx] = 1
|
||||||
|
constraints.append(LinearConstraint(row, -np.inf, 1))
|
||||||
|
|
||||||
|
for t_idx in range(n_t):
|
||||||
|
row = np.zeros(n_vars)
|
||||||
|
for var_idx, (_, trip_i, horas) in enumerate(pares):
|
||||||
|
if trip_i == t_idx:
|
||||||
|
row[var_idx] = horas
|
||||||
|
row[n_x + t_idx] = -1
|
||||||
|
row[n_x + n_t + t_idx] = 1
|
||||||
|
constraints.append(LinearConstraint(row, meta_horas, meta_horas))
|
||||||
|
|
||||||
|
row = np.zeros(n_vars)
|
||||||
|
for var_idx, (_, trip_i, horas) in enumerate(pares):
|
||||||
|
if trip_i == t_idx:
|
||||||
|
row[var_idx] = horas
|
||||||
|
row[hmax_idx] = -1
|
||||||
|
constraints.append(LinearConstraint(row, -np.inf, 0))
|
||||||
|
|
||||||
|
row = np.zeros(n_vars)
|
||||||
|
for var_idx, (_, trip_i, horas) in enumerate(pares):
|
||||||
|
if trip_i == t_idx:
|
||||||
|
row[var_idx] = horas
|
||||||
|
row[hmin_idx] = -1
|
||||||
|
constraints.append(LinearConstraint(row, 0, np.inf))
|
||||||
|
|
||||||
|
result = milp(
|
||||||
|
c=c,
|
||||||
|
integrality=integrality,
|
||||||
|
bounds=bounds,
|
||||||
|
constraints=constraints,
|
||||||
|
options={"time_limit": tempo_limite_segundos, "mip_rel_gap": gap_relativo},
|
||||||
|
)
|
||||||
|
if result.x is None:
|
||||||
|
raise RuntimeError(f"Validacao MILP inviavel: {result.message}")
|
||||||
|
|
||||||
|
escolhidos = result.x[:n_x] > 0.5
|
||||||
|
linhas = []
|
||||||
|
for var_idx, escolhido in enumerate(escolhidos):
|
||||||
|
if not escolhido:
|
||||||
|
continue
|
||||||
|
s_idx, t_idx, horas = pares[var_idx]
|
||||||
|
slot = slots.iloc[s_idx]
|
||||||
|
linhas.append(
|
||||||
|
{
|
||||||
|
"slot_id": int(slot["slot_id"]),
|
||||||
|
"data": slot["data"],
|
||||||
|
"aeronave": slot["aeronave"],
|
||||||
|
"tipo_escala": slot["tipo_escala"],
|
||||||
|
"tripulante_otimizado": trips[t_idx],
|
||||||
|
"horas_voadas": horas,
|
||||||
|
"tripulantes_reais": slot["tripulantes_reais"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
otimizados = pd.DataFrame(linhas)
|
||||||
|
horas_reais = voos.groupby("tripulante")["horas_voadas"].sum().reindex(trips, fill_value=0)
|
||||||
|
horas_otimizadas = otimizados.groupby("tripulante_otimizado")["horas_voadas"].sum().reindex(trips, fill_value=0)
|
||||||
|
comparativo = pd.DataFrame(
|
||||||
|
{
|
||||||
|
"tripulante": trips,
|
||||||
|
"horas_reais_2025": horas_reais.to_numpy(),
|
||||||
|
"horas_otimizadas_meta_50": horas_otimizadas.to_numpy(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
comparativo["delta_otimizado_menos_real"] = comparativo["horas_otimizadas_meta_50"] - comparativo["horas_reais_2025"]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"slots": slots,
|
||||||
|
"otimizados": otimizados,
|
||||||
|
"comparativo": comparativo,
|
||||||
|
"metricas": pd.DataFrame(
|
||||||
|
[
|
||||||
|
{"cenario": "real_2025", **_metricas(comparativo["horas_reais_2025"], meta_horas)},
|
||||||
|
{"cenario": "otimizado_meta_50", **_metricas(comparativo["horas_otimizadas_meta_50"], meta_horas)},
|
||||||
|
]
|
||||||
|
),
|
||||||
|
"status_solver": {
|
||||||
|
"sucesso": bool(result.success),
|
||||||
|
"mensagem": str(result.message),
|
||||||
|
"fun_objetivo": float(result.fun) if result.fun is not None else None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def salvar_relatorio_validacao(base_dir: Path, resultado: dict, meta_horas: float = 50.0) -> Path:
|
||||||
|
out_dir = base_dir / "resultados" / "validacao"
|
||||||
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
arquivo = out_dir / f"validacao_2025_meta_{int(meta_horas)}_{datetime.now():%Y%m%d_%H%M%S}.xlsx"
|
||||||
|
with pd.ExcelWriter(arquivo, engine="openpyxl") as writer:
|
||||||
|
pd.DataFrame([resultado.get("status_solver", {})]).to_excel(writer, sheet_name="solver", index=False)
|
||||||
|
resultado["metricas"].to_excel(writer, sheet_name="metricas", index=False)
|
||||||
|
resultado["comparativo"].to_excel(writer, sheet_name="comparativo_trips", index=False)
|
||||||
|
resultado["slots"].to_excel(writer, sheet_name="voos_2025_slots", index=False)
|
||||||
|
resultado["otimizados"].to_excel(writer, sheet_name="escala_otimizada", index=False)
|
||||||
|
return arquivo
|
||||||
578
web_app.py
Normal file
578
web_app.py
Normal file
@@ -0,0 +1,578 @@
|
|||||||
|
"""Interface web local em HTML para o planejador diario."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import mimetypes
|
||||||
|
import os
|
||||||
|
import threading
|
||||||
|
import webbrowser
|
||||||
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||||
|
from pathlib import Path
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from src.planejador_missao.main import executar_planejamento
|
||||||
|
from src.planejador_missao.quadrinhos import quadrinhos_para_api
|
||||||
|
from src.planejador_missao.utils import normalizar_texto, parse_bool
|
||||||
|
|
||||||
|
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent
|
||||||
|
DADOS_DIR = BASE_DIR / "dados"
|
||||||
|
HOST = "127.0.0.1"
|
||||||
|
PORT = 8050
|
||||||
|
|
||||||
|
|
||||||
|
def read_csv(path: Path, default: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
if not path.exists():
|
||||||
|
return default.copy()
|
||||||
|
return pd.read_csv(path).fillna("")
|
||||||
|
|
||||||
|
|
||||||
|
def load_state() -> dict:
|
||||||
|
parametros_default = pd.DataFrame(
|
||||||
|
{
|
||||||
|
"parametro": ["data_planejamento", "criterio_missao", "criterio_sbv"],
|
||||||
|
"valor": ["hoje", "meta_50", "equalizar_quadrinhos"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
parametros_df = read_csv(DADOS_DIR / "parametros_missao.csv", parametros_default)
|
||||||
|
parametros = dict(zip(parametros_df["parametro"], parametros_df["valor"]))
|
||||||
|
|
||||||
|
aeronaves_default = pd.DataFrame(
|
||||||
|
{
|
||||||
|
"aeronave": ["C98", "C97", "C95"],
|
||||||
|
"disponivel_sede": [True, True, True],
|
||||||
|
"em_pane": [False, False, False],
|
||||||
|
"em_missao_rota": [False, False, False],
|
||||||
|
"hora_livre": ["08:00", "08:00", "08:00"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
aeronaves = read_csv(DADOS_DIR / "aeronaves_disponiveis.csv", aeronaves_default)
|
||||||
|
for col in ["disponivel_sede", "em_pane", "em_missao_rota"]:
|
||||||
|
aeronaves[col] = aeronaves[col].map(parse_bool)
|
||||||
|
|
||||||
|
rota_default = pd.DataFrame(
|
||||||
|
{
|
||||||
|
"origem": ["NT"],
|
||||||
|
"destino": [""],
|
||||||
|
"inicio": [pd.Timestamp.today().strftime("%Y-%m-%d 08:00")],
|
||||||
|
"fim": [pd.Timestamp.today().strftime("%Y-%m-%d 17:00")],
|
||||||
|
"tev_horas": [0],
|
||||||
|
"pernoite_dias": [0],
|
||||||
|
"rota": [""],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
rotas = read_csv(DADOS_DIR / "rotas_acionadas.csv", rota_default)
|
||||||
|
rota = rota_default.iloc[0].to_dict() if rotas.empty else rotas.iloc[0].to_dict()
|
||||||
|
|
||||||
|
trips_default = pd.DataFrame({"tripulante": [], "disponivel": []})
|
||||||
|
tripulantes = read_csv(DADOS_DIR / "tripulantes_disponiveis.csv", trips_default)
|
||||||
|
if not tripulantes.empty:
|
||||||
|
tripulantes["tripulante"] = tripulantes["tripulante"].map(normalizar_texto)
|
||||||
|
tripulantes["disponivel"] = tripulantes["disponivel"].map(parse_bool)
|
||||||
|
tripulantes = tripulantes.sort_values("tripulante")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"parametros": {
|
||||||
|
"data_planejamento": str(parametros.get("data_planejamento", "hoje")),
|
||||||
|
"criterio_missao": str(parametros.get("criterio_missao", "meta_50")),
|
||||||
|
"criterio_sbv": str(parametros.get("criterio_sbv", "equalizar_quadrinhos")),
|
||||||
|
},
|
||||||
|
"aeronaves": aeronaves.to_dict(orient="records"),
|
||||||
|
"rota": rota,
|
||||||
|
"tem_rota": str(rota.get("destino", "")).strip() != "",
|
||||||
|
"tripulantes": tripulantes.to_dict(orient="records"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def save_state(payload: dict) -> None:
|
||||||
|
parametros = payload.get("parametros", {})
|
||||||
|
pd.DataFrame(
|
||||||
|
{
|
||||||
|
"parametro": ["data_planejamento", "criterio_missao", "criterio_sbv"],
|
||||||
|
"valor": [
|
||||||
|
parametros.get("data_planejamento", "hoje"),
|
||||||
|
parametros.get("criterio_missao", "meta_50"),
|
||||||
|
parametros.get("criterio_sbv", "equalizar_quadrinhos"),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
).to_csv(DADOS_DIR / "parametros_missao.csv", index=False)
|
||||||
|
|
||||||
|
pd.DataFrame(payload.get("aeronaves", [])).to_csv(DADOS_DIR / "aeronaves_disponiveis.csv", index=False)
|
||||||
|
|
||||||
|
if payload.get("tem_rota", False):
|
||||||
|
rota = payload.get("rota", {})
|
||||||
|
pd.DataFrame(
|
||||||
|
{
|
||||||
|
"origem": [normalizar_texto(rota.get("origem", "NT"))],
|
||||||
|
"destino": [normalizar_texto(rota.get("destino", ""))],
|
||||||
|
"inicio": [str(rota.get("inicio", "")).strip()],
|
||||||
|
"fim": [str(rota.get("fim", "")).strip()],
|
||||||
|
"tev_horas": [str(rota.get("tev_horas", "0")).strip()],
|
||||||
|
"pernoite_dias": [str(rota.get("pernoite_dias", "0")).strip()],
|
||||||
|
"rota": [normalizar_texto(rota.get("rota", ""))],
|
||||||
|
}
|
||||||
|
).to_csv(DADOS_DIR / "rotas_acionadas.csv", index=False)
|
||||||
|
else:
|
||||||
|
pd.DataFrame(columns=["origem", "destino", "inicio", "fim", "tev_horas", "pernoite_dias", "rota"]).to_csv(
|
||||||
|
DADOS_DIR / "rotas_acionadas.csv", index=False
|
||||||
|
)
|
||||||
|
|
||||||
|
tripulantes = pd.DataFrame(payload.get("tripulantes", []))
|
||||||
|
if not tripulantes.empty:
|
||||||
|
tripulantes["tripulante"] = tripulantes["tripulante"].map(normalizar_texto)
|
||||||
|
tripulantes.to_csv(DADOS_DIR / "tripulantes_disponiveis.csv", index=False)
|
||||||
|
|
||||||
|
|
||||||
|
class Handler(BaseHTTPRequestHandler):
|
||||||
|
def do_GET(self) -> None:
|
||||||
|
path = urlparse(self.path).path
|
||||||
|
if path == "/":
|
||||||
|
self._send_html(INDEX_HTML)
|
||||||
|
return
|
||||||
|
if path == "/api/state":
|
||||||
|
self._send_json(load_state())
|
||||||
|
return
|
||||||
|
if path == "/api/quadrinhos":
|
||||||
|
self._send_json(quadrinhos_para_api(BASE_DIR))
|
||||||
|
return
|
||||||
|
if path.startswith("/assets/"):
|
||||||
|
self._send_file(DADOS_DIR / "assets" / path.removeprefix("/assets/"))
|
||||||
|
return
|
||||||
|
self.send_error(404)
|
||||||
|
|
||||||
|
def do_POST(self) -> None:
|
||||||
|
path = urlparse(self.path).path
|
||||||
|
payload = self._read_json()
|
||||||
|
if path == "/api/save":
|
||||||
|
save_state(payload)
|
||||||
|
self._send_json({"ok": True, "message": "Entradas salvas em dados/."})
|
||||||
|
return
|
||||||
|
if path == "/api/run":
|
||||||
|
save_state(payload)
|
||||||
|
resultado = executar_planejamento(BASE_DIR)
|
||||||
|
self._send_json(
|
||||||
|
{
|
||||||
|
"ok": True,
|
||||||
|
"message": "Escala gerada com sucesso.",
|
||||||
|
"data_planejamento": str(resultado["data_planejamento"]),
|
||||||
|
"arquivo_saida": str(resultado["arquivo_saida"]),
|
||||||
|
"total_candidatas": resultado["total_candidatas"],
|
||||||
|
"total_selecionadas": resultado["total_selecionadas"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if path == "/api/open-latest":
|
||||||
|
arquivos = sorted((BASE_DIR / "resultados").glob("planejamento_diario_*.xlsx"), key=lambda p: p.stat().st_mtime)
|
||||||
|
if not arquivos:
|
||||||
|
self._send_json({"ok": False, "message": "Nenhuma planilha encontrada."}, status=404)
|
||||||
|
return
|
||||||
|
os.startfile(arquivos[-1])
|
||||||
|
self._send_json({"ok": True, "message": f"Abrindo {arquivos[-1]}"})
|
||||||
|
return
|
||||||
|
self.send_error(404)
|
||||||
|
|
||||||
|
def _read_json(self) -> dict:
|
||||||
|
length = int(self.headers.get("Content-Length", "0"))
|
||||||
|
if length == 0:
|
||||||
|
return {}
|
||||||
|
return json.loads(self.rfile.read(length).decode("utf-8"))
|
||||||
|
|
||||||
|
def _send_json(self, payload: dict, status: int = 200) -> None:
|
||||||
|
data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
||||||
|
self.send_response(status)
|
||||||
|
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||||
|
self.send_header("Content-Length", str(len(data)))
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(data)
|
||||||
|
|
||||||
|
def _send_html(self, html: str) -> None:
|
||||||
|
data = html.encode("utf-8")
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-Type", "text/html; charset=utf-8")
|
||||||
|
self.send_header("Content-Length", str(len(data)))
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(data)
|
||||||
|
|
||||||
|
def _send_file(self, path: Path) -> None:
|
||||||
|
try:
|
||||||
|
resolved = path.resolve()
|
||||||
|
assets_dir = (DADOS_DIR / "assets").resolve()
|
||||||
|
if not resolved.is_file() or assets_dir not in resolved.parents:
|
||||||
|
self.send_error(404)
|
||||||
|
return
|
||||||
|
data = resolved.read_bytes()
|
||||||
|
content_type = mimetypes.guess_type(str(resolved))[0] or "application/octet-stream"
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-Type", content_type)
|
||||||
|
self.send_header("Content-Length", str(len(data)))
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(data)
|
||||||
|
except OSError:
|
||||||
|
self.send_error(404)
|
||||||
|
|
||||||
|
def log_message(self, format: str, *args) -> None:
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
INDEX_HTML = r"""<!doctype html>
|
||||||
|
<html lang="pt-BR">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Planejador Diario de Missoes</title>
|
||||||
|
<link rel="icon" href="/assets/favicon.ico">
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--ink:#172033; --muted:#667085; --line:#d7deea; --brand:#1f4e78; --brand-2:#2c6b9f;
|
||||||
|
--bg:#eef3f8; --panel:#ffffff; --soft:#f7fafc; --ok:#26734d; --ok-2:#1d6b46; --warn:#9a3412;
|
||||||
|
--shadow:0 12px 28px rgba(23,32,51,.10); --shadow-soft:0 5px 16px rgba(23,32,51,.07);
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body { margin:0; font-family: Segoe UI, Arial, sans-serif; color:var(--ink); background:var(--bg); }
|
||||||
|
header { background:linear-gradient(135deg, #12334f, #1f4e78 58%, #2f7a9f); color:white; padding:18px 26px; box-shadow:var(--shadow); }
|
||||||
|
.topbar { max-width:1280px; margin:0 auto; display:flex; align-items:center; justify-content:space-between; gap:18px; }
|
||||||
|
.brand { display:flex; align-items:center; gap:14px; min-width:0; }
|
||||||
|
.brand img { width:48px; height:48px; object-fit:contain; background:white; border-radius:6px; padding:5px; }
|
||||||
|
h1 { font-size:22px; line-height:1.15; margin:0; color:white; letter-spacing:0; }
|
||||||
|
.subtitle { margin-top:4px; color:#dceaf5; font-size:13px; }
|
||||||
|
main { padding:22px 24px 32px; max-width:1280px; margin:0 auto; }
|
||||||
|
.grid { display:grid; grid-template-columns: minmax(0, 1fr) minmax(360px, .9fr); gap:18px; align-items:start; }
|
||||||
|
section { background:var(--panel); border:1px solid var(--line); border-radius:8px; padding:18px; box-shadow:var(--shadow-soft); }
|
||||||
|
h2 { font-size:15px; margin:0 0 14px; color:var(--brand); display:flex; align-items:center; gap:8px; }
|
||||||
|
h2::before { content:""; width:4px; height:18px; border-radius:4px; background:var(--brand-2); display:inline-block; }
|
||||||
|
label { display:block; font-size:12px; font-weight:700; color:#344054; margin-bottom:6px; }
|
||||||
|
input, select { width:100%; padding:10px 11px; border:1px solid #c7d3e0; border-radius:6px; font-size:14px; background:white; color:var(--ink); outline:none; transition:border-color .15s, box-shadow .15s; }
|
||||||
|
input:focus, select:focus { border-color:var(--brand-2); box-shadow:0 0 0 3px rgba(47,122,159,.14); }
|
||||||
|
.row { display:grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap:12px; margin-bottom:12px; }
|
||||||
|
.check { display:flex; align-items:center; gap:8px; margin:9px 0; font-size:14px; color:#26364a; }
|
||||||
|
.check input { width:16px; height:16px; accent-color:var(--brand); }
|
||||||
|
.aircraft { border-top:1px solid var(--line); padding-top:13px; margin-top:13px; }
|
||||||
|
.aircraft:first-of-type { border-top:0; padding-top:0; margin-top:0; }
|
||||||
|
.aircraft strong { display:block; margin-bottom:9px; color:#122b43; font-size:14px; }
|
||||||
|
.crewbar { display:flex; gap:8px; margin-bottom:10px; }
|
||||||
|
button { border:0; border-radius:6px; padding:10px 14px; font-weight:700; cursor:pointer; background:#e7edf3; color:#172033; transition:transform .12s, box-shadow .12s, background .12s; }
|
||||||
|
button:hover { transform:translateY(-1px); box-shadow:0 5px 12px rgba(23,32,51,.13); }
|
||||||
|
button.primary { background:#ffffff; color:var(--brand); }
|
||||||
|
button.success { background:var(--ok); color:white; }
|
||||||
|
main button.primary { background:var(--brand); color:white; }
|
||||||
|
main button.success { background:var(--ok-2); color:white; }
|
||||||
|
button:disabled { opacity:.55; cursor:wait; }
|
||||||
|
.crew { height:290px; overflow:auto; border:1px solid var(--line); border-radius:8px; padding:10px; display:grid; grid-template-columns: repeat(4, minmax(78px, 1fr)); gap:6px 10px; background:var(--soft); }
|
||||||
|
.crew label { display:flex; align-items:center; gap:7px; font-size:13px; font-weight:600; margin:0; background:white; border:1px solid #e1e8f0; border-radius:5px; padding:6px 7px; }
|
||||||
|
.crew input { width:15px; height:15px; accent-color:var(--brand); }
|
||||||
|
pre { white-space:pre-wrap; background:#101928; color:#dbe7f2; border-radius:8px; padding:15px; min-height:132px; margin:0; font-size:13px; line-height:1.5; border:1px solid #26374d; }
|
||||||
|
.actions { display:flex; flex-wrap:wrap; gap:10px; }
|
||||||
|
.tabs { display:flex; gap:8px; margin-bottom:18px; border-bottom:1px solid var(--line); }
|
||||||
|
.tab { border-radius:7px 7px 0 0; background:#dfe8f2; color:#1f2937; padding:10px 14px; box-shadow:none; }
|
||||||
|
.tab:hover { box-shadow:none; }
|
||||||
|
.tab.active { background:var(--brand); color:white; }
|
||||||
|
.view { display:none; }
|
||||||
|
.view.active { display:block; }
|
||||||
|
.summary { display:grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap:10px; margin-bottom:12px; }
|
||||||
|
.metric { border:1px solid var(--line); border-radius:8px; padding:12px; background:var(--soft); color:#475467; font-weight:700; }
|
||||||
|
.metric b { display:block; color:var(--brand); font-size:20px; margin-top:3px; }
|
||||||
|
.toolbar { display:grid; grid-template-columns: 1.5fr 180px 180px; gap:10px; margin-bottom:10px; }
|
||||||
|
.tablewrap { overflow:auto; border:1px solid var(--line); border-radius:8px; background:white; max-height:520px; }
|
||||||
|
table { width:100%; border-collapse:collapse; font-size:13px; min-width:980px; }
|
||||||
|
th, td { border-bottom:1px solid var(--line); padding:9px 10px; text-align:left; vertical-align:top; }
|
||||||
|
th { background:#e8f0f8; color:#243b53; position:sticky; top:0; z-index:1; }
|
||||||
|
tbody tr:nth-child(even) { background:#fafcff; }
|
||||||
|
td.num { text-align:right; font-variant-numeric: tabular-nums; }
|
||||||
|
@media (max-width: 900px) { .topbar { align-items:flex-start; flex-direction:column; } .grid { grid-template-columns:1fr; } .crew { grid-template-columns: repeat(2, minmax(80px, 1fr)); } }
|
||||||
|
@media (max-width: 900px) { .summary, .toolbar { grid-template-columns:1fr; } }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<div class="topbar">
|
||||||
|
<div class="brand">
|
||||||
|
<img src="/assets/app_logo.png" alt="">
|
||||||
|
<div>
|
||||||
|
<h1>Planejador Diario de Missoes</h1>
|
||||||
|
<div class="subtitle">Escala, sobreaviso e acompanhamento operacional</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<button onclick="save()">Salvar entradas</button>
|
||||||
|
<button class="primary" onclick="run()">Gerar escala</button>
|
||||||
|
<button class="success" onclick="openLatest()">Abrir planilha</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<div class="tabs">
|
||||||
|
<button class="tab active" data-view="planejamento" onclick="showView('planejamento')">Planejamento</button>
|
||||||
|
<button class="tab" data-view="quadrinhos" onclick="showView('quadrinhos')">Quadrinhos</button>
|
||||||
|
</div>
|
||||||
|
<div id="planejamento" class="view active">
|
||||||
|
<div class="grid">
|
||||||
|
<section>
|
||||||
|
<h2>Parametros</h2>
|
||||||
|
<div class="row">
|
||||||
|
<div><label>Data</label><input id="data_planejamento" placeholder="hoje ou AAAA-MM-DD"></div>
|
||||||
|
<div><label>Criterio missao</label><select id="criterio_missao"><option>meta_50</option><option>meta_110</option><option>menor_custo</option></select></div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div><label>Criterio SBV</label><select id="criterio_sbv"><option>equalizar_quadrinhos</option><option>meta_50</option><option>meta_110</option></select></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Aeronaves</h2>
|
||||||
|
<div id="aeronaves"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Missao acionada</h2>
|
||||||
|
<label class="check"><input id="tem_rota" type="checkbox"> Existe missao de rota acionada</label>
|
||||||
|
<div class="row">
|
||||||
|
<div><label>Origem</label><input id="origem"></div>
|
||||||
|
<div><label>Destino</label><input id="destino"></div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div><label>Inicio</label><input id="inicio" placeholder="AAAA-MM-DD HH:MM"></div>
|
||||||
|
<div><label>Fim</label><input id="fim" placeholder="AAAA-MM-DD HH:MM"></div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div><label>TEV horas</label><input id="tev_horas"></div>
|
||||||
|
<div><label>Pernoites</label><input id="pernoite_dias"></div>
|
||||||
|
</div>
|
||||||
|
<div><label>Rota resumida</label><input id="rota"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Tripulantes disponiveis</h2>
|
||||||
|
<div class="crewbar">
|
||||||
|
<button onclick="setCrew(true)">Marcar todos</button>
|
||||||
|
<button onclick="setCrew(false)">Desmarcar todos</button>
|
||||||
|
</div>
|
||||||
|
<div id="tripulantes" class="crew"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section style="grid-column: 1 / -1;">
|
||||||
|
<h2>Status</h2>
|
||||||
|
<pre id="log">Carregando dados...</pre>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="quadrinhos" class="view">
|
||||||
|
<section>
|
||||||
|
<h2>Quadrinho operacional</h2>
|
||||||
|
<div class="summary">
|
||||||
|
<div class="metric">Tripulantes<b id="qtd_trip">-</b></div>
|
||||||
|
<div class="metric">Quadrinhos abaixo de 50h<b id="qtd_abaixo50">-</b></div>
|
||||||
|
<div class="metric">Media de horas no ano<b id="media_horas">-</b></div>
|
||||||
|
</div>
|
||||||
|
<div class="toolbar">
|
||||||
|
<input id="q_busca" placeholder="Filtrar por tripulante, OI ou observacao" oninput="renderQuadrinhos()">
|
||||||
|
<select id="q_aeronave" onchange="renderQuadrinhos()"><option value="">Todas aeronaves</option><option>C98</option><option>C97</option><option>C95</option></select>
|
||||||
|
<select id="q_meta" onchange="renderQuadrinhos()"><option value="">Todos</option><option value="abaixo50">Abaixo de 50h</option></select>
|
||||||
|
</div>
|
||||||
|
<div class="tablewrap">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Trip</th><th>Aeronave</th><th>Qualificacao</th><th>Proxima OI</th>
|
||||||
|
<th>Horas base</th><th>Horas ano</th><th>SBV</th><th>Faltam 50h</th><th>Ultimo voo</th><th>Obs</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="quadrinhos_body"><tr><td colspan="10">Carregando...</td></tr></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<script>
|
||||||
|
let state = null;
|
||||||
|
let quadrinhos = [];
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
const res = await fetch('/api/state');
|
||||||
|
state = await res.json();
|
||||||
|
document.querySelector('#data_planejamento').value = state.parametros.data_planejamento;
|
||||||
|
document.querySelector('#criterio_missao').value = state.parametros.criterio_missao;
|
||||||
|
document.querySelector('#criterio_sbv').value = state.parametros.criterio_sbv;
|
||||||
|
document.querySelector('#tem_rota').checked = state.tem_rota;
|
||||||
|
for (const key of ['origem','destino','inicio','fim','tev_horas','pernoite_dias','rota']) {
|
||||||
|
document.querySelector('#' + key).value = state.rota[key] ?? '';
|
||||||
|
}
|
||||||
|
renderAircraft();
|
||||||
|
renderCrew();
|
||||||
|
log('Pronto. Edite as entradas e clique em Salvar e gerar escala.');
|
||||||
|
loadQuadrinhos();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showView(name) {
|
||||||
|
for (const view of document.querySelectorAll('.view')) view.classList.toggle('active', view.id === name);
|
||||||
|
for (const tab of document.querySelectorAll('.tab')) tab.classList.toggle('active', tab.dataset.view === name);
|
||||||
|
if (name === 'quadrinhos' && quadrinhos.length === 0) loadQuadrinhos();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadQuadrinhos() {
|
||||||
|
const res = await fetch('/api/quadrinhos');
|
||||||
|
const data = await res.json();
|
||||||
|
quadrinhos = data.linhas ?? [];
|
||||||
|
document.querySelector('#qtd_trip').textContent = data.resumo?.tripulantes ?? 0;
|
||||||
|
document.querySelector('#qtd_abaixo50').textContent = data.resumo?.abaixo_50 ?? 0;
|
||||||
|
document.querySelector('#media_horas').textContent = `${data.resumo?.horas_media ?? 0} h`;
|
||||||
|
renderQuadrinhos();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderQuadrinhos() {
|
||||||
|
const root = document.querySelector('#quadrinhos_body');
|
||||||
|
const busca = (document.querySelector('#q_busca')?.value ?? '').trim().toUpperCase();
|
||||||
|
const aeronave = document.querySelector('#q_aeronave')?.value ?? '';
|
||||||
|
const meta = document.querySelector('#q_meta')?.value ?? '';
|
||||||
|
const rows = quadrinhos.filter((q) => {
|
||||||
|
const texto = `${q.tripulante} ${q.aeronave} ${q.qualificacao} ${q.proxima_oi} ${q.observacao}`.toUpperCase();
|
||||||
|
return (!busca || texto.includes(busca)) && (!aeronave || q.aeronave === aeronave) && (!meta || Number(q.horas_voadas_ano) < 50);
|
||||||
|
});
|
||||||
|
if (rows.length === 0) {
|
||||||
|
root.innerHTML = '<tr><td colspan="10">Nenhum quadrinho encontrado.</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
root.innerHTML = rows.map((q) => `
|
||||||
|
<tr>
|
||||||
|
<td>${q.tripulante}</td>
|
||||||
|
<td>${q.aeronave}</td>
|
||||||
|
<td>${q.qualificacao}</td>
|
||||||
|
<td>${q.proxima_oi}</td>
|
||||||
|
<td class="num">${fmt(q.horas_aeronave_base)}</td>
|
||||||
|
<td class="num">${fmt(q.horas_voadas_ano)}</td>
|
||||||
|
<td class="num">${fmt(q.sbv_ano)}</td>
|
||||||
|
<td class="num">${fmt(q.meta_50_faltante)}</td>
|
||||||
|
<td>${q.ultima_data_voo ?? ''}</td>
|
||||||
|
<td>${q.observacao ?? ''}</td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmt(value) {
|
||||||
|
const n = Number(value);
|
||||||
|
if (!Number.isFinite(n)) return '';
|
||||||
|
return n.toLocaleString('pt-BR', { maximumFractionDigits: 1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAircraft() {
|
||||||
|
const root = document.querySelector('#aeronaves');
|
||||||
|
root.innerHTML = '';
|
||||||
|
for (const av of state.aeronaves) {
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'aircraft';
|
||||||
|
item.innerHTML = `
|
||||||
|
<strong>${av.aeronave}</strong>
|
||||||
|
<label class="check"><input type="checkbox" data-av="${av.aeronave}" data-field="disponivel_sede" ${av.disponivel_sede ? 'checked' : ''}> Disponivel em sede</label>
|
||||||
|
<label class="check"><input type="checkbox" data-av="${av.aeronave}" data-field="em_pane" ${av.em_pane ? 'checked' : ''}> Em pane</label>
|
||||||
|
<label class="check"><input type="checkbox" data-av="${av.aeronave}" data-field="em_missao_rota" ${av.em_missao_rota ? 'checked' : ''}> Em missao de rota</label>
|
||||||
|
<label>Hora livre</label><input value="${av.hora_livre ?? ''}" data-av="${av.aeronave}" data-field="hora_livre">
|
||||||
|
`;
|
||||||
|
root.appendChild(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCrew() {
|
||||||
|
const root = document.querySelector('#tripulantes');
|
||||||
|
root.innerHTML = '';
|
||||||
|
for (const trip of state.tripulantes) {
|
||||||
|
const label = document.createElement('label');
|
||||||
|
label.innerHTML = `<input type="checkbox" data-trip="${trip.tripulante}" ${trip.disponivel ? 'checked' : ''}> ${trip.tripulante}`;
|
||||||
|
root.appendChild(label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function collect() {
|
||||||
|
const payload = {
|
||||||
|
parametros: {
|
||||||
|
data_planejamento: document.querySelector('#data_planejamento').value,
|
||||||
|
criterio_missao: document.querySelector('#criterio_missao').value,
|
||||||
|
criterio_sbv: document.querySelector('#criterio_sbv').value,
|
||||||
|
},
|
||||||
|
tem_rota: document.querySelector('#tem_rota').checked,
|
||||||
|
rota: {},
|
||||||
|
aeronaves: [],
|
||||||
|
tripulantes: [],
|
||||||
|
};
|
||||||
|
for (const key of ['origem','destino','inicio','fim','tev_horas','pernoite_dias','rota']) {
|
||||||
|
payload.rota[key] = document.querySelector('#' + key).value;
|
||||||
|
}
|
||||||
|
for (const av of state.aeronaves) {
|
||||||
|
const out = { aeronave: av.aeronave };
|
||||||
|
for (const field of ['disponivel_sede','em_pane','em_missao_rota']) {
|
||||||
|
out[field] = document.querySelector(`[data-av="${av.aeronave}"][data-field="${field}"]`).checked;
|
||||||
|
}
|
||||||
|
out.hora_livre = document.querySelector(`[data-av="${av.aeronave}"][data-field="hora_livre"]`).value;
|
||||||
|
payload.aeronaves.push(out);
|
||||||
|
}
|
||||||
|
for (const input of document.querySelectorAll('[data-trip]')) {
|
||||||
|
payload.tripulantes.push({ tripulante: input.dataset.trip, disponivel: input.checked });
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
setBusy(true);
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/save', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(collect()) });
|
||||||
|
const data = await res.json();
|
||||||
|
log(data.message);
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
setBusy(true);
|
||||||
|
log('Gerando escala. Aguarde...');
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/run', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(collect()) });
|
||||||
|
const data = await res.json();
|
||||||
|
if (!data.ok) throw new Error(data.message);
|
||||||
|
log(`${data.message}\nData: ${data.data_planejamento}\nColunas candidatas: ${data.total_candidatas}\nEscalas selecionadas: ${data.total_selecionadas}\nArquivo: ${data.arquivo_saida}`);
|
||||||
|
loadQuadrinhos();
|
||||||
|
} catch (err) {
|
||||||
|
log('Erro ao gerar escala:\n' + err.message);
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openLatest() {
|
||||||
|
const res = await fetch('/api/open-latest', { method:'POST', headers:{'Content-Type':'application/json'}, body:'{}' });
|
||||||
|
const data = await res.json();
|
||||||
|
log(data.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCrew(value) {
|
||||||
|
for (const input of document.querySelectorAll('[data-trip]')) input.checked = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBusy(value) {
|
||||||
|
for (const btn of document.querySelectorAll('button')) btn.disabled = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function log(text) {
|
||||||
|
document.querySelector('#log').textContent = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
load();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
server = ThreadingHTTPServer((HOST, PORT), Handler)
|
||||||
|
url = f"http://{HOST}:{PORT}"
|
||||||
|
threading.Timer(0.5, lambda: webbrowser.open(url)).start()
|
||||||
|
print(f"Interface web aberta em {url}")
|
||||||
|
print("Pressione Ctrl+C para encerrar.")
|
||||||
|
server.serve_forever()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user