1121 lines
50 KiB
Python
1121 lines
50 KiB
Python
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!")
|