all(), [ 'protein_name' => 'required|string|max:255', 'ligand_name' => 'nullable|string|max:255', 'ligand_file' => 'nullable|file|required_without:ligand_smiles', 'ligand_smiles' => 'nullable|string|max:2000|required_without:ligand_file', 'protein_file' => 'required|file', 'center_x' => 'required|numeric', 'center_y' => 'required|numeric', 'center_z' => 'required|numeric', 'box_size_x' => 'required|numeric', 'box_size_y' => 'required|numeric', 'box_size_z' => 'required|numeric', 'exhaustiveness' => 'nullable|integer', 'n_poses' => 'nullable|integer', ]); $validator->after(function ($validator) use ($request) { if ($request->hasFile('ligand_file') && $request->filled('ligand_smiles')) { $validator->errors()->add( 'ligand_input', 'Provide either a ligand file or a SMILES string, not both.' ); } }); if ($validator->fails()) { return $this->errorResponse('Validation Error', 422, $validator->errors()); } $isSmiles = $request->filled('ligand_smiles'); // ---- Handle Ligand ---- if ($isSmiles) { // SMILES → PDBQT conversion (synchronous, fast ~1-3s) $conversion = $this->convertSmilesToPdbqt($request->ligand_smiles); if ($conversion['status'] === 'error') { return $this->errorResponse( 'SMILES conversion failed: ' . $conversion['message'], 422 ); } $ligandPath = $conversion['output_file']; } else { $ligandFilename = Str::random(40) . '.pdbqt'; $ligandPath = storage_path('app/private/' . $request->file('ligand_file')->storeAs('docking', $ligandFilename)); } // ---- Handle Protein ---- $proteinFilename = Str::random(40) . '.pdbqt'; $proteinPath = storage_path('app/private/' . $request->file('protein_file')->storeAs('docking', $proteinFilename)); // ---- Create Database Record ---- $job = DockingJob::create([ 'user_id' => $request->user()->id, 'input_type' => $isSmiles ? 'smiles' : 'file', 'smiles' => $isSmiles ? $request->ligand_smiles : null, 'protein_name' => $request->protein_name, 'ligand_name' => $request->ligand_name, 'protein_path' => $proteinPath, 'ligand_path' => $ligandPath, 'status' => 'pending', ]); // ---- Dispatch background docking job ---- RunDockingJob::dispatch($job, [ 'center_x' => $request->center_x, 'center_y' => $request->center_y, 'center_z' => $request->center_z, 'box_size_x' => $request->box_size_x, 'box_size_y' => $request->box_size_y, 'box_size_z' => $request->box_size_z, 'exhaustiveness' => $request->exhaustiveness ?? 8, 'n_poses' => $request->n_poses ?? 5, ]); return $this->successResponse('Docking Job Successfully Queued', [ 'job_id' => $job->id, 'status' => $job->status, ]); } private function convertSmilesToPdbqt(string $smiles): array { $pythonPath = env('DOCKING_PYTHON_PATH', base_path('vina_env/bin/python')); $scriptPath = env('SMILES_SCRIPT_PATH', base_path('scripts/smiles_to_pdbqt.py')); // Generate unique output path $outputFilename = Str::random(40) . '.pdbqt'; $outputDir = storage_path('app/private/docking/generated'); $outputPath = $outputDir . '/' . $outputFilename; // Ensure output directory exists if (! is_dir($outputDir)) { mkdir($outputDir, 0755, true); } $cmd = sprintf( '%s %s %s %s', escapeshellarg($pythonPath), escapeshellarg($scriptPath), escapeshellarg($smiles), escapeshellarg($outputPath) ); try { $result = Process::timeout(120)->run($cmd); $outputData = null; $fullOutput = $result->output(); if (preg_match('/\{"status":\s*"(success|error)".*\}/s', $fullOutput, $matches)) { $outputData = json_decode($matches[0], true); } if (! $result->successful()) { $errorMsg = $outputData['message'] ?? $result->errorOutput() ?: 'Unknown conversion error'; Log::error('SMILES conversion failed', ['smiles' => $smiles, 'error' => $errorMsg]); return ['status' => 'error', 'message' => $errorMsg]; } if ($outputData && $outputData['status'] === 'success') { return $outputData; } if ($outputData && $outputData['status'] === 'error') { return $outputData; } return ['status' => 'error', 'message' => 'Unexpected output from conversion script']; } catch (\Exception $e) { Log::error('SMILES conversion exception', ['smiles' => $smiles, 'error' => $e->getMessage()]); return ['status' => 'error', 'message' => $e->getMessage()]; } } }