Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
af189c4
feat: enhance contour level determination with percentile-based noise…
jobo322 Jul 27, 2026
91ea373
chore: uses percentiles as a simple array 0-100 percentiles
jobo322 Jul 30, 2026
1751e23
feat: enhance minimum contour threshold determination with percentile…
jobo322 Jul 31, 2026
04a9c0b
chore(contours): default min level percentile 70 for symmetrized COSY…
jobo322 Aug 4, 2026
34b8f95
chore: current dirty state
jobo322 Aug 18, 2026
f18c604
wip: proposal of threshold finders
jobo322 Aug 18, 2026
f790c82
chore: keep only one function to determine the min contour level
jobo322 Aug 19, 2026
1969c84
chore: update package-lock
jobo322 Aug 19, 2026
61e0c9a
feat: split and improve contour min level selection in less time
jobo322 Aug 19, 2026
0e9bcbb
chore: improve speed in contor min level determination
jobo322 Aug 20, 2026
c569a70
chore: indexing
jobo322 Aug 20, 2026
3b1f1c1
feat: improve speed by avoid robust noise determination and max absol…
jobo322 Aug 20, 2026
3b9515c
chore: update package-lock.json
jobo322 Aug 20, 2026
c4dd286
refactor: drop unused exports
hamed-musallam Aug 21, 2026
074cb80
chore: update package.json and package-lock.json
hamed-musallam Aug 21, 2026
1836b4b
chore: fix prettier
hamed-musallam Aug 21, 2026
d0fa914
chore: reduce the max value to avoid selecting the max value as a con…
jobo322 Aug 21, 2026
8e99190
chore: fix test case and correct exporting of a helper function
jobo322 Aug 21, 2026
adc310d
chore: remove test of ignored cases
jobo322 Aug 21, 2026
435ec37
refactor(test): move contour tests to _tests_ to fix package workflow…
hamed-musallam Aug 21, 2026
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
746 changes: 395 additions & 351 deletions package-lock.json

Large diffs are not rendered by default.

54 changes: 40 additions & 14 deletions src/data/data2d/Spectrum2D/contours.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import type { Spectrum2D, Spectrum } from '@zakodium/nmrium-core';
import { isSpectrum2DFt } from '@zakodium/nmrium-core';
import type { NmrData2DFt } from 'cheminfo-types';
import { Conrec } from 'ml-conrec';
import { xMaxAbsoluteValue } from 'ml-spectra-processing';
import { matrixToArray } from 'ml-spectra-processing';

import type { SpectrumFTData } from '../../../component/hooks/use2DReducer.tsx';
import { calculateSanPlot } from '../../utilities/calculateSanPlot.js';

import { estimateNoiseLevel } from './findBestMinContour/estimateNoiseLevel.js';
import { findAutomaticContourLevels } from './findBestMinContour/findAutomaticContourLevels.ts';

interface Level {
positive: ContourItem;
Expand All @@ -26,6 +28,7 @@ interface BaseWheelOptions {
altKey: boolean;
invertScroll?: boolean;
}

interface WheelOptions extends BaseWheelOptions {
contourOptions: ContourOptions;
}
Expand All @@ -40,6 +43,7 @@ const DEFAULT_CONTOURS_OPTIONS: ContourOptions = {
numberOfLayers: 10,
},
};

type LevelSign = keyof Level;

const LEVEL_SIGNS: Readonly<[LevelSign, LevelSign]> = ['positive', 'negative'];
Expand All @@ -56,21 +60,43 @@ function getDefaultContoursLevel(spectrum: Spectrum2D, quadrant = 'rr') {
// @ts-expect-error type of NmrData2D should have a discriminator field to separate fid and ft
const quadrantData = data[quadrant];

//@ts-expect-error will be included in nexts versions
const { noise = calculateSanPlot('2D', quadrantData) } = info;

const { positive = 0, negative = 0 } = noise;
const max = Math.max(
Math.abs(quadrantData.minZ),
Math.abs(quadrantData.maxZ),
const {
experiment = '',
// @ts-expect-error will be included in nexts versions
noise,
} = info;
const matrix = matrixToArray(quadrantData.z);

const noiseLevel = (noise as { positive: number; negative: number })
? Math.max(noise.positive, noise.negative)
: estimateNoiseLevel(matrix);

const bestMinLevel = findAutomaticContourLevels(
matrix,
quadrantData.z.length,
quadrantData.z[0].length,
noiseLevel,
{
maxFdr: 0.005,
diagnostics: false,
maxOccupancy: 0.005,
persistenceLevels: 5,
contourRatio: 1.8,
minPersistence: 0.5,
maxVerticalRidgeScore: 0.05,
maxHorizontalRidgeScore: 0.05,
ridgeCoverageThreshold: experiment.includes('jres') ? 0.9 : 0.1,
},
);

const minAbsPeakBase = 0.005 * max;
const minAllowed = 3 * xMaxAbsoluteValue([positive, negative]);

const minLevel = Math.max(minAbsPeakBase, minAllowed);
const { hasT1Noise, minLevelWithoutT1Noise, minLevel, maxAbsoluteValue } =
bestMinLevel;
const minContourLevel = Math.min(
calculateValueOfLevel(minLevel, max, true),
calculateValueOfLevel(
hasT1Noise ? minLevelWithoutT1Noise : minLevel,
maxAbsoluteValue,
true,
),
DEFAULT_CONTOURS_OPTIONS.positive.contourLevels[1] -
DEFAULT_CONTOURS_OPTIONS.positive.numberOfLayers,
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { xMedian } from 'ml-spectra-processing';

const GAUSSIAN_MAD_SCALE = 1 / 0.6744897501960817;
const DEFAULT_MAX_SAMPLES = 65_536;

export interface EstimateNoiseLevelOptions {
/**
* Maximum number of evenly spaced finite values used for estimation.
* @default 65536
*/
maxSamples?: number;
}

/**
* Estimates the standard deviation of signed Gaussian noise using a
* deterministically downsampled median absolute deviation.
*/
export function estimateNoiseLevel(
matrix: ArrayLike<number>,
options: EstimateNoiseLevelOptions = {},
): number {
const { maxSamples = DEFAULT_MAX_SAMPLES } = options;

if (!Number.isInteger(maxSamples) || maxSamples <= 0) {
throw new Error('maxSamples must be a positive integer');
}

const sample = collectFiniteSample(matrix, maxSamples);

if (sample.length === 0) {
throw new Error('matrix must contain at least one finite value');
}

const median = xMedian(sample);

if (median === 0) {
return 1; //assume NUS reconstruction.
}

const averageDeviations = new Float64Array(sample.length);
for (let i = 0; i < sample.length; i++) {
averageDeviations[i] = Math.abs(sample[i] - median);
}

const noiseLevel = xMedian(averageDeviations) * GAUSSIAN_MAD_SCALE;

if (!Number.isFinite(noiseLevel) || noiseLevel < 0) {
throw new Error('unable to estimate a positive noise level');
}

return noiseLevel;
}

function collectFiniteSample(
matrix: ArrayLike<number>,
maxSamples: number,
): Float64Array {
const step = Math.max(1, Math.ceil(matrix.length / maxSamples));
const sample = new Float64Array(Math.ceil(matrix.length / step));
let sampleIndex = 0;

for (let index = 0; index < matrix.length; index += step) {
sample[sampleIndex++] = matrix[index];
}

return sample;
}
Loading
Loading