577 lines
23 KiB
Python
577 lines
23 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"))
|
|
"""))
|
|
|
|
nb.cells = cells
|
|
|
|
with open('modelos.ipynb', 'w', encoding='utf-8') as f:
|
|
nbf.write(nb, f)
|
|
|
|
print("modelos.ipynb gerado com sucesso!")
|