feat: adiciona Modelo 2 para minimização do número absoluto de aeronaves

This commit is contained in:
2026-06-07 20:37:19 -03:00
parent edf6ef9d8f
commit 1cab2bbe36
2 changed files with 272 additions and 36 deletions

View File

@@ -272,6 +272,107 @@ for r in rotas_lista:
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.")
"""))
nb.cells = cells
with open('modelos.ipynb', 'w', encoding='utf-8') as f:

View File

@@ -2,7 +2,7 @@
"cells": [
{
"cell_type": "markdown",
"id": "3057dead",
"id": "07d7e46d",
"metadata": {},
"source": [
"# Alocação de Frotas (Fleet Scheduling) - Aeronaves C-97\n",
@@ -21,13 +21,13 @@
{
"cell_type": "code",
"execution_count": 1,
"id": "8c9a45e2",
"id": "3f9fa4c6",
"metadata": {
"execution": {
"iopub.execute_input": "2026-06-07T23:32:32.992543Z",
"iopub.status.busy": "2026-06-07T23:32:32.992366Z",
"iopub.status.idle": "2026-06-07T23:32:33.601354Z",
"shell.execute_reply": "2026-06-07T23:32:33.600709Z"
"iopub.execute_input": "2026-06-07T23:37:16.312196Z",
"iopub.status.busy": "2026-06-07T23:37:16.311487Z",
"iopub.status.idle": "2026-06-07T23:37:16.933888Z",
"shell.execute_reply": "2026-06-07T23:37:16.933301Z"
}
},
"outputs": [],
@@ -53,7 +53,7 @@
},
{
"cell_type": "markdown",
"id": "cfe85206",
"id": "6054dba5",
"metadata": {},
"source": [
"## 1. Leitura e Limpeza dos Dados\n",
@@ -63,13 +63,13 @@
{
"cell_type": "code",
"execution_count": 2,
"id": "32d7c973",
"id": "da258d7f",
"metadata": {
"execution": {
"iopub.execute_input": "2026-06-07T23:32:33.603095Z",
"iopub.status.busy": "2026-06-07T23:32:33.602885Z",
"iopub.status.idle": "2026-06-07T23:32:33.976197Z",
"shell.execute_reply": "2026-06-07T23:32:33.975310Z"
"iopub.execute_input": "2026-06-07T23:37:16.935502Z",
"iopub.status.busy": "2026-06-07T23:37:16.935296Z",
"iopub.status.idle": "2026-06-07T23:37:17.297220Z",
"shell.execute_reply": "2026-06-07T23:37:17.296482Z"
}
},
"outputs": [],
@@ -101,7 +101,7 @@
},
{
"cell_type": "markdown",
"id": "e1b1fe07",
"id": "2424db2d",
"metadata": {},
"source": [
"## 2. Análise dos Esquadrões e Matrículas (Aeronaves)\n",
@@ -111,13 +111,13 @@
{
"cell_type": "code",
"execution_count": 3,
"id": "5016861c",
"id": "8b308e12",
"metadata": {
"execution": {
"iopub.execute_input": "2026-06-07T23:32:33.977763Z",
"iopub.status.busy": "2026-06-07T23:32:33.977632Z",
"iopub.status.idle": "2026-06-07T23:32:33.989318Z",
"shell.execute_reply": "2026-06-07T23:32:33.988806Z"
"iopub.execute_input": "2026-06-07T23:37:17.298752Z",
"iopub.status.busy": "2026-06-07T23:37:17.298630Z",
"iopub.status.idle": "2026-06-07T23:37:17.310776Z",
"shell.execute_reply": "2026-06-07T23:37:17.310206Z"
}
},
"outputs": [
@@ -176,7 +176,7 @@
},
{
"cell_type": "markdown",
"id": "2fe94947",
"id": "4909bfef",
"metadata": {},
"source": [
"## 3. Análise Estatística das Demandas Diárias\n",
@@ -187,13 +187,13 @@
{
"cell_type": "code",
"execution_count": 4,
"id": "73a0064f",
"id": "b38abd07",
"metadata": {
"execution": {
"iopub.execute_input": "2026-06-07T23:32:33.992417Z",
"iopub.status.busy": "2026-06-07T23:32:33.992279Z",
"iopub.status.idle": "2026-06-07T23:32:34.310958Z",
"shell.execute_reply": "2026-06-07T23:32:34.310336Z"
"iopub.execute_input": "2026-06-07T23:37:17.312196Z",
"iopub.status.busy": "2026-06-07T23:37:17.312078Z",
"iopub.status.idle": "2026-06-07T23:37:17.621631Z",
"shell.execute_reply": "2026-06-07T23:37:17.620923Z"
}
},
"outputs": [
@@ -424,7 +424,7 @@
},
{
"cell_type": "markdown",
"id": "c81003e5",
"id": "6bab0068",
"metadata": {},
"source": [
"## Diagrama de Rede dos Trechos\n",
@@ -434,13 +434,13 @@
{
"cell_type": "code",
"execution_count": 5,
"id": "bb72e82d",
"id": "6198127c",
"metadata": {
"execution": {
"iopub.execute_input": "2026-06-07T23:32:34.312383Z",
"iopub.status.busy": "2026-06-07T23:32:34.312250Z",
"iopub.status.idle": "2026-06-07T23:32:34.430777Z",
"shell.execute_reply": "2026-06-07T23:32:34.430189Z"
"iopub.execute_input": "2026-06-07T23:37:17.623083Z",
"iopub.status.busy": "2026-06-07T23:37:17.622958Z",
"iopub.status.idle": "2026-06-07T23:37:17.740324Z",
"shell.execute_reply": "2026-06-07T23:37:17.739229Z"
}
},
"outputs": [
@@ -497,7 +497,7 @@
},
{
"cell_type": "markdown",
"id": "e5daac1f",
"id": "35633067",
"metadata": {},
"source": [
"## 4. Fleet Assignment Model (FAM) - Nível Brasil\n",
@@ -514,13 +514,13 @@
{
"cell_type": "code",
"execution_count": 6,
"id": "26694904",
"id": "c9a2018d",
"metadata": {
"execution": {
"iopub.execute_input": "2026-06-07T23:32:34.432272Z",
"iopub.status.busy": "2026-06-07T23:32:34.432145Z",
"iopub.status.idle": "2026-06-07T23:32:34.456734Z",
"shell.execute_reply": "2026-06-07T23:32:34.456266Z"
"iopub.execute_input": "2026-06-07T23:37:17.742588Z",
"iopub.status.busy": "2026-06-07T23:37:17.742459Z",
"iopub.status.idle": "2026-06-07T23:37:17.764890Z",
"shell.execute_reply": "2026-06-07T23:37:17.764350Z"
}
},
"outputs": [
@@ -606,6 +606,141 @@
"\n",
"print(f\"\\nTotal de voos alocados na malha diária: {voos_totais} de {total_aeronaves} aeronaves disponíveis.\")\n"
]
},
{
"cell_type": "markdown",
"id": "666ba740",
"metadata": {},
"source": [
"## 5. Modelo 2: Minimização de Aeronaves\n",
"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).\n",
"Considerações solicitadas:\n",
"- O tempo de voo é estimado usando a velocidade média real do C-97 em 2025.\n",
"- As aeronaves podem voar até 24 horas por dia (não consideraremos o tempo de solo no chão nesta versão).\n",
"- Se houver desbalanceamento de malha (mais voos saindo de A do que chegando em A), o modelo criará voos de reposicionamento vazios.\n"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "7b9b8019",
"metadata": {
"execution": {
"iopub.execute_input": "2026-06-07T23:37:17.766458Z",
"iopub.status.busy": "2026-06-07T23:37:17.766338Z",
"iopub.status.idle": "2026-06-07T23:37:19.072202Z",
"shell.execute_reply": "2026-06-07T23:37:19.071522Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Velocidade média calculada para o C-97: 391.76 km/h\n",
"\n",
"== RESULTADO DO MODELO 2 ==\n",
"Status: Optimal\n",
"Número mínimo absoluto de aeronaves C-97 necessárias: 1\n",
"Tempo total de voo diário (incluindo reposicionamento vazio): 16.70 horas\n",
"Taxa de ocupação da frota (Tempo Voo / Tempo Disponível): 69.6%\n",
"\n",
"Voos de Reposicionamento Vazio Gerados (Y) para fechar o ciclo diário:\n",
" -> De SBAN para SBSJ: 1 voo(s) vazio(s) | Tempo de voo: 2.1h\n"
]
}
],
"source": [
"# 1. Calcular a velocidade média do C-97 em 2025\n",
"# Converte a string 'HH:MM:SS' para timedelta e extrai horas\n",
"df_c97_spd = df_c97.copy()\n",
"df_c97_spd['Tempo_Horas'] = pd.to_timedelta(df_c97_spd['Tempo de Voo']).dt.total_seconds() / 3600\n",
"\n",
"def get_dist_row(row):\n",
" try:\n",
" lat1, lon1, alt1 = df_aeroportos.loc[row['Localidade Decolagem'], ['lat', 'lon', 'elevation']]\n",
" lat2, lon2, alt2 = df_aeroportos.loc[row['Localidade Pouso'], ['lat', 'lon', 'elevation']]\n",
" return pm.geodetic2aer(lat1, lon1, alt1, lat2, lon2, alt2)[2] / 1000.0\n",
" except:\n",
" return np.nan\n",
"\n",
"df_c97_spd['Dist_km'] = df_c97_spd.apply(get_dist_row, axis=1)\n",
"\n",
"# Filtra voos válidos para calcular a velocidade média\n",
"df_valid = df_c97_spd[(df_c97_spd['Tempo_Horas'] > 0) & (df_c97_spd['Dist_km'] > 0)].copy()\n",
"df_valid['Velocidade'] = df_valid['Dist_km'] / df_valid['Tempo_Horas']\n",
"vel_media_c97 = df_valid['Velocidade'].mean()\n",
"print(f\"Velocidade média calculada para o C-97: {vel_media_c97:.2f} km/h\")\n",
"\n",
"# 2. Configurar os nós (aeroportos) e demandas da malha das rotas top\n",
"aeroportos_malha = set()\n",
"for r in rotas_lista:\n",
" o, d = r.split('-')\n",
" aeroportos_malha.add(o)\n",
" aeroportos_malha.add(d)\n",
"aeroportos_malha = list(aeroportos_malha)\n",
"\n",
"# Matriz de tempo de voo entre todos os nós da malha (para reposicionamento)\n",
"tempos_voo = {}\n",
"for o in aeroportos_malha:\n",
" for d in aeroportos_malha:\n",
" if o == d:\n",
" tempos_voo[(o, d)] = 0\n",
" else:\n",
" dist = calc_distancia(o, d)\n",
" tempos_voo[(o, d)] = dist / vel_media_c97\n",
"\n",
"# Demanda fixa D_{i,j}\n",
"D = {(o, d): 0 for o in aeroportos_malha for d in aeroportos_malha}\n",
"for r in rotas_lista:\n",
" o, d = r.split('-')\n",
" D[(o, d)] = voos_requeridos[r]\n",
"\n",
"# 3. Modelagem Matemática (Pulp)\n",
"prob2 = pulp.LpProblem(\"Minimizar_Aeronaves\", pulp.LpMinimize)\n",
"\n",
"# Variáveis de reposicionamento vazio Y_{i,j}\n",
"Y = pulp.LpVariable.dicts(\"Y\", [(o, d) for o in aeroportos_malha for d in aeroportos_malha], lowBound=0, cat='Integer')\n",
"\n",
"# Variável de número de aeronaves N\n",
"N = pulp.LpVariable(\"N\", lowBound=0, cat='Integer')\n",
"\n",
"# Função Objetivo: Minimizar N\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)\n",
"prob2 += N + 0.0001 * pulp.lpSum([Y[(i, j)] * tempos_voo[(i, j)] for i in aeroportos_malha for j in aeroportos_malha])\n",
"\n",
"# Restrição de conservação de fluxo para cada aeroporto (Chegadas == Partidas)\n",
"for no in aeroportos_malha:\n",
" chegadas = pulp.lpSum([D[(i, no)] + Y[(i, no)] for i in aeroportos_malha])\n",
" partidas = pulp.lpSum([D[(no, j)] + Y[(no, j)] for j in aeroportos_malha])\n",
" prob2 += chegadas == partidas\n",
"\n",
"# Restrição de tempo (O tempo total de voo da malha não pode exceder N * 24h)\n",
"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])\n",
"prob2 += tempo_total_voo <= N * 24.0\n",
"\n",
"# Solução\n",
"prob2.solve(pulp.PULP_CBC_CMD(msg=False))\n",
"\n",
"print(\"\\n== RESULTADO DO MODELO 2 ==\")\n",
"print(f\"Status: {pulp.LpStatus[prob2.status]}\")\n",
"print(f\"Número mínimo absoluto de aeronaves C-97 necessárias: {int(N.varValue)}\")\n",
"\n",
"tempo_gasto = sum([(D[(i, j)] + Y[(i, j)].varValue) * tempos_voo[(i, j)] for i in aeroportos_malha for j in aeroportos_malha])\n",
"print(f\"Tempo total de voo diário (incluindo reposicionamento vazio): {tempo_gasto:.2f} horas\")\n",
"if N.varValue > 0:\n",
" print(f\"Taxa de ocupação da frota (Tempo Voo / Tempo Disponível): {(tempo_gasto / (N.varValue * 24)) * 100:.1f}%\\n\")\n",
"\n",
"print(\"Voos de Reposicionamento Vazio Gerados (Y) para fechar o ciclo diário:\")\n",
"gerou_vazio = False\n",
"for i in aeroportos_malha:\n",
" for j in aeroportos_malha:\n",
" if Y[(i, j)].varValue > 0:\n",
" 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\")\n",
" gerou_vazio = True\n",
"if not gerou_vazio:\n",
" print(\" Nenhum voo de reposicionamento vazio foi necessário! A malha já é circular/balanceada.\")\n"
]
}
],
"metadata": {