diff --git a/app/scripts/nmr-cli/Dockerfile b/app/scripts/nmr-cli/Dockerfile index c0f1fbf..d267c31 100644 --- a/app/scripts/nmr-cli/Dockerfile +++ b/app/scripts/nmr-cli/Dockerfile @@ -3,13 +3,15 @@ FROM mcr.microsoft.com/playwright:v1.51.1-jammy + SHELL ["/bin/bash", "-o", "pipefail", "-c"] + WORKDIR /app #ENV BASE_NMRIUM_URL=https://nmrium.nmrxiv.org/ ENV BASE_NMRIUM_URL=https://nmriumdev.nmrxiv.org/ - +ENV NMR_PREDICTION_URL=https://nmrshiftdb.nmr.uni-koeln.de/NmrshiftdbServlet/nmrshiftdbaction/quickcheck COPY package.json ./ COPY package-lock.json ./ @@ -22,6 +24,7 @@ RUN npm run build #install the nmr-cli as a global package # for example, nmr-cli parse-spectra -u https://cheminfo.github.io/bruker-data-test/data/zipped/aspirin-1h.zip +# nmr-cli predict -n "1H" --id 1 --type "nmr;1H;1d" --shifts "1" --solvent "Dimethylsulphoxide-D6 (DMSO-D6, C2D6SO)" -m $"\n Ketcher 6122516162D 1 1.00000 0.00000 0\n\n 16 17 0 0 0 0 0 0 0 0999 V2000\n 1.1954 -4.6484 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 2.9258 -4.6479 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 2.0622 -4.1483 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 2.9258 -5.6488 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 1.1954 -5.6533 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 2.0644 -6.1483 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 3.7902 -4.1495 0.0000 N 0 0 0 0 0 0 0 0 0 0 0 0\n 4.6574 -4.6498 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 3.7964 -6.1512 0.0000 N 0 0 0 0 0 0 0 0 0 0 0 0\n 4.6596 -5.6458 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 5.5228 -4.1488 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 5.5277 -6.1421 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0\n 6.3895 -4.6477 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0\n 5.5216 -3.1488 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0\n 7.2548 -4.1466 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 8.1215 -4.6455 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 3 1 2 0 0 0 0\n 1 5 1 0 0 0 0\n 5 6 2 0 0 0 0\n 6 4 1 0 0 0 0\n 4 2 1 0 0 0 0\n 2 3 1 0 0 0 0\n 4 9 1 0 0 0 0\n 9 10 2 0 0 0 0\n 10 8 1 0 0 0 0\n 8 7 2 0 0 0 0\n 7 2 1 0 0 0 0\n 8 11 1 0 0 0 0\n 10 12 1 0 0 0 0\n 11 13 1 0 0 0 0\n 11 14 2 0 0 0 0\n 13 15 1 0 0 0 0\n 15 16 1 0 0 0 0\nM END" RUN npm install . -g diff --git a/app/scripts/nmr-cli/package-lock.json b/app/scripts/nmr-cli/package-lock.json index d5c48fd..8cfb058 100644 --- a/app/scripts/nmr-cli/package-lock.json +++ b/app/scripts/nmr-cli/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "axios": "^1.9.0", "filelist-utils": "^1.11.3", "nmr-load-save": "^3.3.0", "nmr-processing": "^17.1.3", @@ -207,11 +208,26 @@ "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", "dev": true }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "node_modules/atom-sorter": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/atom-sorter/-/atom-sorter-2.2.0.tgz", "integrity": "sha512-ktg7pvTF22Ox/HPJZjUrw3L0dH1nOZg+CjGR0r1iyZO2LDbQX5GgIxOWq4YthifBrcVxfFMkdKVhp5YJ43g0Vw==" }, + "node_modules/axios": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", + "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/baselines": { "version": "1.1.9", "resolved": "https://registry.npmjs.org/baselines/-/baselines-1.1.9.tgz", @@ -242,6 +258,18 @@ "ml-spectra-processing": "^14.9.2" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/cheminfo-types": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/cheminfo-types/-/cheminfo-types-1.8.1.tgz", @@ -276,6 +304,17 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/convert-to-jcamp": { "version": "5.4.11", "resolved": "https://registry.npmjs.org/convert-to-jcamp/-/convert-to-jcamp-5.4.11.tgz", @@ -318,6 +357,14 @@ "d3-color": "1 - 2" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -327,6 +374,19 @@ "node": ">=0.3.1" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/dynamic-typing": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dynamic-typing/-/dynamic-typing-1.0.1.tgz", @@ -346,6 +406,47 @@ "isutf8": "^4.0.0" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -378,6 +479,40 @@ "pako": "^2.1.0" } }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", + "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -391,6 +526,14 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -399,6 +542,41 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-value": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/get-value/-/get-value-3.0.1.tgz", @@ -410,11 +588,58 @@ "node": ">=6.0" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gyromagnetic-ratio": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/gyromagnetic-ratio/-/gyromagnetic-ratio-1.2.1.tgz", "integrity": "sha512-cOkHEsIwHNKe8v/wED9NWa8wvzLx0rpBarUrEpvzdgECMpVQzrLJkaFYsdSxnhaUtWX4uNFxX01PJeFayDCpVA==" }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/heap": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/heap/-/heap-0.2.7.tgz", @@ -543,11 +768,38 @@ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/median-quickselect": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/median-quickselect/-/median-quickselect-1.0.1.tgz", "integrity": "sha512-/QL9ptNuLsdA68qO+2o10TKCyu621zwwTFdLvtu8rzRNKsn8zvuGoq/vDxECPyELFG8wu+BpyoMR9BnsJqfVZQ==" }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/ml-airpls": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ml-airpls/-/ml-airpls-2.0.0.tgz", @@ -1046,6 +1298,11 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", diff --git a/app/scripts/nmr-cli/package.json b/app/scripts/nmr-cli/package.json index ff48805..20f1010 100644 --- a/app/scripts/nmr-cli/package.json +++ b/app/scripts/nmr-cli/package.json @@ -15,6 +15,7 @@ "nmr-cli": "./build/index.js" }, "dependencies": { + "axios": "^1.9.0", "filelist-utils": "^1.11.3", "nmr-load-save": "^3.3.0", "nmr-processing": "^17.1.3", @@ -27,4 +28,4 @@ "ts-node": "^10.9.2", "typescript": "^5.8.2" } -} \ No newline at end of file +} diff --git a/app/scripts/nmr-cli/src/index.ts b/app/scripts/nmr-cli/src/index.ts index a66dec1..cc63fce 100755 --- a/app/scripts/nmr-cli/src/index.ts +++ b/app/scripts/nmr-cli/src/index.ts @@ -1,8 +1,8 @@ #!/usr/bin/env node -import yargs, { type Argv, type CommandModule, type Options, } from "yargs"; -import { loadSpectrumFromURL, loadSpectrumFromFilePath } from "./prase-spectra"; -import { generateSpectrumFromPublicationString } from "./publication-string"; - +import yargs, { type Argv, type CommandModule, type Options } from 'yargs' +import { loadSpectrumFromURL, loadSpectrumFromFilePath } from './prase-spectra' +import { generateSpectrumFromPublicationString } from './publication-string' +import { parsePredictionCommand } from './prediction/parsePredictionCommand' const usageMessage = ` Usage: nmr-cli [options] @@ -10,6 +10,7 @@ Usage: nmr-cli [options] Commands: parse-spectra Parse a spectra file to NMRium file parse-publication-string resurrect spectrum from the publication string + predict Predict spectrum from Mol Options for 'parse-spectra' command: -u, --url File URL @@ -19,21 +20,42 @@ Options for 'parse-spectra' command: Arguments for 'parse-publication-string' command: publicationString Publication string +Options for 'parse-spectra' command: + -u, --url File URL + -p, --path Directory path + -s, --capture-snapshot Capture snapshot + + +Options for 'predict' command: + -n, --nucleus Predicted nucleus "1H" or "13C" (required) + -i, --id Input ID (default: 1) + -t, --type NMR type (default: "nmr;1H;1d") + -s, --shifts Chemical shifts (default: "1") + --solvent NMR solvent (default: "Dimethylsulphoxide-D6 (DMSO-D6, C2D6SO)") + -m, --molText MOL text (required) + --from From in (ppm) + --to To in (ppm) + --nbPoints Number of points (default: 128) + --lineWidth Line width (default: 1) + --frequency NMR frequency (MHz) (default: 400) + --tolerance Tolerance (default: 0.001) + + + Examples: nmr-cli parse-spectra -u file-url -s // Process spectra files from a URL and capture an image for the spectra nmr-cli parse-spectra -p directory-path -s // process a spectra files from a directory and capture an image for the spectra nmr-cli parse-spectra -u file-url // Process spectra files from a URL nmr-cli parse-spectra -p directory-path // Process spectra files from a directory nmr-cli parse-publication-string "your publication string" -`; +` interface FileOptionsArgs { - u?: string; - p?: string; - s?: boolean; + u?: string + p?: string + s?: boolean } - // Define options for parsing a spectra file const fileOptions: { [key in keyof FileOptionsArgs]: Options } = { u: { @@ -53,56 +75,53 @@ const fileOptions: { [key in keyof FileOptionsArgs]: Options } = { describe: 'Capture snapshot', type: 'boolean', }, -} as const; - +} as const const parseFileCommand: CommandModule<{}, FileOptionsArgs> = { command: ['parse-spectra', 'ps'], describe: 'Parse a spectra file to NMRium file', - builder: (yargs) => { - return yargs.options(fileOptions).conflicts('u', 'p') as Argv; + builder: yargs => { + return yargs + .options(fileOptions) + .conflicts('u', 'p') as Argv }, - handler: (argv) => { + handler: argv => { // Handle parsing the spectra file logic based on argv options if (argv?.u) { - loadSpectrumFromURL(argv.u, argv.s).then((result) => { + loadSpectrumFromURL(argv.u, argv.s).then(result => { console.log(JSON.stringify(result)) }) - } if (argv?.p) { - loadSpectrumFromFilePath(argv.p, argv.s).then((result) => { + loadSpectrumFromFilePath(argv.p, argv.s).then(result => { console.log(JSON.stringify(result)) }) } }, -}; +} // Define the parse publication string command const parsePublicationCommand: CommandModule = { command: ['parse-publication-string', 'pps'], describe: 'Parse a publication string', - handler: (argv) => { - const publicationString = argv._[1]; + handler: argv => { + const publicationString = argv._[1] // Handle parsing publication string - if (typeof publicationString == "string") { - const nmriumObject = generateSpectrumFromPublicationString(publicationString); - + if (typeof publicationString == 'string') { + const nmriumObject = + generateSpectrumFromPublicationString(publicationString) - console.log(JSON.stringify(nmriumObject)); + console.log(JSON.stringify(nmriumObject)) } }, -}; +} yargs .usage(usageMessage) .command(parseFileCommand) .command(parsePublicationCommand) + .command(parsePredictionCommand) .showHelpOnFail(true) .help() - .parse(); - - - - + .parse() diff --git a/app/scripts/nmr-cli/src/prediction/generatePredictedSpectrumData.ts b/app/scripts/nmr-cli/src/prediction/generatePredictedSpectrumData.ts new file mode 100644 index 0000000..ec8ca2c --- /dev/null +++ b/app/scripts/nmr-cli/src/prediction/generatePredictedSpectrumData.ts @@ -0,0 +1,131 @@ +export interface ShiftsItem { + atom: number + prediction: number + real: number + diff: number + status: 'accept' | 'missing' | string + hoseCode: string + spheres: number +} + +interface LorentzianOptions { + x: number + fwhm: number +} + +function lorentzian2(options: LorentzianOptions) { + const { x, fwhm } = options + return fwhm ** 2 / (4 * x ** 2 + fwhm ** 2) +} + +interface GenerateSpectrumOptions { + from?: number + to?: number + nbPoints?: number + lineWidth?: number + frequency?: number + tolerance?: number +} + +interface GroupItem { + prediction: number + count: number + atoms: number[] +} + +interface Data1D { + x: number[] + re: number[] +} + +const getLorentzianFactor = (area = 0.9999) => { + if (area >= 1) { + throw new Error('area should be (0 - 1)') + } + const halfResidual = (1 - area) * 0.5 + const quantileFunction = (p: number) => Math.tan(Math.PI * (p - 0.5)) + return ( + (quantileFunction(1 - halfResidual) - quantileFunction(halfResidual)) / 2 + ) +} + +function groupEquivalentShifts(shifts: ShiftsItem[], tolerance = 0.001) { + const groups: GroupItem[] = [] + + for (const shift of shifts) { + const match = groups.find( + g => Math.abs(g.prediction - shift.prediction) < tolerance + ) + if (match) { + match.count += 1 + match.atoms.push(shift.atom) + } else { + groups.push({ + prediction: shift.prediction, + count: 1, + atoms: [shift.atom], + }) + } + } + + return groups +} + +export function generatePredictedSpectrumData( + shifts: ShiftsItem[], + options: GenerateSpectrumOptions = {} +) { + let { from, to } = options + const { + nbPoints = 10240, + frequency = 400, + lineWidth = 1, + tolerance = 0.001, + } = options + + if (!shifts || shifts.length === 0) return [] + + const sortedShifts = shifts + .slice(0) + .sort((a, b) => a.prediction - b.prediction) + + // const acceptedShifts = sortedShifts.filter(shift => + // shift.status === 'accept' + // ); + + const acceptedShifts = sortedShifts + + if (acceptedShifts.length === 0) return [] + from = from ?? acceptedShifts[0].prediction - 1 + to = to ?? (acceptedShifts.at(-1) as ShiftsItem).prediction + 1 + + if (from >= to) { + throw new Error("Invalid range: 'from' is greater or equal then 'to'") + } + + const data: Data1D = { x: [], re: [] } + const stepSize = (to - from) / (nbPoints - 1) + const groupedShifts = groupEquivalentShifts(acceptedShifts, tolerance) + + console.log(groupedShifts) + + const limit = (lineWidth * getLorentzianFactor(0.99)) / frequency + console.log(limit) + for (let i = 0; i < nbPoints; i++) { + const x = from + i * stepSize + let intensity = 0 + for (const { prediction, count } of groupedShifts) { + if (Math.abs(x - prediction) <= limit) { + intensity += + lorentzian2({ x: x - prediction, fwhm: lineWidth / frequency }) * + count + } + } + if (intensity > 0) { + data.x.push(x) + data.re.push(intensity) + } + } + + return data +} diff --git a/app/scripts/nmr-cli/src/prediction/parsePredictionCommand.ts b/app/scripts/nmr-cli/src/prediction/parsePredictionCommand.ts new file mode 100644 index 0000000..7200a9e --- /dev/null +++ b/app/scripts/nmr-cli/src/prediction/parsePredictionCommand.ts @@ -0,0 +1,247 @@ +import { Argv, CommandModule, Options } from 'yargs' +import { + generatePredictedSpectrumData, + ShiftsItem, +} from './generatePredictedSpectrumData' +import { v4 } from '@lukeed/uuid' +import { CURRENT_EXPORT_VERSION } from 'nmr-load-save' +import https from 'https' +import axios from 'axios' + +interface PredictionOptions { + from?: number + to?: number + nbPoints?: number + lineWidth?: number + frequency?: number + tolerance?: number +} + +interface PredictionParameters { + molText: string + id: number + type: string + shifts: string + solvent: string + nucleus: string +} + +const predictionOptions: { [key in keyof PredictionOptions]: Options } = { + from: { + type: 'number', + description: 'From in (ppm)', + }, + to: { + type: 'number', + description: 'To in (ppm)', + }, + nbPoints: { + type: 'number', + description: 'Number of points', + default: 1024, + }, + lineWidth: { + type: 'number', + description: 'Line width', + default: 1, + }, + frequency: { + type: 'number', + description: 'NMR frequency (MHz)', + default: 400, + }, + tolerance: { + type: 'number', + description: 'Tolerance', + default: 0.001, + }, +} as const + +const nmrOptions: { [key in keyof PredictionParameters]: Options } = { + id: { + alias: 'i', + type: 'number', + description: 'Input ID', + default: 1, + }, + type: { + alias: 't', + type: 'string', + description: 'NMR type', + default: 'nmr;1H;1d', + choices: ['nmr;1H;1d', 'nmr;13C;1d'], + }, + shifts: { + alias: 's', + type: 'string', + description: 'Chemical shifts', + default: '1', + }, + solvent: { + type: 'string', + description: 'NMR solvent', + default: 'Dimethylsulphoxide-D6 (DMSO-D6, C2D6SO)', + choices: [ + 'Any', + 'Chloroform-D1 (CDCl3)', + 'Dimethylsulphoxide-D6 (DMSO-D6, C2D6SO)', + 'Methanol-D4 (CD3OD)', + 'Deuteriumoxide (D2O)', + 'Acetone-D6 ((CD3)2CO)', + 'TETRACHLORO-METHANE (CCl4)', + 'Pyridin-D5 (C5D5N)', + 'Benzene-D6 (C6D6)', + 'neat', + 'Tetrahydrofuran-D8 (THF-D8, C4D4O)', + ], + }, + molText: { + alias: 'm', + type: 'string', + description: 'MOL file content', + requiresArg: true, + }, + nucleus: { + alias: 'n', + type: 'string', + description: 'Predicted nucleus', + requiresArg: true, + choices: ['1H', '13C'], + }, +} as const + +interface PredictionResponseItem { + id: number + type: string + statistics: { + accept: number + warning: number + reject: number + missing: number + total: number + } + shifts: ShiftsItem[] +} +interface PredictionResponse { + result: PredictionResponseItem[] +} + +async function predictNMR(options: PredictionArgs): Promise { + const url = process.env['NMR_PREDICTION_URL'] + + if (!url) { + throw new Error('Environment variable NMR_PREDICTION_URL is not defined.') + } + + try { + new URL(url).toString() + } catch { + throw new Error(`Invalid URL in NMR_PREDICTION_URL: "${url}"`) + } + + try { + const { + id, + type, + shifts, + solvent, + from, + to, + nbPoints = 1024, + frequency = 400, + lineWidth = 1, + tolerance = 0.001, + molText, + nucleus, + } = options + + const payload: any = { + inputs: [ + { + id, + type, + shifts, + solvent, + }, + ], + moltxt: molText.replaceAll(/\\n/g, '\n'), + } + + const httpsAgent = new https.Agent({ + rejectUnauthorized: false, + }) + + // Axios POST request with httpsAgent + const response = await axios.post(url, payload, { + headers: { + 'Content-Type': 'application/json', + }, + httpsAgent, + }) + + const responseResult: PredictionResponse = response.data + const spectra = [] + + for (const result of responseResult.result) { + const name = v4() + const data = generatePredictedSpectrumData(result.shifts, { + from, + to, + nbPoints, + lineWidth, + frequency, + tolerance, + }) + + const info = { + isFid: false, + isComplex: false, + dimension: 1, + originFrequency: frequency, + baseFrequency: frequency, + pulseSequence: '', + solvent, + isFt: true, + name, + nucleus, + } + + spectra.push({ + id: v4(), + data, + info, + }) + } + + const nmrium = { data: { spectra }, version: CURRENT_EXPORT_VERSION } + console.log(JSON.stringify(nmrium, null, 2)) + } catch (error) { + console.error( + 'Error:', + error instanceof Error ? error.message : String(error) + ) + + if (axios.isAxiosError(error) && error.response) { + console.error('Response data:', error.response.data) + } else if (error instanceof Error && error.cause) { + console.error('Network Error:', error.cause) + } + } +} + +type PredictionArgs = PredictionParameters & PredictionOptions + +// Define the prediction string command +export const parsePredictionCommand: CommandModule<{}, PredictionArgs> = { + command: ['predict', 'p'], + describe: 'Predict NMR spectrum from mol text', + builder: (yargs: Argv<{}>): Argv => { + return yargs.options({ + ...nmrOptions, + ...predictionOptions, + }) as Argv + }, + handler: async argv => { + await predictNMR(argv) + }, +}