diff --git a/create_nb.py b/create_nb.py index 7a3293e..fe1fc8e 100644 --- a/create_nb.py +++ b/create_nb.py @@ -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: diff --git a/modelos.ipynb b/modelos.ipynb index 06033eb..bd762d4 100644 --- a/modelos.ipynb +++ b/modelos.ipynb @@ -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": {