Adiciona missoes sinteticas, ferry (Fase 3) e consolida versoes anteriores
- Gerador sintetico cross-base (50 missoes, 10 bases, haversine/C-105) - Fase 3: arcos de ferry em construir_rede(); ferry consome horas de celula - oamrp_v2.py e resultados CSV anteriores adicionados ao repositorio - README, docs/about.md e changelog atualizados ate v0.6 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
375
software/oamrp_v2.py
Normal file
375
software/oamrp_v2.py
Normal file
@@ -0,0 +1,375 @@
|
||||
"""
|
||||
============================================================================
|
||||
AIRCRAFT ROUTING - ESQUADRAO ARARA - V2
|
||||
============================================================================
|
||||
Primeira versao do modelo OAMRP usando dados reais pre-processados do
|
||||
registro de voo 2025.
|
||||
|
||||
Diferencas em relacao ao v1:
|
||||
- le o CSV consolidado gerado a partir da Planilha 2025;
|
||||
- filtra uma janela de datas, com janeiro/2025 como padrao;
|
||||
- usa aeronaves reais observadas no registro: 2800, 2803, 2809 e 2811;
|
||||
- gera um CSV de resultado em db/processed/.
|
||||
|
||||
Esta versao ainda e uma rodada exploratoria. Todas as missoes recebem
|
||||
prioridade 1 e a base de manutencao considerada e SBMN.
|
||||
============================================================================
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
import pulp
|
||||
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parents[1]
|
||||
CSV_REGISTRO_PADRAO = BASE_DIR / "db" / "processed" / "registro_voo_2025_consolidado.csv"
|
||||
CSV_RESULTADO_PADRAO = BASE_DIR / "db" / "processed" / "resultado_oamrp_jan_2025.csv"
|
||||
|
||||
DATA_INICIO_PADRAO = "2025-01-01"
|
||||
DATA_FIM_PADRAO = "2025-02-01" # exclusivo
|
||||
|
||||
BASE_MANUTENCAO = "SBMN"
|
||||
BASE_INICIAL = BASE_MANUTENCAO
|
||||
TURNAROUND_MIN_H = 1.0
|
||||
LIMITE_HORAS_F = 300.0
|
||||
PRIORIDADE_PADRAO = 1
|
||||
|
||||
AERONAVES = [
|
||||
# id base inicial horas acumuladas desde ultima inspecao
|
||||
("2800", BASE_INICIAL, 0.0),
|
||||
("2803", BASE_INICIAL, 0.0),
|
||||
("2809", BASE_INICIAL, 0.0),
|
||||
("2811", BASE_INICIAL, 0.0),
|
||||
]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Missao:
|
||||
id: str
|
||||
orig: str
|
||||
dest: str
|
||||
partida_utc: datetime
|
||||
chegada_utc: datetime
|
||||
prioridade: int
|
||||
periodo: str
|
||||
linhas_origem: str
|
||||
aeronave_real: str
|
||||
om: str
|
||||
|
||||
@property
|
||||
def dur_h(self) -> float:
|
||||
return (self.chegada_utc - self.partida_utc).total_seconds() / 3600
|
||||
|
||||
|
||||
def parse_utc(valor: str) -> datetime:
|
||||
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 parse_data_inicio(valor: str) -> datetime:
|
||||
return datetime.fromisoformat(valor).replace(tzinfo=timezone.utc)
|
||||
|
||||
|
||||
def carregar_missoes(caminho: Path, inicio: datetime, fim: datetime) -> list[Missao]:
|
||||
missoes: list[Missao] = []
|
||||
with caminho.open(newline="", encoding="utf-8-sig") as arquivo:
|
||||
leitor = csv.DictReader(arquivo)
|
||||
for linha in leitor:
|
||||
partida = parse_utc(linha["partida_utc"])
|
||||
chegada = parse_utc(linha["chegada_utc"])
|
||||
if not (inicio <= partida < fim):
|
||||
continue
|
||||
if chegada <= partida:
|
||||
raise ValueError(f"Missao {linha['id_registro']}: chegada antes da partida")
|
||||
missoes.append(
|
||||
Missao(
|
||||
id=linha["id_registro"],
|
||||
orig=linha["localidade_dep"],
|
||||
dest=linha["localidade_arr"],
|
||||
partida_utc=partida,
|
||||
chegada_utc=chegada,
|
||||
prioridade=PRIORIDADE_PADRAO,
|
||||
periodo=linha.get("periodo", ""),
|
||||
linhas_origem=linha.get("linhas_origem", ""),
|
||||
aeronave_real=linha.get("aeronave", ""),
|
||||
om=linha.get("om", ""),
|
||||
)
|
||||
)
|
||||
return sorted(missoes, key=lambda m: (m.partida_utc, m.id))
|
||||
|
||||
|
||||
def horas_relativas(missoes: list[Missao]) -> tuple[dict[str, dict[str, object]], datetime]:
|
||||
horizonte_inicio = min(m.partida_utc for m in missoes)
|
||||
dados = {}
|
||||
for missao in missoes:
|
||||
dados[missao.id] = {
|
||||
"orig": missao.orig,
|
||||
"dest": missao.dest,
|
||||
"part": (missao.partida_utc - horizonte_inicio).total_seconds() / 3600,
|
||||
"cheg": (missao.chegada_utc - horizonte_inicio).total_seconds() / 3600,
|
||||
"prio": missao.prioridade,
|
||||
"dur": missao.dur_h,
|
||||
"obj": missao,
|
||||
}
|
||||
return dados, horizonte_inicio
|
||||
|
||||
|
||||
def resolver(missoes: list[Missao]) -> dict[str, object]:
|
||||
if not missoes:
|
||||
raise ValueError("Nenhuma missao encontrada na janela informada")
|
||||
|
||||
miss, horizonte_inicio = horas_relativas(missoes)
|
||||
acft = {a[0]: {"base": a[1], "f": a[2]} for a in AERONAVES}
|
||||
ids = list(miss.keys())
|
||||
ks = list(acft.keys())
|
||||
|
||||
conex = [
|
||||
(i, j)
|
||||
for i in ids
|
||||
for j in ids
|
||||
if i != j
|
||||
and miss[i]["dest"] == miss[j]["orig"]
|
||||
and miss[j]["part"] >= miss[i]["cheg"] + TURNAROUND_MIN_H
|
||||
]
|
||||
|
||||
def pode_iniciar(k: str, i: str) -> bool:
|
||||
return miss[i]["orig"] == acft[k]["base"]
|
||||
|
||||
mdl = pulp.LpProblem("ARARA_OAMRP_v2", pulp.LpMaximize)
|
||||
|
||||
x = {(k, i): pulp.LpVariable(f"x_{k}_{i}", cat="Binary") for k in ks for i in ids}
|
||||
s = {
|
||||
(k, i): pulp.LpVariable(f"s_{k}_{i}", cat="Binary")
|
||||
for k in ks
|
||||
for i in ids
|
||||
if pode_iniciar(k, i)
|
||||
}
|
||||
c = {
|
||||
(k, i, j): pulp.LpVariable(f"c_{k}_{i}_{j}", cat="Binary")
|
||||
for k in ks
|
||||
for (i, j) in conex
|
||||
}
|
||||
z = {i: pulp.LpVariable(f"z_{i}", cat="Binary") for i in ids}
|
||||
|
||||
mdl += pulp.lpSum(miss[i]["prio"] * z[i] for i in ids), "missoes_cumpridas_ponderadas"
|
||||
|
||||
for i in ids:
|
||||
mdl += z[i] == pulp.lpSum(x[(k, i)] for k in ks), f"def_z_{i}"
|
||||
|
||||
for k in ks:
|
||||
for i in ids:
|
||||
entradas = pulp.lpSum(c[(k, jj, i)] for (jj, ii) in conex if ii == i)
|
||||
inicio = s[(k, i)] if (k, i) in s else 0
|
||||
mdl += inicio + entradas == x[(k, i)], f"in_{k}_{i}"
|
||||
|
||||
for k in ks:
|
||||
for i in ids:
|
||||
saidas = pulp.lpSum(c[(k, i, jj)] for (ii, jj) in conex if ii == i)
|
||||
mdl += saidas <= x[(k, i)], f"out_{k}_{i}"
|
||||
|
||||
for k in ks:
|
||||
inicios = [s[(k, i)] for i in ids if (k, i) in s]
|
||||
if inicios:
|
||||
mdl += pulp.lpSum(inicios) <= 1, f"start_{k}"
|
||||
|
||||
for k in ks:
|
||||
mdl += (
|
||||
pulp.lpSum(miss[i]["dur"] * x[(k, i)] for i in ids)
|
||||
<= LIMITE_HORAS_F - acft[k]["f"]
|
||||
), f"horas_{k}"
|
||||
|
||||
solver = pulp.PULP_CBC_CMD(msg=False)
|
||||
mdl.solve(solver)
|
||||
prio_otima = int(round(pulp.value(mdl.objective) or 0))
|
||||
|
||||
# Criterio secundario: entre solucoes com a mesma prioridade, minimiza horas
|
||||
# totais atribuidas. Neste v2 isso evita voos extras irrelevantes se houver
|
||||
# empate em prioridade.
|
||||
mdl += pulp.lpSum(miss[i]["prio"] * z[i] for i in ids) == prio_otima, "fixa_prioridade"
|
||||
mdl.sense = pulp.LpMinimize
|
||||
mdl.setObjective(pulp.lpSum(miss[i]["dur"] * x[(k, i)] for k in ks for i in ids))
|
||||
mdl.solve(solver)
|
||||
|
||||
return {
|
||||
"modelo": mdl,
|
||||
"miss": miss,
|
||||
"acft": acft,
|
||||
"ids": ids,
|
||||
"ks": ks,
|
||||
"conex": conex,
|
||||
"x": x,
|
||||
"s": s,
|
||||
"c": c,
|
||||
"z": z,
|
||||
"horizonte_inicio": horizonte_inicio,
|
||||
"prioridade_otima": prio_otima,
|
||||
}
|
||||
|
||||
|
||||
def rota_da_aeronave(sol: dict[str, object], k: str) -> list[str]:
|
||||
ids = sol["ids"]
|
||||
conex = sol["conex"]
|
||||
s = sol["s"]
|
||||
c = sol["c"]
|
||||
inicio = [
|
||||
i
|
||||
for i in ids
|
||||
if (k, i) in s and s[(k, i)].value() is not None and s[(k, i)].value() > 0.5
|
||||
]
|
||||
if not inicio:
|
||||
return []
|
||||
|
||||
seq = [inicio[0]]
|
||||
while True:
|
||||
atual = seq[-1]
|
||||
prox = [
|
||||
j
|
||||
for (ii, j) in conex
|
||||
if ii == atual and c[(k, atual, j)].value() is not None and c[(k, atual, j)].value() > 0.5
|
||||
]
|
||||
if not prox:
|
||||
break
|
||||
seq.append(prox[0])
|
||||
return seq
|
||||
|
||||
|
||||
def escrever_resultado(sol: dict[str, object], caminho: Path) -> None:
|
||||
caminho.parent.mkdir(parents=True, exist_ok=True)
|
||||
miss = sol["miss"]
|
||||
z = sol["z"]
|
||||
linhas = []
|
||||
for k in sol["ks"]:
|
||||
for ordem, mid in enumerate(rota_da_aeronave(sol, k), start=1):
|
||||
obj = miss[mid]["obj"]
|
||||
linhas.append(
|
||||
{
|
||||
"aeronave_modelo": k,
|
||||
"ordem": ordem,
|
||||
"id": mid,
|
||||
"om": obj.om,
|
||||
"orig": obj.orig,
|
||||
"dest": obj.dest,
|
||||
"partida_utc": obj.partida_utc.isoformat().replace("+00:00", "Z"),
|
||||
"chegada_utc": obj.chegada_utc.isoformat().replace("+00:00", "Z"),
|
||||
"dur_h": f"{obj.dur_h:.2f}",
|
||||
"prioridade": obj.prioridade,
|
||||
"periodo": obj.periodo,
|
||||
"aeronave_real_2025": obj.aeronave_real,
|
||||
"linhas_origem": obj.linhas_origem,
|
||||
"status": "cumprida",
|
||||
}
|
||||
)
|
||||
|
||||
for mid in sol["ids"]:
|
||||
if not z[mid].value() or z[mid].value() <= 0.5:
|
||||
obj = miss[mid]["obj"]
|
||||
linhas.append(
|
||||
{
|
||||
"aeronave_modelo": "",
|
||||
"ordem": "",
|
||||
"id": mid,
|
||||
"om": obj.om,
|
||||
"orig": obj.orig,
|
||||
"dest": obj.dest,
|
||||
"partida_utc": obj.partida_utc.isoformat().replace("+00:00", "Z"),
|
||||
"chegada_utc": obj.chegada_utc.isoformat().replace("+00:00", "Z"),
|
||||
"dur_h": f"{obj.dur_h:.2f}",
|
||||
"prioridade": obj.prioridade,
|
||||
"periodo": obj.periodo,
|
||||
"aeronave_real_2025": obj.aeronave_real,
|
||||
"linhas_origem": obj.linhas_origem,
|
||||
"status": "nao_cumprida",
|
||||
}
|
||||
)
|
||||
|
||||
colunas = [
|
||||
"status",
|
||||
"aeronave_modelo",
|
||||
"ordem",
|
||||
"id",
|
||||
"om",
|
||||
"orig",
|
||||
"dest",
|
||||
"partida_utc",
|
||||
"chegada_utc",
|
||||
"dur_h",
|
||||
"prioridade",
|
||||
"periodo",
|
||||
"aeronave_real_2025",
|
||||
"linhas_origem",
|
||||
]
|
||||
with caminho.open("w", newline="", encoding="utf-8-sig") as arquivo:
|
||||
escritor = csv.DictWriter(arquivo, fieldnames=colunas)
|
||||
escritor.writeheader()
|
||||
escritor.writerows(linhas)
|
||||
|
||||
|
||||
def imprimir_relatorio(sol: dict[str, object], resultado_csv: Path) -> None:
|
||||
miss = sol["miss"]
|
||||
z = sol["z"]
|
||||
ids = sol["ids"]
|
||||
cobertas = [i for i in ids if z[i].value() is not None and z[i].value() > 0.5]
|
||||
prioridade_total = sum(int(miss[i]["prio"]) for i in ids)
|
||||
prioridade_obtida = sum(int(miss[i]["prio"]) for i in cobertas)
|
||||
|
||||
print("=" * 72)
|
||||
print(" OAMRP V2 - Registro de voo 2025 consolidado")
|
||||
print("=" * 72)
|
||||
print(f" Status solver: {pulp.LpStatus[sol['modelo'].status]}")
|
||||
print(f" Horizonte inicia em UTC: {sol['horizonte_inicio'].isoformat().replace('+00:00', 'Z')}")
|
||||
print(f" Missoes cumpridas: {len(cobertas)}/{len(ids)}")
|
||||
print(f" Prioridade obtida: {prioridade_obtida}/{prioridade_total}")
|
||||
print(f" Resultado CSV: {resultado_csv}")
|
||||
|
||||
for k in sol["ks"]:
|
||||
seq = rota_da_aeronave(sol, k)
|
||||
horas = sum(float(miss[i]["dur"]) for i in seq)
|
||||
print(f"\n Aeronave {k}: {len(seq)} missoes | {horas:.1f}h")
|
||||
for mid in seq[:12]:
|
||||
obj = miss[mid]["obj"]
|
||||
print(
|
||||
f" {obj.partida_utc:%d/%m %H:%M}Z "
|
||||
f"{obj.orig}->{obj.dest} OM {obj.om} ({mid})"
|
||||
)
|
||||
if len(seq) > 12:
|
||||
print(f" ... mais {len(seq) - 12} missoes")
|
||||
|
||||
nao_cumpridas = [i for i in ids if i not in cobertas]
|
||||
if nao_cumpridas:
|
||||
print("\n Missoes nao cumpridas (primeiras 20):")
|
||||
for mid in nao_cumpridas[:20]:
|
||||
obj = miss[mid]["obj"]
|
||||
print(f" {obj.partida_utc:%d/%m %H:%M}Z {obj.orig}->{obj.dest} OM {obj.om} ({mid})")
|
||||
if len(nao_cumpridas) > 20:
|
||||
print(f" ... mais {len(nao_cumpridas) - 20} missoes")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Resolve OAMRP v2 com registro de voo real.")
|
||||
parser.add_argument("--csv", type=Path, default=CSV_REGISTRO_PADRAO)
|
||||
parser.add_argument("--inicio", default=DATA_INICIO_PADRAO)
|
||||
parser.add_argument("--fim", default=DATA_FIM_PADRAO)
|
||||
parser.add_argument("--resultado", type=Path, default=CSV_RESULTADO_PADRAO)
|
||||
args = parser.parse_args()
|
||||
|
||||
inicio = parse_data_inicio(args.inicio)
|
||||
fim = parse_data_inicio(args.fim)
|
||||
missoes = carregar_missoes(args.csv, inicio, fim)
|
||||
sol = resolver(missoes)
|
||||
escrever_resultado(sol, args.resultado)
|
||||
imprimir_relatorio(sol, args.resultado)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -240,13 +240,12 @@ def construir_rede(
|
||||
- sumidouro_k : ponto de chegada de cada aeronave k
|
||||
|
||||
Tipos de arcos:
|
||||
- inicio : origem_k → missao_m (m.orig == base_k)
|
||||
- inicio : origem_k → missao_m (com ou sem ferry inicial)
|
||||
- missao_missao : missao_m → missao_m' (dest==orig e gap ≥ TAT)
|
||||
- missao_insp : missao_m → inspecao_k_i (m.dest == SBMN)
|
||||
- insp_missao : inspecao_k_i → missao_m' (m'.orig == SBMN, t_dep ≥ DT_i+TAT)
|
||||
- ferry : missao_m → missao_m' (dest≠orig, gap ≥ TAT + tempo_ferry)
|
||||
- missao_insp : missao_m → inspecao_k_i (m.dest == SBMN, ou com ferry)
|
||||
- insp_missao : inspecao_k_i → missao_m' (m'.orig == SBMN ou com ferry)
|
||||
- fim : missao_m / inspecao_k_i → sumidouro_k
|
||||
|
||||
Fase 3 adicionará arcos de ferry entre bases distintas.
|
||||
"""
|
||||
nos: dict[str, No] = {}
|
||||
arcos: list[Arco] = []
|
||||
@@ -288,11 +287,22 @@ def construir_rede(
|
||||
aeronave_id=a.id,
|
||||
)
|
||||
|
||||
# --- Arcos início: origem_k → missao_m ---
|
||||
# Helper: tempo de ferry entre bases (0 se mesma base)
|
||||
def _ferry_h(b1: str, b2: str) -> float:
|
||||
if b1 == b2:
|
||||
return 0.0
|
||||
if b1 not in BASES_COORDS or b2 not in BASES_COORDS:
|
||||
return float("inf")
|
||||
return _haversine_km(b1, b2) / _VELOCIDADE_CRUZEIRO_KMH
|
||||
|
||||
# --- Arcos início: origem_k → missao_m (com ou sem ferry) ---
|
||||
for a in aeronaves:
|
||||
src_id = f"SRC_{a.id}"
|
||||
for m in missoes:
|
||||
if m.orig != a.base_inicial:
|
||||
fh = _ferry_h(a.base_inicial, m.orig)
|
||||
if fh == float("inf"):
|
||||
continue
|
||||
if m.t_dep < fh: # missão começa antes do ferry terminar
|
||||
continue
|
||||
arc_id = f"INI_{a.id}_{m.id}"
|
||||
arcos.append(
|
||||
@@ -300,19 +310,21 @@ def construir_rede(
|
||||
id=arc_id,
|
||||
orig_id=src_id,
|
||||
dest_id=f"M_{m.id}",
|
||||
tipo="inicio",
|
||||
tipo="inicio" if fh == 0.0 else "ferry",
|
||||
aeronave_id=a.id,
|
||||
horas_ferry=fh,
|
||||
)
|
||||
)
|
||||
|
||||
# --- Arcos missao → missao ---
|
||||
# --- Arcos missao → missao (direto ou com ferry) ---
|
||||
for mi in missoes:
|
||||
for mj in missoes:
|
||||
if mi.id == mj.id:
|
||||
continue
|
||||
if mi.dest != mj.orig:
|
||||
fh = _ferry_h(mi.dest, mj.orig)
|
||||
if fh == float("inf"):
|
||||
continue
|
||||
if mj.t_dep < mi.t_arr + tat_h:
|
||||
if mj.t_dep < mi.t_arr + tat_h + fh:
|
||||
continue
|
||||
arc_id = f"MM_{mi.id}_{mj.id}"
|
||||
arcos.append(
|
||||
@@ -320,7 +332,8 @@ def construir_rede(
|
||||
id=arc_id,
|
||||
orig_id=f"M_{mi.id}",
|
||||
dest_id=f"M_{mj.id}",
|
||||
tipo="missao_missao",
|
||||
tipo="missao_missao" if fh == 0.0 else "ferry",
|
||||
horas_ferry=fh,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -355,19 +368,22 @@ def construir_rede(
|
||||
aeronave_id=a.id,
|
||||
)
|
||||
|
||||
# SRC_k → INSP_k_i (aeronave começa o horizonte em SBMN e vai direto)
|
||||
if a.base_inicial == BASE_MANUTENCAO:
|
||||
# SRC_k → INSP_k_i (com ou sem ferry até SBMN)
|
||||
fh_src = _ferry_h(a.base_inicial, BASE_MANUTENCAO)
|
||||
if fh_src != float("inf"):
|
||||
arcos.append(Arco(
|
||||
id=f"SRC_INSP_{a.id}_{seq}",
|
||||
orig_id=f"SRC_{a.id}",
|
||||
dest_id=insp_nid,
|
||||
tipo="inicio",
|
||||
aeronave_id=a.id,
|
||||
horas_ferry=fh_src,
|
||||
))
|
||||
|
||||
# M_m → INSP_k_i: missões que chegam em SBMN
|
||||
# M_m → INSP_k_i: missões de qualquer base (ferry até SBMN)
|
||||
for m in missoes:
|
||||
if m.dest != BASE_MANUTENCAO:
|
||||
fh = _ferry_h(m.dest, BASE_MANUTENCAO)
|
||||
if fh == float("inf"):
|
||||
continue
|
||||
arcos.append(Arco(
|
||||
id=f"MINSP_{a.id}_{seq}_{m.id}",
|
||||
@@ -375,13 +391,15 @@ def construir_rede(
|
||||
dest_id=insp_nid,
|
||||
tipo="missao_insp",
|
||||
aeronave_id=a.id,
|
||||
horas_ferry=fh,
|
||||
))
|
||||
|
||||
# INSP_k_i → M_m': missões que partem de SBMN após o downtime mínimo
|
||||
# INSP_k_i → M_m': missões de qualquer base após downtime (ferry de SBMN)
|
||||
for m in missoes:
|
||||
if m.orig != BASE_MANUTENCAO:
|
||||
fh = _ferry_h(BASE_MANUTENCAO, m.orig)
|
||||
if fh == float("inf"):
|
||||
continue
|
||||
if m.t_dep < insp.dt_h + tat_h: # muito cedo mesmo no melhor caso
|
||||
if m.t_dep < insp.dt_h + tat_h + fh:
|
||||
continue
|
||||
arcos.append(Arco(
|
||||
id=f"INSPM_{a.id}_{seq}_{m.id}",
|
||||
@@ -389,6 +407,7 @@ def construir_rede(
|
||||
dest_id=f"M_{m.id}",
|
||||
tipo="insp_missao",
|
||||
aeronave_id=a.id,
|
||||
horas_ferry=fh,
|
||||
))
|
||||
|
||||
# INSP_k_i → SNK_k (aeronave encerra o horizonte após inspeção)
|
||||
@@ -566,7 +585,8 @@ def resolver(
|
||||
h_prev = _H_prev(k, seq, a.orig_id)
|
||||
if h_prev is None:
|
||||
continue
|
||||
carga = nos[v_nid].carga_h # dur_h para missões, 0 para inspeções
|
||||
# horas acumuladas = missão + ferry (C6: traslado consome horas de célula)
|
||||
carga = nos[v_nid].carga_h + a.horas_ferry
|
||||
y_var = y[(k, a.id)]
|
||||
mdl += H_v >= h_prev + carga - M_h * (1 - y_var), f"Hlo_{k}_{seq}_{a.id}"
|
||||
mdl += H_v <= h_prev + carga + M_h * (1 - y_var), f"Hhi_{k}_{seq}_{a.id}"
|
||||
@@ -847,7 +867,7 @@ def imprimir_relatorio(
|
||||
total = len(missoes_nos)
|
||||
|
||||
print("=" * 72)
|
||||
print(" OAMRP V3 — Fase 1 (C1–C6) | Esquadrao Arara C-105")
|
||||
print(" OAMRP V3 — Fase 3 (C1–C11 + ferry) | Esquadrao Arara C-105")
|
||||
print("=" * 72)
|
||||
print(f" Status solver : {pulp.LpStatus[sol['modelo'].status]}")
|
||||
print(f" Missoes : {len(cobertas)}/{total} cumpridas")
|
||||
@@ -881,22 +901,47 @@ def imprimir_relatorio(
|
||||
print(f" {k}: {oc:.1f}h (sem restricao de inspecao)")
|
||||
|
||||
print()
|
||||
arcos_por_id = {a.id: a for a in sol["arcos"]}
|
||||
y = sol["y"]
|
||||
|
||||
for k in sol["ks"]:
|
||||
rota = rota_da_aeronave(sol, k)
|
||||
insps = inspecoes_da_aeronave(sol, k)
|
||||
missoes_rota = [n for n in rota if n.tipo == "missao"]
|
||||
horas = sum(n.carga_h for n in missoes_rota)
|
||||
horas_missao = sum(n.carga_h for n in missoes_rota)
|
||||
# horas de ferry: soma horas_ferry dos arcos ativos para este k
|
||||
horas_ferry_total = sum(
|
||||
a.horas_ferry
|
||||
for a in sol["arcos"]
|
||||
if (k, a.id) in y
|
||||
and y[(k, a.id)].value() is not None
|
||||
and y[(k, a.id)].value() > 0.5
|
||||
and a.horas_ferry > 0.0
|
||||
)
|
||||
horas_celula = horas_missao + horas_ferry_total
|
||||
orcamento = acft[k].orcamento_h(catalogo)
|
||||
folga = orcamento - horas
|
||||
folga = orcamento - horas_celula
|
||||
print(
|
||||
f" Aeronave {k}: {len(missoes_rota)} missoes | {horas:.1f}h voadas | "
|
||||
f" Aeronave {k}: {len(missoes_rota)} missoes | "
|
||||
f"{horas_missao:.1f}h voadas + {horas_ferry_total:.1f}h ferry = {horas_celula:.1f}h celula | "
|
||||
f"folga {folga:.1f}h | {len(insps)} inspecao(oes)"
|
||||
)
|
||||
# Linha do tempo unificada: missões e inspeções ordenadas por t_ini
|
||||
# Linha do tempo: missões, inspeções e ferries ordenados por t_ini
|
||||
eventos: list[tuple[float, str]] = []
|
||||
for no in rota:
|
||||
# rastrear nó anterior para identificar ferry
|
||||
nos_rota_ids = [n.id for n in rota]
|
||||
for idx, no in enumerate(rota):
|
||||
if no.tipo == "missao":
|
||||
m = no.missao
|
||||
# detecta ferry antes desta missão
|
||||
if idx > 0:
|
||||
prev = rota[idx - 1]
|
||||
if prev.tipo in ("missao", "origem") and prev.base_out != no.base_in:
|
||||
fh = _haversine_km(prev.base_out, no.base_in) / _VELOCIDADE_CRUZEIRO_KMH
|
||||
eventos.append((
|
||||
no.t_ini - fh,
|
||||
f" [FERRY {prev.base_out}->{no.base_in} {fh:.1f}h]"
|
||||
))
|
||||
eventos.append((
|
||||
no.t_ini,
|
||||
f" {m.partida_utc:%d/%m %H:%M}Z "
|
||||
@@ -906,15 +951,23 @@ def imprimir_relatorio(
|
||||
seq = no.insp_seq
|
||||
s_val = sol["s_insp"][(k, seq)].value() or 0.0
|
||||
insp = catalogo[seq]
|
||||
if idx > 0:
|
||||
prev = rota[idx - 1]
|
||||
if prev.tipo == "missao" and prev.base_out != BASE_MANUTENCAO:
|
||||
fh = _haversine_km(prev.base_out, BASE_MANUTENCAO) / _VELOCIDADE_CRUZEIRO_KMH
|
||||
eventos.append((
|
||||
s_val - fh,
|
||||
f" [FERRY {prev.base_out}->{BASE_MANUTENCAO} {fh:.1f}h (retorno insp)]"
|
||||
))
|
||||
eventos.append((
|
||||
s_val,
|
||||
f" [INSP seq {seq} {insp.sigla}] "
|
||||
f"s={s_val:.1f}h DT={insp.dt_h:.1f}h fim={s_val + insp.dt_h:.1f}h"
|
||||
))
|
||||
for _, linha in sorted(eventos)[:15]:
|
||||
for _, linha in sorted(eventos)[:20]:
|
||||
print(linha)
|
||||
if len(eventos) > 15:
|
||||
print(f" ... mais {len(eventos) - 15} eventos")
|
||||
if len(eventos) > 20:
|
||||
print(f" ... mais {len(eventos) - 20} eventos")
|
||||
print()
|
||||
|
||||
nao = [nid for nid in missoes_nos if nid not in cobertas]
|
||||
@@ -1087,6 +1140,124 @@ def derivar_bases(
|
||||
return sorted(top)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Missões sintéticas
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Coordenadas (lat, lon) das 10 bases operacionais do C-105
|
||||
BASES_COORDS: dict[str, tuple[float, float]] = {
|
||||
"SBMN": (-3.15, -59.99), # Manaus — Ponta Pelada (hub)
|
||||
"SBBE": (-1.38, -48.48), # Belém
|
||||
"SBBV": ( 2.84, -60.69), # Boa Vista
|
||||
"SBSN": (-2.42, -54.79), # Santarém
|
||||
"SBPV": (-8.71, -63.90), # Porto Velho
|
||||
"SBTS": (-4.25, -69.94), # Tabatinga
|
||||
"SBTT": (-3.38, -64.72), # Tefé
|
||||
"SBUA": (-0.15, -67.05), # São Gabriel da Cachoeira
|
||||
"SWBC": (-0.98, -62.92), # Barcelos
|
||||
"SBMY": (-5.81, -61.28), # Manicoré
|
||||
}
|
||||
|
||||
_VELOCIDADE_CRUZEIRO_KMH = 430.0 # C-105 Spartan (típico)
|
||||
_VARIACAO_DUR = 0.10 # ± 10 % de variação aleatória na duração
|
||||
|
||||
|
||||
def _haversine_km(b1: str, b2: str) -> float:
|
||||
import math
|
||||
lat1, lon1 = BASES_COORDS[b1]
|
||||
lat2, lon2 = BASES_COORDS[b2]
|
||||
R = 6371.0
|
||||
dlat = math.radians(lat2 - lat1)
|
||||
dlon = math.radians(lon2 - lon1)
|
||||
a = math.sin(dlat/2)**2 + math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) * math.sin(dlon/2)**2
|
||||
return R * 2 * math.asin(math.sqrt(a))
|
||||
|
||||
|
||||
def gerar_missoes_sinteticas(
|
||||
n: int = 50,
|
||||
seed: Optional[int] = 42,
|
||||
inicio: Optional[datetime] = None,
|
||||
horizonte_h: float = 744.0, # janeiro = 31 × 24
|
||||
n_evam: int = 5,
|
||||
bases: Optional[list[str]] = None,
|
||||
) -> tuple[list[Missao], datetime]:
|
||||
"""
|
||||
Gera n missões aleatórias cross-base (orig ≠ dest) entre as bases listadas.
|
||||
|
||||
Duração estimada via distância haversine ÷ velocidade de cruzeiro do C-105,
|
||||
com variação aleatória de ± VARIACAO_DUR.
|
||||
n_evam missões são marcadas como obrigatórias (hard).
|
||||
"""
|
||||
import math
|
||||
|
||||
if inicio is None:
|
||||
inicio = datetime(2025, 1, 1, tzinfo=timezone.utc)
|
||||
|
||||
bases = list(bases or BASES_COORDS.keys())
|
||||
rng = random.Random(seed)
|
||||
|
||||
pares = [(o, d) for o in bases for d in bases if o != d]
|
||||
missoes: list[Missao] = []
|
||||
|
||||
# sorteia pares com reposição para garantir n missões
|
||||
escolhidos = [pares[rng.randrange(len(pares))] for _ in range(n)]
|
||||
# garante que SBMN aparece em orig ou dest com frequência (hub real)
|
||||
for i in range(0, n, 3):
|
||||
outro = rng.choice([b for b in bases if b != "SBMN"])
|
||||
if rng.random() < 0.5:
|
||||
escolhidos[i] = ("SBMN", outro)
|
||||
else:
|
||||
escolhidos[i] = (outro, "SBMN")
|
||||
|
||||
evam_indices = set(rng.sample(range(n), min(n_evam, n)))
|
||||
|
||||
# distribui decolagens uniformemente ao longo do horizonte com gap mínimo de 2h
|
||||
t_deps = sorted(rng.uniform(0, horizonte_h - 10) for _ in range(n))
|
||||
|
||||
for idx, (t_dep_h, (orig, dest)) in enumerate(zip(t_deps, escolhidos)):
|
||||
dist_km = _haversine_km(orig, dest)
|
||||
dur_base = dist_km / _VELOCIDADE_CRUZEIRO_KMH
|
||||
dur_h = round(dur_base * (1 + rng.uniform(-_VARIACAO_DUR, _VARIACAO_DUR)), 2)
|
||||
dur_h = max(0.5, dur_h)
|
||||
|
||||
t_dep_h = round(t_dep_h, 3)
|
||||
t_arr_h = round(t_dep_h + dur_h, 3)
|
||||
if t_arr_h > horizonte_h:
|
||||
t_arr_h = horizonte_h
|
||||
t_dep_h = max(0.0, t_arr_h - dur_h)
|
||||
|
||||
partida = datetime.fromtimestamp(
|
||||
inicio.timestamp() + t_dep_h * 3600, tz=timezone.utc
|
||||
)
|
||||
chegada = datetime.fromtimestamp(
|
||||
inicio.timestamp() + t_arr_h * 3600, tz=timezone.utc
|
||||
)
|
||||
|
||||
obrigatoria = idx in evam_indices
|
||||
cod = f"EVAM{idx+1:03d}" if obrigatoria else f"SIM{idx+1:03d}"
|
||||
om_str = str(idx + 1)
|
||||
|
||||
missoes.append(Missao(
|
||||
id=f"SIM_{idx+1:03d}_{orig}_{dest}",
|
||||
orig=orig,
|
||||
dest=dest,
|
||||
t_dep=t_dep_h,
|
||||
t_arr=t_arr_h,
|
||||
dur_h=dur_h,
|
||||
prioridade=1,
|
||||
obrigatoria=obrigatoria,
|
||||
partida_utc=partida,
|
||||
chegada_utc=chegada,
|
||||
periodo="DIURNO",
|
||||
linhas_origem="",
|
||||
aeronave_real="",
|
||||
om=om_str,
|
||||
codigo_missao=cod,
|
||||
))
|
||||
|
||||
return missoes, inicio
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Horas iniciais
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -1158,20 +1329,46 @@ def main() -> None:
|
||||
"--hard-pattern", default=HARD_MISSAO_PADRAO,
|
||||
help="Regex no campo 'missao' do CSV que marca missões obrigatórias (Q2).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--sintetico", action="store_true",
|
||||
help="Usa missões sintéticas cross-base em vez do CSV real.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--n-missoes", type=int, default=50,
|
||||
help="Número de missões sintéticas a gerar (padrão: 50).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--n-evam", type=int, default=5,
|
||||
help="Missões obrigatórias (EVAM/hard) nas sintéticas (padrão: 5).",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
inicio = datetime.fromisoformat(args.inicio).replace(tzinfo=timezone.utc)
|
||||
fim = datetime.fromisoformat(args.fim).replace(tzinfo=timezone.utc)
|
||||
|
||||
catalogo = carregar_inspecoes(args.inspecoes)
|
||||
top_n = args.n_bases if args.n_bases > 0 else None
|
||||
bases = derivar_bases(args.csv, inicio, fim, top_n=top_n)
|
||||
bases_validas = set(bases) if top_n else None
|
||||
missoes, _ = carregar_missoes(
|
||||
args.csv, inicio, fim,
|
||||
bases_validas=bases_validas,
|
||||
hard_pattern=args.hard_pattern,
|
||||
)
|
||||
|
||||
if args.sintetico:
|
||||
horizonte_h = (fim - inicio).total_seconds() / 3600
|
||||
bases = list(BASES_COORDS.keys())
|
||||
missoes, horizonte_dt = gerar_missoes_sinteticas(
|
||||
n=args.n_missoes,
|
||||
seed=args.seed,
|
||||
inicio=inicio,
|
||||
horizonte_h=horizonte_h,
|
||||
n_evam=args.n_evam,
|
||||
bases=bases,
|
||||
)
|
||||
print(f" Modo sintético: {len(missoes)} missões geradas entre {len(bases)} bases.")
|
||||
else:
|
||||
top_n = args.n_bases if args.n_bases > 0 else None
|
||||
bases = derivar_bases(args.csv, inicio, fim, top_n=top_n)
|
||||
bases_validas = set(bases) if top_n else None
|
||||
missoes, horizonte_dt = carregar_missoes(
|
||||
args.csv, inicio, fim,
|
||||
bases_validas=bases_validas,
|
||||
hard_pattern=args.hard_pattern,
|
||||
)
|
||||
|
||||
f0 = carregar_horas_iniciais(
|
||||
fonte=args.horas_iniciais,
|
||||
|
||||
Reference in New Issue
Block a user