refactor: simplifica Fleet Assignment para nível Brasil sem restrição de esquadrões (Modelo 1)

This commit is contained in:
2026-06-07 20:32:35 -03:00
parent 15bcd7e962
commit edf6ef9d8f
2 changed files with 104 additions and 134 deletions

View File

@@ -202,22 +202,22 @@ plt.show()
"""))
# Cell 9: Markdown FAM
cells.append(nbf.v4.new_markdown_cell("""## 4. Fleet Assignment Model (FAM)
Conforme o livro base *Airline Operations and Scheduling*, o problema de alocação de frotas busca atribuir os tipos de aeronaves às pernas de voo visando minimizar o custo e as perdas de receita (spill). Como temos apenas um tipo (C-97), nosso problema adapta-se a uma **alocação de esquadrões (e suas respectivas bases)** aos voos requeridos para minimizar o custo total de operação, que é proporcional à distância voada.
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_{s, r}$ (Quantidade de voos operados pelo esquadrão $s$ no trecho $r$)
- **Função Objetivo:** Minimizar a distância total (Distância de posicionamento da base + Distância do trecho + Retorno para a base).
- **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 média (Média PAX / Capacidade do C-97).
2. O total de voos atribuídos a um esquadrão não pode exceder o número de aeronaves disponíveis naquele esquadrão no dia.
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
# 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']]
@@ -230,52 +230,46 @@ def calc_distancia(icao1, icao2):
distancias_voo = {}
for r in rotas_lista:
origem, destino = r.split('-')
dist_trecho = calc_distancia(origem, destino)
for esq, base in bases_esquadroes.items():
dist_ida = calc_distancia(base, origem) if base != origem else 0
dist_volta = calc_distancia(destino, base) if base != destino else 0
distancias_voo[(esq, r)] = dist_ida + dist_trecho + dist_volta
distancias_voo[r] = calc_distancia(origem, destino)
# Demanda de voos por rota (Média diária)
# Demanda de voos por rota
voos_requeridos = {}
for _, row in top_trechos.iterrows():
# Arredondamento para cima da (demanda / capacidade)
voos = math.ceil(row['Media_PAX_Dias_Operados'] / CAPACIDADE_C97)
# Pelo menos 1 voo para suprir a demanda da rota selecionada
voos_requeridos[row['Trecho']] = max(1, voos)
# MODELAGEM COM PULP
prob = pulp.LpProblem("Fleet_Assignment_C97", pulp.LpMinimize)
prob = pulp.LpProblem("Fleet_Assignment_C97_Brasil", pulp.LpMinimize)
# Variáveis
X = pulp.LpVariable.dicts("X", [(esq, r) for esq in aeronaves_por_esquadrao.keys() for r in rotas_lista], lowBound=0, cat='Integer')
# Variáveis (número de voos em cada rota)
X = pulp.LpVariable.dicts("X", rotas_lista, lowBound=0, cat='Integer')
# Função Objetivo
prob += pulp.lpSum([distancias_voo[(esq, r)] * X[(esq, r)] for esq in aeronaves_por_esquadrao.keys() for r in rotas_lista])
# 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 da rota
# Restrição 1: Suprir a demanda
for r in rotas_lista:
prob += pulp.lpSum([X[(esq, r)] for esq in aeronaves_por_esquadrao.keys()]) >= voos_requeridos[r]
prob += X[r] >= voos_requeridos[r]
# Restrição 2: Limite de aeronaves por esquadrão
for esq in aeronaves_por_esquadrao.keys():
prob += pulp.lpSum([X[(esq, r)] for r in rotas_lista]) <= aeronaves_por_esquadrao[esq]
# 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 ==")
print("== RESULTADO DO FLEET ASSIGNMENT (NÍVEL BRASIL) ==")
print(f"Status: {pulp.LpStatus[prob.status]}")
print(f"Distância Total Minimizada: {pulp.value(prob.objective):.2f} km\\n")
print(f"Distância Total da Operação: {pulp.value(prob.objective):.2f} km\\n")
print("Alocações (Voos Diários):")
print("Alocações (Voos Diários Globais):")
voos_totais = 0
for r in rotas_lista:
print(f"\\nTrecho: {r} (Voos necessários: {voos_requeridos[r]})")
for esq in aeronaves_por_esquadrao.keys():
qtd = int(X[(esq, r)].varValue)
if qtd > 0:
print(f" -> {esq} (Base {bases_esquadroes[esq]}): {qtd} voo(s)")
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.")
"""))
nb.cells = cells

View File

@@ -2,7 +2,7 @@
"cells": [
{
"cell_type": "markdown",
"id": "6be31f50",
"id": "3057dead",
"metadata": {},
"source": [
"# Alocação de Frotas (Fleet Scheduling) - Aeronaves C-97\n",
@@ -21,13 +21,13 @@
{
"cell_type": "code",
"execution_count": 1,
"id": "f4b8c44d",
"id": "8c9a45e2",
"metadata": {
"execution": {
"iopub.execute_input": "2026-06-07T23:27:29.153892Z",
"iopub.status.busy": "2026-06-07T23:27:29.153192Z",
"iopub.status.idle": "2026-06-07T23:27:29.811580Z",
"shell.execute_reply": "2026-06-07T23:27:29.810502Z"
"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"
}
},
"outputs": [],
@@ -53,7 +53,7 @@
},
{
"cell_type": "markdown",
"id": "4a3f8284",
"id": "cfe85206",
"metadata": {},
"source": [
"## 1. Leitura e Limpeza dos Dados\n",
@@ -63,13 +63,13 @@
{
"cell_type": "code",
"execution_count": 2,
"id": "43261510",
"id": "32d7c973",
"metadata": {
"execution": {
"iopub.execute_input": "2026-06-07T23:27:29.813511Z",
"iopub.status.busy": "2026-06-07T23:27:29.813233Z",
"iopub.status.idle": "2026-06-07T23:27:30.183686Z",
"shell.execute_reply": "2026-06-07T23:27:30.182986Z"
"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"
}
},
"outputs": [],
@@ -101,7 +101,7 @@
},
{
"cell_type": "markdown",
"id": "bffd4e0c",
"id": "e1b1fe07",
"metadata": {},
"source": [
"## 2. Análise dos Esquadrões e Matrículas (Aeronaves)\n",
@@ -111,13 +111,13 @@
{
"cell_type": "code",
"execution_count": 3,
"id": "40061c45",
"id": "5016861c",
"metadata": {
"execution": {
"iopub.execute_input": "2026-06-07T23:27:30.185144Z",
"iopub.status.busy": "2026-06-07T23:27:30.185022Z",
"iopub.status.idle": "2026-06-07T23:27:30.196292Z",
"shell.execute_reply": "2026-06-07T23:27:30.195756Z"
"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"
}
},
"outputs": [
@@ -176,7 +176,7 @@
},
{
"cell_type": "markdown",
"id": "94ec1687",
"id": "2fe94947",
"metadata": {},
"source": [
"## 3. Análise Estatística das Demandas Diárias\n",
@@ -187,13 +187,13 @@
{
"cell_type": "code",
"execution_count": 4,
"id": "9e2bb9ab",
"id": "73a0064f",
"metadata": {
"execution": {
"iopub.execute_input": "2026-06-07T23:27:30.197674Z",
"iopub.status.busy": "2026-06-07T23:27:30.197555Z",
"iopub.status.idle": "2026-06-07T23:27:30.501774Z",
"shell.execute_reply": "2026-06-07T23:27:30.501194Z"
"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"
}
},
"outputs": [
@@ -424,7 +424,7 @@
},
{
"cell_type": "markdown",
"id": "d6f1f668",
"id": "c81003e5",
"metadata": {},
"source": [
"## Diagrama de Rede dos Trechos\n",
@@ -434,13 +434,13 @@
{
"cell_type": "code",
"execution_count": 5,
"id": "4929bba6",
"id": "bb72e82d",
"metadata": {
"execution": {
"iopub.execute_input": "2026-06-07T23:27:30.503463Z",
"iopub.status.busy": "2026-06-07T23:27:30.503337Z",
"iopub.status.idle": "2026-06-07T23:27:30.631863Z",
"shell.execute_reply": "2026-06-07T23:27:30.631298Z"
"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"
}
},
"outputs": [
@@ -497,30 +497,30 @@
},
{
"cell_type": "markdown",
"id": "59991398",
"id": "e5daac1f",
"metadata": {},
"source": [
"## 4. Fleet Assignment Model (FAM)\n",
"Conforme o livro base *Airline Operations and Scheduling*, o problema de alocação de frotas busca atribuir os tipos de aeronaves às pernas de voo visando minimizar o custo e as perdas de receita (spill). Como temos apenas um tipo (C-97), nosso problema adapta-se a uma **alocação de esquadrões (e suas respectivas bases)** aos voos requeridos para minimizar o custo total de operação, que é proporcional à distância voada.\n",
"## 4. Fleet Assignment Model (FAM) - Nível Brasil\n",
"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.\n",
"\n",
"**Modelo Matemático:**\n",
"- **Variáveis de decisão:** $X_{s, r}$ (Quantidade de voos operados pelo esquadrão $s$ no trecho $r$)\n",
"- **Função Objetivo:** Minimizar a distância total (Distância de posicionamento da base + Distância do trecho + Retorno para a base).\n",
"- **Variáveis de decisão:** $X_r$ (Quantidade de voos alocados no trecho $r$)\n",
"- **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.\n",
"- **Restrições:**\n",
" 1. O número de voos alocados para o trecho deve suprir a demanda média (Média PAX / Capacidade do C-97).\n",
" 2. O total de voos atribuídos a um esquadrão não pode exceder o número de aeronaves disponíveis naquele esquadrão no dia.\n"
" 1. O número de voos alocados para o trecho deve suprir a demanda diária operada daquela rota.\n",
" 2. O total de voos atribuídos no dia não pode exceder o total da frota de C-97 disponível (14 aeronaves).\n"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "be919c7c",
"id": "26694904",
"metadata": {
"execution": {
"iopub.execute_input": "2026-06-07T23:27:30.633209Z",
"iopub.status.busy": "2026-06-07T23:27:30.633086Z",
"iopub.status.idle": "2026-06-07T23:27:30.740716Z",
"shell.execute_reply": "2026-06-07T23:27:30.739554Z"
"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"
}
},
"outputs": [
@@ -528,41 +528,23 @@
"name": "stdout",
"output_type": "stream",
"text": [
"== RESULTADO DO FLEET ASSIGNMENT ==\n",
"== RESULTADO DO FLEET ASSIGNMENT (NÍVEL BRASIL) ==\n",
"Status: Optimal\n",
"Distância Total Minimizada: 15903.36 km\n",
"Distância Total da Operação: 5703.02 km\n",
"\n",
"Alocações (Voos Diários):\n",
"Alocações (Voos Diários Globais):\n",
"Trecho: SBGL-SBBR | Demanda Média PAX: 30 | Alocados: 1 voo(s)\n",
"Trecho: SBBR-SBGL | Demanda Média PAX: 30 | Alocados: 1 voo(s)\n",
"Trecho: SBBR-SBGR | Demanda Média PAX: 30 | Alocados: 1 voo(s)\n",
"Trecho: SBGR-SBBR | Demanda Média PAX: 30 | Alocados: 1 voo(s)\n",
"Trecho: SBGL-SBGR | Demanda Média PAX: 30 | Alocados: 1 voo(s)\n",
"Trecho: SBSJ-SBBR | Demanda Média PAX: 30 | Alocados: 1 voo(s)\n",
"Trecho: SBGR-SBGL | Demanda Média PAX: 30 | Alocados: 1 voo(s)\n",
"Trecho: SBSJ-SBGL | Demanda Média PAX: 30 | Alocados: 1 voo(s)\n",
"Trecho: SBGL-SBSJ | Demanda Média PAX: 30 | Alocados: 1 voo(s)\n",
"Trecho: SBBR-SBAN | Demanda Média PAX: 30 | Alocados: 1 voo(s)\n",
"\n",
"Trecho: SBGL-SBBR (Voos necessários: 1)\n",
" -> ETA6 (Base SBBR): 1 voo(s)\n",
"\n",
"Trecho: SBBR-SBGL (Voos necessários: 1)\n",
" -> ETA2 (Base SBNT): 1 voo(s)\n",
"\n",
"Trecho: SBBR-SBGR (Voos necessários: 1)\n",
" -> ETA5 (Base SBCO): 1 voo(s)\n",
"\n",
"Trecho: SBGR-SBBR (Voos necessários: 1)\n",
" -> ETA6 (Base SBBR): 1 voo(s)\n",
"\n",
"Trecho: SBGL-SBGR (Voos necessários: 1)\n",
" -> ETA3 (Base SBGL): 1 voo(s)\n",
"\n",
"Trecho: SBSJ-SBBR (Voos necessários: 1)\n",
" -> ETA6 (Base SBBR): 1 voo(s)\n",
"\n",
"Trecho: SBGR-SBGL (Voos necessários: 1)\n",
" -> PAMA SP (Base SBGR): 1 voo(s)\n",
"\n",
"Trecho: SBSJ-SBGL (Voos necessários: 1)\n",
" -> ETA3 (Base SBGL): 1 voo(s)\n",
"\n",
"Trecho: SBGL-SBSJ (Voos necessários: 1)\n",
" -> ETA3 (Base SBGL): 1 voo(s)\n",
"\n",
"Trecho: SBBR-SBAN (Voos necessários: 1)\n",
" -> ETA6 (Base SBBR): 1 voo(s)\n"
"Total de voos alocados na malha diária: 10 de 14 aeronaves disponíveis.\n"
]
}
],
@@ -570,7 +552,7 @@
"# Preparação dos dados para o modelo\n",
"rotas_lista = top_trechos['Trecho'].tolist()\n",
"\n",
"# Dicionário de distâncias\n",
"# Dicionário de distâncias apenas dos trechos diretos\n",
"def calc_distancia(icao1, icao2):\n",
" try:\n",
" lat1, lon1, alt1 = df_aeroportos.loc[icao1, ['lat', 'lon', 'elevation']]\n",
@@ -583,52 +565,46 @@
"distancias_voo = {}\n",
"for r in rotas_lista:\n",
" origem, destino = r.split('-')\n",
" dist_trecho = calc_distancia(origem, destino)\n",
" for esq, base in bases_esquadroes.items():\n",
" dist_ida = calc_distancia(base, origem) if base != origem else 0\n",
" dist_volta = calc_distancia(destino, base) if base != destino else 0\n",
" distancias_voo[(esq, r)] = dist_ida + dist_trecho + dist_volta\n",
" distancias_voo[r] = calc_distancia(origem, destino)\n",
"\n",
"# Demanda de voos por rota (Média diária)\n",
"# Demanda de voos por rota\n",
"voos_requeridos = {}\n",
"for _, row in top_trechos.iterrows():\n",
" # Arredondamento para cima da (demanda / capacidade)\n",
" voos = math.ceil(row['Media_PAX_Dias_Operados'] / CAPACIDADE_C97)\n",
" # Pelo menos 1 voo para suprir a demanda da rota selecionada\n",
" voos_requeridos[row['Trecho']] = max(1, voos)\n",
"\n",
"# MODELAGEM COM PULP\n",
"prob = pulp.LpProblem(\"Fleet_Assignment_C97\", pulp.LpMinimize)\n",
"prob = pulp.LpProblem(\"Fleet_Assignment_C97_Brasil\", pulp.LpMinimize)\n",
"\n",
"# Variáveis\n",
"X = pulp.LpVariable.dicts(\"X\", [(esq, r) for esq in aeronaves_por_esquadrao.keys() for r in rotas_lista], lowBound=0, cat='Integer')\n",
"# Variáveis (número de voos em cada rota)\n",
"X = pulp.LpVariable.dicts(\"X\", rotas_lista, lowBound=0, cat='Integer')\n",
"\n",
"# Função Objetivo\n",
"prob += pulp.lpSum([distancias_voo[(esq, r)] * X[(esq, r)] for esq in aeronaves_por_esquadrao.keys() for r in rotas_lista])\n",
"# Função Objetivo: Minimizar distância voada nos trechos\n",
"prob += pulp.lpSum([distancias_voo[r] * X[r] for r in rotas_lista])\n",
"\n",
"# Restrição 1: Suprir a demanda da rota\n",
"# Restrição 1: Suprir a demanda\n",
"for r in rotas_lista:\n",
" prob += pulp.lpSum([X[(esq, r)] for esq in aeronaves_por_esquadrao.keys()]) >= voos_requeridos[r]\n",
" prob += X[r] >= voos_requeridos[r]\n",
"\n",
"# Restrição 2: Limite de aeronaves por esquadrão\n",
"for esq in aeronaves_por_esquadrao.keys():\n",
" prob += pulp.lpSum([X[(esq, r)] for r in rotas_lista]) <= aeronaves_por_esquadrao[esq]\n",
"# Restrição 2: Limite global de frota (nível Brasil)\n",
"prob += pulp.lpSum([X[r] for r in rotas_lista]) <= total_aeronaves\n",
"\n",
"# Solução\n",
"prob.solve(pulp.PULP_CBC_CMD(msg=False))\n",
"\n",
"# Exibição dos resultados\n",
"print(\"== RESULTADO DO FLEET ASSIGNMENT ==\")\n",
"print(\"== RESULTADO DO FLEET ASSIGNMENT (NÍVEL BRASIL) ==\")\n",
"print(f\"Status: {pulp.LpStatus[prob.status]}\")\n",
"print(f\"Distância Total Minimizada: {pulp.value(prob.objective):.2f} km\\n\")\n",
"print(f\"Distância Total da Operação: {pulp.value(prob.objective):.2f} km\\n\")\n",
"\n",
"print(\"Alocações (Voos Diários):\")\n",
"print(\"Alocações (Voos Diários Globais):\")\n",
"voos_totais = 0\n",
"for r in rotas_lista:\n",
" print(f\"\\nTrecho: {r} (Voos necessários: {voos_requeridos[r]})\")\n",
" for esq in aeronaves_por_esquadrao.keys():\n",
" qtd = int(X[(esq, r)].varValue)\n",
" if qtd > 0:\n",
" print(f\" -> {esq} (Base {bases_esquadroes[esq]}): {qtd} voo(s)\")\n"
" qtd = int(X[r].varValue)\n",
" voos_totais += qtd\n",
" print(f\"Trecho: {r:10} | Demanda Média PAX: {voos_requeridos[r]*CAPACIDADE_C97:3.0f} | Alocados: {qtd} voo(s)\")\n",
"\n",
"print(f\"\\nTotal de voos alocados na malha diária: {voos_totais} de {total_aeronaves} aeronaves disponíveis.\")\n"
]
}
],