2025-06-25 17:27:32 +02:00
/ * *
* @ 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 ) ;
2026-03-11 14:56:42 +01:00
// Guard against invalid peakIndex (e.g. empty array returns -1)
if ( peakIndex < 0 || peakIndex >= curve . x . length ) {
return { yPeak : null , x : null , xProcent : null } ;
}
2025-06-25 17:27:32 +02:00
// 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 ) {
2025-11-03 15:22:51 +01:00
2025-06-25 17:27:32 +02:00
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 } % ` ) ;
//*/