diff --git a/CHANGELOG.md b/CHANGELOG.md index 11d7fe67..65d08759 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [1.0.1-rc.10](https://github.com/NFDI4Chem/nmrxiv/compare/v1.0.0-rc.10...v1.0.1-rc.10) (2023-11-16) + + +### Bug Fixes + +* added molecular search ([8dfcc4a](https://github.com/NFDI4Chem/nmrxiv/commit/8dfcc4a0a2b218ee68e7d39d8916ac44e93af508)) +* enable listing of files with missing flag at root and title udpates ([05adf74](https://github.com/NFDI4Chem/nmrxiv/commit/05adf74dbf0161c38475821f7c5528597d717a4a)) +* molecules filtering and other bug fixes ([bb3d701](https://github.com/NFDI4Chem/nmrxiv/commit/bb3d7012123df556c63188443b9741e7a8ea2ab9)) +* updated molecular card link and studies filtering ([cf6763e](https://github.com/NFDI4Chem/nmrxiv/commit/cf6763e6f595285682cc870f5dd5c44bf6d0a3ef)) + ## [1.0.1-rc.9](https://github.com/NFDI4Chem/nmrxiv/compare/v1.0.0-rc.9...v1.0.1-rc.9) (2023-11-15) diff --git a/app/Console/Commands/SanitizeMolecules.php b/app/Console/Commands/SanitizeMolecules.php index bbacd0d2..28e19b7f 100644 --- a/app/Console/Commands/SanitizeMolecules.php +++ b/app/Console/Commands/SanitizeMolecules.php @@ -27,17 +27,17 @@ class SanitizeMolecules extends Command */ public function handle() { - $molecules = Molecule::whereNull('CANONICAL_SMILES')->get(); + $molecules = Molecule::whereNull('canonical_smiles')->get(); foreach ($molecules as $molecule) { echo $molecule->id; echo "\r\n"; $molecule->MOL = ' '.$molecule->MOL; - $standardisedMOL = $this->standardizeMolecule($molecule->MOL); - $molecule->CANONICAL_SMILES = $standardisedMOL['canonical_smiles']; - $molecule->STANDARD_INCHI = $standardisedMOL['inchi']; - $molecule->INCHI_KEY = $standardisedMOL['inchikey']; + $standardisedMOL = $this->standardizeMolecule($molecule->sdf); + $molecule->canonical_smiles = $standardisedMOL['canonical_smiles']; + $molecule->standard_inchi = $standardisedMOL['inchi']; + $molecule->inchi_key = $standardisedMOL['inchikey']; $molecule->save(); } } diff --git a/app/Http/Controllers/API/Schemas/Bioschema/BiochemaController.php b/app/Http/Controllers/API/Schemas/Bioschema/BiochemaController.php index 05d1fa34..405c2013 100644 --- a/app/Http/Controllers/API/Schemas/Bioschema/BiochemaController.php +++ b/app/Http/Controllers/API/Schemas/Bioschema/BiochemaController.php @@ -230,7 +230,7 @@ public function getMolecules($sample) { $molecules = []; foreach ($sample->molecules as &$molecule) { - $inchiKey = $molecule->INCHI_KEY; + $inchiKey = $molecule->inchi_key; $pubchemLink = 'https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/inchikey/'.$inchiKey.'/property/IUPACName/JSON'; $json = json_decode(Http::get($pubchemLink)->body(), true); // $cid = $json['PropertyTable']['Properties'][0]['CID']; @@ -240,14 +240,14 @@ public function getMolecules($sample) // $moleculeSchema['@id'] = $inchiKey; // $moleculeSchema['dct:conformsTo'] = $this->conformsTo(['https://bioschemas.org/profiles/MolecularEntity/0.5-RELEASE']); // $moleculeSchema['identifier'] = $inchiKey; - // $moleculeSchema->name($molecule->CAS_NUMBER); + // $moleculeSchema->name($molecule->cas); // $moleculeSchema->url('https://pubchem.ncbi.nlm.nih.gov/compound/'.$cid); - // $moleculeSchema->inChI('InChI='.$molecule->STANDARD_INCHI); + // $moleculeSchema->inChI('InChI='.$molecule->standard_inchi); // $moleculeSchema->inChIKey($inchiKey); // $moleculeSchema->iupacName($iupacName); - // $moleculeSchema->molecularFormula($molecule->FORMULA); - // $moleculeSchema->molecularWeight($molecule->MOLECULAR_WEIGHT); - // $moleculeSchema->smiles([$molecule->SMILES, $molecule->SMILES_CHIRAL, $molecule->CANONICAL_SMILES]); + // $moleculeSchema->molecularFormula($molecule->molecular_formula); + // $moleculeSchema->molecularWeight($molecule->molecular_weight); + // $moleculeSchema->smiles([$molecule->SMILES, $molecule->absolute_smiles, $molecule->canonical_smiles]); // $moleculeSchema->hasRepresentation($molecule->MOL); // $moleculeSchema->description('Percentage composition: '.$molecule->pivot->percentage_composition.'%'); // array_push($molecules, $moleculeSchema); diff --git a/app/Http/Controllers/API/SearchController.php b/app/Http/Controllers/API/SearchController.php new file mode 100644 index 00000000..f4dd4ff8 --- /dev/null +++ b/app/Http/Controllers/API/SearchController.php @@ -0,0 +1,336 @@ +query('limit'); + $sort = $request->query('sort'); + $limit = $limit ? $limit : 24; + $page = $request->query('page'); + $tagType = $request->get('tagType') ? $request->get('tagType') : null; + + $offset = + (($page != null && $page != 'null' && $page != 0 ? $page : 1) - + 1) * + $limit; + + $query = $request->get('query'); + + $type = $request->query('type') + ? $request->query('type') + : $request->get('type'); + + if ($type) { + $queryType = $type; + } else { + //inchi + $re = + '/^((InChI=)?[^J][0-9BCOHNSOPrIFla+\-\(\)\\\\\/,pqbtmsih]{6,})$/i'; + preg_match_all($re, $query, $imatches, PREG_SET_ORDER, 0); + + if (count($imatches) > 0 && substr($query, 0, 6) == 'InChI=') { + $queryType = 'inchi'; + } + + //inchikey + $re = '/^([0-9A-Z\-]+)$/i'; + preg_match_all($re, $query, $ikmatches, PREG_SET_ORDER, 0); + if ( + count($ikmatches) > 0 && + substr($query, 14, 1) == '-' && + strlen($query) == 27 + ) { + $queryType = 'inchikey'; + } + + // smiles + $re = '/^([^J][0-9BCOHNSOPrIFla@+\-\[\]\(\)\\\\\/%=#$]{6,})$/i'; + preg_match_all($re, $query, $matches, PREG_SET_ORDER, 0); + + if (count($matches) > 0 && substr($query, 14, 1) != '-') { + $queryType = 'smiles'; + } + } + + $filterMap = [ + 'mf' => 'molecular_formula', + + 'mw' => 'molecular_weight', + 'hac' => 'heavy_atom_count', + 'tac' => 'total_atom_count', + + 'arc' => 'aromatic_ring_count', + 'rbc' => 'rotatable_bond_count', + 'mrc' => 'minimal_number_of_rings', + 'fc' => 'formal_charge', + 'cs' => 'contains_sugar', + 'crs' => 'contains_ring_sugars', + 'cls' => 'contains_linear_sugars', + + 'npl' => 'np_likeness_score', + 'alogp' => 'alogp', + 'topopsa' => 'topo_psa', + 'fsp3' => 'fsp3', + 'hba' => 'h_bond_acceptor_count', + 'hbd' => 'h_bond_donor_count', + 'ro5v' => 'rule_of_5_violations', + 'lhba' => 'lipinski_h_bond_acceptor_count', + 'lhbd' => 'lipinski_h_bond_donor_count', + 'lro5v' => 'lipinski_rule_of_5_violations', + 'ds' => 'found_in_databases', + + 'class' => 'chemical_class', + 'subclass' => 'chemical_sub_class', + 'superclass' => 'chemical_super_class', + 'parent' => 'direct_parent_classification', + + ]; + + $queryType = strtolower($queryType); + + $statement = null; + + if ($queryType == 'smiles' || $queryType == 'substructure') { + $statement = + "select id, COUNT(*) OVER () from mols where m@>'". + $query. + "' limit ". + $limit. + ' offset '. + $offset; + } elseif ($queryType == 'inchi') { + $statement = + "select id, COUNT(*) OVER () from molecules WHERE identifier NOTNULL AND standard_inchi LIKE '%". + $query. + "%' limit ". + $limit. + ' offset '. + $offset; + } elseif ($queryType == 'inchikey') { + $statement = + "select id, COUNT(*) OVER () from molecules WHERE identifier NOTNULL AND standard_inchi_key LIKE '%". + $query. + "%' limit ". + $limit. + ' offset '. + $offset; + } elseif ($queryType == 'exact') { + $statement = + "select id, COUNT(*) OVER () from mols where m@='". + $query. + "' limit ". + $limit. + ' offset '. + $offset; + } elseif ($queryType == 'similarity') { + $statement = + "select id, COUNT(*) OVER () from fps where mfp2%morganbv_fp('". + $query. + "') limit ". + $limit. + ' offset '. + $offset; + } elseif ($queryType == 'tags') { + $results = Molecule::withAnyTags([$query], $tagType)->paginate($limit)->items(); + $count = Molecule::withAnyTags([$query], $tagType)->count(); + } elseif ($queryType == 'filters') { + $orConditions = explode('OR', $query); + $isORInitial = true; + $statement = + 'select molecule_id as id, COUNT(*) OVER () from properties where '; + foreach ($orConditions as $orCondition) { + if ($isORInitial === false) { + $statement = $statement.' OR '; + } + $isORInitial = false; + $statement = $statement.'('; + $andConditions = explode(' ', trim($orCondition, ' ')); + $isANDInitial = true; + foreach ($andConditions as $andCondition) { + if ($isANDInitial === false) { + $statement = $statement.' AND '; + } + $isANDInitial = false; + $_filter = explode(':', $andCondition); + if (str_contains($_filter[1], '..')) { + $range = array_values(explode('..', $_filter[1])); + $statement = + $statement. + '('. + $filterMap[$_filter[0]]. + ' between '. + $range[0]. + ' and '. + $range[1]. + ')'; + } elseif ( + $_filter[1] === 'true' || + $_filter[1] === 'false' + ) { + $statement = + $statement. + '('. + $filterMap[$_filter[0]]. + ' = '. + $_filter[1]. + ')'; + } elseif (str_contains($_filter[1], '|')) { + $dbFilters = explode('|', $_filter[1]); + $dbs = explode('+', $dbFilters[0]); + $statement = + $statement. + '('. + $filterMap[$_filter[0]]. + " @> '[\"". + implode('","', $dbs). + "\"]')"; + } else { + if (str_contains($_filter[1], '+')) { + $_filter[1] = str_replace('+', ' ', $_filter[1]); + } + $statement = + $statement. + '('.$filterMap[$_filter[0]].'::TEXT ILIKE \'%'.$_filter[1].'%\')'; + } + } + $statement = $statement.')'; + } + $statement = $statement.' LIMIT '.$limit; + // dd($statement ); + } else { + if ($query) { + $query = str_replace("'", "''", $query); + $statement = + "select id, COUNT(*) OVER () from molecules WHERE identifier NOTNULL AND (\"name\"::TEXT ILIKE '%". + $query. + "%') OR (\"synonyms\"::TEXT ILIKE '%". + $query. + "%') OR (\"identifier\"::TEXT ILIKE '%". + $query. + "%') limit ". + $limit. + ' offset '. + $offset; + } else { + $statement = + 'select id, COUNT(*) OVER () from mols limit '. + $limit. + ' offset '. + $offset; + } + } + if ($statement) { + $expression = DB::raw($statement); + $qString = $expression->getValue( + DB::connection()->getQueryGrammar() + ); + + $hits = DB::select($qString); + + $count = count($hits) > 0 ? $hits[0]->count : 0; + + $ids = implode( + ',', + collect($hits) + ->pluck('id') + ->toArray() + ); + + if ($ids != '') { + $statement = + 'SELECT * FROM molecules WHERE identifier NOTNULL AND ID IN ('. + implode( + ',', + collect($hits) + ->pluck('id') + ->toArray() + ). + ')'; + if ($sort == 'recent') { + $statement = $statement.' ORDER BY created_at DESC'; + } + $expression = DB::raw($statement); + $string = $expression->getValue( + DB::connection()->getQueryGrammar() + ); + $results = DB::select($string); + } else { + $results = []; + $count = 0; + } + } + $pagination = new LengthAwarePaginator( + $results, + $count, + $limit, + $page + ); + + //dd($pagination); + return $pagination; + } catch (QueryException $exception) { + return response()->json( + [ + 'message' => $exception->getMessage(), + ], + 500 + ); + } + } +} diff --git a/app/Http/Controllers/ApplicationController.php b/app/Http/Controllers/ApplicationController.php index 18c994a6..53f76077 100644 --- a/app/Http/Controllers/ApplicationController.php +++ b/app/Http/Controllers/ApplicationController.php @@ -11,6 +11,22 @@ class ApplicationController extends Controller { + /** + * Resolve the incoming compounds search request and renders compounds + * inertia view + * + * @return Inertia\Inertia + */ + public function compounds(Request $request) + { + $query = $request->get('query'); + $limit = $request->get('limit') ? $limit : 24; + $page = $request->query('page'); + $tagType = $request->query('tagType') ? $request->query('tagType') : null; + + return Inertia::render('Public/Compounds', compact(['query', 'limit', 'page', 'tagType'])); + } + /** * Resolve the incoming request into right models and render the * inertia view diff --git a/app/Http/Controllers/DraftController.php b/app/Http/Controllers/DraftController.php index eacde9f6..0b744b04 100644 --- a/app/Http/Controllers/DraftController.php +++ b/app/Http/Controllers/DraftController.php @@ -87,7 +87,6 @@ public function files(Request $request, Draft $draft) 'children' => FileSystemObject::with('children') ->where([ ['level', 0], - ['status', '<>', 'missing'], ['draft_id', $draft->id], ]) ->orderBy('created_at', 'DESC') diff --git a/app/Http/Controllers/StudyController.php b/app/Http/Controllers/StudyController.php index 21c06f4c..5613d0cb 100644 --- a/app/Http/Controllers/StudyController.php +++ b/app/Http/Controllers/StudyController.php @@ -29,13 +29,23 @@ class StudyController extends Controller { public function publicStudiesView(Request $request) { - // $datasets = Cache::rememberForever('datasets', function () { - $studies = StudyResource::collection(Study::with('datasets')->where([['is_public', true], ['is_archived', false]])->filter($request->only('search', 'sort', 'mode'))->paginate(12)->withQueryString()); - // }); + $moleculeId = $request->get('compound'); + if ($moleculeId) { + $molecule = Molecule::where('identifier', $moleculeId)->first(); + if ($molecule) { + $studies = $molecule->studies()->where([['is_public', true], ['is_archived', false]])->filter($request->only('search', 'sort', 'mode'))->paginate(12)->withQueryString(); + } else { + $studies = []; + } + } else { + $studies = Study::with('datasets')->where([['is_public', true], ['is_archived', false]])->filter($request->only('search', 'sort', 'mode'))->paginate(12)->withQueryString(); + } + + $studiesResource = StudyResource::collection($studies); return Inertia::render('Public/Studies', [ 'filters' => $request->all('search', 'sort', 'mode'), - 'studies' => $studies, + 'studies' => $studiesResource, ]); } @@ -133,15 +143,15 @@ public function moleculeStore(Request $request, Study $study) $study->sample()->save($sample); } $inchi = $request->get('InChI'); - $molecule = $sample->molecules->where('STANDARD_INCHI', $inchi)->first(); + $molecule = $sample->molecules->where('standard_inchi', $inchi)->first(); if (is_null($molecule)) { $molecule = Molecule::firstOrCreate([ - 'STANDARD_INCHI' => $inchi, + 'standard_inchi' => $inchi, ], [ - 'FORMULA' => $request->get('formula') ? $request->get('formula') : '', - 'INCHI_KEY' => $request->get('InChIKey') ? $request->get('InChIKey') : '', - 'MOL' => $request->get('mol') ? $request->get('mol') : '', - 'CANONICAL_SMILES' => $request->get('canonical_smiles') ? $request->get('canonical_smiles') : '', + 'molecular_formula' => $request->get('formula') ? $request->get('formula') : '', + 'inchi_key' => $request->get('InChIKey') ? $request->get('InChIKey') : '', + 'sdf' => $request->get('mol') ? $request->get('mol') : '', + 'canonical_smiles' => $request->get('canonical_smiles') ? $request->get('canonical_smiles') : '', ]); $sample->molecules()->syncWithPivotValues([$molecule->id], ['percentage_composition' => $request->get('percentage')], false); } diff --git a/app/Jobs/ArchiveStudy.php b/app/Jobs/ArchiveStudy.php index 5ba6748b..82b909f6 100644 --- a/app/Jobs/ArchiveStudy.php +++ b/app/Jobs/ArchiveStudy.php @@ -158,15 +158,15 @@ public function handle(): void // $standardizedMolecule = $this->standardizeMolecule($mol['molfile']); // // associate // $inchi = $standardizedMolecule['InChI']; - // $molecule = $sample->molecules->where('STANDARD_INCHI', $inchi)->first(); + // $molecule = $sample->molecules->where('standard_inchi', $inchi)->first(); // if (is_null($molecule)) { // $molecule = Molecule::firstOrCreate([ - // 'STANDARD_INCHI' => $inchi, + // 'standard_inchi' => $inchi, // ], [ - // 'FORMULA' => $standardizedMolecule['formula'] ? $standardizedMolecule['formula'] : '', - // 'INCHI_KEY' => $standardizedMolecule['InChIKey'] ? $standardizedMolecule['InChIKey'] : '', - // 'MOL' => $standardizedMolecule['mol'] ? $standardizedMolecule['mol'] : '', - // 'CANONICAL_SMILES' => $standardizedMolecule['canonical_smiles'] ? $standardizedMolecule['canonical_smiles'] : '', + // 'molecular_formula' => $standardizedMolecule['formula'] ? $standardizedMolecule['formula'] : '', + // 'inchi_key' => $standardizedMolecule['InChIKey'] ? $standardizedMolecule['InChIKey'] : '', + // 'sdf' => $standardizedMolecule['mol'] ? $standardizedMolecule['mol'] : '', + // 'canonical_smiles' => $standardizedMolecule['canonical_smiles'] ? $standardizedMolecule['canonical_smiles'] : '', // ]); // $sample->molecules()->syncWithPivotValues([$molecule->id], ['percentage_composition' => 0], false); // } diff --git a/app/Models/Molecule.php b/app/Models/Molecule.php index 99385695..1aeec025 100644 --- a/app/Models/Molecule.php +++ b/app/Models/Molecule.php @@ -11,44 +11,19 @@ class Molecule extends Model use HasFactory; protected $fillable = [ - 'CAS_NUMBER', - 'FORMULA', - 'MOLECULAR_WEIGHT', - 'SMILES', - 'SMILES_CHIRAL', - 'CANONICAL_SMILES', - 'INCHI', - 'STANDARD_INCHI', - 'INCHI_KEY', - 'STANDARD_INCHI_KEY', - 'fp0', - 'fp1', - 'fp2', - 'fp3', - 'fp4', - 'fp5', - 'fp6', - 'fp7', - 'fp8', - 'fp9', - 'fp10', - 'fp11', - 'fp12', - 'fp13', - 'fp14', - 'fp15', - 'DBE', - 'SSSR', - 'SAR', + 'cas', + 'molecular_formula', + 'molecular_weight', + 'smiles', + 'absolute_smiles', + 'canonical_smiles', + 'inchi', + 'standard_inchi', + 'inchi_key', + 'standard_inchi_key', 'COMMENT', - 'fORMULA', - 'MULTIPLICITY_0', - 'MULTIPLICITY_1', - 'MULTIPLICITY_2', - 'MULTIPLICITY_3', - 'VIEWS', - 'DOI', - 'MOL', + 'doi', + 'sdf', ]; /** @@ -67,4 +42,15 @@ public function samples() ->withPivot('percentage_composition') ->withTimestamps(); } + + public function studies() + { + $samples = $this->samples->load('study'); + $studies = []; + foreach ($samples as $sample) { + array_push($studies, $sample->study->id); + } + + return Study::whereIn('id', $studies); + } } diff --git a/app/Models/Sample.php b/app/Models/Sample.php index b5a80ce2..38c1796c 100644 --- a/app/Models/Sample.php +++ b/app/Models/Sample.php @@ -32,6 +32,6 @@ public function molecules() */ public function study() { - return $this->belongsTo(Sample::class); + return $this->belongsTo(Study::class, 'study_id'); } } diff --git a/database/factories/MoleculeFactory.php b/database/factories/MoleculeFactory.php index 368dc357..152aca2d 100644 --- a/database/factories/MoleculeFactory.php +++ b/database/factories/MoleculeFactory.php @@ -30,9 +30,9 @@ public function definition() $output = []; $labels = [ - 'InChI' => 'STANDARD_INCHI', - 'InChIKey' => 'INCHI_KEY', - 'Molecular Formula' => 'FORMULA', + 'InChI' => 'standard_inchi', + 'InChIKey' => 'inchi_key', + 'Molecular Formula' => 'molecular_formula', ]; foreach ($data as $key => $value) { @@ -48,16 +48,16 @@ public function definition() return [ - 'CAS_NUMBER' => null, - 'FORMULA' => $output['FORMULA'], - 'MOLECULAR_WEIGHT' => null, - 'SMILES' => null, - 'SMILES_CHIRAL' => null, - 'CANONICAL_SMILES' => null, - 'INCHI' => null, - 'STANDARD_INCHI' => $output['STANDARD_INCHI'], - 'INCHI_KEY' => $output['INCHI_KEY'], - 'STANDARD_INCHI_KEY' => null, + 'cas' => null, + 'molecular_formula' => $output['molecular_formula'], + 'molecular_weight' => null, + 'smiles' => null, + 'absolute_smiles' => null, + 'canonical_smiles' => null, + 'inchi' => null, + 'standard_inchi' => $output['standard_inchi'], + 'inchi_key' => $output['inchi_key'], + 'standard_inchi_key' => null, 'fp0' => null, 'fp1' => null, 'fp2' => null, @@ -78,7 +78,7 @@ public function definition() 'SSSR' => null, 'SAR' => null, 'COMMENT' => null, - 'MOL' => null, //todo: add mol file + 'sdf' => null, 'MULTIPLICITY_0' => null, 'MULTIPLICITY_1' => null, 'MULTIPLICITY_2' => null, diff --git a/database/migrations/2023_11_15_214811_update_molecules_table.php b/database/migrations/2023_11_15_214811_update_molecules_table.php new file mode 100644 index 00000000..fca91acb --- /dev/null +++ b/database/migrations/2023_11_15_214811_update_molecules_table.php @@ -0,0 +1,150 @@ +renameColumn('INCHI', 'inchi'); + // $table->renameColumn('STANDARD_INCHI', 'standard_inchi'); + // $table->renameColumn('INCHI_KEY', 'inchi_key'); + // $table->renameColumn('STANDARD_INCHI_KEY', 'standard_inchi_key'); + + // $table->renameColumn('canonical_smiles', 'canonical_smiles'); + // $table->renameColumn('smiles', 'smiles'); + // $table->renameColumn('SMILES_CHIRAL', 'absolute_smiles'); + + // $table->renameColumn('CAS_NUMBER', 'cas'); + // $table->renameColumn('MOLECULAR_WEIGHT', 'molecular_weight'); + // $table->renameColumn('FORMULA', 'molecular_formula'); + // $table->renameColumn('MOL', 'sdf'); + + $table->longText('name')->nullable(); + $table->integer('name_trust_level')->default(0)->nullable(); + $table->integer('annotation_level')->default(0)->nullable(); + + $table->longText('synonyms')->nullable(); + $table->longText('iupac_name')->nullable(); + + $table->json('2d')->nullable(); + $table->json('3d')->nullable(); + + $table->longText('structural_comments')->nullable(); + + $table->enum('status', ['APPROVED', 'REVOKED'])->default('APPROVED'); + + $table->boolean('active')->default(1); + $table->boolean('has_stereo')->default(0); + + $table->boolean('has_variants')->default(0); + $table->integer('variants_count')->default(0); + + $table->dropColumn('VIEWS'); + $table->dropColumn('COMMENT'); + $table->dropColumn('DBE'); + $table->dropColumn('SSSR'); + $table->dropColumn('SAR'); + + $table->dropColumn('MULTIPLICITY_0'); + $table->dropColumn('MULTIPLICITY_1'); + $table->dropColumn('MULTIPLICITY_2'); + $table->dropColumn('MULTIPLICITY_3'); + + $table->dropColumn('fp0'); + $table->dropColumn('fp1'); + $table->dropColumn('fp2'); + $table->dropColumn('fp3'); + $table->dropColumn('fp4'); + $table->dropColumn('fp5'); + $table->dropColumn('fp6'); + $table->dropColumn('fp7'); + $table->dropColumn('fp8'); + $table->dropColumn('fp9'); + $table->dropColumn('fp10'); + $table->dropColumn('fp11'); + $table->dropColumn('fp12'); + $table->dropColumn('fp13'); + $table->dropColumn('fp14'); + $table->dropColumn('fp15'); + + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('molecules', function (Blueprint $table) { + $table->bigInteger('fp0')->nullable(); + $table->bigInteger('fp1')->nullable(); + $table->bigInteger('fp2')->nullable(); + $table->bigInteger('fp3')->nullable(); + $table->bigInteger('fp4')->nullable(); + $table->bigInteger('fp5')->nullable(); + $table->bigInteger('fp6')->nullable(); + $table->bigInteger('fp7')->nullable(); + $table->bigInteger('fp8')->nullable(); + $table->bigInteger('fp9')->nullable(); + $table->bigInteger('fp10')->nullable(); + $table->bigInteger('fp11')->nullable(); + $table->bigInteger('fp12')->nullable(); + $table->bigInteger('fp13')->nullable(); + $table->bigInteger('fp14')->nullable(); + $table->bigInteger('fp15')->nullable(); + $table->float('DBE')->nullable(); + $table->integer('SSSR')->nullable(); + $table->integer('SAR')->nullable(); + $table->json('COMMENT')->nullable(); + $table->longText('MOL')->nullable(); + $table->integer('MULTIPLICITY_0')->nullable(); + $table->integer('MULTIPLICITY_1')->nullable(); + $table->integer('MULTIPLICITY_2')->nullable(); + $table->integer('MULTIPLICITY_3')->nullable(); + $table->integer('VIEWS')->nullable(); + + // $table->renameColumn('inchi', 'INCHI'); + // $table->renameColumn('standard_inchi','STANDARD_INCHI'); + // $table->renameColumn('inchi_key', 'INCHI_KEY'); + // $table->renameColumn('standard_inchi_key', 'STANDARD_INCHI_KEY'); + + // $table->renameColumn('canonical_smiles','canonical_smiles'); + // $table->renameColumn('smiles', 'SMILES'); + // $table->renameColumn('absolute_smiles', 'SMILES_CHIRAL'); + + // $table->renameColumn('cas', 'CAS_NUMBER'); + // $table->renameColumn('molecular_weight', 'MOLECULAR_WEIGHT'); + // $table->renameColumn('molecular_formula', 'FORMULA'); + + // $table->renameColumn('comment', 'COMMENT'); + // $table->renameColumn('sdf', 'MOL'); + + $table->dropColumn('name'); + $table->dropColumn('name_trust_level'); + $table->dropColumn('annotation_level'); + + $table->dropColumn('synonyms'); + $table->dropColumn('iupac_name'); + + $table->dropColumn('2d'); + $table->dropColumn('3d'); + + $table->dropColumn('structural_comments'); + + $table->dropColumn('status'); + + $table->dropColumn('active'); + $table->dropColumn('has_stereo'); + + $table->dropColumn('has_variants'); + $table->dropColumn('variants_count'); + }); + } +}; diff --git a/database/scripts/initialise_molecules_index.sql b/database/scripts/initialise_molecules_index.sql new file mode 100644 index 00000000..3bcacf26 --- /dev/null +++ b/database/scripts/initialise_molecules_index.sql @@ -0,0 +1,19 @@ +create extension if not exists rdkit; +select * into mols from (select id,mol_from_smiles(canonical_smiles::cstring) m from molecules) tmp where m is not null; +create index molidx on mols using gist(m); +alter table mols add primary key (id); + +-- select * from molecules where identifier = 'CNP0228556' limit 24 offset 0; +-- SELECT * from properties where molecule_id = 139702; +-- select id, COUNT(*) OVER () from molecules where name = 'Curcumin' limit 24; +-- select count(*) from molecules where smiles@>'c1cccc2c1nncc2' ; +-- select id, COUNT(*) OVER () from molecules where LOWER(synonyms) LIKE '%' || LOWER('Thein') || '%' limit 24 offset 0; +-- select id, COUNT(*) OVER () from molecules where LOWER(name) LIKE LOWER('Caffeine') OR LOWER(synonyms) LIKE '%' || LOWER('Caffeine') || '%' ORDER BY CASE WHEN (name) LIKE LOWER('Caffeine') THEN 1 WHEN LOWER(synonyms) LIKE '%' || LOWER('Caffeine') || '%' THEN 2 END; + +select id, torsionbv_fp(m) as torsionbv,morganbv_fp(m) as mfp2, featmorganbv_fp(m) as ffp2 into fps from mols; +create index fps_ttbv_idx on fps using gist(torsionbv); +create index fps_mfp2_idx on fps using gist(mfp2); +create index fps_ffp2_idx on fps using gist(ffp2); +alter table fps add primary key (id); + +-- select identifier, standard_inchi, standard_inchi_key, properties.score from molecules RIGHT JOIN properties ON properties.molecule_id = molecules.id; \ No newline at end of file diff --git a/resources/js/App/MoleculeCard.vue b/resources/js/App/MoleculeCard.vue new file mode 100644 index 00000000..24c4443b --- /dev/null +++ b/resources/js/App/MoleculeCard.vue @@ -0,0 +1,157 @@ + + diff --git a/resources/js/App/StructureSearch.vue b/resources/js/App/StructureSearch.vue new file mode 100644 index 00000000..f1387e4d --- /dev/null +++ b/resources/js/App/StructureSearch.vue @@ -0,0 +1,225 @@ + + + diff --git a/resources/js/Layouts/AppLayout.vue b/resources/js/Layouts/AppLayout.vue index b33879ef..62172023 100644 --- a/resources/js/Layouts/AppLayout.vue +++ b/resources/js/Layouts/AppLayout.vue @@ -742,6 +742,7 @@ import { StarIcon, FolderIcon, Squares2X2Icon, + SwatchIcon, TrashIcon, } from "@heroicons/vue/24/outline"; import { MagnifyingGlassIcon, PlusIcon } from "@heroicons/vue/24/solid"; @@ -768,6 +769,13 @@ const navigation = [ icon: Squares2X2Icon, bg: "bg-white", }, + { + auth: false, + name: "Compounds", + href: "/compounds", + icon: SwatchIcon, + bg: "bg-white", + }, // { // auth: false, // name: "Spectra", @@ -858,6 +866,7 @@ export default { StudyCreate, Submission, Notification, + SwatchIcon, }, props: { title: String, diff --git a/resources/js/Mixins/Global.js b/resources/js/Mixins/Global.js index 09a896dc..51fa0b6e 100644 --- a/resources/js/Mixins/Global.js +++ b/resources/js/Mixins/Global.js @@ -1,9 +1,228 @@ import * as marked from "marked"; import { copyText } from "vue3-clipboard"; import pluralize from "pluralize"; +import OCL from "openchemlib/full"; export default { methods: { + tagQuery(query) { + if (query) { + if (this.isInchi(query)) { + return "InChi"; + } else if (this.isInChiKey(query)) { + return "InChiKey"; + } else { + try { + let molecule = OCL.Molecule.fromSmiles(query); + } catch (err) { + return "text"; + } + return "smiles"; + } + } + }, + removeDuplicates(a) { + return a.filter(function (value, index) { + return a.indexOf(value) == index; + }); + }, + isInchi(query) { + return ( + !!query + .trim() + .match( + /^((InChI=)?[^J][0-9BCOHNSOPrIFla+\-\(\)\\\/,pqbtmsih]{6,})$/gi + ) && query.startsWith("InChI=") + ); + }, + isInChiKey(query) { + return ( + (25 === query.length && + "-" === query[14] && + !!query.match(/^([0-9A-Z\-]+)$/)) || + (27 === query.length && + "-" === query[14] && + "-" === query[25] && + !!query.match(/^([0-9A-Z\-]+)$/)) + ); + }, + hasAnyRole: function (roles) { + return this.checkIfValueExists(roles, "roles"); + }, + hasAnyPermission: function (permissions) { + return this.checkIfValueExists(permissions, "permissions"); + }, + checkIfValueExists(queryArray, type) { + if (this.$page.props.user && this.$page.props.user[type]) { + let allValues = Array.from(this.$page.props.user[type]); + return queryArray.some((r) => allValues.indexOf(r) >= 0); + } + }, + formatDate(timestamp) { + const date = new Date(timestamp); + return new Intl.DateTimeFormat("default", { + dateStyle: "long", + }).format(date); + }, + formatDateTime(timestamp) { + const date = new Date(timestamp); + return new Intl.DateTimeFormat("en", { + dateStyle: "full", + timeStyle: "short", + }).format(date); + }, + md(data) { + return data ? marked.parse(data) : ""; + }, + getHash(input) { + var hash = 0, + len = input.length; + for (var i = 0; i < len; i++) { + hash = (hash << 5) - hash + input.charCodeAt(i); + hash |= 0; // to 32bit integer + } + return hash; + }, + findLocalItems(query) { + var i, + results = []; + if (typeof window !== "undefined") { + for (i in localStorage) { + if (localStorage.hasOwnProperty(i)) { + if ( + i.match(query) || + (!query && typeof i === "string") + ) { + let value = JSON.parse(localStorage.getItem(i)); + if (value) { + results.push({ key: i, val: value }); + } + } + } + } + } + return results; + }, + slugify(str) { + str = str.replace(/^\s+|\s+$/g, ""); + str = str.toLowerCase(); + var from = "àáäâèéëêìíïîòóöôùúüûñç·/_,:;"; + var to = "aaaaeeeeiiiioooouuuunc------"; + for (var i = 0, l = from.length; i < l; i++) { + str = str.replace( + new RegExp(from.charAt(i), "g"), + to.charAt(i) + ); + } + str = str + .replace(/[^a-z0-9 -]/g, "") + .replace(/\s+/g, "-") + .replace(/-+/g, "-"); + + return str; + }, + copyToClipboard(text) { + copyText(text, undefined, (error) => { + if (error) { + console.log(error); + } else { + console.log("copied"); + // console.log(event) + } + }); + }, + bytesToSize(bytes) { + var sizes = ["Bytes", "KB", "MB", "GB", "TB"]; + if (bytes == 0) return "0 Byte"; + var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024))); + return Math.round(bytes / Math.pow(1024, i), 2) + " " + sizes[i]; + }, + isString(val) { + return typeof val === "string" || val instanceof String; + }, + parseJSON(val) { + if (!val) { + return null; + } + if (this.isString(val)) { + return JSON.parse(val); + } + return val; + }, + downloadMolFile2D(e, smiles, identifier, CM_API) { + axios + .get( + CM_API + + "convert/mol2D?smiles=" + + encodeURIComponent(smiles) + + "&generator=rdkit" + ) + .then((response) => { + this.saveTextAsFile( + response.data, + "Molfile_V2_" + identifier + ".mol", + "chemical/x-mdl-molfile" + ); + }) + .catch((error) => { + console.error("Error downloading the file:", error); + }); + }, + downloadMolFile3D(e, smiles, identifier, CM_API) { + axios + .get( + CM_API + + "convert/mol3D?smiles=" + + encodeURIComponent(smiles) + + "&generator=rdkit" + ) + .then((response) => { + this.saveTextAsFile( + response.data, + "Molfile_V3_" + identifier + ".mol", + "chemical/x-mdl-molfile" + ); + }) + .catch((error) => { + console.error("Error downloading the file:", error); + }); + }, + getMolfileStringBySmiles(smiles) { + try { + const npMolecule = OCL.Molecule.fromSmiles(smiles); + return npMolecule.toMolfileV3(); + } catch (e) { + console.log(e.name + " in OpenChemLib: " + e.message); + } + }, + saveTextAsFile(textToWrite, fileNameToSaveAs, fileType) { + let textFileAsBlob = new Blob([textToWrite], { type: fileType }); + let downloadLink = document.createElement("a"); + downloadLink.download = fileNameToSaveAs; + downloadLink.innerHTML = "Download File"; + + if (window.webkitURL != null) { + downloadLink.href = + window.webkitURL.createObjectURL(textFileAsBlob); + } else { + downloadLink.href = window.URL.createObjectURL(textFileAsBlob); + downloadLink.style.display = "none"; + document.body.appendChild(downloadLink); + } + + downloadLink.click(); + }, + /*Extract Doi from URL*/ + extractDoi(query) { + if (query.indexOf("http") > -1) { + var url = new URL(query); + query = url.pathname.replace("/", ""); + } + return query.trim(); + }, + fixedDecimelPoint(input, decimelPoint) { + return Number.parseFloat(input).toFixed(decimelPoint); + }, hasAnyRole: function (roles) { return this.checkIfValueExists(roles, "roles"); }, diff --git a/resources/js/Pages/Public/Compounds.vue b/resources/js/Pages/Public/Compounds.vue new file mode 100644 index 00000000..f37405f6 --- /dev/null +++ b/resources/js/Pages/Public/Compounds.vue @@ -0,0 +1,878 @@ + + diff --git a/resources/js/Pages/Public/Project/Dataset.vue b/resources/js/Pages/Public/Project/Dataset.vue index 14ad36a0..72b7c5ac 100644 --- a/resources/js/Pages/Public/Project/Dataset.vue +++ b/resources/js/Pages/Public/Project/Dataset.vue @@ -230,7 +230,7 @@
  • {{ - molecule.STANDARD_INCHI + molecule.standard_inchi }}
    @@ -278,7 +278,7 @@ diff --git a/resources/js/Pages/Public/Project/Samples.vue b/resources/js/Pages/Public/Project/Samples.vue index d3cd6971..1ee5907b 100644 --- a/resources/js/Pages/Public/Project/Samples.vue +++ b/resources/js/Pages/Public/Project/Samples.vue @@ -86,7 +86,10 @@
    diff --git a/resources/js/Pages/Public/Projects.vue b/resources/js/Pages/Public/Projects.vue index 783363c9..ce4e082f 100644 --- a/resources/js/Pages/Public/Projects.vue +++ b/resources/js/Pages/Public/Projects.vue @@ -1,27 +1,82 @@