Skip to content

Commit d7a3489

Browse files
committed
Merge branch 'development' of https://github.com/NFDI4Chem/nmrkit into development
2 parents ea9b602 + 21a59c4 commit d7a3489

25 files changed

+1874
-372
lines changed

app/routers/predict.py

Lines changed: 608 additions & 108 deletions
Large diffs are not rendered by default.

app/scripts/nmr-cli/package-lock.json

Lines changed: 6 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/scripts/nmr-cli/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
"mf-parser": "^3.6.0",
2525
"ml-spectra-processing": "^14.19.0",
2626
"nmr-processing": "^22.1.0",
27-
"playwright": "1.56.1",
27+
"openchemlib": "^9.19.0",
28+
"playwright": "^1.56.1",
2829
"yargs": "^18.0.0"
2930
},
3031
"devDependencies": {
@@ -34,4 +35,4 @@
3435
"ts-node": "^10.9.2",
3536
"typescript": "^5.9.3"
3637
}
37-
}
38+
}

app/scripts/nmr-cli/src/index.ts

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
import yargs, { type Argv, type CommandModule, type Options } from 'yargs'
33
import { loadSpectrumFromURL, loadSpectrumFromFilePath } from './parse/prase-spectra'
44
import { generateSpectrumFromPublicationString } from './publication-string'
5-
import { parsePredictionCommand } from './prediction/parsePredictionCommand'
65
import { hideBin } from 'yargs/helpers'
6+
import { parsePredictionCommand } from './prediction'
77

88
const usageMessage = `
99
Usage: nmr-cli <command> [options]
@@ -24,19 +24,40 @@ Arguments for 'parse-publication-string' command:
2424
publicationString Publication string
2525
2626
Options for 'predict' command:
27-
-ps,--peakShape Peak shape algorithm (default: "lorentzian") choices: ["gaussian", "lorentzian"]
28-
-n, --nucleus Predicted nucleus, choices: ["1H","13C"] (required)
27+
28+
Common options:
29+
-e, --engine Prediction engine (required) choices: ["nmrdb.org", "nmrshift"]
30+
--spectra Spectra types to predict (required) choices: ["proton", "carbon", "cosy", "hsqc", "hmbc"]
31+
-s, --structure MOL file content (structure) (required)
32+
33+
nmrdb.org engine options:
34+
--name Compound name (default: "")
35+
--frequency NMR frequency (MHz) (default: 400)
36+
--protonFrom Proton (1H) from in ppm (default: -1)
37+
--protonTo Proton (1H) to in ppm (default: 12)
38+
--carbonFrom Carbon (13C) from in ppm (default: -5)
39+
--carbonTo Carbon (13C) to in ppm (default: 220)
40+
--nbPoints1d 1D number of points (default: 131072)
41+
--lineWidth 1D line width (default: 1)
42+
--nbPoints2dX 2D spectrum X-axis points (default: 1024)
43+
--nbPoints2dY 2D spectrum Y-axis points (default: 1024)
44+
--autoExtendRange Auto extend range (default: true)
45+
46+
nmrshift engine options:
2947
-i, --id Input ID (default: 1)
30-
-t, --type NMR type (default: "nmr;1H;1d")
31-
-s, --shifts Chemical shifts (default: "1")
48+
--shifts Chemical shifts (default: "1")
3249
--solvent NMR solvent (default: "Dimethylsulphoxide-D6 (DMSO-D6, C2D6SO)")
33-
-m, --molText MOL text (required)
34-
--from From in (ppm)
35-
--to To in (ppm)
50+
choices: ["Any", "Chloroform-D1 (CDCl3)", "Dimethylsulphoxide-D6 (DMSO-D6, C2D6SO)",
51+
"Methanol-D4 (CD3OD)", "Deuteriumoxide (D2O)", "Acetone-D6 ((CD3)2CO)",
52+
"TETRACHLORO-METHANE (CCl4)", "Pyridin-D5 (C5D5N)", "Benzene-D6 (C6D6)",
53+
"neat", "Tetrahydrofuran-D8 (THF-D8, C4D4O)"]
54+
--from From in (ppm) for spectrum generation
55+
--to To in (ppm) for spectrum generation
3656
--nbPoints Number of points (default: 1024)
3757
--lineWidth Line width (default: 1)
3858
--frequency NMR frequency (MHz) (default: 400)
3959
--tolerance Tolerance to group peaks with close shift (default: 0.001)
60+
-ps,--peakShape Peak shape algorithm (default: "lorentzian") choices: ["gaussian", "lorentzian"]
4061
4162
4263
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import type { Options } from 'yargs'
2+
import type { Spectrum } from '@zakodium/nmrium-core'
3+
4+
/**
5+
* Supported experiment types
6+
*/
7+
export type Experiment = 'proton' | 'carbon' | 'cosy' | 'hsqc' | 'hmbc'
8+
9+
/**
10+
* Nucleus types used in NMR
11+
*/
12+
export type Nucleus = '1H' | '13C'
13+
14+
/**
15+
* Map from experiment name to nucleus
16+
*/
17+
export const experimentToNucleus: Record<string, Nucleus> = {
18+
proton: '1H',
19+
carbon: '13C',
20+
}
21+
22+
/**
23+
* Base interface that all engines must implement
24+
*/
25+
export interface Engine {
26+
/** Unique engine identifier (e.g., 'nmrdb.org') */
27+
readonly id: string
28+
29+
readonly name: string
30+
readonly description: string
31+
readonly supportedSpectra: readonly Experiment[]
32+
33+
/** Command-line options specific to this engine */
34+
readonly options: Record<string, Options>
35+
36+
/** List of required option keys */
37+
readonly requiredOptions: readonly string[]
38+
39+
/**
40+
* Build the payload options for the API request
41+
* @param argv - Command line arguments
42+
* @returns Options object to send in the API payload
43+
*/
44+
buildPayloadOptions(argv: Record<string, unknown>): any
45+
46+
/**
47+
* Predict and generate spectra
48+
* This is the main entry point for prediction
49+
* @param structure - MOL file content
50+
* @param options - Command line options
51+
* @returns Array of generated spectra
52+
*/
53+
predict(
54+
structure: string,
55+
options: Record<string, unknown>,
56+
): Promise<Spectrum[]>
57+
58+
/**
59+
* Optional: Custom validation beyond required options
60+
* @param argv - Command line arguments
61+
* @returns true if valid, error message if invalid
62+
*/
63+
validate?(argv: Record<string, unknown>): true | string
64+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import './nmrdb/nmrdb.engine'
2+
import './nmrshift/nmrshift.engine'
3+
4+
export { engineRegistry } from './registry';
5+
export type { Engine } from './base';
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { xMinMaxValues } from "ml-spectra-processing"
2+
import { Experiment } from "../../base"
3+
import { isProton } from "../../../../utilities/isProton"
4+
import { Prediction1D, Prediction2D } from "nmr-processing"
5+
import { PredictedSpectraResult, PredictionOptions } from "../nmrdb.engine"
6+
7+
export function checkFromTo(
8+
predictedSpectra: PredictedSpectraResult,
9+
inputOptions: PredictionOptions,
10+
) {
11+
const setFromTo = (inputOptions: any, nucleus: any, fromTo: any) => {
12+
inputOptions['1d'][nucleus].to = fromTo.to
13+
inputOptions['1d'][nucleus].from = fromTo.from
14+
if (fromTo.signalsOutOfRange) {
15+
signalsOutOfRange[nucleus] = true
16+
}
17+
}
18+
19+
const { autoExtendRange, spectra } = inputOptions
20+
const signalsOutOfRange: Record<string, boolean> = {}
21+
22+
for (const exp in predictedSpectra) {
23+
const experiment = exp as Experiment
24+
if (!spectra[experiment]) continue
25+
if (predictedSpectra[experiment]?.signals.length === 0) continue
26+
27+
if (['carbon', 'proton'].includes(experiment)) {
28+
const spectrum = predictedSpectra[experiment] as Prediction1D
29+
const { signals, nucleus } = spectrum
30+
const { from, to } = (inputOptions['1d'] as any)[nucleus]
31+
const fromTo = getNewFromTo({
32+
deltas: signals.map((s) => s.delta),
33+
from,
34+
to,
35+
nucleus,
36+
autoExtendRange,
37+
})
38+
setFromTo(inputOptions, nucleus, fromTo)
39+
} else {
40+
const { signals, nuclei } = predictedSpectra[experiment] as Prediction2D
41+
for (const nucleus of nuclei) {
42+
const axis = isProton(nucleus) ? 'x' : 'y'
43+
const { from, to } = (inputOptions['1d'] as any)[nucleus]
44+
const fromTo = getNewFromTo({
45+
deltas: signals.map((s) => s[axis].delta),
46+
from,
47+
to,
48+
nucleus,
49+
autoExtendRange,
50+
})
51+
setFromTo(inputOptions, nucleus, fromTo)
52+
}
53+
}
54+
}
55+
56+
for (const nucleus of ['1H', '13C']) {
57+
if (signalsOutOfRange[nucleus]) {
58+
const { from, to } = (inputOptions['1d'] as any)[nucleus]
59+
if (autoExtendRange) {
60+
console.log(
61+
`There are ${nucleus} signals out of the range, it was extended to ${from}-${to}.`,
62+
)
63+
} else {
64+
console.log(`There are ${nucleus} signals out of the range.`)
65+
}
66+
}
67+
}
68+
}
69+
70+
71+
72+
function getNewFromTo(params: {
73+
deltas: number[]
74+
from: number
75+
to: number
76+
nucleus: string
77+
autoExtendRange: boolean
78+
}) {
79+
const { deltas, nucleus, autoExtendRange } = params
80+
let { from, to } = params
81+
const { min, max } = xMinMaxValues(deltas)
82+
const signalsOutOfRange = from > min || to < max
83+
84+
if (autoExtendRange && signalsOutOfRange) {
85+
const spread = isProton(nucleus) ? 0.2 : 2
86+
if (from > min) from = min - spread
87+
if (to < max) to = max + spread
88+
}
89+
90+
return { from, to, signalsOutOfRange }
91+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export function generateName(
2+
name: string,
3+
options: { frequency: number | number[]; experiment: string },
4+
) {
5+
const { frequency, experiment } = options
6+
const freq = Array.isArray(frequency) ? frequency[0] : frequency
7+
return name || `${experiment.toUpperCase()}_${freq}MHz`
8+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { getRelativeFrequency, mapRanges, signalsToRanges, signalsToXY, updateIntegralsRelativeValues } from "nmr-processing"
2+
import { generateName } from "./generateName"
3+
import { initiateDatum1D } from "../../../../parse/data/data1D/initiateDatum1D"
4+
import { PredictionOptions } from "../nmrdb.engine"
5+
6+
export function generated1DSpectrum(params: {
7+
options: PredictionOptions
8+
spectrum: any
9+
experiment: string
10+
color: string
11+
}) {
12+
const { spectrum, options, experiment, color } = params
13+
const { signals, joinedSignals, nucleus } = spectrum
14+
15+
const {
16+
name,
17+
'1d': { nbPoints, lineWidth },
18+
frequency: freq,
19+
} = options
20+
21+
const SpectrumName = generateName(name, { frequency: freq, experiment })
22+
const frequency = getRelativeFrequency(nucleus, {
23+
frequency: freq,
24+
nucleus,
25+
})
26+
27+
const { x, y } = signalsToXY(signals, {
28+
...(options['1d'] as any)[nucleus],
29+
frequency,
30+
nbPoints,
31+
lineWidth,
32+
})
33+
34+
const first = x[0] ?? 0
35+
const last = x.at(-1) ?? 0
36+
const getFreqOffset = (freq: any) => {
37+
return (first + last) * freq * 0.5
38+
}
39+
40+
const datum = initiateDatum1D(
41+
{
42+
data: { x, im: null, re: y },
43+
display: { color },
44+
info: {
45+
nucleus,
46+
originFrequency: frequency,
47+
baseFrequency: frequency,
48+
frequencyOffset: Array.isArray(frequency)
49+
? frequency.map(getFreqOffset)
50+
: getFreqOffset(frequency),
51+
pulseSequence: 'prediction',
52+
spectralWidth: Math.abs(first - last),
53+
solvent: '',
54+
experiment,
55+
isFt: true,
56+
name: SpectrumName,
57+
title: SpectrumName,
58+
},
59+
},
60+
{},
61+
)
62+
63+
datum.ranges.values = mapRanges(
64+
signalsToRanges(joinedSignals, { frequency }),
65+
datum,
66+
)
67+
updateIntegralsRelativeValues(datum)
68+
69+
return datum
70+
}
71+

0 commit comments

Comments
 (0)