Skip to content

Commit

Permalink
feat: add xyReduceNonContinuous
Browse files Browse the repository at this point in the history
  • Loading branch information
lpatiny committed Nov 22, 2024
1 parent c6a12bd commit 22a6640
Show file tree
Hide file tree
Showing 4 changed files with 345 additions and 1 deletion.
1 change: 1 addition & 0 deletions src/__tests__/__snapshots__/index.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ exports[`test existence of exported functions 1`] = `
"xyRealMaxYPoint",
"xyRealMinYPoint",
"xyReduce",
"xyReduceNonContinuous",
"xyRolling",
"xySetYValue",
"xySortX",
Expand Down
229 changes: 229 additions & 0 deletions src/xy/__tests__/xyReduceNonContinuous.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import { expect, test } from 'vitest';

import { xyReduceNonContinuous } from '../xyReduceNonContinuous';

const x = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const y = [0, 1, 2, 3, 4, 5, 4, 3, 2, 1, 0];
test('All', () => {
const result = xyReduceNonContinuous(
{ x, y },
{ maxApproximateNbPoints: 20 },
);
expect(result).toStrictEqual({
x: Float64Array.from(x),
y: Float64Array.from(y),
});
});

test('Over sized', () => {
const x2 = [1, 2];
const y2 = [2, 3];
const result = xyReduceNonContinuous(
{ x: x2, y: y2 },
{ maxApproximateNbPoints: 10 },
);
expect(result).toStrictEqual({
x: Float64Array.from([1, 2]),
y: Float64Array.from([2, 3]),
});
});

test('Too large', () => {
const result = {
x: new Float64Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]),
y: new Float64Array([0, 1, 2, 3, 4, 5, 4, 3, 2, 1, 0]),
};
expect(
xyReduceNonContinuous(
{ x, y },
{ maxApproximateNbPoints: 20, from: -10, to: 20 },
),
).toStrictEqual(result);
});

test('Part exact', () => {
const result = {
x: new Float64Array([3, 4, 5]),
y: new Float64Array([3, 4, 5]),
};
expect(
xyReduceNonContinuous(
{ x, y },
{ from: 3, to: 5, maxApproximateNbPoints: 20 },
),
).toStrictEqual(result);
});

test('Part rounded close', () => {
const result = {
x: new Float64Array([3, 4, 5]),
y: new Float64Array([3, 4, 5]),
};
expect(
xyReduceNonContinuous(
{ x, y },
{ from: 3.1, to: 4.9, maxApproximateNbPoints: 20 },
),
).toStrictEqual(result);
});

test('Part rounded far', () => {
const result = {
x: new Float64Array([3, 4, 5]),
y: new Float64Array([3, 4, 5]),
};
expect(
xyReduceNonContinuous(
{ x, y },
{ from: 3.6, to: 4.4, maxApproximateNbPoints: 20 },
),
).toStrictEqual(result);
});

test('Part rounded far 2', () => {
const result = xyReduceNonContinuous({ x, y }, { maxApproximateNbPoints: 5 });
expect(result).toStrictEqual({ x: [0, 3, 6, 8], y: [2, 5, 4, 2] });
});

test('Part rounded big data', () => {
const x2 = [];
const y2 = [];
for (let i = 0; i < 5000000; i++) {
x2.push(i);
y2.push(i);
}
const result = xyReduceNonContinuous(
{ x: x2, y: y2 },
{ maxApproximateNbPoints: 4000 },
);
expect(result.x).toHaveLength(4000);
expect(result.y).toHaveLength(4000);
});

test('Part rounded big data 2', () => {
const x2 = [];
const y2 = [];
for (let i = 0; i < 5000000; i++) {
x2.push(i);
y2.push(i);
}
const result = xyReduceNonContinuous(
{ x: x2, y: y2 },
{ maxApproximateNbPoints: 4000, from: 10, to: 20 },
);
expect(result.x).toStrictEqual(
Float64Array.from([10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]),
);
expect(result.y).toStrictEqual(
Float64Array.from([10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]),
);
});

test('xyCheck non-linear x', () => {
const xs = [];
const ys = [];
for (let i = 0; i < 11; i++) {
xs.push(i * 1.2 ** i);
ys.push(i);
}
const result = xyReduceNonContinuous(
{ x: xs, y: ys },
{ maxApproximateNbPoints: 5 },
);
expect(result.y).toStrictEqual([5, 7, 8, 10]);
});

test('xyCheck extreme non-linear x', () => {
const xs = [];
const ys = [];
for (let i = 0; i < 11; i++) {
xs.push(i * 2 ** i);
ys.push(i);
}
const result = xyReduceNonContinuous(
{ x: xs, y: ys },
{ maxApproximateNbPoints: 5 },
);
expect(result).toStrictEqual({ x: [0, 4608, 10240], y: [8, 9, 10] });
});

test('xyReduceNonContinuous with zones enough points', () => {
const result = xyReduceNonContinuous(
{ x, y },
{
maxApproximateNbPoints: 5,
zones: [
{ from: 0, to: 1 },
{ from: 5, to: 7 },
],
},
);

expect(result).toStrictEqual({
x: new Float64Array([0, 1, 5, 6, 7]),
y: new Float64Array([0, 1, 5, 4, 3]),
});
});

test('xyReduceNonContinuous with zones not enough points edge cases', () => {
const result = xyReduceNonContinuous(
{ x, y },
{
maxApproximateNbPoints: 3,
zones: [
{ from: 0, to: 1 },
{ from: 5, to: 8 },
],
},
);
expect(result).toStrictEqual({ x: [0, 1, 5], y: [0, 1, 5] });
});

test('xyReduceNonContinuous with zones not enough points', () => {
const result = xyReduceNonContinuous(
{ x, y },
{
maxApproximateNbPoints: 4,
zones: [
{ from: 0, to: 1 },
{ from: 5, to: 8 },
],
},
);
// the second zone will have only one point because deltaX is 3.3333
expect(result).toStrictEqual({ x: [0, 1, 5], y: [0, 1, 5] });
});

test('xyReduceNonContinuous with one zone not enough points', () => {
const result = xyReduceNonContinuous(
{ x, y },
{
maxApproximateNbPoints: 4,
zones: [
{ from: -1, to: -1 },
{ from: 3, to: 8 },
],
},
);
expect(result).toStrictEqual({ x: [3, 7], y: [5, 3] });
});

test('Large data with zones', () => {
const x2 = [];
const y2 = [];
for (let i = 0; i < 5000001; i++) {
x2.push(i);
y2.push(i);
}
const result = xyReduceNonContinuous(
{ x: x2, y: y2 },
{
maxApproximateNbPoints: 6,
zones: [
{ from: 0, to: 1000 },
{ from: 1000000, to: 1001000 },
],
},
);
expect(result).toStrictEqual({ x: [0, 1000000], y: [1000, 1001000] });
});
3 changes: 2 additions & 1 deletion src/xy/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ export * from './xyMinYPoint';
export * from './xyPeakInfo';
export * from './xyRealMaxYPoint';
export * from './xyRealMinYPoint';
export * from './xyReduce';
export { xyReduce } from './xyReduce';
export * from './xyReduceNonContinuous';
export * from './xyRolling';
export * from './xySetYValue';
export * from './xySortX';
Expand Down
113 changes: 113 additions & 0 deletions src/xy/xyReduceNonContinuous.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { DataXY, DoubleArray, FromTo } from 'cheminfo-types';

import { zonesNormalize } from '../zones';

import { xyCheck } from './xyCheck';
import { getInternalZones, notEnoughPoints } from './xyReduce';

export interface XYReduceOptions {
/**
* @default x[0]
*/
from?: number;

/**
* @default x[x.length-1]
*/
to?: number;

/**
* Number of points but we could have couple more
* @default 4001
*/
maxApproximateNbPoints?: number;

/**
* Array of zones to keep (from/to object)
* @default []
*/
zones?: FromTo[];
}

/**
* Reduce the number of points while keeping visually the same noise. Practical to
* display many spectra as SVG. This algorithm is designed for non-continuous data.
* We are expecting peaks to be only positive and the x values to be ordered.
* SHOULD NOT BE USED FOR DATA PROCESSING !!!
* @param data - Object that contains property x (an ordered increasing array) and y (an array)
* @param options - options
* @returns Object with x and y arrays
*/
export function xyReduceNonContinuous(
data: DataXY,
options: XYReduceOptions = {},
): DataXY<DoubleArray> {
xyCheck(data);
if (data.x.length < 2) {
// todo we should check that the single point is really in the range and the zones
return {
x: Float64Array.from(data.x),
y: Float64Array.from(data.y),
};
}
const { x, y } = data;
const {
from = x[0],
to = x.at(-1) as number,
maxApproximateNbPoints = 4001,
} = options;
let { zones = [] } = options;

zones = zonesNormalize(zones, { from, to });
if (zones.length === 0) zones = [{ from, to }]; // we take everything

const { internalZones, totalPoints } = getInternalZones(zones, x);

// we calculate the number of points per zone that we should keep
if (totalPoints <= maxApproximateNbPoints) {
return notEnoughPoints(x, y, internalZones, totalPoints);
}

const deltaX = (to - from) / (maxApproximateNbPoints - 1);
const newX: number[] = [];
const newY: number[] = [];
for (const internalZone of internalZones) {
const maxNbPoints =
Math.ceil((internalZone.to - internalZone.from) / deltaX) + 1;
const fromIndex = internalZone.fromIndex;
const toIndex = internalZone.toIndex;

if (toIndex - fromIndex + 1 <= maxNbPoints) {
// we keep all the points
for (let i = fromIndex; i <= toIndex; i++) {
newX.push(x[i]);
newY.push(y[i]);
}
} else {
// we need to reduce the number of points
let currentX = x[fromIndex];
let currentY = y[fromIndex];
let lastX = currentX + deltaX;
newX.push(currentX);
newY.push(currentY);
for (let i = fromIndex; i <= toIndex; i++) {
if (x[i] > lastX) {
// next slot
currentX = x[i];
currentY = y[i];
newX.push(currentX);
newY.push(currentY);
lastX += deltaX;
}
if (y[i] > currentY) {
currentY = y[i];
newY[newY.length - 1] = currentY;
}
}
}
}
return {
x: newX,
y: newY,
};
}

0 comments on commit 22a6640

Please sign in to comment.