Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 26 additions & 10 deletions app/scripts/nmr-cli/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions app/scripts/nmr-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"lodash.merge": "^4.6.2",
"mf-parser": "^3.9.2",
"ml-spectra-processing": "^14.34.0",
"nmr-correlation": "^3.0.2",
"nmr-processing": "^22.23.6",
"openchemlib": "^9.25.0",
"playwright": "1.62.1",
Expand Down
115 changes: 115 additions & 0 deletions app/scripts/nmr-cli/src/correlation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { buildCorrelationData } from 'nmr-correlation'
import type { Options as CorrelationOptions, Spectra } from 'nmr-correlation'
import { FifoLogger } from 'fifo-logger'
import type { NmriumState, Spectrum } from '@zakodium/nmrium-core'
import {
buildWebSource,
core,
loadFileCollection,
parsingOptions,
processSpectra,
} from './parse/prase-spectra'
import { isSpectrum2D } from './parse/data/data2d/isSpectrum2D'

// Default tolerances confirmed by vcnainala on issue #66
const DEFAULT_TOLERANCE_H = 0.02
const DEFAULT_TOLERANCE_C = 0.25

export interface CorrelationInput {
url?: string
dir?: string
mf: string
toleranceH?: number
toleranceC?: number
}

interface ReadSpectraOptions {
url?: string
dir?: string
}

async function readSpectra(
options: ReadSpectraOptions,
logger: FifoLogger
): Promise<Partial<NmriumState>> {
const { url, dir } = options

if (url) {
const { state } = await core.readFromWebSource(buildWebSource(url), {
...parsingOptions,
logger,
})
return state
}

if (dir) {
const { state } = await core.read(await loadFileCollection(dir), {
...parsingOptions,
logger,
})
return state
}

throw new Error('Either a spectra URL or a local directory path is required')
}

// buildCorrelationData needs detected ranges (1D) or zones (2D) to find
// correlations, so require isFt plus at least one detected range/zone.
// This also excludes spectra that failed to initiate or failed detection,
// since those never get ranges/zones populated either.
// Note: a pre-existing bug (see https://github.com/NFDI4Chem/nmrkit/issues/139)
// currently makes every spectrum fail initiation, so real cross-spectrum correlation links are untested here.
function filterSpectra(spectra: Spectrum[]): Spectrum[] {
return spectra.filter(spectrum => {
const { info } = spectrum
if (info.isFt !== true) return false

if (isSpectrum2D(spectrum)) {
const { zones } = spectrum
return zones.values.length > 0
}

const { ranges } = spectrum
return ranges.values.length > 0
})
}

function resolveTolerance(value: number | undefined, fallback: number): number {
return value === undefined || Number.isNaN(value) ? fallback : value
}

export async function generateCorrelationData(input: CorrelationInput) {
const { url, dir, mf, toleranceH, toleranceC } = input
const logger = new FifoLogger()

const state = await readSpectra({ url, dir }, logger)

if (state.data) {
processSpectra(
state.data,
{ autoProcessing: true, autoDetection: true },
logger
)
}

const spectra = filterSpectra(state.data?.spectra ?? [])

const options: CorrelationOptions = {
mf,
tolerance: {
H: resolveTolerance(toleranceH, DEFAULT_TOLERANCE_H),
C: resolveTolerance(toleranceC, DEFAULT_TOLERANCE_C),
},
}

let correlationData
try {
correlationData = buildCorrelationData(spectra as Spectra, options)
} catch (error) {
throw new Error(
`Failed to build correlation data: ${error instanceof Error ? error.message : String(error)}`
)
}

return { ...correlationData, logs: logger.getLogs() }
}
72 changes: 71 additions & 1 deletion app/scripts/nmr-cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { parseSpectra } from './parse/prase-spectra'
import { generateSpectrumFromPublicationString } from './publication-string'
import { generateNMRiumFromPeaks } from './peaks-to-nmrium'
import type { PeaksToNMRiumInput } from './peaks-to-nmrium'
import { generateCorrelationData } from './correlation'
import { hideBin } from 'yargs/helpers'
import { parsePredictionCommand } from './prediction'
import { readFileSync } from 'fs'
Expand All @@ -15,8 +16,16 @@ Usage: nmr-cli <command> [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
predict Predict spectrum from Mol
peaks-to-nmrium Convert a peak list to NMRium object
correlation Build correlation data from NMR spectra fetched from a URL

Options for 'correlation' command:
-u, --url Spectra ZIP file URL
-dir, --dir-path Local directory path
--mf Molecular formula
--tolerance-h, --th H tolerance override (default: 0.02)
--tolerance-c, --tc C tolerance override (default: 0.25)

Options for 'parse-spectra' command:
-u, --url File URL
Expand Down Expand Up @@ -250,12 +259,73 @@ const peaksToNMRiumCommand: CommandModule = {
},
}

// Define the correlation command
const correlationCommand: CommandModule = {
command: ['correlation', 'corr'],
describe: 'Build correlation data from NMR spectra fetched from a URL or a local directory',
builder: yargs => {
return yargs
.options({
u: {
alias: 'url',
describe: 'Spectra ZIP file URL',
type: 'string',
nargs: 1,
},
dir: {
alias: 'dir-path',
describe: 'Local directory path',
type: 'string',
nargs: 1,
},
mf: {
describe: 'Molecular formula',
type: 'string',
demandOption: true,
nargs: 1,
},
'tolerance-h': {
alias: 'th',
describe: 'H tolerance override',
type: 'number',
default: 0.02,
},
'tolerance-c': {
alias: 'tc',
describe: 'C tolerance override',
type: 'number',
default: 0.25,
},
})
.conflicts('u', 'dir')
},
handler: async argv => {
try {
const result = await generateCorrelationData({
url: argv.u as string | undefined,
dir: argv.dir as string | undefined,
mf: argv.mf as string,
toleranceH: argv['tolerance-h'] as number | undefined,
toleranceC: argv['tolerance-c'] as number | undefined,
})
console.log(JSON.stringify(result))
} catch (error) {
console.error(
'Error:',
error instanceof Error ? error.message : String(error),
)
process.exit(1)
}
},
}

yargs(hideBin(process.argv))
.usage(usageMessage)
.command(parseFileCommand)
.command(parsePublicationCommand)
.command(parsePredictionCommand)
.command(peaksToNMRiumCommand)
.command(correlationCommand)
.showHelpOnFail(true)
.help()
.parse()
25 changes: 16 additions & 9 deletions app/scripts/nmr-cli/src/parse/prase-spectra.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,35 +196,42 @@ async function processAndSerialize(
outputResult({ nmriumState: { data, version }, images, logs }, o);
}

async function loadSpectrumFromURL(options: RequiredKey<FileOptionsArgs, 'u'>, logger: FifoLogger) {
const { u: url, include, exclude } = options;

function buildWebSource(url: string) {
const { pathname: relativePath, origin: baseURL } = new URL(url)
const source = {
return {
entries: [
{
relativePath,
},
],
baseURL,
}
}

async function loadSpectrumFromURL(options: RequiredKey<FileOptionsArgs, 'u'>, logger: FifoLogger) {
const { u: url, include, exclude } = options;
Comment thread
hamed-musallam marked this conversation as resolved.

const source = buildWebSource(url)

const { state } = await core.readFromWebSource(source, { ...parsingOptions, fileFilter: { include, exclude }, logger });

processAndSerialize(state, options, logger)

}

async function loadSpectrumFromFilePath(options: RequiredKey<FileOptionsArgs, 'dir'>, logger: FifoLogger) {
const { dir: path, include, exclude } = options;

function loadFileCollection(path: string, include?: string[], exclude?: string[]) {
const dirPath = isAbsolute(path) ? path : join(process.cwd(), path)

const fileCollection = await FileCollection.fromPath(dirPath, {
return FileCollection.fromPath(dirPath, {
unzip: { zipExtensions: ['zip', 'nmredata'] },
filter: { include, exclude },
})
}

async function loadSpectrumFromFilePath(options: RequiredKey<FileOptionsArgs, 'dir'>, logger: FifoLogger) {
const { dir: path, include, exclude } = options;

const fileCollection = await loadFileCollection(path, include, exclude)

const {
state
Expand Down Expand Up @@ -257,4 +264,4 @@ function parseSpectra(argv: yargs.ArgumentsCamelCase<FileOptionsArgs>



export { loadSpectrumFromFilePath, loadSpectrumFromURL, parseSpectra }
export { loadSpectrumFromFilePath, loadSpectrumFromURL, parseSpectra, processSpectra, parsingOptions, core, buildWebSource, loadFileCollection }
Loading