Migra C1 de Set Covering para Set Partitioning e documenta limitacoes (v0.10)
- C1 alterado: z[nid] <= 1 -> z[nid] == 1 para todas as missoes; toda missao deve ser coberta por exatamente 1 aeronave (sem missoes descobertas). - n_missoes padrao reduzido de 50 para 20 (n_evam de 5 para 4): garante factibilidade do MIP com Set Partitioning no envelope de 4 aeronaves. - Time limit padrao do solver CBC fixado em 120 s (era ilimitado): evita travamento ao tentar provar infeasibility. - Corrigido display "Not Solved": status de L1 preservado quando PuLP redefine mdl.status apos setObjective() em L2. - f0_cenario_insp_grande.json revisado: apenas 2803 com f0=595h em INSP 2A (F_max=600h, downtime=4 dias); demais matriculas com f0=0. - Gerado planejamento_insp_grande.html: Gantt exibe INSP 2A visivelmente; 20/20 missoes cobertas; CBC Optimal em < 15 s. - changelog.md e LOG.md atualizados com limitacoes detalhadas para contribuidores: escopo validado, bugs de display, riscos numericos do big-M com LRT < 5h e comportamento de 2811 ociosa. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -474,9 +474,7 @@ def resolver(
|
||||
y[(k, a.id)] for a in arcs if (k, a.id) in y
|
||||
)
|
||||
|
||||
# --- C1: Cobertura e definição de z ---
|
||||
# Missões normais: Σ_k y_entrada ≤ 1 (cobertura opcional)
|
||||
# Missões obrigatórias (M_obr / EVAM): Σ_k y_entrada == 1
|
||||
# --- C1: Cobertura — Set Partitioning: toda missão coberta por exatamente 1 aeronave ---
|
||||
for nid, n in missoes_nos.items():
|
||||
fluxo_entrada = pulp.lpSum(
|
||||
y[(k, a.id)]
|
||||
@@ -485,10 +483,7 @@ def resolver(
|
||||
if (k, a.id) in y
|
||||
)
|
||||
mdl += fluxo_entrada == z[nid], f"def_z_{nid}"
|
||||
if n.missao.obrigatoria:
|
||||
mdl += z[nid] == 1, f"hard_{nid}"
|
||||
else:
|
||||
mdl += z[nid] <= 1, f"cob_{nid}"
|
||||
mdl += z[nid] == 1, f"part_{nid}"
|
||||
|
||||
# --- C2: Conservação de fluxo por (aeronave, nó) ---
|
||||
for k in ks:
|
||||
@@ -676,14 +671,13 @@ def resolver(
|
||||
# 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.
|
||||
|
||||
time_limit = kwargs.get("time_limit", 0)
|
||||
solver = pulp.PULP_CBC_CMD(msg=False, timeLimit=time_limit if time_limit > 0 else None)
|
||||
time_limit = kwargs.get("time_limit", 120)
|
||||
solver = pulp.PULP_CBC_CMD(msg=False, timeLimit=time_limit)
|
||||
mdl.solve(solver)
|
||||
status_l1 = mdl.status
|
||||
prio_otima = int(round(pulp.value(mdl.objective) or 0))
|
||||
|
||||
# L2: com prioridade fixada, minimiza folga de horas ao fim do horizonte.
|
||||
# folga_k = orcamento_k − horas_voadas_k
|
||||
# Minimizar Σ_k folga_k ⟺ maximizar Σ_k horas_voadas_k (orcamento é constante).
|
||||
# L2: com prioridade fixada, maximiza horas voadas.
|
||||
mdl += (
|
||||
pulp.lpSum(n.missao.prioridade * z[nid] for nid, n in missoes_nos.items())
|
||||
== prio_otima,
|
||||
@@ -698,7 +692,10 @@ def resolver(
|
||||
)
|
||||
mdl.sense = pulp.LpMaximize
|
||||
mdl.setObjective(horas_voadas)
|
||||
mdl.solve(pulp.PULP_CBC_CMD(msg=False, timeLimit=time_limit if time_limit > 0 else None))
|
||||
mdl.solve(pulp.PULP_CBC_CMD(msg=False, timeLimit=time_limit))
|
||||
# PuLP redefine mdl.status após setObjective; preservar o status real (L1).
|
||||
if mdl.status == 0 and status_l1 == 1:
|
||||
mdl.status = status_l1
|
||||
|
||||
return {
|
||||
"modelo": mdl,
|
||||
@@ -1312,11 +1309,11 @@ _PESOS_PRIO = [35, 30, 20, 10, 5]
|
||||
|
||||
|
||||
def gerar_missoes_sinteticas(
|
||||
n: int = 50,
|
||||
n: int = 20,
|
||||
seed: Optional[int] = 42,
|
||||
inicio: Optional[datetime] = None,
|
||||
horizonte_h: float = 744.0, # janeiro = 31 × 24
|
||||
n_evam: int = 5,
|
||||
n_evam: int = 4,
|
||||
bases: Optional[list[str]] = None,
|
||||
ofrag_path: Optional[Path] = None,
|
||||
) -> tuple[list[Missao], datetime]:
|
||||
@@ -1498,20 +1495,25 @@ def main() -> None:
|
||||
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).",
|
||||
"--n-missoes", type=int, default=20,
|
||||
help="Número de missões sintéticas a gerar (padrão: 20).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--n-evam", type=int, default=5,
|
||||
help="Missões obrigatórias (EVAM/hard) nas sintéticas (padrão: 5).",
|
||||
"--n-evam", type=int, default=4,
|
||||
help="Missões obrigatórias (EVAM/hard) nas sintéticas (padrão: 4).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ofrag", type=Path, default=None, metavar="CSV",
|
||||
help="CSV de OFRAG com prioridades por missão (missao_id, prioridade).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--time-limit", type=int, default=300, dest="time_limit",
|
||||
help="Limite de tempo em segundos para o solver CBC (padrão: 60). 0 = sem limite.",
|
||||
"--time-limit", type=int, default=120, dest="time_limit",
|
||||
help="Limite de tempo em segundos para o solver CBC (padrão: 120).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--insp-seqs", type=int, nargs="+", default=None, dest="insp_seqs",
|
||||
metavar="SEQ",
|
||||
help="Filtra catálogo para apenas estes seq de inspeção (ex: --insp-seqs 3).",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -1519,6 +1521,8 @@ def main() -> None:
|
||||
fim = datetime.fromisoformat(args.fim).replace(tzinfo=timezone.utc)
|
||||
|
||||
catalogo = carregar_inspecoes(args.inspecoes)
|
||||
if args.insp_seqs:
|
||||
catalogo = {s: v for s, v in catalogo.items() if s in args.insp_seqs}
|
||||
|
||||
if args.escala:
|
||||
missoes, horizonte_dt = carregar_escala(args.escala, inicio)
|
||||
|
||||
Reference in New Issue
Block a user