Processa registro de voo 2025

This commit is contained in:
Cesa-V
2026-06-15 20:22:57 -03:00
parent 8d7dd07b18
commit 11821eaa93
17 changed files with 8973 additions and 25 deletions

4
LOG.md
View File

@@ -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: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 | 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: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. |

View File

@@ -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. 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.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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()

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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."
]
}

View File

@@ -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.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

BIN
db/raw/Planilha 2025.xlsx Normal file

Binary file not shown.

View File

@@ -141,3 +141,34 @@ Para reexecutar o pré-processamento:
```powershell ```powershell
python db/pre_process/relatorio_ciclo_inspecoes_c105_2805_2026-06-15/preprocess_pdf.py 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
```

View File

@@ -59,3 +59,30 @@ Formato:
- O parser atual foi calibrado para o primeiro PDF processado. - O parser atual foi calibrado para o primeiro PDF processado.
- A grafia original do documento fonte foi preservada, inclusive ocorrências de `INPEÇÃO`. - 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.

View 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
}

View File

@@ -21,6 +21,10 @@
============================================================================ ============================================================================
""" """
import csv
from datetime import datetime, timezone
from pathlib import Path
import pulp import pulp
# =========================================================================== # ===========================================================================
@@ -28,7 +32,8 @@ import pulp
# =========================================================================== # ===========================================================================
# Bases (codigos curtos ficticios). 'S' marca bases HABILITADAS a inspecao. # Bases (codigos curtos ficticios). 'S' marca bases HABILITADAS a inspecao.
# Na FAB o conjunto de bases habilitadas e' restrito -> isso aperta o routing. # 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) TURNAROUND_MIN_H = 1.0 # tempo minimo em solo entre 2 missoes (h)
LIMITE_HORAS_F = 100.0 # horas de celula ate inspecao obrigatoria 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. # 'horas_acum' = f_k. LRT inicial de cada aeronave = LIMITE_HORAS_F - f_k.
AERONAVES = [ AERONAVES = [
# id base horas_acum # id base horas_acum
("FAB-2301", "BR", 12.0), ("FAB-2301", "MN", 12.0),
("FAB-2302", "BR", 71.0), # ja gastou muito -> pouco LRT disponivel ("FAB-2302", "MN", 71.0), # ja gastou muito -> pouco LRT disponivel
("FAB-2303", "GL", 30.0), ("FAB-2303", "MN", 30.0),
("FAB-2304", "BR", 95.0), # quase no limite -> mal consegue voar ("FAB-2304", "MN", 95.0), # quase no limite -> mal consegue voar
] ]
# ----- Missoes de transporte: (id, origem, destino, partida_h, chegada_h, prioridade) # ----- Missoes de transporte
# partida/chegada em horas decimais dentro do horizonte (ex.: 0..72 = 3 dias). # Entrada recomendada: data/hora UTC, como viria de uma planilha.
# prioridade: peso da missao (missao critica pesa mais). # Para usar uma planilha, exporte para CSV com as colunas:
MISSOES = [ # id,orig,dest,partida_utc,chegada_utc,prioridade
# id orig dest part cheg prio # e preencha CSV_MISSOES_PATH com o caminho do arquivo.
("M01", "BR", "MN", 6.0, 10.0, 5), # logistica Amazonia (alta prio) CSV_MISSOES_PATH = None
("M02", "MN", "BR", 12.0, 16.0, 5),
("M03", "BR", "RF", 7.0, 10.0, 3), MISSOES_UTC = [
("M04", "RF", "BR", 11.5, 14.5, 3), # id orig dest partida UTC chegada UTC prio
("M05", "BR", "CG", 8.0, 10.0, 2), ("M01", "MN", "BR", "2026-07-01T06:00Z", "2026-07-01T10:00Z", 5),
("M06", "CG", "BR", 11.0, 13.0, 2), ("M02", "BR", "MN", "2026-07-01T12:00Z", "2026-07-01T16:00Z", 5),
("M07", "GL", "BR", 6.5, 8.0, 4), ("M03", "MN", "BE", "2026-07-01T06:30Z", "2026-07-01T09:00Z", 4),
("M08", "BR", "BE", 9.0, 12.5, 4), # Belem ("M04", "BE", "MN", "2026-07-01T11:00Z", "2026-07-01T13:30Z", 4),
("M09", "BE", "BR", 14.0, 17.5, 4), ("M05", "MN", "PV", "2026-07-01T07:00Z", "2026-07-01T09:00Z", 3),
("M10", "BR", "GL", 15.0, 16.5, 1), ("M06", "PV", "MN", "2026-07-01T10:30Z", "2026-07-01T12:30Z", 3),
("M11", "BR", "MN", 30.0, 34.0, 5), # dia 2 ("M07", "MN", "BV", "2026-07-01T08:00Z", "2026-07-01T09:30Z", 3),
("M12", "MN", "BR", 36.0, 40.0, 5), ("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) # 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} for k in KS for (i, j) in conex}
z = {i: pulp.LpVariable(f"z_{i}", cat="Binary") for i in IDS} # missao coberta 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" mdl += pulp.lpSum(miss[i]["prio"] * z[i] for i in IDS), "Missoes_cumpridas_ponderadas"
# --- (R1) Cobertura: cada missao por NO MAXIMO uma aeronave # --- (R1) Cobertura: cada missao por NO MAXIMO uma aeronave
@@ -133,6 +192,24 @@ for k in KS:
# 4) RESOLVER # 4) RESOLVER
# =========================================================================== # ===========================================================================
solver = pulp.PULP_CBC_CMD(msg=False) # troque por HiGHS se instalado 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) mdl.solve(solver)
# =========================================================================== # ===========================================================================
@@ -140,11 +217,17 @@ mdl.solve(solver)
# =========================================================================== # ===========================================================================
print("=" * 70) print("=" * 70)
print(f" STATUS: {pulp.LpStatus[mdl.status]}") 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_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] cob = [i for i in IDS if z[i].value() and z[i].value() > 0.5]
print(f" Missoes cumpridas: {len(cob)}/{len(IDS)} " print(f" Missoes cumpridas: {len(cob)}/{len(IDS)} "
f"| Prioridade obtida: {prio_obt}/{prio_tot}") 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) print("=" * 70)
def rota_da_aeronave(k): def rota_da_aeronave(k):