Corrige modelo OAMRP v3 e alinha nomenclatura com Al-Thani (2016)
Correções ao modelo: - Adiciona conservação de fluxo em nós de inspeção (bug que permitia criação/destruição de unidades de fluxo, inflando cobertura para 50/50 sem atribuição real de rotas) - Remove restrição C6 simplificada que somava horas totais sem respeitar resets de inspeção, tornando 2809/2811 artificialmente ociosos Nomenclatura alinhada ao Al-Thani (2016): - InspecaoParam.lrt_h → f_max (F do artigo: batente máximo legal) - Aeronave.horas_iniciais → f0 (f_k: horas acumuladas no início) - orcamento_h() → lrt_inicial() (LRT = F_max − f0) - horas_iniciais_aleatorias() → gerar_f0_aleatorio() - carregar_horas_iniciais() → carregar_f0() - CLI --horas-iniciais → --f0 Novos arquivos: - software/gerar_ofrag.py: gera tabela de OFRAGs com prioridades 1-5 - software/visualizar_resultado.py: mapa Folium interativo de rotas - db/processed/ofrag.csv: 50 OFRAGs sintéticas (seed 42) - db/processed/mapa_rotas.html: mapa gerado da última rodada Resultado com --sintetico --aleatorio --ofrag: 50/50 missões cumpridas (antes: 30/50 sem ferry, 7/50 com bug de fluxo) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -30,7 +30,7 @@
|
||||
Removidas: seq 18 (INSP 100FH) e seq 23 (INSP 50FH)
|
||||
|
||||
Horas iniciais f0_{k,i}:
|
||||
--horas-iniciais <arquivo.json ou string JSON>
|
||||
--f0 <arquivo.json ou string JSON>
|
||||
--aleatorio [--seed N]
|
||||
padrão: 0,0 para todas (aeronave recém-inspecionada — conservador)
|
||||
============================================================================
|
||||
@@ -120,7 +120,7 @@ class InspecaoParam:
|
||||
sigla: str
|
||||
descricao: str
|
||||
nivel: str
|
||||
lrt_h: float
|
||||
f_max: float # F do Al-Thani: intervalo máximo legal entre manutenções
|
||||
cal_dias: Optional[float]
|
||||
dt_h: float
|
||||
|
||||
@@ -129,20 +129,16 @@ class InspecaoParam:
|
||||
class Aeronave:
|
||||
id: str
|
||||
base_inicial: str
|
||||
horas_iniciais: dict[int, float] = field(default_factory=dict)
|
||||
f0: dict[int, float] = field(default_factory=dict) # f_k do Al-Thani: horas acumuladas no início
|
||||
|
||||
def orcamento_h(self, catalogo: dict[int, InspecaoParam]) -> float:
|
||||
"""
|
||||
Orçamento de horas disponíveis (C6 — Fase 1 simplificado).
|
||||
Retorna min(LRT_i − f0_{k,i}) sobre todas as inspeções ativas.
|
||||
"""
|
||||
def lrt_inicial(self, catalogo: dict[int, InspecaoParam]) -> float:
|
||||
"""Retorna min(F_max_i − f0_{k,i}): LRT disponível no início do horizonte."""
|
||||
if not catalogo:
|
||||
return 0.0
|
||||
margens = [
|
||||
insp.lrt_h - self.horas_iniciais.get(seq, 0.0)
|
||||
return max(0.0, min(
|
||||
insp.f_max - self.f0.get(seq, 0.0)
|
||||
for seq, insp in catalogo.items()
|
||||
]
|
||||
return max(0.0, min(margens))
|
||||
))
|
||||
|
||||
def insp_mais_restritiva(
|
||||
self, catalogo: dict[int, InspecaoParam]
|
||||
@@ -151,9 +147,9 @@ class Aeronave:
|
||||
return None
|
||||
seq_min = min(
|
||||
catalogo,
|
||||
key=lambda s: catalogo[s].lrt_h - self.horas_iniciais.get(s, 0.0),
|
||||
key=lambda s: catalogo[s].f_max - self.f0.get(s, 0.0),
|
||||
)
|
||||
return seq_min, self.horas_iniciais.get(seq_min, 0.0), catalogo[seq_min]
|
||||
return seq_min, self.f0.get(seq_min, 0.0), catalogo[seq_min]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -514,13 +510,16 @@ def resolver(
|
||||
elif n.tipo == "missao":
|
||||
# Missão: fluxo de entrada = fluxo de saída para esta aeronave
|
||||
mdl += entrada_k == saida_k, f"fluxo_{k}_{nid}"
|
||||
elif n.tipo == "inspecao" and n.aeronave_id == k:
|
||||
# Inspeção exclusiva de k: conservação de fluxo (evita criação/destruição)
|
||||
mdl += entrada_k == saida_k, f"fluxo_{k}_{nid}"
|
||||
|
||||
# --- Fase 2: variáveis e restrições C6-reset, C9, C11 ---
|
||||
|
||||
insp_nos = {nid: n for nid, n in nos.items() if n.tipo == "inspecao"}
|
||||
|
||||
# Big-M: usa LRT máximo (para horas) e horizonte temporal (para tempo)
|
||||
M_h = max((insp.lrt_h for insp in catalogo.values()), default=1.0) + 100.0
|
||||
M_h = max((insp.f_max for insp in catalogo.values()), default=1.0) + 100.0
|
||||
M_t = (
|
||||
max((n.t_fim for n in missoes_nos.values()), default=0.0) + 200.0
|
||||
)
|
||||
@@ -551,7 +550,7 @@ def resolver(
|
||||
(k, seq, nid): pulp.LpVariable(
|
||||
f"H_{k}_{seq}_{nid}",
|
||||
lowBound=0,
|
||||
upBound=catalogo[seq].lrt_h,
|
||||
upBound=catalogo[seq].f_max,
|
||||
)
|
||||
for k in ks
|
||||
for seq in catalogo
|
||||
@@ -563,7 +562,7 @@ def resolver(
|
||||
"""Retorna o valor/variável de H_{k,seq} ao sair do nó u."""
|
||||
n = nos[u_nid]
|
||||
if n.tipo == "origem":
|
||||
return acft[k].horas_iniciais.get(seq, 0.0)
|
||||
return acft[k].f0.get(seq, 0.0)
|
||||
# Nó de reset para este clock: saída é 0
|
||||
if n.tipo == "inspecao" and n.insp_seq == seq and n.aeronave_id == k:
|
||||
return 0.0
|
||||
@@ -672,19 +671,9 @@ def resolver(
|
||||
f"c11b_{k1}_{seq1}_{k2}_{seq2}",
|
||||
)
|
||||
|
||||
# --- C6 (Fase 1 — mantido como redundância explícita para verificação) ---
|
||||
for k in ks:
|
||||
orcamento = acft[k].orcamento_h(catalogo)
|
||||
mdl += (
|
||||
pulp.lpSum(
|
||||
n.carga_h * y[(k, a.id)]
|
||||
for nid, n in missoes_nos.items()
|
||||
for a in arcos_entrando[nid]
|
||||
if (k, a.id) in y
|
||||
)
|
||||
<= orcamento,
|
||||
f"orcamento_{k}",
|
||||
)
|
||||
# C6 Fase 1 simplificada foi removida: a propagação de H[k,seq,nid] com reset
|
||||
# já impõe o limite LRT por ciclo corretamente. A versão simplificada somava
|
||||
# horas totais sem respeitar resets de inspeção, super-restringindo o modelo.
|
||||
|
||||
solver = pulp.PULP_CBC_CMD(msg=False)
|
||||
mdl.solve(solver)
|
||||
@@ -881,24 +870,24 @@ def imprimir_relatorio(
|
||||
insp = catalogo[seq]
|
||||
cal = f"{insp.cal_dias:.0f}d" if insp.cal_dias else "—"
|
||||
print(
|
||||
f" seq {seq:2d} {insp.sigla:<18} LRT={insp.lrt_h:>6.0f}h "
|
||||
f" seq {seq:2d} {insp.sigla:<18} F_max={insp.f_max:>6.0f}h "
|
||||
f"CAL={cal:>5} DT={insp.dt_h:.1f}h [{insp.nivel}]"
|
||||
)
|
||||
|
||||
print("\n Orcamento por aeronave:")
|
||||
for k in sol["ks"]:
|
||||
a = acft[k]
|
||||
oc = a.orcamento_h(catalogo)
|
||||
lrt_ini = a.lrt_inicial(catalogo)
|
||||
info = a.insp_mais_restritiva(catalogo)
|
||||
if info:
|
||||
seq_min, f0, insp = info
|
||||
seq_min, f0_val, insp = info
|
||||
print(
|
||||
f" {k}: {oc:.1f}h "
|
||||
f" {k}: LRT_ini={lrt_ini:.1f}h "
|
||||
f"(limitado por seq {seq_min} {insp.sigla}, "
|
||||
f"f0={f0:.1f}h, LRT={insp.lrt_h:.0f}h)"
|
||||
f"f0={f0_val:.1f}h, F_max={insp.f_max:.0f}h)"
|
||||
)
|
||||
else:
|
||||
print(f" {k}: {oc:.1f}h (sem restricao de inspecao)")
|
||||
print(f" {k}: LRT_ini={lrt_ini:.1f}h (sem restricao de inspecao)")
|
||||
|
||||
print()
|
||||
arcos_por_id = {a.id: a for a in sol["arcos"]}
|
||||
@@ -919,12 +908,12 @@ def imprimir_relatorio(
|
||||
and a.horas_ferry > 0.0
|
||||
)
|
||||
horas_celula = horas_missao + horas_ferry_total
|
||||
orcamento = acft[k].orcamento_h(catalogo)
|
||||
folga = orcamento - horas_celula
|
||||
lrt_ini = acft[k].lrt_inicial(catalogo)
|
||||
folga = lrt_ini - horas_celula
|
||||
print(
|
||||
f" Aeronave {k}: {len(missoes_rota)} missoes | "
|
||||
f"{horas_missao:.1f}h voadas + {horas_ferry_total:.1f}h ferry = {horas_celula:.1f}h celula | "
|
||||
f"folga {folga:.1f}h | {len(insps)} inspecao(oes)"
|
||||
f"LRT_ini={lrt_ini:.1f}h folga={folga:.1f}h | {len(insps)} inspecao(oes)"
|
||||
)
|
||||
# Linha do tempo: missões, inspeções e ferries ordenados por t_ini
|
||||
eventos: list[tuple[float, str]] = []
|
||||
@@ -1096,7 +1085,7 @@ def carregar_inspecoes(caminho: Path) -> dict[int, InspecaoParam]:
|
||||
sigla=item["sigla_mnt"],
|
||||
descricao=item["descricao_mnt"],
|
||||
nivel=nivel_desc,
|
||||
lrt_h=float(lrt),
|
||||
f_max=float(lrt),
|
||||
cal_dias=cal_dias,
|
||||
dt_h=dt_h,
|
||||
)
|
||||
@@ -1173,6 +1162,12 @@ def _haversine_km(b1: str, b2: str) -> float:
|
||||
return R * 2 * math.asin(math.sqrt(a))
|
||||
|
||||
|
||||
TIPOS_MISSAO = ["Logística", "Médico", "Pessoal", "Reabastecimento", "Reconhecimento"]
|
||||
|
||||
# Pesos de prioridade: prio 1 (urgente) é mais frequente que prio 5 (baixa)
|
||||
_PESOS_PRIO = [35, 30, 20, 10, 5]
|
||||
|
||||
|
||||
def gerar_missoes_sinteticas(
|
||||
n: int = 50,
|
||||
seed: Optional[int] = 42,
|
||||
@@ -1180,6 +1175,7 @@ def gerar_missoes_sinteticas(
|
||||
horizonte_h: float = 744.0, # janeiro = 31 × 24
|
||||
n_evam: int = 5,
|
||||
bases: Optional[list[str]] = None,
|
||||
ofrag_path: Optional[Path] = None,
|
||||
) -> tuple[list[Missao], datetime]:
|
||||
"""
|
||||
Gera n missões aleatórias cross-base (orig ≠ dest) entre as bases listadas.
|
||||
@@ -1187,6 +1183,7 @@ def gerar_missoes_sinteticas(
|
||||
Duração estimada via distância haversine ÷ velocidade de cruzeiro do C-105,
|
||||
com variação aleatória de ± VARIACAO_DUR.
|
||||
n_evam missões são marcadas como obrigatórias (hard).
|
||||
Se ofrag_path for fornecido, prioridades são lidas do CSV de OFRAG.
|
||||
"""
|
||||
import math
|
||||
|
||||
@@ -1196,6 +1193,15 @@ def gerar_missoes_sinteticas(
|
||||
bases = list(bases or BASES_COORDS.keys())
|
||||
rng = random.Random(seed)
|
||||
|
||||
# Carrega prioridades do CSV de OFRAG, se fornecido
|
||||
ofrag_prios: dict[str, int] = {}
|
||||
ofrag_tipos: dict[str, str] = {}
|
||||
if ofrag_path and ofrag_path.exists():
|
||||
with ofrag_path.open(newline="", encoding="utf-8-sig") as f:
|
||||
for row in csv.DictReader(f):
|
||||
ofrag_prios[row["missao_id"]] = int(row["prioridade"])
|
||||
ofrag_tipos[row["missao_id"]] = row.get("tipo_missao", "")
|
||||
|
||||
pares = [(o, d) for o in bases for d in bases if o != d]
|
||||
missoes: list[Missao] = []
|
||||
|
||||
@@ -1236,15 +1242,24 @@ def gerar_missoes_sinteticas(
|
||||
obrigatoria = idx in evam_indices
|
||||
cod = f"EVAM{idx+1:03d}" if obrigatoria else f"SIM{idx+1:03d}"
|
||||
om_str = str(idx + 1)
|
||||
missao_id = f"SIM_{idx+1:03d}_{orig}_{dest}"
|
||||
|
||||
# Prioridade: EVAM sempre prio 1 (urgente); demais lidas do OFRAG ou geradas
|
||||
if obrigatoria:
|
||||
prio = 1
|
||||
elif missao_id in ofrag_prios:
|
||||
prio = ofrag_prios[missao_id]
|
||||
else:
|
||||
prio = rng.choices(range(1, 6), weights=_PESOS_PRIO)[0]
|
||||
|
||||
missoes.append(Missao(
|
||||
id=f"SIM_{idx+1:03d}_{orig}_{dest}",
|
||||
id=missao_id,
|
||||
orig=orig,
|
||||
dest=dest,
|
||||
t_dep=t_dep_h,
|
||||
t_arr=t_arr_h,
|
||||
dur_h=dur_h,
|
||||
prioridade=1,
|
||||
prioridade=prio,
|
||||
obrigatoria=obrigatoria,
|
||||
partida_utc=partida,
|
||||
chegada_utc=chegada,
|
||||
@@ -1263,34 +1278,36 @@ def gerar_missoes_sinteticas(
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def horas_iniciais_aleatorias(
|
||||
def gerar_f0_aleatorio(
|
||||
aeronaves: list[str],
|
||||
catalogo: dict[int, InspecaoParam],
|
||||
seed: Optional[int] = None,
|
||||
) -> dict[str, dict[int, float]]:
|
||||
"""Sorteia f0_{k,i} entre 10% e 90% de F_max_i para cada aeronave e inspeção."""
|
||||
rng = random.Random(seed)
|
||||
return {
|
||||
k: {
|
||||
seq: round(rng.uniform(0.1 * insp.lrt_h, 0.9 * insp.lrt_h), 1)
|
||||
seq: round(rng.uniform(0.1 * insp.f_max, 0.9 * insp.f_max), 1)
|
||||
for seq, insp in catalogo.items()
|
||||
}
|
||||
for k in aeronaves
|
||||
}
|
||||
|
||||
|
||||
def carregar_horas_iniciais(
|
||||
def carregar_f0(
|
||||
fonte: Optional[str],
|
||||
aleatorio: bool,
|
||||
aeronaves: list[str],
|
||||
catalogo: dict[int, InspecaoParam],
|
||||
seed: Optional[int] = None,
|
||||
) -> dict[str, dict[int, float]]:
|
||||
"""Retorna dict[aeronave_id -> dict[seq -> f0]] com horas iniciais acumuladas."""
|
||||
if fonte:
|
||||
p = Path(fonte)
|
||||
raw = json.loads(p.read_text(encoding="utf-8")) if p.exists() else json.loads(fonte)
|
||||
return {k: {int(s): float(v) for s, v in d.items()} for k, d in raw.items()}
|
||||
if aleatorio:
|
||||
return horas_iniciais_aleatorias(aeronaves, catalogo, seed=seed)
|
||||
return gerar_f0_aleatorio(aeronaves, catalogo, seed=seed)
|
||||
return {k: {seq: 0.0 for seq in catalogo} for k in aeronaves}
|
||||
|
||||
|
||||
@@ -1309,8 +1326,8 @@ def main() -> None:
|
||||
parser.add_argument("--fim", default=DATA_FIM_PADRAO)
|
||||
parser.add_argument("--resultado", type=Path, default=CSV_RESULTADO_PADRAO)
|
||||
parser.add_argument(
|
||||
"--horas-iniciais", metavar="JSON",
|
||||
help='f0_{k,i}: arquivo ou string JSON. Ex: {"2800": {"23": 35.0}}',
|
||||
"--f0", metavar="JSON", dest="f0",
|
||||
help='f0_{k,i} (horas acumuladas iniciais): arquivo ou string JSON. Ex: {"2800": {"2": 150.0}}',
|
||||
)
|
||||
parser.add_argument(
|
||||
"--aleatorio", action="store_true",
|
||||
@@ -1341,6 +1358,10 @@ def main() -> None:
|
||||
"--n-evam", type=int, default=5,
|
||||
help="Missões obrigatórias (EVAM/hard) nas sintéticas (padrão: 5).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ofrag", type=Path, default=None, metavar="CSV",
|
||||
help="CSV de OFRAG com prioridades por missão (missao_id, prioridade).",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
inicio = datetime.fromisoformat(args.inicio).replace(tzinfo=timezone.utc)
|
||||
@@ -1358,6 +1379,7 @@ def main() -> None:
|
||||
horizonte_h=horizonte_h,
|
||||
n_evam=args.n_evam,
|
||||
bases=bases,
|
||||
ofrag_path=args.ofrag,
|
||||
)
|
||||
print(f" Modo sintético: {len(missoes)} missões geradas entre {len(bases)} bases.")
|
||||
else:
|
||||
@@ -1370,15 +1392,15 @@ def main() -> None:
|
||||
hard_pattern=args.hard_pattern,
|
||||
)
|
||||
|
||||
f0 = carregar_horas_iniciais(
|
||||
fonte=args.horas_iniciais,
|
||||
f0_map = carregar_f0(
|
||||
fonte=args.f0,
|
||||
aleatorio=args.aleatorio,
|
||||
aeronaves=args.aeronaves,
|
||||
catalogo=catalogo,
|
||||
seed=args.seed,
|
||||
)
|
||||
aeronaves = [
|
||||
Aeronave(id=k, base_inicial=args.base_inicial, horas_iniciais=f0.get(k, {}))
|
||||
Aeronave(id=k, base_inicial=args.base_inicial, f0=f0_map.get(k, {}))
|
||||
for k in args.aeronaves
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user