ailixir-api / app /Services /AdmetService.php
AILIXIR Bot
Auto-sync: 201d269f287526269b8ffded5f450ff36b46c740
907b200
Raw
History Blame Contribute Delete
13.2 kB
<?php
namespace App\Services;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Redis;
use App\Models\Admet;
class AdmetService
{
protected int $batchSize = 100;
protected int $singleBatchLimit = 6;
protected int $timeout = 120;
protected string $queueKey = 'admet_batch_queue';
protected string $lockKey = 'admet_batch_lock';
protected function parseSmiles(string $input): array
{
if (empty($input)) {
return [];
}
$decoded = json_decode($input, true);
if (is_array($decoded)) {
$result = array_filter(array_map('trim', $decoded));
return array_slice(array_values($result), 0, $this->singleBatchLimit);
}
$input = preg_replace('/[;|\\t\\n\\r\\\\\\/:\\-_ ]+/', ',', $input);
$smiles = explode(',', $input);
$smiles = array_map('trim', $smiles);
$smiles = array_filter($smiles, fn($value) => $value !== '' && $value !== null);
$smiles = array_values(array_unique($smiles));
return array_slice($smiles, 0, $this->singleBatchLimit);
}
protected function parseFileContent(string $content, string $extension): array
{
$lines = explode("\n", $content);
$smilesList = [];
foreach ($lines as $line) {
$line = trim($line);
if (empty($line) || str_starts_with($line, '#')) continue;
if ($extension === 'csv') {
if (str_contains(strtolower($line), 'smiles')) continue;
$row = str_getcsv($line);
$smiles = trim($row[0] ?? '');
} else {
$smiles = $line;
}
if (!empty($smiles)) {
$smilesList[] = $smiles;
}
}
return array_slice(array_values(array_unique($smilesList)), 0, $this->batchSize);
}
public function predictFromFile(string $fileContent, string $extension): array
{
$smilesList = $this->parseFileContent($fileContent, $extension);
if (empty($smilesList)) {
throw new \Exception('No valid SMILES found in the uploaded file');
}
return $this->processSmilesList($smilesList, $extension);
}
public function predictBatchFromString(string $input)
{
$smilesList = $this->parseSmiles($input);
if (empty($smilesList)) {
throw new \Exception('No valid SMILES provided');
}
return $this->processSmilesList($smilesList);
}
public function predictSingle(string $smiles)
{
// البحث بدون user_id
$existing = Admet::where('smiles', $smiles)->first();
if ($existing) {
return $this->formatResult($existing, $smiles);
}
return $this->predictBatchFromString($smiles);
}
protected function processSmilesList(array $smilesList, ?string $fileType = null): array
{
[$dbResults, $missingSmiles] = $this->getExistingPredictions($smilesList);
$apiResults = [];
if (!empty($missingSmiles)) {
$apiResults = $this->fetchFromApi($missingSmiles);
}
$result = [
'total_processed' => count($dbResults) + count($apiResults),
'total_smiles' => count($smilesList),
'results' => array_merge($dbResults, $apiResults)
];
if ($fileType) {
$result['file_type'] = $fileType;
}
return $result;
}
protected function getExistingPredictions(array $smilesList): array
{
$dbResults = [];
$missingSmiles = [];
foreach ($smilesList as $smiles) {
// إزالة التصفية حسب user_id
$existing = Admet::where('smiles', $smiles)->first();
if ($existing) {
$dbResults[] = array_merge($this->formatResult($existing, $smiles), ['source' => 'database']);
} else {
$missingSmiles[] = $smiles;
}
}
return [$dbResults, $missingSmiles];
}
protected function fetchFromApi(array $smilesList): array
{
if (empty($smilesList)) {
return [];
}
try {
$response = Http::timeout($this->timeout)->post(
config('services.admet.url') . '/predict/batch',
['smiles_list' => $smilesList]
);
if (!$response->successful()) {
\Log::error('ADMET API request failed', [
'status' => $response->status(),
'body' => $response->body(),
]);
return $this->createErrorResults($smilesList, 'API request failed: ' . $response->status());
}
$data = $response->json();
$results_data = isset($data['results']) ? $data : ['results' => $data];
return $this->saveApiResults($results_data, $smilesList);
} catch (\Exception $e) {
\Log::error('ADMET API exception', ['error' => $e->getMessage()]);
return $this->createErrorResults($smilesList, $e->getMessage());
}
}
protected function saveApiResults(array $data, array $smilesList): array
{
$results = [];
if (isset($data['results']) && is_array($data['results'])) {
foreach ($data['results'] as $index => $result) {
if (!isset($smilesList[$index])) continue;
$smiles = $smilesList[$index];
$predictions_data = $result['predictions'] ?? $result;
if (!empty($result['error']) || empty($predictions_data)) {
$results[] = [
'smiles' => $smiles,
'error' => $result['error'] ?? 'No predictions returned',
'source' => 'api',
];
continue;
}
// حفظ بدون user_id
$admet = $this->saveToDatabase($smiles, $predictions_data);
$results[] = array_merge($this->formatResult($admet, $smiles), ['source' => 'api']);
}
} else {
\Log::warning('Unexpected ADMET API response format', ['data' => $data]);
return $this->createErrorResults($smilesList, 'Invalid response format from API');
}
return $results;
}
protected function saveToDatabase(string $smiles, array $predictions): Admet
{
// إزالة user_id من updateOrCreate
return Admet::updateOrCreate(
['smiles' => $smiles], // فقط smiles للبحث
[
'absorption' => $predictions['Absorption'] ?? $predictions['absorption'] ?? null,
'distribution' => $predictions['Distribution'] ?? $predictions['distribution'] ?? null,
'metabolism' => $predictions['Metabolism'] ?? $predictions['metabolism'] ?? null,
'excretion' => $predictions['Excretion'] ?? $predictions['excretion'] ?? null,
'toxicity' => $predictions['Toxicity'] ?? $predictions['toxicity'] ?? null,
]
);
}
protected function createErrorResults(array $smilesList, string $error): array
{
$errors = [];
foreach ($smilesList as $smiles) {
$errors[] = [
'smiles' => $smiles,
'error' => $error,
'source' => 'error'
];
}
return $errors;
}
protected function formatResult(Admet $admet, string $smiles): array
{
return [
'smiles' => $smiles,
'absorption' => (float) ($admet->absorption ?? 0),
'distribution' => (float) ($admet->distribution ?? 0),
'metabolism' => (float) ($admet->metabolism ?? 0),
'excretion' => (float) ($admet->excretion ?? 0),
'toxicity' => (float) ($admet->toxicity ?? 0),
];
}
// ==================== نظام قائمة الانتظار ====================
protected function waitForResult(string $requestId, int $timeout = 30): array
{
$start = microtime(true);
while (true) {
$result = Cache::get("admet_result_$requestId");
if ($result !== null) {
Cache::forget("admet_result_$requestId");
return $result;
}
if ((microtime(true) - $start) > $timeout) {
throw new \Exception('Timeout waiting for result after ' . $timeout . ' seconds');
}
usleep(100000);
}
}
protected function tryProcessBatch(): void
{
$lock = Cache::lock($this->lockKey, 60);
if ($lock->get()) {
try {
usleep(10000);
$this->processBatch();
} finally {
$lock->release();
}
}
}
protected function processBatch(): void
{
$requests = [];
while (($item = Redis::lpop($this->queueKey)) !== null) {
$requests[] = json_decode($item, true);
}
if (empty($requests)) {
return;
}
[$allSmiles, $smilesToRequestMap] = $this->buildRequestMap($requests);
if (empty($allSmiles)) {
return;
}
try {
$response = Http::timeout($this->timeout)->post(
config('services.admet.url') . '/predict/batch',
['smiles_list' => $allSmiles]
);
if (!$response->successful()) {
throw new \Exception('Batch request failed: ' . $response->body());
}
$data = $response->json();
$apiResults = $data['results'] ?? [];
$resultsPerRequest = $this->distributeBatchResults($apiResults, $smilesToRequestMap);
foreach ($resultsPerRequest as $requestId => $requestResults) {
$payload = [
'total_processed' => count($requestResults),
'total_smiles' => count($requestResults),
'results' => $requestResults,
];
Cache::put("admet_result_$requestId", $payload, 60);
}
$this->handleMissingRequests($requests, $resultsPerRequest);
} catch (\Exception $e) {
\Log::error('ADMET batch processing failed: ' . $e->getMessage());
foreach ($requests as $req) {
Cache::put("admet_result_{$req['id']}", [
'error' => 'Batch processing failed: ' . $e->getMessage()
], 60);
}
throw $e;
}
}
protected function buildRequestMap(array $requests): array
{
$smilesToRequestMap = [];
$allSmiles = [];
foreach ($requests as $reqIndex => $req) {
foreach ($req['smiles_list'] as $smilesIndex => $smiles) {
$smilesKey = count($allSmiles);
$allSmiles[] = $smiles;
$smilesToRequestMap[$smilesKey] = [
'request_id' => $req['id'],
'request_index' => $reqIndex,
'smiles_index' => $smilesIndex,
'smiles' => $smiles,
// إزالة user_id من هنا
];
}
}
return [$allSmiles, $smilesToRequestMap];
}
protected function distributeBatchResults(array $apiResults, array $smilesToRequestMap): array
{
$resultsPerRequest = [];
foreach ($apiResults as $resultIndex => $result) {
if (!isset($smilesToRequestMap[$resultIndex])) {
continue;
}
$map = $smilesToRequestMap[$resultIndex];
$requestId = $map['request_id'];
$predictions = $result['predictions'] ?? $result;
// حفظ بدون user_id
$admet = $this->saveToDatabase($map['smiles'], $predictions);
$resultsPerRequest[$requestId][] = $this->formatResult($admet, $map['smiles']);
}
return $resultsPerRequest;
}
protected function handleMissingRequests(array $requests, array $resultsPerRequest): void
{
foreach ($requests as $req) {
if (!isset($resultsPerRequest[$req['id']])) {
Cache::put("admet_result_{$req['id']}", [
'error' => 'No predictions received for any SMILES',
'smiles_list' => $req['smiles_list']
], 60);
}
}
}
public function getBatchStatus(string $requestId)
{
return Cache::get("admet_result_$requestId");
}
public function clearOldResults(int $olderThanMinutes = 60): void
{
$keys = Redis::keys('admet_result_*');
if (empty($keys)) {
return;
}
foreach ($keys as $key) {
$ttl = Redis::ttl($key);
if ($ttl < 0 || $ttl > ($olderThanMinutes * 60)) {
Redis::del($key);
}
}
}
}