192 lines
9.0 KiB
Python
192 lines
9.0 KiB
Python
"""
|
|
Worker script to run the SCIP solver in a separate process.
|
|
This allows capturing the C++ stdout (EnableOutput) and streaming it back to Streamlit.
|
|
"""
|
|
|
|
import sys
|
|
import pandas as pd
|
|
import math
|
|
import datetime
|
|
from ortools.linear_solver import pywraplp
|
|
import json
|
|
from vincenty import vincenty
|
|
|
|
import os
|
|
import sqlite3
|
|
|
|
# Ensure src module is visible if run standalone
|
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
|
|
|
from src.fleet_assignment.config import DB_PATH, RESULTS_FILE, COST_FILE, JSON_AEROPORTOS, JSON_EMISSORES
|
|
|
|
def run_solver(time_limit_sec=300):
|
|
print(f"--- Starting Global Solver Worker (Limit: {time_limit_sec}s) ---")
|
|
|
|
conn = sqlite3.connect(DB_PATH)
|
|
df_frota_db = pd.read_sql_query("SELECT * FROM frota", conn)
|
|
demands_db = pd.read_sql_query("SELECT data_apenas as 'Data Apenas', local_dec as 'Localidade Decolagem', local_pou as 'Localidade Pouso', pax as 'PAX' FROM demandas", conn)
|
|
conn.close()
|
|
|
|
df_aeroportos = pd.read_json(JSON_AEROPORTOS, orient='index')
|
|
|
|
# Format demands as required by downstream logic
|
|
pax_demanda_completa = demands_db.groupby(['Data Apenas', 'Localidade Decolagem', 'Localidade Pouso'])['PAX'].sum().reset_index()
|
|
demands = pax_demanda_completa[pax_demanda_completa['PAX'] > 0].copy()
|
|
|
|
with open(JSON_EMISSORES, 'r') as f:
|
|
emissores_data = json.load(f)[0]
|
|
icaos_emissores = list(emissores_data.values())
|
|
|
|
icaos_dec = demands['Localidade Decolagem'].unique().tolist()
|
|
icaos_pou = demands['Localidade Pouso'].unique().tolist()
|
|
all_icaos = [icao for icao in list(set(icaos_dec + icaos_pou + icaos_emissores)) if str(icao).strip() not in ['0', '']]
|
|
|
|
coords = df_aeroportos.loc[df_aeroportos.index.intersection(all_icaos), ['lat', 'lon']]
|
|
valid_icaos = coords.index.tolist()
|
|
|
|
dist_matrix = pd.DataFrame(index=valid_icaos, columns=valid_icaos)
|
|
for i in valid_icaos:
|
|
for j in valid_icaos:
|
|
if i == j: dist_matrix.loc[i, j] = 0.0
|
|
else:
|
|
lat1, lon1 = coords.loc[i, 'lat'], coords.loc[i, 'lon']
|
|
lat2, lon2 = coords.loc[j, 'lat'], coords.loc[j, 'lon']
|
|
dist_matrix.loc[i, j] = round(vincenty((lat1, lon1), (lat2, lon2)), 2)
|
|
|
|
AERONAVES_INTERESSE = ['C-97', 'C-95M', 'C-105', 'KC-390', 'KC-30', 'C-99A', 'C-98', 'C-98A']
|
|
CAPACIDADES_AERONAVES_INTERESSE = [30, 12, 73, 80, 238, 50, 10, 14]
|
|
ALCANCE_AERONAVES_INTERESSE = [1600, 1900, 5000, 6000, 14500, 2200, 2400, 2400]
|
|
DIARIAS_AERONAVES_INTERESSE = [x * 60 for x in [4, 3, 3, 3, 10, 3, 3, 3]]
|
|
|
|
frotas = []
|
|
for idx, row in df_frota_db.iterrows():
|
|
modelo, emissor = row['Modelo'], row['Emissor']
|
|
consumo = row['Consumo (km/l)']
|
|
if consumo > 0 and modelo in AERONAVES_INTERESSE:
|
|
a_idx = AERONAVES_INTERESSE.index(modelo)
|
|
frotas.append({
|
|
'modelo': str(modelo),
|
|
'emissor': str(emissor),
|
|
'base': emissores_data.get(str(emissor), None),
|
|
'frota_max': int(row['Tamanho da Frota']),
|
|
'consumo_kml': float(consumo),
|
|
'capacidade': int(CAPACIDADES_AERONAVES_INTERESSE[a_idx]),
|
|
'alcance': float(ALCANCE_AERONAVES_INTERESSE[a_idx]),
|
|
'velocidade_media': float(row['Velocidade Média (km/h)']),
|
|
'diaria_litros': float(DIARIAS_AERONAVES_INTERESSE[a_idx])
|
|
})
|
|
frotas = [f for f in frotas if f['base'] is not None and f['base'] in valid_icaos]
|
|
|
|
df_dias = pd.to_datetime(demands['Data Apenas']).dt.date
|
|
dias_unicos_list = sorted(df_dias.dropna().unique())
|
|
if not dias_unicos_list:
|
|
print("NO DEMAND DATA.")
|
|
return
|
|
|
|
min_date = min(dias_unicos_list)
|
|
max_date = max(dias_unicos_list)
|
|
todas_as_datas = [min_date + datetime.timedelta(days=i) for i in range((max_date - min_date).days + 1)]
|
|
|
|
print("Pre-computing distances and overnight stays...")
|
|
unique_routes = demands[['Localidade Decolagem', 'Localidade Pouso']].drop_duplicates()
|
|
route_info = {}
|
|
for idx, row in unique_routes.iterrows():
|
|
l_dec = str(row['Localidade Decolagem'])
|
|
l_pou = str(row['Localidade Pouso'])
|
|
if l_dec not in valid_icaos or l_pou not in valid_icaos:
|
|
continue
|
|
for f in frotas:
|
|
m, e, base = f['modelo'], f['emissor'], f['base']
|
|
dist_total = dist_matrix.loc[base, l_dec] + dist_matrix.loc[l_dec, l_pou] + dist_matrix.loc[l_pou, base]
|
|
tempo_missao = dist_total / f['velocidade_media'] if f['velocidade_media'] > 0 else float('inf')
|
|
|
|
if tempo_missao <= 96.0:
|
|
num_pernoites = max(0, math.ceil(tempo_missao / 12.0) - 1)
|
|
fator1 = 1.25 if dist_matrix.loc[base, l_dec] > f['alcance'] else 1.0
|
|
fator2 = 1.25 if dist_matrix.loc[l_dec, l_pou] > f['alcance'] else 1.0
|
|
fator3 = 1.25 if dist_matrix.loc[l_pou, base] > f['alcance'] else 1.0
|
|
comb_missao = ((dist_matrix.loc[base, l_dec]*fator1 + dist_matrix.loc[l_dec, l_pou]*fator2 + dist_matrix.loc[l_pou, base]*fator3) / f['consumo_kml']) + (num_pernoites * f['diaria_litros'])
|
|
route_info[(m, e, l_dec, l_pou)] = {'num_pernoites': num_pernoites, 'combustivel_missao': comb_missao}
|
|
|
|
solver = pywraplp.Solver.CreateSolver('SCIP')
|
|
solver.SetTimeLimit(time_limit_sec * 1000)
|
|
solver.EnableOutput() # O MAIS IMPORTANTE: Isso escreve no stdout para o subprocess ler!
|
|
|
|
print("Building Demand Matrix...")
|
|
x, s = {}, {}
|
|
for t in dias_unicos_list:
|
|
demandas_dia = demands[df_dias == t]
|
|
for idx, d_row in demandas_dia.iterrows():
|
|
l_dec, l_pou = str(d_row['Localidade Decolagem']), str(d_row['Localidade Pouso'])
|
|
if l_dec not in valid_icaos or l_pou not in valid_icaos: continue
|
|
|
|
s[(l_dec, l_pou, t)] = solver.NumVar(0, solver.infinity(), f"s_{l_dec}_{l_pou}_{t}")
|
|
restricao_pax = solver.Constraint(float(d_row['PAX']), solver.infinity(), "")
|
|
restricao_pax.SetCoefficient(s[(l_dec, l_pou, t)], 1)
|
|
|
|
for f in frotas:
|
|
m, e = f['modelo'], f['emissor']
|
|
if (m, e, l_dec, l_pou) in route_info:
|
|
x[(m, e, l_dec, l_pou, t)] = solver.IntVar(0, solver.infinity(), f"x_{m}_{e}_{l_dec}_{l_pou}_{t}")
|
|
restricao_pax.SetCoefficient(x[(m, e, l_dec, l_pou, t)], f['capacidade'])
|
|
|
|
print("Building Fleet Temporal Matrix...")
|
|
for f in frotas:
|
|
m, e, frota_max = f['modelo'], f['emissor'], f['frota_max']
|
|
for t_atual in todas_as_datas:
|
|
restricao_frota = solver.Constraint(0, frota_max, "")
|
|
for t_start in dias_unicos_list:
|
|
diff_days = (t_atual - t_start).days
|
|
if 0 <= diff_days <= 7:
|
|
for idx, d_row in demands[df_dias == t_start].iterrows():
|
|
l_dec, l_pou = str(d_row['Localidade Decolagem']), str(d_row['Localidade Pouso'])
|
|
if (m, e, l_dec, l_pou, t_start) in x:
|
|
if diff_days <= route_info[(m, e, l_dec, l_pou)]['num_pernoites']:
|
|
restricao_frota.SetCoefficient(x[(m, e, l_dec, l_pou, t_start)], 1)
|
|
|
|
print("\n--- STARTING SCIP OPTIMIZATION ---\n")
|
|
sys.stdout.flush()
|
|
|
|
objetivo = solver.Objective()
|
|
objetivo.SetMinimization()
|
|
for var in s.values(): objetivo.SetCoefficient(var, 1e8)
|
|
for (m, e, l_dec, l_pou, t), var in x.items():
|
|
objetivo.SetCoefficient(var, route_info[(m, e, l_dec, l_pou)]['combustivel_missao'])
|
|
|
|
status = solver.Solve()
|
|
|
|
print(f"\n--- SCIP FINISHED ---")
|
|
if status in [pywraplp.Solver.OPTIMAL, pywraplp.Solver.FEASIBLE]:
|
|
obj_val = solver.Objective().Value()
|
|
try:
|
|
best_bound = solver.Objective().BestBound()
|
|
except:
|
|
best_bound = obj_val
|
|
gap = abs((obj_val - best_bound) / obj_val) * 100 if obj_val > 0 else 0
|
|
|
|
print(f"Final Status: {'OPTIMAL' if status == pywraplp.Solver.OPTIMAL else 'FEASIBLE (Time Limit)'}")
|
|
print(f"Optimized Cost: {obj_val:,.2f} L")
|
|
with open(COST_FILE, "w") as f:
|
|
f.write(str(obj_val))
|
|
print(f"Dual Bound: {best_bound:,.2f} L")
|
|
print(f"Gap: {gap:.4f}%")
|
|
|
|
results = []
|
|
for (m, e, l_dec, l_pou, t), var in x.items():
|
|
if var.solution_value() > 0:
|
|
results.append({
|
|
'Data': t, 'Modelo': m, 'Emissor': e,
|
|
'Local Decolagem': l_dec, 'Local Pouso': l_pou,
|
|
'Qtd Aeronaves': int(var.solution_value()),
|
|
'Pernoites': route_info[(m, e, l_dec, l_pou)]['num_pernoites']
|
|
})
|
|
df_results = pd.DataFrame(results)
|
|
df_results.to_csv(RESULTS_FILE, sep=';', index=False)
|
|
print(f"Results successfully saved to {RESULTS_FILE}")
|
|
else:
|
|
print("\n[ERROR] Solver failed to find a solution.")
|
|
|
|
if __name__ == '__main__':
|
|
time_limit = int(sys.argv[1]) if len(sys.argv) > 1 else 300
|
|
run_solver(time_limit)
|