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
716 changes: 608 additions & 108 deletions app/routers/predict.py

Large diffs are not rendered by default.

11 changes: 6 additions & 5 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.

5 changes: 3 additions & 2 deletions app/scripts/nmr-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
"mf-parser": "^3.6.0",
"ml-spectra-processing": "^14.19.0",
"nmr-processing": "^22.1.0",
"playwright": "1.56.1",
"openchemlib": "^9.19.0",
"playwright": "^1.56.1",
"yargs": "^18.0.0"
},
"devDependencies": {
Expand All @@ -34,4 +35,4 @@
"ts-node": "^10.9.2",
"typescript": "^5.9.3"
}
}
}
37 changes: 29 additions & 8 deletions app/scripts/nmr-cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
import yargs, { type Argv, type CommandModule, type Options } from 'yargs'
import { loadSpectrumFromURL, loadSpectrumFromFilePath } from './parse/prase-spectra'
import { generateSpectrumFromPublicationString } from './publication-string'
import { parsePredictionCommand } from './prediction/parsePredictionCommand'
import { hideBin } from 'yargs/helpers'
import { parsePredictionCommand } from './prediction'

const usageMessage = `
Usage: nmr-cli <command> [options]
Expand All @@ -24,19 +24,40 @@ Arguments for 'parse-publication-string' command:
publicationString Publication string

Options for 'predict' command:
-ps,--peakShape Peak shape algorithm (default: "lorentzian") choices: ["gaussian", "lorentzian"]
-n, --nucleus Predicted nucleus, choices: ["1H","13C"] (required)

Common options:
-e, --engine Prediction engine (required) choices: ["nmrdb.org", "nmrshift"]
--spectra Spectra types to predict (required) choices: ["proton", "carbon", "cosy", "hsqc", "hmbc"]
-s, --structure MOL file content (structure) (required)

nmrdb.org engine options:
--name Compound name (default: "")
--frequency NMR frequency (MHz) (default: 400)
--protonFrom Proton (1H) from in ppm (default: -1)
--protonTo Proton (1H) to in ppm (default: 12)
--carbonFrom Carbon (13C) from in ppm (default: -5)
--carbonTo Carbon (13C) to in ppm (default: 220)
--nbPoints1d 1D number of points (default: 131072)
--lineWidth 1D line width (default: 1)
--nbPoints2dX 2D spectrum X-axis points (default: 1024)
--nbPoints2dY 2D spectrum Y-axis points (default: 1024)
--autoExtendRange Auto extend range (default: true)

nmrshift engine options:
-i, --id Input ID (default: 1)
-t, --type NMR type (default: "nmr;1H;1d")
-s, --shifts Chemical shifts (default: "1")
--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)
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)"]
--from From in (ppm) for spectrum generation
--to To in (ppm) for spectrum generation
--nbPoints Number of points (default: 1024)
--lineWidth Line width (default: 1)
--frequency NMR frequency (MHz) (default: 400)
--tolerance Tolerance to group peaks with close shift (default: 0.001)
-ps,--peakShape Peak shape algorithm (default: "lorentzian") choices: ["gaussian", "lorentzian"]



Expand Down
64 changes: 64 additions & 0 deletions app/scripts/nmr-cli/src/prediction/engines/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { Options } from 'yargs'
import type { Spectrum } from '@zakodium/nmrium-core'

/**
* Supported experiment types
*/
export type Experiment = 'proton' | 'carbon' | 'cosy' | 'hsqc' | 'hmbc'

/**
* Nucleus types used in NMR
*/
export type Nucleus = '1H' | '13C'

/**
* Map from experiment name to nucleus
*/
export const experimentToNucleus: Record<string, Nucleus> = {
proton: '1H',
carbon: '13C',
}

/**
* Base interface that all engines must implement
*/
export interface Engine {
/** Unique engine identifier (e.g., 'nmrdb.org') */
readonly id: string

readonly name: string
readonly description: string
readonly supportedSpectra: readonly Experiment[]

/** Command-line options specific to this engine */
readonly options: Record<string, Options>

/** List of required option keys */
readonly requiredOptions: readonly string[]

/**
* Build the payload options for the API request
* @param argv - Command line arguments
* @returns Options object to send in the API payload
*/
buildPayloadOptions(argv: Record<string, unknown>): any

/**
* Predict and generate spectra
* This is the main entry point for prediction
* @param structure - MOL file content
* @param options - Command line options
* @returns Array of generated spectra
*/
predict(
structure: string,
options: Record<string, unknown>,
): Promise<Spectrum[]>

/**
* Optional: Custom validation beyond required options
* @param argv - Command line arguments
* @returns true if valid, error message if invalid
*/
validate?(argv: Record<string, unknown>): true | string
}
5 changes: 5 additions & 0 deletions app/scripts/nmr-cli/src/prediction/engines/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import './nmrdb/nmrdb.engine'
import './nmrshift/nmrshift.engine'

export { engineRegistry } from './registry';
export type { Engine } from './base';
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { xMinMaxValues } from "ml-spectra-processing"
import { Experiment } from "../../base"
import { isProton } from "../../../../utilities/isProton"
import { Prediction1D, Prediction2D } from "nmr-processing"
import { PredictedSpectraResult, PredictionOptions } from "../nmrdb.engine"

export function checkFromTo(
predictedSpectra: PredictedSpectraResult,
inputOptions: PredictionOptions,
) {
const setFromTo = (inputOptions: any, nucleus: any, fromTo: any) => {
inputOptions['1d'][nucleus].to = fromTo.to
inputOptions['1d'][nucleus].from = fromTo.from
if (fromTo.signalsOutOfRange) {
signalsOutOfRange[nucleus] = true
}
}

const { autoExtendRange, spectra } = inputOptions
const signalsOutOfRange: Record<string, boolean> = {}

for (const exp in predictedSpectra) {
const experiment = exp as Experiment
if (!spectra[experiment]) continue
if (predictedSpectra[experiment]?.signals.length === 0) continue

if (['carbon', 'proton'].includes(experiment)) {
const spectrum = predictedSpectra[experiment] as Prediction1D
const { signals, nucleus } = spectrum
const { from, to } = (inputOptions['1d'] as any)[nucleus]
const fromTo = getNewFromTo({
deltas: signals.map((s) => s.delta),
from,
to,
nucleus,
autoExtendRange,
})
setFromTo(inputOptions, nucleus, fromTo)
} else {
const { signals, nuclei } = predictedSpectra[experiment] as Prediction2D
for (const nucleus of nuclei) {
const axis = isProton(nucleus) ? 'x' : 'y'
const { from, to } = (inputOptions['1d'] as any)[nucleus]
const fromTo = getNewFromTo({
deltas: signals.map((s) => s[axis].delta),
from,
to,
nucleus,
autoExtendRange,
})
setFromTo(inputOptions, nucleus, fromTo)
}
}
}

for (const nucleus of ['1H', '13C']) {
if (signalsOutOfRange[nucleus]) {
const { from, to } = (inputOptions['1d'] as any)[nucleus]
if (autoExtendRange) {
console.log(
`There are ${nucleus} signals out of the range, it was extended to ${from}-${to}.`,
)
} else {
console.log(`There are ${nucleus} signals out of the range.`)
}
}
}
}



function getNewFromTo(params: {
deltas: number[]
from: number
to: number
nucleus: string
autoExtendRange: boolean
}) {
const { deltas, nucleus, autoExtendRange } = params
let { from, to } = params
const { min, max } = xMinMaxValues(deltas)
const signalsOutOfRange = from > min || to < max

if (autoExtendRange && signalsOutOfRange) {
const spread = isProton(nucleus) ? 0.2 : 2
if (from > min) from = min - spread
if (to < max) to = max + spread
}

return { from, to, signalsOutOfRange }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export function generateName(
name: string,
options: { frequency: number | number[]; experiment: string },
) {
const { frequency, experiment } = options
const freq = Array.isArray(frequency) ? frequency[0] : frequency
return name || `${experiment.toUpperCase()}_${freq}MHz`
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { getRelativeFrequency, mapRanges, signalsToRanges, signalsToXY, updateIntegralsRelativeValues } from "nmr-processing"
import { generateName } from "./generateName"
import { initiateDatum1D } from "../../../../parse/data/data1D/initiateDatum1D"
import { PredictionOptions } from "../nmrdb.engine"

export function generated1DSpectrum(params: {
options: PredictionOptions
spectrum: any
experiment: string
color: string
}) {
const { spectrum, options, experiment, color } = params
const { signals, joinedSignals, nucleus } = spectrum

const {
name,
'1d': { nbPoints, lineWidth },
frequency: freq,
} = options

const SpectrumName = generateName(name, { frequency: freq, experiment })
const frequency = getRelativeFrequency(nucleus, {
frequency: freq,
nucleus,
})

const { x, y } = signalsToXY(signals, {
...(options['1d'] as any)[nucleus],
frequency,
nbPoints,
lineWidth,
})

const first = x[0] ?? 0
const last = x.at(-1) ?? 0
const getFreqOffset = (freq: any) => {
return (first + last) * freq * 0.5
}

const datum = initiateDatum1D(
{
data: { x, im: null, re: y },
display: { color },
info: {
nucleus,
originFrequency: frequency,
baseFrequency: frequency,
frequencyOffset: Array.isArray(frequency)
? frequency.map(getFreqOffset)
: getFreqOffset(frequency),
pulseSequence: 'prediction',
spectralWidth: Math.abs(first - last),
solvent: '',
experiment,
isFt: true,
name: SpectrumName,
title: SpectrumName,
},
},
{},
)

datum.ranges.values = mapRanges(
signalsToRanges(joinedSignals, { frequency }),
datum,
)
updateIntegralsRelativeValues(datum)

return datum
}

Loading