peaks-similarity
Version:
Peaks similarity - calculate the similarity between 2 ordered arrays of peaks
368 lines • 13.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Comparator = exports.COMMON_BOTH = exports.COMMON_SECOND = exports.COMMON_FIRST = exports.COMMON_NO = void 0;
const calculateOverlapFromDiff_1 = require("./calculateOverlapFromDiff");
const checkPeaks_1 = require("./checkPeaks");
const commonExtractAndNormalize_1 = require("./commonExtractAndNormalize");
const extract_1 = require("./extract");
const extractAndNormalize_1 = require("./extractAndNormalize");
const getCommonArray_js_1 = require("./getCommonArray.js");
const getIntersection_1 = require("./getIntersection");
const normalize_1 = require("./normalize");
exports.COMMON_NO = 0;
exports.COMMON_FIRST = 1;
exports.COMMON_SECOND = 2;
exports.COMMON_BOTH = 3;
/**
* A number, or a string containing a number.
* @typedef {([number[],number[]]|[number,number][]|{x:number[],y:number[]})} Peaks
*/
/**
* Create a comparator class
* {object} [options={}]
* {string} [options.common=''] should we take only common peaks 'first', 'second', 'both', ''
* {number} [options.widthBottom=2] bottom trapezoid width for similarity evaluation
* {number} [options.widthTop=1] top trapezoid width for similarity evaluation
* {number} [options.from] from region used for similarity calculation
* {number} [options.to] to region used for similarity calculation
*/
class Comparator {
constructor(options = {}) {
this.array1 = [];
this.array2 = [];
this.setOptions(options);
}
/*
2 formats are allowed:
[[x1,x2,...],[y1,y2,...]] or [[x1,y1],[x2,y2], ...]
*/
setOptions(options = {}) {
if (typeof options.common === 'string') {
if (options.common.toLowerCase() === 'first') {
this.common = exports.COMMON_FIRST;
}
else if (options.common.toLowerCase() === 'second') {
this.common = exports.COMMON_SECOND;
}
else if (options.common.toLowerCase() === 'both') {
this.common = exports.COMMON_BOTH;
}
else {
this.common = exports.COMMON_NO;
}
}
else if (options.common === true) {
this.common = exports.COMMON_BOTH;
}
else {
this.common = exports.COMMON_NO;
}
this.trapezoid = options.trapezoid;
this.commonFactor = options.commonFactor || this.commonFactor || 4;
const { widthBottom = this.widthBottom || 2, widthTop = this.widthTop || 1, from = this.from, to = this.to, } = options;
this.setTrapezoid(widthBottom, widthTop);
this.setFromTo(from, to);
}
/**
*
* @param {Peaks} peaks
*/
setPeaks1(peaks) {
this.array1 = (0, checkPeaks_1.checkPeaks)(peaks);
if (this.common) {
const extracts = (0, commonExtractAndNormalize_1.commonExtractAndNormalize)(this.array1, this.array2, this.widthBottom, this.from, this.to, this.common);
this.array1Extract = extracts.data1;
this.array1ExtractInfo = extracts.info1;
this.array2Extract = extracts.data2;
this.array2ExtractInfo = extracts.info2;
}
else {
const extract = (0, extractAndNormalize_1.extractAndNormalize)(this.array1, this.from, this.to);
this.array1Extract = extract.data;
this.array1ExtractInfo = extract.info;
}
}
/**
*
* @param {Peaks} peaks
*/
setPeaks2(peaks) {
this.array2 = (0, checkPeaks_1.checkPeaks)(peaks);
if (this.common) {
const extracts = (0, commonExtractAndNormalize_1.commonExtractAndNormalize)(this.array1, this.array2, this.widthBottom, this.from, this.to, this.common);
this.array1Extract = extracts.data1;
this.array1ExtractInfo = extracts.info1;
this.array2Extract = extracts.data2;
this.array2ExtractInfo = extracts.info2;
}
else {
const extract = (0, extractAndNormalize_1.extractAndNormalize)(this.array2, this.from, this.to);
this.array2Extract = extract.data;
this.array2ExtractInfo = extract.info;
}
}
getExtract1() {
return this.array1Extract;
}
getExtract2() {
return this.array2Extract;
}
getExtractInfo1() {
return this.array1ExtractInfo;
}
getExtractInfo2() {
return this.array2ExtractInfo;
}
/**
* Set the new bottom and top width of the trapezoid
* @param {number} newWidthBottom
* @param {number} newWidthTop
*/
setTrapezoid(newWidthBottom, newWidthTop) {
this.widthTop = newWidthTop;
this.widthBottom = newWidthBottom;
this.widthSlope = (this.widthBottom - this.widthTop) / 2;
if (this.widthBottom < this.widthTop) {
throw new Error('widthBottom has to be larger than widthTop');
}
}
/**
* Set the from / to for comparison
* @param {number} newFrom - set the new from value
* @param {number} newTo - set the new to value
* @returns
*/
setFromTo(newFrom, newTo) {
if (newFrom === this.from && newTo === this.to)
return;
this.from = newFrom;
this.to = newTo;
if (this.common) {
const extracts = (0, commonExtractAndNormalize_1.commonExtractAndNormalize)(this.array1, this.array2, this.widthBottom, this.from, this.to, this.common, this.commonFactor);
this.array1Extract = extracts.data1;
this.array1ExtractInfo = extracts.info1;
this.array2Extract = extracts.data2;
this.array2ExtractInfo = extracts.info2;
}
else {
let extract1 = (0, extractAndNormalize_1.extractAndNormalize)(this.array1, this.from, this.to);
this.array1Extract = extract1.data;
this.array1ExtractInfo = extract1.info;
let extract2 = (0, extractAndNormalize_1.extractAndNormalize)(this.array2, this.from, this.to);
this.array2Extract = extract2.data;
this.array2ExtractInfo = extract2.info;
}
}
/**
*
* @param {number} x1
* @param {number} y1
* @param {number} x2
* @param {number} y2
* @returns
*/
getOverlap(x1, y1, x2, y2) {
if (y1 === 0 || y2 === 0)
return 0;
// TAKE CARE !!! We multiply the diff by 2 !!!
const diff = Math.abs(x1 - x2) * 2;
if (diff > this.widthBottom)
return 0;
if (diff <= this.widthTop) {
return Math.min(y1, y2);
}
const maxValue = (Math.max(y1, y2) * (this.widthBottom - diff)) /
(this.widthBottom - this.widthTop);
return Math.min(y1, y2, maxValue);
}
/**
* This is the old trapezoid similarity
* @param {number} x1
* @param {number} y1
* @param {number} x2
* @param {number} y2
* @param {number} widthTop
* @param {number} widthBottom
* @returns
*/
getOverlapTrapezoid(x1, y1, x2, y2, widthTop, widthBottom) {
// eslint-disable-next-line no-console
console.error('getOverlapTrapezoid should not be used anymore');
const factor = 2 / (widthTop + widthBottom); // correction for surface=1
if (y1 === 0 || y2 === 0)
return 0;
if (x1 === x2) {
// they have the same position
return Math.min(y1, y2);
}
const diff = Math.abs(x1 - x2);
if (diff >= widthBottom)
return 0;
if (y1 === y2) {
// do they have the same height ???
// we need to find the common length
if (diff <= widthTop) {
return ((widthTop + widthBottom) / 2 - diff) * y1 * factor;
}
else if (diff <= widthBottom) {
return ((((((widthBottom - diff) * y1) / 2) * (diff - widthTop)) /
(widthBottom - widthTop)) *
factor);
}
return 0;
}
else {
// the height are different and not the same position ...
// we need to consider only one segment to find its intersection
const small = Math.min(y1, y2);
const big = Math.max(y1, y2);
const targets = [
[
[0, 0],
[this.widthSlope, small],
],
[
[this.widthSlope, small],
[this.widthSlope + widthTop, small],
],
[
[widthTop + this.widthSlope, small],
[widthBottom, 0],
],
];
let segment;
if ((x1 > x2 && y1 > y2) || (x1 < x2 && y1 < y2)) {
segment = [
[diff, 0],
[diff + this.widthSlope, big],
];
}
else {
segment = [
[diff + this.widthSlope, big],
[diff, 0],
];
}
for (let i = 0; i < 3; i++) {
const intersection = (0, getIntersection_1.getIntersection)(targets[i], segment);
if (intersection) {
switch (i) {
case 0:
return small - ((diff * intersection.y) / 2) * factor;
case 1: // to simplify ...
// console.log(" ",widthSlope,small,big,intersection.x)
return ((((this.widthSlope * small) / (2 * big)) * small +
(widthTop + this.widthSlope - intersection.x) * small +
(this.widthSlope * small) / 2) *
factor);
case 2:
return (((widthBottom - diff) * intersection.y) / 2) * factor;
default:
throw new Error(`unexpected intersection value: ${i}`);
}
}
}
}
return NaN;
}
/**
* This method calculates the total diff. The sum of positive value will yield to overlap
* @returns
*/
calculateDiff() {
// we need to take 2 pointers
// and travel progressively between them ...
const newFirst = [
this.array1Extract[0].slice(),
this.array1Extract[1].slice(),
];
const newSecond = [
this.array2Extract[0].slice(),
this.array2Extract[1].slice(),
];
const array1Length = this.array1Extract[0]
? this.array1Extract[0].length
: 0;
const array2Length = this.array2Extract[0]
? this.array2Extract[0].length
: 0;
let pos1 = 0;
let pos2 = 0;
let previous2 = 0;
while (pos1 < array1Length) {
const diff = newFirst[0][pos1] - this.array2Extract[0][pos2];
if (Math.abs(diff) < this.widthBottom) {
// there is some overlap
let overlap;
if (this.trapezoid) {
// old trapezoid overlap similarity
overlap = this.getOverlapTrapezoid(newFirst[0][pos1], newFirst[1][pos1], newSecond[0][pos2], newSecond[1][pos2], this.widthTop, this.widthBottom);
}
else {
overlap = this.getOverlap(newFirst[0][pos1], newFirst[1][pos1], newSecond[0][pos2], newSecond[1][pos2], this.widthTop, this.widthBottom);
}
newFirst[1][pos1] -= overlap;
newSecond[1][pos2] -= overlap;
if (pos2 < array2Length - 1) {
pos2++;
}
else {
pos1++;
pos2 = previous2;
}
}
else if (diff > 0 && pos2 < array2Length - 1) {
pos2++;
previous2 = pos2;
}
else {
pos1++;
pos2 = previous2;
}
}
return newSecond;
}
/**
* Set the new peaks and return info
* @param {Peaks} newPeaks1
* @param {Peaks} newPeaks2
* @returns
*/
getSimilarity(newPeaks1, newPeaks2) {
if (newPeaks1)
this.setPeaks1(newPeaks1);
if (newPeaks2)
this.setPeaks2(newPeaks2);
const diff = this.calculateDiff();
return {
diff,
extract1: this.getExtract1(),
extract2: this.getExtract2(),
extractInfo1: this.getExtractInfo1(),
extractInfo2: this.getExtractInfo2(),
similarity: (0, calculateOverlapFromDiff_1.calculateOverlapFromDiff)(diff),
widthBottom: this.widthBottom,
widthTop: this.widthTop,
};
}
/**
* This works mainly when you have a array1 that is fixed
* newPeaks2 have to be normalized ! (sum to 1)
* @param {Peaks} newPeaks2
* @param {number} from
* @param {number} to
* @returns
*/
fastSimilarity(newPeaks2, from, to) {
this.array1Extract = (0, extract_1.extract)(this.array1, from, to);
this.array2Extract = newPeaks2;
if (this.common & exports.COMMON_SECOND) {
this.array1Extract = (0, getCommonArray_js_1.getCommonArray)(this.array1Extract, this.array2Extract, this.widthBottom);
}
(0, normalize_1.normalize)(this.array1Extract);
const diff = this.calculateDiff();
return (0, calculateOverlapFromDiff_1.calculateOverlapFromDiff)(diff);
}
}
exports.Comparator = Comparator;
//# sourceMappingURL=index.js.map