Create main.py
Browse files
main.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
%%writefile main.py
|
| 2 |
+
from fastapi import FastAPI, HTTPException, Security
|
| 3 |
+
from fastapi.security import APIKeyHeader
|
| 4 |
+
from pydantic import BaseModel
|
| 5 |
+
import pandas as pd
|
| 6 |
+
import numpy as np
|
| 7 |
+
import joblib
|
| 8 |
+
|
| 9 |
+
# --- 1. CONFIGURAÇÃO ---
|
| 10 |
+
app = FastAPI(title="EcoHull AI - Transpetro API", description="Previsão de Bioincrustação e Perda de Performance")
|
| 11 |
+
|
| 12 |
+
# Carregar Modelos
|
| 13 |
+
try:
|
| 14 |
+
model = joblib.load("modelo_casco.joblib")
|
| 15 |
+
regua_fisica = joblib.load("regua_fisica.joblib")
|
| 16 |
+
le = joblib.load("label_encoder.joblib")
|
| 17 |
+
print("✅ Modelos carregados no servidor!")
|
| 18 |
+
except Exception as e:
|
| 19 |
+
print(f"❌ Erro ao carregar modelos: {e}")
|
| 20 |
+
|
| 21 |
+
# Segurança (Simples)
|
| 22 |
+
API_KEY = "hackathon_transpetro_2025"
|
| 23 |
+
api_key_header = APIKeyHeader(name="access_token", auto_error=False)
|
| 24 |
+
|
| 25 |
+
async def get_api_key(api_key_header: str = Security(api_key_header)):
|
| 26 |
+
if api_key_header == API_KEY: return api_key_header
|
| 27 |
+
raise HTTPException(status_code=403, detail="Token Inválido")
|
| 28 |
+
|
| 29 |
+
# --- 2. MODELO DE DADOS (O que o Backend deve mandar) ---
|
| 30 |
+
class DadosNavio(BaseModel):
|
| 31 |
+
shipName: str
|
| 32 |
+
speed: float # Nós
|
| 33 |
+
duration: float # Horas
|
| 34 |
+
distance: float # Milhas
|
| 35 |
+
beaufortScale: int
|
| 36 |
+
Area_Molhada: float
|
| 37 |
+
MASSA_TOTAL_TON: float
|
| 38 |
+
TIPO_COMBUSTIVEL_PRINCIPAL: str # "VLSFO", "MGO"
|
| 39 |
+
decLatitude: float
|
| 40 |
+
decLongitude: float
|
| 41 |
+
DiasDesdeUltimaLimpeza: float
|
| 42 |
+
|
| 43 |
+
# --- 3. PIPELINE DE ENGENHARIA (A Mágica) ---
|
| 44 |
+
def processar_dados(data: dict):
|
| 45 |
+
df = pd.DataFrame([data])
|
| 46 |
+
|
| 47 |
+
# A. Geo Risk
|
| 48 |
+
def get_region_risk(row):
|
| 49 |
+
lat, lon = row['decLatitude'], row['decLongitude']
|
| 50 |
+
if (-10 <= lat <= 22) and (95 <= lon <= 145): return 5.0 # Asia
|
| 51 |
+
if (18 <= lat <= 30) and (-98 <= lon <= -80): return 5.0 # Golfo
|
| 52 |
+
if (-18 <= lat <= 5) and (-50 <= lon <= -34): return 5.0 # BR NE
|
| 53 |
+
if (30 <= lat <= 45) and (-6 <= lon <= 36): return 4.5 # Med
|
| 54 |
+
if (-25 <= lat < -18) and (-55 <= lon <= -39): return 4.0 # BR SE
|
| 55 |
+
if (-34 <= lat < -25) and (-55 <= lon <= -47): return 3.5 # BR Sul
|
| 56 |
+
abs_lat = abs(lat)
|
| 57 |
+
return 5.0 if abs_lat <= 23.5 else (3.0 if abs_lat <= 40 else 2.0)
|
| 58 |
+
|
| 59 |
+
df['RISCO_REGIONAL'] = df.apply(get_region_risk, axis=1)
|
| 60 |
+
|
| 61 |
+
# B. Química
|
| 62 |
+
mapa_energia = {'LSHFO': 40.5, 'MGO': 42.7, 'VLSFO': 41.2}
|
| 63 |
+
def get_densidade(tipo):
|
| 64 |
+
for k, v in mapa_energia.items():
|
| 65 |
+
if str(k) in str(tipo): return v
|
| 66 |
+
return 41.0
|
| 67 |
+
|
| 68 |
+
df['fator_energia'] = df['TIPO_COMBUSTIVEL_PRINCIPAL'].apply(get_densidade)
|
| 69 |
+
df['ENERGIA_CALCULADA_MJ'] = df['MASSA_TOTAL_TON'] * 1000 * df['fator_energia']
|
| 70 |
+
|
| 71 |
+
# C. Física (Usando a Régua Carregada)
|
| 72 |
+
df['velocidade_cubo'] = df['speed'] ** 3
|
| 73 |
+
X_regua = (df['velocidade_cubo'] * df['duration']).values.reshape(-1,1)
|
| 74 |
+
|
| 75 |
+
# Previsão da régua
|
| 76 |
+
df['ENERGIA_ESPERADA'] = regua_fisica.predict(X_regua)
|
| 77 |
+
df['PERFORMANCE_LOSS_MJ'] = df['ENERGIA_CALCULADA_MJ'] - df['ENERGIA_ESPERADA']
|
| 78 |
+
|
| 79 |
+
# Seleção Final
|
| 80 |
+
cols = ['speed', 'duration', 'distance', 'beaufortScale',
|
| 81 |
+
'Area_Molhada', 'PERFORMANCE_LOSS_MJ',
|
| 82 |
+
'DiasDesdeUltimaLimpeza', 'velocidade_cubo', 'RISCO_REGIONAL']
|
| 83 |
+
|
| 84 |
+
return df[cols].fillna(0)
|
| 85 |
+
|
| 86 |
+
# --- 4. ENDPOINT (Onde o Backend bate) ---
|
| 87 |
+
@app.post("/predict")
|
| 88 |
+
def predict(dados: DadosNavio, token: str = Security(get_api_key)):
|
| 89 |
+
try:
|
| 90 |
+
# 1. Processar
|
| 91 |
+
df_pronto = processar_dados(dados.dict())
|
| 92 |
+
|
| 93 |
+
# 2. Predizer
|
| 94 |
+
pred_encoded = model.predict(df_pronto)[0]
|
| 95 |
+
pred_label = int(pred_encoded)
|
| 96 |
+
|
| 97 |
+
# 3. Decodificar (Label Encoder opcional se soubermos que 0-4 é LOF)
|
| 98 |
+
# Vamos retornar o número e o texto
|
| 99 |
+
texto_lof = ["BOM", "REGULAR", "ATENÇÃO", "RUIM", "CRÍTICO"][pred_label]
|
| 100 |
+
|
| 101 |
+
# 4. Cálculo Financeiro Rápido (Estimativa)
|
| 102 |
+
perda_mj = float(df_pronto['PERFORMANCE_LOSS_MJ'].values[0])
|
| 103 |
+
prejuizo_usd = (perda_mj / 41200.0) * 650.0 if perda_mj > 0 else 0.0
|
| 104 |
+
|
| 105 |
+
return {
|
| 106 |
+
"status": "sucesso",
|
| 107 |
+
"navio": dados.shipName,
|
| 108 |
+
"lof_previsto": pred_label,
|
| 109 |
+
"condicao_texto": texto_lof,
|
| 110 |
+
"dados_tecnicos": {
|
| 111 |
+
"perda_performance_mj": round(perda_mj, 2),
|
| 112 |
+
"risco_regional": float(df_pronto['RISCO_REGIONAL'].values[0]),
|
| 113 |
+
"prejuizo_estimado_viagem_usd": round(prejuizo_usd, 2)
|
| 114 |
+
},
|
| 115 |
+
"recomendacao": "AGENDAR LIMPEZA" if pred_label >= 3 else "MONITORAR"
|
| 116 |
+
}
|
| 117 |
+
except Exception as e:
|
| 118 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 119 |
+
|
| 120 |
+
@app.get("/")
|
| 121 |
+
def home():
|
| 122 |
+
return {"msg": "API EcoHull Online. Use POST /predict"}
|