From edf6ef9d8fee51d88ac893912bdf22e44a5f3f89 Mon Sep 17 00:00:00 2001 From: Luciano Silva do Lago Date: Sun, 7 Jun 2026 20:32:35 -0300 Subject: [PATCH] =?UTF-8?q?refactor:=20simplifica=20Fleet=20Assignment=20p?= =?UTF-8?q?ara=20n=C3=ADvel=20Brasil=20sem=20restri=C3=A7=C3=A3o=20de=20es?= =?UTF-8?q?quadr=C3=B5es=20(Modelo=201)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- create_nb.py | 60 ++++++++--------- modelos.ipynb | 178 ++++++++++++++++++++++---------------------------- 2 files changed, 104 insertions(+), 134 deletions(-) diff --git a/create_nb.py b/create_nb.py index 57406f0..7a3293e 100644 --- a/create_nb.py +++ b/create_nb.py @@ -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 diff --git a/modelos.ipynb b/modelos.ipynb index 1aabe12..06033eb 100644 --- a/modelos.ipynb +++ b/modelos.ipynb @@ -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" ] } ],