From af189c491b44eec7dacbc7efb95ba7bdef2a4fe5 Mon Sep 17 00:00:00 2001 From: jobo322 Date: Mon, 27 Jul 2026 11:01:10 -0500 Subject: [PATCH 01/20] feat: enhance contour level determination with percentile-based noise calculation --- src/data/data2d/Spectrum2D/contours.ts | 31 +++++++++++++++++++++----- src/data/utilities/calculateSanPlot.ts | 3 ++- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/data/data2d/Spectrum2D/contours.ts b/src/data/data2d/Spectrum2D/contours.ts index eb5665a555..869def0605 100644 --- a/src/data/data2d/Spectrum2D/contours.ts +++ b/src/data/data2d/Spectrum2D/contours.ts @@ -26,6 +26,7 @@ interface BaseWheelOptions { altKey: boolean; invertScroll?: boolean; } + interface WheelOptions extends BaseWheelOptions { contourOptions: ContourOptions; } @@ -40,6 +41,7 @@ const DEFAULT_CONTOURS_OPTIONS: ContourOptions = { numberOfLayers: 10, }, }; + type LevelSign = keyof Level; const LEVEL_SIGNS: Readonly<[LevelSign, LevelSign]> = ['positive', 'negative']; @@ -51,24 +53,41 @@ interface ContoursManagerReturn { } function getDefaultContoursLevel(spectrum: Spectrum2D, quadrant = 'rr') { - const { data, info } = spectrum; + const { data, info, filters } = spectrum; // @ts-expect-error type of NmrData2D should have a discriminator field to separate fid and ft const quadrantData = data[quadrant]; + + const { acquisitionScheme } = info; //@ts-expect-error will be included in nexts versions - const { noise = calculateSanPlot('2D', quadrantData) } = info; + const { noise = calculateSanPlot('2D', quadrantData, { magnitudeMode: acquisitionScheme === 'notPhaseSensitive' }) } = info; - const { positive = 0, negative = 0 } = noise; + const { positive = 0, negative = 0, percentiles } = noise; + const minAllowedFromNoise = 10 * Math.max(positive, negative); const max = Math.max( Math.abs(quadrantData.minZ), Math.abs(quadrantData.maxZ), ); - const minAbsPeakBase = 0.005 * max; - const minAllowed = 3 * xMaxAbsoluteValue([positive, negative]); + const isSymmetrized = filters.some((filter) => filter.name === 'symmetrizeCosyLike' && filter.enabled); + const isNUS = filters.some((filter) => filter.name === 'nusDimension2' && filter.enabled); + + const { positive: pPositive, negative: pNegative } = percentiles + + const percentileValue = isSymmetrized ? isNUS ? 60 : 90 : 99; + const pPositiveIndex = pPositive(percentileValue) //getClosestYIndex(pPositive, 50, 1 * 50 / pPositive.length, percentileValue); + const pNegativeIndex = pNegative(percentileValue) //getClosestYIndex(pNegative, 50, 1 * 50 / pNegative.length, percentileValue); + + const minAllowedByPercentile = Math.max(pPositive[pPositiveIndex] ?? 0, pNegative[pNegativeIndex] ?? 0); + + const { log10 } = Math; + const inLogScaleNoise = log10(minAllowedFromNoise) / log10(2); + const maxValue = log10(max) / log10(2); + const middleValue = 10 ** (log10(2) * ((maxValue - inLogScaleNoise) / 2 + inLogScaleNoise)); + + const minLevel = isNUS ? Math.min(middleValue, minAllowedByPercentile) : Math.max(middleValue, minAllowedByPercentile); - const minLevel = Math.max(minAbsPeakBase, minAllowed); const minContourLevel = Math.min( calculateValueOfLevel(minLevel, max, true), DEFAULT_CONTOURS_OPTIONS.positive.contourLevels[1] - diff --git a/src/data/utilities/calculateSanPlot.ts b/src/data/utilities/calculateSanPlot.ts index c3548797f5..9a27f5e82a 100644 --- a/src/data/utilities/calculateSanPlot.ts +++ b/src/data/utilities/calculateSanPlot.ts @@ -4,13 +4,14 @@ import { xNoiseSanPlot } from 'ml-spectra-processing'; export function calculateSanPlot( dimension: T, data: T extends '1D' ? NmrData1D : NmrData2DFt['rr'], + options?: { magnitudeMode?: boolean }, ) { const input = dimension === '1D' ? prepare1DData(data as NmrData1D) : prepare2DData(data as NmrData2DFt['rr']); - return xNoiseSanPlot(input); + return xNoiseSanPlot(input, options); } function prepare1DData(data: NmrData1D) { From 91ea3731d2bbfba210a65499d3abb18da743924e Mon Sep 17 00:00:00 2001 From: jobo322 Date: Thu, 30 Jul 2026 10:33:55 -0500 Subject: [PATCH 02/20] chore: uses percentiles as a simple array 0-100 percentiles --- src/data/data2d/Spectrum2D/contours.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/data/data2d/Spectrum2D/contours.ts b/src/data/data2d/Spectrum2D/contours.ts index 869def0605..9b35bdeb3a 100644 --- a/src/data/data2d/Spectrum2D/contours.ts +++ b/src/data/data2d/Spectrum2D/contours.ts @@ -76,8 +76,8 @@ function getDefaultContoursLevel(spectrum: Spectrum2D, quadrant = 'rr') { const { positive: pPositive, negative: pNegative } = percentiles const percentileValue = isSymmetrized ? isNUS ? 60 : 90 : 99; - const pPositiveIndex = pPositive(percentileValue) //getClosestYIndex(pPositive, 50, 1 * 50 / pPositive.length, percentileValue); - const pNegativeIndex = pNegative(percentileValue) //getClosestYIndex(pNegative, 50, 1 * 50 / pNegative.length, percentileValue); + const pPositiveIndex = pPositive[percentileValue]; + const pNegativeIndex = pNegative[percentileValue]; const minAllowedByPercentile = Math.max(pPositive[pPositiveIndex] ?? 0, pNegative[pNegativeIndex] ?? 0); From 1751e23684c3b3e7390d41f58893ad28b51442ed Mon Sep 17 00:00:00 2001 From: jobo322 Date: Fri, 31 Jul 2026 11:09:53 -0500 Subject: [PATCH 03/20] feat: enhance minimum contour threshold determination with percentile-based calculations --- src/data/data2d/Spectrum2D/contours.ts | 156 ++++++++++++++++++++++--- 1 file changed, 140 insertions(+), 16 deletions(-) diff --git a/src/data/data2d/Spectrum2D/contours.ts b/src/data/data2d/Spectrum2D/contours.ts index 9b35bdeb3a..4a423409a0 100644 --- a/src/data/data2d/Spectrum2D/contours.ts +++ b/src/data/data2d/Spectrum2D/contours.ts @@ -1,6 +1,6 @@ import type { Spectrum2D, Spectrum } from '@zakodium/nmrium-core'; import { isSpectrum2DFt } from '@zakodium/nmrium-core'; -import type { NmrData2DFt } from 'cheminfo-types'; +import type { DataXY, NmrData2DFt } from 'cheminfo-types'; import { Conrec } from 'ml-conrec'; import { xMaxAbsoluteValue } from 'ml-spectra-processing'; @@ -63,11 +63,13 @@ function getDefaultContoursLevel(spectrum: Spectrum2D, quadrant = 'rr') { //@ts-expect-error will be included in nexts versions const { noise = calculateSanPlot('2D', quadrantData, { magnitudeMode: acquisitionScheme === 'notPhaseSensitive' }) } = info; - const { positive = 0, negative = 0, percentiles } = noise; - const minAllowedFromNoise = 10 * Math.max(positive, negative); + const {percentiles, sanplot } = noise; + const sanPlotMax = getSanPlotMinMax(sanplot ?? {}); + const positiveSanPlotMax = sanPlotMax.positive?.max ?? 0; + const negativeSanPlotMax = sanPlotMax.negative?.max ?? 0; const max = Math.max( - Math.abs(quadrantData.minZ), - Math.abs(quadrantData.maxZ), + positiveSanPlotMax, + negativeSanPlotMax, ); const isSymmetrized = filters.some((filter) => filter.name === 'symmetrizeCosyLike' && filter.enabled); @@ -75,19 +77,19 @@ function getDefaultContoursLevel(spectrum: Spectrum2D, quadrant = 'rr') { const { positive: pPositive, negative: pNegative } = percentiles - const percentileValue = isSymmetrized ? isNUS ? 60 : 90 : 99; - const pPositiveIndex = pPositive[percentileValue]; - const pNegativeIndex = pNegative[percentileValue]; - - const minAllowedByPercentile = Math.max(pPositive[pPositiveIndex] ?? 0, pNegative[pNegativeIndex] ?? 0); - - const { log10 } = Math; - const inLogScaleNoise = log10(minAllowedFromNoise) / log10(2); - const maxValue = log10(max) / log10(2); - const middleValue = 10 ** (log10(2) * ((maxValue - inLogScaleNoise) / 2 + inLogScaleNoise)); + const percentileValue = isSymmetrized ? (isNUS ? 60 : 90) : 99; + const pPositiveValue = pPositive[percentileValue]; + const pNegativeValue = pNegative[percentileValue]; + + const optimalContourLevel = getContourThresholdFromPercentiles(pPositive, pNegative, { + minP: 80, + maxP: 99, + madScale: 1, + }); - const minLevel = isNUS ? Math.min(middleValue, minAllowedByPercentile) : Math.max(middleValue, minAllowedByPercentile); + const minAllowedByPercentile = Math.max(pPositiveValue ?? 0, pNegativeValue ?? 0); + const minLevel = isNUS ? Math.min(optimalContourLevel, minAllowedByPercentile) : Math.max(optimalContourLevel, minAllowedByPercentile); const minContourLevel = Math.min( calculateValueOfLevel(minLevel, max, true), DEFAULT_CONTOURS_OPTIONS.positive.contourLevels[1] - @@ -113,6 +115,128 @@ function getDefaultContoursLevel(spectrum: Spectrum2D, quadrant = 'rr') { return defaultLevel; } +function getSanPlotMinMax( + sanplot: Record, + options: { logBaseY?: number } = {}, +): Record { + const { logBaseY = 2 } = options; + + const result: Record = {}; + + for (const [key, series] of Object.entries(sanplot)) { + const y = series.y; + if (y.length === 0) { + result[key] = { min: Number.MIN_SAFE_INTEGER, max: Number.MIN_SAFE_INTEGER }; + continue; + } + + const first = logBaseY ** y[0]; + const last = logBaseY ** (y.at(-1) ?? 1); + + result[key] = { + min: Math.min(first, last), + max: Math.max(first, last), + }; + } + + return result; +} + + function getContourThresholdFromPercentiles( + positivePercentiles: readonly number[], + negativePercentiles: readonly number[], + options: ContourThresholdOptions = {}, +): number { + const positiveContourLevel = findOptimalContourThreshold(positivePercentiles, options); + const negativeContourLevel = findOptimalContourThreshold(negativePercentiles, options); + + const positiveThreshold = positivePercentiles[positiveContourLevel.optimalPercentile]; + const negativeThreshold = negativePercentiles[negativeContourLevel.optimalPercentile]; + + return Math.max(positiveThreshold, negativeThreshold); +} +/** + * Finds the optimal minimum contour threshold for a 2D NMR spectrum + * using the Robust Median-MAD formulation on a percentile-intensity array. + * + * @param {number[]} percentiles - Array where index = percentile (0-100), + * value = intensity at that percentile. + * @param {object} options - Configuration options + * @returns {object} - { optimalPercentile, optimalThreshold, maxSNR } + */ +interface ContourThresholdOptions { + minP?: number; + maxP?: number; + madScale?: number; + scoreRatio?: number; +} + +interface OptimalContourMinLevel { + optimalPercentile: number; + optimalThreshold: number; + maxSNR: number; +} + +function interpolate(arr: readonly number[], idx: number): number { + const i = Math.floor(idx); + const j = Math.ceil(idx); + if (i === j || i < 0 || j >= arr.length) { + return arr[Math.max(0, Math.min(arr.length - 1, Math.round(idx)))]; + } + return arr[i] + (idx - i) * (arr[j] - arr[i]); +} + +function findOptimalContourThreshold( + percentiles: readonly number[], + options: ContourThresholdOptions = {} +): OptimalContourMinLevel { + const { minP = 80, maxP = 99, madScale = 1 } = options; + + if (madScale <= 0) { + throw new Error('madScale must be > 0 to avoid division by zero.'); + } + + // Linear interpolation for non-integer percentile indices + + let bestP = minP; + let bestSNR = -Infinity; + + for (let p = minP; p <= maxP; p++) { + const idxMedian = p / 2; + const idxQ1 = p / 4; + const idxQ3 = (3 * p) / 4; + const idxMeanAbove = (p + 100) / 2; + + const medianBelow = interpolate(percentiles, idxMedian); + const q1Below = interpolate(percentiles, idxQ1); + const q3Below = interpolate(percentiles, idxQ3); + const meanAbove = interpolate(percentiles, idxMeanAbove); + + const madBelow = (q3Below - q1Below) / 2; + if (madBelow <= 0) continue; + + const snr = (meanAbove - medianBelow) / (madBelow * madScale); + + // ✦ Coverage weight: fraction of points ABOVE the threshold + // At p=80 → weight=0.20, at p=99 → weight=0.01 + // This penalizes thresholds that exclude too much. + const coverage = (100 - p) / 100; + + // Optional: sharpen the penalty with an exponent + const score = snr * coverage ** 0.5; // sqrt softens it + + if (score > bestSNR) { + bestSNR = score; + bestP = p; + } +} + return { + optimalPercentile: bestP, + optimalThreshold: interpolate(percentiles, bestP), + maxSNR: bestSNR + }; +} + function contoursManager(options: ContourOptions): ContoursManagerReturn { const contourOptions = { ...options }; return { From 04a9c0b6e5b6b590f987165b2ce8fbe6626f01e9 Mon Sep 17 00:00:00 2001 From: jobo322 Date: Tue, 4 Aug 2026 15:20:16 -0500 Subject: [PATCH 04/20] chore(contours): default min level percentile 70 for symmetrized COSY spectra --- src/data/data2d/Spectrum2D/contours.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/data/data2d/Spectrum2D/contours.ts b/src/data/data2d/Spectrum2D/contours.ts index 4a423409a0..51fd1af099 100644 --- a/src/data/data2d/Spectrum2D/contours.ts +++ b/src/data/data2d/Spectrum2D/contours.ts @@ -77,7 +77,7 @@ function getDefaultContoursLevel(spectrum: Spectrum2D, quadrant = 'rr') { const { positive: pPositive, negative: pNegative } = percentiles - const percentileValue = isSymmetrized ? (isNUS ? 60 : 90) : 99; + const percentileValue = isSymmetrized ? (isNUS ? 60 : 70) : 99; const pPositiveValue = pPositive[percentileValue]; const pNegativeValue = pNegative[percentileValue]; @@ -89,7 +89,7 @@ function getDefaultContoursLevel(spectrum: Spectrum2D, quadrant = 'rr') { const minAllowedByPercentile = Math.max(pPositiveValue ?? 0, pNegativeValue ?? 0); - const minLevel = isNUS ? Math.min(optimalContourLevel, minAllowedByPercentile) : Math.max(optimalContourLevel, minAllowedByPercentile); + const minLevel = isSymmetrized || isNUS ? Math.min(optimalContourLevel, minAllowedByPercentile) : Math.max(optimalContourLevel, minAllowedByPercentile); const minContourLevel = Math.min( calculateValueOfLevel(minLevel, max, true), DEFAULT_CONTOURS_OPTIONS.positive.contourLevels[1] - From 34b8f95a943f79f057998f8d6584b49ff0c3bf1c Mon Sep 17 00:00:00 2001 From: jobo322 Date: Tue, 18 Aug 2026 16:35:26 -0500 Subject: [PATCH 05/20] chore: current dirty state --- src/data/data2d/Spectrum2D/contours.ts | 217 ++++++++----------------- 1 file changed, 70 insertions(+), 147 deletions(-) diff --git a/src/data/data2d/Spectrum2D/contours.ts b/src/data/data2d/Spectrum2D/contours.ts index 51fd1af099..15cac8a920 100644 --- a/src/data/data2d/Spectrum2D/contours.ts +++ b/src/data/data2d/Spectrum2D/contours.ts @@ -2,11 +2,17 @@ import type { Spectrum2D, Spectrum } from '@zakodium/nmrium-core'; import { isSpectrum2DFt } from '@zakodium/nmrium-core'; import type { DataXY, NmrData2DFt } from 'cheminfo-types'; import { Conrec } from 'ml-conrec'; -import { xMaxAbsoluteValue } from 'ml-spectra-processing'; +import { matrixMaxAbsoluteZ, matrixMinMaxZ, matrixToArray } from 'ml-spectra-processing'; +import type { Spectrum } from 'nmr-correlation'; import type { SpectrumFTData } from '../../../component/hooks/use2DReducer.tsx'; import { calculateSanPlot } from '../../utilities/calculateSanPlot.js'; +import { computeMinContourLevel } from './autoContourLevel.ts'; +import { NMRContourCalculator } from './countourLevelFinder.ts'; +import { findAutomaticContourLevels } from './findBestMinContour.ts'; +import { getContourThresholdFromPercentiles } from './getContourThresholdFromPercentiles.ts'; + interface Level { positive: ContourItem; negative: ContourItem; @@ -58,44 +64,81 @@ 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]; - const { acquisitionScheme } = info; //@ts-expect-error will be included in nexts versions - const { noise = calculateSanPlot('2D', quadrantData, { magnitudeMode: acquisitionScheme === 'notPhaseSensitive' }) } = info; - - const {percentiles, sanplot } = noise; - const sanPlotMax = getSanPlotMinMax(sanplot ?? {}); - const positiveSanPlotMax = sanPlotMax.positive?.max ?? 0; - const negativeSanPlotMax = sanPlotMax.negative?.max ?? 0; - const max = Math.max( - positiveSanPlotMax, - negativeSanPlotMax, + const { + experiment = '', + noise = calculateSanPlot('2D', quadrantData, { + magnitudeMode: acquisitionScheme === 'notPhaseSensitive', + }), + } = info; + + const { percentiles, sanplot } = noise; + + const max = matrixMaxAbsoluteZ(quadrantData.z) // Math.max(positiveSanPlotMax, negativeSanPlotMax);// + + const isSymmetrized = filters.some( + (filter) => filter.name === 'symmetrizeCosyLike' && filter.enabled, + ); + const isNUS = filters.some( + (filter) => filter.name === 'nusDimension2' && filter.enabled, ); - const isSymmetrized = filters.some((filter) => filter.name === 'symmetrizeCosyLike' && filter.enabled); - const isNUS = filters.some((filter) => filter.name === 'nusDimension2' && filter.enabled); - - const { positive: pPositive, negative: pNegative } = percentiles - + const { positive: pPositive, negative: pNegative } = percentiles; + const percentileValue = isSymmetrized ? (isNUS ? 60 : 70) : 99; const pPositiveValue = pPositive[percentileValue]; const pNegativeValue = pNegative[percentileValue]; - const optimalContourLevel = getContourThresholdFromPercentiles(pPositive, pNegative, { - minP: 80, - maxP: 99, - madScale: 1, - }); - - const minAllowedByPercentile = Math.max(pPositiveValue ?? 0, pNegativeValue ?? 0); - - const minLevel = isSymmetrized || isNUS ? Math.min(optimalContourLevel, minAllowedByPercentile) : Math.max(optimalContourLevel, minAllowedByPercentile); + const optimalContourLevel = getContourThresholdFromPercentiles( + pPositive, + pNegative, + { + minP: 80, + maxP: 99, + madScale: 1, + }, + ); + // console.log(matrixToArray(quadrantData.z).length, matrixMinMaxZ(quadrantData.z), matrixMaxAbsoluteZ(quadrantData.z), max, sanplot, 'length of data') + // const contourLevelProposal = computeMinContourLevel( + // matrixToArray(quadrantData.z), + // Math.max(noise.positive, noise.negative), + // { + // signalPurityTarget: 0.998, + // searchMaxMultiple: 1000, + // } + // ); + + const newProposarGemma = NMRContourCalculator.calculateInitialMinLevel(matrixToArray(quadrantData.z), quadrantData.z.length, + quadrantData.z[0].length, { contourRatio: 1.5, noiseSigma: Math.max(noise.positive, noise.negative), targetSignalLevels: 10, signalPercentile: 0.999 }); + const bestMinLevel = findAutomaticContourLevels( + matrixToArray(quadrantData.z), + quadrantData.z.length, + quadrantData.z[0].length, + Math.max(noise.poistive, noise.negative), + { + persistenceLevels: 10, + ridgeCoverageThreshold: experiment.includes('jres') ? 0.8 : 0.1 + } + ); + console.log(newProposarGemma, 'newProposarGemma'); + console.log(contourLevelProposal, 'contourLevelProposal'); + console.log(bestMinLevel, 'bestMinLevel'); + // const minAllowedByPercentile = Math.max( + // pPositiveValue ?? 0, + // pNegativeValue ?? 0, + // ); + + // const minLevel = + // isSymmetrized || isNUS + // ? Math.min(optimalContourLevel, minAllowedByPercentile) + // : Math.max(optimalContourLevel, minAllowedByPercentile); const minContourLevel = Math.min( - calculateValueOfLevel(minLevel, max, true), + calculateValueOfLevel(bestMinLevel.minLevelWithoutT1Noise, max, true), DEFAULT_CONTOURS_OPTIONS.positive.contourLevels[1] - DEFAULT_CONTOURS_OPTIONS.positive.numberOfLayers, ); - + console.log(`minLevel`, minContourLevel, calculateValueOfLevel(contourLevelProposal.minLevel, max, true), calculateValueOfLevel(bestMinLevel.minLevelWithoutT1Noise, max, true)); const defaultLevel: ContourOptions = { negative: { numberOfLayers: DEFAULT_CONTOURS_OPTIONS.negative.numberOfLayers, @@ -115,127 +158,7 @@ function getDefaultContoursLevel(spectrum: Spectrum2D, quadrant = 'rr') { return defaultLevel; } -function getSanPlotMinMax( - sanplot: Record, - options: { logBaseY?: number } = {}, -): Record { - const { logBaseY = 2 } = options; - - const result: Record = {}; - - for (const [key, series] of Object.entries(sanplot)) { - const y = series.y; - if (y.length === 0) { - result[key] = { min: Number.MIN_SAFE_INTEGER, max: Number.MIN_SAFE_INTEGER }; - continue; - } - - const first = logBaseY ** y[0]; - const last = logBaseY ** (y.at(-1) ?? 1); - - result[key] = { - min: Math.min(first, last), - max: Math.max(first, last), - }; - } - - return result; -} - - function getContourThresholdFromPercentiles( - positivePercentiles: readonly number[], - negativePercentiles: readonly number[], - options: ContourThresholdOptions = {}, -): number { - const positiveContourLevel = findOptimalContourThreshold(positivePercentiles, options); - const negativeContourLevel = findOptimalContourThreshold(negativePercentiles, options); - const positiveThreshold = positivePercentiles[positiveContourLevel.optimalPercentile]; - const negativeThreshold = negativePercentiles[negativeContourLevel.optimalPercentile]; - - return Math.max(positiveThreshold, negativeThreshold); -} -/** - * Finds the optimal minimum contour threshold for a 2D NMR spectrum - * using the Robust Median-MAD formulation on a percentile-intensity array. - * - * @param {number[]} percentiles - Array where index = percentile (0-100), - * value = intensity at that percentile. - * @param {object} options - Configuration options - * @returns {object} - { optimalPercentile, optimalThreshold, maxSNR } - */ -interface ContourThresholdOptions { - minP?: number; - maxP?: number; - madScale?: number; - scoreRatio?: number; -} - -interface OptimalContourMinLevel { - optimalPercentile: number; - optimalThreshold: number; - maxSNR: number; -} - -function interpolate(arr: readonly number[], idx: number): number { - const i = Math.floor(idx); - const j = Math.ceil(idx); - if (i === j || i < 0 || j >= arr.length) { - return arr[Math.max(0, Math.min(arr.length - 1, Math.round(idx)))]; - } - return arr[i] + (idx - i) * (arr[j] - arr[i]); -} - -function findOptimalContourThreshold( - percentiles: readonly number[], - options: ContourThresholdOptions = {} -): OptimalContourMinLevel { - const { minP = 80, maxP = 99, madScale = 1 } = options; - - if (madScale <= 0) { - throw new Error('madScale must be > 0 to avoid division by zero.'); - } - - // Linear interpolation for non-integer percentile indices - - let bestP = minP; - let bestSNR = -Infinity; - - for (let p = minP; p <= maxP; p++) { - const idxMedian = p / 2; - const idxQ1 = p / 4; - const idxQ3 = (3 * p) / 4; - const idxMeanAbove = (p + 100) / 2; - - const medianBelow = interpolate(percentiles, idxMedian); - const q1Below = interpolate(percentiles, idxQ1); - const q3Below = interpolate(percentiles, idxQ3); - const meanAbove = interpolate(percentiles, idxMeanAbove); - - const madBelow = (q3Below - q1Below) / 2; - if (madBelow <= 0) continue; - - const snr = (meanAbove - medianBelow) / (madBelow * madScale); - - // ✦ Coverage weight: fraction of points ABOVE the threshold - // At p=80 → weight=0.20, at p=99 → weight=0.01 - // This penalizes thresholds that exclude too much. - const coverage = (100 - p) / 100; - - // Optional: sharpen the penalty with an exponent - const score = snr * coverage ** 0.5; // sqrt softens it - - if (score > bestSNR) { - bestSNR = score; - bestP = p; - } -} - return { - optimalPercentile: bestP, - optimalThreshold: interpolate(percentiles, bestP), - maxSNR: bestSNR - }; -} function contoursManager(options: ContourOptions): ContoursManagerReturn { const contourOptions = { ...options }; From f18c604292f814d9a599e3c35017bf78bd7de73f Mon Sep 17 00:00:00 2001 From: jobo322 Date: Tue, 18 Aug 2026 16:36:26 -0500 Subject: [PATCH 06/20] wip: proposal of threshold finders --- .../data2d/Spectrum2D/autoContourLevel.ts | 382 ++++++ .../data2d/Spectrum2D/findBestMinContour.ts | 1186 +++++++++++++++++ .../getContourThresholdFromPercentiles.ts | 102 ++ 3 files changed, 1670 insertions(+) create mode 100644 src/data/data2d/Spectrum2D/autoContourLevel.ts create mode 100644 src/data/data2d/Spectrum2D/findBestMinContour.ts create mode 100644 src/data/data2d/Spectrum2D/getContourThresholdFromPercentiles.ts diff --git a/src/data/data2d/Spectrum2D/autoContourLevel.ts b/src/data/data2d/Spectrum2D/autoContourLevel.ts new file mode 100644 index 0000000000..6bee7c028b --- /dev/null +++ b/src/data/data2d/Spectrum2D/autoContourLevel.ts @@ -0,0 +1,382 @@ +/** + * autoContourLevel.ts + * + * Automatic minimum-contour-level selection for 2D NMR spectra. + * + * Algorithm: Signal Purity Threshold (SPT) + * ───────────────────────────────────────── + * Finds the smallest absolute-intensity threshold L such that the + * fraction of pixels above L that are *genuine signal* (not Gaussian + * noise) meets a configurable target (default 50 %). + * + * The threshold is symmetric: use +L_min for positive contours and + * −L_min for negative contours. + */ + +// ─── Math utilities ──────────────────────────────────────────────────────── + +/** + * Complementary error function erfc(x) = 1 − erf(x). + * + * Rational approximation from Abramowitz & Stegun §7.1.26. + * Max absolute error: |ε| < 1.5 × 10⁻⁷ for all real x. + * + * For Gaussian X ~ N(0, σ²): P(|X| > L) = erfc(L / (σ√2)) + */ +export function erfc(x: number): number { + if (x < 0) return 2 - erfc(-x); + const t = 1 / (1 + 0.3275911 * x); + return ( + t * + (0.254829592 + + t * + (-0.284496736 + + t * (1.421413741 + t * (-1.453152027 + t * 1.061405429)))) * + Math.exp(-x * x) + ); +} + +// ─── Types ───────────────────────────────────────────────────────────────── + +export interface AutoContourOptions { + /** + * Desired fraction of pixels above L_min that are genuine signal. + * + * Range: (0, 1). Default: 0.5. + * + * Interpretation: + * 0.5 → transition point (half noise, half signal above L_min). + * Good balance: weak peaks are included, modest noise. + * 0.7 → cleaner display; may hide the weakest peaks. + * 0.9 → very clean; only use when weak-peak visibility is not critical. + * + * This is the most impactful parameter to expose to the user. + */ + signalPurityTarget?: number; + + /** + * Hard lower bound: L_min is always ≥ this × noiseSigma, + * even if purity is already satisfied at a lower threshold. + * + * Prevents accidental noise display from σ mis-estimation. + * Default: 3.0 + */ + minimumNoiseMultiple?: number; + + /** + * Fallback multiple of σ used when the algorithm cannot find a + * data-driven threshold (very noisy, near-empty, or poor-SNR spectra). + * + * Default: 8.0 (conservative; produces few or no contours when + * no real signal is detectable, which is the correct visual output). + */ + fallbackNoiseMultiple?: number; + + /** + * Upper limit of the search range in multiples of noiseSigma. + * Increase for spectra with extreme dynamic range (> 10 000 : 1). + * Default: 30.0 + */ + searchMaxMultiple?: number; + + /** + * Minimum number of pixels that must lie above a candidate L_min + * for it to be considered valid. + * + * Prevents choosing a threshold where only 1–2 outlier pixels qualify, + * which would give an unreliable purity estimate. + * Default: 10 + */ + minimumPixelCount?: number; + + /** + * Number of log-spaced candidates evaluated during the sweep. + * Higher → finer resolution in the L axis; negligible speed cost. + * Default: 300 + */ + searchSteps?: number; +} + +export interface AutoContourResult { + /** + * Recommended minimum contour level (absolute intensity). + * Use +minLevel for positive contours, −minLevel for negative ones. + */ + minLevel: number; + + /** minLevel / noiseSigma for easy interpretation and logging. */ + noiseMultiple: number; + + /** + * True → a data-driven threshold was found in the spectrum. + * False → the algorithm fell back to fallbackNoiseMultiple × σ + * (noisy, sparse, or near-empty spectrum). + */ + dataAdaptive: boolean; + + /** + * Estimated fraction of pixels above minLevel that are genuine signal + * rather than Gaussian noise. Range [0, 1]. + * + * Useful for diagnostics and for surfacing confidence to the user + * ("threshold at 5.2 σ — 78 % signal purity"). + */ + signalPurityAtThreshold: number; +} + +// ─── Main algorithm ──────────────────────────────────────────────────────── + +/** + * Automatically select the minimum contour level for a 2D NMR spectrum. + * + * ## How it works + * + * 1. Sort absolute intensities once. + * 2. For each candidate threshold L (log-spaced from 3 σ to 30 σ): + * actual = number of pixels with |I| > L (binary search) + * noise = N · erfc(L / (σ√2)) (theoretical) + * purity = (actual − noise) / actual + * 3. Return the smallest L where purity ≥ target AND actual ≥ N_min. + * 4. If no such L exists, return fallbackNoiseMultiple × σ. + * + * ## Complexity + * Sorting: O(N log N) — once. + * Sweep: O(steps · log N) ≈ O(300 · 20) = O(6000) — negligible. + * + * @param intensities Flattened spectrum matrix (real-valued, row-major). + * Positive and negative values are both handled. + * @param noiseSigma Estimated noise standard deviation (must be > 0). + * Typically measured in a spectral region known to + * contain no peaks. + * @param options Algorithm tuning parameters (all optional). + */ +export function computeMinContourLevel( + intensities: ArrayLike, + noiseSigma: number, + options: AutoContourOptions = {}, +): AutoContourResult { + const { + signalPurityTarget = 0.99, + minimumNoiseMultiple = 5, + fallbackNoiseMultiple = 8, + searchMaxMultiple = 50, + minimumPixelCount = Math.floor(intensities.length * 0.005), + searchSteps = 300, + } = options; + + // ── Guard: return fallback for degenerate inputs ───────────────────────── + const fallback: AutoContourResult = { + minLevel: fallbackNoiseMultiple * noiseSigma, + noiseMultiple: fallbackNoiseMultiple, + dataAdaptive: false, + signalPurityAtThreshold: 0, + }; + + const N = intensities.length; + + if (N === 0 || !(noiseSigma > 0) || !Number.isFinite(noiseSigma)) return fallback; + + // ── 1. Compute and sort absolute intensities ───────────────────────────── + const absArr = new Float64Array(N); + for (let i = 0; i < N; i++) { + absArr[i] = Math.abs(intensities[i]); + } + absArr.sort(); // ascending; in-place, O(N log N) + + // ── 2. O(log N) count of pixels strictly above a threshold ─────────────── + function countAbove(threshold: number): number { + let lo = 0; + let hi = N; + while (lo < hi) { + const mid = (lo + hi) >>> 1; + if (absArr[mid] <= threshold) { + lo = mid + 1 + } else { + hi = mid + }; + } + return N - lo; + } + + // ── 3. Theoretical noise count above threshold ─────────────────────────── + // For X ~ N(0, σ²): P(|X| > L) = erfc(L / (σ√2)) + const SQRT2 = Math.SQRT2; + function noiseCountAbove(L: number): number { + return N * erfc(L / (noiseSigma * SQRT2)); + } + + // ── 4. Log sweep from floor to ceiling ─────────────────────────────────── + // + // Purity condition: + // R(L) = (actual − noise) / actual ≥ target + // ⟺ actual ≥ noise / (1 − target) [since actual > 0] + // + // Sweep low → high; break at the first L satisfying the condition. + // This gives the *minimum* threshold where signal dominates. + + const Lfloor = minimumNoiseMultiple * noiseSigma; + const Lceil = searchMaxMultiple * noiseSigma; + const logLo = Math.log(Lfloor); + const logHi = Math.log(Lceil); + const purityScale = 1 / (1 - signalPurityTarget); // e.g. 2.0 for target 0.5 + + for (let step = 0; step <= searchSteps; step++) { + const L = Math.exp(logLo + (step / searchSteps) * (logHi - logLo)); + const actual = countAbove(L); + + // Skip if too few pixels above this threshold (unreliable purity estimate). + if (actual < minimumPixelCount) continue; + + const noise = noiseCountAbove(L); + if (actual >= purityScale * noise) { + // Purity condition met. Clamp to [0,1] to handle small σ errors. + const signalPurityAtThreshold = Math.min( + 1, + Math.max(0, (actual - noise) / actual), + ); + return { + minLevel: L, + noiseMultiple: L / noiseSigma, + dataAdaptive: true, + signalPurityAtThreshold, + }; + } + } + + // No threshold found: spectrum is too noisy or too sparse. + return fallback; +} + +// ─── Companion utilities ─────────────────────────────────────────────────── + +/** + * Compute a robust maximum contour level from the spectrum. + * + * Uses a high percentile of |intensities| (default: 99.9th) to avoid + * a single outlier pixel inflating L_max and compressing all contours + * into the bottom of the range. + * + * @param intensities Flattened spectrum matrix. + * @param percentile Quantile in (0, 1). Default: 0.999. + */ +export function computeMaxContourLevel( + intensities: ArrayLike, + percentile = 0.999, +): number { + const N = intensities.length; + if (N === 0) return 1; + + const absArr = new Float64Array(N); + for (let i = 0; i < N; i++) { + absArr[i] = Math.abs(intensities[i]); + } + absArr.sort(); + + const idx = Math.min(Math.floor(percentile * N), N - 1); + return absArr[idx]; +} + +// ─── Exponential contour level builder ──────────────────────────────────── + +export interface ContourLevels { + /** Positive contour levels, ascending. */ + positive: number[]; + /** Negative contour levels (mirrors of positive, descending). */ + negative: number[]; + /** The exponential ratio r used: L_{n+1} = L_n · r */ + ratio: number; +} + +export interface BuildContourOptions { + /** Minimum contour level (from computeMinContourLevel). */ + minLevel: number; + /** Maximum contour level (from computeMaxContourLevel or user override). */ + maxLevel: number; + /** + * Number of positive levels. Ignored if `ratio` is provided. + * Default: 12. + */ + nLevels?: number; + /** + * Fixed exponential ratio r. If provided, nLevels is derived as + * floor(log(maxLevel / minLevel) / log(r)) + 1 + * so the levels span the full [minLevel, maxLevel] range. + */ + ratio?: number; +} + +/** + * Build symmetric, exponentially spaced contour levels. + * + * Positive levels: [L_min, L_min·r, L_min·r², …, L_max] + * Negative levels: mirror of positive, negated. + * + * This is the function to call after `computeMinContourLevel` and + * `computeMaxContourLevel`. + */ +export function buildContourLevels(opts: BuildContourOptions): ContourLevels { + const { minLevel, maxLevel, nLevels = 12 } = opts; + + if (!(minLevel > 0)) throw new RangeError('minLevel must be positive'); + if (!(maxLevel > minLevel)) { + throw new RangeError('maxLevel must be greater than minLevel'); + } + + let ratio: number; + let n: number; + + if (opts.ratio !== undefined) { + ratio = opts.ratio; + if (!(ratio > 1)) throw new RangeError('ratio must be > 1'); + n = Math.max( + 1, + Math.floor(Math.log(maxLevel / minLevel) / Math.log(ratio)) + 1, + ); + } else { + n = Math.max(2, nLevels); + ratio = (maxLevel / minLevel) ** (1 / (n - 1)); + } + + const positive = Array.from({ length: n }, (_, i) => minLevel * ratio ** i); + return { + positive, + negative: [...positive].toReversed().map((v) => -v), + ratio, + }; +} + +// ─── End-to-end usage example ────────────────────────────────────────────── + +/* +import { computeMinContourLevel, computeMaxContourLevel, buildContourLevels } from './autoContourLevel'; + +// spectrum: your Float64Array (rows × cols, flattened) +// noiseSigma: from your existing noise estimation + +const minResult = computeMinContourLevel(spectrum, noiseSigma, { + signalPurityTarget: 0.5, // expose to user as "noise cutoff sensitivity" +}); + +const maxLevel = computeMaxContourLevel(spectrum, 0.999); + +const { positive, negative, ratio } = buildContourLevels({ + minLevel: minResult.minLevel, + maxLevel, + nLevels: 12, // expose to user +}); + +// Log for diagnostics: +console.log( + `L_min = ${minResult.minLevel.toFixed(2)} ` + + `(${minResult.noiseMultiple.toFixed(1)} σ, ` + + `purity = ${(minResult.signalPurityAtThreshold * 100).toFixed(0)} %, ` + + `adaptive = ${minResult.dataAdaptive})` +); +console.log(`ratio r = ${ratio.toFixed(3)}, ${positive.length} levels`); + +// Pass `positive` and `negative` to your contour renderer. +// Initial user controls: +// L_min slider → rerun buildContourLevels with minLevel = slider value +// L_max slider → rerun with maxLevel = slider value +// nLevels → rerun with nLevels = new value +*/ diff --git a/src/data/data2d/Spectrum2D/findBestMinContour.ts b/src/data/data2d/Spectrum2D/findBestMinContour.ts new file mode 100644 index 0000000000..ddbbf29f69 --- /dev/null +++ b/src/data/data2d/Spectrum2D/findBestMinContour.ts @@ -0,0 +1,1186 @@ +import { matrixMaxAbsoluteZ, xMaxAbsoluteValue } from "ml-spectra-processing"; + +export interface AutoContourDiagnostics { + recommended: ThresholdDiagnostics; + ridgeFree: ThresholdDiagnostics; + + /** + * True when directional ridge structures were detected + * at the recommended minimum. + */ + hasT1Noise: boolean; +} + +export interface ThresholdDiagnostics { + sigmaMultiplier: number; + minLevel: number; + + occupancy: number; + fdr: number; + persistence: number; + + verticalRidgeScore: number; + horizontalRidgeScore: number; +} + +export interface AutoContourResult { + /** + * Recommended minimum contour level. + * + * Optimized for preserving meaningful weak peaks while + * avoiding ordinary noise. + */ + minLevel: number; + + /** + * Minimum contour level at which detected directional + * ridge structures (e.g. t1-noise) are no longer present. + * + * This is useful as an optional "ridge-free" visualization. + */ + minLevelWithoutT1Noise: number; + + /** + * Same values expressed in multiples of the noise level. + */ + sigmaMultiplier: number; + sigmaMultiplierWithoutT1Noise: number; + + diagnostics?: AutoContourDiagnostics; +} + +export interface AutoContourOptions { + /** + * Multiplicative ratio between consecutive contour levels. + * + * The contour levels are generated as: + * + * Lₙ = L₀ × contourRatioⁿ + * + * Must be greater than 1. + * + * A smaller value produces more closely spaced contour levels, + * while a larger value produces fewer, more widely spaced levels. + * + * @default 1.4 + */ + contourRatio?: number; + + /** + * Maximum acceptable false discovery rate (FDR) for the + * automatically selected minimum contour level. + * + * The FDR estimates the fraction of pixels above the selected + * threshold that could be explained by Gaussian noise, assuming + * the supplied noiseLevel is accurate. + * + * Smaller values produce a cleaner visualization but may suppress + * weak peaks. + * + * @default 0.05 + */ + maxFdr?: number; + + /** + * Maximum fraction of the spectrum that may be above the + * automatically selected minimum contour level. + * + * This prevents very low thresholds from producing contours over + * a large fraction of the spectrum. + * + * Expressed as a fraction in the range [0, 1]. + * + * For example, 0.02 means that at most 2% of pixels may be above + * the minimum contour threshold. + * + * @default 0.02 + */ + maxOccupancy?: number; + + /** + * Minimum persistence required for spectral structures across + * successive exponential contour levels. + * + * Persistence measures how much of the active spectral structure + * remains when moving from one contour level to the next. + * + * Higher values favor stable, coherent peaks and reject fragmented + * noise structures more aggressively. + * + * Expressed as a fraction in the range [0, 1]. + * + * @default 0.35 + */ + minPersistence?: number; + + /** + * Maximum acceptable score for vertical ridge-like structures. + * + * This is primarily intended to detect structured artifacts such + * as t1 noise, which often appears as elongated structures along + * the F1 dimension. + * + * Smaller values make the ridge-free threshold more conservative. + * + * @default 0.25 + */ + maxVerticalRidgeScore?: number; + + /** + * Maximum acceptable score for horizontal ridge-like structures. + * + * This can detect horizontal streaks or other structured artifacts + * extending predominantly along the F2 dimension. + * + * Smaller values make the ridge-free threshold more conservative. + * + * @default 0.25 + */ + maxHorizontalRidgeScore?: number; + + /** + * Fraction of a row or column that must be occupied by active + * contour pixels for the row or column to be considered + * ridge-like. + * + * For vertical ridge detection, a column is considered suspicious + * when the fraction of active pixels along F1 is greater than or + * equal to this value. + * + * For horizontal ridge detection, the same criterion is applied + * along F2. + * + * Expressed as a fraction in the range [0, 1]. + * + * Lower values make ridge detection more sensitive. + * + * @default 0.5 + */ + ridgeCoverageThreshold?: number; + + /** + * Lowest threshold considered by the automatic contour search, + * expressed as a multiple of the supplied noise level. + * + * For example, minSigma = 2 means that thresholds below 2 × + * noiseLevel are never considered. + * + * Lower values can preserve weaker peaks but increase the risk + * of including noise. + * + * @default 2 + */ + minSigma?: number; + + /** + * Highest threshold considered by the automatic contour search, + * expressed as a multiple of the supplied noise level. + * + * This limits the search range and prevents the algorithm from + * examining excessively high contour levels. + * + * @default 10 + */ + maxSigma?: number; + + /** + * Exponent used to determine the spacing between candidate + * minimum contour levels. + * + * Candidate thresholds are generated multiplicatively using: + * + * candidateStep = contourRatio ** candidateRatioExponent + * + * For example, with contourRatio = 1.4 and exponent = 0.5: + * + * candidateStep = sqrt(1.4) + * + * A value of 1 evaluates candidates directly on the contour-level + * hierarchy. Smaller values provide finer threshold resolution + * while increasing the amount of analysis. + * + * @default 0.5 + */ + candidateRatioExponent?: number; + + /** + * Number of successive contour levels used when evaluating + * spectral persistence. + * + * A larger value requires spectral structures to remain stable + * across more contour levels and therefore produces a more + * conservative threshold. + * + * @default 4 + */ + persistenceLevels?: number; + + /** + * Maximum dimension used for spatial analysis. + * + * Large input matrices are reduced using max pooling before + * calculating occupancy, persistence, and ridge metrics. + * + * The original matrix is not modified. + * + * Max pooling is used instead of averaging so that narrow, + * high-intensity spectral features are preserved during analysis. + * + * Larger values improve spatial resolution but increase + * computational cost and memory usage. + * + * @default 384 + */ + maxAnalysisDimension?: number; + + /** + * Whether diagnostic information should be returned. + * + * Diagnostics include the selected thresholds, FDR, occupancy, + * persistence, vertical and horizontal ridge scores, and the + * individual criteria used by the automatic threshold selection. + * + * Disable this when diagnostics are not needed and the smallest + * possible result object is preferred. + * + * @default true + */ + diagnostics?: boolean; +} + +interface EvaluatedThreshold { + sigmaMultiplier: number; + + activePixels: number; + occupancy: number; + fdr: number; + persistence: number; + + verticalRidgeScore: number; + horizontalRidgeScore: number; +} + +function evaluateThreshold( + matrix: Float64Array, + rows: number, + cols: number, + noiseLevel: number, + sigmaMultiplier: number, + contourRatio: number, + persistenceLevels: number, + ridgeCoverageThreshold: number, +): EvaluatedThreshold { + const threshold = + sigmaMultiplier * noiseLevel; + + const mask = + createMask( + matrix, + threshold, + ); + + const activePixels = + countActivePixels(mask); + + const totalPixels = + rows * cols; + + const occupancy = + activePixels / totalPixels; + + if (activePixels === 0) { + return { + sigmaMultiplier, + activePixels: 0, + occupancy: 0, + fdr: 0, + persistence: 0, + verticalRidgeScore: 0, + horizontalRidgeScore: 0, + }; + } + + const expectedNoisePixels = + totalPixels * + gaussianTwoSidedTail( + sigmaMultiplier, + ); + + const fdr = + Math.min( + 1, + expectedNoisePixels / + activePixels, + ); + + const persistence = + computePersistence( + matrix, + rows, + cols, + threshold, + contourRatio, + persistenceLevels, + ); + + const ridge = + computeRidgeScores( + mask, + rows, + cols, + ridgeCoverageThreshold, + ); + + return { + sigmaMultiplier, + + activePixels, + occupancy, + + fdr, + persistence, + + verticalRidgeScore: + ridge.vertical, + + horizontalRidgeScore: + ridge.horizontal, + }; +} + +export function findAutomaticContourLevels( + matrix: ArrayLike, + rows: number, + cols: number, + noiseLevel: number, + options: AutoContourOptions = {}, +): AutoContourResult { + validateInput( + matrix, + rows, + cols, + noiseLevel, + ); + + const { + contourRatio = 1.4, + + maxFdr = 0.05, + maxOccupancy = 0.02, + minPersistence = 0.35, + + maxVerticalRidgeScore = 0.25, + maxHorizontalRidgeScore = 0.25, + + ridgeCoverageThreshold = 0.5, + + minSigma = 5, + maxSigma = 1000, + + candidateRatioExponent = 0.5, + + persistenceLevels = 4, + + maxAnalysisDimension = 384, + + diagnostics = true, + } = options; + + if (contourRatio <= 1) { + throw new Error( + 'contourRatio must be > 1', + ); + } + + /* + * Analyze a max-pooled representation. + */ + const analysis = maxPoolAbsolute( + matrix, + rows, + cols, + maxAnalysisDimension, + ); + + const { + values, + rows: analysisRows, + cols: analysisCols, + } = analysis; + + const maxSigmaInData = + findMaximumNormalizedValue( + values, + noiseLevel, + ); + console.log(maxSigmaInData, 'maxSigmaInData'); + /* + * Generate candidate minimum thresholds. + */ + const candidates = + generateCandidateSigmaLevels( + minSigma, + maxSigmaInData, + contourRatio, + candidateRatioExponent, + ); + + /* + * ------------------------------------------------------- + * STEP 1 + * + * Find the lowest threshold that is statistically clean + * and sufficiently persistent. + * + * t1-ridges are deliberately NOT rejected here. + * ------------------------------------------------------- + */ + + let recommended: + EvaluatedThreshold | undefined; + + for (const sigmaMultiplier of candidates) { + const evaluation = + evaluateThreshold( + values, + analysisRows, + analysisCols, + noiseLevel, + sigmaMultiplier, + contourRatio, + persistenceLevels, + ridgeCoverageThreshold, + ); + + const passed = + evaluation.fdr <= maxFdr && + evaluation.occupancy <= maxOccupancy && + evaluation.persistence >= minPersistence; + + if (passed) { + recommended = evaluation; + break; + } + } + + /* + * Fallback when no candidate satisfies all conditions. + */ + if (!recommended) { + recommended = + evaluateFallbackThreshold( + values, + analysisRows, + analysisCols, + noiseLevel, + candidates, + contourRatio, + persistenceLevels, + ridgeCoverageThreshold, + maxFdr, + maxOccupancy, + minPersistence, + ); + } + + /* + * ------------------------------------------------------- + * STEP 2 + * + * Find the first threshold >= recommended where the + * directional ridge disappears. + * ------------------------------------------------------- + */ + + const ridgeFreeLevel = + findRidgeFreeThresholdTopDown( + values, + analysisRows, + analysisCols, + noiseLevel, + recommended.sigmaMultiplier * + noiseLevel, + xMaxAbsoluteValue(matrix as Float64Array), + contourRatio, + maxVerticalRidgeScore, + maxHorizontalRidgeScore, + ridgeCoverageThreshold, + ); + + const ridgeFree = + evaluateThreshold( + values, + analysisRows, + analysisCols, + noiseLevel, + ridgeFreeLevel / noiseLevel, + contourRatio, + 1, + ridgeCoverageThreshold, + ); + + const result: AutoContourResult = { + minLevel: + recommended.sigmaMultiplier * + noiseLevel, + + minLevelWithoutT1Noise: + ridgeFreeLevel, + + sigmaMultiplier: + recommended.sigmaMultiplier, + + sigmaMultiplierWithoutT1Noise: + ridgeFreeLevel / noiseLevel, + }; + + if (diagnostics) { + result.diagnostics = { + recommended: { + sigmaMultiplier: + recommended.sigmaMultiplier, + + minLevel: + recommended.sigmaMultiplier * + noiseLevel, + + occupancy: + recommended.occupancy, + + fdr: + recommended.fdr, + + persistence: + recommended.persistence, + + verticalRidgeScore: + recommended.verticalRidgeScore, + + horizontalRidgeScore: + recommended.horizontalRidgeScore, + }, + + ridgeFree: { + sigmaMultiplier: + ridgeFree.sigmaMultiplier, + + minLevel: + ridgeFreeLevel, + + occupancy: + ridgeFree.occupancy, + + fdr: + ridgeFree.fdr, + + persistence: + ridgeFree.persistence, + + verticalRidgeScore: + ridgeFree.verticalRidgeScore, + + horizontalRidgeScore: + ridgeFree.horizontalRidgeScore, + }, + + hasT1Noise: + recommended.verticalRidgeScore > + maxVerticalRidgeScore || + recommended.horizontalRidgeScore > + maxHorizontalRidgeScore, + }; + } + + return result; +} + +function evaluateFallbackThreshold( + matrix: Float64Array, + rows: number, + cols: number, + noiseLevel: number, + candidates: number[], + contourRatio: number, + persistenceLevels: number, + ridgeCoverageThreshold: number, + maxFdr: number, + maxOccupancy: number, + minPersistence: number, +): EvaluatedThreshold { + let best: + EvaluatedThreshold | undefined; + + let bestScore = Infinity; + + for (const sigma of candidates) { + const evaluation = + evaluateThreshold( + matrix, + rows, + cols, + noiseLevel, + sigma, + contourRatio, + persistenceLevels, + ridgeCoverageThreshold, + ); + + const score = + violation( + evaluation.fdr, + maxFdr, + ) * 4 + + + violation( + evaluation.occupancy, + maxOccupancy, + ) * 2 + + + violation( + minPersistence, + evaluation.persistence, + ) * 2; + + if (score < bestScore) { + bestScore = score; + best = evaluation; + } + } + + if (!best) { + throw new Error( + 'Unable to evaluate contour thresholds', + ); + } + + return best; +} + +function findRidgeFreeThreshold( + matrix: Float64Array, + rows: number, + cols: number, + noiseLevel: number, + startingSigma: number, + maxSigma: number, + contourRatio: number, + maxVerticalRidgeScore: number, + maxHorizontalRidgeScore: number, + ridgeCoverageThreshold: number, +): EvaluatedThreshold { + /* + * Start at the recommended level. + */ + let sigma = startingSigma; + + /* + * We move through the same exponential contour hierarchy + * used by the visualization. + * + * This is important because the returned threshold corresponds + * naturally to the contour system. + */ + while (sigma <= maxSigma) { + const evaluation = + evaluateThreshold( + matrix, + rows, + cols, + noiseLevel, + sigma, + contourRatio, + 1, + ridgeCoverageThreshold, + ); + + const ridgeFree = + evaluation.verticalRidgeScore <= + maxVerticalRidgeScore && + evaluation.horizontalRidgeScore <= + maxHorizontalRidgeScore; + + if (ridgeFree) { + return evaluation; + } + + sigma *= contourRatio; + } + + /* + * No ridge-free threshold was found within maxSigma. + * + * Return the highest tested threshold. + */ + return evaluateThreshold( + matrix, + rows, + cols, + noiseLevel, + Math.min(sigma, maxSigma), + contourRatio, + 1, + ridgeCoverageThreshold, + ); +} + +function findRidgeFreeThresholdTopDown( + matrix: Float64Array, + rows: number, + cols: number, + noiseLevel: number, + minLevel: number, + maxLevel: number, + contourRatio: number, + maxVerticalRidgeScore: number, + maxHorizontalRidgeScore: number, + ridgeCoverageThreshold: number, + ridgePersistenceLevels = 2, +): number { + /* + * Start from the actual maximum contour level. + */ + let level = maxLevel; + + let lastCleanLevel = level; + + let consecutiveRidgeLevels = 0; + + while (level >= minLevel) { + const evaluation = + evaluateThreshold( + matrix, + rows, + cols, + noiseLevel, + level / noiseLevel, + contourRatio, + 1, + ridgeCoverageThreshold, + ); + + const hasRidge = + evaluation.verticalRidgeScore > + maxVerticalRidgeScore || + evaluation.horizontalRidgeScore > + maxHorizontalRidgeScore; + + if (hasRidge) { + consecutiveRidgeLevels++; + + if ( + consecutiveRidgeLevels >= + ridgePersistenceLevels + ) { + /* + * The ridge has appeared persistently. + * + * The previous clean level is the answer. + */ + return lastCleanLevel; + } + } else { + consecutiveRidgeLevels = 0; + + lastCleanLevel = level; + } + + level /= contourRatio; + } + + return lastCleanLevel; +} +function generateCandidateSigmaLevels( + minSigma: number, + maxSigma: number, + contourRatio: number, + exponent: number, +): number[] { + const step = + contourRatio ** exponent; + + const candidates: number[] = []; + + let sigma = minSigma; + + while ( + sigma <= + maxSigma * (1 + 1e-12) + ) { + candidates.push(sigma); + sigma *= step; + } + + return candidates; +} + + +function computePersistence( + matrix: Float64Array, + rows: number, + cols: number, + initialThreshold: number, + contourRatio: number, + levels: number, +): number { + let threshold = initialThreshold; + + let previousArea = + countValuesAboveThreshold( + matrix, + threshold, + ); + + if (previousArea === 0) { + return 0; + } + + let minimumPersistence = 1; + + for (let level = 1; level < levels; level++) { + threshold *= contourRatio; + + const currentArea = + countValuesAboveThreshold( + matrix, + threshold, + ); + + if (currentArea === 0) { + return 0; + } + + const persistence = + currentArea / previousArea; + + minimumPersistence = Math.min( + minimumPersistence, + persistence, + ); + + previousArea = currentArea; + } + + return minimumPersistence; +} + +interface RidgeScores { + vertical: number; + horizontal: number; +} + +function computeRidgeScores( + mask: Uint8Array, + rows: number, + cols: number, + coverageThreshold: number, +): RidgeScores { + const rowCounts = new Uint32Array(rows); + const colCounts = new Uint32Array(cols); + + let activePixels = 0; + + for (let row = 0; row < rows; row++) { + for (let col = 0; col < cols; col++) { + const index = row * cols + col; + + if (!mask[index]) { + continue; + } + + activePixels++; + + rowCounts[row]++; + colCounts[col]++; + } + } + + if (activePixels === 0) { + return { + vertical: 0, + horizontal: 0, + }; + } + + /* + * A vertical ridge occupies a large fraction of F1 + * in one or more F2 columns. + */ + let verticalRidgePixels = 0; + + for (let col = 0; col < cols; col++) { + const coverage = + colCounts[col] / rows; + + if (coverage >= coverageThreshold) { + verticalRidgePixels += + colCounts[col]; + } + } + + /* + * A horizontal ridge occupies a large fraction of F2 + * in one or more F1 rows. + */ + let horizontalRidgePixels = 0; + + for (let row = 0; row < rows; row++) { + const coverage = + rowCounts[row] / cols; + + if (coverage >= coverageThreshold) { + horizontalRidgePixels += + rowCounts[row]; + } + } + + return { + vertical: + verticalRidgePixels / activePixels, + + horizontal: + horizontalRidgePixels / activePixels, + }; +} + +function createMask( + matrix: Float64Array, + threshold: number, +): Uint8Array { + const mask = new Uint8Array(matrix.length); + + for (let i = 0; i < matrix.length; i++) { + if (matrix[i] >= threshold) { + mask[i] = 1; + } + } + + return mask; +} + +interface AnalysisMatrix { + values: Float64Array; + rows: number; + cols: number; +} + +function maxPoolAbsolute( + matrix: ArrayLike, + rows: number, + cols: number, + maxDimension: number, +): AnalysisMatrix { + const scale = + Math.max( + 1, + Math.ceil( + Math.max(rows, cols) / + maxDimension, + ), + ); + + const analysisRows = + Math.ceil(rows / scale); + + const analysisCols = + Math.ceil(cols / scale); + + const values = + new Float64Array( + analysisRows * analysisCols, + ); + + for ( + let analysisRow = 0; + analysisRow < analysisRows; + analysisRow++ + ) { + const sourceRowStart = + analysisRow * scale; + + const sourceRowEnd = + Math.min( + rows, + sourceRowStart + scale, + ); + + for ( + let analysisCol = 0; + analysisCol < analysisCols; + analysisCol++ + ) { + const sourceColStart = + analysisCol * scale; + + const sourceColEnd = + Math.min( + cols, + sourceColStart + scale, + ); + + let maximum = 0; + + for ( + let row = sourceRowStart; + row < sourceRowEnd; + row++ + ) { + const offset = row * cols; + + for ( + let col = sourceColStart; + col < sourceColEnd; + col++ + ) { + const value = + Math.abs(matrix[offset + col]); + + if ( + Number.isFinite(value) && + value > maximum + ) { + maximum = value; + } + } + } + + values[ + analysisRow * analysisCols + + analysisCol + ] = maximum; + } + } + + return { + values, + rows: analysisRows, + cols: analysisCols, + }; +} + +function countActivePixels( + mask: Uint8Array, +): number { + let count = 0; + + for (let i = 0; i < mask.length; i++) { + count += mask[i]; + } + + return count; +} + +function countValuesAboveThreshold( + matrix: Float64Array, + threshold: number, +): number { + let count = 0; + + for (let i = 0; i < matrix.length; i++) { + if (matrix[i] >= threshold) { + count++; + } + } + + return count; +} + +function findMaximumNormalizedValue( + matrix: Float64Array, + noiseLevel: number, +): number { + let maximum = 0; + + for (let i = 0; i < matrix.length; i++) { + const value = + matrix[i] / noiseLevel; + + if (value > maximum) { + maximum = value; + } + } + + return maximum; +} + +function gaussianTwoSidedTail( + k: number, +): number { + return erfc(k / Math.SQRT2); +} + +/** + * Approximation of the complementary error function. + */ +function erfc(x: number): number { + const sign = x < 0 ? -1 : 1; + const ax = Math.abs(x); + + const p = 0.3275911; + + const a1 = 0.254829592; + const a2 = -0.284496736; + const a3 = 1.421413741; + const a4 = -1.453152027; + const a5 = 1.061405429; + + const t = 1 / (1 + p * ax); + + const polynomial = + (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * + t; + + const result = + polynomial * Math.exp(-ax * ax); + + return sign >= 0 + ? result + : 2 - result; +} + +function violation( + value: number, + limit: number, +): number { + if (limit === 0) { + return value === 0 ? 0 : 1; + } + + return Math.max( + 0, + value / limit - 1, + ); +} + +function validateInput( + matrix: ArrayLike, + rows: number, + cols: number, + noiseLevel: number, +): void { + if (!Number.isInteger(rows) || rows <= 0) { + throw new Error( + 'rows must be a positive integer', + ); + } + + if (!Number.isInteger(cols) || cols <= 0) { + throw new Error( + 'cols must be a positive integer', + ); + } + + if (matrix.length !== rows * cols) { + throw new Error( + 'matrix.length must equal rows * cols', + ); + } + + if ( + !Number.isFinite(noiseLevel) || + noiseLevel <= 0 + ) { + throw new Error( + 'noiseLevel must be a positive finite number', + ); + } +} \ No newline at end of file diff --git a/src/data/data2d/Spectrum2D/getContourThresholdFromPercentiles.ts b/src/data/data2d/Spectrum2D/getContourThresholdFromPercentiles.ts new file mode 100644 index 0000000000..c3e0cd1bda --- /dev/null +++ b/src/data/data2d/Spectrum2D/getContourThresholdFromPercentiles.ts @@ -0,0 +1,102 @@ +export function getContourThresholdFromPercentiles( + positivePercentiles: readonly number[], + negativePercentiles: readonly number[], + options: ContourThresholdOptions = {}, +): number { + const positiveContourLevel = findOptimalContourThreshold( + positivePercentiles, + options, + ); + const negativeContourLevel = findOptimalContourThreshold( + negativePercentiles, + options, + ); + + const positiveThreshold = + positivePercentiles[positiveContourLevel.optimalPercentile]; + const negativeThreshold = + negativePercentiles[negativeContourLevel.optimalPercentile]; + + return Math.max(positiveThreshold, negativeThreshold); +} +/** + * Finds the optimal minimum contour threshold for a 2D NMR spectrum + * using the Robust Median-MAD formulation on a percentile-intensity array. + * + * @param {number[]} percentiles - Array where index = percentile (0-100), + * value = intensity at that percentile. + * @param {object} options - Configuration options + * @returns {object} - { optimalPercentile, optimalThreshold, maxSNR } + */ +interface ContourThresholdOptions { + minP?: number; + maxP?: number; + madScale?: number; + scoreRatio?: number; +} + +interface OptimalContourMinLevel { + optimalPercentile: number; + optimalThreshold: number; + maxSNR: number; +} + +function interpolate(arr: readonly number[], idx: number): number { + const i = Math.floor(idx); + const j = Math.ceil(idx); + if (i === j || i < 0 || j >= arr.length) { + return arr[Math.max(0, Math.min(arr.length - 1, Math.round(idx)))]; + } + return arr[i] + (idx - i) * (arr[j] - arr[i]); +} + +function findOptimalContourThreshold( + percentiles: readonly number[], + options: ContourThresholdOptions = {}, +): OptimalContourMinLevel { + const { minP = 80, maxP = 99, madScale = 1 } = options; + + if (madScale <= 0) { + throw new Error('madScale must be > 0 to avoid division by zero.'); + } + + // Linear interpolation for non-integer percentile indices + + let bestP = minP; + let bestSNR = -Infinity; + + for (let p = minP; p <= maxP; p++) { + const idxMedian = p / 2; + const idxQ1 = p / 4; + const idxQ3 = (3 * p) / 4; + const idxMeanAbove = (p + 100) / 2; + + const medianBelow = interpolate(percentiles, idxMedian); + const q1Below = interpolate(percentiles, idxQ1); + const q3Below = interpolate(percentiles, idxQ3); + const meanAbove = interpolate(percentiles, idxMeanAbove); + + const madBelow = (q3Below - q1Below) / 2; + if (madBelow <= 0) continue; + + const snr = (meanAbove - medianBelow) / (madBelow * madScale); + + // ✦ Coverage weight: fraction of points ABOVE the threshold + // At p=80 → weight=0.20, at p=99 → weight=0.01 + // This penalizes thresholds that exclude too much. + const coverage = (100 - p) / 100; + + // Optional: sharpen the penalty with an exponent + const score = snr * coverage ** 0.5; // sqrt softens it + + if (score > bestSNR) { + bestSNR = score; + bestP = p; + } + } + return { + optimalPercentile: bestP, + optimalThreshold: interpolate(percentiles, bestP), + maxSNR: bestSNR, + }; +} \ No newline at end of file From f790c827116a3414e4ca75d4e92a460d20d5100c Mon Sep 17 00:00:00 2001 From: jobo322 Date: Tue, 18 Aug 2026 23:35:11 -0500 Subject: [PATCH 07/20] chore: keep only one function to determine the min contour level --- .../data2d/Spectrum2D/autoContourLevel.ts | 382 ------------------ src/data/data2d/Spectrum2D/contours.ts | 77 +--- .../data2d/Spectrum2D/findBestMinContour.ts | 280 +++++-------- .../getContourThresholdFromPercentiles.ts | 102 ----- 4 files changed, 116 insertions(+), 725 deletions(-) delete mode 100644 src/data/data2d/Spectrum2D/autoContourLevel.ts delete mode 100644 src/data/data2d/Spectrum2D/getContourThresholdFromPercentiles.ts diff --git a/src/data/data2d/Spectrum2D/autoContourLevel.ts b/src/data/data2d/Spectrum2D/autoContourLevel.ts deleted file mode 100644 index 6bee7c028b..0000000000 --- a/src/data/data2d/Spectrum2D/autoContourLevel.ts +++ /dev/null @@ -1,382 +0,0 @@ -/** - * autoContourLevel.ts - * - * Automatic minimum-contour-level selection for 2D NMR spectra. - * - * Algorithm: Signal Purity Threshold (SPT) - * ───────────────────────────────────────── - * Finds the smallest absolute-intensity threshold L such that the - * fraction of pixels above L that are *genuine signal* (not Gaussian - * noise) meets a configurable target (default 50 %). - * - * The threshold is symmetric: use +L_min for positive contours and - * −L_min for negative contours. - */ - -// ─── Math utilities ──────────────────────────────────────────────────────── - -/** - * Complementary error function erfc(x) = 1 − erf(x). - * - * Rational approximation from Abramowitz & Stegun §7.1.26. - * Max absolute error: |ε| < 1.5 × 10⁻⁷ for all real x. - * - * For Gaussian X ~ N(0, σ²): P(|X| > L) = erfc(L / (σ√2)) - */ -export function erfc(x: number): number { - if (x < 0) return 2 - erfc(-x); - const t = 1 / (1 + 0.3275911 * x); - return ( - t * - (0.254829592 + - t * - (-0.284496736 + - t * (1.421413741 + t * (-1.453152027 + t * 1.061405429)))) * - Math.exp(-x * x) - ); -} - -// ─── Types ───────────────────────────────────────────────────────────────── - -export interface AutoContourOptions { - /** - * Desired fraction of pixels above L_min that are genuine signal. - * - * Range: (0, 1). Default: 0.5. - * - * Interpretation: - * 0.5 → transition point (half noise, half signal above L_min). - * Good balance: weak peaks are included, modest noise. - * 0.7 → cleaner display; may hide the weakest peaks. - * 0.9 → very clean; only use when weak-peak visibility is not critical. - * - * This is the most impactful parameter to expose to the user. - */ - signalPurityTarget?: number; - - /** - * Hard lower bound: L_min is always ≥ this × noiseSigma, - * even if purity is already satisfied at a lower threshold. - * - * Prevents accidental noise display from σ mis-estimation. - * Default: 3.0 - */ - minimumNoiseMultiple?: number; - - /** - * Fallback multiple of σ used when the algorithm cannot find a - * data-driven threshold (very noisy, near-empty, or poor-SNR spectra). - * - * Default: 8.0 (conservative; produces few or no contours when - * no real signal is detectable, which is the correct visual output). - */ - fallbackNoiseMultiple?: number; - - /** - * Upper limit of the search range in multiples of noiseSigma. - * Increase for spectra with extreme dynamic range (> 10 000 : 1). - * Default: 30.0 - */ - searchMaxMultiple?: number; - - /** - * Minimum number of pixels that must lie above a candidate L_min - * for it to be considered valid. - * - * Prevents choosing a threshold where only 1–2 outlier pixels qualify, - * which would give an unreliable purity estimate. - * Default: 10 - */ - minimumPixelCount?: number; - - /** - * Number of log-spaced candidates evaluated during the sweep. - * Higher → finer resolution in the L axis; negligible speed cost. - * Default: 300 - */ - searchSteps?: number; -} - -export interface AutoContourResult { - /** - * Recommended minimum contour level (absolute intensity). - * Use +minLevel for positive contours, −minLevel for negative ones. - */ - minLevel: number; - - /** minLevel / noiseSigma for easy interpretation and logging. */ - noiseMultiple: number; - - /** - * True → a data-driven threshold was found in the spectrum. - * False → the algorithm fell back to fallbackNoiseMultiple × σ - * (noisy, sparse, or near-empty spectrum). - */ - dataAdaptive: boolean; - - /** - * Estimated fraction of pixels above minLevel that are genuine signal - * rather than Gaussian noise. Range [0, 1]. - * - * Useful for diagnostics and for surfacing confidence to the user - * ("threshold at 5.2 σ — 78 % signal purity"). - */ - signalPurityAtThreshold: number; -} - -// ─── Main algorithm ──────────────────────────────────────────────────────── - -/** - * Automatically select the minimum contour level for a 2D NMR spectrum. - * - * ## How it works - * - * 1. Sort absolute intensities once. - * 2. For each candidate threshold L (log-spaced from 3 σ to 30 σ): - * actual = number of pixels with |I| > L (binary search) - * noise = N · erfc(L / (σ√2)) (theoretical) - * purity = (actual − noise) / actual - * 3. Return the smallest L where purity ≥ target AND actual ≥ N_min. - * 4. If no such L exists, return fallbackNoiseMultiple × σ. - * - * ## Complexity - * Sorting: O(N log N) — once. - * Sweep: O(steps · log N) ≈ O(300 · 20) = O(6000) — negligible. - * - * @param intensities Flattened spectrum matrix (real-valued, row-major). - * Positive and negative values are both handled. - * @param noiseSigma Estimated noise standard deviation (must be > 0). - * Typically measured in a spectral region known to - * contain no peaks. - * @param options Algorithm tuning parameters (all optional). - */ -export function computeMinContourLevel( - intensities: ArrayLike, - noiseSigma: number, - options: AutoContourOptions = {}, -): AutoContourResult { - const { - signalPurityTarget = 0.99, - minimumNoiseMultiple = 5, - fallbackNoiseMultiple = 8, - searchMaxMultiple = 50, - minimumPixelCount = Math.floor(intensities.length * 0.005), - searchSteps = 300, - } = options; - - // ── Guard: return fallback for degenerate inputs ───────────────────────── - const fallback: AutoContourResult = { - minLevel: fallbackNoiseMultiple * noiseSigma, - noiseMultiple: fallbackNoiseMultiple, - dataAdaptive: false, - signalPurityAtThreshold: 0, - }; - - const N = intensities.length; - - if (N === 0 || !(noiseSigma > 0) || !Number.isFinite(noiseSigma)) return fallback; - - // ── 1. Compute and sort absolute intensities ───────────────────────────── - const absArr = new Float64Array(N); - for (let i = 0; i < N; i++) { - absArr[i] = Math.abs(intensities[i]); - } - absArr.sort(); // ascending; in-place, O(N log N) - - // ── 2. O(log N) count of pixels strictly above a threshold ─────────────── - function countAbove(threshold: number): number { - let lo = 0; - let hi = N; - while (lo < hi) { - const mid = (lo + hi) >>> 1; - if (absArr[mid] <= threshold) { - lo = mid + 1 - } else { - hi = mid - }; - } - return N - lo; - } - - // ── 3. Theoretical noise count above threshold ─────────────────────────── - // For X ~ N(0, σ²): P(|X| > L) = erfc(L / (σ√2)) - const SQRT2 = Math.SQRT2; - function noiseCountAbove(L: number): number { - return N * erfc(L / (noiseSigma * SQRT2)); - } - - // ── 4. Log sweep from floor to ceiling ─────────────────────────────────── - // - // Purity condition: - // R(L) = (actual − noise) / actual ≥ target - // ⟺ actual ≥ noise / (1 − target) [since actual > 0] - // - // Sweep low → high; break at the first L satisfying the condition. - // This gives the *minimum* threshold where signal dominates. - - const Lfloor = minimumNoiseMultiple * noiseSigma; - const Lceil = searchMaxMultiple * noiseSigma; - const logLo = Math.log(Lfloor); - const logHi = Math.log(Lceil); - const purityScale = 1 / (1 - signalPurityTarget); // e.g. 2.0 for target 0.5 - - for (let step = 0; step <= searchSteps; step++) { - const L = Math.exp(logLo + (step / searchSteps) * (logHi - logLo)); - const actual = countAbove(L); - - // Skip if too few pixels above this threshold (unreliable purity estimate). - if (actual < minimumPixelCount) continue; - - const noise = noiseCountAbove(L); - if (actual >= purityScale * noise) { - // Purity condition met. Clamp to [0,1] to handle small σ errors. - const signalPurityAtThreshold = Math.min( - 1, - Math.max(0, (actual - noise) / actual), - ); - return { - minLevel: L, - noiseMultiple: L / noiseSigma, - dataAdaptive: true, - signalPurityAtThreshold, - }; - } - } - - // No threshold found: spectrum is too noisy or too sparse. - return fallback; -} - -// ─── Companion utilities ─────────────────────────────────────────────────── - -/** - * Compute a robust maximum contour level from the spectrum. - * - * Uses a high percentile of |intensities| (default: 99.9th) to avoid - * a single outlier pixel inflating L_max and compressing all contours - * into the bottom of the range. - * - * @param intensities Flattened spectrum matrix. - * @param percentile Quantile in (0, 1). Default: 0.999. - */ -export function computeMaxContourLevel( - intensities: ArrayLike, - percentile = 0.999, -): number { - const N = intensities.length; - if (N === 0) return 1; - - const absArr = new Float64Array(N); - for (let i = 0; i < N; i++) { - absArr[i] = Math.abs(intensities[i]); - } - absArr.sort(); - - const idx = Math.min(Math.floor(percentile * N), N - 1); - return absArr[idx]; -} - -// ─── Exponential contour level builder ──────────────────────────────────── - -export interface ContourLevels { - /** Positive contour levels, ascending. */ - positive: number[]; - /** Negative contour levels (mirrors of positive, descending). */ - negative: number[]; - /** The exponential ratio r used: L_{n+1} = L_n · r */ - ratio: number; -} - -export interface BuildContourOptions { - /** Minimum contour level (from computeMinContourLevel). */ - minLevel: number; - /** Maximum contour level (from computeMaxContourLevel or user override). */ - maxLevel: number; - /** - * Number of positive levels. Ignored if `ratio` is provided. - * Default: 12. - */ - nLevels?: number; - /** - * Fixed exponential ratio r. If provided, nLevels is derived as - * floor(log(maxLevel / minLevel) / log(r)) + 1 - * so the levels span the full [minLevel, maxLevel] range. - */ - ratio?: number; -} - -/** - * Build symmetric, exponentially spaced contour levels. - * - * Positive levels: [L_min, L_min·r, L_min·r², …, L_max] - * Negative levels: mirror of positive, negated. - * - * This is the function to call after `computeMinContourLevel` and - * `computeMaxContourLevel`. - */ -export function buildContourLevels(opts: BuildContourOptions): ContourLevels { - const { minLevel, maxLevel, nLevels = 12 } = opts; - - if (!(minLevel > 0)) throw new RangeError('minLevel must be positive'); - if (!(maxLevel > minLevel)) { - throw new RangeError('maxLevel must be greater than minLevel'); - } - - let ratio: number; - let n: number; - - if (opts.ratio !== undefined) { - ratio = opts.ratio; - if (!(ratio > 1)) throw new RangeError('ratio must be > 1'); - n = Math.max( - 1, - Math.floor(Math.log(maxLevel / minLevel) / Math.log(ratio)) + 1, - ); - } else { - n = Math.max(2, nLevels); - ratio = (maxLevel / minLevel) ** (1 / (n - 1)); - } - - const positive = Array.from({ length: n }, (_, i) => minLevel * ratio ** i); - return { - positive, - negative: [...positive].toReversed().map((v) => -v), - ratio, - }; -} - -// ─── End-to-end usage example ────────────────────────────────────────────── - -/* -import { computeMinContourLevel, computeMaxContourLevel, buildContourLevels } from './autoContourLevel'; - -// spectrum: your Float64Array (rows × cols, flattened) -// noiseSigma: from your existing noise estimation - -const minResult = computeMinContourLevel(spectrum, noiseSigma, { - signalPurityTarget: 0.5, // expose to user as "noise cutoff sensitivity" -}); - -const maxLevel = computeMaxContourLevel(spectrum, 0.999); - -const { positive, negative, ratio } = buildContourLevels({ - minLevel: minResult.minLevel, - maxLevel, - nLevels: 12, // expose to user -}); - -// Log for diagnostics: -console.log( - `L_min = ${minResult.minLevel.toFixed(2)} ` + - `(${minResult.noiseMultiple.toFixed(1)} σ, ` + - `purity = ${(minResult.signalPurityAtThreshold * 100).toFixed(0)} %, ` + - `adaptive = ${minResult.dataAdaptive})` -); -console.log(`ratio r = ${ratio.toFixed(3)}, ${positive.length} levels`); - -// Pass `positive` and `negative` to your contour renderer. -// Initial user controls: -// L_min slider → rerun buildContourLevels with minLevel = slider value -// L_max slider → rerun with maxLevel = slider value -// nLevels → rerun with nLevels = new value -*/ diff --git a/src/data/data2d/Spectrum2D/contours.ts b/src/data/data2d/Spectrum2D/contours.ts index 15cac8a920..150ae78e3e 100644 --- a/src/data/data2d/Spectrum2D/contours.ts +++ b/src/data/data2d/Spectrum2D/contours.ts @@ -1,17 +1,13 @@ import type { Spectrum2D, Spectrum } from '@zakodium/nmrium-core'; import { isSpectrum2DFt } from '@zakodium/nmrium-core'; -import type { DataXY, NmrData2DFt } from 'cheminfo-types'; +import type { NmrData2DFt } from 'cheminfo-types'; import { Conrec } from 'ml-conrec'; -import { matrixMaxAbsoluteZ, matrixMinMaxZ, matrixToArray } from 'ml-spectra-processing'; -import type { Spectrum } from 'nmr-correlation'; +import { matrixMaxAbsoluteZ, matrixToArray } from 'ml-spectra-processing'; import type { SpectrumFTData } from '../../../component/hooks/use2DReducer.tsx'; import { calculateSanPlot } from '../../utilities/calculateSanPlot.js'; -import { computeMinContourLevel } from './autoContourLevel.ts'; -import { NMRContourCalculator } from './countourLevelFinder.ts'; import { findAutomaticContourLevels } from './findBestMinContour.ts'; -import { getContourThresholdFromPercentiles } from './getContourThresholdFromPercentiles.ts'; interface Level { positive: ContourItem; @@ -59,86 +55,44 @@ interface ContoursManagerReturn { } function getDefaultContoursLevel(spectrum: Spectrum2D, quadrant = 'rr') { - const { data, info, filters } = spectrum; + const { data, info } = spectrum; // @ts-expect-error type of NmrData2D should have a discriminator field to separate fid and ft const quadrantData = data[quadrant]; const { acquisitionScheme } = info; - //@ts-expect-error will be included in nexts versions + const { experiment = '', + //@ts-expect-error will be included in nexts versions noise = calculateSanPlot('2D', quadrantData, { magnitudeMode: acquisitionScheme === 'notPhaseSensitive', }), } = info; - const { percentiles, sanplot } = noise; + const max = matrixMaxAbsoluteZ(quadrantData.z); - const max = matrixMaxAbsoluteZ(quadrantData.z) // Math.max(positiveSanPlotMax, negativeSanPlotMax);// - - const isSymmetrized = filters.some( - (filter) => filter.name === 'symmetrizeCosyLike' && filter.enabled, - ); - const isNUS = filters.some( - (filter) => filter.name === 'nusDimension2' && filter.enabled, - ); - - const { positive: pPositive, negative: pNegative } = percentiles; - - const percentileValue = isSymmetrized ? (isNUS ? 60 : 70) : 99; - const pPositiveValue = pPositive[percentileValue]; - const pNegativeValue = pNegative[percentileValue]; - - const optimalContourLevel = getContourThresholdFromPercentiles( - pPositive, - pNegative, - { - minP: 80, - maxP: 99, - madScale: 1, - }, - ); - // console.log(matrixToArray(quadrantData.z).length, matrixMinMaxZ(quadrantData.z), matrixMaxAbsoluteZ(quadrantData.z), max, sanplot, 'length of data') - // const contourLevelProposal = computeMinContourLevel( - // matrixToArray(quadrantData.z), - // Math.max(noise.positive, noise.negative), - // { - // signalPurityTarget: 0.998, - // searchMaxMultiple: 1000, - // } - // ); - - const newProposarGemma = NMRContourCalculator.calculateInitialMinLevel(matrixToArray(quadrantData.z), quadrantData.z.length, - quadrantData.z[0].length, { contourRatio: 1.5, noiseSigma: Math.max(noise.positive, noise.negative), targetSignalLevels: 10, signalPercentile: 0.999 }); const bestMinLevel = findAutomaticContourLevels( matrixToArray(quadrantData.z), quadrantData.z.length, quadrantData.z[0].length, - Math.max(noise.poistive, noise.negative), + Math.max(noise.positive, noise.negative), { + maxFdr: 0.05, + maxOccupancy: 0.03, persistenceLevels: 10, - ridgeCoverageThreshold: experiment.includes('jres') ? 0.8 : 0.1 - } + contourRatio: 1.6, + maxVerticalRidgeScore: 0.1, + maxHorizontalRidgeScore: 0.1, + ridgeCoverageThreshold: experiment.includes('jres') ? 0.8 : 0.1, + }, ); - console.log(newProposarGemma, 'newProposarGemma'); - console.log(contourLevelProposal, 'contourLevelProposal'); - console.log(bestMinLevel, 'bestMinLevel'); - // const minAllowedByPercentile = Math.max( - // pPositiveValue ?? 0, - // pNegativeValue ?? 0, - // ); - - // const minLevel = - // isSymmetrized || isNUS - // ? Math.min(optimalContourLevel, minAllowedByPercentile) - // : Math.max(optimalContourLevel, minAllowedByPercentile); + const minContourLevel = Math.min( calculateValueOfLevel(bestMinLevel.minLevelWithoutT1Noise, max, true), DEFAULT_CONTOURS_OPTIONS.positive.contourLevels[1] - DEFAULT_CONTOURS_OPTIONS.positive.numberOfLayers, ); - console.log(`minLevel`, minContourLevel, calculateValueOfLevel(contourLevelProposal.minLevel, max, true), calculateValueOfLevel(bestMinLevel.minLevelWithoutT1Noise, max, true)); const defaultLevel: ContourOptions = { negative: { numberOfLayers: DEFAULT_CONTOURS_OPTIONS.negative.numberOfLayers, @@ -159,7 +113,6 @@ function getDefaultContoursLevel(spectrum: Spectrum2D, quadrant = 'rr') { } - function contoursManager(options: ContourOptions): ContoursManagerReturn { const contourOptions = { ...options }; return { diff --git a/src/data/data2d/Spectrum2D/findBestMinContour.ts b/src/data/data2d/Spectrum2D/findBestMinContour.ts index ddbbf29f69..9a3cc975c7 100644 --- a/src/data/data2d/Spectrum2D/findBestMinContour.ts +++ b/src/data/data2d/Spectrum2D/findBestMinContour.ts @@ -1,4 +1,4 @@ -import { matrixMaxAbsoluteZ, xMaxAbsoluteValue } from "ml-spectra-processing"; +import { xMaxAbsoluteValue } from "ml-spectra-processing"; export interface AutoContourDiagnostics { recommended: ThresholdDiagnostics; @@ -172,17 +172,6 @@ export interface AutoContourOptions { */ minSigma?: number; - /** - * Highest threshold considered by the automatic contour search, - * expressed as a multiple of the supplied noise level. - * - * This limits the search range and prevents the algorithm from - * examining excessively high contour levels. - * - * @default 10 - */ - maxSigma?: number; - /** * Exponent used to determine the spacing between candidate * minimum contour levels. @@ -248,105 +237,7 @@ export interface AutoContourOptions { diagnostics?: boolean; } -interface EvaluatedThreshold { - sigmaMultiplier: number; - - activePixels: number; - occupancy: number; - fdr: number; - persistence: number; - - verticalRidgeScore: number; - horizontalRidgeScore: number; -} - -function evaluateThreshold( - matrix: Float64Array, - rows: number, - cols: number, - noiseLevel: number, - sigmaMultiplier: number, - contourRatio: number, - persistenceLevels: number, - ridgeCoverageThreshold: number, -): EvaluatedThreshold { - const threshold = - sigmaMultiplier * noiseLevel; - - const mask = - createMask( - matrix, - threshold, - ); - - const activePixels = - countActivePixels(mask); - - const totalPixels = - rows * cols; - - const occupancy = - activePixels / totalPixels; - - if (activePixels === 0) { - return { - sigmaMultiplier, - activePixels: 0, - occupancy: 0, - fdr: 0, - persistence: 0, - verticalRidgeScore: 0, - horizontalRidgeScore: 0, - }; - } - - const expectedNoisePixels = - totalPixels * - gaussianTwoSidedTail( - sigmaMultiplier, - ); - - const fdr = - Math.min( - 1, - expectedNoisePixels / - activePixels, - ); - - const persistence = - computePersistence( - matrix, - rows, - cols, - threshold, - contourRatio, - persistenceLevels, - ); - - const ridge = - computeRidgeScores( - mask, - rows, - cols, - ridgeCoverageThreshold, - ); - - return { - sigmaMultiplier, - - activePixels, - occupancy, - - fdr, - persistence, - - verticalRidgeScore: - ridge.vertical, - horizontalRidgeScore: - ridge.horizontal, - }; -} export function findAutomaticContourLevels( matrix: ArrayLike, @@ -375,7 +266,6 @@ export function findAutomaticContourLevels( ridgeCoverageThreshold = 0.5, minSigma = 5, - maxSigma = 1000, candidateRatioExponent = 0.5, @@ -413,7 +303,6 @@ export function findAutomaticContourLevels( values, noiseLevel, ); - console.log(maxSigmaInData, 'maxSigmaInData'); /* * Generate candidate minimum thresholds. */ @@ -594,6 +483,106 @@ export function findAutomaticContourLevels( return result; } +interface EvaluatedThreshold { + sigmaMultiplier: number; + + activePixels: number; + occupancy: number; + fdr: number; + persistence: number; + + verticalRidgeScore: number; + horizontalRidgeScore: number; +} + +function evaluateThreshold( + matrix: Float64Array, + rows: number, + cols: number, + noiseLevel: number, + sigmaMultiplier: number, + contourRatio: number, + persistenceLevels: number, + ridgeCoverageThreshold: number, +): EvaluatedThreshold { + const threshold = + sigmaMultiplier * noiseLevel; + + const mask = + createMask( + matrix, + threshold, + ); + + const activePixels = + countActivePixels(mask); + + const totalPixels = + rows * cols; + + const occupancy = + activePixels / totalPixels; + + if (activePixels === 0) { + return { + sigmaMultiplier, + activePixels: 0, + occupancy: 0, + fdr: 0, + persistence: 0, + verticalRidgeScore: 0, + horizontalRidgeScore: 0, + }; + } + + const expectedNoisePixels = + totalPixels * + gaussianTwoSidedTail( + sigmaMultiplier, + ); + + const fdr = + Math.min( + 1, + expectedNoisePixels / + activePixels, + ); + + const persistence = + computePersistence( + matrix, + rows, + cols, + threshold, + contourRatio, + persistenceLevels, + ); + + const ridge = + computeRidgeScores( + mask, + rows, + cols, + ridgeCoverageThreshold, + ); + + return { + sigmaMultiplier, + + activePixels, + occupancy, + + fdr, + persistence, + + verticalRidgeScore: + ridge.vertical, + + horizontalRidgeScore: + ridge.horizontal, + }; +} + function evaluateFallbackThreshold( matrix: Float64Array, rows: number, @@ -656,73 +645,6 @@ function evaluateFallbackThreshold( return best; } -function findRidgeFreeThreshold( - matrix: Float64Array, - rows: number, - cols: number, - noiseLevel: number, - startingSigma: number, - maxSigma: number, - contourRatio: number, - maxVerticalRidgeScore: number, - maxHorizontalRidgeScore: number, - ridgeCoverageThreshold: number, -): EvaluatedThreshold { - /* - * Start at the recommended level. - */ - let sigma = startingSigma; - - /* - * We move through the same exponential contour hierarchy - * used by the visualization. - * - * This is important because the returned threshold corresponds - * naturally to the contour system. - */ - while (sigma <= maxSigma) { - const evaluation = - evaluateThreshold( - matrix, - rows, - cols, - noiseLevel, - sigma, - contourRatio, - 1, - ridgeCoverageThreshold, - ); - - const ridgeFree = - evaluation.verticalRidgeScore <= - maxVerticalRidgeScore && - evaluation.horizontalRidgeScore <= - maxHorizontalRidgeScore; - - if (ridgeFree) { - return evaluation; - } - - sigma *= contourRatio; - } - - /* - * No ridge-free threshold was found within maxSigma. - * - * Return the highest tested threshold. - */ - return evaluateThreshold( - matrix, - rows, - cols, - noiseLevel, - Math.min(sigma, maxSigma), - contourRatio, - 1, - ridgeCoverageThreshold, - ); -} - function findRidgeFreeThresholdTopDown( matrix: Float64Array, rows: number, diff --git a/src/data/data2d/Spectrum2D/getContourThresholdFromPercentiles.ts b/src/data/data2d/Spectrum2D/getContourThresholdFromPercentiles.ts deleted file mode 100644 index c3e0cd1bda..0000000000 --- a/src/data/data2d/Spectrum2D/getContourThresholdFromPercentiles.ts +++ /dev/null @@ -1,102 +0,0 @@ -export function getContourThresholdFromPercentiles( - positivePercentiles: readonly number[], - negativePercentiles: readonly number[], - options: ContourThresholdOptions = {}, -): number { - const positiveContourLevel = findOptimalContourThreshold( - positivePercentiles, - options, - ); - const negativeContourLevel = findOptimalContourThreshold( - negativePercentiles, - options, - ); - - const positiveThreshold = - positivePercentiles[positiveContourLevel.optimalPercentile]; - const negativeThreshold = - negativePercentiles[negativeContourLevel.optimalPercentile]; - - return Math.max(positiveThreshold, negativeThreshold); -} -/** - * Finds the optimal minimum contour threshold for a 2D NMR spectrum - * using the Robust Median-MAD formulation on a percentile-intensity array. - * - * @param {number[]} percentiles - Array where index = percentile (0-100), - * value = intensity at that percentile. - * @param {object} options - Configuration options - * @returns {object} - { optimalPercentile, optimalThreshold, maxSNR } - */ -interface ContourThresholdOptions { - minP?: number; - maxP?: number; - madScale?: number; - scoreRatio?: number; -} - -interface OptimalContourMinLevel { - optimalPercentile: number; - optimalThreshold: number; - maxSNR: number; -} - -function interpolate(arr: readonly number[], idx: number): number { - const i = Math.floor(idx); - const j = Math.ceil(idx); - if (i === j || i < 0 || j >= arr.length) { - return arr[Math.max(0, Math.min(arr.length - 1, Math.round(idx)))]; - } - return arr[i] + (idx - i) * (arr[j] - arr[i]); -} - -function findOptimalContourThreshold( - percentiles: readonly number[], - options: ContourThresholdOptions = {}, -): OptimalContourMinLevel { - const { minP = 80, maxP = 99, madScale = 1 } = options; - - if (madScale <= 0) { - throw new Error('madScale must be > 0 to avoid division by zero.'); - } - - // Linear interpolation for non-integer percentile indices - - let bestP = minP; - let bestSNR = -Infinity; - - for (let p = minP; p <= maxP; p++) { - const idxMedian = p / 2; - const idxQ1 = p / 4; - const idxQ3 = (3 * p) / 4; - const idxMeanAbove = (p + 100) / 2; - - const medianBelow = interpolate(percentiles, idxMedian); - const q1Below = interpolate(percentiles, idxQ1); - const q3Below = interpolate(percentiles, idxQ3); - const meanAbove = interpolate(percentiles, idxMeanAbove); - - const madBelow = (q3Below - q1Below) / 2; - if (madBelow <= 0) continue; - - const snr = (meanAbove - medianBelow) / (madBelow * madScale); - - // ✦ Coverage weight: fraction of points ABOVE the threshold - // At p=80 → weight=0.20, at p=99 → weight=0.01 - // This penalizes thresholds that exclude too much. - const coverage = (100 - p) / 100; - - // Optional: sharpen the penalty with an exponent - const score = snr * coverage ** 0.5; // sqrt softens it - - if (score > bestSNR) { - bestSNR = score; - bestP = p; - } - } - return { - optimalPercentile: bestP, - optimalThreshold: interpolate(percentiles, bestP), - maxSNR: bestSNR, - }; -} \ No newline at end of file From 1969c8429479285528b721f0df3454338b13fe92 Mon Sep 17 00:00:00 2001 From: jobo322 Date: Wed, 19 Aug 2026 09:31:47 -0500 Subject: [PATCH 08/20] chore: update package-lock --- package-lock.json | 76 +++++++++++++++++++++++++++-------------------- 1 file changed, 44 insertions(+), 32 deletions(-) diff --git a/package-lock.json b/package-lock.json index 47ac9020f2..a9d4e27748 100644 --- a/package-lock.json +++ b/package-lock.json @@ -180,6 +180,7 @@ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -440,6 +441,7 @@ "resolved": "https://registry.npmjs.org/@blueprintjs/core/-/core-6.18.0.tgz", "integrity": "sha512-EQVKWl/RFrPHfD42Bcicvxqwjwh+OnNSroXOdmnJZC+FP1xlcawOjTl38ewgJEaCZ6txcqiw/CGAft7/8zJ4RQ==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@blueprintjs/colors": "^5.1.16", "@blueprintjs/icons": "^6.13.0", @@ -471,6 +473,7 @@ "resolved": "https://registry.npmjs.org/@blueprintjs/icons/-/icons-6.13.0.tgz", "integrity": "sha512-wEQgADFPwufKiKeF/L5K21bKgouuIbIdYPvMPFv2tt7reSuCEftX0dZpga+21JY4KvEJIz+xtVJoqMmXBesiwQ==", "license": "Apache-2.0", + "peer": true, "dependencies": { "change-case": "^4.1.2", "classnames": "^2.3.1", @@ -492,6 +495,7 @@ "resolved": "https://registry.npmjs.org/@blueprintjs/select/-/select-6.3.4.tgz", "integrity": "sha512-vJa6MrXgPyQt4PRc9Z6c1vaHAKJMI1zloVph5eKOfv7YjSb1WlbH/bFWKunANxqSlncfug6FfaHkIRh7UYZ1Wg==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@blueprintjs/colors": "^5.1.16", "@blueprintjs/core": "^6.18.0", @@ -546,6 +550,7 @@ "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@keyv/serialize": "^1.1.1" } @@ -704,6 +709,7 @@ "integrity": "sha512-kLgLShnWADDVreKC63pBrWkcvxgZzFIfO34Jhx/SWfuOIA3cD8AXT+HjyuLfoGJ7mUb58hv2kUziKzEy4INb1w==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=22.18.0" } @@ -785,7 +791,8 @@ "resolved": "https://registry.npmjs.org/@cspell/dict-css/-/dict-css-4.1.2.tgz", "integrity": "sha512-+ylGoKdwZ2sVOCOnU2Eq5wDZx+RaVX3HoKyNHGGsFvhSw6IidQ6tH/mAPKBDofViHJoWCPNlklE0lTr6MDG3QA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@cspell/dict-dart": { "version": "2.3.2", @@ -925,14 +932,16 @@ "resolved": "https://registry.npmjs.org/@cspell/dict-html/-/dict-html-4.0.15.tgz", "integrity": "sha512-GJYnYKoD9fmo2OI0aySEGZOjThnx3upSUvV7mmqUu8oG+mGgzqm82P/f7OqsuvTaInZZwZbo+PwJQd/yHcyFIw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@cspell/dict-html-symbol-entities": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@cspell/dict-html-symbol-entities/-/dict-html-symbol-entities-4.0.5.tgz", "integrity": "sha512-429alTD4cE0FIwpMucvSN35Ld87HCyuM8mF731KU5Rm4Je2SG6hmVx7nkBsLyrmH3sQukTcr1GaiZsiEg8svPA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@cspell/dict-java": { "version": "5.0.12", @@ -1130,7 +1139,8 @@ "resolved": "https://registry.npmjs.org/@cspell/dict-typescript/-/dict-typescript-3.2.3.tgz", "integrity": "sha512-zXh1wYsNljQZfWWdSPYwQhpwiuW0KPW1dSd8idjMRvSD0aSvWWHoWlrMsmZeRl4qM4QCEAjua8+cjflm41cQBg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@cspell/dict-vue": { "version": "3.0.5", @@ -1240,6 +1250,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" }, @@ -1288,6 +1299,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" } @@ -1368,31 +1380,6 @@ "integrity": "sha512-SlJDfG6RPeEX8wEVv6ZB3kak4MmbtyiI2qX/5zuKdordbrhB/iaJ58GVMZgJ6P1sJaM1gMgENFYYeg1JWrCFrA==", "license": "MIT" }, - "node_modules/@emnapi/core": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz", - "integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.3", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", - "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz", @@ -1400,7 +1387,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -1463,6 +1449,7 @@ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -2778,6 +2765,7 @@ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" @@ -3176,7 +3164,8 @@ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "devOptional": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@standard-schema/utils": { "version": "0.3.0", @@ -3477,6 +3466,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -3508,6 +3498,7 @@ "integrity": "sha512-p088eaGrzYz1s+7cov0aMOCkNGTJlVxF4jgubf28c8L0Cv9Rloj8YBHnv4hXLq6IIEE1AsjNWavO+k+8kP2Y0A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.66.0", @@ -3547,6 +3538,7 @@ "integrity": "sha512-X6ypGChaWYk6PBtUg2BwuTZEFFcHJAtGTVJ9/lCTOufhZ4i9fNolQNnktq+kkMCwMj7V8Svsq7+TxSDslmhE0g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.66.0", "@typescript-eslint/types": "8.66.0", @@ -3816,6 +3808,7 @@ "integrity": "sha512-8MVGEFnJIcdGjcbfKmeq8z0pZHH0JlVtoVZH9Q/qwUp6wyFnEJUBMrw9DCaj+ra3vShGmhavjalMIhPNxZAUcw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.1.11", @@ -4108,6 +4101,7 @@ "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4131,6 +4125,7 @@ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -4644,6 +4639,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.44", "caniuse-lite": "^1.0.30001806", @@ -5425,6 +5421,7 @@ "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" @@ -5574,6 +5571,7 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -6163,6 +6161,7 @@ "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -6999,6 +6998,7 @@ "resolved": "https://registry.npmjs.org/fifo-logger/-/fifo-logger-2.0.1.tgz", "integrity": "sha512-AwCaBK389hl67z4AJ5+8uOsxU07olw0DzowzA6Znr/eaItMCsXXxzA1DjY/KCABWu/4Bq+wrBhn1p7hsjNDv4g==", "license": "MIT", + "peer": true, "dependencies": { "typescript-event-target": "^1.1.1" } @@ -8501,6 +8501,7 @@ "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "jiti": "lib/jiti-cli.mjs" } @@ -10013,7 +10014,8 @@ "version": "9.25.0", "resolved": "https://registry.npmjs.org/openchemlib/-/openchemlib-9.25.0.tgz", "integrity": "sha512-FGTaZLJRTGXNC7khx8QvX/EiQBHpH1ncUbT7YXDJhw/Y/aDUKe5WrUIqTguMEtSs3GUHcRUjNUhfkpxulx2UXw==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/openchemlib-utils": { "version": "8.18.0", @@ -10444,6 +10446,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", @@ -10486,6 +10489,7 @@ "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -10766,6 +10770,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -10791,6 +10796,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -10876,6 +10882,7 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.85.0.tgz", "integrity": "sha512-U2MTriFXnclmV4rOE20p2DcRFv5WEg3FIcBFOKcOLFHDVvGIMPvLTkTWefUsonmlaVy23khVDxDWym6uJVGOzw==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -12708,6 +12715,7 @@ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12912,6 +12920,7 @@ "integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.5", @@ -13005,6 +13014,7 @@ "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.1.11", "@vitest/mocker": "4.1.11", @@ -13381,6 +13391,7 @@ "resolved": "https://registry.npmjs.org/yup/-/yup-1.7.1.tgz", "integrity": "sha512-GKHFX2nXul2/4Dtfxhozv701jLQHdf6J34YDh2cEkpqoo8le5Mg6/LrdseVLrFarmFygZTlfIhHx/QKfb/QWXw==", "license": "MIT", + "peer": true, "dependencies": { "property-expr": "^2.0.5", "tiny-case": "^1.0.3", @@ -13393,6 +13404,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } From 61e0c9ad102afa0841b3134668f30909df7ee736 Mon Sep 17 00:00:00 2001 From: jobo322 Date: Wed, 19 Aug 2026 12:02:49 -0500 Subject: [PATCH 09/20] feat: split and improve contour min level selection in less time --- src/data/data2d/Spectrum2D/contours.ts | 31 +- .../data2d/Spectrum2D/findBestMinContour.ts | 1108 ----------------- .../findBestMinContour/evaluation.ts | 383 ++++++ .../findAutomaticContourLevels.ts | 408 ++++++ .../Spectrum2D/findBestMinContour/math.ts | 59 + 5 files changed, 870 insertions(+), 1119 deletions(-) delete mode 100644 src/data/data2d/Spectrum2D/findBestMinContour.ts create mode 100644 src/data/data2d/Spectrum2D/findBestMinContour/evaluation.ts create mode 100644 src/data/data2d/Spectrum2D/findBestMinContour/findAutomaticContourLevels.ts create mode 100644 src/data/data2d/Spectrum2D/findBestMinContour/math.ts diff --git a/src/data/data2d/Spectrum2D/contours.ts b/src/data/data2d/Spectrum2D/contours.ts index 150ae78e3e..ba30004359 100644 --- a/src/data/data2d/Spectrum2D/contours.ts +++ b/src/data/data2d/Spectrum2D/contours.ts @@ -7,7 +7,7 @@ import { matrixMaxAbsoluteZ, matrixToArray } from 'ml-spectra-processing'; import type { SpectrumFTData } from '../../../component/hooks/use2DReducer.tsx'; import { calculateSanPlot } from '../../utilities/calculateSanPlot.js'; -import { findAutomaticContourLevels } from './findBestMinContour.ts'; +import { findAutomaticContourLevels } from './findBestMinContour/findAutomaticContourLevels.ts'; interface Level { positive: ContourItem; @@ -61,10 +61,10 @@ function getDefaultContoursLevel(spectrum: Spectrum2D, quadrant = 'rr') { const quadrantData = data[quadrant]; const { acquisitionScheme } = info; - + const { experiment = '', - //@ts-expect-error will be included in nexts versions + //@ts-expect-error will be included in nexts versions noise = calculateSanPlot('2D', quadrantData, { magnitudeMode: acquisitionScheme === 'notPhaseSensitive', }), @@ -78,18 +78,28 @@ function getDefaultContoursLevel(spectrum: Spectrum2D, quadrant = 'rr') { quadrantData.z[0].length, Math.max(noise.positive, noise.negative), { - maxFdr: 0.05, - maxOccupancy: 0.03, - persistenceLevels: 10, - contourRatio: 1.6, - maxVerticalRidgeScore: 0.1, - maxHorizontalRidgeScore: 0.1, + maxFdr: 0.01, + maxOccupancy: 0.01, + persistenceLevels: 5, + contourRatio: 1.8, + maxVerticalRidgeScore: 0.05, + maxHorizontalRidgeScore: 0.05, ridgeCoverageThreshold: experiment.includes('jres') ? 0.8 : 0.1, }, ); + const { + diagnostics: { hasT1Noise }, + minLevelWithoutT1Noise, + minLevel, + } = bestMinLevel; + console.log(hasT1Noise, minLevelWithoutT1Noise, minLevel); const minContourLevel = Math.min( - calculateValueOfLevel(bestMinLevel.minLevelWithoutT1Noise, max, true), + calculateValueOfLevel( + hasT1Noise ? minLevelWithoutT1Noise : minLevel, + max, + true, + ), DEFAULT_CONTOURS_OPTIONS.positive.contourLevels[1] - DEFAULT_CONTOURS_OPTIONS.positive.numberOfLayers, ); @@ -112,7 +122,6 @@ function getDefaultContoursLevel(spectrum: Spectrum2D, quadrant = 'rr') { return defaultLevel; } - function contoursManager(options: ContourOptions): ContoursManagerReturn { const contourOptions = { ...options }; return { diff --git a/src/data/data2d/Spectrum2D/findBestMinContour.ts b/src/data/data2d/Spectrum2D/findBestMinContour.ts deleted file mode 100644 index 9a3cc975c7..0000000000 --- a/src/data/data2d/Spectrum2D/findBestMinContour.ts +++ /dev/null @@ -1,1108 +0,0 @@ -import { xMaxAbsoluteValue } from "ml-spectra-processing"; - -export interface AutoContourDiagnostics { - recommended: ThresholdDiagnostics; - ridgeFree: ThresholdDiagnostics; - - /** - * True when directional ridge structures were detected - * at the recommended minimum. - */ - hasT1Noise: boolean; -} - -export interface ThresholdDiagnostics { - sigmaMultiplier: number; - minLevel: number; - - occupancy: number; - fdr: number; - persistence: number; - - verticalRidgeScore: number; - horizontalRidgeScore: number; -} - -export interface AutoContourResult { - /** - * Recommended minimum contour level. - * - * Optimized for preserving meaningful weak peaks while - * avoiding ordinary noise. - */ - minLevel: number; - - /** - * Minimum contour level at which detected directional - * ridge structures (e.g. t1-noise) are no longer present. - * - * This is useful as an optional "ridge-free" visualization. - */ - minLevelWithoutT1Noise: number; - - /** - * Same values expressed in multiples of the noise level. - */ - sigmaMultiplier: number; - sigmaMultiplierWithoutT1Noise: number; - - diagnostics?: AutoContourDiagnostics; -} - -export interface AutoContourOptions { - /** - * Multiplicative ratio between consecutive contour levels. - * - * The contour levels are generated as: - * - * Lₙ = L₀ × contourRatioⁿ - * - * Must be greater than 1. - * - * A smaller value produces more closely spaced contour levels, - * while a larger value produces fewer, more widely spaced levels. - * - * @default 1.4 - */ - contourRatio?: number; - - /** - * Maximum acceptable false discovery rate (FDR) for the - * automatically selected minimum contour level. - * - * The FDR estimates the fraction of pixels above the selected - * threshold that could be explained by Gaussian noise, assuming - * the supplied noiseLevel is accurate. - * - * Smaller values produce a cleaner visualization but may suppress - * weak peaks. - * - * @default 0.05 - */ - maxFdr?: number; - - /** - * Maximum fraction of the spectrum that may be above the - * automatically selected minimum contour level. - * - * This prevents very low thresholds from producing contours over - * a large fraction of the spectrum. - * - * Expressed as a fraction in the range [0, 1]. - * - * For example, 0.02 means that at most 2% of pixels may be above - * the minimum contour threshold. - * - * @default 0.02 - */ - maxOccupancy?: number; - - /** - * Minimum persistence required for spectral structures across - * successive exponential contour levels. - * - * Persistence measures how much of the active spectral structure - * remains when moving from one contour level to the next. - * - * Higher values favor stable, coherent peaks and reject fragmented - * noise structures more aggressively. - * - * Expressed as a fraction in the range [0, 1]. - * - * @default 0.35 - */ - minPersistence?: number; - - /** - * Maximum acceptable score for vertical ridge-like structures. - * - * This is primarily intended to detect structured artifacts such - * as t1 noise, which often appears as elongated structures along - * the F1 dimension. - * - * Smaller values make the ridge-free threshold more conservative. - * - * @default 0.25 - */ - maxVerticalRidgeScore?: number; - - /** - * Maximum acceptable score for horizontal ridge-like structures. - * - * This can detect horizontal streaks or other structured artifacts - * extending predominantly along the F2 dimension. - * - * Smaller values make the ridge-free threshold more conservative. - * - * @default 0.25 - */ - maxHorizontalRidgeScore?: number; - - /** - * Fraction of a row or column that must be occupied by active - * contour pixels for the row or column to be considered - * ridge-like. - * - * For vertical ridge detection, a column is considered suspicious - * when the fraction of active pixels along F1 is greater than or - * equal to this value. - * - * For horizontal ridge detection, the same criterion is applied - * along F2. - * - * Expressed as a fraction in the range [0, 1]. - * - * Lower values make ridge detection more sensitive. - * - * @default 0.5 - */ - ridgeCoverageThreshold?: number; - - /** - * Lowest threshold considered by the automatic contour search, - * expressed as a multiple of the supplied noise level. - * - * For example, minSigma = 2 means that thresholds below 2 × - * noiseLevel are never considered. - * - * Lower values can preserve weaker peaks but increase the risk - * of including noise. - * - * @default 2 - */ - minSigma?: number; - - /** - * Exponent used to determine the spacing between candidate - * minimum contour levels. - * - * Candidate thresholds are generated multiplicatively using: - * - * candidateStep = contourRatio ** candidateRatioExponent - * - * For example, with contourRatio = 1.4 and exponent = 0.5: - * - * candidateStep = sqrt(1.4) - * - * A value of 1 evaluates candidates directly on the contour-level - * hierarchy. Smaller values provide finer threshold resolution - * while increasing the amount of analysis. - * - * @default 0.5 - */ - candidateRatioExponent?: number; - - /** - * Number of successive contour levels used when evaluating - * spectral persistence. - * - * A larger value requires spectral structures to remain stable - * across more contour levels and therefore produces a more - * conservative threshold. - * - * @default 4 - */ - persistenceLevels?: number; - - /** - * Maximum dimension used for spatial analysis. - * - * Large input matrices are reduced using max pooling before - * calculating occupancy, persistence, and ridge metrics. - * - * The original matrix is not modified. - * - * Max pooling is used instead of averaging so that narrow, - * high-intensity spectral features are preserved during analysis. - * - * Larger values improve spatial resolution but increase - * computational cost and memory usage. - * - * @default 384 - */ - maxAnalysisDimension?: number; - - /** - * Whether diagnostic information should be returned. - * - * Diagnostics include the selected thresholds, FDR, occupancy, - * persistence, vertical and horizontal ridge scores, and the - * individual criteria used by the automatic threshold selection. - * - * Disable this when diagnostics are not needed and the smallest - * possible result object is preferred. - * - * @default true - */ - diagnostics?: boolean; -} - - - -export function findAutomaticContourLevels( - matrix: ArrayLike, - rows: number, - cols: number, - noiseLevel: number, - options: AutoContourOptions = {}, -): AutoContourResult { - validateInput( - matrix, - rows, - cols, - noiseLevel, - ); - - const { - contourRatio = 1.4, - - maxFdr = 0.05, - maxOccupancy = 0.02, - minPersistence = 0.35, - - maxVerticalRidgeScore = 0.25, - maxHorizontalRidgeScore = 0.25, - - ridgeCoverageThreshold = 0.5, - - minSigma = 5, - - candidateRatioExponent = 0.5, - - persistenceLevels = 4, - - maxAnalysisDimension = 384, - - diagnostics = true, - } = options; - - if (contourRatio <= 1) { - throw new Error( - 'contourRatio must be > 1', - ); - } - - /* - * Analyze a max-pooled representation. - */ - const analysis = maxPoolAbsolute( - matrix, - rows, - cols, - maxAnalysisDimension, - ); - - const { - values, - rows: analysisRows, - cols: analysisCols, - } = analysis; - - const maxSigmaInData = - findMaximumNormalizedValue( - values, - noiseLevel, - ); - /* - * Generate candidate minimum thresholds. - */ - const candidates = - generateCandidateSigmaLevels( - minSigma, - maxSigmaInData, - contourRatio, - candidateRatioExponent, - ); - - /* - * ------------------------------------------------------- - * STEP 1 - * - * Find the lowest threshold that is statistically clean - * and sufficiently persistent. - * - * t1-ridges are deliberately NOT rejected here. - * ------------------------------------------------------- - */ - - let recommended: - EvaluatedThreshold | undefined; - - for (const sigmaMultiplier of candidates) { - const evaluation = - evaluateThreshold( - values, - analysisRows, - analysisCols, - noiseLevel, - sigmaMultiplier, - contourRatio, - persistenceLevels, - ridgeCoverageThreshold, - ); - - const passed = - evaluation.fdr <= maxFdr && - evaluation.occupancy <= maxOccupancy && - evaluation.persistence >= minPersistence; - - if (passed) { - recommended = evaluation; - break; - } - } - - /* - * Fallback when no candidate satisfies all conditions. - */ - if (!recommended) { - recommended = - evaluateFallbackThreshold( - values, - analysisRows, - analysisCols, - noiseLevel, - candidates, - contourRatio, - persistenceLevels, - ridgeCoverageThreshold, - maxFdr, - maxOccupancy, - minPersistence, - ); - } - - /* - * ------------------------------------------------------- - * STEP 2 - * - * Find the first threshold >= recommended where the - * directional ridge disappears. - * ------------------------------------------------------- - */ - - const ridgeFreeLevel = - findRidgeFreeThresholdTopDown( - values, - analysisRows, - analysisCols, - noiseLevel, - recommended.sigmaMultiplier * - noiseLevel, - xMaxAbsoluteValue(matrix as Float64Array), - contourRatio, - maxVerticalRidgeScore, - maxHorizontalRidgeScore, - ridgeCoverageThreshold, - ); - - const ridgeFree = - evaluateThreshold( - values, - analysisRows, - analysisCols, - noiseLevel, - ridgeFreeLevel / noiseLevel, - contourRatio, - 1, - ridgeCoverageThreshold, - ); - - const result: AutoContourResult = { - minLevel: - recommended.sigmaMultiplier * - noiseLevel, - - minLevelWithoutT1Noise: - ridgeFreeLevel, - - sigmaMultiplier: - recommended.sigmaMultiplier, - - sigmaMultiplierWithoutT1Noise: - ridgeFreeLevel / noiseLevel, - }; - - if (diagnostics) { - result.diagnostics = { - recommended: { - sigmaMultiplier: - recommended.sigmaMultiplier, - - minLevel: - recommended.sigmaMultiplier * - noiseLevel, - - occupancy: - recommended.occupancy, - - fdr: - recommended.fdr, - - persistence: - recommended.persistence, - - verticalRidgeScore: - recommended.verticalRidgeScore, - - horizontalRidgeScore: - recommended.horizontalRidgeScore, - }, - - ridgeFree: { - sigmaMultiplier: - ridgeFree.sigmaMultiplier, - - minLevel: - ridgeFreeLevel, - - occupancy: - ridgeFree.occupancy, - - fdr: - ridgeFree.fdr, - - persistence: - ridgeFree.persistence, - - verticalRidgeScore: - ridgeFree.verticalRidgeScore, - - horizontalRidgeScore: - ridgeFree.horizontalRidgeScore, - }, - - hasT1Noise: - recommended.verticalRidgeScore > - maxVerticalRidgeScore || - recommended.horizontalRidgeScore > - maxHorizontalRidgeScore, - }; - } - - return result; -} - -interface EvaluatedThreshold { - sigmaMultiplier: number; - - activePixels: number; - occupancy: number; - fdr: number; - persistence: number; - - verticalRidgeScore: number; - horizontalRidgeScore: number; -} - -function evaluateThreshold( - matrix: Float64Array, - rows: number, - cols: number, - noiseLevel: number, - sigmaMultiplier: number, - contourRatio: number, - persistenceLevels: number, - ridgeCoverageThreshold: number, -): EvaluatedThreshold { - const threshold = - sigmaMultiplier * noiseLevel; - - const mask = - createMask( - matrix, - threshold, - ); - - const activePixels = - countActivePixels(mask); - - const totalPixels = - rows * cols; - - const occupancy = - activePixels / totalPixels; - - if (activePixels === 0) { - return { - sigmaMultiplier, - activePixels: 0, - occupancy: 0, - fdr: 0, - persistence: 0, - verticalRidgeScore: 0, - horizontalRidgeScore: 0, - }; - } - - const expectedNoisePixels = - totalPixels * - gaussianTwoSidedTail( - sigmaMultiplier, - ); - - const fdr = - Math.min( - 1, - expectedNoisePixels / - activePixels, - ); - - const persistence = - computePersistence( - matrix, - rows, - cols, - threshold, - contourRatio, - persistenceLevels, - ); - - const ridge = - computeRidgeScores( - mask, - rows, - cols, - ridgeCoverageThreshold, - ); - - return { - sigmaMultiplier, - - activePixels, - occupancy, - - fdr, - persistence, - - verticalRidgeScore: - ridge.vertical, - - horizontalRidgeScore: - ridge.horizontal, - }; -} - -function evaluateFallbackThreshold( - matrix: Float64Array, - rows: number, - cols: number, - noiseLevel: number, - candidates: number[], - contourRatio: number, - persistenceLevels: number, - ridgeCoverageThreshold: number, - maxFdr: number, - maxOccupancy: number, - minPersistence: number, -): EvaluatedThreshold { - let best: - EvaluatedThreshold | undefined; - - let bestScore = Infinity; - - for (const sigma of candidates) { - const evaluation = - evaluateThreshold( - matrix, - rows, - cols, - noiseLevel, - sigma, - contourRatio, - persistenceLevels, - ridgeCoverageThreshold, - ); - - const score = - violation( - evaluation.fdr, - maxFdr, - ) * 4 + - - violation( - evaluation.occupancy, - maxOccupancy, - ) * 2 + - - violation( - minPersistence, - evaluation.persistence, - ) * 2; - - if (score < bestScore) { - bestScore = score; - best = evaluation; - } - } - - if (!best) { - throw new Error( - 'Unable to evaluate contour thresholds', - ); - } - - return best; -} - -function findRidgeFreeThresholdTopDown( - matrix: Float64Array, - rows: number, - cols: number, - noiseLevel: number, - minLevel: number, - maxLevel: number, - contourRatio: number, - maxVerticalRidgeScore: number, - maxHorizontalRidgeScore: number, - ridgeCoverageThreshold: number, - ridgePersistenceLevels = 2, -): number { - /* - * Start from the actual maximum contour level. - */ - let level = maxLevel; - - let lastCleanLevel = level; - - let consecutiveRidgeLevels = 0; - - while (level >= minLevel) { - const evaluation = - evaluateThreshold( - matrix, - rows, - cols, - noiseLevel, - level / noiseLevel, - contourRatio, - 1, - ridgeCoverageThreshold, - ); - - const hasRidge = - evaluation.verticalRidgeScore > - maxVerticalRidgeScore || - evaluation.horizontalRidgeScore > - maxHorizontalRidgeScore; - - if (hasRidge) { - consecutiveRidgeLevels++; - - if ( - consecutiveRidgeLevels >= - ridgePersistenceLevels - ) { - /* - * The ridge has appeared persistently. - * - * The previous clean level is the answer. - */ - return lastCleanLevel; - } - } else { - consecutiveRidgeLevels = 0; - - lastCleanLevel = level; - } - - level /= contourRatio; - } - - return lastCleanLevel; -} -function generateCandidateSigmaLevels( - minSigma: number, - maxSigma: number, - contourRatio: number, - exponent: number, -): number[] { - const step = - contourRatio ** exponent; - - const candidates: number[] = []; - - let sigma = minSigma; - - while ( - sigma <= - maxSigma * (1 + 1e-12) - ) { - candidates.push(sigma); - sigma *= step; - } - - return candidates; -} - - -function computePersistence( - matrix: Float64Array, - rows: number, - cols: number, - initialThreshold: number, - contourRatio: number, - levels: number, -): number { - let threshold = initialThreshold; - - let previousArea = - countValuesAboveThreshold( - matrix, - threshold, - ); - - if (previousArea === 0) { - return 0; - } - - let minimumPersistence = 1; - - for (let level = 1; level < levels; level++) { - threshold *= contourRatio; - - const currentArea = - countValuesAboveThreshold( - matrix, - threshold, - ); - - if (currentArea === 0) { - return 0; - } - - const persistence = - currentArea / previousArea; - - minimumPersistence = Math.min( - minimumPersistence, - persistence, - ); - - previousArea = currentArea; - } - - return minimumPersistence; -} - -interface RidgeScores { - vertical: number; - horizontal: number; -} - -function computeRidgeScores( - mask: Uint8Array, - rows: number, - cols: number, - coverageThreshold: number, -): RidgeScores { - const rowCounts = new Uint32Array(rows); - const colCounts = new Uint32Array(cols); - - let activePixels = 0; - - for (let row = 0; row < rows; row++) { - for (let col = 0; col < cols; col++) { - const index = row * cols + col; - - if (!mask[index]) { - continue; - } - - activePixels++; - - rowCounts[row]++; - colCounts[col]++; - } - } - - if (activePixels === 0) { - return { - vertical: 0, - horizontal: 0, - }; - } - - /* - * A vertical ridge occupies a large fraction of F1 - * in one or more F2 columns. - */ - let verticalRidgePixels = 0; - - for (let col = 0; col < cols; col++) { - const coverage = - colCounts[col] / rows; - - if (coverage >= coverageThreshold) { - verticalRidgePixels += - colCounts[col]; - } - } - - /* - * A horizontal ridge occupies a large fraction of F2 - * in one or more F1 rows. - */ - let horizontalRidgePixels = 0; - - for (let row = 0; row < rows; row++) { - const coverage = - rowCounts[row] / cols; - - if (coverage >= coverageThreshold) { - horizontalRidgePixels += - rowCounts[row]; - } - } - - return { - vertical: - verticalRidgePixels / activePixels, - - horizontal: - horizontalRidgePixels / activePixels, - }; -} - -function createMask( - matrix: Float64Array, - threshold: number, -): Uint8Array { - const mask = new Uint8Array(matrix.length); - - for (let i = 0; i < matrix.length; i++) { - if (matrix[i] >= threshold) { - mask[i] = 1; - } - } - - return mask; -} - -interface AnalysisMatrix { - values: Float64Array; - rows: number; - cols: number; -} - -function maxPoolAbsolute( - matrix: ArrayLike, - rows: number, - cols: number, - maxDimension: number, -): AnalysisMatrix { - const scale = - Math.max( - 1, - Math.ceil( - Math.max(rows, cols) / - maxDimension, - ), - ); - - const analysisRows = - Math.ceil(rows / scale); - - const analysisCols = - Math.ceil(cols / scale); - - const values = - new Float64Array( - analysisRows * analysisCols, - ); - - for ( - let analysisRow = 0; - analysisRow < analysisRows; - analysisRow++ - ) { - const sourceRowStart = - analysisRow * scale; - - const sourceRowEnd = - Math.min( - rows, - sourceRowStart + scale, - ); - - for ( - let analysisCol = 0; - analysisCol < analysisCols; - analysisCol++ - ) { - const sourceColStart = - analysisCol * scale; - - const sourceColEnd = - Math.min( - cols, - sourceColStart + scale, - ); - - let maximum = 0; - - for ( - let row = sourceRowStart; - row < sourceRowEnd; - row++ - ) { - const offset = row * cols; - - for ( - let col = sourceColStart; - col < sourceColEnd; - col++ - ) { - const value = - Math.abs(matrix[offset + col]); - - if ( - Number.isFinite(value) && - value > maximum - ) { - maximum = value; - } - } - } - - values[ - analysisRow * analysisCols + - analysisCol - ] = maximum; - } - } - - return { - values, - rows: analysisRows, - cols: analysisCols, - }; -} - -function countActivePixels( - mask: Uint8Array, -): number { - let count = 0; - - for (let i = 0; i < mask.length; i++) { - count += mask[i]; - } - - return count; -} - -function countValuesAboveThreshold( - matrix: Float64Array, - threshold: number, -): number { - let count = 0; - - for (let i = 0; i < matrix.length; i++) { - if (matrix[i] >= threshold) { - count++; - } - } - - return count; -} - -function findMaximumNormalizedValue( - matrix: Float64Array, - noiseLevel: number, -): number { - let maximum = 0; - - for (let i = 0; i < matrix.length; i++) { - const value = - matrix[i] / noiseLevel; - - if (value > maximum) { - maximum = value; - } - } - - return maximum; -} - -function gaussianTwoSidedTail( - k: number, -): number { - return erfc(k / Math.SQRT2); -} - -/** - * Approximation of the complementary error function. - */ -function erfc(x: number): number { - const sign = x < 0 ? -1 : 1; - const ax = Math.abs(x); - - const p = 0.3275911; - - const a1 = 0.254829592; - const a2 = -0.284496736; - const a3 = 1.421413741; - const a4 = -1.453152027; - const a5 = 1.061405429; - - const t = 1 / (1 + p * ax); - - const polynomial = - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * - t; - - const result = - polynomial * Math.exp(-ax * ax); - - return sign >= 0 - ? result - : 2 - result; -} - -function violation( - value: number, - limit: number, -): number { - if (limit === 0) { - return value === 0 ? 0 : 1; - } - - return Math.max( - 0, - value / limit - 1, - ); -} - -function validateInput( - matrix: ArrayLike, - rows: number, - cols: number, - noiseLevel: number, -): void { - if (!Number.isInteger(rows) || rows <= 0) { - throw new Error( - 'rows must be a positive integer', - ); - } - - if (!Number.isInteger(cols) || cols <= 0) { - throw new Error( - 'cols must be a positive integer', - ); - } - - if (matrix.length !== rows * cols) { - throw new Error( - 'matrix.length must equal rows * cols', - ); - } - - if ( - !Number.isFinite(noiseLevel) || - noiseLevel <= 0 - ) { - throw new Error( - 'noiseLevel must be a positive finite number', - ); - } -} \ No newline at end of file diff --git a/src/data/data2d/Spectrum2D/findBestMinContour/evaluation.ts b/src/data/data2d/Spectrum2D/findBestMinContour/evaluation.ts new file mode 100644 index 0000000000..b1e8ae641b --- /dev/null +++ b/src/data/data2d/Spectrum2D/findBestMinContour/evaluation.ts @@ -0,0 +1,383 @@ +import { gaussianTwoSidedTail, violation } from './math.js'; + +export interface EvaluatedThreshold { + sigmaMultiplier: number; + + activePixels: number; + occupancy: number; + fdr: number; + persistence: number; + + verticalRidgeScore: number; + horizontalRidgeScore: number; +} + +export interface AnalysisMatrix { + values: Float64Array; + rows: number; + cols: number; +} + +export interface RidgeScores { + vertical: number; + horizontal: number; +} + +export function evaluateThreshold( + matrix: Float64Array, + rows: number, + cols: number, + noiseLevel: number, + sigmaMultiplier: number, + contourRatio: number, + persistenceLevels: number, + ridgeCoverageThreshold: number, +): EvaluatedThreshold { + const threshold = sigmaMultiplier * noiseLevel; + const mask = createMask(matrix, threshold); + const activePixels = countActivePixels(mask); + const totalPixels = rows * cols; + const occupancy = activePixels / totalPixels; + + if (activePixels === 0) { + return { + sigmaMultiplier, + activePixels: 0, + occupancy: 0, + fdr: 0, + persistence: 0, + verticalRidgeScore: 0, + horizontalRidgeScore: 0, + }; + } + + const expectedNoisePixels = + totalPixels * gaussianTwoSidedTail(sigmaMultiplier); + const fdr = Math.min(1, expectedNoisePixels / activePixels); + const persistence = computePersistence( + matrix, + rows, + cols, + threshold, + contourRatio, + persistenceLevels, + ); + const ridge = computeRidgeScores(mask, rows, cols, ridgeCoverageThreshold); + + return { + sigmaMultiplier, + activePixels, + occupancy, + fdr, + persistence, + verticalRidgeScore: ridge.vertical, + horizontalRidgeScore: ridge.horizontal, + }; +} + +export function evaluateFallbackThreshold( + matrix: Float64Array, + rows: number, + cols: number, + noiseLevel: number, + candidates: number[], + contourRatio: number, + persistenceLevels: number, + ridgeCoverageThreshold: number, + maxFdr: number, + maxOccupancy: number, + minPersistence: number, +): EvaluatedThreshold { + let best: EvaluatedThreshold | undefined; + let bestScore = Infinity; + + for (const sigma of candidates) { + const evaluation = evaluateThreshold( + matrix, + rows, + cols, + noiseLevel, + sigma, + contourRatio, + persistenceLevels, + ridgeCoverageThreshold, + ); + + const score = + violation(evaluation.fdr, maxFdr) * 4 + + violation(evaluation.occupancy, maxOccupancy) * 2 + + violation(minPersistence, evaluation.persistence) * 2; + + if (score < bestScore) { + bestScore = score; + best = evaluation; + } + } + + if (!best) { + throw new Error('Unable to evaluate contour thresholds'); + } + + return best; +} + +export function findRidgeFreeThresholdTopDown( + matrix: Float64Array, + rows: number, + cols: number, + noiseLevel: number, + minLevel: number, + maxLevel: number, + contourRatio: number, + maxVerticalRidgeScore: number, + maxHorizontalRidgeScore: number, + ridgeCoverageThreshold: number, + ridgePersistenceLevels = 1, +): number { + let level = maxLevel; + let lastCleanLevel = level; + let consecutiveRidgeLevels = 0; + + while (level >= minLevel) { + const evaluation = evaluateThreshold( + matrix, + rows, + cols, + noiseLevel, + level / noiseLevel, + contourRatio, + 1, + ridgeCoverageThreshold, + ); + + const hasRidge = + evaluation.verticalRidgeScore > maxVerticalRidgeScore || + evaluation.horizontalRidgeScore > maxHorizontalRidgeScore; + + if (hasRidge) { + consecutiveRidgeLevels++; + + if (consecutiveRidgeLevels >= ridgePersistenceLevels) { + return lastCleanLevel; + } + } else { + consecutiveRidgeLevels = 0; + lastCleanLevel = level; + } + + level /= contourRatio; + } + + return lastCleanLevel; +} + +export function generateCandidateSigmaLevels( + minSigma: number, + maxSigma: number, + contourRatio: number, + exponent: number, +): number[] { + const step = contourRatio ** exponent; + const candidates: number[] = []; + + let sigma = minSigma; + + while (sigma <= maxSigma * (1 + 1e-12)) { + candidates.push(sigma); + sigma *= step; + } + + return candidates; +} + +export function computePersistence( + matrix: Float64Array, + rows: number, + cols: number, + initialThreshold: number, + contourRatio: number, + levels: number, +): number { + let threshold = initialThreshold; + let previousArea = countValuesAboveThreshold(matrix, threshold); + + if (previousArea === 0) { + return 0; + } + + let minimumPersistence = 1; + + for (let level = 1; level < levels; level++) { + threshold *= contourRatio; + + const currentArea = countValuesAboveThreshold(matrix, threshold); + + if (currentArea === 0) { + return 0; + } + + const persistence = currentArea / previousArea; + minimumPersistence = Math.min(minimumPersistence, persistence); + previousArea = currentArea; + } + + return minimumPersistence; +} + +export function computeRidgeScores( + mask: Uint8Array, + rows: number, + cols: number, + coverageThreshold: number, +): RidgeScores { + const rowCounts = new Uint32Array(rows); + const colCounts = new Uint32Array(cols); + let activePixels = 0; + + for (let row = 0; row < rows; row++) { + for (let col = 0; col < cols; col++) { + const index = row * cols + col; + + if (!mask[index]) { + continue; + } + + activePixels++; + rowCounts[row]++; + colCounts[col]++; + } + } + + if (activePixels === 0) { + return { + vertical: 0, + horizontal: 0, + }; + } + + let verticalRidgePixels = 0; + + for (let col = 0; col < cols; col++) { + const coverage = colCounts[col] / rows; + + if (coverage >= coverageThreshold) { + verticalRidgePixels += colCounts[col]; + } + } + + let horizontalRidgePixels = 0; + + for (let row = 0; row < rows; row++) { + const coverage = rowCounts[row] / cols; + + if (coverage >= coverageThreshold) { + horizontalRidgePixels += rowCounts[row]; + } + } + + return { + vertical: verticalRidgePixels / activePixels, + horizontal: horizontalRidgePixels / activePixels, + }; +} + +export function createMask( + matrix: Float64Array, + threshold: number, +): Uint8Array { + const mask = new Uint8Array(matrix.length); + + for (const [index, value] of matrix.entries()) { + if (value >= threshold) { + mask[index] = 1; + } + } + + return mask; +} + +export function maxPoolAbsolute( + matrix: ArrayLike, + rows: number, + cols: number, + maxDimension: number, +): AnalysisMatrix { + const scale = Math.max(1, Math.ceil(Math.max(rows, cols) / maxDimension)); + const analysisRows = Math.ceil(rows / scale); + const analysisCols = Math.ceil(cols / scale); + const values = new Float64Array(analysisRows * analysisCols); + + for (let analysisRow = 0; analysisRow < analysisRows; analysisRow++) { + const sourceRowStart = analysisRow * scale; + const sourceRowEnd = Math.min(rows, sourceRowStart + scale); + + for (let analysisCol = 0; analysisCol < analysisCols; analysisCol++) { + const sourceColStart = analysisCol * scale; + const sourceColEnd = Math.min(cols, sourceColStart + scale); + + let maximum = 0; + + for (let row = sourceRowStart; row < sourceRowEnd; row++) { + const offset = row * cols; + + for (let col = sourceColStart; col < sourceColEnd; col++) { + const value = Math.abs(matrix[offset + col]); + + if (Number.isFinite(value) && value > maximum) { + maximum = value; + } + } + } + + values[analysisRow * analysisCols + analysisCol] = maximum; + } + } + + return { + values, + rows: analysisRows, + cols: analysisCols, + }; +} + +export function countActivePixels(mask: Uint8Array): number { + let count = 0; + + for (const value of mask) { + count += value; + } + + return count; +} + +export function countValuesAboveThreshold( + matrix: Float64Array, + threshold: number, +): number { + let count = 0; + + for (const value of matrix) { + if (value >= threshold) { + count++; + } + } + + return count; +} + +export function findMaximumNormalizedValue( + matrix: Float64Array, + noiseLevel: number, +): number { + let maximum = 0; + + for (const value of matrix) { + const normalized = value / noiseLevel; + + if (normalized > maximum) { + maximum = normalized; + } + } + + return maximum; +} diff --git a/src/data/data2d/Spectrum2D/findBestMinContour/findAutomaticContourLevels.ts b/src/data/data2d/Spectrum2D/findBestMinContour/findAutomaticContourLevels.ts new file mode 100644 index 0000000000..235595c6b5 --- /dev/null +++ b/src/data/data2d/Spectrum2D/findBestMinContour/findAutomaticContourLevels.ts @@ -0,0 +1,408 @@ +import { xMaxAbsoluteValue } from 'ml-spectra-processing'; + +import { + evaluateFallbackThreshold, + evaluateThreshold, + findMaximumNormalizedValue, + findRidgeFreeThresholdTopDown, + generateCandidateSigmaLevels, + maxPoolAbsolute, +} from './evaluation.js'; +import { validateInput } from './math.js'; + +export interface AutoContourDiagnostics { + recommended: ThresholdDiagnostics; + ridgeFree: ThresholdDiagnostics; + + /** + * True when directional ridge structures were detected + * at the recommended minimum. + */ + hasT1Noise: boolean; +} + +export interface ThresholdDiagnostics { + sigmaMultiplier: number; + minLevel: number; + + occupancy: number; + fdr: number; + persistence: number; + + verticalRidgeScore: number; + horizontalRidgeScore: number; +} + +export type AutoContourResult = { + /** + * Recommended minimum contour level. + */ + minLevel: number; + + /** + * Minimum contour level at which detected directional + * ridge structures (e.g. t1-noise) are no longer present. + */ + minLevelWithoutT1Noise: number; + + /** + * Same values expressed in multiples of the noise level. + */ + sigmaMultiplier: number; + sigmaMultiplierWithoutT1Noise: number; +} & (TDiagnostics extends true + ? { + diagnostics: AutoContourDiagnostics; + } + : { + diagnostics?: AutoContourDiagnostics; + }); + +export interface AutoContourOptions { + /** + * Multiplicative ratio between consecutive contour levels. + * + * The contour levels are generated as: + * + * Lₙ = L₀ × contourRatioⁿ + * + * Must be greater than 1. + * + * A smaller value produces more closely spaced contour levels, + * while a larger value produces fewer, more widely spaced levels. + * + * @default 1.4 + */ + contourRatio?: number; + + /** + * Maximum acceptable false discovery rate (FDR) for the + * automatically selected minimum contour level. + * + * The FDR estimates the fraction of pixels above the selected + * threshold that could be explained by Gaussian noise, assuming + * the supplied noiseLevel is accurate. + * + * Smaller values produce a cleaner visualization but may suppress + * weak peaks. + * + * @default 0.05 + */ + maxFdr?: number; + + /** + * Maximum fraction of the spectrum that may be above the + * automatically selected minimum contour level. + * + * This prevents very low thresholds from producing contours over + * a large fraction of the spectrum. + * + * Expressed as a fraction in the range [0, 1]. + * + * For example, 0.02 means that at most 2% of pixels may be above + * the minimum contour threshold. + * + * @default 0.02 + */ + maxOccupancy?: number; + + /** + * Minimum persistence required for spectral structures across + * successive exponential contour levels. + * + * Persistence measures how much of the active spectral structure + * remains when moving from one contour level to the next. + * + * Higher values favor stable, coherent peaks and reject fragmented + * noise structures more aggressively. + * + * Expressed as a fraction in the range [0, 1]. + * + * @default 0.35 + */ + minPersistence?: number; + + /** + * Maximum acceptable score for vertical ridge-like structures. + * + * This is primarily intended to detect structured artifacts such + * as t1 noise, which often appears as elongated structures along + * the F1 dimension. + * + * Smaller values make the ridge-free threshold more conservative. + * + * @default 0.25 + */ + maxVerticalRidgeScore?: number; + + /** + * Maximum acceptable score for horizontal ridge-like structures. + * + * This can detect horizontal streaks or other structured artifacts + * extending predominantly along the F2 dimension. + * + * Smaller values make the ridge-free threshold more conservative. + * + * @default 0.25 + */ + maxHorizontalRidgeScore?: number; + + /** + * Fraction of a row or column that must be occupied by active + * contour pixels for the row or column to be considered + * ridge-like. + * + * For vertical ridge detection, a column is considered suspicious + * when the fraction of active pixels along F1 is greater than or + * equal to this value. + * + * For horizontal ridge detection, the same criterion is applied + * along F2. + * + * Expressed as a fraction in the range [0, 1]. + * + * Lower values make ridge detection more sensitive. + * + * @default 0.5 + */ + ridgeCoverageThreshold?: number; + + /** + * Lowest threshold considered by the automatic contour search, + * expressed as a multiple of the supplied noise level. + * + * For example, minSigma = 2 means that thresholds below 2 × + * noiseLevel are never considered. + * + * Lower values can preserve weaker peaks but increase the risk + * of including noise. + * + * @default 2 + */ + minSigma?: number; + + /** + * Exponent used to determine the spacing between candidate + * minimum contour levels. + * + * Candidate thresholds are generated multiplicatively using: + * + * candidateStep = contourRatio ** candidateRatioExponent + * + * For example, with contourRatio = 1.4 and exponent = 0.5: + * + * candidateStep = sqrt(1.4) + * + * A value of 1 evaluates candidates directly on the contour-level + * hierarchy. Smaller values provide finer threshold resolution + * while increasing the amount of analysis. + * + * @default 0.5 + */ + candidateRatioExponent?: number; + + /** + * Number of successive contour levels used when evaluating + * spectral persistence. + * + * A larger value requires spectral structures to remain stable + * across more contour levels and therefore produces a more + * conservative threshold. + * + * @default 4 + */ + persistenceLevels?: number; + + /** + * Maximum dimension used for spatial analysis. + * + * Large input matrices are reduced using max pooling before + * calculating occupancy, persistence, and ridge metrics. + * + * The original matrix is not modified. + * + * Max pooling is used instead of averaging so that narrow, + * high-intensity spectral features are preserved during analysis. + * + * Larger values improve spatial resolution but increase + * computational cost and memory usage. + * + * @default 384 + */ + maxAnalysisDimension?: number; + /** + * Whether diagnostic information should be returned. + * + * Diagnostics include the selected thresholds, FDR, occupancy, + * persistence, vertical and horizontal ridge scores, and the + * individual criteria used by the automatic threshold selection. + * + * Disable this when diagnostics are not needed and the smallest + * possible result object is preferred. + * + * @default true + */ + diagnostics?: boolean; +} + +export function findAutomaticContourLevels( + matrix: ArrayLike, + rows: number, + cols: number, + noiseLevel: number, + options: AutoContourOptions & { diagnostics: true }, +): AutoContourResult; +export function findAutomaticContourLevels( + matrix: ArrayLike, + rows: number, + cols: number, + noiseLevel: number, + options?: AutoContourOptions, +): AutoContourResult; +export function findAutomaticContourLevels( + matrix: ArrayLike, + rows: number, + cols: number, + noiseLevel: number, + options: AutoContourOptions & { diagnostics: false }, +): AutoContourResult; + +export function findAutomaticContourLevels( + matrix: ArrayLike, + rows: number, + cols: number, + noiseLevel: number, + options: AutoContourOptions = {}, +): AutoContourResult { + validateInput(matrix, rows, cols, noiseLevel); + + const { + contourRatio = 1.4, + maxFdr = 0.05, + maxOccupancy = 0.02, + minPersistence = 0.35, + maxVerticalRidgeScore = 0.25, + maxHorizontalRidgeScore = 0.25, + ridgeCoverageThreshold = 0.5, + minSigma = 5, + candidateRatioExponent = 0.5, + persistenceLevels = 4, + maxAnalysisDimension = 384, + diagnostics = true, + } = options; + + if (contourRatio <= 1) { + throw new Error('contourRatio must be > 1'); + } + + const analysis = maxPoolAbsolute(matrix, rows, cols, maxAnalysisDimension); + const { values, rows: analysisRows, cols: analysisCols } = analysis; + + const maxSigmaInData = findMaximumNormalizedValue(values, noiseLevel); + const candidates = generateCandidateSigmaLevels( + minSigma, + maxSigmaInData, + contourRatio, + candidateRatioExponent, + ); + + let recommended: ReturnType | undefined; + + for (const sigmaMultiplier of candidates) { + const evaluation = evaluateThreshold( + values, + analysisRows, + analysisCols, + noiseLevel, + sigmaMultiplier, + contourRatio, + persistenceLevels, + ridgeCoverageThreshold, + ); + + const passed = + evaluation.fdr <= maxFdr && + evaluation.occupancy <= maxOccupancy && + evaluation.persistence >= minPersistence; + + if (passed) { + recommended = evaluation; + break; + } + } + + if (!recommended) { + recommended = evaluateFallbackThreshold( + values, + analysisRows, + analysisCols, + noiseLevel, + candidates, + contourRatio, + persistenceLevels, + ridgeCoverageThreshold, + maxFdr, + maxOccupancy, + minPersistence, + ); + } + + const ridgeFreeLevel = findRidgeFreeThresholdTopDown( + values, + analysisRows, + analysisCols, + noiseLevel, + recommended.sigmaMultiplier * noiseLevel, + xMaxAbsoluteValue(matrix as Float64Array), + contourRatio, + maxVerticalRidgeScore, + maxHorizontalRidgeScore, + ridgeCoverageThreshold, + ); + + const ridgeFree = evaluateThreshold( + values, + analysisRows, + analysisCols, + noiseLevel, + ridgeFreeLevel / noiseLevel, + contourRatio, + 1, + ridgeCoverageThreshold, + ); + + const result: AutoContourResult = { + minLevel: recommended.sigmaMultiplier * noiseLevel, + minLevelWithoutT1Noise: ridgeFreeLevel, + sigmaMultiplier: recommended.sigmaMultiplier, + sigmaMultiplierWithoutT1Noise: ridgeFreeLevel / noiseLevel, + }; + + if (diagnostics) { + result.diagnostics = { + recommended: { + sigmaMultiplier: recommended.sigmaMultiplier, + minLevel: recommended.sigmaMultiplier * noiseLevel, + occupancy: recommended.occupancy, + fdr: recommended.fdr, + persistence: recommended.persistence, + verticalRidgeScore: recommended.verticalRidgeScore, + horizontalRidgeScore: recommended.horizontalRidgeScore, + }, + ridgeFree: { + sigmaMultiplier: ridgeFree.sigmaMultiplier, + minLevel: ridgeFreeLevel, + occupancy: ridgeFree.occupancy, + fdr: ridgeFree.fdr, + persistence: ridgeFree.persistence, + verticalRidgeScore: ridgeFree.verticalRidgeScore, + horizontalRidgeScore: ridgeFree.horizontalRidgeScore, + }, + hasT1Noise: + recommended.verticalRidgeScore > maxVerticalRidgeScore || + recommended.horizontalRidgeScore > maxHorizontalRidgeScore, + }; + } + + return result; +} diff --git a/src/data/data2d/Spectrum2D/findBestMinContour/math.ts b/src/data/data2d/Spectrum2D/findBestMinContour/math.ts new file mode 100644 index 0000000000..bfda0ff998 --- /dev/null +++ b/src/data/data2d/Spectrum2D/findBestMinContour/math.ts @@ -0,0 +1,59 @@ +export function gaussianTwoSidedTail(k: number): number { + return erfc(k / Math.SQRT2); +} + +/** + * Approximation of the complementary error function. + * @TODO it would be replaced by the same implementation from ml-spectra-processing. + */ +export function erfc(x: number): number { + const sign = x < 0 ? -1 : 1; + const ax = Math.abs(x); + + const p = 0.3275911; + + const a1 = 0.254829592; + const a2 = -0.284496736; + const a3 = 1.421413741; + const a4 = -1.453152027; + const a5 = 1.061405429; + + const t = 1 / (1 + p * ax); + + const polynomial = ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t; + + const result = polynomial * Math.exp(-ax * ax); + + return sign >= 0 ? result : 2 - result; +} + +export function violation(value: number, limit: number): number { + if (limit === 0) { + return value === 0 ? 0 : 1; + } + + return Math.max(0, value / limit - 1); +} + +export function validateInput( + matrix: ArrayLike, + rows: number, + cols: number, + noiseLevel: number, +): void { + if (!Number.isInteger(rows) || rows <= 0) { + throw new Error('rows must be a positive integer'); + } + + if (!Number.isInteger(cols) || cols <= 0) { + throw new Error('cols must be a positive integer'); + } + + if (matrix.length !== rows * cols) { + throw new Error('matrix.length must equal rows * cols'); + } + + if (!Number.isFinite(noiseLevel) || noiseLevel <= 0) { + throw new Error('noiseLevel must be a positive finite number'); + } +} From 0e9bcbb235e2369ba6c49932d7026c99fd15b290 Mon Sep 17 00:00:00 2001 From: jobo322 Date: Thu, 20 Aug 2026 05:51:33 -0500 Subject: [PATCH 10/20] chore: improve speed in contor min level determination --- .../findBestMinContour/evaluation.ts | 166 ++++++++++++------ .../findAutomaticContourLevels.test.ts | 82 +++++++++ .../findAutomaticContourLevels.ts | 64 ++++--- 3 files changed, 234 insertions(+), 78 deletions(-) create mode 100644 src/data/data2d/Spectrum2D/findBestMinContour/findAutomaticContourLevels.test.ts diff --git a/src/data/data2d/Spectrum2D/findBestMinContour/evaluation.ts b/src/data/data2d/Spectrum2D/findBestMinContour/evaluation.ts index b1e8ae641b..7d95acc4a3 100644 --- a/src/data/data2d/Spectrum2D/findBestMinContour/evaluation.ts +++ b/src/data/data2d/Spectrum2D/findBestMinContour/evaluation.ts @@ -23,6 +23,52 @@ export interface RidgeScores { horizontal: number; } +interface ThresholdMetrics { + activePixels: number; + ridge: RidgeScores; +} + +function evaluateThresholdMetrics( + matrix: Float64Array, + rows: number, + cols: number, + threshold: number, + ridgeCoverageThreshold: number, + includeRidgeScores: boolean, +): ThresholdMetrics { + const rowCounts = includeRidgeScores ? new Uint32Array(rows) : undefined; + const colCounts = includeRidgeScores ? new Uint32Array(cols) : undefined; + let activePixels = 0; + + for (let row = 0; row < rows; row++) { + for (let col = 0; col < cols; col++) { + const index = row * cols + col; + + if (!(matrix[index] >= threshold)) continue; + + activePixels++; + if (rowCounts && colCounts) { + rowCounts[row]++; + colCounts[col]++; + } + } + } + + const ridge = + rowCounts && colCounts + ? calculateRidgeScores( + rowCounts, + colCounts, + rows, + cols, + activePixels, + ridgeCoverageThreshold, + ) + : { vertical: 0, horizontal: 0 }; + + return { activePixels, ridge }; +} + export function evaluateThreshold( matrix: Float64Array, rows: number, @@ -32,10 +78,17 @@ export function evaluateThreshold( contourRatio: number, persistenceLevels: number, ridgeCoverageThreshold: number, + includeRidgeScores = true, ): EvaluatedThreshold { const threshold = sigmaMultiplier * noiseLevel; - const mask = createMask(matrix, threshold); - const activePixels = countActivePixels(mask); + const { activePixels, ridge } = evaluateThresholdMetrics( + matrix, + rows, + cols, + threshold, + ridgeCoverageThreshold, + includeRidgeScores, + ); const totalPixels = rows * cols; const occupancy = activePixels / totalPixels; @@ -62,7 +115,6 @@ export function evaluateThreshold( contourRatio, persistenceLevels, ); - const ridge = computeRidgeScores(mask, rows, cols, ridgeCoverageThreshold); return { sigmaMultiplier, @@ -76,14 +128,7 @@ export function evaluateThreshold( } export function evaluateFallbackThreshold( - matrix: Float64Array, - rows: number, - cols: number, - noiseLevel: number, - candidates: number[], - contourRatio: number, - persistenceLevels: number, - ridgeCoverageThreshold: number, + evaluations: EvaluatedThreshold[], maxFdr: number, maxOccupancy: number, minPersistence: number, @@ -91,18 +136,7 @@ export function evaluateFallbackThreshold( let best: EvaluatedThreshold | undefined; let bestScore = Infinity; - for (const sigma of candidates) { - const evaluation = evaluateThreshold( - matrix, - rows, - cols, - noiseLevel, - sigma, - contourRatio, - persistenceLevels, - ridgeCoverageThreshold, - ); - + for (const evaluation of evaluations) { const score = violation(evaluation.fdr, maxFdr) * 4 + violation(evaluation.occupancy, maxOccupancy) * 2 + @@ -139,20 +173,17 @@ export function findRidgeFreeThresholdTopDown( let consecutiveRidgeLevels = 0; while (level >= minLevel) { - const evaluation = evaluateThreshold( + const ridge = evaluateRidgeScores( matrix, rows, cols, - noiseLevel, - level / noiseLevel, - contourRatio, - 1, + (level / noiseLevel) * noiseLevel, ridgeCoverageThreshold, ); const hasRidge = - evaluation.verticalRidgeScore > maxVerticalRidgeScore || - evaluation.horizontalRidgeScore > maxHorizontalRidgeScore; + ridge.vertical > maxVerticalRidgeScore || + ridge.horizontal > maxHorizontalRidgeScore; if (hasRidge) { consecutiveRidgeLevels++; @@ -224,30 +255,14 @@ export function computePersistence( return minimumPersistence; } -export function computeRidgeScores( - mask: Uint8Array, +function calculateRidgeScores( + rowCounts: Uint32Array, + colCounts: Uint32Array, rows: number, cols: number, + activePixels: number, coverageThreshold: number, ): RidgeScores { - const rowCounts = new Uint32Array(rows); - const colCounts = new Uint32Array(cols); - let activePixels = 0; - - for (let row = 0; row < rows; row++) { - for (let col = 0; col < cols; col++) { - const index = row * cols + col; - - if (!mask[index]) { - continue; - } - - activePixels++; - rowCounts[row]++; - colCounts[col]++; - } - } - if (activePixels === 0) { return { vertical: 0, @@ -281,6 +296,57 @@ export function computeRidgeScores( }; } +export function computeRidgeScores( + mask: Uint8Array, + rows: number, + cols: number, + coverageThreshold: number, +): RidgeScores { + const rowCounts = new Uint32Array(rows); + const colCounts = new Uint32Array(cols); + let activePixels = 0; + + for (let row = 0; row < rows; row++) { + for (let col = 0; col < cols; col++) { + const index = row * cols + col; + + if (!mask[index]) { + continue; + } + + activePixels++; + rowCounts[row]++; + colCounts[col]++; + } + } + + return calculateRidgeScores( + rowCounts, + colCounts, + rows, + cols, + activePixels, + coverageThreshold, + ); +} + +export function evaluateRidgeScores( + matrix: Float64Array, + rows: number, + cols: number, + threshold: number, + coverageThreshold: number, +): RidgeScores { + return evaluateThresholdMetrics( + matrix, + rows, + cols, + threshold, + coverageThreshold, + true, + ).ridge; +} + export function createMask( matrix: Float64Array, threshold: number, diff --git a/src/data/data2d/Spectrum2D/findBestMinContour/findAutomaticContourLevels.test.ts b/src/data/data2d/Spectrum2D/findBestMinContour/findAutomaticContourLevels.test.ts new file mode 100644 index 0000000000..51ecf18833 --- /dev/null +++ b/src/data/data2d/Spectrum2D/findBestMinContour/findAutomaticContourLevels.test.ts @@ -0,0 +1,82 @@ +import { expect, test } from 'vitest'; + +import { evaluateThreshold } from './evaluation.js'; +import { findAutomaticContourLevels } from './findAutomaticContourLevels.js'; + +test('skipping ridge metrics preserves threshold selection metrics', () => { + const matrix = new Float64Array([0, 2, 3, 0]); + const withRidgeMetrics = evaluateThreshold(matrix, 2, 2, 1, 2, 1.5, 2, 0.5); + const withoutRidgeMetrics = evaluateThreshold( + matrix, + 2, + 2, + 1, + 2, + 1.5, + 2, + 0.5, + ); + + expect(withoutRidgeMetrics).toEqual({ + ...withRidgeMetrics, + verticalRidgeScore: 0, + horizontalRidgeScore: 0, + }); + expect(withRidgeMetrics.verticalRidgeScore).toBe(1); + expect(withRidgeMetrics.horizontalRidgeScore).toBe(1); +}); + +test('diagnostics do not change selected contour levels', () => { + const matrix = new Float64Array(36); + matrix[7] = 3; + matrix[21] = 8; + const options = { + contourRatio: 1.6, + maxFdr: 0.01, + maxOccupancy: 0.2, + minPersistence: 0.35, + minSigma: 2, + candidateRatioExponent: 0.5, + persistenceLevels: 1, + maxVerticalRidgeScore: 0.25, + maxHorizontalRidgeScore: 0.25, + ridgeCoverageThreshold: 0.5, + }; + + const withDiagnostics = findAutomaticContourLevels(matrix, 6, 6, 1, { + ...options, + diagnostics: true, + }); + const withoutDiagnostics = findAutomaticContourLevels(matrix, 6, 6, 1, { + ...options, + diagnostics: false, + }); + + expect(withoutDiagnostics).toMatchObject({ + minLevel: withDiagnostics.minLevel, + minLevelWithoutT1Noise: withDiagnostics.minLevelWithoutT1Noise, + sigmaMultiplier: withDiagnostics.sigmaMultiplier, + sigmaMultiplierWithoutT1Noise: + withDiagnostics.sigmaMultiplierWithoutT1Noise, + }); + expect(withDiagnostics.diagnostics).toBeDefined(); + expect(withoutDiagnostics.diagnostics).toBeUndefined(); +}); + +test('fallback uses the first cached candidate when scores tie', () => { + const matrix = new Float64Array([ + 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]); + + const result = findAutomaticContourLevels(matrix, 4, 4, 1, { + contourRatio: 2, + candidateRatioExponent: 1, + minSigma: 2, + maxFdr: 0, + maxOccupancy: 0, + minPersistence: 1, + diagnostics: false, + }); + + expect(result.sigmaMultiplier).toBe(2); +}); diff --git a/src/data/data2d/Spectrum2D/findBestMinContour/findAutomaticContourLevels.ts b/src/data/data2d/Spectrum2D/findBestMinContour/findAutomaticContourLevels.ts index 235595c6b5..013b06a235 100644 --- a/src/data/data2d/Spectrum2D/findBestMinContour/findAutomaticContourLevels.ts +++ b/src/data/data2d/Spectrum2D/findBestMinContour/findAutomaticContourLevels.ts @@ -306,6 +306,7 @@ export function findAutomaticContourLevels( candidateRatioExponent, ); + const evaluations: Array> = []; let recommended: ReturnType | undefined; for (const sigmaMultiplier of candidates) { @@ -318,7 +319,9 @@ export function findAutomaticContourLevels( contourRatio, persistenceLevels, ridgeCoverageThreshold, + false, ); + evaluations.push(evaluation); const passed = evaluation.fdr <= maxFdr && @@ -333,14 +336,7 @@ export function findAutomaticContourLevels( if (!recommended) { recommended = evaluateFallbackThreshold( - values, - analysisRows, - analysisCols, - noiseLevel, - candidates, - contourRatio, - persistenceLevels, - ridgeCoverageThreshold, + evaluations, maxFdr, maxOccupancy, minPersistence, @@ -360,17 +356,6 @@ export function findAutomaticContourLevels( ridgeCoverageThreshold, ); - const ridgeFree = evaluateThreshold( - values, - analysisRows, - analysisCols, - noiseLevel, - ridgeFreeLevel / noiseLevel, - contourRatio, - 1, - ridgeCoverageThreshold, - ); - const result: AutoContourResult = { minLevel: recommended.sigmaMultiplier * noiseLevel, minLevelWithoutT1Noise: ridgeFreeLevel, @@ -379,15 +364,38 @@ export function findAutomaticContourLevels( }; if (diagnostics) { + const recommendedDiagnostics = evaluateThreshold( + values, + analysisRows, + analysisCols, + noiseLevel, + recommended.sigmaMultiplier, + contourRatio, + persistenceLevels, + ridgeCoverageThreshold, + true, + ); + const ridgeFree = evaluateThreshold( + values, + analysisRows, + analysisCols, + noiseLevel, + ridgeFreeLevel / noiseLevel, + contourRatio, + 1, + ridgeCoverageThreshold, + true, + ); + result.diagnostics = { recommended: { - sigmaMultiplier: recommended.sigmaMultiplier, - minLevel: recommended.sigmaMultiplier * noiseLevel, - occupancy: recommended.occupancy, - fdr: recommended.fdr, - persistence: recommended.persistence, - verticalRidgeScore: recommended.verticalRidgeScore, - horizontalRidgeScore: recommended.horizontalRidgeScore, + sigmaMultiplier: recommendedDiagnostics.sigmaMultiplier, + minLevel: recommendedDiagnostics.sigmaMultiplier * noiseLevel, + occupancy: recommendedDiagnostics.occupancy, + fdr: recommendedDiagnostics.fdr, + persistence: recommendedDiagnostics.persistence, + verticalRidgeScore: recommendedDiagnostics.verticalRidgeScore, + horizontalRidgeScore: recommendedDiagnostics.horizontalRidgeScore, }, ridgeFree: { sigmaMultiplier: ridgeFree.sigmaMultiplier, @@ -399,8 +407,8 @@ export function findAutomaticContourLevels( horizontalRidgeScore: ridgeFree.horizontalRidgeScore, }, hasT1Noise: - recommended.verticalRidgeScore > maxVerticalRidgeScore || - recommended.horizontalRidgeScore > maxHorizontalRidgeScore, + recommendedDiagnostics.verticalRidgeScore > maxVerticalRidgeScore || + recommendedDiagnostics.horizontalRidgeScore > maxHorizontalRidgeScore, }; } From c569a703c70ed28622b2ad2fd31b5a9f12048c40 Mon Sep 17 00:00:00 2001 From: jobo322 Date: Thu, 20 Aug 2026 06:31:30 -0500 Subject: [PATCH 11/20] chore: indexing --- .../findBestMinContour/evaluation.ts | 59 ++++++++++++++++++- .../findAutomaticContourLevels.test.ts | 58 +++++++++++++++++- .../findAutomaticContourLevels.ts | 5 ++ 3 files changed, 119 insertions(+), 3 deletions(-) diff --git a/src/data/data2d/Spectrum2D/findBestMinContour/evaluation.ts b/src/data/data2d/Spectrum2D/findBestMinContour/evaluation.ts index 7d95acc4a3..4ab3b23cb4 100644 --- a/src/data/data2d/Spectrum2D/findBestMinContour/evaluation.ts +++ b/src/data/data2d/Spectrum2D/findBestMinContour/evaluation.ts @@ -23,11 +23,50 @@ export interface RidgeScores { horizontal: number; } +export interface ThresholdIndex { + countAtLeast(threshold: number): number; +} + interface ThresholdMetrics { activePixels: number; ridge: RidgeScores; } +export function createThresholdIndex(matrix: Float64Array): ThresholdIndex { + const sortedValues = new Float64Array(matrix.length); + let valueCount = 0; + + for (const value of matrix) { + if (!Number.isNaN(value)) { + sortedValues[valueCount++] = value; + } + } + + const values = sortedValues.subarray(0, valueCount); + values.sort(); + + return { + countAtLeast(threshold) { + if (Number.isNaN(threshold)) return 0; + + let low = 0; + let high = values.length; + + while (low < high) { + const middle = low + Math.floor((high - low) / 2); + + if (values[middle] < threshold) { + low = middle + 1; + } else { + high = middle; + } + } + + return values.length - low; + }, + }; +} + function evaluateThresholdMetrics( matrix: Float64Array, rows: number, @@ -35,7 +74,15 @@ function evaluateThresholdMetrics( threshold: number, ridgeCoverageThreshold: number, includeRidgeScores: boolean, + thresholdIndex?: ThresholdIndex, ): ThresholdMetrics { + if (!includeRidgeScores && thresholdIndex) { + return { + activePixels: thresholdIndex.countAtLeast(threshold), + ridge: { vertical: 0, horizontal: 0 }, + }; + } + const rowCounts = includeRidgeScores ? new Uint32Array(rows) : undefined; const colCounts = includeRidgeScores ? new Uint32Array(cols) : undefined; let activePixels = 0; @@ -79,6 +126,7 @@ export function evaluateThreshold( persistenceLevels: number, ridgeCoverageThreshold: number, includeRidgeScores = true, + thresholdIndex?: ThresholdIndex, ): EvaluatedThreshold { const threshold = sigmaMultiplier * noiseLevel; const { activePixels, ridge } = evaluateThresholdMetrics( @@ -88,6 +136,7 @@ export function evaluateThreshold( threshold, ridgeCoverageThreshold, includeRidgeScores, + thresholdIndex, ); const totalPixels = rows * cols; const occupancy = activePixels / totalPixels; @@ -114,6 +163,7 @@ export function evaluateThreshold( threshold, contourRatio, persistenceLevels, + thresholdIndex, ); return { @@ -228,9 +278,12 @@ export function computePersistence( initialThreshold: number, contourRatio: number, levels: number, + thresholdIndex?: ThresholdIndex, ): number { let threshold = initialThreshold; - let previousArea = countValuesAboveThreshold(matrix, threshold); + let previousArea = thresholdIndex + ? thresholdIndex.countAtLeast(threshold) + : countValuesAboveThreshold(matrix, threshold); if (previousArea === 0) { return 0; @@ -241,7 +294,9 @@ export function computePersistence( for (let level = 1; level < levels; level++) { threshold *= contourRatio; - const currentArea = countValuesAboveThreshold(matrix, threshold); + const currentArea = thresholdIndex + ? thresholdIndex.countAtLeast(threshold) + : countValuesAboveThreshold(matrix, threshold); if (currentArea === 0) { return 0; diff --git a/src/data/data2d/Spectrum2D/findBestMinContour/findAutomaticContourLevels.test.ts b/src/data/data2d/Spectrum2D/findBestMinContour/findAutomaticContourLevels.test.ts index 51ecf18833..554f46eadf 100644 --- a/src/data/data2d/Spectrum2D/findBestMinContour/findAutomaticContourLevels.test.ts +++ b/src/data/data2d/Spectrum2D/findBestMinContour/findAutomaticContourLevels.test.ts @@ -1,8 +1,63 @@ import { expect, test } from 'vitest'; -import { evaluateThreshold } from './evaluation.js'; +import { + computePersistence, + countValuesAboveThreshold, + createThresholdIndex, + evaluateThreshold, +} from './evaluation.js'; import { findAutomaticContourLevels } from './findAutomaticContourLevels.js'; +test('sorted threshold counts preserve direct comparison semantics', () => { + const matrix = new Float64Array([ + Number.NaN, + Number.NEGATIVE_INFINITY, + -1, + 0, + 0, + 2, + 2, + Number.POSITIVE_INFINITY, + ]); + const thresholds = [ + Number.NEGATIVE_INFINITY, + -1, + 0, + 1, + 2, + Number.POSITIVE_INFINITY, + Number.NaN, + ]; + const thresholdIndex = createThresholdIndex(matrix); + + for (const threshold of thresholds) { + expect(thresholdIndex.countAtLeast(threshold)).toBe( + countValuesAboveThreshold(matrix, threshold), + ); + } + + expect( + computePersistence(matrix, 1, matrix.length, -1, 2, 4, thresholdIndex), + ).toBe(computePersistence(matrix, 1, matrix.length, -1, 2, 4)); + + expect( + evaluateThreshold( + matrix, + 1, + matrix.length, + 1, + 0, + 2, + 4, + 0.5, + false, + thresholdIndex, + ), + ).toEqual( + evaluateThreshold(matrix, 1, matrix.length, 1, 0, 2, 4, 0.5, false), + ); +}); + test('skipping ridge metrics preserves threshold selection metrics', () => { const matrix = new Float64Array([0, 2, 3, 0]); const withRidgeMetrics = evaluateThreshold(matrix, 2, 2, 1, 2, 1.5, 2, 0.5); @@ -15,6 +70,7 @@ test('skipping ridge metrics preserves threshold selection metrics', () => { 1.5, 2, 0.5, + false, ); expect(withoutRidgeMetrics).toEqual({ diff --git a/src/data/data2d/Spectrum2D/findBestMinContour/findAutomaticContourLevels.ts b/src/data/data2d/Spectrum2D/findBestMinContour/findAutomaticContourLevels.ts index 013b06a235..5636aa4603 100644 --- a/src/data/data2d/Spectrum2D/findBestMinContour/findAutomaticContourLevels.ts +++ b/src/data/data2d/Spectrum2D/findBestMinContour/findAutomaticContourLevels.ts @@ -1,6 +1,7 @@ import { xMaxAbsoluteValue } from 'ml-spectra-processing'; import { + createThresholdIndex, evaluateFallbackThreshold, evaluateThreshold, findMaximumNormalizedValue, @@ -297,6 +298,7 @@ export function findAutomaticContourLevels( const analysis = maxPoolAbsolute(matrix, rows, cols, maxAnalysisDimension); const { values, rows: analysisRows, cols: analysisCols } = analysis; + const thresholdIndex = createThresholdIndex(values); const maxSigmaInData = findMaximumNormalizedValue(values, noiseLevel); const candidates = generateCandidateSigmaLevels( @@ -320,6 +322,7 @@ export function findAutomaticContourLevels( persistenceLevels, ridgeCoverageThreshold, false, + thresholdIndex, ); evaluations.push(evaluation); @@ -374,6 +377,7 @@ export function findAutomaticContourLevels( persistenceLevels, ridgeCoverageThreshold, true, + thresholdIndex, ); const ridgeFree = evaluateThreshold( values, @@ -385,6 +389,7 @@ export function findAutomaticContourLevels( 1, ridgeCoverageThreshold, true, + thresholdIndex, ); result.diagnostics = { From 3b1f1c1131d0d1094e903f87f16e0a66a24dc725 Mon Sep 17 00:00:00 2001 From: jobo322 Date: Thu, 20 Aug 2026 10:58:20 -0500 Subject: [PATCH 12/20] feat: improve speed by avoid robust noise determination and max absolute value independently avoid the use of sanplot if info.noise does not exist --- src/data/data2d/Spectrum2D/contours.ts | 33 ++++----- .../estimateNoiseLevel.test.ts | 36 ++++++++++ .../findBestMinContour/estimateNoiseLevel.ts | 67 +++++++++++++++++++ .../findBestMinContour/evaluation.ts | 21 ++---- .../findAutomaticContourLevels.test.ts | 13 ++++ .../findAutomaticContourLevels.ts | 20 ++++-- 6 files changed, 151 insertions(+), 39 deletions(-) create mode 100644 src/data/data2d/Spectrum2D/findBestMinContour/estimateNoiseLevel.test.ts create mode 100644 src/data/data2d/Spectrum2D/findBestMinContour/estimateNoiseLevel.ts diff --git a/src/data/data2d/Spectrum2D/contours.ts b/src/data/data2d/Spectrum2D/contours.ts index ba30004359..51e206b77c 100644 --- a/src/data/data2d/Spectrum2D/contours.ts +++ b/src/data/data2d/Spectrum2D/contours.ts @@ -2,11 +2,11 @@ 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 { matrixMaxAbsoluteZ, matrixToArray } 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 { @@ -60,31 +60,31 @@ 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]; - const { acquisitionScheme } = info; - const { experiment = '', - //@ts-expect-error will be included in nexts versions - noise = calculateSanPlot('2D', quadrantData, { - magnitudeMode: acquisitionScheme === 'notPhaseSensitive', - }), + // @ts-expect-error will be included in nexts versions + noise, } = info; + const matrix = matrixToArray(quadrantData.z); - const max = matrixMaxAbsoluteZ(quadrantData.z); + const noiseLevel = (noise as { positive: number; negative: number }) + ? Math.max(noise.positive, noise.negative) + : estimateNoiseLevel(matrix); const bestMinLevel = findAutomaticContourLevels( - matrixToArray(quadrantData.z), + matrix, quadrantData.z.length, quadrantData.z[0].length, - Math.max(noise.positive, noise.negative), + noiseLevel, { - maxFdr: 0.01, - maxOccupancy: 0.01, + maxFdr: 0.005, + maxOccupancy: 0.005, persistenceLevels: 5, contourRatio: 1.8, + minPersistence: 0.5, maxVerticalRidgeScore: 0.05, maxHorizontalRidgeScore: 0.05, - ridgeCoverageThreshold: experiment.includes('jres') ? 0.8 : 0.1, + ridgeCoverageThreshold: experiment.includes('jres') ? 0.9 : 0.1, }, ); @@ -92,17 +92,18 @@ function getDefaultContoursLevel(spectrum: Spectrum2D, quadrant = 'rr') { diagnostics: { hasT1Noise }, minLevelWithoutT1Noise, minLevel, + maxAbsoluteValue, } = bestMinLevel; - console.log(hasT1Noise, minLevelWithoutT1Noise, minLevel); const minContourLevel = Math.min( calculateValueOfLevel( hasT1Noise ? minLevelWithoutT1Noise : minLevel, - max, + maxAbsoluteValue, true, ), DEFAULT_CONTOURS_OPTIONS.positive.contourLevels[1] - DEFAULT_CONTOURS_OPTIONS.positive.numberOfLayers, ); + const defaultLevel: ContourOptions = { negative: { numberOfLayers: DEFAULT_CONTOURS_OPTIONS.negative.numberOfLayers, diff --git a/src/data/data2d/Spectrum2D/findBestMinContour/estimateNoiseLevel.test.ts b/src/data/data2d/Spectrum2D/findBestMinContour/estimateNoiseLevel.test.ts new file mode 100644 index 0000000000..d184f6bcc2 --- /dev/null +++ b/src/data/data2d/Spectrum2D/findBestMinContour/estimateNoiseLevel.test.ts @@ -0,0 +1,36 @@ +import { expect, test } from 'vitest'; + +import { estimateNoiseLevel } from './estimateNoiseLevel.js'; + +const GAUSSIAN_MAD_SCALE = 1 / 0.6744897501960817; + +test('estimates Gaussian noise from a downsampled matrix', () => { + const matrix = new Float64Array([-2, 99, -1, 99, 1, 99, 2, 99]); + + expect(estimateNoiseLevel(matrix, { maxSamples: 4 })).toBeCloseTo( + GAUSSIAN_MAD_SCALE, + ); +}); + +test('ignores non-finite sampled values', () => { + const matrix = new Float64Array([ + Number.NaN, + -2, + 0, + Number.POSITIVE_INFINITY, + 1, + Number.NEGATIVE_INFINITY, + 2, + ]); + + expect(estimateNoiseLevel(matrix)).toBeCloseTo(GAUSSIAN_MAD_SCALE); +}); + +test('rejects invalid sample sizes and zero MAD', () => { + expect(() => + estimateNoiseLevel(new Float64Array([0, 1]), { maxSamples: 0 }), + ).toThrow('maxSamples must be a positive integer'); + expect(() => estimateNoiseLevel(new Float64Array([0, 0]))).toThrow( + 'unable to estimate a positive noise level', + ); +}); diff --git a/src/data/data2d/Spectrum2D/findBestMinContour/estimateNoiseLevel.ts b/src/data/data2d/Spectrum2D/findBestMinContour/estimateNoiseLevel.ts new file mode 100644 index 0000000000..a18ee170a3 --- /dev/null +++ b/src/data/data2d/Spectrum2D/findBestMinContour/estimateNoiseLevel.ts @@ -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, + 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, + 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; +} diff --git a/src/data/data2d/Spectrum2D/findBestMinContour/evaluation.ts b/src/data/data2d/Spectrum2D/findBestMinContour/evaluation.ts index 4ab3b23cb4..36dfbad2c4 100644 --- a/src/data/data2d/Spectrum2D/findBestMinContour/evaluation.ts +++ b/src/data/data2d/Spectrum2D/findBestMinContour/evaluation.ts @@ -16,6 +16,7 @@ export interface AnalysisMatrix { values: Float64Array; rows: number; cols: number; + maxAbsoluteValue: number; } export interface RidgeScores { @@ -427,6 +428,7 @@ export function maxPoolAbsolute( const analysisRows = Math.ceil(rows / scale); const analysisCols = Math.ceil(cols / scale); const values = new Float64Array(analysisRows * analysisCols); + let maxAbsoluteValue = 0; for (let analysisRow = 0; analysisRow < analysisRows; analysisRow++) { const sourceRowStart = analysisRow * scale; @@ -451,6 +453,7 @@ export function maxPoolAbsolute( } values[analysisRow * analysisCols + analysisCol] = maximum; + maxAbsoluteValue = Math.max(maxAbsoluteValue, maximum); } } @@ -458,6 +461,7 @@ export function maxPoolAbsolute( values, rows: analysisRows, cols: analysisCols, + maxAbsoluteValue, }; } @@ -485,20 +489,3 @@ export function countValuesAboveThreshold( return count; } - -export function findMaximumNormalizedValue( - matrix: Float64Array, - noiseLevel: number, -): number { - let maximum = 0; - - for (const value of matrix) { - const normalized = value / noiseLevel; - - if (normalized > maximum) { - maximum = normalized; - } - } - - return maximum; -} diff --git a/src/data/data2d/Spectrum2D/findBestMinContour/findAutomaticContourLevels.test.ts b/src/data/data2d/Spectrum2D/findBestMinContour/findAutomaticContourLevels.test.ts index 554f46eadf..c71e99de51 100644 --- a/src/data/data2d/Spectrum2D/findBestMinContour/findAutomaticContourLevels.test.ts +++ b/src/data/data2d/Spectrum2D/findBestMinContour/findAutomaticContourLevels.test.ts @@ -5,9 +5,21 @@ import { countValuesAboveThreshold, createThresholdIndex, evaluateThreshold, + maxPoolAbsolute, } from './evaluation.js'; import { findAutomaticContourLevels } from './findAutomaticContourLevels.js'; +test('maxPoolAbsolute returns the global finite absolute maximum', () => { + const analysis = maxPoolAbsolute( + new Float64Array([-3, Number.NaN, 2, -7, Number.POSITIVE_INFINITY, 4]), + 2, + 3, + 2, + ); + + expect(analysis.maxAbsoluteValue).toBe(7); +}); + test('sorted threshold counts preserve direct comparison semantics', () => { const matrix = new Float64Array([ Number.NaN, @@ -28,6 +40,7 @@ test('sorted threshold counts preserve direct comparison semantics', () => { Number.POSITIVE_INFINITY, Number.NaN, ]; + const thresholdIndex = createThresholdIndex(matrix); for (const threshold of thresholds) { diff --git a/src/data/data2d/Spectrum2D/findBestMinContour/findAutomaticContourLevels.ts b/src/data/data2d/Spectrum2D/findBestMinContour/findAutomaticContourLevels.ts index 5636aa4603..088d52eb3b 100644 --- a/src/data/data2d/Spectrum2D/findBestMinContour/findAutomaticContourLevels.ts +++ b/src/data/data2d/Spectrum2D/findBestMinContour/findAutomaticContourLevels.ts @@ -1,10 +1,7 @@ -import { xMaxAbsoluteValue } from 'ml-spectra-processing'; - import { createThresholdIndex, evaluateFallbackThreshold, evaluateThreshold, - findMaximumNormalizedValue, findRidgeFreeThresholdTopDown, generateCandidateSigmaLevels, maxPoolAbsolute, @@ -51,6 +48,10 @@ export type AutoContourResult = { */ sigmaMultiplier: number; sigmaMultiplierWithoutT1Noise: number; + /** + * Maximum absolute value in the analysis matrix used for contour level selection. + */ + maxAbsoluteValue: number; } & (TDiagnostics extends true ? { diagnostics: AutoContourDiagnostics; @@ -297,10 +298,15 @@ export function findAutomaticContourLevels( } const analysis = maxPoolAbsolute(matrix, rows, cols, maxAnalysisDimension); - const { values, rows: analysisRows, cols: analysisCols } = analysis; + const { + values, + rows: analysisRows, + cols: analysisCols, + maxAbsoluteValue, + } = analysis; const thresholdIndex = createThresholdIndex(values); - const maxSigmaInData = findMaximumNormalizedValue(values, noiseLevel); + const maxSigmaInData = maxAbsoluteValue / noiseLevel; const candidates = generateCandidateSigmaLevels( minSigma, maxSigmaInData, @@ -352,7 +358,7 @@ export function findAutomaticContourLevels( analysisCols, noiseLevel, recommended.sigmaMultiplier * noiseLevel, - xMaxAbsoluteValue(matrix as Float64Array), + maxAbsoluteValue, contourRatio, maxVerticalRidgeScore, maxHorizontalRidgeScore, @@ -360,8 +366,10 @@ export function findAutomaticContourLevels( ); const result: AutoContourResult = { + maxAbsoluteValue, minLevel: recommended.sigmaMultiplier * noiseLevel, minLevelWithoutT1Noise: ridgeFreeLevel, + sigmaMultiplier: recommended.sigmaMultiplier, sigmaMultiplierWithoutT1Noise: ridgeFreeLevel / noiseLevel, }; From 3b9515c5f78f614ba5004d77742036644581314d Mon Sep 17 00:00:00 2001 From: jobo322 Date: Thu, 20 Aug 2026 11:12:43 -0500 Subject: [PATCH 13/20] chore: update package-lock.json --- package-lock.json | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index a9d4e27748..4857ff5d50 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2209,9 +2209,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2229,9 +2226,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2249,9 +2243,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2269,9 +2260,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2289,9 +2277,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2309,9 +2294,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2329,9 +2311,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2349,9 +2328,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ From c4dd286e7d353368e2a0eb0d76adb6166a123500 Mon Sep 17 00:00:00 2001 From: hamed musallam Date: Fri, 21 Aug 2026 13:25:11 +0200 Subject: [PATCH 14/20] refactor: drop unused exports drop unused exports: RidgeScores, AutoContourDiagnostics, ThresholdDiagnostics, erfc, and evaluateRidgeScores are only used within the findBestMinContour module, Also removes computeRidgeScores, createMask, and countActivePixels, which were unused code. --- .../findBestMinContour/evaluation.ts | 79 +++---------------- .../findAutomaticContourLevels.ts | 4 +- .../Spectrum2D/findBestMinContour/math.ts | 2 +- 3 files changed, 13 insertions(+), 72 deletions(-) diff --git a/src/data/data2d/Spectrum2D/findBestMinContour/evaluation.ts b/src/data/data2d/Spectrum2D/findBestMinContour/evaluation.ts index 36dfbad2c4..604009a8eb 100644 --- a/src/data/data2d/Spectrum2D/findBestMinContour/evaluation.ts +++ b/src/data/data2d/Spectrum2D/findBestMinContour/evaluation.ts @@ -19,7 +19,7 @@ export interface AnalysisMatrix { maxAbsoluteValue: number; } -export interface RidgeScores { +interface RidgeScores { vertical: number; horizontal: number; } @@ -105,13 +105,13 @@ function evaluateThresholdMetrics( const ridge = rowCounts && colCounts ? calculateRidgeScores( - rowCounts, - colCounts, - rows, - cols, - activePixels, - ridgeCoverageThreshold, - ) + rowCounts, + colCounts, + rows, + cols, + activePixels, + ridgeCoverageThreshold, + ) : { vertical: 0, horizontal: 0 }; return { activePixels, ridge }; @@ -352,41 +352,7 @@ function calculateRidgeScores( }; } -export function computeRidgeScores( - mask: Uint8Array, - rows: number, - cols: number, - coverageThreshold: number, -): RidgeScores { - const rowCounts = new Uint32Array(rows); - const colCounts = new Uint32Array(cols); - let activePixels = 0; - - for (let row = 0; row < rows; row++) { - for (let col = 0; col < cols; col++) { - const index = row * cols + col; - - if (!mask[index]) { - continue; - } - - activePixels++; - rowCounts[row]++; - colCounts[col]++; - } - } - - return calculateRidgeScores( - rowCounts, - colCounts, - rows, - cols, - activePixels, - coverageThreshold, - ); -} - -export function evaluateRidgeScores( +function evaluateRidgeScores( matrix: Float64Array, rows: number, cols: number, @@ -403,21 +369,6 @@ export function evaluateRidgeScores( ).ridge; } -export function createMask( - matrix: Float64Array, - threshold: number, -): Uint8Array { - const mask = new Uint8Array(matrix.length); - - for (const [index, value] of matrix.entries()) { - if (value >= threshold) { - mask[index] = 1; - } - } - - return mask; -} - export function maxPoolAbsolute( matrix: ArrayLike, rows: number, @@ -465,17 +416,7 @@ export function maxPoolAbsolute( }; } -export function countActivePixels(mask: Uint8Array): number { - let count = 0; - - for (const value of mask) { - count += value; - } - - return count; -} - -export function countValuesAboveThreshold( +function countValuesAboveThreshold( matrix: Float64Array, threshold: number, ): number { diff --git a/src/data/data2d/Spectrum2D/findBestMinContour/findAutomaticContourLevels.ts b/src/data/data2d/Spectrum2D/findBestMinContour/findAutomaticContourLevels.ts index 088d52eb3b..2ac93d9d1d 100644 --- a/src/data/data2d/Spectrum2D/findBestMinContour/findAutomaticContourLevels.ts +++ b/src/data/data2d/Spectrum2D/findBestMinContour/findAutomaticContourLevels.ts @@ -8,7 +8,7 @@ import { } from './evaluation.js'; import { validateInput } from './math.js'; -export interface AutoContourDiagnostics { +interface AutoContourDiagnostics { recommended: ThresholdDiagnostics; ridgeFree: ThresholdDiagnostics; @@ -19,7 +19,7 @@ export interface AutoContourDiagnostics { hasT1Noise: boolean; } -export interface ThresholdDiagnostics { +interface ThresholdDiagnostics { sigmaMultiplier: number; minLevel: number; diff --git a/src/data/data2d/Spectrum2D/findBestMinContour/math.ts b/src/data/data2d/Spectrum2D/findBestMinContour/math.ts index bfda0ff998..b8e33c719d 100644 --- a/src/data/data2d/Spectrum2D/findBestMinContour/math.ts +++ b/src/data/data2d/Spectrum2D/findBestMinContour/math.ts @@ -6,7 +6,7 @@ export function gaussianTwoSidedTail(k: number): number { * Approximation of the complementary error function. * @TODO it would be replaced by the same implementation from ml-spectra-processing. */ -export function erfc(x: number): number { +function erfc(x: number): number { const sign = x < 0 ? -1 : 1; const ax = Math.abs(x); From 074cb80a386edff212e71e0b39183b648708f459 Mon Sep 17 00:00:00 2001 From: hamed musallam Date: Fri, 21 Aug 2026 13:33:31 +0200 Subject: [PATCH 15/20] chore: update package.json and package-lock.json --- package-lock.json | 826 +++++++++++++++++++++++++--------------------- package.json | 2 +- 2 files changed, 442 insertions(+), 386 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4857ff5d50..6f93913094 100644 --- a/package-lock.json +++ b/package-lock.json @@ -180,7 +180,6 @@ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -441,7 +440,6 @@ "resolved": "https://registry.npmjs.org/@blueprintjs/core/-/core-6.18.0.tgz", "integrity": "sha512-EQVKWl/RFrPHfD42Bcicvxqwjwh+OnNSroXOdmnJZC+FP1xlcawOjTl38ewgJEaCZ6txcqiw/CGAft7/8zJ4RQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@blueprintjs/colors": "^5.1.16", "@blueprintjs/icons": "^6.13.0", @@ -473,7 +471,6 @@ "resolved": "https://registry.npmjs.org/@blueprintjs/icons/-/icons-6.13.0.tgz", "integrity": "sha512-wEQgADFPwufKiKeF/L5K21bKgouuIbIdYPvMPFv2tt7reSuCEftX0dZpga+21JY4KvEJIz+xtVJoqMmXBesiwQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "change-case": "^4.1.2", "classnames": "^2.3.1", @@ -495,7 +492,6 @@ "resolved": "https://registry.npmjs.org/@blueprintjs/select/-/select-6.3.4.tgz", "integrity": "sha512-vJa6MrXgPyQt4PRc9Z6c1vaHAKJMI1zloVph5eKOfv7YjSb1WlbH/bFWKunANxqSlncfug6FfaHkIRh7UYZ1Wg==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@blueprintjs/colors": "^5.1.16", "@blueprintjs/core": "^6.18.0", @@ -550,7 +546,6 @@ "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@keyv/serialize": "^1.1.1" } @@ -709,7 +704,6 @@ "integrity": "sha512-kLgLShnWADDVreKC63pBrWkcvxgZzFIfO34Jhx/SWfuOIA3cD8AXT+HjyuLfoGJ7mUb58hv2kUziKzEy4INb1w==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=22.18.0" } @@ -766,9 +760,9 @@ "license": "MIT" }, "node_modules/@cspell/dict-cpp": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@cspell/dict-cpp/-/dict-cpp-7.0.2.tgz", - "integrity": "sha512-dfbeERiVNeqmo/npivdR6rDiBCqZi3QtjH2Z0HFcXwpdj6i97dX1xaKyK2GUsO/p4u1TOv63Dmj5Vm48haDpuA==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@cspell/dict-cpp/-/dict-cpp-7.1.0.tgz", + "integrity": "sha512-rcjycobioQUd9Jm/Y1n8U+sq6ytpZ0iV8AYIo+vyEFfutC5x7g6hL6Fh3bCdh6IporGHRqfEutGK/4sfopD2ZA==", "dev": true, "license": "MIT" }, @@ -791,8 +785,7 @@ "resolved": "https://registry.npmjs.org/@cspell/dict-css/-/dict-css-4.1.2.tgz", "integrity": "sha512-+ylGoKdwZ2sVOCOnU2Eq5wDZx+RaVX3HoKyNHGGsFvhSw6IidQ6tH/mAPKBDofViHJoWCPNlklE0lTr6MDG3QA==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@cspell/dict-dart": { "version": "2.3.2", @@ -844,9 +837,9 @@ "license": "MIT" }, "node_modules/@cspell/dict-en-common-misspellings": { - "version": "2.1.13", - "resolved": "https://registry.npmjs.org/@cspell/dict-en-common-misspellings/-/dict-en-common-misspellings-2.1.13.tgz", - "integrity": "sha512-00rpydUxKNWY2xxrSx+h46aNWLvbkJdd57SsnEFt24fbs1fROhXZ6XSQu+gQz/zNuiCvFi4Ro3ej9DLbEdWQmQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cspell/dict-en-common-misspellings/-/dict-en-common-misspellings-2.2.0.tgz", + "integrity": "sha512-5PmCHv+AhY0LVNo3bE1FdRKMmw1esKj83GPwLymnVPYNtlI2Jf6y27EjC0azg4zny5keWmku0GQiMf55Fi+qeA==", "dev": true, "license": "CC BY-SA 4.0" }, @@ -907,9 +900,9 @@ "license": "MIT" }, "node_modules/@cspell/dict-golang": { - "version": "6.0.26", - "resolved": "https://registry.npmjs.org/@cspell/dict-golang/-/dict-golang-6.0.26.tgz", - "integrity": "sha512-YKA7Xm5KeOd14v5SQ4ll6afe9VSy3a2DWM7L9uBq4u3lXToRBQ1W5PRa+/Q9udd+DTURyVVnQ+7b9cnOlNxaRg==", + "version": "6.0.27", + "resolved": "https://registry.npmjs.org/@cspell/dict-golang/-/dict-golang-6.0.27.tgz", + "integrity": "sha512-rRDb2JL8EefaezHGTEclKXCs4eMOS0q/datk94Lm7KQOhlGlzS9FDVZiR1/guh0o+iJCmejQuf5NR2FQeQSWsg==", "dev": true, "license": "MIT" }, @@ -928,20 +921,18 @@ "license": "MIT" }, "node_modules/@cspell/dict-html": { - "version": "4.0.15", - "resolved": "https://registry.npmjs.org/@cspell/dict-html/-/dict-html-4.0.15.tgz", - "integrity": "sha512-GJYnYKoD9fmo2OI0aySEGZOjThnx3upSUvV7mmqUu8oG+mGgzqm82P/f7OqsuvTaInZZwZbo+PwJQd/yHcyFIw==", + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@cspell/dict-html/-/dict-html-4.0.16.tgz", + "integrity": "sha512-9B6/Cpb5YVcYgsEAlKudun1yd4ONllYS/ZibKLYea2apPR+l2vQdARnPrP9kTqa7NUNfRKl7m9Fb6k9rjimnow==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@cspell/dict-html-symbol-entities": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@cspell/dict-html-symbol-entities/-/dict-html-symbol-entities-4.0.5.tgz", "integrity": "sha512-429alTD4cE0FIwpMucvSN35Ld87HCyuM8mF731KU5Rm4Je2SG6hmVx7nkBsLyrmH3sQukTcr1GaiZsiEg8svPA==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@cspell/dict-java": { "version": "5.0.12", @@ -1000,22 +991,22 @@ "license": "MIT" }, "node_modules/@cspell/dict-markdown": { - "version": "2.0.17", - "resolved": "https://registry.npmjs.org/@cspell/dict-markdown/-/dict-markdown-2.0.17.tgz", - "integrity": "sha512-H8bAxih6U8NOnSPL7R8My+tqjaB4tmnJTjERuz4zYqmf+cH+5xshX3UVgKlwWFcyjsYfv/zEDuRdMctQv1q6HQ==", + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/@cspell/dict-markdown/-/dict-markdown-2.0.18.tgz", + "integrity": "sha512-KLRSwVrwKDz4n7bl2XQjzvaBZbGgx5QKkP5Ok1b24xaO3TpXsLhAbAn1mItvFlI2tF6Rkt83iYjQcYppywuf5Q==", "dev": true, "license": "MIT", "peerDependencies": { "@cspell/dict-css": "^4.1.2", - "@cspell/dict-html": "^4.0.15", + "@cspell/dict-html": "^4.0.16", "@cspell/dict-html-symbol-entities": "^4.0.5", "@cspell/dict-typescript": "^3.2.3" } }, "node_modules/@cspell/dict-monkeyc": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/@cspell/dict-monkeyc/-/dict-monkeyc-1.0.12.tgz", - "integrity": "sha512-MN7Vs11TdP5mbdNFQP5x2Ac8zOBm97ARg6zM5Sb53YQt/eMvXOMvrep7+/+8NJXs0jkp70bBzjqU4APcqBFNAw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@cspell/dict-monkeyc/-/dict-monkeyc-1.1.0.tgz", + "integrity": "sha512-mc/hgvSy/emOIYtc8kuVEaLUlnaERunfAubfJ5plUXq6s0A69bcukJzUzSt9C0UTCwS28641ILRPv80m3L9QbA==", "dev": true, "license": "MIT" }, @@ -1027,9 +1018,9 @@ "license": "MIT" }, "node_modules/@cspell/dict-npm": { - "version": "5.2.43", - "resolved": "https://registry.npmjs.org/@cspell/dict-npm/-/dict-npm-5.2.43.tgz", - "integrity": "sha512-H2gYwtu59dNO9662Uq0usfuhyNd7lZJE1C61a/UXcpRyWWSrTo2Bz+vwGYp1bXZ1LmjXadqvwJ8ArFlGdiadNQ==", + "version": "5.2.46", + "resolved": "https://registry.npmjs.org/@cspell/dict-npm/-/dict-npm-5.2.46.tgz", + "integrity": "sha512-Z5GG59c17UEk6BPZ3lVD/++GvPBI+O05OTGm4sRA/0I/qszbstuYRw+j2//5f8SWzOFXKaGYI8kygwKX9EQZ9Q==", "dev": true, "license": "MIT" }, @@ -1055,9 +1046,9 @@ "license": "MIT" }, "node_modules/@cspell/dict-python": { - "version": "4.2.29", - "resolved": "https://registry.npmjs.org/@cspell/dict-python/-/dict-python-4.2.29.tgz", - "integrity": "sha512-OnEt1a35iuQzc2Ize1qU/43ZyF10urRKAm+mlTz++vnAgDLBHpKfWakpSK50nyL5/1WvyQ8BaMjb52MBLEpTeA==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@cspell/dict-python/-/dict-python-4.4.0.tgz", + "integrity": "sha512-49Eju/a97V6ExAeMJDM2Tk4jsYr0pm+kQzqQrKLAZdrRd6VrLIgseI481EQSmgoVkPPdsuwK1xj9ZlpDQIF4qg==", "dev": true, "license": "MIT", "dependencies": { @@ -1072,9 +1063,9 @@ "license": "MIT" }, "node_modules/@cspell/dict-ruby": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@cspell/dict-ruby/-/dict-ruby-5.1.1.tgz", - "integrity": "sha512-LHrp84oEV6q1ZxPPyj4z+FdKyq1XAKYPtmGptrd+uwHbrF/Ns5+fy6gtSi7pS+uc0zk3JdO9w/tPK+8N1/7WUA==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@cspell/dict-ruby/-/dict-ruby-5.1.2.tgz", + "integrity": "sha512-f6Slf11N91ittf71AWlfVVG9GZPezVRBfcMPCTNTWhUsY/VEspkBRZYDhPXjV4df3jqHy5YJs8nlsFcFVF/bMA==", "dev": true, "license": "MIT" }, @@ -1100,9 +1091,9 @@ "license": "MIT" }, "node_modules/@cspell/dict-software-terms": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/@cspell/dict-software-terms/-/dict-software-terms-5.2.4.tgz", - "integrity": "sha512-z6y/TGH3QNf5wB4pVvN/P3GfFEW/Whf6QAekNsIn06VKl95dnamfpkPWqV8rEtCixQFaKalb5+y9hRQXH3XQ1g==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/@cspell/dict-software-terms/-/dict-software-terms-5.4.1.tgz", + "integrity": "sha512-tY88gnOWj2ax6h+i7BjU7dIH1f3fvY4WypS0a+TpjBgXC318AdOMmy0zK1SzxGNOjhWJfCeCZNVoLFM/DuOG4g==", "dev": true, "license": "MIT" }, @@ -1139,8 +1130,7 @@ "resolved": "https://registry.npmjs.org/@cspell/dict-typescript/-/dict-typescript-3.2.3.tgz", "integrity": "sha512-zXh1wYsNljQZfWWdSPYwQhpwiuW0KPW1dSd8idjMRvSD0aSvWWHoWlrMsmZeRl4qM4QCEAjua8+cjflm41cQBg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@cspell/dict-vue": { "version": "3.0.5", @@ -1250,7 +1240,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" }, @@ -1259,9 +1248,9 @@ } }, "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.7.tgz", - "integrity": "sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig==", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.8.tgz", + "integrity": "sha512-CpMLjAvwQg3BL5S0IeqsZNMH7EQrEWi0kLKOC13ZBF0ZwERiLWlibNPJr8G1kdU3Ms/r2KiNrF81pUh2HwAHdg==", "dev": true, "funding": [ { @@ -1299,7 +1288,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" } @@ -1380,10 +1368,33 @@ "integrity": "sha512-SlJDfG6RPeEX8wEVv6ZB3kak4MmbtyiI2qX/5zuKdordbrhB/iaJ58GVMZgJ6P1sJaM1gMgENFYYeg1JWrCFrA==", "license": "MIT" }, + "node_modules/@emnapi/core": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.2.tgz", + "integrity": "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@emnapi/wasi-threads": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz", - "integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", "dev": true, "license": "MIT", "optional": true, @@ -1449,7 +1460,6 @@ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -2023,9 +2033,9 @@ "license": "MIT" }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.2.tgz", - "integrity": "sha512-JfB4kuJQjaoHuCTseIINHtHWeJnvgEcxjwA5t/Y00ZgaOO1Crz3fjT/p8kT28zA/Caz7oiUMn3d6H2yOVCVwuw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.3.tgz", + "integrity": "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==", "dev": true, "license": "MIT", "optional": true, @@ -2040,8 +2050,8 @@ "url": "https://github.com/sponsors/Brooooooklyn" }, "peerDependencies": { - "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.3", - "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.3" + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" } }, "node_modules/@nodelib/fs.scandir": { @@ -2209,6 +2219,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2226,6 +2239,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2243,6 +2259,9 @@ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2260,6 +2279,9 @@ "riscv64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2277,6 +2299,9 @@ "riscv64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2294,6 +2319,9 @@ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2311,6 +2339,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2328,6 +2359,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2406,9 +2440,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.142.0.tgz", - "integrity": "sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==", + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.143.0.tgz", + "integrity": "sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==", "dev": true, "license": "MIT", "funding": { @@ -2521,6 +2555,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2535,6 +2572,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2549,6 +2589,9 @@ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2563,6 +2606,9 @@ "riscv64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2577,6 +2623,9 @@ "riscv64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2591,6 +2640,9 @@ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2605,6 +2657,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2619,6 +2674,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2658,40 +2716,6 @@ "node": ">=14.0.0" } }, - "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.2.tgz", - "integrity": "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", - "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", - "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@oxc-resolver/binding-win32-arm64-msvc": { "version": "11.24.2", "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.24.2.tgz", @@ -2741,7 +2765,6 @@ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" @@ -2847,10 +2870,27 @@ "react-dom": ">=16.9.0" } }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.5.tgz", + "integrity": "sha512-DLe/i+l8ynIBY7XEQ191TeZvCoowIGa18R+dIV30GW7DiOtp74i/xX8hs8GUjW5ARV7VZuie3d6AumSmCwbeRA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.2.tgz", - "integrity": "sha512-l7x215OGvo1s52JWmR8U/DAVzEDWBCIbTm28aeJV/WDTSHgcKXaZTuBT0hJMs5NggilfJTW3clZVvd24yfKJxA==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.5.tgz", + "integrity": "sha512-zXcwKlQApYAOELHd8PwKDFkagYF9Wy4e0RJ+0qnzl9Pjnpj75TEG8ufv40p2J7kCEfwZAsNiuzRIyNNMWT38ig==", "cpu": [ "arm64" ], @@ -2865,9 +2905,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.2.tgz", - "integrity": "sha512-9u9Xv6c1AJZT0FfwH5vrMG5Jjcwhc1MlyrPu0XfTqkzsmqfks2M6W/o5XwAJgVVN/jHpqqngC1WevHKKTIUtIA==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.5.tgz", + "integrity": "sha512-dK4QakI42nzWgJT5sm4y4y/O//D4OxM75/cH28RLV+nzIN9AY+YsbuUVrUTjlLjXR6vpyxFbSsbmNuJ6BP9sww==", "cpu": [ "arm64" ], @@ -2882,9 +2922,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.2.tgz", - "integrity": "sha512-9W1mbGZAfW3oqd85bhBkmpyHCCzL1TeG/zFFP3vg7b0rlly8cxOcre5nXwz+LHazCwac2MNWgPdPCHndABjpWQ==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.5.tgz", + "integrity": "sha512-fqSALaUu1Wjd1nK2uW2kJDWdLCc8lx1IcY+MTY26Aurfdx19anlzhqXOgCFbBFQnlFDTn4TC1/7Nz4Bl2mLP3A==", "cpu": [ "x64" ], @@ -2899,9 +2939,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.2.tgz", - "integrity": "sha512-0p1lhiCSCyaerFwtrdZQUx7NqGk6LQnaRKWX7tFQqwQgvX0rjM15cIkm3pax1UpEakK14C4mOxx/jSqCBdBRqQ==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.5.tgz", + "integrity": "sha512-/vCnNxlkxs9tKxNDcyWUePpJ/PgTzxIaVhoM5SmG8UV+GR/IcPam4VYxi7GIMo7PSDuNqlJqvprqii9NqqVCMw==", "cpu": [ "x64" ], @@ -2916,9 +2956,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.2.tgz", - "integrity": "sha512-e+cOJXrJ2L3zx6YzqPg+f6Wbk3V1cKB8bOhbaYdVYN3DdquzNdRAmrbETz1qnt5yp/c7JNlNjmITiA2cVneQ7w==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.5.tgz", + "integrity": "sha512-abk0NLA519LxRCszmbE0jYKuQ9YPocOXTiOXOo6Yr+YAT95VH+PtqYAjOJvGKt3viEd/x4qzabAlwd5bHOOARg==", "cpu": [ "arm" ], @@ -2933,13 +2973,16 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.2.tgz", - "integrity": "sha512-JsSMsj6sNat/MuhG5fnBD7QgbtpHKVe30x5/bAVirDHdhoQRXJkF6xc0Jqk8O4fiCUQAzMOoH9wZi3m60c8wtg==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.5.tgz", + "integrity": "sha512-Y7eALiJ8lr0M2HH103Js+g7V34wf6snlpZLAsHI90uLhr3PVlNsbFVAXJC9d/V6BnPyKtpSwI+NcB/RLxsQxuA==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2950,13 +2993,16 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.2.tgz", - "integrity": "sha512-B5G/zJdHaoJn9vD50eGHWkiWfmq8Uhi3IiLPJTzmZTrAalk1bztUikSXo0qga18ibE0IXboyeMUnhPjhAJ45wQ==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.5.tgz", + "integrity": "sha512-xMvZgnbZg4YVnR/AX2b3oOPDTFYJvUVaJg5FedA/LuvexAtXibZQej4cnTkw3rjsJ/ggUROB64TdtETiim+FYA==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2967,13 +3013,16 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.2.tgz", - "integrity": "sha512-6mC/awzKka8W6EoekjegpfGkjz8jXWDX63pqu/HYVpyKtZfu65Jsh4QAH3Kej3CAv/c1oGX7psTmFEbr0mDxLA==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.5.tgz", + "integrity": "sha512-GRjeqTUDHTo5GwntsLaAMcBahG3nlpjftXWZLN73HiYQlhwEowvarFgQnRnQZtIp4keXX7quXFbG38uPZBa2EA==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2984,13 +3033,16 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.2.tgz", - "integrity": "sha512-412MX9fJLdA1IK28EZnc8jYv2HRTleOZgfLQumJ5zy7OeJLZlg/CETwFaXjNmGVxG51cFHpKLqb5LKvBC+HsHA==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.5.tgz", + "integrity": "sha512-vLNTR45F2Uwc8AufkNXPmB4VliaXs+FvcheEogIzOXzO4l+LzieXF5A/TWxLy5HtqpsRCHUfd0lPVrrdgXdLHQ==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3001,13 +3053,16 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.2.tgz", - "integrity": "sha512-Q/+HI/ToJafZ1iCqGgVQXUEkIjufHCTF0gBQ2a5o3cg7GJ2h0qyq3nvvSmU+bGda2/7ygXpTY4TM6gO9OhQ0ZA==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.5.tgz", + "integrity": "sha512-Mgj59/HTuYeK9Gz2MA+mBWKnHsAgkBSec15ZMb1st3oIfFbX7gCjOae7GydHhzcyQi9Z/7M1QuN9bR3oFqF0jQ==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3018,13 +3073,16 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.2.tgz", - "integrity": "sha512-ZKp/w41n6wCvxzxQHtQSbuphfX3Y4cCvbjkKHusrLx4lh+JWLTU7StSltO/DKARISzbj368d+qUaCjI8K2wzXw==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.5.tgz", + "integrity": "sha512-mY8AP0/ichsbhAxGnLa3d3+MwV0EfgrPND2bplI3Ym8T6R2pJ0N87bvrKVwNXmdy3jnr6eQBecdqx/HMknBmpA==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -3035,9 +3093,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.2.tgz", - "integrity": "sha512-pxE6xD4KS3eAROkKK5yrhB9/3+vhlhVGMvlQLbdpzrBGDbKrnzx3RLwPaHvasLo6jgaiBLn7e04Df9C4tYhjmA==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.5.tgz", + "integrity": "sha512-8SLssA2oweAxyRgDp789ACfRb/3P+zNRJpzZxSizxF9m8NUDQ4+3xjo8ttjhVGGw6Qxb70oZiEtIjaKikCO7Yw==", "cpu": [ "arm64" ], @@ -3052,9 +3110,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.2.tgz", - "integrity": "sha512-4MqEue5re+xIZzAWsB8sj0P1kqZySWqIuN4t6QaIO/YA6SFwySOLruvWQFzfmqk8LBK2P30KCSJwf6mJCZZ5/A==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.5.tgz", + "integrity": "sha512-vGbruD5zquhoc8D9SViXgN2FBJtNdTyQ4DtG+SWiEGlJiAzoKcZ2xp+xuXCffhubVdt0NJlTZqkeRuERy7g8Cw==", "cpu": [ "arm64" ], @@ -3069,9 +3127,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.2.tgz", - "integrity": "sha512-NweNxxD0Nf9t8v7kodun45Ijp3EIwYY+uydPP6qBEYvfBqhIjN6dZMzlQja3tqX/aLs3F3Uz+AxDpKgRhpOZQg==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.5.tgz", + "integrity": "sha512-e/SXpgISz+IoqVcSSI0rx/d/he8zqLex+/rCWpnHpmVfmPIUjag9H6P7zotf0gJHwPUhQxZ/mF8tr6acebT9yw==", "cpu": [ "x64" ], @@ -3140,8 +3198,7 @@ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "devOptional": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@standard-schema/utils": { "version": "0.3.0", @@ -3233,13 +3290,33 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/@tanstack/react-table": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-9.1.2.tgz", + "integrity": "sha512-YQPZFJ1nIi/bjjwsPZVouABgahDcl7Gdm33CdTStUJBn0DjEVJ2uhSTVmIoWt9MVKdQziXGAsXipSzy949Hygg==", + "license": "MIT", + "dependencies": { + "@tanstack/react-store": "^0.11.0", + "@tanstack/table-core": "9.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=18" + } + }, "node_modules/@tanstack/react-virtual": { - "version": "3.14.9", - "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.14.9.tgz", - "integrity": "sha512-qZyr0FZDP8rDC4WBhsryIZmAd9bveJvFGUJJtskWaew6/0dTRS6wZxnR6VQ5bY2KwL3LjerrHqQLk3a0GKcPXQ==", + "version": "3.14.10", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.14.10.tgz", + "integrity": "sha512-SRyoUbdFMRHuYXMijV5H4ZarQWpXkj3iANq8OFre+pybeVap8ZJjZ3Nz9bVjx4d8PfobVUQUdKyyyHYk3E+djw==", "license": "MIT", "dependencies": { - "@tanstack/virtual-core": "3.17.7" + "@tanstack/virtual-core": "3.17.8" }, "funding": { "type": "github", @@ -3260,10 +3337,26 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tanstack/table-core": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-9.1.2.tgz", + "integrity": "sha512-ONpWQeass1sfg80CWF1NSwQ8r3GiqxA2lT/EdqIcrDEPZ0Z+0mM94eQoFYLPN0Kztzj8TQVb2+PrSZSItqA61g==", + "license": "MIT", + "dependencies": { + "@tanstack/store": "^0.11.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tanstack/virtual-core": { - "version": "3.17.7", - "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.17.7.tgz", - "integrity": "sha512-bp+v10y65sp2H7WpWfIMyxTNfl8ZVfxFTLRjPIFRryi6FV/J33z4IS53WO4pTk36KlvJ4iLiQz+oaydDC1xbcA==", + "version": "3.17.8", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.17.8.tgz", + "integrity": "sha512-BfEvehNpOT75r5Ksc5xW6NZuXujTfb7nlSEyVu4XHG3gdxNg1KqXruWbDewXOUaUYIo4oRbSfkjIajz4MAT8tA==", "license": "MIT", "funding": { "type": "github", @@ -3442,7 +3535,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -3469,18 +3561,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.66.0.tgz", - "integrity": "sha512-p088eaGrzYz1s+7cov0aMOCkNGTJlVxF4jgubf28c8L0Cv9Rloj8YBHnv4hXLq6IIEE1AsjNWavO+k+8kP2Y0A==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.67.0.tgz", + "integrity": "sha512-Un7Heoyj65NREbKAyIrFxeM143NZpExWmy1Nep4DLeQOeLlTeumPjoNKnBrU5D5moWXbPJgRa5Uwcdu0faVNGQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.66.0", - "@typescript-eslint/type-utils": "8.66.0", - "@typescript-eslint/utils": "8.66.0", - "@typescript-eslint/visitor-keys": "8.66.0", + "@typescript-eslint/scope-manager": "8.67.0", + "@typescript-eslint/type-utils": "8.67.0", + "@typescript-eslint/utils": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -3493,7 +3584,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.66.0", + "@typescript-eslint/parser": "^8.67.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } @@ -3509,17 +3600,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.66.0.tgz", - "integrity": "sha512-X6ypGChaWYk6PBtUg2BwuTZEFFcHJAtGTVJ9/lCTOufhZ4i9fNolQNnktq+kkMCwMj7V8Svsq7+TxSDslmhE0g==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.67.0.tgz", + "integrity": "sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.66.0", - "@typescript-eslint/types": "8.66.0", - "@typescript-eslint/typescript-estree": "8.66.0", - "@typescript-eslint/visitor-keys": "8.66.0", + "@typescript-eslint/scope-manager": "8.67.0", + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0", "debug": "^4.4.3" }, "engines": { @@ -3535,14 +3625,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.66.0.tgz", - "integrity": "sha512-7MthGPTt4BP69lSryqpqq8HQqxuzynssckL/jyDyk3+TNMQ3y2jFWkptCrktWvBrP+EH787Nl5N5Qpw7WZg+5g==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.67.0.tgz", + "integrity": "sha512-cvE8c7ulYeXN9fYuszhCeCsbzyVEXuhrRCybnBre7TUmqb5nRmBfQAwCj0O3WJFDeyAZt4VYv51vMCC9LHSdYw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.66.0", - "@typescript-eslint/types": "^8.66.0", + "@typescript-eslint/tsconfig-utils": "^8.67.0", + "@typescript-eslint/types": "^8.67.0", "debug": "^4.4.3" }, "engines": { @@ -3557,14 +3647,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.66.0.tgz", - "integrity": "sha512-8TGcH25j9zqJ/IULB/ppyhRvxA8QYfFEZ7nfbg6/BN9spDgb8fPWQXlE5l8TWBL50EtUx007uZ1o9VOwrq2/9g==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.67.0.tgz", + "integrity": "sha512-EgvsleTwS4E+WzzSvem8fAUubLwatMNF1B5hHSLQxcvs7q2dtRhGyujHwLJSYlG41niJ7GP24Aha2+0mb1b2kg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.66.0", - "@typescript-eslint/visitor-keys": "8.66.0" + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3575,9 +3665,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.66.0.tgz", - "integrity": "sha512-9D5gLYZG4rOjcoag8MQ/fWI8WqA9wcPDyOGyWtWFhvM1lHRbliqUSPIY5J3zqCU1tvSwzXxnnjhQhz5Ne7mJ4g==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.67.0.tgz", + "integrity": "sha512-vV+LUSv5njUWsknE71fqKTlXUva+R76SaeORd6Zojcunk/6DvKFXONU3BrAs2H49mbygUXt6gbYunzwqNwlhdg==", "dev": true, "license": "MIT", "engines": { @@ -3592,15 +3682,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.66.0.tgz", - "integrity": "sha512-LG2dWfjZQQp0ADtAu/EWJVayefGL2UEZ3CDeI44D9v3rXB/WYUqE/jpO28KrEKul5AySrmI+Zh1v6v+xW2U9+g==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.67.0.tgz", + "integrity": "sha512-aVWDXbRmdXO9siTfX4ditQI1T9+zVcNazT48EJCD0v40/9RIFoUgZ05CmGEq9H2gixRpjUn/iplwvlcvutJW/Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.66.0", - "@typescript-eslint/typescript-estree": "8.66.0", - "@typescript-eslint/utils": "8.66.0", + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0", + "@typescript-eslint/utils": "8.67.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -3617,9 +3707,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.66.0.tgz", - "integrity": "sha512-H6gcYaSDOyvL3AD/jHUtUFo2jqGgn/F6nuyuZSu0QTesxL+cP4dQoIMrODRofuJC09g64+WgZ6tE19Y1N2YIFQ==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.67.0.tgz", + "integrity": "sha512-sBtgslww8nsMYUjhdPBiSyUqSzT8uR6g93A2QXnQC8+cGdjz0CyaOdqHDRJb1AtORbZCNUJBBeFA/tNR2uQmww==", "dev": true, "license": "MIT", "engines": { @@ -3631,16 +3721,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.66.0.tgz", - "integrity": "sha512-8/x4INiiQb10jGgXYD7116/zQ+OL84ZIFn0za68wwFHCanT/VLbBEroWht8RV8fn0/ZCAoazHLQgwUC0UQcDfg==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.67.0.tgz", + "integrity": "sha512-EKQBCE9yNlRJYm7jdTW5AhDacDUmSwQb0FAJAmK2EKYrNXIsa2vxcSZx6PvJ/dEdI6lS+Y9W+EXckLj0iPFGcw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.66.0", - "@typescript-eslint/tsconfig-utils": "8.66.0", - "@typescript-eslint/types": "8.66.0", - "@typescript-eslint/visitor-keys": "8.66.0", + "@typescript-eslint/project-service": "8.67.0", + "@typescript-eslint/tsconfig-utils": "8.67.0", + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -3698,16 +3788,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.66.0.tgz", - "integrity": "sha512-jasearZPolBw5NJNYGMwxzHMF83niVWmMU1VdHzG1CyfI2VS7f7nZltnKtHcg20hW+7Uo5GfK4MeDPoU3qI8EA==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.67.0.tgz", + "integrity": "sha512-U9D1FdwEWBwok3hxxSdhclMb0twvt9QnjIQ0VfQ1AiX2epnpSgv2ubVDsayOFyY8K6FX+AQ7E0FKWVG3iKsj1A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.66.0", - "@typescript-eslint/types": "8.66.0", - "@typescript-eslint/typescript-estree": "8.66.0" + "@typescript-eslint/scope-manager": "8.67.0", + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3722,13 +3812,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.66.0.tgz", - "integrity": "sha512-dkKR8q+lKciskj1Y3vthHktl+3cMLWGyVUP23bRiPZ5O9BRT++4EqDDV+TVeIKBL1VXVEqrJlz8MYbcnvJcAlg==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.67.0.tgz", + "integrity": "sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/types": "8.67.0", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -3753,9 +3843,9 @@ } }, "node_modules/@vitejs/plugin-react": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.5.tgz", - "integrity": "sha512-BOVzne/NL162sMdResB25mUv+vWMF5NoAjNf09TeGlE7ZpszZWSD3winycicLJw72yeVsoCn/2kOhEuCvEShMA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.0.tgz", + "integrity": "sha512-qd2BzUBehkov86WFhg0JkEFEYyCLG9uPCe6qWTY/kRlss9OvJrOF2UbIWT7p+8IzZHkEu0DNGHc4HSv+JdDLsw==", "dev": true, "license": "MIT", "dependencies": { @@ -3767,6 +3857,7 @@ "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", + "oxc-transform-react": "^0.145.0", "vite": "^8.0.0" }, "peerDependenciesMeta": { @@ -3775,6 +3866,9 @@ }, "babel-plugin-react-compiler": { "optional": true + }, + "oxc-transform-react": { + "optional": true } } }, @@ -3784,7 +3878,6 @@ "integrity": "sha512-8MVGEFnJIcdGjcbfKmeq8z0pZHH0JlVtoVZH9Q/qwUp6wyFnEJUBMrw9DCaj+ra3vShGmhavjalMIhPNxZAUcw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.1.11", @@ -4061,9 +4154,9 @@ "license": "MIT" }, "node_modules/@zip.js/zip.js": { - "version": "2.8.53", - "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.8.53.tgz", - "integrity": "sha512-Vr55wt21XKD0kZFCQEZOtbViUMUK/U0MH2MDsHxu6FSyYk3Wu+afh+f9xD01x0kerDNfHmUVrNpcgYAgtnRtjA==", + "version": "2.8.54", + "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.8.54.tgz", + "integrity": "sha512-Nr2JRdMBxmdh6v8JYb/BBipvTvBr3Ztwf6+bIdVxb1EMaNNm5gsM9R80pB+c9S2WMdODb3LIWiXfYekDA3+JSw==", "license": "BSD-3-Clause", "engines": { "bun": ">=0.7.0", @@ -4077,7 +4170,6 @@ "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4101,7 +4193,6 @@ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -4169,9 +4260,9 @@ } }, "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", "dev": true, "license": "MIT", "engines": { @@ -4465,12 +4556,12 @@ "license": "MIT" }, "node_modules/attr-accept": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz", - "integrity": "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-4.0.0.tgz", + "integrity": "sha512-hmCnJClmeKNKlsBHgbM8yLZRiQZ4/20UXbLJb6OUT16eWcM5/xNZerr80a/zCYob768KIGq++aLrQNTuwPsIOQ==", "license": "MIT", "engines": { - "node": ">=4" + "node": ">= 22" } }, "node_modules/available-typed-arrays": { @@ -4512,9 +4603,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.11.12", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.12.tgz", - "integrity": "sha512-r7WnVImvVCeFpf2DOXfy41aPWzeNg3H/A2X4dKmy1QL0MSyyk/e7z8ihJ3N6Nn2PsdhkVlqnEfnUE4a05P2aTA==", + "version": "2.11.16", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.16.tgz", + "integrity": "sha512-H/bNPUFHewJHyCTdjn1n3Pit5+2GmWT6mmeHImPX+8MA9NA6b67jO4gYmi4jTbCJb2otq34KMZnovndDPqJwhQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -4596,9 +4687,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.7", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", - "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", "dev": true, "funding": [ { @@ -4615,13 +4706,12 @@ } ], "license": "MIT", - "peer": true, "dependencies": { - "baseline-browser-mapping": "^2.10.44", - "caniuse-lite": "^1.0.30001806", - "electron-to-chromium": "^1.5.393", - "node-releases": "^2.0.51", - "update-browserslist-db": "^1.2.3" + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" }, "bin": { "browserslist": "cli.js" @@ -4760,9 +4850,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001806", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", - "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", + "version": "1.0.30001809", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", + "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==", "dev": true, "funding": [ { @@ -4963,9 +5053,9 @@ "license": "MIT" }, "node_modules/colord": { - "version": "2.9.3", - "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", - "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.10.0.tgz", + "integrity": "sha512-AidJptpBJmjTclAp9BkLwJi0T93fo5epJnbaZslpg6QVzpHjAiveF55mE9AcUJiGMqRHgMDY8soMsQtuNYMHfw==", "dev": true, "license": "MIT" }, @@ -5113,13 +5203,16 @@ } }, "node_modules/core-js-compat": { - "version": "3.49.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz", - "integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==", + "version": "3.50.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.50.0.tgz", + "integrity": "sha512-XGpFGbMLHwSt74YLTKho7Ib242qi6O8MSX+sRokV4oz7iKXvQWGYZthjIhjRGMxjzVkAubBO512dKGYcefmX3Q==", "dev": true, "license": "MIT", "dependencies": { - "browserslist": "^4.28.1" + "browserslist": "^4.28.7" + }, + "engines": { + "node": ">=6.4.0" }, "funding": { "type": "opencollective", @@ -5397,7 +5490,6 @@ "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" @@ -5547,7 +5639,6 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -5857,9 +5948,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.400", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.400.tgz", - "integrity": "sha512-96EWDNjM59SYflgeV5Ylsf4EMiq1a25YjCnJH7cxn/AF2H3pILRweaUnoLax0yKHWdpOzY6JKEu45e8irqZIHA==", + "version": "1.5.412", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.412.tgz", + "integrity": "sha512-z4rMe3esBzlzovKHj4gxJnsCGZRK5l4baUvm+gCGJBPE+gsyUMKsuU9tnEUtI1dOebXz1ytAPGjvXhmQ7rIPwA==", "dev": true, "license": "ISC" }, @@ -6040,9 +6131,9 @@ } }, "node_modules/es-module-lexer": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", - "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", "dev": true, "license": "MIT" }, @@ -6135,9 +6226,9 @@ "version": "9.39.5", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -6228,9 +6319,9 @@ } }, "node_modules/eslint-config-cheminfo-react/node_modules/globals": { - "version": "17.9.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.9.0.tgz", - "integrity": "sha512-m/MvAW61QVU5VDNF1Vj8axt016h8w7L5TU1e9zlab7XIttAT2YAlCwl75K1fOqvMM9apmD7lbCIRhpfkhmxhCg==", + "version": "17.11.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.11.0.tgz", + "integrity": "sha512-Z2I8hM+PbJDXQDq3Icgpzv+mPdwr68iZUU9d5WW4FuXfDUQfkZaZuvjMv42/5crNyw154+9+VWXbYrUgDXbxNw==", "dev": true, "license": "MIT", "engines": { @@ -6256,9 +6347,9 @@ } }, "node_modules/eslint-config-cheminfo/node_modules/globals": { - "version": "17.9.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.9.0.tgz", - "integrity": "sha512-m/MvAW61QVU5VDNF1Vj8axt016h8w7L5TU1e9zlab7XIttAT2YAlCwl75K1fOqvMM9apmD7lbCIRhpfkhmxhCg==", + "version": "17.11.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.11.0.tgz", + "integrity": "sha512-Z2I8hM+PbJDXQDq3Icgpzv+mPdwr68iZUU9d5WW4FuXfDUQfkZaZuvjMv42/5crNyw154+9+VWXbYrUgDXbxNw==", "dev": true, "license": "MIT", "engines": { @@ -6510,9 +6601,9 @@ } }, "node_modules/eslint-plugin-react-refresh": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.3.tgz", - "integrity": "sha512-5EMmLCV98Pi4o/f/3DP/v/tNqLHMIc9I8LKClNDWhZ9JTho89/kQcitCXQBMG7sAfVRK0Ie3T2EDOzp1YXYiVA==", + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.4.tgz", + "integrity": "sha512-7bqTKz7T0r+HKWFarNXByDE9/5+73wI2ru+M3zuqGbR7s/b/5/pQJXZoufWlrngqGqoZto73ZkGumCdLxk+4rw==", "dev": true, "license": "MIT", "peerDependencies": { @@ -6520,9 +6611,9 @@ } }, "node_modules/eslint-plugin-react-you-might-not-need-an-effect": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-you-might-not-need-an-effect/-/eslint-plugin-react-you-might-not-need-an-effect-1.0.1.tgz", - "integrity": "sha512-oOhQTYhor88Xp8RVytq25tvBfiAjU0r9SCDC51Qop+3Wg5BR1xGMAkM+/dV4MZbcMhdaU1L9bkv6LC95JmiTig==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-you-might-not-need-an-effect/-/eslint-plugin-react-you-might-not-need-an-effect-1.0.2.tgz", + "integrity": "sha512-HgYol2zhH3KbnW9Q4FY/FcIINbsYVL6rwQESChqBC2s9SLWtw1wg05+J7SCrr3BfQpNY2ReYhf8xjpd2JhKJOQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6626,9 +6717,9 @@ "license": "MIT" }, "node_modules/eslint-plugin-unicorn/node_modules/globals": { - "version": "17.9.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.9.0.tgz", - "integrity": "sha512-m/MvAW61QVU5VDNF1Vj8axt016h8w7L5TU1e9zlab7XIttAT2YAlCwl75K1fOqvMM9apmD7lbCIRhpfkhmxhCg==", + "version": "17.11.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.11.0.tgz", + "integrity": "sha512-Z2I8hM+PbJDXQDq3Icgpzv+mPdwr68iZUU9d5WW4FuXfDUQfkZaZuvjMv42/5crNyw154+9+VWXbYrUgDXbxNw==", "dev": true, "license": "MIT", "engines": { @@ -6974,7 +7065,6 @@ "resolved": "https://registry.npmjs.org/fifo-logger/-/fifo-logger-2.0.1.tgz", "integrity": "sha512-AwCaBK389hl67z4AJ5+8uOsxU07olw0DzowzA6Znr/eaItMCsXXxzA1DjY/KCABWu/4Bq+wrBhn1p7hsjNDv4g==", "license": "MIT", - "peer": true, "dependencies": { "typescript-event-target": "^1.1.1" } @@ -7009,12 +7099,12 @@ "license": "MIT" }, "node_modules/file-selector": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-4.1.0.tgz", - "integrity": "sha512-Io1mP8CI3zec5Bxy3P3TxdrKnt35Cm8vNIHnZsvyj43l4YFjD4NRInBp240S5bDJQ0EP1jnh7nCAwXsO818OCg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-5.0.1.tgz", + "integrity": "sha512-v0g/PTeuQgvKCBrVRsfVudvwXlRHSWHEQkVgKawgCGHkEpKA1clp3Om5jvEVhz8G9W/mOYjJH9FhkH4C888PgQ==", "license": "MIT", "engines": { - "node": ">= 20" + "node": ">= 22" } }, "node_modules/fill-range": { @@ -7489,9 +7579,9 @@ } }, "node_modules/globby": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/globby/-/globby-16.2.2.tgz", - "integrity": "sha512-NLvV9ubZ6NDsJaOpKPy3cQeJpKi9DcWiyCiFUpJPA0YihRqiE6RWaLUmgNNPr8MgPpLZjnBjSmou7uZBRJv9wA==", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/globby/-/globby-16.2.4.tgz", + "integrity": "sha512-c8B/VNLmxRcmqqenRA9t+9IyOjf9+V6lTxPaUJLqOCONdQkWZ0ETYgX0qbtJqPsgCNusT9MZ5Jeidw8Eb9tn2g==", "dev": true, "license": "MIT", "dependencies": { @@ -7499,6 +7589,7 @@ "fast-glob": "^3.3.3", "ignore": "^7.0.5", "is-path-inside": "^4.0.0", + "micromatch": "^4.0.8", "slash": "^5.1.0", "unicorn-magic": "^0.4.0" }, @@ -8477,7 +8568,6 @@ "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "jiti": "lib/jiti-cli.mjs" } @@ -8818,6 +8908,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -8839,6 +8932,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -8860,6 +8956,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -8881,6 +8980,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -9638,9 +9740,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.17", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.17.tgz", - "integrity": "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "dev": true, "funding": [ { @@ -9758,9 +9860,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.52", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.52.tgz", - "integrity": "sha512-MRlTqhAfoMx/4mhEbPo3Hi02g9LJZaJkka69V6h67Cb1gjrAG0jsTE4CZX1eptNx+VCAwJmfpnDIF4P0Nh1A7A==", + "version": "2.0.53", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", + "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", "dev": true, "license": "MIT", "engines": { @@ -9990,8 +10092,7 @@ "version": "9.25.0", "resolved": "https://registry.npmjs.org/openchemlib/-/openchemlib-9.25.0.tgz", "integrity": "sha512-FGTaZLJRTGXNC7khx8QvX/EiQBHpH1ncUbT7YXDJhw/Y/aDUKe5WrUIqTguMEtSs3GUHcRUjNUhfkpxulx2UXw==", - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/openchemlib-utils": { "version": "8.18.0", @@ -10085,16 +10186,6 @@ "@oxc-parser/binding-win32-x64-msvc": "0.143.0" } }, - "node_modules/oxc-parser/node_modules/@oxc-project/types": { - "version": "0.143.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.143.0.tgz", - "integrity": "sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - } - }, "node_modules/oxc-resolver": { "version": "11.24.2", "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.24.2.tgz", @@ -10422,7 +10513,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", @@ -10460,12 +10550,11 @@ } }, "node_modules/postcss-selector-parser": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz", - "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz", + "integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -10746,7 +10835,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -10772,7 +10860,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -10796,13 +10883,13 @@ } }, "node_modules/react-dropzone": { - "version": "20.1.0", - "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-20.1.0.tgz", - "integrity": "sha512-id1t9JDYNQeFzzIfB5/C6TrpLchy29rTSDxsBH/pcxhILyv/6bSOTJwOlvUsTWXkC2GduuHIldDV6UQXNWfIuA==", + "version": "20.1.1", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-20.1.1.tgz", + "integrity": "sha512-2cilRFP8bsjDOHpV0sJ6XY8pzJhmz4/cQ6s9yeckOACWYDR+n4MGGtnJh3Rycq/9SLhDWugqDx1z5mtfmOOZhw==", "license": "MIT", "dependencies": { - "attr-accept": "^2.2.5", - "file-selector": "^4.1.0" + "attr-accept": "^4.0.0", + "file-selector": "^5.0.0" }, "engines": { "node": ">= 22" @@ -10858,7 +10945,6 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.85.0.tgz", "integrity": "sha512-U2MTriFXnclmV4rOE20p2DcRFv5WEg3FIcBFOKcOLFHDVvGIMPvLTkTWefUsonmlaVy23khVDxDWym6uJVGOzw==", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -11064,42 +11150,6 @@ "react-dom": "^18.3.1 || ^19.2.7" } }, - "node_modules/react-science/node_modules/@tanstack/react-table": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-9.1.2.tgz", - "integrity": "sha512-YQPZFJ1nIi/bjjwsPZVouABgahDcl7Gdm33CdTStUJBn0DjEVJ2uhSTVmIoWt9MVKdQziXGAsXipSzy949Hygg==", - "license": "MIT", - "dependencies": { - "@tanstack/react-store": "^0.11.0", - "@tanstack/table-core": "9.1.2" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": ">=18" - } - }, - "node_modules/react-science/node_modules/@tanstack/table-core": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-9.1.2.tgz", - "integrity": "sha512-ONpWQeass1sfg80CWF1NSwQ8r3GiqxA2lT/EdqIcrDEPZ0Z+0mM94eQoFYLPN0Kztzj8TQVb2+PrSZSItqA61g==", - "license": "MIT", - "dependencies": { - "@tanstack/store": "^0.11.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, "node_modules/react-table": { "version": "7.8.0", "resolved": "https://registry.npmjs.org/react-table/-/react-table-7.8.0.tgz", @@ -11313,13 +11363,13 @@ } }, "node_modules/rolldown": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.2.tgz", - "integrity": "sha512-opwpo1tQBAcpSUJDt94B7hhLNGOKjCdE//XXjeLrnx9b83bjnw45tXdg1b09yEw/VLFBJGZpwRULMmOZo7ol+A==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.5.tgz", + "integrity": "sha512-VD2IE5PUG4Oj8zz2VGykiYd5wbnjdIiSsNQb8Qu5B+noEp+A78mu2iVvpp27g8es14Tk9rofNs5Tku9iQCS4fA==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.142.0", + "@oxc-project/types": "=0.146.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -11329,20 +11379,31 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.2.2", - "@rolldown/binding-darwin-arm64": "1.2.2", - "@rolldown/binding-darwin-x64": "1.2.2", - "@rolldown/binding-freebsd-x64": "1.2.2", - "@rolldown/binding-linux-arm-gnueabihf": "1.2.2", - "@rolldown/binding-linux-arm64-gnu": "1.2.2", - "@rolldown/binding-linux-arm64-musl": "1.2.2", - "@rolldown/binding-linux-ppc64-gnu": "1.2.2", - "@rolldown/binding-linux-s390x-gnu": "1.2.2", - "@rolldown/binding-linux-x64-gnu": "1.2.2", - "@rolldown/binding-linux-x64-musl": "1.2.2", - "@rolldown/binding-openharmony-arm64": "1.2.2", - "@rolldown/binding-win32-arm64-msvc": "1.2.2", - "@rolldown/binding-win32-x64-msvc": "1.2.2" + "@rolldown/binding-android-arm-eabi": "1.2.5", + "@rolldown/binding-android-arm64": "1.2.5", + "@rolldown/binding-darwin-arm64": "1.2.5", + "@rolldown/binding-darwin-x64": "1.2.5", + "@rolldown/binding-freebsd-x64": "1.2.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.5", + "@rolldown/binding-linux-arm64-gnu": "1.2.5", + "@rolldown/binding-linux-arm64-musl": "1.2.5", + "@rolldown/binding-linux-ppc64-gnu": "1.2.5", + "@rolldown/binding-linux-s390x-gnu": "1.2.5", + "@rolldown/binding-linux-x64-gnu": "1.2.5", + "@rolldown/binding-linux-x64-musl": "1.2.5", + "@rolldown/binding-openharmony-arm64": "1.2.5", + "@rolldown/binding-win32-arm64-msvc": "1.2.5", + "@rolldown/binding-win32-x64-msvc": "1.2.5" + } + }, + "node_modules/rolldown/node_modules/@oxc-project/types": { + "version": "0.146.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.146.0.tgz", + "integrity": "sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" } }, "node_modules/run-parallel": { @@ -11813,9 +11874,9 @@ } }, "node_modules/smol-toml": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.7.1.tgz", - "integrity": "sha512-PPlsspAZ4jbMBu5DMFhfUGDQLu/vrL4SyBROVS37x8ynnVmFIs1VPBz1Co8Xks3TvpIaZXmU85y4DrQ+UyVFoQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.8.0.tgz", + "integrity": "sha512-kCZr2V3ch9i00x8zXRhjUNVcjG9ijES5dDudkXvUVCT5QlJNQWElSJdZqyPemffHoLNUYwOcou0Fy+ojN0uHSQ==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -12691,7 +12752,6 @@ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12701,16 +12761,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.66.0.tgz", - "integrity": "sha512-QlEbBPz/RuJ1XUHj29nm3t0F/O/cSlEnntozqPOYHnnTGAXFamnMBu5i9Vn6vhUPHGAjR+Vl+5J8vPN/BMUrJw==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.67.0.tgz", + "integrity": "sha512-S2udFs8tCKEKffuJ4TB1idGUZiXdCPGi3IPBGWXarbLQ5UPXORV8QEVzJ4gCRduURMb5EkpNCdjbk0eDIuI8Yg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.66.0", - "@typescript-eslint/parser": "8.66.0", - "@typescript-eslint/typescript-estree": "8.66.0", - "@typescript-eslint/utils": "8.66.0" + "@typescript-eslint/eslint-plugin": "8.67.0", + "@typescript-eslint/parser": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0", + "@typescript-eslint/utils": "8.67.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -12780,9 +12840,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz", + "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==", "dev": true, "funding": [ { @@ -12891,17 +12951,16 @@ } }, "node_modules/vite": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz", - "integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==", + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", + "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.5", - "postcss": "^8.5.25", - "rolldown": "~1.2.1", + "postcss": "^8.5.26", + "rolldown": "~1.2.4", "tinyglobby": "^0.2.17" }, "bin": { @@ -12918,7 +12977,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.4.0", + "@vitejs/devtools": "^0.4.0 || ^0.5.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", @@ -12990,7 +13049,6 @@ "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/expect": "4.1.11", "@vitest/mocker": "4.1.11", @@ -13367,7 +13425,6 @@ "resolved": "https://registry.npmjs.org/yup/-/yup-1.7.1.tgz", "integrity": "sha512-GKHFX2nXul2/4Dtfxhozv701jLQHdf6J34YDh2cEkpqoo8le5Mg6/LrdseVLrFarmFygZTlfIhHx/QKfb/QWXw==", "license": "MIT", - "peer": true, "dependencies": { "property-expr": "^2.0.5", "tiny-case": "^1.0.3", @@ -13380,7 +13437,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 9aa490518d..64e6bb01ae 100644 --- a/package.json +++ b/package.json @@ -165,4 +165,4 @@ "volta": { "node": "24.19.0" } -} +} \ No newline at end of file From 1836b4b3682a492f7108820bd1a7b03f6ae43da9 Mon Sep 17 00:00:00 2001 From: hamed musallam Date: Fri, 21 Aug 2026 13:43:24 +0200 Subject: [PATCH 16/20] chore: fix prettier --- package.json | 2 +- .../Spectrum2D/findBestMinContour/evaluation.ts | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 64e6bb01ae..9aa490518d 100644 --- a/package.json +++ b/package.json @@ -165,4 +165,4 @@ "volta": { "node": "24.19.0" } -} \ No newline at end of file +} diff --git a/src/data/data2d/Spectrum2D/findBestMinContour/evaluation.ts b/src/data/data2d/Spectrum2D/findBestMinContour/evaluation.ts index 604009a8eb..8b836e5979 100644 --- a/src/data/data2d/Spectrum2D/findBestMinContour/evaluation.ts +++ b/src/data/data2d/Spectrum2D/findBestMinContour/evaluation.ts @@ -105,13 +105,13 @@ function evaluateThresholdMetrics( const ridge = rowCounts && colCounts ? calculateRidgeScores( - rowCounts, - colCounts, - rows, - cols, - activePixels, - ridgeCoverageThreshold, - ) + rowCounts, + colCounts, + rows, + cols, + activePixels, + ridgeCoverageThreshold, + ) : { vertical: 0, horizontal: 0 }; return { activePixels, ridge }; From d0fa91469feb7d74cebc7d791e1e941df3530fda Mon Sep 17 00:00:00 2001 From: jobo322 Date: Fri, 21 Aug 2026 09:23:02 -0500 Subject: [PATCH 17/20] chore: reduce the max value to avoid selecting the max value as a contour level --- src/data/data2d/Spectrum2D/findBestMinContour/evaluation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/data2d/Spectrum2D/findBestMinContour/evaluation.ts b/src/data/data2d/Spectrum2D/findBestMinContour/evaluation.ts index 8b836e5979..15e5849ace 100644 --- a/src/data/data2d/Spectrum2D/findBestMinContour/evaluation.ts +++ b/src/data/data2d/Spectrum2D/findBestMinContour/evaluation.ts @@ -412,7 +412,7 @@ export function maxPoolAbsolute( values, rows: analysisRows, cols: analysisCols, - maxAbsoluteValue, + maxAbsoluteValue: maxAbsoluteValue * 0.95, }; } From 8e99190beaf2a601589098ae08af00db8267aa4d Mon Sep 17 00:00:00 2001 From: jobo322 Date: Fri, 21 Aug 2026 11:04:48 -0500 Subject: [PATCH 18/20] chore: fix test case and correct exporting of a helper function --- src/data/data2d/Spectrum2D/contours.ts | 9 +++------ .../findBestMinContour/evaluation.ts | 2 +- .../findAutomaticContourLevels.test.ts | 4 +++- .../findAutomaticContourLevels.ts | 20 ++++++++++--------- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/data/data2d/Spectrum2D/contours.ts b/src/data/data2d/Spectrum2D/contours.ts index 51e206b77c..6fee79c9de 100644 --- a/src/data/data2d/Spectrum2D/contours.ts +++ b/src/data/data2d/Spectrum2D/contours.ts @@ -78,6 +78,7 @@ function getDefaultContoursLevel(spectrum: Spectrum2D, quadrant = 'rr') { noiseLevel, { maxFdr: 0.005, + diagnostics: false, maxOccupancy: 0.005, persistenceLevels: 5, contourRatio: 1.8, @@ -88,12 +89,8 @@ function getDefaultContoursLevel(spectrum: Spectrum2D, quadrant = 'rr') { }, ); - const { - diagnostics: { hasT1Noise }, - minLevelWithoutT1Noise, - minLevel, - maxAbsoluteValue, - } = bestMinLevel; + const { hasT1Noise, minLevelWithoutT1Noise, minLevel, maxAbsoluteValue } = + bestMinLevel; const minContourLevel = Math.min( calculateValueOfLevel( hasT1Noise ? minLevelWithoutT1Noise : minLevel, diff --git a/src/data/data2d/Spectrum2D/findBestMinContour/evaluation.ts b/src/data/data2d/Spectrum2D/findBestMinContour/evaluation.ts index 15e5849ace..fbd5f0eb29 100644 --- a/src/data/data2d/Spectrum2D/findBestMinContour/evaluation.ts +++ b/src/data/data2d/Spectrum2D/findBestMinContour/evaluation.ts @@ -416,7 +416,7 @@ export function maxPoolAbsolute( }; } -function countValuesAboveThreshold( +export function countValuesAboveThreshold( matrix: Float64Array, threshold: number, ): number { diff --git a/src/data/data2d/Spectrum2D/findBestMinContour/findAutomaticContourLevels.test.ts b/src/data/data2d/Spectrum2D/findBestMinContour/findAutomaticContourLevels.test.ts index c71e99de51..6d98830e8e 100644 --- a/src/data/data2d/Spectrum2D/findBestMinContour/findAutomaticContourLevels.test.ts +++ b/src/data/data2d/Spectrum2D/findBestMinContour/findAutomaticContourLevels.test.ts @@ -17,7 +17,7 @@ test('maxPoolAbsolute returns the global finite absolute maximum', () => { 2, ); - expect(analysis.maxAbsoluteValue).toBe(7); + expect(analysis.maxAbsoluteValue).toBe(7 * 0.95); }); test('sorted threshold counts preserve direct comparison semantics', () => { @@ -127,9 +127,11 @@ test('diagnostics do not change selected contour levels', () => { sigmaMultiplier: withDiagnostics.sigmaMultiplier, sigmaMultiplierWithoutT1Noise: withDiagnostics.sigmaMultiplierWithoutT1Noise, + hasT1Noise: withDiagnostics.hasT1Noise, }); expect(withDiagnostics.diagnostics).toBeDefined(); expect(withoutDiagnostics.diagnostics).toBeUndefined(); + expect(withoutDiagnostics.hasT1Noise).toBe(withDiagnostics.hasT1Noise); }); test('fallback uses the first cached candidate when scores tie', () => { diff --git a/src/data/data2d/Spectrum2D/findBestMinContour/findAutomaticContourLevels.ts b/src/data/data2d/Spectrum2D/findBestMinContour/findAutomaticContourLevels.ts index 2ac93d9d1d..d4517bad70 100644 --- a/src/data/data2d/Spectrum2D/findBestMinContour/findAutomaticContourLevels.ts +++ b/src/data/data2d/Spectrum2D/findBestMinContour/findAutomaticContourLevels.ts @@ -11,12 +11,6 @@ import { validateInput } from './math.js'; interface AutoContourDiagnostics { recommended: ThresholdDiagnostics; ridgeFree: ThresholdDiagnostics; - - /** - * True when directional ridge structures were detected - * at the recommended minimum. - */ - hasT1Noise: boolean; } interface ThresholdDiagnostics { @@ -43,6 +37,12 @@ export type AutoContourResult = { */ minLevelWithoutT1Noise: number; + /** + * True when directional ridge structures were detected + * at the recommended minimum. + */ + hasT1Noise: boolean; + /** * Same values expressed in multiples of the noise level. */ @@ -365,10 +365,15 @@ export function findAutomaticContourLevels( ridgeCoverageThreshold, ); + const hasT1Noise = + recommended.verticalRidgeScore > maxVerticalRidgeScore || + recommended.horizontalRidgeScore > maxHorizontalRidgeScore; + const result: AutoContourResult = { maxAbsoluteValue, minLevel: recommended.sigmaMultiplier * noiseLevel, minLevelWithoutT1Noise: ridgeFreeLevel, + hasT1Noise, sigmaMultiplier: recommended.sigmaMultiplier, sigmaMultiplierWithoutT1Noise: ridgeFreeLevel / noiseLevel, @@ -419,9 +424,6 @@ export function findAutomaticContourLevels( verticalRidgeScore: ridgeFree.verticalRidgeScore, horizontalRidgeScore: ridgeFree.horizontalRidgeScore, }, - hasT1Noise: - recommendedDiagnostics.verticalRidgeScore > maxVerticalRidgeScore || - recommendedDiagnostics.horizontalRidgeScore > maxHorizontalRidgeScore, }; } From adc310d80515e576c15e3a26ef04dcefdeb60b14 Mon Sep 17 00:00:00 2001 From: jobo322 Date: Fri, 21 Aug 2026 11:46:35 -0500 Subject: [PATCH 19/20] chore: remove test of ignored cases --- .../estimateNoiseLevel.test.ts | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/data/data2d/Spectrum2D/findBestMinContour/estimateNoiseLevel.test.ts b/src/data/data2d/Spectrum2D/findBestMinContour/estimateNoiseLevel.test.ts index d184f6bcc2..0363552c7d 100644 --- a/src/data/data2d/Spectrum2D/findBestMinContour/estimateNoiseLevel.test.ts +++ b/src/data/data2d/Spectrum2D/findBestMinContour/estimateNoiseLevel.test.ts @@ -12,25 +12,8 @@ test('estimates Gaussian noise from a downsampled matrix', () => { ); }); -test('ignores non-finite sampled values', () => { - const matrix = new Float64Array([ - Number.NaN, - -2, - 0, - Number.POSITIVE_INFINITY, - 1, - Number.NEGATIVE_INFINITY, - 2, - ]); - - expect(estimateNoiseLevel(matrix)).toBeCloseTo(GAUSSIAN_MAD_SCALE); -}); - test('rejects invalid sample sizes and zero MAD', () => { expect(() => estimateNoiseLevel(new Float64Array([0, 1]), { maxSamples: 0 }), ).toThrow('maxSamples must be a positive integer'); - expect(() => estimateNoiseLevel(new Float64Array([0, 0]))).toThrow( - 'unable to estimate a positive noise level', - ); }); From 435ec3724d56455f29734f2dfa969441987d430f Mon Sep 17 00:00:00 2001 From: hamed musallam Date: Fri, 21 Aug 2026 21:08:35 +0200 Subject: [PATCH 20/20] refactor(test): move contour tests to _tests_ to fix package workflow failure --- .../estimateNoiseLevel.test.ts | 2 +- .../findAutomaticContourLevels.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/data/data2d/{Spectrum2D/findBestMinContour => __tests__}/estimateNoiseLevel.test.ts (85%) rename src/data/data2d/{Spectrum2D/findBestMinContour => __tests__}/findAutomaticContourLevels.test.ts (95%) diff --git a/src/data/data2d/Spectrum2D/findBestMinContour/estimateNoiseLevel.test.ts b/src/data/data2d/__tests__/estimateNoiseLevel.test.ts similarity index 85% rename from src/data/data2d/Spectrum2D/findBestMinContour/estimateNoiseLevel.test.ts rename to src/data/data2d/__tests__/estimateNoiseLevel.test.ts index 0363552c7d..cf369fc964 100644 --- a/src/data/data2d/Spectrum2D/findBestMinContour/estimateNoiseLevel.test.ts +++ b/src/data/data2d/__tests__/estimateNoiseLevel.test.ts @@ -1,6 +1,6 @@ import { expect, test } from 'vitest'; -import { estimateNoiseLevel } from './estimateNoiseLevel.js'; +import { estimateNoiseLevel } from '../Spectrum2D/findBestMinContour/estimateNoiseLevel.js'; const GAUSSIAN_MAD_SCALE = 1 / 0.6744897501960817; diff --git a/src/data/data2d/Spectrum2D/findBestMinContour/findAutomaticContourLevels.test.ts b/src/data/data2d/__tests__/findAutomaticContourLevels.test.ts similarity index 95% rename from src/data/data2d/Spectrum2D/findBestMinContour/findAutomaticContourLevels.test.ts rename to src/data/data2d/__tests__/findAutomaticContourLevels.test.ts index 6d98830e8e..9e375faa3c 100644 --- a/src/data/data2d/Spectrum2D/findBestMinContour/findAutomaticContourLevels.test.ts +++ b/src/data/data2d/__tests__/findAutomaticContourLevels.test.ts @@ -6,8 +6,8 @@ import { createThresholdIndex, evaluateThreshold, maxPoolAbsolute, -} from './evaluation.js'; -import { findAutomaticContourLevels } from './findAutomaticContourLevels.js'; +} from '../Spectrum2D/findBestMinContour/evaluation.js'; +import { findAutomaticContourLevels } from '../Spectrum2D/findBestMinContour/findAutomaticContourLevels.js'; test('maxPoolAbsolute returns the global finite absolute maximum', () => { const analysis = maxPoolAbsolute(