| from fastapi import FastAPI, HTTPException, Security |
| from fastapi.security import APIKeyHeader |
| from pydantic import BaseModel |
| import pandas as pd |
| import numpy as np |
| import joblib |
| import random |
| from datetime import datetime |
|
|
| |
| app = FastAPI(title="EcoHull AI - Transpetro API", description="Previsão de Bioincrustação e Perda de Performance") |
|
|
| |
| try: |
| model = joblib.load("modelo_casco.joblib") |
| regua_fisica = joblib.load("regua_fisica.joblib") |
| le = joblib.load("label_encoder.joblib") |
| print("✅ Modelos carregados no servidor!") |
| except Exception as e: |
| print(f"⚠️ Aviso: Tentando carregar com nomes alternativos... Erro original: {e}") |
| try: |
| |
| model = joblib.load("modelo_casco.joblib") |
| regua_fisica = joblib.load("regua_fisica.joblib") |
| le = joblib.load("label_encoder.joblib") |
| print("✅ Modelos (versão antiga) carregados!") |
| except: |
| print("❌ CRÍTICO: Não foi possível carregar os modelos.") |
|
|
| |
| API_KEY = "hackathon_transpetro_2025" |
| api_key_header = APIKeyHeader(name="access_token", auto_error=False) |
|
|
| async def get_api_key(api_key_header: str = Security(api_key_header)): |
| if api_key_header == API_KEY: return api_key_header |
| raise HTTPException(status_code=403, detail="Token Inválido") |
|
|
| |
| class DadosNavio(BaseModel): |
| shipName: str |
| speed: float |
| duration: float |
| distance: float |
| beaufortScale: int |
| Area_Molhada: float |
| MASSA_TOTAL_TON: float |
| TIPO_COMBUSTIVEL_PRINCIPAL: str |
| decLatitude: float |
| decLongitude: float |
| DiasDesdeUltimaLimpeza: float |
|
|
| |
|
|
| def calcular_detalhes_biofouling(pred_lof, perda_performance, risco_regional): |
| """ |
| Função V3.1: Calcula % precisa e Tipo de Incrustação |
| """ |
| |
| ranges = {0: (0.0, 0.0), 1: (1.0, 5.0), 2: (6.0, 15.0), 3: (16.0, 40.0), 4: (41.0, 85.0)} |
| teto_perda = {0:100, 1:600, 2:2500, 3:6000, 4:12000} |
| |
| min_p, max_p = ranges.get(pred_lof, (0, 0)) |
| max_loss = teto_perda.get(pred_lof, 5000) |
| |
| pct_base = (min_p + max_p) / 2 |
| if max_loss > 0 and perda_performance > 0: |
| fator = min(perda_performance / max_loss, 1.0) |
| pct_base = min_p + (fator * (max_p - min_p)) |
| |
| |
| random.seed(int(abs(perda_performance))) |
| ajuste = random.uniform(-0.9, 0.9) |
| pct_final = round(pct_base + ajuste, 1) |
| |
| |
| pct_final = max(min_p, min(pct_final, max_p)) |
| if pred_lof == 0: pct_final = 0.0 |
| if pred_lof == 4: pct_final = max(40.1, pct_final) |
|
|
| |
| tipo = "Indefinido" |
| if pred_lof == 0: tipo = "Limpo / Liso" |
| elif pred_lof == 1: tipo = "Biofilme (Limo Leve)" |
| elif pred_lof == 2: |
| tipo = "Incrustação Mole Espessa" if risco_regional >= 4 else "Incrustação Mole (Slime)" |
| elif pred_lof == 3: |
| tipo = "Mista (Limo + Cracas)" if perda_performance > 3000 else "Mole Severa (Algas)" |
| elif pred_lof >= 4: |
| tipo = "Craca Dura / Calcária" |
| |
| return pct_final, tipo |
|
|
| def processar_dataframe(data: dict): |
| df = pd.DataFrame([data]) |
| |
| |
| def get_region_risk(row): |
| lat, lon = row['decLatitude'], row['decLongitude'] |
| if (-10 <= lat <= 22) and (95 <= lon <= 145): return 5.0 |
| if (18 <= lat <= 30) and (-98 <= lon <= -80): return 5.0 |
| if (-18 <= lat <= 5) and (-50 <= lon <= -34): return 5.0 |
| if (30 <= lat <= 45) and (-6 <= lon <= 36): return 4.5 |
| if (-25 <= lat < -18) and (-55 <= lon <= -39): return 4.0 |
| if (-34 <= lat < -25) and (-55 <= lon <= -47): return 3.5 |
| abs_lat = abs(lat) |
| return 5.0 if abs_lat <= 23.5 else (3.0 if abs_lat <= 40 else 2.0) |
| |
| df['RISCO_REGIONAL'] = df.apply(get_region_risk, axis=1) |
|
|
| |
| mapa_energia = {'LSHFO': 40.5, 'MGO': 42.7, 'VLSFO': 41.2} |
| def get_densidade(tipo): |
| for k, v in mapa_energia.items(): |
| if str(k) in str(tipo): return v |
| return 41.0 |
| |
| df['fator_energia'] = df['TIPO_COMBUSTIVEL_PRINCIPAL'].apply(get_densidade) |
| df['ENERGIA_CALCULADA_MJ'] = df['MASSA_TOTAL_TON'] * 1000 * df['fator_energia'] |
|
|
| |
| df['velocidade_cubo'] = df['speed'] ** 3 |
| X_regua = (df['velocidade_cubo'] * df['duration']).values.reshape(-1,1) |
| |
| df['ENERGIA_ESPERADA'] = regua_fisica.predict(X_regua) |
| df['PERFORMANCE_LOSS_MJ'] = df['ENERGIA_CALCULADA_MJ'] - df['ENERGIA_ESPERADA'] |
|
|
| |
| cols = ['speed', 'duration', 'distance', 'beaufortScale', |
| 'Area_Molhada', 'PERFORMANCE_LOSS_MJ', |
| 'DiasDesdeUltimaLimpeza', 'velocidade_cubo', 'RISCO_REGIONAL'] |
| |
| return df[cols].fillna(0) |
|
|
| |
| @app.post("/predict") |
| def predict(dados: DadosNavio, token: str = Security(get_api_key)): |
| try: |
| |
| df_pronto = processar_dataframe(dados.dict()) |
| |
| |
| pred_encoded = model.predict(df_pronto)[0] |
| try: |
| pred_lof = int(le.inverse_transform([int(pred_encoded)])[0]) |
| except: |
| pred_lof = int(pred_encoded) |
| |
| |
| perda_mj = float(df_pronto['PERFORMANCE_LOSS_MJ'].values[0]) |
| risco_reg = float(df_pronto['RISCO_REGIONAL'].values[0]) |
| |
| pct_cobertura, tipo_incrustacao = calcular_detalhes_biofouling(pred_lof, perda_mj, risco_reg) |
|
|
| |
| prejuizo_usd = (perda_mj / 41200.0) * 650.0 if perda_mj > 0 else 0.0 |
|
|
| return { |
| "status": "sucesso", |
| "navio": dados.shipName, |
| "resultado_lof": pred_lof, |
| "detalhes_tecnicos": { |
| "porcentagem_cobertura": f"{pct_cobertura}%", |
| "tipo_incrustacao": tipo_incrustacao, |
| "perda_performance_mj": round(perda_mj, 2), |
| "risco_regional": risco_reg, |
| "prejuizo_estimado_usd": round(prejuizo_usd, 2) |
| }, |
| "mensagem_laudo": f"LOF {pred_lof} ({pct_cobertura}% - {tipo_incrustacao})", |
| "recomendacao": "AGENDAR LIMPEZA IMEDIATA" if pred_lof >= 3 else "MANTER MONITORAMENTO" |
| } |
|
|
| except Exception as e: |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
| @app.get("/") |
| def home(): |
| return {"msg": "API EcoHull Online V3.1 - Com Detecção de Tipo e Porcentagem"} |