Files
generalFunctions/src/predict/predict_class.js
znetsixe 9610e7138d Added extra pump data
lagged sample in measurement
2025-11-03 15:22:51 +01:00

595 lines
20 KiB
JavaScript

/**
* @file Predict_class.js
*
* Permission is hereby granted to any person obtaining a copy of this software
* and associated documentation files (the "Software"), to use it for personal
* or non-commercial purposes, with the following restrictions:
*
* 1. **No Copying or Redistribution**: The Software or any of its parts may not
* be copied, merged, distributed, sublicensed, or sold without explicit
* prior written permission from the author.
*
* 2. **Commercial Use**: Any use of the Software for commercial purposes requires
* a valid license, obtainable only with the explicit consent of the author.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM,
* OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Ownership of this code remains solely with the original author. Unauthorized
* use of this Software is strictly prohibited.
*
* @summary Class for predicting values based on a multidimensional curve.
* @description Class for predicting values based on a multidimensional curve.
* @module Predict_class
* @requires EventEmitter
* @requires ConfigUtils
* @requires Interpolation
* @requires Logger
* @exports Predict
* @version 0.1.0
* @since 0.1.0
*
* Author:
* - Rene De Ren
* Email:
* - rene@thegoldenbasket.nl
* Future Improvements:
- Add more interpolation types
- **Local Derivative (Slope)**: Instantaneous rate of change (dY/dX) at the current X. Useful for determining if the curve is ascending or descending.
- **Second Derivative (Curvature)**: Curvature (d²Y/dX²) at the current X. Indicates how quickly the slope is changing (e.g., sharp or broad peaks).
- **Distance to Nearest Local Peak or Valley**: X-distance from the current X to the closest local maximum or minimum. Useful for detecting proximity to turning points.
- **Global Statistics (Mean, Median, Std Dev)**:
- Mean: Average of Y.
- Median: Middle Y value (sorted).
- Std Dev: Variability of Y. Provides insight into central tendency and spread, aiding in normalization or anomaly detection.
- **Integrated Area Under the Curve (AUC)**: Numerical integration of Y across the X-range. Useful for total sums or energy-related calculations.
- **Peak “Sharpness” or “Prominence”**: Measure of a peak's height and width relative to surrounding valleys. Important for signal processing or optimization.
- **Nearest Points Around Current X**: Data points (or interpolated values) immediately to the left and right of the current X. Useful for local interpolation or neighbor analysis.
- **Forecast / Extrapolation**: Estimated Y values outside the known X-range. Useful for exploring scenarios slightly beyond the data range (use with caution).
- **Peak Count**: Total number of local maxima in the curve. Useful for identifying all peaks and their prominence.
- **Position Relative to Mean (or Other Reference Lines)**: Distance (in percent or absolute value) of the current Y from a reference line (e.g., mean or median). Provides context relative to average or baseline levels.
- **Local Slope Trend**: Direction of the slope (up, down, or flat) at the current X. Useful for identifying trends or inflection points.
- **Local Curvature Trend**: Direction of the curvature (concave up, concave down, or flat) at the current X. Useful for identifying inflection points or turning points.
- **Local Peak-to-Valley Ratio**: Ratio of the current peak height to the nearest valley depth. Useful for identifying peak prominence or sharpness.
- ** Keep track of previous request and next request to identify slope and curvature
*/
const EventEmitter = require('events');
const Logger = require('../helper/logger.js');
const defaultConfig = require('./predictConfig.json');
const ConfigUtils = require('../helper/configUtils');
const Interpolation = require('./interpolation');
class Predict {
constructor(config = {}) {
// Initialize dependencies
this.emitter = new EventEmitter(); // Own EventEmitter
this.configUtils = new ConfigUtils(defaultConfig);
this.config = this.configUtils.initConfig(config);
// Init after config is set
this.logger = new Logger(this.config.general.logging.enabled,this.config.general.logging.logLevel, this.config.general.name);
this.interpolation = new Interpolation(this.config.interpolation);
// Input and state
this.inputCurve = {};
this.currentF = 0;
this.currentX = 0;
this.outputY = 0;
// Curves and Splines
this.normalizedCurve = {};
this.calculatedCurve = {};
this.fCurve = {};
this.currentFxyCurve = {};
this.normalizedSplines = {};
this.fSplines = {};
this.currentFxySplines = {};
// Stored min/max values
this.xValues = {};
this.fValues = {};
this.yValues = {};
this.currentFxyXMin = 0;
this.currentFxyXMax = 0;
this.currentFxyYMin = 0;
this.currentFxyYMax = 0;
// From config
this.normMin = this.config.normalization.parameters.min;
this.normMax = this.config.normalization.parameters.max;
this.calculationPoints = this.config.normalization.parameters.curvePoints;
this.interpolationType = this.config.interpolation.type;
// Load curve if provided
if (config.curve) {
this.inputCurveData = config.curve;
} else {
this.logger.warn("No curve data provided. Please set curve data using setCurveData method. Using default");
this.inputCurveData = this.config.curve;
}
}
// Improved function to get a local peak in an array by starting in the middle.
// It also handles the case of a tie by preferring the left side (arbitrary choice)
// when array[start] == leftValue or array[start] == rightValue.
getLocalPeak(array) {
if (!Array.isArray(array) || array.length === 0) {
return { peak: null, peakIndex: -1 };
}
let left = 0;
let right = array.length - 1;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
// Safely retrieve left/right neighbor values (use -Infinity if out of bounds)
const leftVal = mid - 1 >= 0 ? array[mid - 1] : -Infinity;
const rightVal = mid + 1 < array.length ? array[mid + 1] : -Infinity;
const currentVal = array[mid];
// Check if mid is a local peak
if (currentVal >= leftVal && currentVal >= rightVal) {
return { peak: currentVal, peakIndex: mid };
}
// If left neighbor is bigger, move left
if (leftVal > currentVal) {
right = mid - 1;
}
// Otherwise, move right
else {
left = mid + 1;
}
}
// If no local peak is found
return { peak: null, peakIndex: -1 };
}
// Function what uses the peak in the y array to return the yPeak, x value and its procentual value
getPosXofYpeak(curve) {
//find index of y peak
const { peak , peakIndex } = this.getLocalPeak(curve.y);
// scale the x value to procentual value
const yPeak = peak;
const x = curve.x[peakIndex];
const xMin = Math.min(...curve.x);
const xMax = Math.max(...curve.x);
const xProcent = (x - xMin) / (xMax - xMin) * 100;
return { yPeak, x, xProcent };
}
calcRelativePositionToPeak(curve , outputY) {
//find y peak
const { peak } = this.getLocalPeak(curve.y);
if ( peak === null ) {
this.logger.warn("No peak found in curve");
return -1;
}
// Calculate the "peak-only" percentage:
// - Distance from peak, relative to peak itself
// - 0% => outputY == peak, 100% => outputY == 0 (if peak != 0)
let peakOnlyPercentage;
const distanceFromPeak = Math.abs(peak - outputY);
if (peak === 0) {
// If peak is 0, then the concept of "peak-only" percentage is tricky.
// If outputY is also 0 => 0%, otherwise => Infinity.
peakOnlyPercentage = distanceFromPeak === 0 ? 0 : Number.POSITIVE_INFINITY;
} else {
peakOnlyPercentage = (distanceFromPeak / peak) * 100;
}
// Calculate the range-based percentage:
// - Range = [yMin, peak]
// - 0% => outputY == peak, 100% => outputY == yMin
const yMin = Math.min(...curve.y);
let rangeBasedPercentage = -1;
// If peak <= yMin, there is no vertical range for normalization
if (peak > yMin) {
const distanceFromPeakRange = peak - outputY; // Not absolute
const totalRange = peak - yMin;
rangeBasedPercentage = (distanceFromPeakRange / totalRange) * 100;
// Optionally clamp to [0, 100] if outputY goes out of bounds
rangeBasedPercentage = Math.max(0, Math.min(100, rangeBasedPercentage));
}
return {
peakOnlyPercentage: Math.round(peakOnlyPercentage * 100) / 100,
rangeBasedPercentage: Math.round(rangeBasedPercentage * 100) / 100
};
}
// Function to retrieve current curve including the interpolated active point
retrieveActiveCurve(){
// Retreive y values
const yValues = this.currentFxyCurve[this.fDimension].y;
// Retreive normalized x values
const xValues = this.denormalizeXvals( this.currentFxyCurve[this.fDimension].x );
//check what the current x value is
const currentX = this.currentX;
//check current y Output value
const outputY = this.outputY;
//find where the current x value should be in the xValues array
const index = xValues.findIndex((x) => x > currentX);
// push the yOutput value in the yValues array between the current x value
yValues.splice(index, 0, outputY);
xValues.splice(index, 0, currentX);
return { xValues, yValues };
}
set fDimension(newF) {
if (newF < this.fValues.min || newF > this.fValues.max) {
this.logger.warn(`New f =${newF} is constrained to fit between min=${this.fValues.min} and max=${this.fValues.max}`);
newF = this.constrain(newF,this.fValues.min,this.fValues.max);
}
if (newF in this.calculatedCurve) {
this.currentFxyCurve[newF] = this.calculatedCurve[newF];
this.currentFxySplines = this.normalizedSplines;
} else {
this.currentFxyCurve = this.buildSingleFxyCurve(
this.fSplines,
this.calculatedCurve,
newF,
this.calculationPoints
);
this.currentFxySplines = this.buildXySplines(this.currentFxyCurve, this.interpolationType);
}
const yArray = this.currentFxyCurve[newF].y;
this.currentFxyYMin = Math.min(...yArray);
this.currentFxyYMax = Math.max(...yArray);
this.calculateFxyXRange(newF);
this.currentF = newF;
this.logger.debug(`Calculating new yValue using X= ${this.currentX}`);
// Recalculate output y based on currentX
this.y(this.currentX);
}
get fDimension() {
return this.currentF;
}
// Function to predict Y value based on X value
y(x) {
// Clamp value before normalization
if (x > this.currentFxyXMax) x = this.currentFxyXMax;
if (x < this.currentFxyXMin) x = this.currentFxyXMin;
//keep track of current x value
this.currentX = x;
this.logger.debug(`Interpolating x using input=${x} , currentFxyXmin=${this.currentFxyXMin}, currentFxyXMax=${this.currentFxyXMax}, normMin=${this.normMin}, normMax=${this.normMax} `);
const normalizedX = this.interpolation.interpolate_lin_single_point(
x,
this.currentFxyXMin,
this.currentFxyXMax,
this.normMin,
this.normMax
);
this.logger.debug(`Calculating new Y value using ${normalizedX}`);
this.outputY = this.currentFxySplines[this.fDimension].interpolate(normalizedX);
return this.outputY;
}
set yOutput(y) {
this.outputY = y;
//by emitting this one output we dont have to use the entire class
this.emitter.emit('yOutput', this.outputY);
}
get yOutput() {
return this.outputY;
}
set inputCurveData(curve) {
try {
this.inputCurve = curve;
this.buildAllFxyCurves(curve);
} catch (error) {
this.logger.error(`Curve validation failed: ${error.message}`);
this.inputCurve = null; // Reset curve data if validation fails
}
}
get inputCurveData() {
return this.inputCurve;
}
updateCurve(curve) {
this.logger.info("Updating curve data");
// update config with new curve data merged with existing config
const newConfig = {...this.config, curve: curve};
this.config = this.configUtils.updateConfig(newConfig);
const validatedCurve = this.config.curve;
this.inputCurve = validatedCurve;
this.buildAllFxyCurves(validatedCurve);
}
constrain(value,min,max) {
return Math.min(Math.max(value, min), max);
}
buildAllFxyCurves(curve) {
let globalMinY = Infinity;
let globalMaxY = -Infinity;
for (const fKey of Object.keys(curve)) {
const f = Number(fKey);
this.xValues[f] = {
min: Math.min(...curve[f].x),
max: Math.max(...curve[f].x),
};
const fMinY = Math.min(...curve[f].y);
const fMaxY = Math.max(...curve[f].y);
if (fMinY < globalMinY) globalMinY = fMinY;
if (fMaxY > globalMaxY) globalMaxY = fMaxY;
// Normalize curves
this.normalizedCurve[f] = this.normalizeCurve(curve[f], this.normMin, this.normMax);
}
this.normalizedSplines = this.buildXySplines(this.normalizedCurve, this.interpolationType);
// Build calculated curves (same #points across all f)
for (const f of Object.keys(this.normalizedCurve)) {
this.calculatedCurve[f] = this.buildCalculatedCurve(this.normalizedSplines, f, this.calculationPoints);
}
this.fCurve = this.buildFCurve(this.calculatedCurve, this.calculationPoints);
this.fSplines = this.buildFSplines(this.fCurve, this.interpolationType);
const fKeys = Object.keys(curve).map(Number);
this.fValues.min = Math.min(...fKeys);
this.fValues.max = Math.max(...fKeys);
this.yValues.lowest = globalMinY;
this.yValues.highest = globalMaxY;
// Set initial fDimension to min
this.fDimension = this.fValues.min;
this.logger.debug(` !!! Initial fDimension set to ${this.fValues.min}`);
}
normalizeVal(val, normMin, normMax) {
return this.interpolation.interpolate_lin_single_point(val, normMin, normMax, 1, this.calculationPoints);
}
normalizeCurve(curve, normMin, normMax) {
return {
x: this.interpolation.interpolate_lin_curve_points(curve.x, normMin, normMax),
y: curve.y,
};
}
denormalizeXvals(xValues) {
// Retrieve the normalized x-array from the current Fxy curve
const normalizedX = xValues;
// Map each normalized x to its denormalized value
const denormalizedX = normalizedX.map(nx => {
return this.interpolation.interpolate_lin_single_point(
nx,
this.normMin,
this.normMax,
this.currentFxyXMin,
this.currentFxyXMax
);
});
// Return a new object with denormalized x and the original y array
return denormalizedX;
}
// interpolate input x value to denormalized x value
denormalizeX(x) {
return this.interpolation.interpolate_lin_single_point(
x,
this.normMin,
this.normMax,
this.currentFxyXMin,
this.currentFxyXMax
);
}
buildCalculatedCurve(splines, f, pointsCount) {
const cCurve = { x: [], y: [] };
for (let i = 1; i <= pointsCount; i++) {
const nx = this.interpolation.interpolate_lin_single_point(i, 1, pointsCount, this.normMin, this.normMax);
cCurve.x.push(nx);
cCurve.y.push(splines[f].interpolate(nx));
}
return cCurve;
}
buildFCurve(curve, pointsCount) {
const fCurve = {};
for (let i = 0; i < pointsCount; i++) {
fCurve[i] = { x: [], y: [] };
}
for (let i = 0; i < pointsCount; i++) {
for (const [f, val] of Object.entries(curve)) {
fCurve[i].x.push(Number(f));
fCurve[i].y.push(val.y[i]);
}
}
return fCurve;
}
buildFSplines(fCurve, type) {
const fSplines = {};
for (const i of Object.keys(fCurve)) {
fSplines[i] = this.loadSpline(fCurve[i], type);
}
return fSplines;
}
buildSingleFxyCurve(fSplines, cCurve, f, pointsCount) {
const singleCurve = { [f]: { x: [], y: [] } };
const keys = Object.keys(cCurve);
const firstKey = keys[0];
for (let i = 0; i < pointsCount; i++) {
singleCurve[f].x.push(cCurve[firstKey].x[i]);
singleCurve[f].y.push(fSplines[i].interpolate(f));
}
return singleCurve;
}
buildXySplines(curves, type) {
const xySplines = {};
for (const f of Object.keys(curves)) {
xySplines[f] = this.loadSpline(curves[f], type);
}
return xySplines;
}
loadSpline(curve, type) {
const splineObj = new Interpolation();
splineObj.load_spline(curve.x, curve.y, type);
return splineObj;
}
calculateFxyXRange(value) {
const keys = Object.keys(this.inputCurve).map(Number).sort((a, b) => a - b);
for (let i = 0; i < keys.length; i++) {
const cur = keys[i];
const next = keys[i + 1];
if (value === cur) {
this.currentFxyXMin = this.xValues[cur].min;
this.currentFxyXMax = this.xValues[cur].max;
return;
}
if (next && value > cur && value < next) {
this.currentFxyXMin = this.interpolation.interpolate_lin_single_point(
value, cur, next, this.xValues[cur].min, this.xValues[next].min
);
this.currentFxyXMax = this.interpolation.interpolate_lin_single_point(
value, cur, next, this.xValues[cur].max, this.xValues[next].max
);
return;
}
}
}
getOutput() {
return {
x: this.currentX,
y: this.yOutput,
f: this.currentF,
yOutputPosVsPeak: {
peakOnlyPercentage: this.calcRelativePositionToPeak(this.currentFxyCurve[this.fDimension], this.outputY).peakOnlyPercentage,
rangeBasedPercentage: this.calcRelativePositionToPeak(this.currentFxyCurve[this.fDimension], this.outputY).rangeBasedPercentage
},
posXyPeak: this.getPosXofYpeak(this.currentFxyCurve[this.fDimension]),
xRange: { min: this.currentFxyXMin, max: this.currentFxyXMax },
yRange: { min: this.currentFxyYMin, max: this.currentFxyYMax },
};
}
}
module.exports = Predict;
/*
// Example usage
let example =
{
0:
{
x:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
y:[5, 15, 25, 35, 45, 55, 45, 35, 25, 15],
},
100:
{
x:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
y:[50, 150, 250, 350, 450, 550, 450, 350, 250, 150],
}
}
//set curve data in config
let config = {curve:example};
var predict = new Predict(config=config);
console.log(" showing curve data");
console.log(predict.inputCurveData);
console.log(" showing config data");
console.log(predict.config);
// specify dimension f if there is no dim f then specify 0 as example 2
console.log(" showing config data");
console.log(predict.config);
console.log(`lowest y value ever seen : ${predict.yValues.lowest}`);
console.log(`higehst y value ever seen : ${predict.yValues.highest}`);
predict.fDimension = 0;
console.log(`default x : ${predict.currentX}`);
console.log(`min x : ${predict.currentFxyXMin} , max x : ${predict.currentFxyXMax} for f : ${predict.fDimension}`);
console.log(`min y : ${predict.currentFxyYMin} , max y : ${predict.currentFxyYMax} for f : ${predict.fDimension}`);
console.log(`Y prediction is= ${predict.outputY} @ f : ${predict.fDimension} `);
// specify x value to predict y
const yVal = predict.y(x=0);
console.log(`For x : ${predict.currentX} is the predicted value ${yVal} @ f : ${predict.fDimension} `);
console.log(predict.retrieveActiveCurve());
const peak = predict.getLocalPeak(predict.currentFxyCurve[predict.fDimension].y);
console.log(predict.getPosXofYpeak(predict.currentFxyCurve[predict.fDimension]));
const { peakOnlyPercentage, rangeBasedPercentage } = predict.calcRelativePositionToPeak(predict.currentFxyCurve[predict.fDimension], predict.outputY);
console.log(`Peak-only percentage: ${peakOnlyPercentage}%, Range-based percentage: ${rangeBasedPercentage}%`);
//*/