| import os |
| import sys |
| import asyncio |
| import numpy as np |
| import pandas as pd |
| import io |
| import time |
| import logging |
| from datetime import datetime, date |
| from typing import Dict, List |
| from functools import partial |
|
|
| |
| from fastapi import FastAPI, HTTPException, Security, Depends, Request, File, UploadFile |
| from fastapi.security import APIKeyHeader |
| from fastapi.responses import StreamingResponse |
| from pydantic import BaseModel |
| |
| from smi_ssed.load import load_smi_ssed |
| import torch |
| from torch import nn |
| from torch.utils.data import Dataset, DataLoader |
| from tqdm import tqdm |
|
|
| logging.basicConfig(level=logging.INFO) |
| app = FastAPI() |
|
|
|
|
| |
| |
|
|
| |
| api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False) |
|
|
|
|
| |
|
|
| |
| class Message(BaseModel): |
| message: str |
|
|
| @app.post("/refresh-keys", response_model=Message) |
| async def refresh_keys(): |
| |
| return {"message": "API key validation is now handled via database on demand."} |
|
|
|
|
| |
| class SmileInput(BaseModel): |
| smiles: str |
|
|
|
|
| |
| |
| SMILES_COLUMN_NAME = 'SMILES' |
| FINETUNED_CHECKPOINT_PATH = '/app/CDI/model/finetuned_model.pth' |
| YOUR_EMBEDDING_DIM = 8192 |
| MAX_LENGTH = 128 |
| BATCH_SIZE = 32 |
|
|
| device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') |
| print(f"Using device: {device}") |
|
|
| |
| class SmiSsedPredictor(nn.Module): |
| def __init__(self, smi_ssed_model, embedding_dim): |
| super(SmiSsedPredictor, self).__init__() |
| self.base_model_encoder = smi_ssed_model.encoder |
| true_hidden_dim = self.base_model_encoder.mamba.embedding.weight.shape[1] |
| self.regressor = nn.Linear(true_hidden_dim, embedding_dim) |
|
|
| def forward(self, input_ids, attention_mask): |
| outputs = self.base_model_encoder(input_ids, mask=attention_mask) |
| hidden_states = outputs[0] |
|
|
| if hidden_states.dim() == 2: |
| batch_size = input_ids.shape[0] |
| if batch_size > 0: |
| seq_len = hidden_states.shape[0] // batch_size |
| hidden_states = hidden_states.view(batch_size, seq_len, -1) |
|
|
| expanded_mask = attention_mask.unsqueeze(-1) |
|
|
| if expanded_mask.shape[1] != hidden_states.shape[1]: |
| mask_for_pooling = torch.zeros_like(hidden_states) |
| slice_len = min(expanded_mask.shape[1], hidden_states.shape[1]) |
| mask_for_pooling[:, :slice_len, :] = expanded_mask[:, :slice_len, :] |
| else: |
| mask_for_pooling = expanded_mask |
|
|
| sum_hidden_states = torch.sum(hidden_states * mask_for_pooling, 1) |
| sum_mask = torch.clamp(mask_for_pooling.sum(1), min=1e-9) |
| molecular_representation = sum_hidden_states / sum_mask |
|
|
| predicted_embedding = self.regressor(molecular_representation) |
| return predicted_embedding |
|
|
| |
| class InferenceDataset(Dataset): |
| def __init__(self, smiles_list, tokenizer, max_length): |
| self.tokenizer = tokenizer |
| self.smiles = smiles_list |
| self.max_length = max_length |
|
|
| def __len__(self): |
| return len(self.smiles) |
|
|
| def __getitem__(self, index): |
| smiles_str = self.smiles[index] |
| encoding = self.tokenizer.encode_plus( |
| smiles_str, |
| add_special_tokens=True, |
| max_length=self.max_length, |
| padding='max_length', |
| truncation=True, |
| return_attention_mask=True, |
| return_tensors='pt', |
| ) |
| input_ids = encoding['input_ids'] |
| attention_mask = (input_ids != 0).float() |
| return { |
| 'input_ids': input_ids.flatten(), |
| 'attention_mask': attention_mask.flatten() |
| } |
|
|
| try: |
| smi_ssed_base_model = load_smi_ssed( |
| folder="/workspace/materials.smi_ssed/smi_ssed/inference/smi_ssed", |
| ckpt_filename='smi_ssed_130.pt' |
| ) |
| tokenizer = smi_ssed_base_model.tokenizer |
| model = SmiSsedPredictor(smi_ssed_base_model, YOUR_EMBEDDING_DIM).to(device) |
| print(f"Loading fine-tuned weights from: {FINETUNED_CHECKPOINT_PATH}") |
| checkpoint = torch.load(FINETUNED_CHECKPOINT_PATH, map_location=device) |
| model.load_state_dict(checkpoint['model_state_dict']) |
| model.eval() |
| except Exception as e: |
| logging.error(f"Failed to load ML model dependencies: {e}") |
| |
|
|
|
|
|
|
| async def featurizer_generator(file_content_buffer: io.StringIO): |
| logging.info("Generator started with in-memory buffer...") |
| |
| try: |
| inference_df = pd.read_csv(file_content_buffer) |
| print("--- 1. Loading Model and Tokenizer ---") |
|
|
| |
| |
|
|
| print("Model loaded successfully.") |
|
|
| print("\n--- 2. Preparing Data for Inference ---") |
| |
|
|
| smiles_to_predict = inference_df[SMILES_COLUMN_NAME].tolist() |
| print(f"Found {len(smiles_to_predict)} SMILES to process.") |
|
|
| |
| inference_dataset = InferenceDataset(smiles_to_predict, tokenizer, MAX_LENGTH) |
| inference_loader = DataLoader(inference_dataset, batch_size=BATCH_SIZE, shuffle=False) |
|
|
|
|
| with torch.no_grad(): |
| for batch in tqdm(inference_loader, desc="Predicting Embeddings"): |
| try: |
|
|
| input_ids = batch['input_ids'].to(device) |
| attention_mask = batch['attention_mask'].to(device) |
|
|
|
|
| batch_outputs = [] |
| for i in range(input_ids.size(0)): |
| single_output = model(input_ids[i].unsqueeze(0), attention_mask[i].unsqueeze(0)) |
| batch_outputs.append(single_output) |
| outputs = torch.cat(batch_outputs, dim=0) |
| |
| |
| outputs =outputs.cpu().numpy() |
| current_batch_rows = outputs.shape[0] |
| rows_to_pad = BATCH_SIZE - current_batch_rows |
| if rows_to_pad > 0: |
| DTYPE = outputs.dtype |
| padding = np.zeros((rows_to_pad, YOUR_EMBEDDING_DIM), dtype=DTYPE) |
| outputs = np.vstack([outputs, padding]) |
|
|
| yield outputs.tobytes() |
| logging.info(f"Streamed batch {i+1}") |
| await asyncio.sleep(0.05) |
| |
| del input_ids, attention_mask, batch_outputs, outputs |
| torch.cuda.empty_cache() |
| except Exception as e: |
| logging.error(f"Error processing batch {i+1}: {e}") |
| break |
| except Exception as e: |
| logging.error(f"Failed to read or process CSV buffer: {e}") |
|
|
| logging.info("Finished streaming.") |
|
|
|
|
| from typing import Annotated |
|
|
| from typing import Annotated |
| from fastapi import Depends, Request, Security |
|
|
|
|
|
|
|
|
| |
|
|
| @app.post("/stream-features-from-csv") |
| async def stream_features_from_csv( |
| |
| file: UploadFile = File(...) |
| ): |
| """ |
| Endpoint for CSV stream featurization with database-backed rate limiting. |
| """ |
| file_content = (await file.read()).decode("utf-8") |
| file_buffer = io.StringIO(file_content) |
|
|
| return StreamingResponse( |
| featurizer_generator(file_buffer), |
| media_type="application/octet-stream" |
| ) |
|
|
| @app.post("/predict-single-smile", response_model=List[float]) |
| async def predict_single_smile( |
| smile_input: SmileInput |
| ): |
| """ |
| Endpoint for single SMILES prediction with database-backed rate limiting. |
| """ |
| try: |
| |
| inference_dataset = InferenceDataset([smile_input.smiles], tokenizer, MAX_LENGTH) |
| input_data = inference_dataset[0] |
| input_ids = input_data['input_ids'].to(device).unsqueeze(0) |
| attention_mask = input_data['attention_mask'].to(device).unsqueeze(0) |
|
|
| |
| with torch.no_grad(): |
| embedding = model(input_ids, attention_mask) |
| embedding = embedding.cpu().numpy().flatten().tolist() |
| return embedding |
| except Exception as e: |
| |
| raise HTTPException(status_code=500, detail=f"Error processing SMILES: {str(e)}") |
| |
|
|
|
|
|
|
|
|