ml-array-xy-equally-spaced
Version:
Get the closest point for a specific abscissa value
323 lines (268 loc) • 8.48 kB
JavaScript
var sequentialFill = require('ml-array-sequential-fill');
var mlZones = require('ml-zones');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var sequentialFill__default = /*#__PURE__*/_interopDefaultLegacy(sequentialFill);
/**
* function that retrieves the getEquallySpacedData with the variant "slot"
*
* @param {Array<number>} x
* @param {Array<number>} y
* @param {number} from - Initial point
* @param {number} to - Final point
* @param {number} numberOfPoints
* @return {Array} - Array of y's equally spaced with the variant "slot"
*/
function equallySpacedSlot(x, y, from, to, numberOfPoints) {
let xLength = x.length;
let step = (to - from) / (numberOfPoints > 1 ? numberOfPoints - 1 : 1);
let halfStep = step / 2;
let lastStep = x[x.length - 1] - x[x.length - 2];
let start = from - halfStep;
let output = new Array(numberOfPoints);
// Init main variables
let min = start;
let max = start + step;
let previousX = -Number.MAX_VALUE;
let previousY = 0;
let nextX = x[0];
let nextY = y[0];
let frontOutsideSpectra = 0;
let backOutsideSpectra = true;
let currentValue = 0;
// for slot algorithm
let currentPoints = 0;
let i = 1; // index of input
let j = 0; // index of output
main: while (true) {
if (previousX >= nextX) throw new Error('x must be a growing series');
while (previousX - max > 0) {
// no overlap with original point, just consume current value
if (backOutsideSpectra) {
currentPoints++;
backOutsideSpectra = false;
}
output[j] = currentPoints <= 0 ? 0 : currentValue / currentPoints;
j++;
if (j === numberOfPoints) {
break main;
}
min = max;
max += step;
currentValue = 0;
currentPoints = 0;
}
if (previousX > min) {
currentValue += previousY;
currentPoints++;
}
if (previousX === -Number.MAX_VALUE || frontOutsideSpectra > 1) {
currentPoints--;
}
previousX = nextX;
previousY = nextY;
if (i < xLength) {
nextX = x[i];
nextY = y[i];
i++;
} else {
nextX += lastStep;
nextY = 0;
frontOutsideSpectra++;
}
}
return output;
}
/**
* Function that calculates the integral of the line between two
* x-coordinates, given the slope and intercept of the line.
* @param {number} x0
* @param {number} x1
* @param {number} slope
* @param {number} intercept
* @return {number} integral value.
*/
function integral(x0, x1, slope, intercept) {
return (
0.5 * slope * x1 * x1 +
intercept * x1 -
(0.5 * slope * x0 * x0 + intercept * x0)
);
}
/**
* function that retrieves the getEquallySpacedData with the variant "smooth"
*
* @param {Array<number>} x
* @param {Array<number>} y
* @param {number} from - Initial point
* @param {number} to - Final point
* @param {number} numberOfPoints
* @return {Array} - Array of y's equally spaced with the variant "smooth"
*/
function equallySpacedSmooth(x, y, from, to, numberOfPoints) {
let xLength = x.length;
let step = (to - from) / (numberOfPoints > 1 ? numberOfPoints - 1 : 1);
let halfStep = step / 2;
let output = new Array(numberOfPoints);
let initialOriginalStep = x[1] - x[0];
let lastOriginalStep = x[xLength - 1] - x[xLength - 2];
// Init main variables
let min = from - halfStep;
let max = from + halfStep;
let previousX = Number.MIN_SAFE_INTEGER;
let previousY = 0;
let nextX = x[0] - initialOriginalStep;
let nextY = 0;
let currentValue = 0;
let slope = 0;
let intercept = 0;
let sumAtMin = 0;
let sumAtMax = 0;
let i = 0; // index of input
let j = 0; // index of output
function getSlope(x0, y0, x1, y1) {
return (y1 - y0) / (x1 - x0);
}
let add = 0;
main: while (true) {
if (previousX >= nextX) throw new Error('x must be a growing series');
if (previousX <= min && min <= nextX) {
add = integral(0, min - previousX, slope, previousY);
sumAtMin = currentValue + add;
}
while (nextX - max >= 0) {
// no overlap with original point, just consume current value
add = integral(0, max - previousX, slope, previousY);
sumAtMax = currentValue + add;
output[j++] = (sumAtMax - sumAtMin) / step;
if (j === numberOfPoints) {
break main;
}
min = max;
max += step;
sumAtMin = sumAtMax;
}
currentValue += integral(previousX, nextX, slope, intercept);
previousX = nextX;
previousY = nextY;
if (i < xLength) {
nextX = x[i];
nextY = y[i];
i++;
} else if (i === xLength) {
nextX += lastOriginalStep;
nextY = 0;
}
slope = getSlope(previousX, previousY, nextX, nextY);
intercept = -slope * previousX + previousY;
}
return output;
}
/**
* Function that returns a Number array of equally spaced numberOfPoints
* containing a representation of intensities of the spectra arguments x
* and y.
*
* The options parameter contains an object in the following form:
* from: starting point
* to: last point
* numberOfPoints: number of points between from and to
* variant: "slot" or "smooth" - smooth is the default option
*
* The slot variant consist that each point in the new array is calculated
* averaging the existing points between the slot that belongs to the current
* value. The smooth variant is the same but takes the integral of the range
* of the slot and divide by the step size between two points in the new array.
*
* If exclusions zone are present, zones are ignored !
* @param {object} [arrayXY={}] - object containing 2 properties x and y (both an array)
* @param {object} [options={}]
* @param {number} [options.from=x[0]]
* @param {number} [options.to=x[x.length-1]]
* @param {string} [options.variant='smooth']
* @param {number} [options.numberOfPoints=100]
* @param {Array} [options.exclusions=[]] array of from / to that should be skipped for the generation of the points
* @param {Array} [options.zones=[]] array of from / to that should be kept
* @return {object<x: Array, y:Array>} new object with x / y array with the equally spaced data.
*/
function equallySpaced(arrayXY = {}, options = {}) {
let { x, y } = arrayXY;
let xLength = x.length;
let reverse = false;
if (x.length > 1 && x[0] > x[1]) {
x = x.slice().reverse();
y = y.slice().reverse();
reverse = true;
}
let {
from = x[0],
to = x[xLength - 1],
variant = 'smooth',
numberOfPoints = 100,
exclusions = [],
zones = [],
} = options;
if (xLength !== y.length) {
throw new RangeError("the x and y vector doesn't have the same size.");
}
if (typeof from !== 'number' || isNaN(from)) {
throw new RangeError("'from' option must be a number");
}
if (typeof to !== 'number' || isNaN(to)) {
throw new RangeError("'to' option must be a number");
}
if (typeof numberOfPoints !== 'number' || isNaN(numberOfPoints)) {
throw new RangeError("'numberOfPoints' option must be a number");
}
if (numberOfPoints < 2) {
throw new RangeError("'numberOfPoints' option must be greater than 1");
}
if (zones.length === 0) {
zones = mlZones.invert(exclusions, { from, to });
}
zones = mlZones.zonesWithPoints(zones, numberOfPoints, { from, to });
let xResult = [];
let yResult = [];
for (let zone of zones) {
let zoneResult = processZone(
x,
y,
zone.from,
zone.to,
zone.numberOfPoints,
variant);
xResult = xResult.concat(zoneResult.x);
yResult = yResult.concat(zoneResult.y);
}
if (reverse) {
if (from < to) {
return { x: xResult.reverse(), y: yResult.reverse() };
} else {
return { x: xResult, y: yResult };
}
} else {
if (from < to) {
return { x: xResult, y: yResult };
} else {
return { x: xResult.reverse(), y: yResult.reverse() };
}
}
}
function processZone(x, y, from, to, numberOfPoints, variant) {
if (numberOfPoints < 1) {
throw new RangeError('the number of points must be at least 1');
}
let output =
variant === 'slot'
? equallySpacedSlot(x, y, from, to, numberOfPoints)
: equallySpacedSmooth(x, y, from, to, numberOfPoints);
return {
x: sequentialFill__default["default"]({
from,
to,
size: numberOfPoints,
}),
y: output,
};
}
module.exports = equallySpaced;
;