From 481441ce3278d077b944453705d1f3c9225a7946 Mon Sep 17 00:00:00 2001 From: hamed musallam Date: Fri, 31 Oct 2025 11:30:32 +0100 Subject: [PATCH 1/6] feat: auto processing 1d core functions --- app/scripts/nmr-cli/package-lock.json | 67 ++++++++++++++-- app/scripts/nmr-cli/package.json | 7 +- .../data/data1D/convertDataToFloat64Array.ts | 13 ++++ .../src/parse/data/data1D/initSumOptions.ts | 56 ++++++++++++++ .../src/parse/data/data1D/initiateDatum1D.ts | 77 +++++++++++++++++++ .../parse/data/data1D/initiateIntegrals.ts | 20 +++++ .../src/parse/data/data1D/initiatePeaks.ts | 13 ++++ .../src/parse/data/data1D/initiateRanges.ts | 20 +++++ .../nmr-cli/src/parse/data/initiateFilters.ts | 8 ++ .../nmr-cli/src/{ => parse}/prase-spectra.ts | 0 .../src/parse/type/MoleculeExtended.ts | 11 +++ .../nmr-cli/src/parse/utility/getAtom.ts | 3 + .../nmr-cli/src/parse/utility/isProton.ts | 3 + 13 files changed, 292 insertions(+), 6 deletions(-) create mode 100644 app/scripts/nmr-cli/src/parse/data/data1D/convertDataToFloat64Array.ts create mode 100644 app/scripts/nmr-cli/src/parse/data/data1D/initSumOptions.ts create mode 100644 app/scripts/nmr-cli/src/parse/data/data1D/initiateDatum1D.ts create mode 100644 app/scripts/nmr-cli/src/parse/data/data1D/initiateIntegrals.ts create mode 100644 app/scripts/nmr-cli/src/parse/data/data1D/initiatePeaks.ts create mode 100644 app/scripts/nmr-cli/src/parse/data/data1D/initiateRanges.ts create mode 100644 app/scripts/nmr-cli/src/parse/data/initiateFilters.ts rename app/scripts/nmr-cli/src/{ => parse}/prase-spectra.ts (100%) create mode 100644 app/scripts/nmr-cli/src/parse/type/MoleculeExtended.ts create mode 100644 app/scripts/nmr-cli/src/parse/utility/getAtom.ts create mode 100644 app/scripts/nmr-cli/src/parse/utility/isProton.ts diff --git a/app/scripts/nmr-cli/package-lock.json b/app/scripts/nmr-cli/package-lock.json index 76095cb..37ab0fb 100644 --- a/app/scripts/nmr-cli/package-lock.json +++ b/app/scripts/nmr-cli/package-lock.json @@ -9,10 +9,14 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@zakodium/nmr-types": "^0.4.0", "@zakodium/nmrium-core": "^0.4.2", "@zakodium/nmrium-core-plugins": "^0.5.3", - "axios": "^1.12.2", + "axios": "^1.13.0", "file-collection": "^5.4.0", + "lodash.merge": "^4.6.2", + "mf-parser": "^3.6.0", + "ml-spectra-processing": "^14.18.0", "nmr-processing": "^20.1.0", "playwright": "^1.56.1", "yargs": "^18.0.0" @@ -21,6 +25,7 @@ "nmr-cli": "build/index.js" }, "devDependencies": { + "@types/lodash.merge": "^4.6.9", "@types/node": "^24.9.1", "@types/yargs": "^17.0.34", "ts-node": "^10.9.2", @@ -94,6 +99,23 @@ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "dev": true }, + "node_modules/@types/lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash.merge": { + "version": "4.6.9", + "resolved": "https://registry.npmjs.org/@types/lodash.merge/-/lodash.merge-4.6.9.tgz", + "integrity": "sha512-23sHDPmzd59kUgWyKGiOMO2Qb9YtqRO/x4IhkgNUiPQ1+5MUVqi6bCZeq9nBJ17msjIMbEIO5u+XW4Kz6aGUhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/node": { "version": "24.9.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz", @@ -120,6 +142,17 @@ "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", "dev": true }, + "node_modules/@zakodium/nmr-types": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@zakodium/nmr-types/-/nmr-types-0.4.0.tgz", + "integrity": "sha512-teWfqqfvgI5zhWv9FObbmedrbHWEyau84/NCQk4ykbJ1uVEHU6srmKJFOGngkWTY3/Dbr0sJp2MRm7eBaNlvdA==", + "license": "CC-BY-NC-SA-4.0", + "dependencies": { + "ml-peak-shape-generator": "^4.2.0", + "ml-signal-processing": "^2.1.0", + "ml-spectra-processing": "^14.18.0" + } + }, "node_modules/@zakodium/nmrium-core": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/@zakodium/nmrium-core/-/nmrium-core-0.4.2.tgz", @@ -235,9 +268,9 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", - "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.1.tgz", + "integrity": "sha512-hU4EGxxt+j7TQijx1oYdAjw4xuIp1wRQSsbMFwSthCWeBQur1eF+qJ5iQ5sN3Tw8YRzQNKb8jszgBdMDVqwJcw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -277,6 +310,18 @@ "node": ">= 0.4" } }, + "node_modules/chemical-elements": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/chemical-elements/-/chemical-elements-2.2.1.tgz", + "integrity": "sha512-Khr3m8RhBbNwDb2MSo9Zb9O+dcUuFourUC0hK+YxNhAtEhOwJPVTMDQeDi1vUwH44tUeNRNKriUs2QQFNQvxgg==", + "license": "MIT" + }, + "node_modules/chemical-groups": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/chemical-groups/-/chemical-groups-2.2.3.tgz", + "integrity": "sha512-rIhA7dC2OJNbQeEFM6+3u81hItYWkaYbWh7awn3hy9RI1qCvhQgdTkrvt7zlLCmcp2nuMzJZUXSju6etBsf6lA==", + "license": "MIT" + }, "node_modules/cheminfo-types": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/cheminfo-types/-/cheminfo-types-1.8.1.tgz", @@ -719,7 +764,8 @@ "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "license": "MIT" }, "node_modules/make-error": { "version": "1.3.6", @@ -742,6 +788,17 @@ "integrity": "sha512-/QL9ptNuLsdA68qO+2o10TKCyu621zwwTFdLvtu8rzRNKsn8zvuGoq/vDxECPyELFG8wu+BpyoMR9BnsJqfVZQ==", "license": "ISC" }, + "node_modules/mf-parser": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/mf-parser/-/mf-parser-3.6.0.tgz", + "integrity": "sha512-vBE7hE8ZB2rtMPxJZHgfuMQIF98ebqXUDTtG/EzapRJ/CDurI/bEo8ZEyQI+ZKznGXr6HGcnBdoE2+U52v/JtA==", + "license": "MIT", + "dependencies": { + "atom-sorter": "^2.2.1", + "chemical-elements": "^2.2.1", + "chemical-groups": "^2.2.3" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", diff --git a/app/scripts/nmr-cli/package.json b/app/scripts/nmr-cli/package.json index 5f8dc57..53c95a6 100644 --- a/app/scripts/nmr-cli/package.json +++ b/app/scripts/nmr-cli/package.json @@ -15,18 +15,23 @@ "nmr-cli": "./build/index.js" }, "dependencies": { + "@zakodium/nmr-types": "^0.4.0", "@zakodium/nmrium-core": "^0.4.2", "@zakodium/nmrium-core-plugins": "^0.5.3", "axios": "^1.13.0", "file-collection": "^5.4.0", + "lodash.merge": "^4.6.2", + "mf-parser": "^3.6.0", + "ml-spectra-processing": "^14.18.0", "nmr-processing": "^20.1.0", "playwright": "^1.56.1", "yargs": "^18.0.0" }, "devDependencies": { + "@types/lodash.merge": "^4.6.9", "@types/node": "^24.9.1", "@types/yargs": "^17.0.34", "ts-node": "^10.9.2", "typescript": "^5.9.3" } -} \ No newline at end of file +} diff --git a/app/scripts/nmr-cli/src/parse/data/data1D/convertDataToFloat64Array.ts b/app/scripts/nmr-cli/src/parse/data/data1D/convertDataToFloat64Array.ts new file mode 100644 index 0000000..b0c39d4 --- /dev/null +++ b/app/scripts/nmr-cli/src/parse/data/data1D/convertDataToFloat64Array.ts @@ -0,0 +1,13 @@ +import type { NmrData1D } from 'cheminfo-types'; + +function convert(value: Float64Array | number[] = []): Float64Array { + return !ArrayBuffer.isView(value) && value ? Float64Array.from(value) : value; +} + +export function convertDataToFloat64Array(data: NmrData1D): NmrData1D { + return { + x: convert(data.x), + re: convert(data.re), + im: convert(data?.im), + }; +} diff --git a/app/scripts/nmr-cli/src/parse/data/data1D/initSumOptions.ts b/app/scripts/nmr-cli/src/parse/data/data1D/initSumOptions.ts new file mode 100644 index 0000000..d02e573 --- /dev/null +++ b/app/scripts/nmr-cli/src/parse/data/data1D/initSumOptions.ts @@ -0,0 +1,56 @@ +import type { SumOptions } from '@zakodium/nmr-types'; +import { MF } from 'mf-parser'; +import getAtom from '../../utility/getAtom'; +import { MoleculeExtended } from '../../type/MoleculeExtended'; + +export { + updateIntegralsRelativeValues, + updateRangesRelativeValues, +} from 'nmr-processing'; + + +export interface SumParams { + nucleus: string; + molecules: MoleculeExtended[]; +} + +export type SetSumOptions = Omit; + +export function initSumOptions( + options: Partial, + params: SumParams, +) { + let newOptions: SumOptions = { + sum: undefined, + isSumConstant: true, + sumAuto: true, + ...options, + }; + const { molecules, nucleus } = params; + + if (options.sumAuto && Array.isArray(molecules) && molecules.length > 0) { + const { mf, id } = molecules[0]; + newOptions = { ...newOptions, sumAuto: true, mf, moleculeId: id }; + } else { + const { mf, moleculeId, ...resOptions } = newOptions; + newOptions = { ...resOptions, sumAuto: false }; + } + if (!newOptions.sum) { + newOptions.sum = getSum(newOptions.mf || null, nucleus); + } + + return newOptions; +} + +export function getSum(mf: string | null | undefined, nucleus: string) { + const defaultSum = 100; + + if (!mf || !nucleus) return defaultSum; + + const atom = getAtom(nucleus); + const atoms = new MF(mf).getInfo().atoms; + + return atoms[atom] || defaultSum; +} + + diff --git a/app/scripts/nmr-cli/src/parse/data/data1D/initiateDatum1D.ts b/app/scripts/nmr-cli/src/parse/data/data1D/initiateDatum1D.ts new file mode 100644 index 0000000..b8cd347 --- /dev/null +++ b/app/scripts/nmr-cli/src/parse/data/data1D/initiateDatum1D.ts @@ -0,0 +1,77 @@ +import type { + Spectrum1D, +} from '@zakodium/nmrium-core'; +import { Filters1DManager } from 'nmr-processing'; + +import { initSumOptions } from './initSumOptions.js'; +import { initiateRanges } from './initiateRanges.js'; +import { convertDataToFloat64Array } from './convertDataToFloat64Array.js'; +import { initiateFilters } from '../initiateFilters.js'; +import { MoleculeExtended } from '../../type/MoleculeExtended.js'; +import { initiatePeaks } from './initiatePeaks.js'; +import { initiateIntegrals } from './initiateIntegrals.js'; + + +interface InitiateDatum1DOptions { + molecules?: MoleculeExtended[]; +} + +export function initiateDatum1D( + spectrum: any, + options: InitiateDatum1DOptions = {}, +): Spectrum1D { + const { molecules = [] } = options; + + const { integrals, ranges, ...restSpectrum } = spectrum; + const spectrumObj: Spectrum1D = { ...restSpectrum }; + spectrumObj.id = spectrum.id || crypto.randomUUID(); + + spectrumObj.display = { + isVisible: true, + isRealSpectrumVisible: true, + ...spectrum.display, + }; + + spectrumObj.info = { + nucleus: '1H', // 1H, 13C, 19F, ... + isFid: false, + isComplex: false, // if isComplex is true that mean it contains real/ imaginary x set, if not hid re/im button . + dimension: 1, + ...spectrum.info, + }; + + spectrumObj.originalInfo = spectrumObj.info; + + spectrumObj.meta = { ...spectrum.meta }; + + spectrumObj.customInfo = { ...spectrum.customInfo }; + + spectrumObj.data = convertDataToFloat64Array(spectrum.data); + + spectrumObj.originalData = spectrumObj.data; + + spectrumObj.filters = initiateFilters(spectrum?.filters); //array of object {name: "FilterName", options: FilterOptions = {value | object} } + + const { nucleus } = spectrumObj.info; + + spectrumObj.peaks = initiatePeaks(spectrum, spectrumObj); + + const integralsOptions = initSumOptions(integrals?.options || {}, { + nucleus, + molecules, + }); + spectrumObj.integrals = initiateIntegrals( + spectrum, + spectrumObj, + integralsOptions, + ); + + const rangesOptions = initSumOptions(ranges?.options || {}, { + nucleus, + molecules, + }); + spectrumObj.ranges = initiateRanges(spectrum, spectrumObj, rangesOptions); + + + return spectrumObj; +} diff --git a/app/scripts/nmr-cli/src/parse/data/data1D/initiateIntegrals.ts b/app/scripts/nmr-cli/src/parse/data/data1D/initiateIntegrals.ts new file mode 100644 index 0000000..f5953a6 --- /dev/null +++ b/app/scripts/nmr-cli/src/parse/data/data1D/initiateIntegrals.ts @@ -0,0 +1,20 @@ +import type { Integrals } from '@zakodium/nmr-types'; +import type { Spectrum1D } from '@zakodium/nmrium-core'; +import merge from 'lodash.merge'; +import { mapIntegrals } from 'nmr-processing'; + +export function initiateIntegrals( + inputSpectrum: Partial, + spectrum: Spectrum1D, + options: Integrals['options'], +) { + return merge( + { + values: [], + options, + }, + { + values: mapIntegrals(inputSpectrum?.integrals?.values || [], spectrum), + }, + ); +} diff --git a/app/scripts/nmr-cli/src/parse/data/data1D/initiatePeaks.ts b/app/scripts/nmr-cli/src/parse/data/data1D/initiatePeaks.ts new file mode 100644 index 0000000..c998297 --- /dev/null +++ b/app/scripts/nmr-cli/src/parse/data/data1D/initiatePeaks.ts @@ -0,0 +1,13 @@ +import type { Peaks } from '@zakodium/nmr-types'; +import type { Spectrum1D } from '@zakodium/nmrium-core'; +import merge from 'lodash.merge'; +import { mapPeaks } from 'nmr-processing'; + +export function initiatePeaks( + inputSpectrum: Partial, + spectrum: Spectrum1D, +): Peaks { + return merge({ values: [], options: {} }, inputSpectrum.peaks, { + values: mapPeaks(inputSpectrum?.peaks?.values || [], spectrum), + }); +} diff --git a/app/scripts/nmr-cli/src/parse/data/data1D/initiateRanges.ts b/app/scripts/nmr-cli/src/parse/data/data1D/initiateRanges.ts new file mode 100644 index 0000000..529eefe --- /dev/null +++ b/app/scripts/nmr-cli/src/parse/data/data1D/initiateRanges.ts @@ -0,0 +1,20 @@ +import type { Ranges } from '@zakodium/nmr-types'; +import type { Spectrum1D } from '@zakodium/nmrium-core'; +import merge from 'lodash.merge'; +import { mapRanges } from 'nmr-processing'; + +export function initiateRanges( + inputSpectrum: Partial, + spectrum: Spectrum1D, + options: Ranges['options'], +) { + return merge( + { + values: [], + options, + }, + { + values: mapRanges(inputSpectrum?.ranges?.values || [], spectrum), + }, + ); +} diff --git a/app/scripts/nmr-cli/src/parse/data/initiateFilters.ts b/app/scripts/nmr-cli/src/parse/data/initiateFilters.ts new file mode 100644 index 0000000..53e1e79 --- /dev/null +++ b/app/scripts/nmr-cli/src/parse/data/initiateFilters.ts @@ -0,0 +1,8 @@ +export function initiateFilters(inputFilters: any): any { + if (!inputFilters || !Array.isArray(inputFilters)) return []; + + return inputFilters.map((filter) => ({ + ...filter, + id: filter?.id || crypto.randomUUID(), + })); +} diff --git a/app/scripts/nmr-cli/src/prase-spectra.ts b/app/scripts/nmr-cli/src/parse/prase-spectra.ts similarity index 100% rename from app/scripts/nmr-cli/src/prase-spectra.ts rename to app/scripts/nmr-cli/src/parse/prase-spectra.ts diff --git a/app/scripts/nmr-cli/src/parse/type/MoleculeExtended.ts b/app/scripts/nmr-cli/src/parse/type/MoleculeExtended.ts new file mode 100644 index 0000000..7e71a00 --- /dev/null +++ b/app/scripts/nmr-cli/src/parse/type/MoleculeExtended.ts @@ -0,0 +1,11 @@ +import { StateMolecule } from "@zakodium/nmrium-core"; + +export interface MoleculeExtended + extends Required>, + Omit { + mf: string; + em: number; + mw: number; + svg: string; + atoms: Record; +} \ No newline at end of file diff --git a/app/scripts/nmr-cli/src/parse/utility/getAtom.ts b/app/scripts/nmr-cli/src/parse/utility/getAtom.ts new file mode 100644 index 0000000..68e66ec --- /dev/null +++ b/app/scripts/nmr-cli/src/parse/utility/getAtom.ts @@ -0,0 +1,3 @@ +export default function getAtom(nucleus: string): string { + return nucleus?.replaceAll(/\d/g, '') || ''; +} diff --git a/app/scripts/nmr-cli/src/parse/utility/isProton.ts b/app/scripts/nmr-cli/src/parse/utility/isProton.ts new file mode 100644 index 0000000..85f6e6e --- /dev/null +++ b/app/scripts/nmr-cli/src/parse/utility/isProton.ts @@ -0,0 +1,3 @@ +export function isProton(nucleus: string) { + return nucleus === '1H'; +} From d152538c34e192995b0492879b6e35e27e56dcc4 Mon Sep 17 00:00:00 2001 From: hamed musallam Date: Fri, 31 Oct 2025 11:31:04 +0100 Subject: [PATCH 2/6] feat: auto ranges detection core function --- .../src/parse/data/data1D/detectRanges.ts | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 app/scripts/nmr-cli/src/parse/data/data1D/detectRanges.ts diff --git a/app/scripts/nmr-cli/src/parse/data/data1D/detectRanges.ts b/app/scripts/nmr-cli/src/parse/data/data1D/detectRanges.ts new file mode 100644 index 0000000..dd8ddd7 --- /dev/null +++ b/app/scripts/nmr-cli/src/parse/data/data1D/detectRanges.ts @@ -0,0 +1,145 @@ +import { xFindClosestIndex } from "ml-spectra-processing"; +import { isProton } from "../../utility/isProton"; +import { Spectrum1D } from "@zakodium/nmrium-core"; +import { mapRanges, OptionsXYAutoPeaksPicking, updateRangesRelativeValues, xyAutoRangesPicking } from "nmr-processing"; + + +//TODO expose OptionsPeaksToRanges from nmr-processing +interface OptionsPeaksToRanges { + /** + * Number of hydrogens or some number to normalize the integration data. If it's zero return the absolute integration value + * @default 100 + */ + integrationSum?: number; + /** + * if it is true, it will join any overlaped ranges. + * @default true + */ + joinOverlapRanges?: boolean; + /** + * If exits it remove all the signals with integration < clean value + * @default 0.4 + */ + clean?: number; + /** + * If true, the Janalyzer function is run over signals to compile the patterns. + * @default true + */ + compile?: boolean; + /** + * option to chose between approx area with peaks or the sum of the points of given range ('sum', 'peaks') + * @default 'sum' + */ + integralType?: string; + /** + * Observed frequency + * @default 400 + */ + frequency?: number; + /** + * distance limit to clustering peaks. + * @default 16 + */ + frequencyCluster?: number; + /** + * If true, it will keep the peaks for each signal + */ + keepPeaks?: boolean; + /** + * Nucleus + * @default '1H' + */ + nucleus?: string; + /** + * ratio of heights between the extreme peaks + * @default 1.5 + */ + symRatio?: number; +} + + +interface AutoDetectOptions { + from?: number; + to?: number; + minMaxRatio?: number; + lookNegative?: number; +} + + + +export function detectRanges( + spectrum: Spectrum1D, + options: AutoDetectOptions = {}, +) { + + + const { from, to, minMaxRatio = 0.05, lookNegative = false } = options + const { info: { nucleus, solvent, originFrequency }, data } = spectrum; + let { x, re } = data + const windowFromIndex = from ? xFindClosestIndex(x, from) : undefined; + const windowToIndex = to ? xFindClosestIndex(x, to) : undefined; + + const isProtonic = isProton(nucleus); + + const peakPickingOptions: OptionsXYAutoPeaksPicking = { + ...defaultPeakPickingOptions, + smoothY: undefined, + sensitivity: 100, + broadWidth: 0.05, + thresholdFactor: 8, + minMaxRatio, + direction: lookNegative ? 'both' : 'positive', + frequency: originFrequency, + sgOptions: undefined, + + }; + + const rangesOptions: OptionsPeaksToRanges = { + nucleus, + compile: isProtonic, + frequency: originFrequency, + integrationSum: isProtonic ? spectrum.ranges.options.sum : 100, + frequencyCluster: isProtonic ? 16 : 0, + clean: 0.5, + keepPeaks: true, + joinOverlapRanges: isProtonic, + }; + + if (windowFromIndex !== undefined && windowToIndex !== undefined) { + x = x.slice(windowFromIndex, windowToIndex); + re = re.slice(windowFromIndex, windowToIndex); + } + + + const ranges = xyAutoRangesPicking( + { x, y: re }, + { + impurities: nucleus === '13C' ? { solvent: solvent || '' } : undefined, + peakPicking: peakPickingOptions, + ranges: rangesOptions, + }, + ); + + + spectrum.ranges.values = spectrum.ranges.values.concat( + mapRanges(ranges, spectrum), + ); + + updateRangesRelativeValues(spectrum); +} + + +const defaultPeakPickingOptions: OptionsXYAutoPeaksPicking = { + minMaxRatio: 1, + shape: { kind: 'lorentzian' }, + realTopDetection: true, + maxCriteria: true, + smoothY: true, + sensitivity: 100, + broadWidth: 0.25, + broadRatio: 0.0025, + thresholdFactor: 5, + sgOptions: { windowSize: 7, polynomial: 3 }, + frequency: 0 +}; + From a445d85a59678165617d1fb0fb3e51b3809e60f7 Mon Sep 17 00:00:00 2001 From: hamed musallam Date: Fri, 31 Oct 2025 11:46:51 +0100 Subject: [PATCH 3/6] feat: auto processing 2d core functions --- .../src/parse/data/data2d/initiateDatum2D.ts | 69 +++++++++++++++++++ .../src/parse/data/data2d/initiateZones.ts | 24 +++++++ .../src/parse/data/data2d/isSpectrum2D.ts | 36 ++++++++++ 3 files changed, 129 insertions(+) create mode 100644 app/scripts/nmr-cli/src/parse/data/data2d/initiateDatum2D.ts create mode 100644 app/scripts/nmr-cli/src/parse/data/data2d/initiateZones.ts create mode 100644 app/scripts/nmr-cli/src/parse/data/data2d/isSpectrum2D.ts diff --git a/app/scripts/nmr-cli/src/parse/data/data2d/initiateDatum2D.ts b/app/scripts/nmr-cli/src/parse/data/data2d/initiateDatum2D.ts new file mode 100644 index 0000000..3676f1e --- /dev/null +++ b/app/scripts/nmr-cli/src/parse/data/data2d/initiateDatum2D.ts @@ -0,0 +1,69 @@ +import type { + Spectrum2D, +} from '@zakodium/nmrium-core'; +import { Filters2DManager } from 'nmr-processing'; + + +import { initiateZones } from './initiateZones.js'; +import { initiateFilters } from '../initiateFilters.js'; + +const defaultMinMax = { z: [], minX: 0, minY: 0, maxX: 0, maxY: 0 }; + + +function initiateDisplay(spectrum: any) { + return { + isPositiveVisible: true, + isNegativeVisible: true, + isVisible: true, + dimension: 2, + ...spectrum.display, + }; +} + +function initiateInfo(spectrum: any) { + return { + nucleus: ['1H', '1H'], + isFt: true, + isFid: false, + isComplex: false, // if isComplex is true that mean it contains real/ imaginary x set, if not hid re/im button . + dimension: 2, + ...spectrum.info, + }; +} + + +export function initiateDatum2D( + spectrum: any, +): Spectrum2D { + const datum: any = { ...spectrum }; + + datum.id = spectrum.id || crypto.randomUUID(); + + datum.display = initiateDisplay(spectrum); + + datum.info = initiateInfo(spectrum); + + datum.originalInfo = datum.info; + + datum.meta = { ...spectrum.meta }; + + datum.customInfo = { ...spectrum.customInfo }; + + datum.data = getData(datum, spectrum); + datum.originalData = datum.data; + datum.filters = initiateFilters(spectrum?.filters); + + datum.zones = initiateZones(spectrum, datum as Spectrum2D); + + //reapply filters after load the original data + + return datum; +} + +function getData(datum: any, options: any) { + if (datum.info.isFid) { + const { re = defaultMinMax, im = defaultMinMax } = options.data; + return { re, im }; + } + return { rr: defaultMinMax, ...options.data }; +} diff --git a/app/scripts/nmr-cli/src/parse/data/data2d/initiateZones.ts b/app/scripts/nmr-cli/src/parse/data/data2d/initiateZones.ts new file mode 100644 index 0000000..cc16095 --- /dev/null +++ b/app/scripts/nmr-cli/src/parse/data/data2d/initiateZones.ts @@ -0,0 +1,24 @@ +import type { Zones } from '@zakodium/nmr-types'; +import type { Spectrum2D } from '@zakodium/nmrium-core'; +import merge from 'lodash.merge'; +import { mapZones } from 'nmr-processing'; + +export function initiateZones( + options: Partial<{ zones: Zones }>, + spectrum: Spectrum2D, +) { + return merge( + { + values: [], + options: { + sum: undefined, + isSumConstant: true, + sumAuto: true, + }, + }, + options.zones, + { + values: mapZones(options?.zones?.values || [], spectrum), + }, + ); +} diff --git a/app/scripts/nmr-cli/src/parse/data/data2d/isSpectrum2D.ts b/app/scripts/nmr-cli/src/parse/data/data2d/isSpectrum2D.ts new file mode 100644 index 0000000..480309b --- /dev/null +++ b/app/scripts/nmr-cli/src/parse/data/data2d/isSpectrum2D.ts @@ -0,0 +1,36 @@ +import type { Spectrum2D, Spectrum } from '@zakodium/nmrium-core'; +import type { NmrData2D, NmrData2DFid, NmrData2DFt } from 'cheminfo-types'; + +export function isSpectrum2D( + spectrum: Spectrum | undefined, +): spectrum is Spectrum2D { + return spectrum?.info.dimension === 2; +} + +function isQuadrantsData(data: NmrData2D): data is NmrData2DFt { + return 'rr' in data && 'ri' in data && 'ir' in data && 'ii' in data; +} +function isFt2DData(data: NmrData2D): data is NmrData2DFt { + return 'rr' in data; +} + +export function isFid2DData(data: NmrData2D): data is NmrData2DFid { + return 're' in data; +} + +export function isFid2DSpectrum( + spectrum: Spectrum, +): spectrum is Spectrum2D & { data: NmrData2DFid } { + return isSpectrum2D(spectrum) && isFid2DData(spectrum.data); +} + +export function isFt2DSpectrum( + spectrum: Spectrum, +): spectrum is Spectrum2D & { data: NmrData2DFt } { + return isSpectrum2D(spectrum) && isFt2DData(spectrum.data); +} +export function isQuadrants2DSpectrum( + spectrum: Spectrum, +): spectrum is Spectrum2D & { data: NmrData2DFt } { + return isSpectrum2D(spectrum) && isQuadrantsData(spectrum.data); +} From 376d8c3cf9542e707f77bb5bc5e9d4cd8e3ab9fc Mon Sep 17 00:00:00 2001 From: hamed musallam Date: Fri, 31 Oct 2025 11:47:18 +0100 Subject: [PATCH 4/6] feat: auto zones detection core function --- .../src/parse/data/data2d/detectZones.ts | 24 ++++ .../parse/data/data2d/getDetectionZones.ts | 121 ++++++++++++++++++ 2 files changed, 145 insertions(+) create mode 100644 app/scripts/nmr-cli/src/parse/data/data2d/detectZones.ts create mode 100644 app/scripts/nmr-cli/src/parse/data/data2d/getDetectionZones.ts diff --git a/app/scripts/nmr-cli/src/parse/data/data2d/detectZones.ts b/app/scripts/nmr-cli/src/parse/data/data2d/detectZones.ts new file mode 100644 index 0000000..62cfe28 --- /dev/null +++ b/app/scripts/nmr-cli/src/parse/data/data2d/detectZones.ts @@ -0,0 +1,24 @@ +import { Spectrum2D } from "@zakodium/nmrium-core"; +import { isFt2DSpectrum } from "./isSpectrum2D"; +import { mapZones } from "nmr-processing"; +import { getDetectionZones } from "./getDetectionZones"; +import { Zone } from "@zakodium/nmr-types"; + +export function detectZones(spectrum: Spectrum2D) { + + if (!isFt2DSpectrum(spectrum)) return; + + const { data } = spectrum; + + const { rr: { minX, maxX, minY, maxY } } = data; + const detectionOptions = { + selectedZone: { fromX: minX, toX: maxX, fromY: minY, toY: maxY }, + thresholdFactor: 1, + maxPercentCutOff: 0.03, + }; + + const zones = getDetectionZones(spectrum, detectionOptions); + spectrum.zones.values = mapZones(zones as Zone[], spectrum); + + +} \ No newline at end of file diff --git a/app/scripts/nmr-cli/src/parse/data/data2d/getDetectionZones.ts b/app/scripts/nmr-cli/src/parse/data/data2d/getDetectionZones.ts new file mode 100644 index 0000000..03c62a4 --- /dev/null +++ b/app/scripts/nmr-cli/src/parse/data/data2d/getDetectionZones.ts @@ -0,0 +1,121 @@ +import type { Spectrum2D } from '@zakodium/nmrium-core'; +import { xyzAutoZonesPicking } from 'nmr-processing'; + +export interface DetectionZonesOptions { + selectedZone: { + fromX: number; + fromY: number; + toX: number; + toY: number; + }; + thresholdFactor: number; + maxPercentCutOff: number; + tolerances?: number[]; + convolutionByFFT?: boolean; + enhanceSymmetry?: boolean; +} + +/** + * + * @param {object} options + * @param {object} options.selectedZone + * @param {number} options.selectedZone.fromX + * @param {number} options.selectedZone.fromY + * @param {number} options.selectedZone.toX + * @param {number} options.selectedZone.toY + * @param {number} options.thresholdFactor + * @param {boolean} options.convolutionByFFT + */ +export function getDetectionZones( + spectrum: Spectrum2D, + options: DetectionZonesOptions, +) { + let dataMatrix = {}; + const { selectedZone } = options; + if (selectedZone) { + options.enhanceSymmetry = false; + dataMatrix = getSubMatrix(spectrum, selectedZone); + } else { + dataMatrix = spectrum.data; + } + + return autoZonesDetection(dataMatrix, { + ...options, + info: spectrum.info, + }); +} + +function autoZonesDetection(data: any, options: any) { + const { + clean, + tolerances, + thresholdFactor, + maxPercentCutOff, + convolutionByFFT, + info: { nucleus: nuclei, originFrequency }, + } = options; + + const { enhanceSymmetry = nuclei[0] === nuclei[1] } = options; + + const zones = xyzAutoZonesPicking(data, { + nuclei, + tolerances, + observedFrequencies: originFrequency, + thresholdFactor, + realTopDetection: true, + clean, + maxPercentCutOff, + enhanceSymmetry, + convolutionByFFT, + }); + + return zones; +} + +function getSubMatrix(datum: any, selectedZone: any) { + const { fromX, toX, fromY, toY } = selectedZone; + const data = datum.data.rr; + const xStep = (data.maxX - data.minX) / (data.z[0].length - 1); + const yStep = (data.maxY - data.minY) / (data.z.length - 1); + let xIndexFrom = Math.max(Math.floor((fromX - data.minX) / xStep), 0); + let yIndexFrom = Math.max(Math.floor((fromY - data.minY) / yStep), 0); + let xIndexTo = Math.min( + Math.floor((toX - data.minX) / xStep), + data.z[0].length - 1, + ); + let yIndexTo = Math.min( + Math.floor((toY - data.minY) / yStep), + data.z.length - 1, + ); + + if (xIndexFrom > xIndexTo) [xIndexFrom, xIndexTo] = [xIndexTo, xIndexFrom]; + if (yIndexFrom > yIndexTo) [yIndexFrom, yIndexTo] = [yIndexTo, yIndexFrom]; + + const dataMatrix: any = { + z: [], + maxX: data.minX + xIndexTo * xStep, + minX: data.minX + xIndexFrom * xStep, + maxY: data.minY + yIndexTo * yStep, + minY: data.minY + yIndexFrom * yStep, + }; + let maxZ = Number.MIN_SAFE_INTEGER; + let minZ = Number.MAX_SAFE_INTEGER; + + const nbXPoints = xIndexTo - xIndexFrom + 1; + + for (let j = yIndexFrom; j < yIndexTo; j++) { + const row = new Float32Array(nbXPoints); + let xIndex = xIndexFrom; + for (let i = 0; i < nbXPoints; i++) { + row[i] = data.z[j][xIndex++]; + } + for (const rowValue of row) { + if (maxZ < rowValue) maxZ = rowValue; + if (minZ > rowValue) minZ = rowValue; + } + dataMatrix.z.push(Array.from(row)); + } + dataMatrix.minZ = minZ; + dataMatrix.maxZ = maxZ; + return dataMatrix; +} From 13754e643fc85a2afb6409e9fb8186ee70e572fc Mon Sep 17 00:00:00 2001 From: hamed musallam Date: Mon, 3 Nov 2025 13:51:03 +0100 Subject: [PATCH 5/6] refator: generateSpectrumFromRanges ranges type --- app/scripts/nmr-cli/src/publication-string.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/scripts/nmr-cli/src/publication-string.ts b/app/scripts/nmr-cli/src/publication-string.ts index 8f0d288..e8a719a 100644 --- a/app/scripts/nmr-cli/src/publication-string.ts +++ b/app/scripts/nmr-cli/src/publication-string.ts @@ -1,10 +1,10 @@ import { resurrect, rangesToXY, - type NMRRangeWithIntegration, } from 'nmr-processing' import { CURRENT_EXPORT_VERSION } from '@zakodium/nmrium-core' import { castToArray } from './utilities/castToArray' +import { NMRRange } from '@zakodium/nmr-types' interface Info { nucleus: string @@ -13,7 +13,7 @@ interface Info { } function generateSpectrumFromRanges( - ranges: NMRRangeWithIntegration[], + ranges: NMRRange[], info: Info ) { const { nucleus, solvent, name = null } = info From 2d84e7e9e3b16eebcecffbfad61e2f1e6dba5927 Mon Sep 17 00:00:00 2001 From: hamed musallam Date: Mon, 3 Nov 2025 13:52:43 +0100 Subject: [PATCH 6/6] feat: expose options for automatic processing and range/zone detection --- app/scripts/nmr-cli/src/index.ts | 83 ++++++++++++++----- .../nmr-cli/src/parse/prase-spectra.ts | 67 +++++++++++++-- 2 files changed, 122 insertions(+), 28 deletions(-) diff --git a/app/scripts/nmr-cli/src/index.ts b/app/scripts/nmr-cli/src/index.ts index 95b810d..4368b21 100755 --- a/app/scripts/nmr-cli/src/index.ts +++ b/app/scripts/nmr-cli/src/index.ts @@ -1,6 +1,6 @@ #!/usr/bin/env node import yargs, { type Argv, type CommandModule, type Options } from 'yargs' -import { loadSpectrumFromURL, loadSpectrumFromFilePath } from './prase-spectra' +import { loadSpectrumFromURL, loadSpectrumFromFilePath } from './parse/prase-spectra' import { generateSpectrumFromPublicationString } from './publication-string' import { parsePredictionCommand } from './prediction/parsePredictionCommand' import { hideBin } from 'yargs/helpers' @@ -14,18 +14,14 @@ Commands: predict Predict spectrum from Mol Options for 'parse-spectra' command: - -u, --url File URL - -p, --path Directory path - -s, --capture-snapshot Capture snapshot + -u, --url File URL + -dir, --dir-path Directory path + -s, --capture-snapshot Capture snapshot + -p, --auto-processing Automatic processing of spectrum (FID → FT spectra). + -d, --auto-detection Enable ranges and zones automatic detection. 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: -ps,--peakShape Peak shape algorithm (default: "lorentzian") choices: ["gaussian", "lorentzian"] @@ -46,16 +42,43 @@ Options for 'predict' command: 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 -dir 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-spectra -dir directory-path // Process spectra files from a directory nmr-cli parse-publication-string "your publication string" ` -interface FileOptionsArgs { - u?: string - p?: string - s?: boolean +export interface FileOptionsArgs { + /** + * -u, --url + * File URL to load remote spectra or data. + */ + u?: string; + + /** + * -dir, --dir-path + * Local directory path for file input or output. + */ + dir?: string; + + /** + * -s, --capture-snapshot + * Capture a visual snapshot of the current state or spectrum. + */ + s?: boolean; + + /** + * -p, --auto-processing + * Automatically process spectrum from FID to FT spectra. + * Mandatory when automatic detection (`--auto-detection`) is enabled. + */ + p?: boolean; + + /** + * -d, --auto-detection + * Perform automatic ranges and zones detection. + */ + d?: boolean; } // Define options for parsing a spectra file @@ -66,8 +89,8 @@ const fileOptions: { [key in keyof FileOptionsArgs]: Options } = { type: 'string', nargs: 1, }, - p: { - alias: 'path', + dir: { + alias: 'dir-path', describe: 'Directory path', type: 'string', nargs: 1, @@ -77,6 +100,16 @@ const fileOptions: { [key in keyof FileOptionsArgs]: Options } = { describe: 'Capture snapshot', type: 'boolean', }, + p: { + alias: 'auto-processing', + describe: 'Auto processing', + type: 'boolean', + }, + d: { + alias: 'auto-detection', + describe: 'Ranges and zones auto detection', + type: 'boolean', + }, } as const const parseFileCommand: CommandModule<{}, FileOptionsArgs> = { @@ -85,21 +118,25 @@ const parseFileCommand: CommandModule<{}, FileOptionsArgs> = { builder: yargs => { return yargs .options(fileOptions) - .conflicts('u', 'p') as Argv + .conflicts('u', 'dir') as Argv }, handler: argv => { + + const { u, dir } = argv; // Handle parsing the spectra file logic based on argv options - if (argv?.u) { - loadSpectrumFromURL(argv.u, argv.s).then(result => { + if (u) { + loadSpectrumFromURL({ u, ...argv }).then(result => { console.log(JSON.stringify(result)) }) } - if (argv?.p) { - loadSpectrumFromFilePath(argv.p, argv.s).then(result => { + + if (dir) { + loadSpectrumFromFilePath({ dir, ...argv }).then(result => { console.log(JSON.stringify(result)) }) } + }, } diff --git a/app/scripts/nmr-cli/src/parse/prase-spectra.ts b/app/scripts/nmr-cli/src/parse/prase-spectra.ts index 2044e4c..36953e9 100644 --- a/app/scripts/nmr-cli/src/parse/prase-spectra.ts +++ b/app/scripts/nmr-cli/src/parse/prase-spectra.ts @@ -1,8 +1,23 @@ import { join, isAbsolute } from 'path' -import { type NmriumState } from '@zakodium/nmrium-core' +import { NmriumData, ParsingOptions, type NmriumState } from '@zakodium/nmrium-core' import init from '@zakodium/nmrium-core-plugins' import playwright from 'playwright' import { FileCollection } from 'file-collection' +import { FileOptionsArgs } from '..' +import { isSpectrum2D } from './data/data2d/isSpectrum2D' +import { initiateDatum2D } from './data/data2d/initiateDatum2D' +import { initiateDatum1D } from './data/data1D/initiateDatum1D' +import { detectZones } from './data/data2d/detectZones' +import { detectRanges } from './data/data1D/detectRanges' +import { Filters1DManager, Filters2DManager } from 'nmr-processing' + +type RequiredKey = Omit & Required>; + +const parsingOptions: ParsingOptions = { + onLoadProcessing: { autoProcessing: true }, + sourceSelector: { general: { dataSelection: 'preferFT' } }, + experimentalFeatures: true +}; interface Snapshot { image: string @@ -79,9 +94,39 @@ async function captureSpectraViewAsBase64(nmriumState: Partial) { await browser.close() return snapshots + +} + +interface ProcessSpectraOptions { + autoDetection: boolean; autoProcessing: boolean; } -async function loadSpectrumFromURL(url: string, enableSnapshot = false) { +function processSpectra(data: NmriumData, options: ProcessSpectraOptions) { + + const { autoDetection = false, autoProcessing = false } = options + + for (let index = 0; index < data.spectra.length; index++) { + const inputSpectrum = data.spectra[index] + const is2D = isSpectrum2D(inputSpectrum); + const spectrum = is2D ? initiateDatum2D(inputSpectrum) : initiateDatum1D(inputSpectrum); + + if (autoProcessing) { + isSpectrum2D(spectrum) ? Filters2DManager.reapplyFilters(spectrum) : Filters1DManager.reapplyFilters(spectrum) + } + + if (autoDetection && spectrum.info.isFt) { + isSpectrum2D(spectrum) ? detectZones(spectrum) : detectRanges(spectrum); + } + + data.spectra[index] = spectrum; + } + + +} + +async function loadSpectrumFromURL(options: RequiredKey) { + const { u: url, s: enableSnapshot = false, p: autoProcessing = false, d: autoDetection = false } = options; + const { pathname: relativePath, origin: baseURL } = new URL(url) const source = { entries: [ @@ -92,11 +137,16 @@ async function loadSpectrumFromURL(url: string, enableSnapshot = false) { baseURL, } - const [nmriumState] = await core.readFromWebSource(source); + const [nmriumState] = await core.readFromWebSource(source, parsingOptions); const { data, version } = nmriumState; + + if (data) { + processSpectra(data, { autoDetection, autoProcessing }); + } + let images: Snapshot[] = [] if (enableSnapshot) { @@ -106,7 +156,9 @@ async function loadSpectrumFromURL(url: string, enableSnapshot = false) { return { data, version, images } } -async function loadSpectrumFromFilePath(path: string, enableSnapshot = false) { +async function loadSpectrumFromFilePath(options: RequiredKey) { + const { dir: path, s: enableSnapshot = false, p: autoProcessing = false, d: autoDetection = false } = options; + const dirPath = isAbsolute(path) ? path : join(process.cwd(), path) const fileCollection = await FileCollection.fromPath(dirPath, { @@ -115,7 +167,12 @@ async function loadSpectrumFromFilePath(path: string, enableSnapshot = false) { const { nmriumState: { data, version }, - } = await core.read(fileCollection) + } = await core.read(fileCollection, parsingOptions) + + + if (data) { + processSpectra(data, { autoDetection, autoProcessing }) + } let images: Snapshot[] = []