import nbformat as nbf nb = nbf.v4.new_notebook() cells = [] # Cell 1: Markdown Introduction cells.append(nbf.v4.new_markdown_cell("""# Alocação de Frotas (Fleet Scheduling) - Aeronaves C-97 Este projeto visa realizar a alocação de frota (Fleet Assignment) baseada nos princípios do livro referenciado *Airline Operations and Scheduling*. Diferentemente do modelo anterior que utilizava aeronaves de carga, este foca exclusivamente na aeronave de passageiros **C-97**. ## Objetivos: 1. Remover cálculos de aeronaves de carga. 2. Analisar as demandas diárias para a aeronave C-97. 3. Identificar os esquadrões detentores dessas aeronaves e a quantidade de aeronaves (matrículas únicas). 4. Calcular os 5 trechos mais relevantes do ano de 2025 utilizando estatística e gráficos. 5. Realizar o Fleet Assignment para os 5 trechos, visando minimizar o custo total da operação (Modelo 1 proposto). """)) # Cell 2: Imports cells.append(nbf.v4.new_code_cell("""# Importação de bibliotecas import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns import pulp import pymap3d as pm import math import networkx as nx # Configuração de estilo para os gráficos sns.set_theme(style="whitegrid") # Constantes globais CSV_FILEPATH = "SISCO_AEREA_01-01-2025_A_31-12-2025_1911251714Z.csv" JSON_AEROPORTOS = "airports.json" CAPACIDADE_C97 = 30 # Capacidade média de passageiros do C-97 (Brasília) """)) # Cell 3: Markdown Load Data cells.append(nbf.v4.new_markdown_cell("""## 1. Leitura e Limpeza dos Dados Vamos carregar os dados, filtrar apenas para o modelo `C-97` e tratar os campos de localidades e datas. """)) # Cell 4: Code Load Data cells.append(nbf.v4.new_code_cell("""# Carregamento do banco de dados df = pd.read_csv(CSV_FILEPATH, low_memory=False) df_aeroportos = pd.read_json(JSON_AEROPORTOS, orient='index') # Tratamento básico de valores nulos expressos como 'NIL' df.replace('NIL', 0, inplace=True) # Filtro para a aeronave C-97 (Passageiros) e remoção de registros de carga df_c97 = df[df['Modelo'] == 'C-97'].copy() # Filtrando decolagens e pousos conhecidos df_c97 = df_c97[df_c97['Localidade Decolagem'] != 0] df_c97 = df_c97[df_c97['Localidade Pouso'] != 0] # Criação da coluna Trecho df_c97['Trecho'] = df_c97['Localidade Decolagem'] + '-' + df_c97['Localidade Pouso'] # Conversão da coluna PAX para numérico df_c97['PAX'] = pd.to_numeric(df_c97['PAX']).fillna(0) # Tratamento de datas df_c97['Dep TimeStamp'] = pd.to_datetime(df_c97['Data de Decolagem'] + ' ' + df_c97['Hora de Decolagem'], format='mixed', dayfirst=True) df_c97['Data Apenas'] = df_c97['Dep TimeStamp'].dt.date """)) # Cell 5: Markdown Squadrons cells.append(nbf.v4.new_markdown_cell("""## 2. Análise dos Esquadrões e Matrículas (Aeronaves) Identificaremos quais esquadrões operam o C-97 e qual a disponibilidade de aeronaves (número de matrículas únicas), pois isso definirá as restrições de frota. """)) # Cell 6: Code Squadrons cells.append(nbf.v4.new_code_cell("""# Obtendo esquadrões e matrículas únicas esquadroes = df_c97['Emissor'].unique() matriculas_unicas = df_c97['Matrícula'].unique() total_aeronaves = len(matriculas_unicas) print(f"Esquadrões que operam o C-97: {', '.join(esquadroes)}") print(f"Total de matrículas únicas (aeronaves disponíveis): {total_aeronaves}") # Aeronaves por esquadrão aeronaves_por_esquadrao = df_c97.groupby('Emissor')['Matrícula'].nunique().to_dict() print("\\nAeronaves por esquadrão:") for esq, qtd in aeronaves_por_esquadrao.items(): print(f" - {esq}: {qtd} aeronaves") # Identificar a base principal de cada esquadrão (aeroporto com mais decolagens) bases_esquadroes = {} for esq in aeronaves_por_esquadrao.keys(): base = df_c97[df_c97['Emissor'] == esq]['Localidade Decolagem'].mode()[0] bases_esquadroes[esq] = base print("\\nBases inferidas por esquadrão (pela moda de decolagens):") for esq, base in bases_esquadroes.items(): print(f" - {esq}: {base}") """)) # Cell 7: Markdown Stats cells.append(nbf.v4.new_markdown_cell("""## 3. Análise Estatística das Demandas Diárias Para definir os trechos mais relevantes do ano, calcularemos a demanda de passageiros (PAX) agregada diariamente. Em seguida, extraímos a **média de passageiros diários** e o **desvio padrão** para cada trecho. A relevância será determinada pela maior média de demanda diária de PAX. Começaremos focando em 5 trechos (alvo variável). """)) # Cell 8: Code Stats cells.append(nbf.v4.new_code_cell("""# Demanda diária por trecho demanda_diaria = df_c97.groupby(['Trecho', 'Data Apenas'])['PAX'].sum().reset_index() # Criar um range de datas para todo o ano de 2025 para contabilizar os dias sem voo import itertools datas_2025 = pd.date_range(start='2025-01-01', end='2025-12-31').date todos_trechos = demanda_diaria['Trecho'].unique() idx = pd.MultiIndex.from_product([todos_trechos, datas_2025], names=['Trecho', 'Data Apenas']) demanda_diaria_completa = pd.DataFrame(index=idx).reset_index() # Mesclar com os dados reais e preencher NaN com 0 demanda_diaria_completa = pd.merge(demanda_diaria_completa, demanda_diaria, on=['Trecho', 'Data Apenas'], how='left').fillna({'PAX': 0}) # Estatísticas (Média e Desvio Padrão) por trecho estatisticas_trechos = demanda_diaria_completa.groupby('Trecho').agg( Media_PAX_Diario=('PAX', 'mean'), Desvio_Padrao_PAX=('PAX', 'std'), Total_PAX=('PAX', 'sum'), Dias_Operados=('PAX', lambda x: (x > 0).sum()) ).reset_index() # Média PAX por dias operados (usado para o Fleet Assignment Model) estatisticas_trechos['Media_PAX_Dias_Operados'] = estatisticas_trechos.apply( lambda row: row['Total_PAX'] / row['Dias_Operados'] if row['Dias_Operados'] > 0 else 0, axis=1 ) # Nova métrica solicitada: PAX * Dias_Operados estatisticas_trechos['Score_Relevancia'] = estatisticas_trechos['Total_PAX'] * estatisticas_trechos['Dias_Operados'] # Ordenar pelas rotas com maior métrica (PAX * Dias_Operados) estatisticas_trechos = estatisticas_trechos.sort_values(by='Score_Relevancia', ascending=False) # Definir número de trechos alvo NUM_TRECHOS = 10 top_trechos = estatisticas_trechos.head(NUM_TRECHOS) display(top_trechos) # Gráfico plt.figure(figsize=(10, 6)) sns.barplot(data=top_trechos, x='Trecho', y='Score_Relevancia', hue='Trecho', palette='viridis', legend=False) plt.title(f'Top {NUM_TRECHOS} Trechos Mais Relevantes (PAX * Dias Operados)') plt.ylabel('Score de Relevância (PAX * Dias Operados)') plt.xlabel('Trecho (Origem - Destino)') plt.xticks(rotation=45) plt.tight_layout() plt.show() """)) # Cell: Markdown Diagrama de Rede cells.append(nbf.v4.new_markdown_cell("""## Diagrama de Rede dos Trechos Para melhor visualização dos trechos mais relevantes, abaixo temos um diagrama de rede conectando as origens aos destinos. """)) # Cell: Code Diagrama de Rede cells.append(nbf.v4.new_code_cell("""# Criação do grafo direcionado G = nx.DiGraph() # Adiciona arestas com peso baseado no Score de Relevância for _, row in top_trechos.iterrows(): origem, destino = row['Trecho'].split('-') peso = row['Score_Relevancia'] G.add_edge(origem, destino, weight=peso) # Configuração do layout do grafo - k maior afasta mais os nós pos = nx.spring_layout(G, k=2.5, iterations=100, seed=42) plt.figure(figsize=(12, 8)) # Define o tamanho do nó tamanho_no = 1200 # Desenha os nós nx.draw_networkx_nodes(G, pos, node_size=tamanho_no, node_color='skyblue', edgecolors='black') # Desenha as arestas, informando o node_size para que a seta não fique escondida sob o nó nx.draw_networkx_edges( G, pos, edgelist=G.edges(), arrows=True, arrowstyle='-|>', arrowsize=25, edge_color='gray', width=2, node_size=tamanho_no, connectionstyle='arc3,rad=0.15' ) # Desenha os rótulos (nomes dos aeroportos) nx.draw_networkx_labels(G, pos, font_size=12, font_family='sans-serif', font_weight='bold') plt.title('Diagrama de Rede - Top Trechos Relevantes', fontsize=14) plt.axis('off') plt.tight_layout() plt.show() """)) # Cell 9: Markdown FAM 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_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 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 apenas dos trechos diretos def calc_distancia(icao1, icao2): try: lat1, lon1, alt1 = df_aeroportos.loc[icao1, ['lat', 'lon', 'elevation']] lat2, lon2, alt2 = df_aeroportos.loc[icao2, ['lat', 'lon', 'elevation']] # Retorna a distância oblíqua em km return pm.geodetic2aer(lat1, lon1, alt1, lat2, lon2, alt2)[2] / 1000.0 except KeyError: return 9999.0 # Penalidade caso aeroporto não exista no json distancias_voo = {} for r in rotas_lista: origem, destino = r.split('-') distancias_voo[r] = calc_distancia(origem, destino) # Demanda de voos por rota voos_requeridos = {} for _, row in top_trechos.iterrows(): voos = math.ceil(row['Media_PAX_Dias_Operados'] / CAPACIDADE_C97) voos_requeridos[row['Trecho']] = max(1, voos) # MODELAGEM COM PULP prob = pulp.LpProblem("Fleet_Assignment_C97_Brasil", pulp.LpMinimize) # Variáveis (número de voos em cada rota) X = pulp.LpVariable.dicts("X", rotas_lista, lowBound=0, cat='Integer') # 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 for r in rotas_lista: prob += X[r] >= voos_requeridos[r] # 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 (NÍVEL BRASIL) ==") print(f"Status: {pulp.LpStatus[prob.status]}") print(f"Distância Total da Operação: {pulp.value(prob.objective):.2f} km\\n") print("Alocações (Voos Diários Globais):") voos_totais = 0 for r in rotas_lista: 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.") """)) # Cell 11: Markdown Modelo 2 cells.append(nbf.v4.new_markdown_cell("""## 5. Modelo 2: Minimização de Aeronaves Neste modelo, o objetivo é encontrar o **número mínimo absoluto de aeronaves** necessário para operar a malha diária estabelecida (os top trechos). Considerações solicitadas: - O tempo de voo é estimado usando a velocidade média real do C-97 em 2025. - As aeronaves podem voar até 24 horas por dia (não consideraremos o tempo de solo no chão nesta versão). - Se houver desbalanceamento de malha (mais voos saindo de A do que chegando em A), o modelo criará voos de reposicionamento vazios. """)) # Cell 12: Code Modelo 2 cells.append(nbf.v4.new_code_cell("""# 1. Calcular a velocidade média do C-97 em 2025 # Converte a string 'HH:MM:SS' para timedelta e extrai horas df_c97_spd = df_c97.copy() df_c97_spd['Tempo_Horas'] = pd.to_timedelta(df_c97_spd['Tempo de Voo']).dt.total_seconds() / 3600 def get_dist_row(row): try: lat1, lon1, alt1 = df_aeroportos.loc[row['Localidade Decolagem'], ['lat', 'lon', 'elevation']] lat2, lon2, alt2 = df_aeroportos.loc[row['Localidade Pouso'], ['lat', 'lon', 'elevation']] return pm.geodetic2aer(lat1, lon1, alt1, lat2, lon2, alt2)[2] / 1000.0 except: return np.nan df_c97_spd['Dist_km'] = df_c97_spd.apply(get_dist_row, axis=1) # Filtra voos válidos para calcular a velocidade média df_valid = df_c97_spd[(df_c97_spd['Tempo_Horas'] > 0) & (df_c97_spd['Dist_km'] > 0)].copy() df_valid['Velocidade'] = df_valid['Dist_km'] / df_valid['Tempo_Horas'] vel_media_c97 = df_valid['Velocidade'].mean() print(f"Velocidade média calculada para o C-97: {vel_media_c97:.2f} km/h") # 2. Configurar os nós (aeroportos) e demandas da malha das rotas top aeroportos_malha = set() for r in rotas_lista: o, d = r.split('-') aeroportos_malha.add(o) aeroportos_malha.add(d) aeroportos_malha = list(aeroportos_malha) # Matriz de tempo de voo entre todos os nós da malha em MINUTOS (inteiro) tempos_voo_min = {} for o in aeroportos_malha: for d in aeroportos_malha: if o == d: tempos_voo_min[(o, d)] = 0 else: dist = calc_distancia(o, d) tempos_voo_min[(o, d)] = int(round((dist / vel_media_c97) * 60)) # Demanda fixa D_{i,j} D = {(o, d): 0 for o in aeroportos_malha for d in aeroportos_malha} for r in rotas_lista: o, d = r.split('-') D[(o, d)] = voos_requeridos[r] # 3. Modelagem Matemática (Pulp) prob2 = pulp.LpProblem("Minimizar_Aeronaves", pulp.LpMinimize) # Variáveis de reposicionamento vazio Y_{i,j} Y = pulp.LpVariable.dicts("Y", [(o, d) for o in aeroportos_malha for d in aeroportos_malha], lowBound=0, cat='Integer') # Variável de número de aeronaves N N = pulp.LpVariable("N", lowBound=0, cat='Integer') # Função Objetivo: Minimizar N # Para desempatar soluções com mesmo N, damos um pequeno peso pro tempo de reposicionamento vazio (para pegar o roteamento mais curto também) prob2 += N + 0.0001 * pulp.lpSum([Y[(i, j)] * tempos_voo_min[(i, j)] for i in aeroportos_malha for j in aeroportos_malha]) # Restrição de conservação de fluxo para cada aeroporto (Chegadas == Partidas) for no in aeroportos_malha: chegadas = pulp.lpSum([D[(i, no)] + Y[(i, no)] for i in aeroportos_malha]) partidas = pulp.lpSum([D[(no, j)] + Y[(no, j)] for j in aeroportos_malha]) prob2 += chegadas == partidas # Restrição de tempo (O tempo total de voo da malha não pode exceder N * 1440 min) tempo_total_voo = pulp.lpSum([(D[(i, j)] + Y[(i, j)]) * tempos_voo_min[(i, j)] for i in aeroportos_malha for j in aeroportos_malha]) prob2 += tempo_total_voo <= N * 1440 # Solução prob2.solve(pulp.PULP_CBC_CMD(msg=False)) print("\\n== RESULTADO DO MODELO 2 ==") print(f"Status: {pulp.LpStatus[prob2.status]}") print(f"Número mínimo absoluto de aeronaves C-97 necessárias: {int(N.varValue)}") tempo_gasto = sum([(D[(i, j)] + int(Y[(i, j)].varValue)) * tempos_voo_min[(i, j)] for i in aeroportos_malha for j in aeroportos_malha]) print(f"Tempo total de voo diário (incluindo reposicionamento vazio): {int(tempo_gasto)} min ({tempo_gasto/60:.2f} h)") if N.varValue > 0: print(f"Taxa de ocupação da frota (Tempo Voo / Tempo Disponível): {(tempo_gasto / (N.varValue * 1440)) * 100:.1f}%\\n") print("Voos de Reposicionamento Vazio Gerados (Y) para fechar o ciclo diário:") gerou_vazio = False for i in aeroportos_malha: for j in aeroportos_malha: if Y[(i, j)].varValue > 0: print(f" -> De {i} para {j}: {int(Y[(i, j)].varValue)} voo(s) vazio(s) | Tempo de voo: {tempos_voo_min[(i,j)]*int(Y[(i,j)].varValue)} min") gerou_vazio = True if not gerou_vazio: print(" Nenhum voo de reposicionamento vazio foi necessário! A malha já é circular/balanceada.") # GERAR TABELA DE HORÁRIOS COM TABULATE E DATETIME from tabulate import tabulate import datetime # 1. Construir a lista de todos os voos a serem realizados voos_para_fazer = [] # Cópia para saber quantos voos de passageiros faltam alocar D_copy = {k: v for k, v in D.items()} for i in aeroportos_malha: for j in aeroportos_malha: total_voos = D[(i, j)] + int(Y[(i, j)].varValue) for _ in range(total_voos): if D_copy[(i, j)] > 0: tipo = 'Passageiro' D_copy[(i, j)] -= 1 else: tipo = 'Vazio (Reposicionamento)' voos_para_fazer.append({'origem': i, 'destino': j, 'tipo': tipo, 'tempo': tempos_voo_min[(i, j)]}) # 2. Simular a alocação sequencial de horários tabela_horarios = [] aeronave_id = 1 while voos_para_fazer: voo_atual = voos_para_fazer.pop(0) hora_atual = datetime.datetime(2025, 1, 1, 0, 0, 0) tempo_delta = datetime.timedelta(minutes=int(voo_atual['tempo'])) hora_chegada = hora_atual + tempo_delta tabela_horarios.append([ f"Aeronave {aeronave_id}", voo_atual['origem'], voo_atual['destino'], voo_atual['tipo'], hora_atual.strftime('%H:%M'), hora_chegada.strftime('%H:%M'), f"{voo_atual['tempo']} min" ]) hora_atual = hora_chegada local_atual = voo_atual['destino'] # Continuar traçando o caminho para esta aeronave até que não caiba mais nas 24h while True: prox_voo = None for idx, v in enumerate(voos_para_fazer): if v['origem'] == local_atual: if (hora_atual - datetime.datetime(2025, 1, 1, 0, 0, 0) + datetime.timedelta(minutes=int(v['tempo']))).total_seconds() <= 1440 * 60: prox_voo = voos_para_fazer.pop(idx) break if prox_voo: tempo_delta = datetime.timedelta(minutes=int(prox_voo['tempo'])) hora_chegada = hora_atual + tempo_delta tabela_horarios.append([ f"Aeronave {aeronave_id}", prox_voo['origem'], prox_voo['destino'], prox_voo['tipo'], hora_atual.strftime('%H:%M'), hora_chegada.strftime('%H:%M'), f"{prox_voo['tempo']} min" ]) hora_atual = hora_chegada local_atual = prox_voo['destino'] else: break aeronave_id += 1 print("\\n== QUADRO DE HORÁRIOS DIÁRIOS ==") print(tabulate(tabela_horarios, headers=["Aeronave", "Origem", "Destino", "Tipo", "Partida", "Chegada", "Duração"], tablefmt="github")) """)) # Cell 13: Markdown Modelo 3 cells.append(nbf.v4.new_markdown_cell("""## 6. Modelo 3: Minimização de Aeronaves com TAT (Turnaround Time) Nesta variação do Modelo 2, adicionamos uma restrição crítica da vida real: o Tempo de Solo ou **Turnaround Time (TAT)**. Considerações: - TAT estipulado: 40 minutos (0.66 horas) por cada voo (tempo necessário para desembarque, reabastecimento, verificações e embarque). - O tempo total de dedicação de uma aeronave por ciclo passa a ser a soma do seu Tempo de Voo + 40 minutos de TAT no aeroporto de destino. """)) # Cell 14: Code Modelo 3 cells.append(nbf.v4.new_code_cell("""# 3. Modelagem Matemática Modelo 3 (Pulp) com TAT prob3 = pulp.LpProblem("Minimizar_Aeronaves_TAT", pulp.LpMinimize) # Variáveis Y3 = pulp.LpVariable.dicts("Y3", [(o, d) for o in aeroportos_malha for d in aeroportos_malha], lowBound=0, cat='Integer') N3 = pulp.LpVariable("N3", lowBound=0, cat='Integer') TAT_min = 40 # Função Objetivo prob3 += N3 + 0.0001 * pulp.lpSum([Y3[(i, j)] * tempos_voo_min[(i, j)] for i in aeroportos_malha for j in aeroportos_malha]) # Restrição de fluxo for no in aeroportos_malha: chegadas = pulp.lpSum([D[(i, no)] + Y3[(i, no)] for i in aeroportos_malha]) partidas = pulp.lpSum([D[(no, j)] + Y3[(no, j)] for j in aeroportos_malha]) prob3 += chegadas == partidas # Restrição de tempo com TAT # Cada voo (D ou Y) consome tempo de voo + TAT tempo_total_operacional = pulp.lpSum([(D[(i, j)] + Y3[(i, j)]) * (tempos_voo_min[(i, j)] + TAT_min) for i in aeroportos_malha for j in aeroportos_malha]) prob3 += tempo_total_operacional <= N3 * 1440 # Solução prob3.solve(pulp.PULP_CBC_CMD(msg=False)) print("\\n== RESULTADO DO MODELO 3 (COM 40 MIN DE SOLO) ==") print(f"Status: {pulp.LpStatus[prob3.status]}") print(f"Número mínimo absoluto de aeronaves C-97 necessárias: {int(N3.varValue)}") tempo_voo_puro = sum([(D[(i, j)] + int(Y3[(i, j)].varValue)) * tempos_voo_min[(i, j)] for i in aeroportos_malha for j in aeroportos_malha]) total_voos_realizados = sum([(D[(i, j)] + int(Y3[(i, j)].varValue)) for i in aeroportos_malha for j in aeroportos_malha]) tempo_solo_total = total_voos_realizados * TAT_min print(f"Tempo total de voo puro: {tempo_voo_puro} min ({tempo_voo_puro/60:.2f} h)") print(f"Tempo total gasto em solo (TAT): {tempo_solo_total} min ({tempo_solo_total/60:.2f} h)") print(f"Tempo total operacional comprometido: {tempo_voo_puro + tempo_solo_total} min ({(tempo_voo_puro + tempo_solo_total)/60:.2f} h)") if N3.varValue > 0: print(f"Taxa de ocupação da frota (Tempo Operacional / Tempo Disponível): {((tempo_voo_puro + tempo_solo_total) / (N3.varValue * 1440)) * 100:.1f}%\\n") # GERAR TABELA DE HORÁRIOS COM TABULATE E DATETIME voos_para_fazer3 = [] D_copy3 = {k: v for k, v in D.items()} for i in aeroportos_malha: for j in aeroportos_malha: total_voos3 = D[(i, j)] + int(Y3[(i, j)].varValue) for _ in range(total_voos3): if D_copy3[(i, j)] > 0: tipo = 'Passageiro' D_copy3[(i, j)] -= 1 else: tipo = 'Vazio (Reposicionamento)' voos_para_fazer3.append({'origem': i, 'destino': j, 'tipo': tipo, 'tempo': tempos_voo_min[(i, j)]}) tabela_horarios3 = [] aeronave_id3 = 1 while voos_para_fazer3: voo_atual = voos_para_fazer3.pop(0) hora_atual = datetime.datetime(2025, 1, 1, 0, 0, 0) tempo_delta = datetime.timedelta(minutes=int(voo_atual['tempo'])) hora_chegada = hora_atual + tempo_delta tabela_horarios3.append([ f"Aeronave {aeronave_id3}", voo_atual['origem'], voo_atual['destino'], voo_atual['tipo'], hora_atual.strftime('%H:%M'), hora_chegada.strftime('%H:%M'), f"{voo_atual['tempo']} min" ]) # Próxima partida só depois do TAT de 40 min hora_atual = hora_chegada + datetime.timedelta(minutes=40) local_atual = voo_atual['destino'] while True: prox_voo = None for idx, v in enumerate(voos_para_fazer3): if v['origem'] == local_atual: if (hora_atual - datetime.datetime(2025, 1, 1, 0, 0, 0) + datetime.timedelta(minutes=int(v['tempo']))).total_seconds() <= 1440 * 60: prox_voo = voos_para_fazer3.pop(idx) break if prox_voo: tempo_delta = datetime.timedelta(minutes=int(prox_voo['tempo'])) hora_chegada = hora_atual + tempo_delta tabela_horarios3.append([ f"Aeronave {aeronave_id3}", prox_voo['origem'], prox_voo['destino'], prox_voo['tipo'], hora_atual.strftime('%H:%M'), hora_chegada.strftime('%H:%M'), f"{prox_voo['tempo']} min" ]) hora_atual = hora_chegada + datetime.timedelta(minutes=40) local_atual = prox_voo['destino'] else: break aeronave_id3 += 1 print("== QUADRO DE HORÁRIOS DIÁRIOS (COM 40 MIN TAT) ==") print(tabulate(tabela_horarios3, headers=["Aeronave", "Origem", "Destino", "Tipo", "Partida", "Chegada", "Duração Voo"], tablefmt="github")) """)) # ========================================== # SEÇÃO C-99 # ========================================== cells.append(nbf.v4.new_markdown_cell("""# Alocação de Frotas - Aeronaves C-99A Agora replicaremos a mesma lógica e os mesmos modelos (1, 2 e 3) para a aeronave C-99A (que será nomeada como Modelos 4, 5 e 6). """)) cells.append(nbf.v4.new_code_cell("""# 1. Filtro e Limpeza C-99A df_c99 = df[df['Modelo'] == 'C-99A'].copy() df_c99 = df_c99[df_c99['Localidade Decolagem'] != 0] df_c99 = df_c99[df_c99['Localidade Pouso'] != 0] df_c99['Trecho'] = df_c99['Localidade Decolagem'] + '-' + df_c99['Localidade Pouso'] df_c99['PAX'] = pd.to_numeric(df_c99['PAX']).fillna(0) df_c99['Dep TimeStamp'] = pd.to_datetime(df_c99['Data de Decolagem'] + ' ' + df_c99['Hora de Decolagem'], format='mixed', dayfirst=True) df_c99['Data Apenas'] = df_c99['Dep TimeStamp'].dt.date CAPACIDADE_C99 = 50 matriculas_unicas_c99 = df_c99['Matrícula'].unique() total_aeronaves_c99 = len(matriculas_unicas_c99) print(f"Total de matrículas únicas C-99A (aeronaves disponíveis): {total_aeronaves_c99}") # Demanda diária demanda_diaria_c99 = df_c99.groupby(['Trecho', 'Data Apenas'])['PAX'].sum().reset_index() import itertools todos_trechos_c99 = demanda_diaria_c99['Trecho'].unique() idx_c99 = pd.MultiIndex.from_product([todos_trechos_c99, datas_2025], names=['Trecho', 'Data Apenas']) demanda_diaria_completa_c99 = pd.DataFrame(index=idx_c99).reset_index() demanda_diaria_completa_c99 = pd.merge(demanda_diaria_completa_c99, demanda_diaria_c99, on=['Trecho', 'Data Apenas'], how='left').fillna({'PAX': 0}) estatisticas_trechos_c99 = demanda_diaria_completa_c99.groupby('Trecho').agg( Media_PAX_Diario=('PAX', 'mean'), Desvio_Padrao_PAX=('PAX', 'std'), Total_PAX=('PAX', 'sum'), Dias_Operados=('PAX', lambda x: (x > 0).sum()) ).reset_index() estatisticas_trechos_c99['Media_PAX_Dias_Operados'] = estatisticas_trechos_c99.apply( lambda row: row['Total_PAX'] / row['Dias_Operados'] if row['Dias_Operados'] > 0 else 0, axis=1 ) estatisticas_trechos_c99['Score_Relevancia'] = estatisticas_trechos_c99['Total_PAX'] * estatisticas_trechos_c99['Dias_Operados'] estatisticas_trechos_c99 = estatisticas_trechos_c99.sort_values(by='Score_Relevancia', ascending=False) NUM_TRECHOS_C99 = 10 top_trechos_c99 = estatisticas_trechos_c99.head(NUM_TRECHOS_C99) display(top_trechos_c99) plt.figure(figsize=(10, 6)) sns.barplot(data=top_trechos_c99, x='Trecho', y='Score_Relevancia', hue='Trecho', palette='magma', legend=False) plt.title(f'Top {NUM_TRECHOS_C99} Trechos C-99A (PAX * Dias Operados)') plt.xticks(rotation=45) plt.tight_layout() plt.show() # Rede C-99 G_c99 = nx.DiGraph() for _, row in top_trechos_c99.iterrows(): origem, destino = row['Trecho'].split('-') G_c99.add_edge(origem, destino, weight=row['Score_Relevancia']) pos_c99 = nx.spring_layout(G_c99, k=2.5, iterations=100, seed=42) plt.figure(figsize=(12, 8)) nx.draw_networkx_nodes(G_c99, pos_c99, node_size=1200, node_color='lightcoral', edgecolors='black') nx.draw_networkx_edges(G_c99, pos_c99, edgelist=G_c99.edges(), arrows=True, arrowstyle='-|>', arrowsize=25, edge_color='gray', width=2, node_size=1200, connectionstyle='arc3,rad=0.15') nx.draw_networkx_labels(G_c99, pos_c99, font_size=12, font_weight='bold') plt.title('Diagrama de Rede - C-99A', fontsize=14) plt.axis('off') plt.show() """)) cells.append(nbf.v4.new_markdown_cell("""## MODELO 4: Minimização de Distância (Custo) Equivalente ao Modelo 1, mas para C-99A. """)) cells.append(nbf.v4.new_code_cell("""# Preparação Modelo 4 rotas_lista_c99 = top_trechos_c99['Trecho'].tolist() distancias_voo_c99 = {} for r in rotas_lista_c99: origem, destino = r.split('-') distancias_voo_c99[r] = calc_distancia(origem, destino) voos_requeridos_c99 = {} for _, row in top_trechos_c99.iterrows(): voos = math.ceil(row['Media_PAX_Dias_Operados'] / CAPACIDADE_C99) voos_requeridos_c99[row['Trecho']] = max(1, voos) prob4 = pulp.LpProblem("Fleet_Assignment_C99_Brasil", pulp.LpMinimize) X_c99 = pulp.LpVariable.dicts("X_c99", rotas_lista_c99, lowBound=0, cat='Integer') prob4 += pulp.lpSum([distancias_voo_c99[r] * X_c99[r] for r in rotas_lista_c99]) for r in rotas_lista_c99: prob4 += X_c99[r] >= voos_requeridos_c99[r] prob4 += pulp.lpSum([X_c99[r] for r in rotas_lista_c99]) <= total_aeronaves_c99 prob4.solve(pulp.PULP_CBC_CMD(msg=False)) print("== MODELO 4 (C-99A) ==") print(f"Status: {pulp.LpStatus[prob4.status]}") print(f"Distância Total: {pulp.value(prob4.objective):.2f} km\\n") for r in rotas_lista_c99: print(f"Trecho: {r:10} | Alocados: {int(X_c99[r].varValue)} voo(s)") """)) cells.append(nbf.v4.new_markdown_cell("""## MODELO 5: Minimização de Frota C-99A (S/ TAT) Equivalente ao Modelo 2. """)) cells.append(nbf.v4.new_code_cell("""# Velocidade Média C-99A df_c99_spd = df_c99.copy() df_c99_spd['Tempo_Horas'] = pd.to_timedelta(df_c99_spd['Tempo de Voo']).dt.total_seconds() / 3600 df_c99_spd['Dist_km'] = df_c99_spd.apply(get_dist_row, axis=1) df_valid_c99 = df_c99_spd[(df_c99_spd['Tempo_Horas'] > 0) & (df_c99_spd['Dist_km'] > 0)].copy() df_valid_c99['Velocidade'] = df_valid_c99['Dist_km'] / df_valid_c99['Tempo_Horas'] vel_media_c99 = df_valid_c99['Velocidade'].mean() print(f"Velocidade média C-99A: {vel_media_c99:.2f} km/h") aeroportos_malha_c99 = set() for r in rotas_lista_c99: o, d = r.split('-') aeroportos_malha_c99.add(o) aeroportos_malha_c99.add(d) aeroportos_malha_c99 = list(aeroportos_malha_c99) tempos_voo_min_c99 = {} for o in aeroportos_malha_c99: for d in aeroportos_malha_c99: if o == d: tempos_voo_min_c99[(o, d)] = 0 else: dist = calc_distancia(o, d) tempos_voo_min_c99[(o, d)] = int(round((dist / vel_media_c99) * 60)) D_c99 = {(o, d): 0 for o in aeroportos_malha_c99 for d in aeroportos_malha_c99} for r in rotas_lista_c99: o, d = r.split('-') D_c99[(o, d)] = voos_requeridos_c99[r] prob5 = pulp.LpProblem("Minimizar_Aeronaves_C99", pulp.LpMinimize) Y5 = pulp.LpVariable.dicts("Y5", [(o, d) for o in aeroportos_malha_c99 for d in aeroportos_malha_c99], lowBound=0, cat='Integer') N5 = pulp.LpVariable("N5", lowBound=0, cat='Integer') prob5 += N5 + 0.0001 * pulp.lpSum([Y5[(i, j)] * tempos_voo_min_c99[(i, j)] for i in aeroportos_malha_c99 for j in aeroportos_malha_c99]) for no in aeroportos_malha_c99: chegadas = pulp.lpSum([D_c99[(i, no)] + Y5[(i, no)] for i in aeroportos_malha_c99]) partidas = pulp.lpSum([D_c99[(no, j)] + Y5[(no, j)] for j in aeroportos_malha_c99]) prob5 += chegadas == partidas tempo_total_voo_c99 = pulp.lpSum([(D_c99[(i, j)] + Y5[(i, j)]) * tempos_voo_min_c99[(i, j)] for i in aeroportos_malha_c99 for j in aeroportos_malha_c99]) prob5 += tempo_total_voo_c99 <= N5 * 1440 prob5.solve(pulp.PULP_CBC_CMD(msg=False)) print("\\n== RESULTADO DO MODELO 5 (C-99A) ==") print(f"Status: {pulp.LpStatus[prob5.status]}") print(f"Aeronaves mínimas: {int(N5.varValue)}") tempo_gasto_c99 = sum([(D_c99[(i, j)] + int(Y5[(i, j)].varValue)) * tempos_voo_min_c99[(i, j)] for i in aeroportos_malha_c99 for j in aeroportos_malha_c99]) print(f"Tempo total de voo: {int(tempo_gasto_c99)} min ({tempo_gasto_c99/60:.2f} h)\\n") voos_para_fazer_c99 = [] D_copy_c99 = {k: v for k, v in D_c99.items()} for i in aeroportos_malha_c99: for j in aeroportos_malha_c99: total_voos = D_c99[(i, j)] + int(Y5[(i, j)].varValue) for _ in range(total_voos): if D_copy_c99[(i, j)] > 0: tipo = 'Passageiro' D_copy_c99[(i, j)] -= 1 else: tipo = 'Vazio' voos_para_fazer_c99.append({'origem': i, 'destino': j, 'tipo': tipo, 'tempo': tempos_voo_min_c99[(i, j)]}) tabela_horarios_c99 = [] aeronave_id = 1 while voos_para_fazer_c99: voo_atual = voos_para_fazer_c99.pop(0) hora_atual = datetime.datetime(2025, 1, 1, 0, 0, 0) tempo_delta = datetime.timedelta(minutes=int(voo_atual['tempo'])) hora_chegada = hora_atual + tempo_delta tabela_horarios_c99.append([f"Aeronave {aeronave_id}", voo_atual['origem'], voo_atual['destino'], voo_atual['tipo'], hora_atual.strftime('%H:%M'), hora_chegada.strftime('%H:%M'), f"{voo_atual['tempo']} min"]) hora_atual = hora_chegada local_atual = voo_atual['destino'] while True: prox_voo = None for idx, v in enumerate(voos_para_fazer_c99): if v['origem'] == local_atual: if (hora_atual - datetime.datetime(2025, 1, 1, 0, 0, 0) + datetime.timedelta(minutes=int(v['tempo']))).total_seconds() <= 1440 * 60: prox_voo = voos_para_fazer_c99.pop(idx) break if prox_voo: tempo_delta = datetime.timedelta(minutes=int(prox_voo['tempo'])) hora_chegada = hora_atual + tempo_delta tabela_horarios_c99.append([f"Aeronave {aeronave_id}", prox_voo['origem'], prox_voo['destino'], prox_voo['tipo'], hora_atual.strftime('%H:%M'), hora_chegada.strftime('%H:%M'), f"{prox_voo['tempo']} min"]) hora_atual = hora_chegada local_atual = prox_voo['destino'] else: break aeronave_id += 1 print(tabulate(tabela_horarios_c99, headers=["Aeronave", "Origem", "Destino", "Tipo", "Partida", "Chegada", "Duração"], tablefmt="github")) """)) cells.append(nbf.v4.new_markdown_cell("""## MODELO 6: Minimização de Frota C-99A (C/ 40 min TAT) Equivalente ao Modelo 3. """)) cells.append(nbf.v4.new_code_cell("""# Modelo 6 prob6 = pulp.LpProblem("Minimizar_Aeronaves_C99_TAT", pulp.LpMinimize) Y6 = pulp.LpVariable.dicts("Y6", [(o, d) for o in aeroportos_malha_c99 for d in aeroportos_malha_c99], lowBound=0, cat='Integer') N6 = pulp.LpVariable("N6", lowBound=0, cat='Integer') TAT_min = 40 prob6 += N6 + 0.0001 * pulp.lpSum([Y6[(i, j)] * tempos_voo_min_c99[(i, j)] for i in aeroportos_malha_c99 for j in aeroportos_malha_c99]) for no in aeroportos_malha_c99: chegadas = pulp.lpSum([D_c99[(i, no)] + Y6[(i, no)] for i in aeroportos_malha_c99]) partidas = pulp.lpSum([D_c99[(no, j)] + Y6[(no, j)] for j in aeroportos_malha_c99]) prob6 += chegadas == partidas tempo_total_op_c99 = pulp.lpSum([(D_c99[(i, j)] + Y6[(i, j)]) * (tempos_voo_min_c99[(i, j)] + TAT_min) for i in aeroportos_malha_c99 for j in aeroportos_malha_c99]) prob6 += tempo_total_op_c99 <= N6 * 1440 prob6.solve(pulp.PULP_CBC_CMD(msg=False)) print("\\n== RESULTADO DO MODELO 6 (C-99A C/ TAT 40 MIN) ==") print(f"Status: {pulp.LpStatus[prob6.status]}") print(f"Aeronaves mínimas: {int(N6.varValue)}") tempo_voo_c99_6 = sum([(D_c99[(i, j)] + int(Y6[(i, j)].varValue)) * tempos_voo_min_c99[(i, j)] for i in aeroportos_malha_c99 for j in aeroportos_malha_c99]) tot_voos_6 = sum([(D_c99[(i, j)] + int(Y6[(i, j)].varValue)) for i in aeroportos_malha_c99 for j in aeroportos_malha_c99]) solo_tot_6 = tot_voos_6 * TAT_min print(f"Tempo total operacional comprometido: {tempo_voo_c99_6 + solo_tot_6} min ({(tempo_voo_c99_6 + solo_tot_6)/60:.2f} h)\\n") voos_para_fazer_c99_6 = [] D_copy_c99_6 = {k: v for k, v in D_c99.items()} for i in aeroportos_malha_c99: for j in aeroportos_malha_c99: tot = D_c99[(i, j)] + int(Y6[(i, j)].varValue) for _ in range(tot): if D_copy_c99_6[(i, j)] > 0: tipo = 'Passageiro' D_copy_c99_6[(i, j)] -= 1 else: tipo = 'Vazio' voos_para_fazer_c99_6.append({'origem': i, 'destino': j, 'tipo': tipo, 'tempo': tempos_voo_min_c99[(i, j)]}) tabela_horarios_c99_6 = [] aeronave_id = 1 while voos_para_fazer_c99_6: voo_atual = voos_para_fazer_c99_6.pop(0) hora_atual = datetime.datetime(2025, 1, 1, 0, 0, 0) tempo_delta = datetime.timedelta(minutes=int(voo_atual['tempo'])) hora_chegada = hora_atual + tempo_delta tabela_horarios_c99_6.append([f"Aeronave {aeronave_id}", voo_atual['origem'], voo_atual['destino'], voo_atual['tipo'], hora_atual.strftime('%H:%M'), hora_chegada.strftime('%H:%M'), f"{voo_atual['tempo']} min"]) hora_atual = hora_chegada + datetime.timedelta(minutes=40) local_atual = voo_atual['destino'] while True: prox_voo = None for idx, v in enumerate(voos_para_fazer_c99_6): if v['origem'] == local_atual: if (hora_atual - datetime.datetime(2025, 1, 1, 0, 0, 0) + datetime.timedelta(minutes=int(v['tempo']))).total_seconds() <= 1440 * 60: prox_voo = voos_para_fazer_c99_6.pop(idx) break if prox_voo: tempo_delta = datetime.timedelta(minutes=int(prox_voo['tempo'])) hora_chegada = hora_atual + tempo_delta tabela_horarios_c99_6.append([f"Aeronave {aeronave_id}", prox_voo['origem'], prox_voo['destino'], prox_voo['tipo'], hora_atual.strftime('%H:%M'), hora_chegada.strftime('%H:%M'), f"{prox_voo['tempo']} min"]) hora_atual = hora_chegada + datetime.timedelta(minutes=40) local_atual = prox_voo['destino'] else: break aeronave_id += 1 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 with open('modelos.ipynb', 'w', encoding='utf-8') as f: nbf.write(nb, f) print("modelos.ipynb gerado com sucesso!")