Files
fleet-assignment/create_nb.py

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 (para reposicionamento)
tempos_voo = {}
for o in aeroportos_malha:
for d in aeroportos_malha:
if o == d:
tempos_voo[(o, d)] = 0
else:
dist = calc_distancia(o, d)
tempos_voo[(o, d)] = dist / vel_media_c97
# 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[(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 * 24h)
tempo_total_voo = pulp.lpSum([(D[(i, j)] + Y[(i, j)]) * tempos_voo[(i, j)] for i in aeroportos_malha for j in aeroportos_malha])
prob2 += tempo_total_voo <= N * 24.0
# 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)] + Y[(i, j)].varValue) * tempos_voo[(i, j)] for i in aeroportos_malha for j in aeroportos_malha])
print(f"Tempo total de voo diário (incluindo reposicionamento vazio): {tempo_gasto:.2f} horas")
if N.varValue > 0:
print(f"Taxa de ocupação da frota (Tempo Voo / Tempo Disponível): {(tempo_gasto / (N.varValue * 24)) * 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[(i,j)]*Y[(i,j)].varValue:.1f}h")
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[(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(hours=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']:.2f} h"
])
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(hours=v['tempo'])).total_seconds() <= 24 * 3600:
prox_voo = voos_para_fazer.pop(idx)
break
if prox_voo:
tempo_delta = datetime.timedelta(hours=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']:.2f} h"
])
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_horas = 40.0 / 60.0
# Função Objetivo
prob3 += N3 + 0.0001 * pulp.lpSum([Y3[(i, j)] * tempos_voo[(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[(i, j)] + TAT_horas) for i in aeroportos_malha for j in aeroportos_malha])
prob3 += tempo_total_operacional <= N3 * 24.0
# 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)] + Y3[(i, j)].varValue) * tempos_voo[(i, j)] for i in aeroportos_malha for j in aeroportos_malha])
total_voos_realizados = sum([(D[(i, j)] + Y3[(i, j)].varValue) for i in aeroportos_malha for j in aeroportos_malha])
tempo_solo_total = total_voos_realizados * TAT_horas
print(f"Tempo total de voo puro: {tempo_voo_puro:.2f} horas")
print(f"Tempo total gasto em solo (TAT): {tempo_solo_total:.2f} horas")
print(f"Tempo total operacional comprometido: {tempo_voo_puro + tempo_solo_total:.2f} horas")
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 * 24)) * 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[(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(hours=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']:.2f} h"
])
# 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(hours=v['tempo'])).total_seconds() <= 24 * 3600:
prox_voo = voos_para_fazer3.pop(idx)
break
if prox_voo:
tempo_delta = datetime.timedelta(hours=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']:.2f} h"
])
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!")