diff --git a/src/x/__tests__/xNoiseSanPlot.test.ts b/src/x/__tests__/xNoiseSanPlot.test.ts index 3ee9cf9e..2a6af132 100644 --- a/src/x/__tests__/xNoiseSanPlot.test.ts +++ b/src/x/__tests__/xNoiseSanPlot.test.ts @@ -1055,4 +1055,13 @@ test('get noise level', () => { //the SNR should be less because the biggest peak is not present. expect(noiseWithoutBigPeaks.snr).toBeLessThan(noise.snr); + + expect(Object.keys(noise.percentiles.positive)).toHaveLength(100); + expect(Object.keys(noise.percentiles.negative)).toHaveLength(100); + expect(noise.percentiles.positive[99]).toBeGreaterThan( + noise.percentiles.positive[98], + ); + expect(noise.percentiles.negative[99]).toBeGreaterThan( + noise.percentiles.negative[98], + ); }); diff --git a/src/x/xNoiseSanPlot.ts b/src/x/xNoiseSanPlot.ts index b68036ec..cf0e07b8 100644 --- a/src/x/xNoiseSanPlot.ts +++ b/src/x/xNoiseSanPlot.ts @@ -70,6 +70,10 @@ export interface XNoiseSanPlotResult { negative: number; snr: number; sanplot: Record; + percentiles: { + positive: Record; + negative: Record; + }; } /** @@ -154,6 +158,10 @@ export function xNoiseSanPlot( negative: { from: firstNegativeValueIndex, to: input.length }, }, }), + percentiles: { + positive: getPercentiles(signPositive), + negative: getPercentiles(signNegative), + }, }; } @@ -194,6 +202,7 @@ function calculateNoiseLevel( const effectiveCutOffDist = (cutOffDist * cloneSign.length + cutOffSignalsIndex) / (cloneSign.length + cutOffSignalsIndex); + const refinedCorrectionFactor = -1 * simpleNormInvValue(effectiveCutOffDist / 2); noiseLevel /= refinedCorrectionFactor; @@ -220,10 +229,6 @@ function calculateNoiseLevel( * it will be biased and noisy. We therefore scan candidate quantile windows * and pick the one where the sigma estimates are most self-consistent * (lowest variance) - a proxy for "where the noise region reliably starts". - * <<<<<<< Updated upstream - * - * ======= - * >>>>>>> Stashed changes * @param signPositive - an array of positive numbers. * @param options - optional parameters to configure the cut-off determination. * @param options.magnitudeMode - if true, uses magnitude mode for normalization. Default is false. @@ -255,10 +260,11 @@ function determineCutOff( // For each quantile fraction, compute a local sigma estimate by inverting // the theoretical relationship between order statistics and the assumed // noise distribution (Gaussian or Rayleigh). + const { from, to, step } = considerList; const sigmaEstimates: Array<[quantileFraction: number, sigma: number]> = []; for ( - let quantileFraction = 0.01; - quantileFraction <= 0.99; + let quantileFraction = from - step / 2; + quantileFraction <= to + step / 2; quantileFraction += 0.01 ) { const index = Math.round(indexMax * quantileFraction); @@ -267,7 +273,6 @@ function determineCutOff( sigmaEstimates.push([quantileFraction, Math.abs(sigma)]); } - const { from, to, step } = considerList; const halfWindow = step / 2; let bestVariance = Number.MAX_SAFE_INTEGER; @@ -375,19 +380,6 @@ function scale( return { x: xAxis, y: array }; } -/** - * Prepares and processes the input data array based on the provided options. - * @param array - the input array of numbers to be processed. - * @param options - an object containing the following properties: - * - scaleFactor: A number by which to scale each element of the array. - * - mask: An optional array of the same length as the input array, where - * elements corresponding to `true` values will be excluded from processing. - * @param options.scaleFactor - * @param options.mask - * @param from - * @returns A new Float64Array containing the processed data, scaled by the - * scaleFactor and sorted in descending order. - */ function createNegativeSign(array: Float64Array, from: number): Float64Array { const length = array.length - from; const result = new Float64Array(length); @@ -397,9 +389,55 @@ function createNegativeSign(array: Float64Array, from: number): Float64Array { return result; } +/** + * Calculates intensity percentiles ranging from the 50th to the 99.5th percentile + * (in 0.5 increments) from a pre-sorted array. + * @param sorted - a pre-sorted array of numbers. + * @returns An object mapping percentile labels to their corresponding values. + */ +function getPercentiles(sorted: NumberArray): Record { + if (sorted.length === 0) return {}; + + const nbPercentiles = 100; + const result: Record = {}; + const maxIndex = sorted.length - 1; + + for (let i = 0; i < nbPercentiles; i++) { + const pLabel = 50 + (i * 50) / nbPercentiles; + const percentile = pLabel / 100; + const index = maxIndex - Math.floor(percentile * maxIndex); + + result[pLabel] = sorted[index]; + } + + return result; +} + +interface PrepareDataOptions { + /** + * A multiplier by which to scale each element of the array. + * Note: Scaling is only applied if this value is strictly greater than 1. + */ + scaleFactor: number; + + /** + * An optional array of the same length as the input array. + * Elements in the input array corresponding to truthy values in this mask + * will be excluded from the final processed output. + */ + mask?: NumberArray; +} + +/** + * Prepares and processes the input data array based on the provided options. + * @param array - The input array of numbers to be processed. + * @param options - The configuration options for processing the data. + * @returns A new Float64Array containing the processed data, scaled by the + * scaleFactor and sorted in descending order. + */ function prepareData( array: NumberArray, - options: { scaleFactor: number; mask?: NumberArray }, + options: PrepareDataOptions, ): Float64Array { const { scaleFactor, mask } = options;