Processa registro de voo 2025
This commit is contained in:
4
LOG.md
4
LOG.md
@@ -22,3 +22,7 @@ Legenda de autores:
|
||||
| 2026-06-15 | 18:11 | VTO | Agrupou artefatos do relatório de ciclo em subpasta própria | `pre_process/relatorio_ciclo_inspecoes_c105_2805_2026-06-15/`; `README.md`; `docs/about.md` | Removido `.gitkeep` de `pre_process/` porque a pasta deixou de estar vazia; `.gitkeep` de `processed/` foi mantido. |
|
||||
| 2026-06-15 | 18:33 | VTO | Comparou ICA 66-31/2023 com relatório de ciclo C-105 2805 | `raw/ICA 66-31 2023.pdf`; `pre_process/comparacao_ica_66_31_c105_2023/` | A comparação usou o Anexo D textual do PDF completo da ICA. |
|
||||
| 2026-06-15 | 19:01 | VTO | Reorganizou dados em `db/` e adicionou protótipo OAMRP | `db/`; `software/oamrp_v1.py`; `.gitignore`; `README.md`; `CONTEXTO.md`; `docs/about.md`; `docs/authors.md`; `docs/changelog.md` | Commit `1131267`; diretórios `raw/`, `pre_process/` e `processed/` movidos para `db/`; modelo MIP inicial em PuLP adicionado em `software/`. |
|
||||
| 2026-06-15 | 19:56 | VTO | Pre-processou a Planilha 2025 para registro de voo | `db/raw/Planilha 2025.xlsx`; `db/pre_process/registro_voo_2025/`; `db/processed/registro_voo_2025.csv` | Extraidos 1421 registros; colunas `REAL` ate `COM2` descartadas; 53 linhas cruzam 00Z; 1 registro sem OM e 9 sem pousos preservados para revisao. |
|
||||
| 2026-06-15 | 19:56 | VTO | Consolidou transicoes `ROTA` do registro de voo 2025 | `db/pre_process/registro_voo_2025/preprocess_planilha_2025.py`; `db/processed/registro_voo_2025_consolidado.csv` | `ROTA` tratado como marcador de transicao; 1421 linhas originais viraram 1268 trechos consolidados; 135 trechos com `ROTA` foram convertidos para origem/destino reais. |
|
||||
| 2026-06-15 | 19:56 | VTO | Validou o registro de voo 2025 consolidado | `db/pre_process/registro_voo_2025/validacao_registro_voo_2025.md`; `db/processed/registro_voo_2025_consolidado.csv` | Sem duplicidades, sem divergencias de duracao e sem sobreposicoes temporais; pendentes 1 registro sem OM, 1 trecho com pousos zero e 3 quebras de continuidade espacial. |
|
||||
| 2026-06-15 | 19:56 | VTO | Removeu registro isolado da aeronave 2806 | `db/pre_process/registro_voo_2025/preprocess_planilha_2025.py`; `db/processed/registro_voo_2025.csv`; `db/processed/registro_voo_2025_consolidado.csv` | Linha original 793 excluida dos artefatos processados; registro final passou para 1420 linhas e consolidado para 1267 trechos; nao ha mais registro sem OM. |
|
||||
|
||||
20
README.md
20
README.md
@@ -162,3 +162,23 @@ software/oamrp_v1.py
|
||||
```
|
||||
|
||||
Esse processamento extraiu 18 inspeções e separou campos como inspeção, referência, vencimento, TSO, letra, nível, duração, intervalo por horas de voo, meses contínuos e pousos.
|
||||
## Atualizacao - registro de voo 2025
|
||||
|
||||
A `Planilha 2025.xlsx` foi pre-processada a partir de `db/raw/` sem alterar o arquivo original.
|
||||
|
||||
Arquivos gerados:
|
||||
|
||||
```text
|
||||
db/pre_process/registro_voo_2025/preprocess_planilha_2025.py
|
||||
db/pre_process/registro_voo_2025/registro_voo_2025_limpo.csv
|
||||
db/pre_process/registro_voo_2025/registro_voo_2025_limpo.xlsx
|
||||
db/pre_process/registro_voo_2025/registro_voo_2025_consolidado.csv
|
||||
db/pre_process/registro_voo_2025/missoes_oamrp_2025.csv
|
||||
db/pre_process/registro_voo_2025/missoes_oamrp_2025_consolidado.csv
|
||||
db/pre_process/registro_voo_2025/resumo_registro_voo_2025.json
|
||||
db/pre_process/registro_voo_2025/validacao_registro_voo_2025.md
|
||||
db/processed/registro_voo_2025.csv
|
||||
db/processed/registro_voo_2025_consolidado.csv
|
||||
```
|
||||
|
||||
O consolidado trata `ROTA` como marcador de transicao, nao como localidade operacional, e remove a linha original 793 da aeronave 2806 dos artefatos processados.
|
||||
|
||||
1421
db/pre_process/registro_voo_2025/missoes_oamrp_2025.csv
Normal file
1421
db/pre_process/registro_voo_2025/missoes_oamrp_2025.csv
Normal file
File diff suppressed because it is too large
Load Diff
1268
db/pre_process/registro_voo_2025/missoes_oamrp_2025_consolidado.csv
Normal file
1268
db/pre_process/registro_voo_2025/missoes_oamrp_2025_consolidado.csv
Normal file
File diff suppressed because it is too large
Load Diff
509
db/pre_process/registro_voo_2025/preprocess_planilha_2025.py
Normal file
509
db/pre_process/registro_voo_2025/preprocess_planilha_2025.py
Normal file
@@ -0,0 +1,509 @@
|
||||
"""
|
||||
Pre-processa a planilha bruta de voos de 2025.
|
||||
|
||||
Entrada:
|
||||
db/raw/Planilha 2025.xlsx
|
||||
|
||||
Saidas:
|
||||
db/pre_process/registro_voo_2025/registro_voo_2025_limpo.xlsx
|
||||
db/pre_process/registro_voo_2025/registro_voo_2025_limpo.csv
|
||||
db/pre_process/registro_voo_2025/missoes_oamrp_2025.csv
|
||||
db/pre_process/registro_voo_2025/resumo_registro_voo_2025.json
|
||||
db/processed/registro_voo_2025.csv
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import json
|
||||
from collections import Counter
|
||||
from datetime import date, datetime, time, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import openpyxl
|
||||
|
||||
|
||||
ANO = 2025
|
||||
BASE_DIR = Path(__file__).resolve().parents[3]
|
||||
RAW_PATH = BASE_DIR / "db" / "raw" / "Planilha 2025.xlsx"
|
||||
PREPROCESS_DIR = BASE_DIR / "db" / "pre_process" / "registro_voo_2025"
|
||||
PROCESSED_DIR = BASE_DIR / "db" / "processed"
|
||||
|
||||
LIMPO_XLSX = PREPROCESS_DIR / "registro_voo_2025_limpo.xlsx"
|
||||
LIMPO_CSV = PREPROCESS_DIR / "registro_voo_2025_limpo.csv"
|
||||
CONSOLIDADO_CSV = PREPROCESS_DIR / "registro_voo_2025_consolidado.csv"
|
||||
MISSOES_CSV = PREPROCESS_DIR / "missoes_oamrp_2025.csv"
|
||||
MISSOES_CONSOLIDADAS_CSV = PREPROCESS_DIR / "missoes_oamrp_2025_consolidado.csv"
|
||||
RESUMO_JSON = PREPROCESS_DIR / "resumo_registro_voo_2025.json"
|
||||
PROCESSED_CSV = PROCESSED_DIR / "registro_voo_2025.csv"
|
||||
PROCESSED_CONSOLIDADO_CSV = PROCESSED_DIR / "registro_voo_2025_consolidado.csv"
|
||||
|
||||
MES_MAP = {
|
||||
"JAN": 1,
|
||||
"FEV": 2,
|
||||
"MAR": 3,
|
||||
"ABR": 4,
|
||||
"MAI": 5,
|
||||
"JUN": 6,
|
||||
"JUL": 7,
|
||||
"AGO": 8,
|
||||
"SET": 9,
|
||||
"OUT": 10,
|
||||
"NOV": 11,
|
||||
"DEZ": 12,
|
||||
}
|
||||
MES_ABBR = {numero: nome for nome, numero in MES_MAP.items()}
|
||||
LINHAS_ORIGEM_EXCLUIDAS = {793}
|
||||
|
||||
COLUNAS_REGISTRO = [
|
||||
"id_registro",
|
||||
"linha_origem",
|
||||
"data",
|
||||
"data_iso",
|
||||
"mes",
|
||||
"mes_numero",
|
||||
"om",
|
||||
"aeronave",
|
||||
"etapa",
|
||||
"localidade_dep",
|
||||
"localidade_arr",
|
||||
"horario_z_dep",
|
||||
"horario_z_arr",
|
||||
"partida_utc",
|
||||
"chegada_utc",
|
||||
"tempo_voo",
|
||||
"tempo_voo_min",
|
||||
"segmento",
|
||||
"pousos",
|
||||
"missao",
|
||||
"periodo",
|
||||
"cruza_meia_noite",
|
||||
]
|
||||
|
||||
COLUNAS_OAMRP = [
|
||||
"id",
|
||||
"orig",
|
||||
"dest",
|
||||
"partida_utc",
|
||||
"chegada_utc",
|
||||
"prioridade",
|
||||
]
|
||||
|
||||
COLUNAS_CONSOLIDADAS = [
|
||||
"id_registro",
|
||||
"linhas_origem",
|
||||
"data",
|
||||
"data_iso",
|
||||
"mes",
|
||||
"mes_numero",
|
||||
"om",
|
||||
"aeronave",
|
||||
"etapa_inicio",
|
||||
"etapa_fim",
|
||||
"localidade_dep",
|
||||
"localidade_arr",
|
||||
"horario_z_dep",
|
||||
"horario_z_arr",
|
||||
"partida_utc",
|
||||
"chegada_utc",
|
||||
"tempo_voo",
|
||||
"tempo_voo_min",
|
||||
"segmento_inicio",
|
||||
"segmento_fim",
|
||||
"pousos",
|
||||
"missao",
|
||||
"periodo",
|
||||
"cruza_meia_noite",
|
||||
"qtd_linhas_originais",
|
||||
"tem_rota_intermediaria",
|
||||
]
|
||||
|
||||
|
||||
def texto_limpo(valor: Any) -> str:
|
||||
if valor is None:
|
||||
return ""
|
||||
return str(valor).strip()
|
||||
|
||||
|
||||
def normalizar_mes(valor: Any) -> str:
|
||||
texto = texto_limpo(valor).upper().replace(".", "")
|
||||
return texto
|
||||
|
||||
|
||||
def parse_data(valor_data: Any, valor_mes: Any) -> date:
|
||||
if isinstance(valor_data, datetime):
|
||||
return valor_data.date()
|
||||
if isinstance(valor_data, date):
|
||||
return valor_data
|
||||
|
||||
texto = texto_limpo(valor_data).lower().replace(".", "")
|
||||
if "/" not in texto:
|
||||
raise ValueError(f"Data invalida: {valor_data!r}")
|
||||
dia_txt, mes_txt = texto.split("/", 1)
|
||||
mes_key = normalizar_mes(mes_txt or valor_mes)
|
||||
if mes_key not in MES_MAP:
|
||||
raise ValueError(f"Mes invalido: data={valor_data!r}, mes={valor_mes!r}")
|
||||
return date(ANO, MES_MAP[mes_key], int(dia_txt))
|
||||
|
||||
|
||||
def minutos_do_dia(valor: Any) -> tuple[int, int]:
|
||||
"""Retorna (minutos no dia, deslocamento em dias informado pelo Excel)."""
|
||||
if isinstance(valor, datetime):
|
||||
return valor.hour * 60 + valor.minute, 0
|
||||
if isinstance(valor, time):
|
||||
return valor.hour * 60 + valor.minute, 0
|
||||
if isinstance(valor, timedelta):
|
||||
total_min = int(round(valor.total_seconds() / 60))
|
||||
return total_min % (24 * 60), total_min // (24 * 60)
|
||||
if isinstance(valor, (int, float)):
|
||||
total_min = int(round(float(valor) * 24 * 60))
|
||||
return total_min % (24 * 60), total_min // (24 * 60)
|
||||
|
||||
texto = texto_limpo(valor)
|
||||
if ":" in texto:
|
||||
partes = texto.split(":")
|
||||
return int(partes[0]) * 60 + int(partes[1]), 0
|
||||
raise ValueError(f"Horario invalido: {valor!r}")
|
||||
|
||||
|
||||
def minutos_duracao(valor: Any) -> int:
|
||||
if isinstance(valor, datetime):
|
||||
return valor.hour * 60 + valor.minute
|
||||
if isinstance(valor, time):
|
||||
return valor.hour * 60 + valor.minute
|
||||
if isinstance(valor, timedelta):
|
||||
return int(round(valor.total_seconds() / 60))
|
||||
if isinstance(valor, (int, float)):
|
||||
return int(round(float(valor) * 24 * 60))
|
||||
|
||||
texto = texto_limpo(valor)
|
||||
if ":" in texto:
|
||||
horas, minutos = texto.split(":", 1)
|
||||
return int(horas) * 60 + int(minutos)
|
||||
raise ValueError(f"Duracao invalida: {valor!r}")
|
||||
|
||||
|
||||
def hhmm(minutos: int) -> str:
|
||||
minutos = minutos % (24 * 60)
|
||||
return f"{minutos // 60:02d}:{minutos % 60:02d}"
|
||||
|
||||
|
||||
def duracao_hhmm(minutos: int) -> str:
|
||||
return f"{minutos // 60:02d}:{minutos % 60:02d}"
|
||||
|
||||
|
||||
def iso_utc(data_voo: date, minutos: int, deslocamento_dias: int = 0) -> str:
|
||||
data_hora = datetime.combine(data_voo, time()) + timedelta(
|
||||
days=deslocamento_dias,
|
||||
minutes=minutos,
|
||||
)
|
||||
return data_hora.isoformat(timespec="minutes") + ":00Z"
|
||||
|
||||
|
||||
def atualizar_datas_do_registro(
|
||||
registro: dict[str, Any],
|
||||
partida: datetime,
|
||||
chegada: datetime,
|
||||
) -> None:
|
||||
data_voo = partida.date()
|
||||
registro["data"] = data_voo.strftime("%d/%m")
|
||||
registro["data_iso"] = data_voo.isoformat()
|
||||
registro["mes_numero"] = data_voo.month
|
||||
registro["mes"] = MES_ABBR[data_voo.month]
|
||||
registro["partida_utc"] = partida.isoformat(timespec="minutes").replace("+00:00", "") + ":00Z"
|
||||
registro["chegada_utc"] = chegada.isoformat(timespec="minutes").replace("+00:00", "") + ":00Z"
|
||||
registro["cruza_meia_noite"] = chegada.date() > partida.date()
|
||||
|
||||
|
||||
def id_registro(linha: dict[str, Any]) -> str:
|
||||
om = texto_limpo(linha["om"]) or "SEMOM"
|
||||
etapa = texto_limpo(linha["etapa"]) or "SEMETAPA"
|
||||
return f"RV2025_OM{om}_E{etapa}_L{linha['linha_origem']}"
|
||||
|
||||
|
||||
def linha_tem_voo(ws: openpyxl.worksheet.worksheet.Worksheet, row: int) -> bool:
|
||||
return any(ws.cell(row, col).value not in (None, "") for col in range(1, 13))
|
||||
|
||||
|
||||
def extrair_registros() -> list[dict[str, Any]]:
|
||||
wb = openpyxl.load_workbook(RAW_PATH, data_only=True)
|
||||
ws = wb.active
|
||||
|
||||
registros: list[dict[str, Any]] = []
|
||||
for row in range(4, ws.max_row + 1):
|
||||
if row in LINHAS_ORIGEM_EXCLUIDAS:
|
||||
continue
|
||||
if not linha_tem_voo(ws, row):
|
||||
continue
|
||||
|
||||
data_voo = parse_data(ws.cell(row, 1).value, ws.cell(row, 2).value)
|
||||
mes = normalizar_mes(ws.cell(row, 2).value)
|
||||
dep_min, dep_offset = minutos_do_dia(ws.cell(row, 8).value)
|
||||
arr_min, arr_offset_excel = minutos_do_dia(ws.cell(row, 9).value)
|
||||
arr_offset = max(arr_offset_excel, 1 if arr_min <= dep_min else 0)
|
||||
tempo_min = minutos_duracao(ws.cell(row, 10).value)
|
||||
|
||||
registro = {
|
||||
"linha_origem": row,
|
||||
"data": data_voo.strftime("%d/%m"),
|
||||
"data_iso": data_voo.isoformat(),
|
||||
"mes": mes,
|
||||
"mes_numero": MES_MAP.get(mes, ""),
|
||||
"om": ws.cell(row, 3).value,
|
||||
"aeronave": ws.cell(row, 4).value,
|
||||
"etapa": ws.cell(row, 5).value,
|
||||
"localidade_dep": texto_limpo(ws.cell(row, 6).value).upper(),
|
||||
"localidade_arr": texto_limpo(ws.cell(row, 7).value).upper(),
|
||||
"horario_z_dep": hhmm(dep_min),
|
||||
"horario_z_arr": hhmm(arr_min),
|
||||
"partida_utc": iso_utc(data_voo, dep_min, dep_offset),
|
||||
"chegada_utc": iso_utc(data_voo, arr_min, arr_offset),
|
||||
"tempo_voo": duracao_hhmm(tempo_min),
|
||||
"tempo_voo_min": tempo_min,
|
||||
"segmento": ws.cell(row, 11).value,
|
||||
"pousos": ws.cell(row, 12).value,
|
||||
"missao": texto_limpo(ws.cell(row, 35).value).upper(),
|
||||
"periodo": texto_limpo(ws.cell(row, 36).value).upper(),
|
||||
"cruza_meia_noite": arr_offset > dep_offset,
|
||||
}
|
||||
registro["id_registro"] = id_registro(registro)
|
||||
registros.append(registro)
|
||||
|
||||
corrigir_continuidade_temporal(registros)
|
||||
return registros
|
||||
|
||||
|
||||
def corrigir_continuidade_temporal(registros: list[dict[str, Any]]) -> None:
|
||||
grupos: dict[tuple[str, str], list[dict[str, Any]]] = {}
|
||||
for registro in registros:
|
||||
chave = (texto_limpo(registro["om"]), texto_limpo(registro["aeronave"]))
|
||||
grupos.setdefault(chave, []).append(registro)
|
||||
|
||||
for linhas in grupos.values():
|
||||
linhas.sort(key=lambda registro: registro["linha_origem"])
|
||||
chegada_anterior: datetime | None = None
|
||||
for registro in linhas:
|
||||
partida = parse_iso_utc(registro["partida_utc"])
|
||||
chegada = parse_iso_utc(registro["chegada_utc"])
|
||||
while chegada_anterior is not None and partida < chegada_anterior:
|
||||
partida += timedelta(days=1)
|
||||
chegada += timedelta(days=1)
|
||||
atualizar_datas_do_registro(registro, partida, chegada)
|
||||
chegada_anterior = chegada
|
||||
|
||||
|
||||
def escrever_csv(caminho: Path, colunas: list[str], linhas: list[dict[str, Any]]) -> None:
|
||||
with caminho.open("w", newline="", encoding="utf-8-sig") as arquivo:
|
||||
escritor = csv.DictWriter(arquivo, fieldnames=colunas)
|
||||
escritor.writeheader()
|
||||
escritor.writerows(linhas)
|
||||
|
||||
|
||||
def escrever_xlsx(caminho: Path, colunas: list[str], linhas: list[dict[str, Any]]) -> None:
|
||||
wb = openpyxl.Workbook()
|
||||
ws = wb.active
|
||||
ws.title = "registro_voo_2025"
|
||||
ws.append(colunas)
|
||||
for linha in linhas:
|
||||
ws.append([linha[coluna] for coluna in colunas])
|
||||
ws.freeze_panes = "A2"
|
||||
ws.auto_filter.ref = ws.dimensions
|
||||
for column_cells in ws.columns:
|
||||
letter = column_cells[0].column_letter
|
||||
max_len = max(len(texto_limpo(cell.value)) for cell in column_cells)
|
||||
ws.column_dimensions[letter].width = min(max(max_len + 2, 10), 28)
|
||||
wb.save(caminho)
|
||||
|
||||
|
||||
def preparar_oamrp(registros: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
linhas = []
|
||||
for registro in registros:
|
||||
linhas.append(
|
||||
{
|
||||
"id": registro["id_registro"],
|
||||
"orig": registro["localidade_dep"],
|
||||
"dest": registro["localidade_arr"],
|
||||
"partida_utc": registro["partida_utc"],
|
||||
"chegada_utc": registro["chegada_utc"],
|
||||
"prioridade": 1,
|
||||
}
|
||||
)
|
||||
return linhas
|
||||
|
||||
|
||||
def toca_rota(registro: dict[str, Any]) -> bool:
|
||||
return "ROTA" in {registro["localidade_dep"], registro["localidade_arr"]}
|
||||
|
||||
|
||||
def parse_iso_utc(valor: str) -> datetime:
|
||||
return datetime.fromisoformat(valor.replace("Z", "+00:00"))
|
||||
|
||||
|
||||
def consolidar_grupo(grupo: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
primeiro = grupo[0]
|
||||
ultimo = grupo[-1]
|
||||
tempo_total = sum(int(registro["tempo_voo_min"]) for registro in grupo)
|
||||
pousos_total = sum(int(registro["pousos"] or 0) for registro in grupo)
|
||||
missoes = sorted({registro["missao"] for registro in grupo if registro["missao"]})
|
||||
periodos = sorted({registro["periodo"] for registro in grupo if registro["periodo"]})
|
||||
linhas = [str(registro["linha_origem"]) for registro in grupo]
|
||||
cruza_meia_noite = parse_iso_utc(ultimo["chegada_utc"]).date() > parse_iso_utc(
|
||||
primeiro["partida_utc"]
|
||||
).date()
|
||||
|
||||
return {
|
||||
"id_registro": f"{primeiro['id_registro']}_C{ultimo['linha_origem']}",
|
||||
"linhas_origem": ";".join(linhas),
|
||||
"data": primeiro["data"],
|
||||
"data_iso": primeiro["data_iso"],
|
||||
"mes": primeiro["mes"],
|
||||
"mes_numero": primeiro["mes_numero"],
|
||||
"om": primeiro["om"],
|
||||
"aeronave": primeiro["aeronave"],
|
||||
"etapa_inicio": primeiro["etapa"],
|
||||
"etapa_fim": ultimo["etapa"],
|
||||
"localidade_dep": primeiro["localidade_dep"],
|
||||
"localidade_arr": ultimo["localidade_arr"],
|
||||
"horario_z_dep": primeiro["horario_z_dep"],
|
||||
"horario_z_arr": ultimo["horario_z_arr"],
|
||||
"partida_utc": primeiro["partida_utc"],
|
||||
"chegada_utc": ultimo["chegada_utc"],
|
||||
"tempo_voo": duracao_hhmm(tempo_total),
|
||||
"tempo_voo_min": tempo_total,
|
||||
"segmento_inicio": primeiro["segmento"],
|
||||
"segmento_fim": ultimo["segmento"],
|
||||
"pousos": pousos_total,
|
||||
"missao": ";".join(missoes),
|
||||
"periodo": ";".join(periodos),
|
||||
"cruza_meia_noite": cruza_meia_noite,
|
||||
"qtd_linhas_originais": len(grupo),
|
||||
"tem_rota_intermediaria": any(toca_rota(registro) for registro in grupo),
|
||||
}
|
||||
|
||||
|
||||
def consolidar_transicoes_rota(registros: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
grupos: dict[tuple[str, str], list[dict[str, Any]]] = {}
|
||||
for registro in registros:
|
||||
chave = (texto_limpo(registro["om"]), texto_limpo(registro["aeronave"]))
|
||||
grupos.setdefault(chave, []).append(registro)
|
||||
|
||||
consolidados: list[dict[str, Any]] = []
|
||||
for chave in sorted(grupos):
|
||||
linhas = sorted(
|
||||
grupos[chave],
|
||||
key=lambda registro: (parse_iso_utc(registro["partida_utc"]), registro["linha_origem"]),
|
||||
)
|
||||
idx = 0
|
||||
while idx < len(linhas):
|
||||
atual = linhas[idx]
|
||||
if not toca_rota(atual):
|
||||
consolidados.append(consolidar_grupo([atual]))
|
||||
idx += 1
|
||||
continue
|
||||
|
||||
grupo = [atual]
|
||||
idx += 1
|
||||
while (
|
||||
idx < len(linhas)
|
||||
and toca_rota(linhas[idx])
|
||||
and grupo[-1]["localidade_arr"] == linhas[idx]["localidade_dep"]
|
||||
):
|
||||
grupo.append(linhas[idx])
|
||||
idx += 1
|
||||
if grupo[-1]["localidade_arr"] != "ROTA":
|
||||
break
|
||||
consolidados.append(consolidar_grupo(grupo))
|
||||
|
||||
return sorted(consolidados, key=lambda registro: parse_iso_utc(registro["partida_utc"]))
|
||||
|
||||
|
||||
def preparar_oamrp_consolidado(registros: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
linhas = []
|
||||
for registro in registros:
|
||||
linhas.append(
|
||||
{
|
||||
"id": registro["id_registro"],
|
||||
"orig": registro["localidade_dep"],
|
||||
"dest": registro["localidade_arr"],
|
||||
"partida_utc": registro["partida_utc"],
|
||||
"chegada_utc": registro["chegada_utc"],
|
||||
"prioridade": 1,
|
||||
}
|
||||
)
|
||||
return linhas
|
||||
|
||||
|
||||
def gerar_resumo(
|
||||
registros: list[dict[str, Any]],
|
||||
registros_consolidados: list[dict[str, Any]],
|
||||
) -> dict[str, Any]:
|
||||
faltantes = {
|
||||
coluna: sum(registro[coluna] in (None, "") for registro in registros)
|
||||
for coluna in COLUNAS_REGISTRO
|
||||
}
|
||||
return {
|
||||
"arquivo_origem": str(RAW_PATH.relative_to(BASE_DIR)),
|
||||
"ano_assumido": ANO,
|
||||
"linhas_origem_excluidas": sorted(LINHAS_ORIGEM_EXCLUIDAS),
|
||||
"registros_extraidos": len(registros),
|
||||
"registros_consolidados": len(registros_consolidados),
|
||||
"linhas_com_cruzamento_de_meia_noite": sum(
|
||||
1 for registro in registros if registro["cruza_meia_noite"]
|
||||
),
|
||||
"linhas_com_rota": sum(1 for registro in registros if toca_rota(registro)),
|
||||
"trechos_consolidados_com_rota": sum(
|
||||
1 for registro in registros_consolidados if registro["tem_rota_intermediaria"]
|
||||
),
|
||||
"aeronaves": dict(sorted(Counter(r["aeronave"] for r in registros).items())),
|
||||
"periodos": dict(Counter(r["periodo"] for r in registros)),
|
||||
"meses": dict(Counter(r["mes"] for r in registros)),
|
||||
"campos_faltantes": {k: v for k, v in faltantes.items() if v},
|
||||
"observacoes": [
|
||||
"Colunas REAL ate COM2 descartadas conforme criterio informado.",
|
||||
"Horarios tratados como UTC/Z; nao houve conversao de fuso.",
|
||||
"Chegadas iguais ou anteriores a partida foram deslocadas para o dia seguinte.",
|
||||
"SEGMTO foi preservado da planilha original, sem recalculo.",
|
||||
"Transicoes com ROTA tambem foram consolidadas em arquivo separado.",
|
||||
"Prioridade OAMRP definida como 1 por falta de coluna de prioridade na origem.",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
PREPROCESS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
PROCESSED_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
registros = extrair_registros()
|
||||
registros_consolidados = consolidar_transicoes_rota(registros)
|
||||
escrever_csv(LIMPO_CSV, COLUNAS_REGISTRO, registros)
|
||||
escrever_csv(PROCESSED_CSV, COLUNAS_REGISTRO, registros)
|
||||
escrever_xlsx(LIMPO_XLSX, COLUNAS_REGISTRO, registros)
|
||||
escrever_csv(CONSOLIDADO_CSV, COLUNAS_CONSOLIDADAS, registros_consolidados)
|
||||
escrever_csv(PROCESSED_CONSOLIDADO_CSV, COLUNAS_CONSOLIDADAS, registros_consolidados)
|
||||
escrever_csv(MISSOES_CSV, COLUNAS_OAMRP, preparar_oamrp(registros))
|
||||
escrever_csv(
|
||||
MISSOES_CONSOLIDADAS_CSV,
|
||||
COLUNAS_OAMRP,
|
||||
preparar_oamrp_consolidado(registros_consolidados),
|
||||
)
|
||||
|
||||
with RESUMO_JSON.open("w", encoding="utf-8") as arquivo:
|
||||
json.dump(
|
||||
gerar_resumo(registros, registros_consolidados),
|
||||
arquivo,
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
)
|
||||
arquivo.write("\n")
|
||||
|
||||
print(f"Registros extraidos: {len(registros)}")
|
||||
print(f"Registros consolidados: {len(registros_consolidados)}")
|
||||
print(f"CSV final: {PROCESSED_CSV.relative_to(BASE_DIR)}")
|
||||
print(f"CSV consolidado: {PROCESSED_CONSOLIDADO_CSV.relative_to(BASE_DIR)}")
|
||||
print(f"CSV OAMRP: {MISSOES_CSV.relative_to(BASE_DIR)}")
|
||||
print(f"CSV OAMRP consolidado: {MISSOES_CONSOLIDADAS_CSV.relative_to(BASE_DIR)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
1268
db/pre_process/registro_voo_2025/registro_voo_2025_consolidado.csv
Normal file
1268
db/pre_process/registro_voo_2025/registro_voo_2025_consolidado.csv
Normal file
File diff suppressed because it is too large
Load Diff
1421
db/pre_process/registro_voo_2025/registro_voo_2025_limpo.csv
Normal file
1421
db/pre_process/registro_voo_2025/registro_voo_2025_limpo.csv
Normal file
File diff suppressed because it is too large
Load Diff
BIN
db/pre_process/registro_voo_2025/registro_voo_2025_limpo.xlsx
Normal file
BIN
db/pre_process/registro_voo_2025/registro_voo_2025_limpo.xlsx
Normal file
Binary file not shown.
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"arquivo_origem": "db\\raw\\Planilha 2025.xlsx",
|
||||
"ano_assumido": 2025,
|
||||
"linhas_origem_excluidas": [
|
||||
793
|
||||
],
|
||||
"registros_extraidos": 1420,
|
||||
"registros_consolidados": 1267,
|
||||
"linhas_com_cruzamento_de_meia_noite": 53,
|
||||
"linhas_com_rota": 288,
|
||||
"trechos_consolidados_com_rota": 135,
|
||||
"aeronaves": {
|
||||
"2800": 329,
|
||||
"2803": 513,
|
||||
"2809": 212,
|
||||
"2811": 366
|
||||
},
|
||||
"periodos": {
|
||||
"NOTURNO": 247,
|
||||
"DIURNO": 1145,
|
||||
"NVG": 28
|
||||
},
|
||||
"meses": {
|
||||
"JAN": 69,
|
||||
"FEV": 179,
|
||||
"MAR": 166,
|
||||
"ABR": 184,
|
||||
"MAI": 218,
|
||||
"JUN": 144,
|
||||
"JUL": 106,
|
||||
"AGO": 36,
|
||||
"SET": 160,
|
||||
"OUT": 29,
|
||||
"NOV": 66,
|
||||
"DEZ": 63
|
||||
},
|
||||
"campos_faltantes": {
|
||||
"pousos": 9
|
||||
},
|
||||
"observacoes": [
|
||||
"Colunas REAL ate COM2 descartadas conforme criterio informado.",
|
||||
"Horarios tratados como UTC/Z; nao houve conversao de fuso.",
|
||||
"Chegadas iguais ou anteriores a partida foram deslocadas para o dia seguinte.",
|
||||
"SEGMTO foi preservado da planilha original, sem recalculo.",
|
||||
"Transicoes com ROTA tambem foram consolidadas em arquivo separado.",
|
||||
"Prioridade OAMRP definida como 1 por falta de coluna de prioridade na origem."
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
# Validacao do registro de voo 2025
|
||||
|
||||
Fonte validada: `db/processed/registro_voo_2025_consolidado.csv`
|
||||
|
||||
## Resultado geral
|
||||
|
||||
- Linhas originais limpas: 1420.
|
||||
- Trechos consolidados: 1267.
|
||||
- Linhas originais excluidas: 793.
|
||||
- Trechos com `ROTA` no consolidado: 0.
|
||||
- Transicoes `ROTA` convertidas para origem/destino reais: 135.
|
||||
- Cruzamentos de 00Z preservados: 53.
|
||||
- IDs duplicados: 0.
|
||||
- Divergencias de duracao no registro original: 0.
|
||||
- Trechos consolidados com tempo decorrido menor que tempo de voo: 0.
|
||||
- Sobreposicoes temporais dentro da mesma OM/aeronave: 0.
|
||||
|
||||
## Pendencias encontradas
|
||||
|
||||
### 1. Trecho consolidado com pousos zero
|
||||
|
||||
Trecho consolidado:
|
||||
|
||||
| Campo | Valor |
|
||||
| --- | --- |
|
||||
| ID | RV2025_OM219_E4_L1108_C1109 |
|
||||
| Linhas originais | 1108;1109 |
|
||||
| Data | 01/09 |
|
||||
| OM | 219 |
|
||||
| Aeronave | 2809 |
|
||||
| Trecho | SBCG -> SBCG |
|
||||
| Tempo de voo | 01:50 |
|
||||
| Pousos | 0 |
|
||||
| Missao | 50TT04;50TT05 |
|
||||
| Periodo | NOTURNO |
|
||||
|
||||
Classificacao: verificar se o pouso ficou ausente por estar dividido pela transicao `ROTA` em 00Z.
|
||||
|
||||
### 2. Quebras de continuidade espacial
|
||||
|
||||
Foram encontradas 3 quebras de continuidade espacial dentro da mesma OM/aeronave, sem sobreposicao temporal.
|
||||
|
||||
| OM | Aeronave | Trecho anterior | Trecho seguinte | Hipotese |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 7 | 2809 | SBMN -> SBBV | SBMN -> SBBV | Provavel erro ou duplicidade de origem; nao ha retorno registrado entre as duas etapas. |
|
||||
| 55 | 2803 | SBUA -> SWMK | SWMQ -> SBUA | Provavel divergencia de digitacao entre `SWMK` e `SWMQ`. |
|
||||
| 199 | 2800 | SBBR -> SBBR | SBGL -> SBAN | Lacuna operacional ou etapa ausente entre Brasilia e Galeao. |
|
||||
|
||||
## Decisao de validacao
|
||||
|
||||
O arquivo consolidado esta tecnicamente consistente para um primeiro uso no modelo OAMRP, desde que as 3 quebras espaciais sejam tratadas como excecoes conhecidas ou filtradas em uma rodada inicial.
|
||||
|
||||
Recomendacao para a primeira simulacao:
|
||||
|
||||
- Usar `db/pre_process/registro_voo_2025/missoes_oamrp_2025_consolidado.csv`.
|
||||
- Comecar por janeiro/2025, pois o periodo contem apenas a quebra da OM 7 entre as pendencias de continuidade listadas.
|
||||
- Manter as pendencias acima sem correcao automatica ate confirmacao humana.
|
||||
1421
db/processed/registro_voo_2025.csv
Normal file
1421
db/processed/registro_voo_2025.csv
Normal file
File diff suppressed because it is too large
Load Diff
1268
db/processed/registro_voo_2025_consolidado.csv
Normal file
1268
db/processed/registro_voo_2025_consolidado.csv
Normal file
File diff suppressed because it is too large
Load Diff
BIN
db/raw/Planilha 2025.xlsx
Normal file
BIN
db/raw/Planilha 2025.xlsx
Normal file
Binary file not shown.
@@ -141,3 +141,34 @@ Para reexecutar o pré-processamento:
|
||||
```powershell
|
||||
python db/pre_process/relatorio_ciclo_inspecoes_c105_2805_2026-06-15/preprocess_pdf.py
|
||||
```
|
||||
## Registro de voo 2025
|
||||
|
||||
O arquivo `db/raw/Planilha 2025.xlsx` foi pre-processado por `db/pre_process/registro_voo_2025/preprocess_planilha_2025.py`.
|
||||
|
||||
O processamento gera duas visoes principais:
|
||||
|
||||
- `db/processed/registro_voo_2025.csv`: registro limpo, preservando a granularidade das linhas originais relevantes.
|
||||
- `db/processed/registro_voo_2025_consolidado.csv`: registro consolidado para roteamento, com `ROTA` tratado como marcador de transicao.
|
||||
|
||||
Tambem sao gerados CSVs no formato esperado pelo prototipo OAMRP:
|
||||
|
||||
```text
|
||||
db/pre_process/registro_voo_2025/missoes_oamrp_2025.csv
|
||||
db/pre_process/registro_voo_2025/missoes_oamrp_2025_consolidado.csv
|
||||
```
|
||||
|
||||
Criterios aplicados:
|
||||
|
||||
- colunas entre `REAL` e `COM2` descartadas;
|
||||
- datas de 2025 normalizadas para formato ISO e `dd/mm`;
|
||||
- horarios tratados como UTC/Z;
|
||||
- chegadas iguais ou anteriores a partida deslocadas para o dia seguinte;
|
||||
- transicoes com `ROTA` consolidadas em origem/destino reais;
|
||||
- linha original 793, aeronave 2806, excluida dos artefatos processados;
|
||||
- `SEGMTO` preservado como campo original, sem recalculo automatico.
|
||||
|
||||
O relatorio de validacao esta em:
|
||||
|
||||
```text
|
||||
db/pre_process/registro_voo_2025/validacao_registro_voo_2025.md
|
||||
```
|
||||
|
||||
@@ -59,3 +59,30 @@ Formato:
|
||||
|
||||
- O parser atual foi calibrado para o primeiro PDF processado.
|
||||
- A grafia original do documento fonte foi preservada, inclusive ocorrências de `INPEÇÃO`.
|
||||
## v0.3 - VTO - 2026-06
|
||||
|
||||
### Adicionado
|
||||
|
||||
- Pre-processamento da `Planilha 2025.xlsx` para registro de voo limpo.
|
||||
- Saidas em CSV/XLSX sem mesclas e CSV OAMRP com `id,orig,dest,partida_utc,chegada_utc,prioridade`.
|
||||
- Resumo JSON com contagens, campos faltantes e criterios de conversao.
|
||||
|
||||
### Alterado
|
||||
|
||||
- Criado arquivo final `db/processed/registro_voo_2025.csv`.
|
||||
|
||||
### Observacoes
|
||||
|
||||
- `SEGMTO` foi preservado da planilha original; cruzamento de 00Z foi registrado em campo separado.
|
||||
- Colunas entre `REAL` e `COM2` foram descartadas.
|
||||
## v0.3.1 - VTO - 2026-06
|
||||
|
||||
### Alterado
|
||||
|
||||
- Registro de voo 2025 regenerado removendo a linha original 793, aeronave 2806.
|
||||
- Consolidado passou para 1267 trechos e registro limpo para 1420 linhas.
|
||||
- Documentacao atualizada em `README.md`, `docs/about.md`, `LOG.md` e relatorio de validacao.
|
||||
|
||||
### Observacoes
|
||||
|
||||
- A remocao foi aplicada no script de pre-processamento para manter reprodutibilidade.
|
||||
|
||||
102
docs/roteiro_apresentacao_oamrp.doc
Normal file
102
docs/roteiro_apresentacao_oamrp.doc
Normal file
@@ -0,0 +1,102 @@
|
||||
{\rtf1\ansi\deff0
|
||||
{\fonttbl{\f0 Arial;}{\f1 Courier New;}}
|
||||
\fs24
|
||||
\b Roteiro para Apresentacao PowerPoint - OAMRP v1\b0\par
|
||||
\par
|
||||
\b Tema:\b0 Aircraft Routing - Esquadrao Arara (Transporte C-95/C-105)\par
|
||||
\b Objetivo da apresentacao:\b0 apresentar uma primeira ideia de modelo matematico/computacional para apoiar a atribuicao de aeronaves a missoes de transporte, considerando prioridade operacional, continuidade de rota, tempo minimo em solo e limite de horas ate inspecao.\par
|
||||
\par
|
||||
\b Ideia inicial 1 - Modelo MIP compacto para atribuicao de aeronaves\b0\par
|
||||
\par
|
||||
Esta primeira ideia usa um modelo de programacao inteira mista (MIP) para escolher quais missoes serao cumpridas e por qual aeronave. Nesta versao, Manaus (MN) foi definida como base operacional do Esquadrao Arara, e as missoes simuladas partem de MN para outras localidades representativas da malha da FAB. As missoes sao informadas com data e hora UTC, como em uma planilha operacional, e o codigo converte internamente para horas decimais no horizonte de planejamento. O foco nao e minimizar custo, como em modelos civis tradicionais, mas maximizar o cumprimento ponderado de missoes, dando maior peso as missoes de maior prioridade.\par
|
||||
\par
|
||||
O modelo trata cada missao como opcional. Isso significa que uma missao pode ficar de fora quando nao houver aeronave disponivel, quando a conexao temporal/geografica nao for viavel, ou quando o limite de horas de celula ate a proxima inspecao impedir a execucao segura da rota.\par
|
||||
\par
|
||||
\b Estrutura sugerida dos slides\b0\par
|
||||
\par
|
||||
\b Slide 1 - Titulo\b0\par
|
||||
Titulo sugerido: Otimizacao de Rotas e Atribuicao de Aeronaves no Esquadrao Arara.\par
|
||||
Fala sugerida: Esta apresentacao mostra uma primeira proposta de ferramenta de apoio a decisao para distribuir aeronaves C-95/C-105 entre missoes de transporte, respeitando restricoes operacionais e de manutencao.\par
|
||||
\par
|
||||
\b Slide 2 - Problema operacional\b0\par
|
||||
Mensagem central: o esquadrao precisa decidir rapidamente quais aeronaves atendem quais missoes, mantendo coerencia de rota, disponibilidade temporal e margem de horas antes da inspecao.\par
|
||||
Pontos para apresentar:\par
|
||||
- Ha varias missoes com origens, destinos, horarios e prioridades diferentes.\par
|
||||
- Cada aeronave parte de Manaus (MN), base operacional do Arara nesta simulacao, e possui horas acumuladas desde a ultima inspecao.\par
|
||||
- Nem toda combinacao de missoes e viavel para uma mesma aeronave.\par
|
||||
\par
|
||||
\b Slide 3 - Objetivo do modelo\b0\par
|
||||
Mensagem central: maximizar o valor operacional das missoes cumpridas e, em segundo nivel, organizar o uso do LRT para apoiar a manutencao.\par
|
||||
Fala sugerida: Em vez de obrigar que todas as missoes sejam atendidas, o modelo escolhe o melhor conjunto possivel. Missoes mais importantes recebem maior peso. Depois de encontrar a melhor prioridade operacional, o modelo escolhe a distribuicao que reduz o LRT restante ponderado, priorizando aeronaves mais proximas da manutencao.\par
|
||||
\par
|
||||
\b Slide 4 - Entradas do modelo\b0\par
|
||||
Entradas usadas na versao inicial:\par
|
||||
- Base operacional simulada: MN, Manaus.\par
|
||||
- Base habilitada a inspecao: MN, Manaus.\par
|
||||
- Turnaround minimo: 1 hora.\par
|
||||
- Limite de horas ate inspecao obrigatoria: 100 horas.\par
|
||||
- Aeronaves: FAB-2301, FAB-2302, FAB-2303 e FAB-2304.\par
|
||||
- Missoes: 20 missoes simuladas entre Manaus e outras localidades, incluindo BR, BE, PV, BV, GL, CG, RF e NT.\par
|
||||
- Horarios das missoes: entrada em data/hora UTC, com possibilidade de carregar CSV exportado de planilha.\par
|
||||
\par
|
||||
\b Slide 5 - Restricoes consideradas\b0\par
|
||||
Restricoes principais:\par
|
||||
- Cada missao pode ser cumprida por no maximo uma aeronave.\par
|
||||
- A aeronave so pode iniciar uma rota em uma missao que parte de sua base inicial.\par
|
||||
- A continuidade da rota exige que o destino de uma missao seja a origem da proxima.\par
|
||||
- Deve haver turnaround minimo entre chegada e nova partida.\par
|
||||
- A soma das horas voadas por aeronave nao pode ultrapassar o LRT disponivel.\par
|
||||
\par
|
||||
\b Slide 6 - Resultado da execucao inicial\b0\par
|
||||
Resultado obtido ao rodar o arquivo software/oamrp_v1.py:\par
|
||||
\par
|
||||
{\f1
|
||||
STATUS: Optimal\par
|
||||
Missoes cumpridas: 20/20\par
|
||||
Prioridade obtida: 74/74\par
|
||||
Todas as missoes foram atendidas\par
|
||||
Horizonte inicia em UTC: 2026-07-01T06:00:00Z\par
|
||||
LRT restante total: 124h\par
|
||||
Criterio secundario: menor LRT restante ponderado para manutencao\par
|
||||
}\f0
|
||||
\par
|
||||
Fala sugerida: Com os dados de exemplo, o solver encontrou uma solucao otima. As 20 missoes simuladas foram cumpridas, com aproveitamento integral da prioridade operacional planejada. Em seguida, a solucao foi refinada para usar melhor o LRT das aeronaves mais proximas da manutencao.\par
|
||||
\par
|
||||
\b Slide 7 - Distribuicao das aeronaves\b0\par
|
||||
Rotas encontradas:\par
|
||||
\par
|
||||
{\f1
|
||||
FAB-2301: M07 -> M08\par
|
||||
FAB-2302: M01 -> M02 -> M11 -> M12 -> M13 -> M14 -> M19 -> M20\par
|
||||
FAB-2303: M05 -> M06 -> M09 -> M10 -> M15 -> M16 -> M17 -> M18\par
|
||||
FAB-2304: M03 -> M04\par
|
||||
}\f0
|
||||
\par
|
||||
Fala sugerida: O resultado mostra uma sequencia coerente para cada aeronave. Cada rota respeita origem, destino, horarios e limite de horas disponiveis ate a inspecao.\par
|
||||
\par
|
||||
\b Slide 8 - Leitura operacional do resultado\b0\par
|
||||
Mensagem central: a malha simulada mostra que a frota consegue cumprir o pacote completo de missoes, mas algumas aeronaves ficam mais pressionadas pelo limite de horas.\par
|
||||
Pontos para apresentar:\par
|
||||
- A prioridade total possivel era 74.\par
|
||||
- A solucao atingiu 74 pontos de prioridade.\par
|
||||
- Todas as missoes foram cumpridas.\par
|
||||
- A FAB-2302 e a FAB-2304 ficaram com LRT restante igual a 0 hora, mostrando aeronaves prontas para entrar no ciclo de manutencao.\par
|
||||
\par
|
||||
\b Slide 9 - Valor para o planejamento\b0\par
|
||||
Beneficios da abordagem:\par
|
||||
- Apoia decisoes rapidas e rastreaveis.\par
|
||||
- Ajuda a visualizar gargalos de frota, horario e manutencao.\par
|
||||
- Permite testar cenarios com novas missoes, prioridades ou disponibilidade de aeronaves.\par
|
||||
- Reduz a dependencia de avaliacao manual quando ha muitas combinacoes possiveis.\par
|
||||
\par
|
||||
\b Slide 10 - Proximos passos\b0\par
|
||||
Evolucoes sugeridas:\par
|
||||
- Substituir os dados ficticios por dados reais do Esquadrao Arara.\par
|
||||
- Incluir pousos intermediarios, pernoites e janelas de disponibilidade de tripulacao.\par
|
||||
- Modelar inspecao com reset de horas em bases habilitadas.\par
|
||||
- Gerar graficos e tabelas automaticas para uso direto em briefing.\par
|
||||
- Comparar cenarios: frota completa, aeronave indisponivel, aumento de demanda ou restricao de base.\par
|
||||
\par
|
||||
\b Fechamento sugerido\b0\par
|
||||
Esta versao inicial demonstra que e possivel transformar o problema de atribuicao de aeronaves em um modelo de otimizacao explicavel. Mesmo com dados simples, o modelo ja produz uma solucao coerente, priorizada e aderente as restricoes principais de operacao e manutencao.\par
|
||||
}
|
||||
@@ -21,6 +21,10 @@
|
||||
============================================================================
|
||||
"""
|
||||
|
||||
import csv
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
import pulp
|
||||
|
||||
# ===========================================================================
|
||||
@@ -28,7 +32,8 @@ import pulp
|
||||
# ===========================================================================
|
||||
# Bases (codigos curtos ficticios). 'S' marca bases HABILITADAS a inspecao.
|
||||
# Na FAB o conjunto de bases habilitadas e' restrito -> isso aperta o routing.
|
||||
BASES_INSPECAO = {"BR", "GL"} # ex.: Brasilia (hub do esq.) e Galeao
|
||||
# MN representa Manaus, base operacional do Esquadrao Arara nesta simulacao.
|
||||
BASES_INSPECAO = {"MN"} # Manaus
|
||||
|
||||
TURNAROUND_MIN_H = 1.0 # tempo minimo em solo entre 2 missoes (h)
|
||||
LIMITE_HORAS_F = 100.0 # horas de celula ate inspecao obrigatoria
|
||||
@@ -38,31 +43,85 @@ LIMITE_HORAS_F = 100.0 # horas de celula ate inspecao obrigatori
|
||||
# 'horas_acum' = f_k. LRT inicial de cada aeronave = LIMITE_HORAS_F - f_k.
|
||||
AERONAVES = [
|
||||
# id base horas_acum
|
||||
("FAB-2301", "BR", 12.0),
|
||||
("FAB-2302", "BR", 71.0), # ja gastou muito -> pouco LRT disponivel
|
||||
("FAB-2303", "GL", 30.0),
|
||||
("FAB-2304", "BR", 95.0), # quase no limite -> mal consegue voar
|
||||
("FAB-2301", "MN", 12.0),
|
||||
("FAB-2302", "MN", 71.0), # ja gastou muito -> pouco LRT disponivel
|
||||
("FAB-2303", "MN", 30.0),
|
||||
("FAB-2304", "MN", 95.0), # quase no limite -> mal consegue voar
|
||||
]
|
||||
|
||||
# ----- Missoes de transporte: (id, origem, destino, partida_h, chegada_h, prioridade)
|
||||
# partida/chegada em horas decimais dentro do horizonte (ex.: 0..72 = 3 dias).
|
||||
# prioridade: peso da missao (missao critica pesa mais).
|
||||
MISSOES = [
|
||||
# id orig dest part cheg prio
|
||||
("M01", "BR", "MN", 6.0, 10.0, 5), # logistica Amazonia (alta prio)
|
||||
("M02", "MN", "BR", 12.0, 16.0, 5),
|
||||
("M03", "BR", "RF", 7.0, 10.0, 3),
|
||||
("M04", "RF", "BR", 11.5, 14.5, 3),
|
||||
("M05", "BR", "CG", 8.0, 10.0, 2),
|
||||
("M06", "CG", "BR", 11.0, 13.0, 2),
|
||||
("M07", "GL", "BR", 6.5, 8.0, 4),
|
||||
("M08", "BR", "BE", 9.0, 12.5, 4), # Belem
|
||||
("M09", "BE", "BR", 14.0, 17.5, 4),
|
||||
("M10", "BR", "GL", 15.0, 16.5, 1),
|
||||
("M11", "BR", "MN", 30.0, 34.0, 5), # dia 2
|
||||
("M12", "MN", "BR", 36.0, 40.0, 5),
|
||||
# ----- Missoes de transporte
|
||||
# Entrada recomendada: data/hora UTC, como viria de uma planilha.
|
||||
# Para usar uma planilha, exporte para CSV com as colunas:
|
||||
# id,orig,dest,partida_utc,chegada_utc,prioridade
|
||||
# e preencha CSV_MISSOES_PATH com o caminho do arquivo.
|
||||
CSV_MISSOES_PATH = None
|
||||
|
||||
MISSOES_UTC = [
|
||||
# id orig dest partida UTC chegada UTC prio
|
||||
("M01", "MN", "BR", "2026-07-01T06:00Z", "2026-07-01T10:00Z", 5),
|
||||
("M02", "BR", "MN", "2026-07-01T12:00Z", "2026-07-01T16:00Z", 5),
|
||||
("M03", "MN", "BE", "2026-07-01T06:30Z", "2026-07-01T09:00Z", 4),
|
||||
("M04", "BE", "MN", "2026-07-01T11:00Z", "2026-07-01T13:30Z", 4),
|
||||
("M05", "MN", "PV", "2026-07-01T07:00Z", "2026-07-01T09:00Z", 3),
|
||||
("M06", "PV", "MN", "2026-07-01T10:30Z", "2026-07-01T12:30Z", 3),
|
||||
("M07", "MN", "BV", "2026-07-01T08:00Z", "2026-07-01T09:30Z", 3),
|
||||
("M08", "BV", "MN", "2026-07-01T11:00Z", "2026-07-01T12:30Z", 3),
|
||||
("M09", "MN", "GL", "2026-07-01T18:00Z", "2026-07-01T23:00Z", 4),
|
||||
("M10", "GL", "MN", "2026-07-02T01:00Z", "2026-07-02T06:00Z", 4),
|
||||
("M11", "MN", "CG", "2026-07-01T20:00Z", "2026-07-01T23:00Z", 2),
|
||||
("M12", "CG", "MN", "2026-07-02T01:00Z", "2026-07-02T04:00Z", 2),
|
||||
("M13", "MN", "RF", "2026-07-02T06:00Z", "2026-07-02T11:00Z", 4),
|
||||
("M14", "RF", "MN", "2026-07-02T13:00Z", "2026-07-02T18:00Z", 4),
|
||||
("M15", "MN", "NT", "2026-07-02T08:00Z", "2026-07-02T12:30Z", 3),
|
||||
("M16", "NT", "MN", "2026-07-02T14:00Z", "2026-07-02T18:30Z", 3),
|
||||
("M17", "MN", "BR", "2026-07-03T00:00Z", "2026-07-03T04:00Z", 5),
|
||||
("M18", "BR", "MN", "2026-07-03T06:00Z", "2026-07-03T10:00Z", 5),
|
||||
("M19", "MN", "BE", "2026-07-03T02:00Z", "2026-07-03T04:30Z", 4),
|
||||
("M20", "BE", "MN", "2026-07-03T06:00Z", "2026-07-03T08:30Z", 4),
|
||||
]
|
||||
|
||||
def parse_utc(valor):
|
||||
"""Converte texto ISO UTC da planilha para datetime com timezone."""
|
||||
texto = str(valor).strip()
|
||||
if texto.endswith("Z"):
|
||||
texto = texto[:-1] + "+00:00"
|
||||
dt = datetime.fromisoformat(texto)
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.replace(tzinfo=timezone.utc)
|
||||
return dt.astimezone(timezone.utc)
|
||||
|
||||
def carregar_missoes_csv(caminho):
|
||||
with Path(caminho).open(newline="", encoding="utf-8-sig") as arq:
|
||||
leitor = csv.DictReader(arq)
|
||||
return [
|
||||
(
|
||||
linha["id"],
|
||||
linha["orig"],
|
||||
linha["dest"],
|
||||
linha["partida_utc"],
|
||||
linha["chegada_utc"],
|
||||
int(linha["prioridade"]),
|
||||
)
|
||||
for linha in leitor
|
||||
]
|
||||
|
||||
def converter_missoes_para_horas(missoes_utc):
|
||||
datas_partida = [parse_utc(m[3]) for m in missoes_utc]
|
||||
horizonte_inicio = min(datas_partida)
|
||||
missoes_horas = []
|
||||
for mid, orig, dest, partida_utc, chegada_utc, prio in missoes_utc:
|
||||
partida = parse_utc(partida_utc)
|
||||
chegada = parse_utc(chegada_utc)
|
||||
if chegada <= partida:
|
||||
raise ValueError(f"Missao {mid}: chegada_utc deve ser depois de partida_utc")
|
||||
part_h = (partida - horizonte_inicio).total_seconds() / 3600
|
||||
cheg_h = (chegada - horizonte_inicio).total_seconds() / 3600
|
||||
missoes_horas.append((mid, orig, dest, part_h, cheg_h, prio))
|
||||
return missoes_horas, horizonte_inicio
|
||||
|
||||
MISSOES_FONTE = carregar_missoes_csv(CSV_MISSOES_PATH) if CSV_MISSOES_PATH else MISSOES_UTC
|
||||
MISSOES, HORIZONTE_INICIO_UTC = converter_missoes_para_horas(MISSOES_FONTE)
|
||||
|
||||
# ===========================================================================
|
||||
# 2) PRE-PROCESSAMENTO (gera conexoes viaveis)
|
||||
# ===========================================================================
|
||||
@@ -95,7 +154,7 @@ c = {(k, i, j): pulp.LpVariable(f"c_{k}_{i}_{j}", cat="Binary") # k voa i de
|
||||
for k in KS for (i, j) in conex}
|
||||
z = {i: pulp.LpVariable(f"z_{i}", cat="Binary") for i in IDS} # missao coberta
|
||||
|
||||
# --- Objetivo: maximizar prioridade total das missoes cumpridas
|
||||
# --- Objetivo primario: maximizar prioridade total das missoes cumpridas
|
||||
mdl += pulp.lpSum(miss[i]["prio"] * z[i] for i in IDS), "Missoes_cumpridas_ponderadas"
|
||||
|
||||
# --- (R1) Cobertura: cada missao por NO MAXIMO uma aeronave
|
||||
@@ -133,6 +192,24 @@ for k in KS:
|
||||
# 4) RESOLVER
|
||||
# ===========================================================================
|
||||
solver = pulp.PULP_CBC_CMD(msg=False) # troque por HiGHS se instalado
|
||||
|
||||
# Etapa 1: maximiza o cumprimento ponderado das missoes.
|
||||
mdl.solve(solver)
|
||||
prio_otima = int(pulp.value(mdl.objective))
|
||||
|
||||
# Etapa 2: mantendo a prioridade otima, minimiza o LRT restante ponderado.
|
||||
# Como as horas totais voadas podem ser constantes quando todas as missoes sao
|
||||
# cumpridas, o peso prioriza consumir LRT das aeronaves mais proximas da
|
||||
# manutencao, sem sacrificar nenhuma missao prioritaria da primeira etapa.
|
||||
lrt_restante = {
|
||||
k: (LIMITE_HORAS_F - acft[k]["f"]) -
|
||||
pulp.lpSum(miss[i]["dur"] * x[(k, i)] for i in IDS)
|
||||
for k in KS
|
||||
}
|
||||
pesos_lrt = {k: 1 / (LIMITE_HORAS_F - acft[k]["f"]) for k in KS}
|
||||
mdl += pulp.lpSum(miss[i]["prio"] * z[i] for i in IDS) == prio_otima, "fixa_prioridade_otima"
|
||||
mdl.sense = pulp.LpMinimize
|
||||
mdl.setObjective(pulp.lpSum(pesos_lrt[k] * lrt_restante[k] for k in KS))
|
||||
mdl.solve(solver)
|
||||
|
||||
# ===========================================================================
|
||||
@@ -140,11 +217,17 @@ mdl.solve(solver)
|
||||
# ===========================================================================
|
||||
print("=" * 70)
|
||||
print(f" STATUS: {pulp.LpStatus[mdl.status]}")
|
||||
print(f" Horizonte inicia em UTC: {HORIZONTE_INICIO_UTC.isoformat().replace('+00:00', 'Z')}")
|
||||
prio_tot = sum(miss[i]["prio"] for i in IDS)
|
||||
prio_obt = int(pulp.value(mdl.objective))
|
||||
prio_obt = prio_otima
|
||||
cob = [i for i in IDS if z[i].value() and z[i].value() > 0.5]
|
||||
print(f" Missoes cumpridas: {len(cob)}/{len(IDS)} "
|
||||
f"| Prioridade obtida: {prio_obt}/{prio_tot}")
|
||||
lrt_total = sum((LIMITE_HORAS_F - acft[k]["f"]) -
|
||||
sum(miss[i]["dur"] * x[(k, i)].value() for i in IDS)
|
||||
for k in KS)
|
||||
print(f" LRT restante total: {lrt_total:g}h")
|
||||
print(" Criterio secundario: menor LRT restante ponderado p/ manutencao")
|
||||
print("=" * 70)
|
||||
|
||||
def rota_da_aeronave(k):
|
||||
@@ -185,4 +268,4 @@ if descobertas:
|
||||
# NOTA - usar HiGHS (recomendado p/ instancias maiores, sem licenca):
|
||||
# pip install highspy
|
||||
# solver = pulp.HiGHS(msg=False) # PuLP 3.x expoe HiGHS nativo
|
||||
# ---------------------------------------------------------------------------
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user