refactor: simplifica Fleet Assignment para nível Brasil sem restrição de esquadrões (Modelo 1)

This commit is contained in:
2026-06-07 20:32:35 -03:00
parent 15bcd7e962
commit edf6ef9d8f
2 changed files with 104 additions and 134 deletions

View File

@@ -202,22 +202,22 @@ plt.show()
"""))
# Cell 9: Markdown FAM
cells.append(nbf.v4.new_markdown_cell("""## 4. Fleet Assignment Model (FAM)
Conforme o livro base *Airline Operations and Scheduling*, o problema de alocação de frotas busca atribuir os tipos de aeronaves às pernas de voo visando minimizar o custo e as perdas de receita (spill). Como temos apenas um tipo (C-97), nosso problema adapta-se a uma **alocação de esquadrões (e suas respectivas bases)** aos voos requeridos para minimizar o custo total de operação, que é proporcional à distância voada.
cells.append(nbf.v4.new_markdown_cell("""## 4. Fleet Assignment Model (FAM) - Nível Brasil
Conforme o MODELO 1 solicitado (aeronaves podem pernoitar em qualquer hub), não há restrição de base ou esquadrão específico. Toda a frota de C-97 opera a nível nacional.
**Modelo Matemático:**
- **Variáveis de decisão:** $X_{s, r}$ (Quantidade de voos operados pelo esquadrão $s$ no trecho $r$)
- **Função Objetivo:** Minimizar a distância total (Distância de posicionamento da base + Distância do trecho + Retorno para a base).
- **Variáveis de decisão:** $X_r$ (Quantidade de voos alocados no trecho $r$)
- **Função Objetivo:** Minimizar a distância total da operação (Distância do trecho $\\times$ voos no trecho). Como as aeronaves não precisam retornar a uma base fixa, desconsideramos voos de posicionamento.
- **Restrições:**
1. O número de voos alocados para o trecho deve suprir a demanda média (Média PAX / Capacidade do C-97).
2. O total de voos atribuídos a um esquadrão não pode exceder o número de aeronaves disponíveis naquele esquadrão no dia.
1. O número de voos alocados para o trecho deve suprir a demanda diária operada daquela rota.
2. O total de voos atribuídos no dia não pode exceder o total da frota de C-97 disponível (14 aeronaves).
"""))
# Cell 10: Code FAM
cells.append(nbf.v4.new_code_cell("""# Preparação dos dados para o modelo
rotas_lista = top_trechos['Trecho'].tolist()
# Dicionário de distâncias
# Dicionário de distâncias apenas dos trechos diretos
def calc_distancia(icao1, icao2):
try:
lat1, lon1, alt1 = df_aeroportos.loc[icao1, ['lat', 'lon', 'elevation']]
@@ -230,52 +230,46 @@ def calc_distancia(icao1, icao2):
distancias_voo = {}
for r in rotas_lista:
origem, destino = r.split('-')
dist_trecho = calc_distancia(origem, destino)
for esq, base in bases_esquadroes.items():
dist_ida = calc_distancia(base, origem) if base != origem else 0
dist_volta = calc_distancia(destino, base) if base != destino else 0
distancias_voo[(esq, r)] = dist_ida + dist_trecho + dist_volta
distancias_voo[r] = calc_distancia(origem, destino)
# Demanda de voos por rota (Média diária)
# Demanda de voos por rota
voos_requeridos = {}
for _, row in top_trechos.iterrows():
# Arredondamento para cima da (demanda / capacidade)
voos = math.ceil(row['Media_PAX_Dias_Operados'] / CAPACIDADE_C97)
# Pelo menos 1 voo para suprir a demanda da rota selecionada
voos_requeridos[row['Trecho']] = max(1, voos)
# MODELAGEM COM PULP
prob = pulp.LpProblem("Fleet_Assignment_C97", pulp.LpMinimize)
prob = pulp.LpProblem("Fleet_Assignment_C97_Brasil", pulp.LpMinimize)
# Variáveis
X = pulp.LpVariable.dicts("X", [(esq, r) for esq in aeronaves_por_esquadrao.keys() for r in rotas_lista], lowBound=0, cat='Integer')
# Variáveis (número de voos em cada rota)
X = pulp.LpVariable.dicts("X", rotas_lista, lowBound=0, cat='Integer')
# Função Objetivo
prob += pulp.lpSum([distancias_voo[(esq, r)] * X[(esq, r)] for esq in aeronaves_por_esquadrao.keys() for r in rotas_lista])
# Função Objetivo: Minimizar distância voada nos trechos
prob += pulp.lpSum([distancias_voo[r] * X[r] for r in rotas_lista])
# Restrição 1: Suprir a demanda da rota
# Restrição 1: Suprir a demanda
for r in rotas_lista:
prob += pulp.lpSum([X[(esq, r)] for esq in aeronaves_por_esquadrao.keys()]) >= voos_requeridos[r]
prob += X[r] >= voos_requeridos[r]
# Restrição 2: Limite de aeronaves por esquadrão
for esq in aeronaves_por_esquadrao.keys():
prob += pulp.lpSum([X[(esq, r)] for r in rotas_lista]) <= aeronaves_por_esquadrao[esq]
# Restrição 2: Limite global de frota (nível Brasil)
prob += pulp.lpSum([X[r] for r in rotas_lista]) <= total_aeronaves
# Solução
prob.solve(pulp.PULP_CBC_CMD(msg=False))
# Exibição dos resultados
print("== RESULTADO DO FLEET ASSIGNMENT ==")
print("== RESULTADO DO FLEET ASSIGNMENT (NÍVEL BRASIL) ==")
print(f"Status: {pulp.LpStatus[prob.status]}")
print(f"Distância Total Minimizada: {pulp.value(prob.objective):.2f} km\\n")
print(f"Distância Total da Operação: {pulp.value(prob.objective):.2f} km\\n")
print("Alocações (Voos Diários):")
print("Alocações (Voos Diários Globais):")
voos_totais = 0
for r in rotas_lista:
print(f"\\nTrecho: {r} (Voos necessários: {voos_requeridos[r]})")
for esq in aeronaves_por_esquadrao.keys():
qtd = int(X[(esq, r)].varValue)
if qtd > 0:
print(f" -> {esq} (Base {bases_esquadroes[esq]}): {qtd} voo(s)")
qtd = int(X[r].varValue)
voos_totais += qtd
print(f"Trecho: {r:10} | Demanda Média PAX: {voos_requeridos[r]*CAPACIDADE_C97:3.0f} | Alocados: {qtd} voo(s)")
print(f"\\nTotal de voos alocados na malha diária: {voos_totais} de {total_aeronaves} aeronaves disponíveis.")
"""))
nb.cells = cells