feat: adiciona Modelos 7, 8 e 9 para resolver problema com mix de frota C-97 e C-99 e demanda unificada
This commit is contained in:
254
create_nb.py
254
create_nb.py
@@ -858,6 +858,260 @@ while voos_para_fazer_c99_6:
|
|||||||
print(tabulate(tabela_horarios_c99_6, headers=["Aeronave", "Origem", "Destino", "Tipo", "Partida", "Chegada", "Duração"], tablefmt="github"))
|
print(tabulate(tabela_horarios_c99_6, headers=["Aeronave", "Origem", "Destino", "Tipo", "Partida", "Chegada", "Duração"], tablefmt="github"))
|
||||||
"""))
|
"""))
|
||||||
|
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# SEÇÃO MIX C-97 + C-99A
|
||||||
|
# ==========================================
|
||||||
|
cells.append(nbf.v4.new_markdown_cell("""# Alocação de Frotas - Mix (C-97 + C-99A)
|
||||||
|
Agora vamos analisar a demanda de forma unificada (soma dos passageiros transportados por C-97 e C-99A) e colocar as duas frotas para trabalhar em conjunto nos Modelos 7, 8 e 9.
|
||||||
|
"""))
|
||||||
|
|
||||||
|
cells.append(nbf.v4.new_code_cell("""# 1. Filtro e Limpeza Unificados (Mix)
|
||||||
|
df_mix = df[df['Modelo'].isin(['C-97', 'C-99A'])].copy()
|
||||||
|
df_mix = df_mix[df_mix['Localidade Decolagem'] != 0]
|
||||||
|
df_mix = df_mix[df_mix['Localidade Pouso'] != 0]
|
||||||
|
df_mix['Trecho'] = df_mix['Localidade Decolagem'] + '-' + df_mix['Localidade Pouso']
|
||||||
|
df_mix['PAX'] = pd.to_numeric(df_mix['PAX']).fillna(0)
|
||||||
|
df_mix['Dep TimeStamp'] = pd.to_datetime(df_mix['Data de Decolagem'] + ' ' + df_mix['Hora de Decolagem'], format='mixed', dayfirst=True)
|
||||||
|
df_mix['Data Apenas'] = df_mix['Dep TimeStamp'].dt.date
|
||||||
|
|
||||||
|
# Demanda diária
|
||||||
|
demanda_diaria_mix = df_mix.groupby(['Trecho', 'Data Apenas'])['PAX'].sum().reset_index()
|
||||||
|
|
||||||
|
todos_trechos_mix = demanda_diaria_mix['Trecho'].unique()
|
||||||
|
idx_mix = pd.MultiIndex.from_product([todos_trechos_mix, datas_2025], names=['Trecho', 'Data Apenas'])
|
||||||
|
demanda_diaria_completa_mix = pd.DataFrame(index=idx_mix).reset_index()
|
||||||
|
demanda_diaria_completa_mix = pd.merge(demanda_diaria_completa_mix, demanda_diaria_mix, on=['Trecho', 'Data Apenas'], how='left').fillna({'PAX': 0})
|
||||||
|
|
||||||
|
estatisticas_trechos_mix = demanda_diaria_completa_mix.groupby('Trecho').agg(
|
||||||
|
Total_PAX=('PAX', 'sum'),
|
||||||
|
Dias_Operados=('PAX', lambda x: (x > 0).sum())
|
||||||
|
).reset_index()
|
||||||
|
|
||||||
|
estatisticas_trechos_mix['Media_PAX_Dias_Operados'] = estatisticas_trechos_mix.apply(
|
||||||
|
lambda row: row['Total_PAX'] / row['Dias_Operados'] if row['Dias_Operados'] > 0 else 0, axis=1
|
||||||
|
)
|
||||||
|
estatisticas_trechos_mix['Score_Relevancia'] = estatisticas_trechos_mix['Total_PAX'] * estatisticas_trechos_mix['Dias_Operados']
|
||||||
|
estatisticas_trechos_mix = estatisticas_trechos_mix.sort_values(by='Score_Relevancia', ascending=False)
|
||||||
|
|
||||||
|
NUM_TRECHOS_MIX = 10
|
||||||
|
top_trechos_mix = estatisticas_trechos_mix.head(NUM_TRECHOS_MIX)
|
||||||
|
|
||||||
|
display(top_trechos_mix)
|
||||||
|
|
||||||
|
plt.figure(figsize=(10, 6))
|
||||||
|
sns.barplot(data=top_trechos_mix, x='Trecho', y='Score_Relevancia', hue='Trecho', palette='cubehelix', legend=False)
|
||||||
|
plt.title(f'Top {NUM_TRECHOS_MIX} Trechos MIX (C-97 + C-99A)')
|
||||||
|
plt.xticks(rotation=45)
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
# Rede Mix
|
||||||
|
G_mix = nx.DiGraph()
|
||||||
|
for _, row in top_trechos_mix.iterrows():
|
||||||
|
origem, destino = row['Trecho'].split('-')
|
||||||
|
G_mix.add_edge(origem, destino, weight=row['Score_Relevancia'])
|
||||||
|
pos_mix = nx.spring_layout(G_mix, k=2.5, iterations=100, seed=42)
|
||||||
|
plt.figure(figsize=(12, 8))
|
||||||
|
nx.draw_networkx_nodes(G_mix, pos_mix, node_size=1200, node_color='plum', edgecolors='black')
|
||||||
|
nx.draw_networkx_edges(G_mix, pos_mix, edgelist=G_mix.edges(), arrows=True, arrowstyle='-|>', arrowsize=25, edge_color='gray', width=2, node_size=1200, connectionstyle='arc3,rad=0.15')
|
||||||
|
nx.draw_networkx_labels(G_mix, pos_mix, font_size=12, font_weight='bold')
|
||||||
|
plt.title('Diagrama de Rede - Unificada', fontsize=14)
|
||||||
|
plt.axis('off')
|
||||||
|
plt.show()
|
||||||
|
"""))
|
||||||
|
|
||||||
|
cells.append(nbf.v4.new_markdown_cell("""## MODELO 7: Minimização de Distância (Custo) com Mix de Frota
|
||||||
|
A malha será atendida pelas duas aeronaves. O modelo decide quem alocar para cobrir a **demanda em passageiros** de cada rota, visando minimizar o número total de voos * distância (simplificação de custo de combustível), e não exceder o tamanho da frota física de cada modelo.
|
||||||
|
"""))
|
||||||
|
|
||||||
|
cells.append(nbf.v4.new_code_cell("""# Preparação Modelo 7
|
||||||
|
rotas_lista_mix = top_trechos_mix['Trecho'].tolist()
|
||||||
|
|
||||||
|
distancias_voo_mix = {}
|
||||||
|
for r in rotas_lista_mix:
|
||||||
|
origem, destino = r.split('-')
|
||||||
|
distancias_voo_mix[r] = calc_distancia(origem, destino)
|
||||||
|
|
||||||
|
demanda_pax_mix = {}
|
||||||
|
for _, row in top_trechos_mix.iterrows():
|
||||||
|
demanda_pax_mix[row['Trecho']] = row['Media_PAX_Dias_Operados']
|
||||||
|
|
||||||
|
prob7 = pulp.LpProblem("Fleet_Assignment_Mix_Brasil", pulp.LpMinimize)
|
||||||
|
|
||||||
|
X_97 = pulp.LpVariable.dicts("X_97", rotas_lista_mix, lowBound=0, cat='Integer')
|
||||||
|
X_99 = pulp.LpVariable.dicts("X_99", rotas_lista_mix, lowBound=0, cat='Integer')
|
||||||
|
|
||||||
|
# Minimizar distância combinada
|
||||||
|
prob7 += pulp.lpSum([distancias_voo_mix[r] * (X_97[r] + X_99[r]) for r in rotas_lista_mix])
|
||||||
|
|
||||||
|
# Restrição de demanda (Em PAX)
|
||||||
|
for r in rotas_lista_mix:
|
||||||
|
prob7 += (X_97[r] * CAPACIDADE_C97) + (X_99[r] * CAPACIDADE_C99) >= demanda_pax_mix[r]
|
||||||
|
|
||||||
|
# Restrição de uso da frota global para o dia (simplificado a 1 perna por aeronave aqui)
|
||||||
|
prob7 += pulp.lpSum([X_97[r] for r in rotas_lista_mix]) <= total_aeronaves
|
||||||
|
prob7 += pulp.lpSum([X_99[r] for r in rotas_lista_mix]) <= total_aeronaves_c99
|
||||||
|
|
||||||
|
prob7.solve(pulp.PULP_CBC_CMD(msg=False))
|
||||||
|
|
||||||
|
print("== MODELO 7 (MIX C-97 e C-99A) ==")
|
||||||
|
print(f"Status: {pulp.LpStatus[prob7.status]}")
|
||||||
|
print(f"Distância Total da Operação Combinada: {pulp.value(prob7.objective):.2f} km\\n")
|
||||||
|
|
||||||
|
for r in rotas_lista_mix:
|
||||||
|
c97_alloc = int(X_97[r].varValue)
|
||||||
|
c99_alloc = int(X_99[r].varValue)
|
||||||
|
capacidade_gerada = (c97_alloc * CAPACIDADE_C97) + (c99_alloc * CAPACIDADE_C99)
|
||||||
|
print(f"Trecho: {r:10} | Demanda PAX: {demanda_pax_mix[r]:.0f} | Oferecido: {capacidade_gerada} | Alocados -> C-97: {c97_alloc}, C-99A: {c99_alloc}")
|
||||||
|
"""))
|
||||||
|
|
||||||
|
|
||||||
|
cells.append(nbf.v4.new_markdown_cell("""## MODELO 8: Minimização Absoluta de Frota Mix (S/ TAT)
|
||||||
|
Diferente do Modelo 7, permitimos ciclos completos (vários voos por aeronave no dia) para minimizar a quantidade real de fuselagens exigidas para a operação. O modelo fará os reposicionamentos vazios necessários e alocará o voo ao tipo ideal.
|
||||||
|
"""))
|
||||||
|
|
||||||
|
cells.append(nbf.v4.new_code_cell("""# Malha Aeroportuária
|
||||||
|
aeroportos_malha_mix = set()
|
||||||
|
for r in rotas_lista_mix:
|
||||||
|
o, d = r.split('-')
|
||||||
|
aeroportos_malha_mix.add(o)
|
||||||
|
aeroportos_malha_mix.add(d)
|
||||||
|
aeroportos_malha_mix = list(aeroportos_malha_mix)
|
||||||
|
|
||||||
|
# Matrizes de tempo exclusivas de cada aeronave na malha mix
|
||||||
|
t_voo_97 = {}
|
||||||
|
t_voo_99 = {}
|
||||||
|
for o in aeroportos_malha_mix:
|
||||||
|
for d in aeroportos_malha_mix:
|
||||||
|
if o == d:
|
||||||
|
t_voo_97[(o, d)] = 0
|
||||||
|
t_voo_99[(o, d)] = 0
|
||||||
|
else:
|
||||||
|
dist = calc_distancia(o, d)
|
||||||
|
t_voo_97[(o, d)] = int(round((dist / vel_media_c97) * 60))
|
||||||
|
t_voo_99[(o, d)] = int(round((dist / vel_media_c99) * 60))
|
||||||
|
|
||||||
|
prob8 = pulp.LpProblem("Minimizar_Frota_Mix", pulp.LpMinimize)
|
||||||
|
|
||||||
|
# D = Voos de Demanda (agora variável)
|
||||||
|
D_97 = pulp.LpVariable.dicts("D_97", [(o, d) for o in aeroportos_malha_mix for d in aeroportos_malha_mix], lowBound=0, cat='Integer')
|
||||||
|
D_99 = pulp.LpVariable.dicts("D_99", [(o, d) for o in aeroportos_malha_mix for d in aeroportos_malha_mix], lowBound=0, cat='Integer')
|
||||||
|
|
||||||
|
# Y = Voos de Reposicionamento Vazios
|
||||||
|
Y_97 = pulp.LpVariable.dicts("Y_97", [(o, d) for o in aeroportos_malha_mix for d in aeroportos_malha_mix], lowBound=0, cat='Integer')
|
||||||
|
Y_99 = pulp.LpVariable.dicts("Y_99", [(o, d) for o in aeroportos_malha_mix for d in aeroportos_malha_mix], lowBound=0, cat='Integer')
|
||||||
|
|
||||||
|
N_97 = pulp.LpVariable("N_97", lowBound=0, cat='Integer')
|
||||||
|
N_99 = pulp.LpVariable("N_99", lowBound=0, cat='Integer')
|
||||||
|
|
||||||
|
# Objetivo: Minimizar a soma de aeronaves.
|
||||||
|
# Adicionamos pequenas penalidades para desempate:
|
||||||
|
# 1. Preferir tempo de voo menor nos reposicionamentos
|
||||||
|
# 2. C-99 é mais cara/maior, penalidade irrisória para preferir C-97 se ambas puderem (custo indireto)
|
||||||
|
penalidade_voo_97 = pulp.lpSum([(D_97[(i, j)] + Y_97[(i, j)]) * t_voo_97[(i, j)] for i in aeroportos_malha_mix for j in aeroportos_malha_mix])
|
||||||
|
penalidade_voo_99 = pulp.lpSum([(D_99[(i, j)] + Y_99[(i, j)]) * t_voo_99[(i, j)] for i in aeroportos_malha_mix for j in aeroportos_malha_mix])
|
||||||
|
prob8 += N_97 + N_99 + 0.001 * N_99 + 0.00001 * (penalidade_voo_97 + penalidade_voo_99)
|
||||||
|
|
||||||
|
# Restrição de Demanda em PAX
|
||||||
|
# Todas as rotas que não são Top10 tem demanda = 0
|
||||||
|
for i in aeroportos_malha_mix:
|
||||||
|
for j in aeroportos_malha_mix:
|
||||||
|
r_name = f"{i}-{j}"
|
||||||
|
demanda = demanda_pax_mix.get(r_name, 0)
|
||||||
|
prob8 += (D_97[(i, j)] * CAPACIDADE_C97) + (D_99[(i, j)] * CAPACIDADE_C99) >= demanda
|
||||||
|
|
||||||
|
# Restrições de Fluxo Circulares (separados por tipo)
|
||||||
|
for no in aeroportos_malha_mix:
|
||||||
|
# C-97
|
||||||
|
chegadas_97 = pulp.lpSum([D_97[(i, no)] + Y_97[(i, no)] for i in aeroportos_malha_mix])
|
||||||
|
partidas_97 = pulp.lpSum([D_97[(no, j)] + Y_97[(no, j)] for j in aeroportos_malha_mix])
|
||||||
|
prob8 += chegadas_97 == partidas_97
|
||||||
|
|
||||||
|
# C-99
|
||||||
|
chegadas_99 = pulp.lpSum([D_99[(i, no)] + Y_99[(i, no)] for i in aeroportos_malha_mix])
|
||||||
|
partidas_99 = pulp.lpSum([D_99[(no, j)] + Y_99[(no, j)] for j in aeroportos_malha_mix])
|
||||||
|
prob8 += chegadas_99 == partidas_99
|
||||||
|
|
||||||
|
# Restrições de tempo operacional diário
|
||||||
|
prob8 += pulp.lpSum([(D_97[(i, j)] + Y_97[(i, j)]) * t_voo_97[(i, j)] for i in aeroportos_malha_mix for j in aeroportos_malha_mix]) <= N_97 * 1440
|
||||||
|
prob8 += pulp.lpSum([(D_99[(i, j)] + Y_99[(i, j)]) * t_voo_99[(i, j)] for i in aeroportos_malha_mix for j in aeroportos_malha_mix]) <= N_99 * 1440
|
||||||
|
|
||||||
|
# Restrições Físicas
|
||||||
|
prob8 += N_97 <= total_aeronaves
|
||||||
|
prob8 += N_99 <= total_aeronaves_c99
|
||||||
|
|
||||||
|
prob8.solve(pulp.PULP_CBC_CMD(msg=False))
|
||||||
|
|
||||||
|
print("\\n== RESULTADO DO MODELO 8 (MIX SEM TAT) ==")
|
||||||
|
print(f"Status: {pulp.LpStatus[prob8.status]}")
|
||||||
|
print(f"Aeronaves C-97 necessárias: {int(N_97.varValue)}")
|
||||||
|
print(f"Aeronaves C-99A necessárias: {int(N_99.varValue)}")
|
||||||
|
print(f"Frota total mobilizada: {int(N_97.varValue) + int(N_99.varValue)} aeronaves\\n")
|
||||||
|
|
||||||
|
print("Alocações para satisfazer a Demanda:")
|
||||||
|
for r in rotas_lista_mix:
|
||||||
|
origem, destino = r.split('-')
|
||||||
|
d97_val = int(D_97[(origem, destino)].varValue)
|
||||||
|
d99_val = int(D_99[(origem, destino)].varValue)
|
||||||
|
print(f" -> Trecho {r:10}: C-97 faz {d97_val} voo(s) | C-99A faz {d99_val} voo(s)")
|
||||||
|
"""))
|
||||||
|
|
||||||
|
cells.append(nbf.v4.new_markdown_cell("""## MODELO 9: Minimização Absoluta de Frota Mix (C/ TAT 40 MIN)
|
||||||
|
Por fim, a versão que soma a demanda, usa as duas frotas simultaneamente, permite múltiplos voos por dia, gera reposicionamentos e pune **40 minutos** no chão (TAT) para cada perna voada por qualquer um dos aviões.
|
||||||
|
"""))
|
||||||
|
|
||||||
|
cells.append(nbf.v4.new_code_cell("""# Modelo 9
|
||||||
|
prob9 = pulp.LpProblem("Minimizar_Frota_Mix_TAT", pulp.LpMinimize)
|
||||||
|
|
||||||
|
D_97_t = pulp.LpVariable.dicts("D_97t", [(o, d) for o in aeroportos_malha_mix for d in aeroportos_malha_mix], lowBound=0, cat='Integer')
|
||||||
|
D_99_t = pulp.LpVariable.dicts("D_99t", [(o, d) for o in aeroportos_malha_mix for d in aeroportos_malha_mix], lowBound=0, cat='Integer')
|
||||||
|
Y_97_t = pulp.LpVariable.dicts("Y_97t", [(o, d) for o in aeroportos_malha_mix for d in aeroportos_malha_mix], lowBound=0, cat='Integer')
|
||||||
|
Y_99_t = pulp.LpVariable.dicts("Y_99t", [(o, d) for o in aeroportos_malha_mix for d in aeroportos_malha_mix], lowBound=0, cat='Integer')
|
||||||
|
|
||||||
|
N_97_t = pulp.LpVariable("N_97t", lowBound=0, cat='Integer')
|
||||||
|
N_99_t = pulp.LpVariable("N_99t", lowBound=0, cat='Integer')
|
||||||
|
|
||||||
|
TAT_min = 40
|
||||||
|
|
||||||
|
penalidade_97 = pulp.lpSum([(D_97_t[(i, j)] + Y_97_t[(i, j)]) * t_voo_97[(i, j)] for i in aeroportos_malha_mix for j in aeroportos_malha_mix])
|
||||||
|
penalidade_99 = pulp.lpSum([(D_99_t[(i, j)] + Y_99_t[(i, j)]) * t_voo_99[(i, j)] for i in aeroportos_malha_mix for j in aeroportos_malha_mix])
|
||||||
|
prob9 += N_97_t + N_99_t + 0.001 * N_99_t + 0.00001 * (penalidade_97 + penalidade_99)
|
||||||
|
|
||||||
|
for i in aeroportos_malha_mix:
|
||||||
|
for j in aeroportos_malha_mix:
|
||||||
|
r_name = f"{i}-{j}"
|
||||||
|
demanda = demanda_pax_mix.get(r_name, 0)
|
||||||
|
prob9 += (D_97_t[(i, j)] * CAPACIDADE_C97) + (D_99_t[(i, j)] * CAPACIDADE_C99) >= demanda
|
||||||
|
|
||||||
|
for no in aeroportos_malha_mix:
|
||||||
|
prob9 += pulp.lpSum([D_97_t[(i, no)] + Y_97_t[(i, no)] for i in aeroportos_malha_mix]) == pulp.lpSum([D_97_t[(no, j)] + Y_97_t[(no, j)] for j in aeroportos_malha_mix])
|
||||||
|
prob9 += pulp.lpSum([D_99_t[(i, no)] + Y_99_t[(i, no)] for i in aeroportos_malha_mix]) == pulp.lpSum([D_99_t[(no, j)] + Y_99_t[(no, j)] for j in aeroportos_malha_mix])
|
||||||
|
|
||||||
|
# TAT INCLUSO AQUI
|
||||||
|
prob9 += pulp.lpSum([(D_97_t[(i, j)] + Y_97_t[(i, j)]) * (t_voo_97[(i, j)] + TAT_min) for i in aeroportos_malha_mix for j in aeroportos_malha_mix]) <= N_97_t * 1440
|
||||||
|
prob9 += pulp.lpSum([(D_99_t[(i, j)] + Y_99_t[(i, j)]) * (t_voo_99[(i, j)] + TAT_min) for i in aeroportos_malha_mix for j in aeroportos_malha_mix]) <= N_99_t * 1440
|
||||||
|
|
||||||
|
prob9 += N_97_t <= total_aeronaves
|
||||||
|
prob9 += N_99_t <= total_aeronaves_c99
|
||||||
|
|
||||||
|
prob9.solve(pulp.PULP_CBC_CMD(msg=False))
|
||||||
|
|
||||||
|
print("\\n== RESULTADO DO MODELO 9 (MIX COM 40 MIN TAT) ==")
|
||||||
|
print(f"Status: {pulp.LpStatus[prob9.status]}")
|
||||||
|
print(f"Aeronaves C-97 necessárias: {int(N_97_t.varValue)}")
|
||||||
|
print(f"Aeronaves C-99A necessárias: {int(N_99_t.varValue)}")
|
||||||
|
print(f"Frota total mobilizada: {int(N_97_t.varValue) + int(N_99_t.varValue)} aeronaves\\n")
|
||||||
|
|
||||||
|
tempo_97_voo = sum([(int(D_97_t[(i, j)].varValue) + int(Y_97_t[(i, j)].varValue)) * t_voo_97[(i, j)] for i in aeroportos_malha_mix for j in aeroportos_malha_mix])
|
||||||
|
tempo_99_voo = sum([(int(D_99_t[(i, j)].varValue) + int(Y_99_t[(i, j)].varValue)) * t_voo_99[(i, j)] for i in aeroportos_malha_mix for j in aeroportos_malha_mix])
|
||||||
|
|
||||||
|
total_voos = sum([(int(D_97_t[(i, j)].varValue) + int(Y_97_t[(i, j)].varValue) + int(D_99_t[(i, j)].varValue) + int(Y_99_t[(i, j)].varValue)) for i in aeroportos_malha_mix for j in aeroportos_malha_mix])
|
||||||
|
|
||||||
|
print(f"Tempo total de voo da frota combinada: {tempo_97_voo + tempo_99_voo} min ({(tempo_97_voo + tempo_99_voo)/60:.2f} h)")
|
||||||
|
print(f"Tempo de solo desperdiçado (TAT 40 min em {total_voos} voos): {total_voos * TAT_min} min ({(total_voos * TAT_min)/60:.2f} h)")
|
||||||
|
"""))
|
||||||
|
|
||||||
nb.cells = cells
|
nb.cells = cells
|
||||||
|
|
||||||
with open('modelos.ipynb', 'w', encoding='utf-8') as f:
|
with open('modelos.ipynb', 'w', encoding='utf-8') as f:
|
||||||
|
|||||||
786
modelos.ipynb
786
modelos.ipynb
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user