Spaces:
Runtime error
Runtime error
Download api_service.py from happynickamazon/zenvolt-api: direct link, hf CLI and curl.
- Browser
- Download file 15.7 kB
-
https://huggingface.co/spaces/happynickamazon/zenvolt-api/resolve/main/api_service.py
- Command line
-
hf download hf://spaces/happynickamazon/zenvolt-api/api_service.py
-
curl -L -o api_service.py https://huggingface.co/spaces/happynickamazon/zenvolt-api/resolve/main/api_service.py
15.7 kB
| """ | |
| FastAPI Service for NASA SOH Prediction Model | |
| Production API endpoint for battery health predictions. | |
| Connects Python ML pipeline to React frontend apps. | |
| """ | |
| from fastapi import FastAPI, File, UploadFile, HTTPException, Depends, Query | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.responses import JSONResponse | |
| from pydantic import BaseModel, Field | |
| from typing import List, Optional, Dict | |
| from sqlalchemy.orm import Session | |
| from datetime import datetime | |
| import pandas as pd | |
| import numpy as np | |
| import pickle | |
| import os | |
| import base64 | |
| from pathlib import Path | |
| import json | |
| import warnings | |
| warnings.filterwarnings('ignore') | |
| from predict_soh import predict_soh, calculate_data_quality | |
| from feature_engineering_causal import engineer_features_causal | |
| from database import get_db, init_db, Asset, HealthPrediction | |
| # Initialize FastAPI app | |
| app = FastAPI( | |
| title="ZenVolt NASA SOH Trust Engine API", | |
| description="Production API for battery State of Health prediction using NASA-trained models", | |
| version="2.0.0" | |
| ) | |
| # Initialize database on startup | |
| async def startup_event(): | |
| init_db() | |
| print("✓ API service started") | |
| # CORS middleware | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_credentials=False, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # Request/Response Models | |
| class BMSDataPoint(BaseModel): | |
| """Single BMS data point.""" | |
| cycle_index: int | |
| voltage: float | |
| current: float | |
| temperature: float | |
| capacity: float | |
| timestamp: Optional[str] = None | |
| class BMSDataInput(BaseModel): | |
| """BMS data input for prediction.""" | |
| data: List[BMSDataPoint] | |
| initial_capacity: Optional[float] = None | |
| forecast_horizons: Optional[List[int]] = Field(default=[10, 50, 100]) | |
| class HealthPredictionResponse(BaseModel): | |
| """ML prediction response.""" | |
| current_soh: Dict[str, float] | |
| health_score: float | |
| remaining_life: Dict[str, float] | |
| degradation_rate: Dict[str, float] | |
| forecasts: Dict[str, Dict[str, float]] | |
| data_quality: Dict[str, float] | |
| current_cycle: int | |
| confidence: float | |
| model_info: Dict[str, str] | |
| class ModelInfoResponse(BaseModel): | |
| """Model information for Trust Engine proof.""" | |
| model_type: str | |
| training_data: str | |
| validation_data: str | |
| metrics: Dict[str, float] | |
| feature_count: int | |
| version: str | |
| class AssetCreate(BaseModel): | |
| """Model for creating a new asset.""" | |
| owner_id: str | |
| project_name: str | |
| asset_id: str | |
| location: str | |
| capacity: float | |
| bms_provider: Optional[str] = None | |
| category: List[str] = Field(default_factory=list) | |
| project_stage: str = "Pending" | |
| initial_prediction: Optional[Dict] = None # Initial prediction data | |
| class AssetResponse(BaseModel): | |
| """Asset response model.""" | |
| id: str | |
| owner_id: str | |
| projectName: str | |
| assetId: str | |
| location: str | |
| capacity: float | |
| bmsProvider: Optional[str] | |
| category: List[str] | |
| projectStage: str | |
| healthScore: Optional[float] | |
| remainingLife: Optional[float] | |
| degradationRate: Optional[float] | |
| confidence: Optional[float] | |
| stateOfCharge: Optional[float] | |
| voltage: Optional[float] | |
| temperature: Optional[float] | |
| isOnline: bool | |
| nftHash: Optional[str] | |
| nftMintDate: Optional[str] | |
| blockExplorerUrl: Optional[str] | |
| createdAt: Optional[str] | |
| updatedAt: Optional[str] | |
| lastUpdated: Optional[str] | |
| class FleetMetricsResponse(BaseModel): | |
| """Fleet metrics response.""" | |
| totalAssets: int | |
| avgHealth: float | |
| totalCapacity: float | |
| certifiedCount: int | |
| criticalCount: int | |
| avgRemainingLife: float | |
| # Include asset routes safely | |
| print("🔍 Attempting to mount asset routes...") | |
| try: | |
| import asset_routes | |
| app.include_router(asset_routes.router) | |
| print("✅ SUCCESS: Asset routes mounted at /api/assets") | |
| except Exception as e: | |
| print(f"❌ CRITICAL FAILURE: Could not mount asset routes: {e}") | |
| traceback.print_exc() | |
| async def root(): | |
| """Root endpoint.""" | |
| environment = "Docker/HuggingFace" if os.path.exists("/.dockerenv") or os.environ.get("HOSTNAME", "").endswith("-space") else "Local/Standalone" | |
| return { | |
| "service": "ZenVolt NASA SOH Trust Engine API", | |
| "version": "2.1.2", | |
| "status": "operational", | |
| "environment": environment, | |
| "engine": "Standard (Random Forest + Scikit-Learn)", | |
| "endpoints": { | |
| "test": "/api/test", | |
| "assets": "/api/assets", | |
| "model_info": "/api/model-info", | |
| "visualizations": "/api/visualizations/{plot_name}" | |
| } | |
| } | |
| async def test_route(): | |
| """Direct test route to verify sub-paths are reachable.""" | |
| return {"message": "Sub-path connectivity verified!", "status": "200 OK"} | |
| async def list_routes(): | |
| """Diagnostic endpoint to see what the server actually knows.""" | |
| routes = [] | |
| for route in app.routes: | |
| routes.append({ | |
| "path": route.path, | |
| "name": route.name, | |
| "methods": list(route.methods) if hasattr(route, "methods") else [] | |
| }) | |
| return { | |
| "registered_count": len(routes), | |
| "registered_routes": routes, | |
| "file_structure": os.listdir(".") | |
| } | |
| async def health_check(): | |
| """Health check endpoint.""" | |
| model_exists = os.path.exists('./models/nasa_soh_model.pkl') | |
| pipeline_exists = os.path.exists('./models/feature_pipeline_causal.pkl') | |
| return { | |
| "status": "healthy" if (model_exists and pipeline_exists) else "degraded", | |
| "model_loaded": model_exists, | |
| "pipeline_loaded": pipeline_exists | |
| } | |
| async def predict_health_metrics(input_data: BMSDataInput): | |
| """ | |
| Main endpoint: Predict battery health from BMS data. | |
| Uses NASA-trained model for Trust Engine proof. | |
| """ | |
| try: | |
| # Convert to DataFrame | |
| data_dict = { | |
| 'cycle_index': [d.cycle_index for d in input_data.data], | |
| 'voltage': [d.voltage for d in input_data.data], | |
| 'current': [d.current for d in input_data.data], | |
| 'temperature': [d.temperature for d in input_data.data], | |
| 'capacity': [d.capacity for d in input_data.data] | |
| } | |
| if input_data.data[0].timestamp: | |
| data_dict['timestamp'] = [d.timestamp for d in input_data.data] | |
| df = pd.DataFrame(data_dict) | |
| # Get predictions | |
| predictions = predict_soh( | |
| battery_data=df, | |
| model_path='./models/nasa_soh_model.pkl', | |
| pipeline_path='./models/feature_pipeline_causal.pkl', | |
| initial_capacity=input_data.initial_capacity, | |
| forecast_horizons=input_data.forecast_horizons | |
| ) | |
| # Load model info | |
| model_info = get_model_info_internal() | |
| # Add model info to response | |
| predictions['model_info'] = { | |
| 'model_type': model_info['model_type'], | |
| 'training_data': model_info['training_data'], | |
| 'validation_data': model_info['validation_data'], | |
| 'r2_score': str(model_info['metrics']['r2']), | |
| 'version': model_info['version'] | |
| } | |
| return HealthPredictionResponse(**predictions) | |
| except FileNotFoundError as e: | |
| raise HTTPException(status_code=404, detail=f"Model files not found: {e}. Please train the model first.") | |
| except ValueError as e: | |
| raise HTTPException(status_code=400, detail=f"Invalid input data: {e}") | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"Prediction failed: {str(e)}") | |
| async def predict_from_csv(file: UploadFile = File(...)): | |
| """ | |
| Predict from CSV file upload. | |
| CSV should have columns: cycle_index, voltage, current, temperature, capacity | |
| """ | |
| try: | |
| # Read CSV | |
| contents = await file.read() | |
| df = pd.read_csv(pd.io.common.BytesIO(contents)) | |
| # Validate columns | |
| required_cols = ['cycle_index', 'voltage', 'current', 'temperature', 'capacity'] | |
| missing_cols = [col for col in required_cols if col not in df.columns] | |
| if missing_cols: | |
| raise ValueError(f"Missing required columns: {missing_cols}") | |
| # Get predictions | |
| predictions = predict_soh( | |
| battery_data=df, | |
| model_path='./models/nasa_soh_model.pkl', | |
| pipeline_path='./models/feature_pipeline_causal.pkl' | |
| ) | |
| return predictions | |
| except Exception as e: | |
| raise HTTPException(status_code=400, detail=f"CSV processing failed: {str(e)}") | |
| async def get_model_info(): | |
| """Get NASA model information for Trust Engine proof.""" | |
| try: | |
| info = get_model_info_internal() | |
| return ModelInfoResponse(**info) | |
| except FileNotFoundError: | |
| raise HTTPException(status_code=404, detail="Model files not found. Please train the model first.") | |
| def get_model_info_internal() -> Dict: | |
| """Internal function to get model info.""" | |
| # Try to load expert model metadata first | |
| expert_model_path = './models/nasa_soh_model.pkl' | |
| if os.path.exists(expert_model_path): | |
| with open(expert_model_path, 'rb') as f: | |
| model_data = pickle.load(f) | |
| # Use expert model metadata if available | |
| if 'metrics' in model_data and 'val_r2' in model_data['metrics']: | |
| metrics = model_data['metrics'] | |
| return { | |
| 'model_type': model_data.get('model_type', 'RandomForestRegressor'), | |
| 'training_data': ', '.join(model_data.get('training_batteries', ['B0005', 'B0006'])), | |
| 'validation_data': model_data.get('validation_battery', 'B0007'), | |
| 'metrics': { | |
| 'r2': float(metrics.get('val_r2', metrics.get('train_r2', 0.0))), | |
| 'mae': float(metrics.get('val_mae', metrics.get('train_mae', 0.0))), | |
| 'rmse': float(metrics.get('val_rmse', metrics.get('train_rmse', 0.0))), | |
| 'mape': float(metrics.get('val_mape', metrics.get('train_mape', 0.0))) | |
| }, | |
| 'feature_count': len(model_data.get('feature_cols', [])), | |
| 'version': model_data.get('version', '2.0_expert'), | |
| 'oob_score': metrics.get('oob_score') | |
| } | |
| # Fallback to model_comparison.csv | |
| comparison_df = pd.read_csv('./data/model_comparison.csv') | |
| best_model = comparison_df.iloc[0] | |
| # Load feature pipeline | |
| if os.path.exists('./models/feature_pipeline_causal.pkl'): | |
| with open('./models/feature_pipeline_causal.pkl', 'rb') as f: | |
| pipeline_info = pickle.load(f) | |
| feature_count = len(pipeline_info['feature_cols']) | |
| version = pipeline_info.get('version', '1.0_causal') | |
| else: | |
| feature_count = 47 # From expert model | |
| version = '1.0_causal' | |
| return { | |
| 'model_type': 'NASA-trained Random Forest (Causal Features)', | |
| 'training_data': 'NASA B0005, B0006 (168 cycles each)', | |
| 'validation_data': 'NASA B0007 (168 cycles)', | |
| 'metrics': { | |
| 'r2': float(best_model['R² Score']), | |
| 'mae': float(best_model['MAE']), | |
| 'rmse': float(best_model['RMSE']), | |
| 'mape': float(best_model['MAPE (%)']) | |
| }, | |
| 'feature_count': feature_count, | |
| 'version': version | |
| } | |
| async def get_visualization(plot_name: str): | |
| """ | |
| Serve visualization images for PDF reports. | |
| Returns base64-encoded image. | |
| """ | |
| plot_map = { | |
| 'health_forecast': 'health_forecast.png', | |
| 'error_growth': 'error_growth.png', | |
| 'model_simplicity': 'model_simplicity.png', | |
| 'model_comparison': '01_model_comparison.png', | |
| 'actual_vs_predicted': '02_actual_vs_predicted.png', | |
| 'time_series': '03_time_series_prediction.png', | |
| 'rul_prediction': '04_rul_prediction.png', | |
| 'feature_importance': '05_feature_importance.png', | |
| 'transfer_learning': '06_transfer_learning.png' | |
| } | |
| filename = plot_map.get(plot_name, plot_name) | |
| plot_path = f'./plots/{filename}' | |
| if not os.path.exists(plot_path): | |
| raise HTTPException(status_code=404, detail=f"Plot not found: {plot_name}") | |
| try: | |
| with open(plot_path, 'rb') as f: | |
| image_data = base64.b64encode(f.read()).decode() | |
| return { | |
| 'plot_name': plot_name, | |
| 'image': f'data:image/png;base64,{image_data}', | |
| 'format': 'png' | |
| } | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"Failed to load plot: {str(e)}") | |
| async def get_all_visualizations(): | |
| """Get all visualization plots for PDF reports.""" | |
| plots = [ | |
| ('health_forecast', 'health_forecast.png'), | |
| ('error_growth', 'error_growth.png'), | |
| ('model_simplicity', 'model_simplicity.png'), | |
| ('model_comparison', '01_model_comparison.png'), | |
| ('actual_vs_predicted', '02_actual_vs_predicted.png'), | |
| ('time_series', '03_time_series_prediction.png'), | |
| ('rul_prediction', '04_rul_prediction.png'), | |
| ('feature_importance', '05_feature_importance.png'), | |
| ('transfer_learning', '06_transfer_learning.png') | |
| ] | |
| result = {} | |
| for plot_name, filename in plots: | |
| plot_path = f'./plots/{filename}' | |
| if os.path.exists(plot_path): | |
| try: | |
| with open(plot_path, 'rb') as f: | |
| image_data = base64.b64encode(f.read()).decode() | |
| result[plot_name] = f'data:image/png;base64,{image_data}' | |
| except Exception as e: | |
| print(f"Warning: Could not load {plot_name}: {e}") | |
| return { | |
| 'visualizations': result, | |
| 'count': len(result) | |
| } | |
| async def get_validation_report(): | |
| """Get validation report data.""" | |
| report_path = './data/nasa_model_validation.csv' | |
| if not os.path.exists(report_path): | |
| raise HTTPException(status_code=404, detail="Validation report not found. Please run production_pipeline.py first.") | |
| try: | |
| df = pd.read_csv(report_path) | |
| return { | |
| 'data': df.to_dict(orient='records'), | |
| 'summary': { | |
| 'total_cycles': len(df), | |
| 'train_cycles': len(df[df['dataset'] == 'train']), | |
| 'val_cycles': len(df[df['dataset'] == 'validation']), | |
| 'mean_mae': df['abs_error'].mean(), | |
| 'mean_rmse': np.sqrt((df['error'] ** 2).mean()), | |
| 'mean_mape': df['error_pct'].mean() | |
| } | |
| } | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"Failed to load validation report: {str(e)}") | |
| if __name__ == "__main__": | |
| import uvicorn | |
| print("="*80) | |
| print("Starting ZenVolt NASA SOH Trust Engine API") | |
| print("="*80) | |
| print("\nEndpoints:") | |
| print(" POST /api/predict - Predict battery health from BMS data") | |
| print(" GET /api/model-info - Get model information") | |
| print(" GET /api/visualizations/{plot_name} - Get visualization images") | |
| print(" GET /health - Health check") | |
| print("\nStarting server on http://localhost:8000") | |
| print("="*80) | |
| uvicorn.run(app, host="0.0.0.0", port=8000, log_level="info") | |