From d9a1696a2f4588a0a022747e634559571977cc98 Mon Sep 17 00:00:00 2001 From: demvlad Date: Sat, 10 May 2025 20:47:36 +0300 Subject: [PATCH 01/17] Added functions to compute PSD by RPM and Throttle --- src/graph_spectrum_calc.js | 77 +++++++++++++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 2 deletions(-) diff --git a/src/graph_spectrum_calc.js b/src/graph_spectrum_calc.js index f29989aa..5cd5ec7b 100644 --- a/src/graph_spectrum_calc.js +++ b/src/graph_spectrum_calc.js @@ -218,6 +218,75 @@ GraphSpectrumCalc._dataLoadFrequencyVsX = function(vsFieldNames, minValue = Infi }; +GraphSpectrumCalc._dataLoadPowerSpectralDensityVsX = function(vsFieldNames, minValue = Infinity, maxValue = -Infinity) { + + const flightSamples = this._getFlightSamplesFreqVsX(vsFieldNames, minValue, maxValue, false); + + // We divide it into FREQ_VS_THR_CHUNK_TIME_MS FFT chunks, we calculate the average throttle + // for each chunk. We use a moving window to get more chunks available. + const fftChunkLength = Math.round(this._blackBoxRate * FREQ_VS_THR_CHUNK_TIME_MS / 1000); + const fftChunkWindow = Math.round(fftChunkLength / FREQ_VS_THR_WINDOW_DIVISOR); + + let maxNoise = 0; // Stores the maximum amplitude of the fft over all chunks + let psdLength = 0; + // Matrix where each row represents a bin of vs values, and the columns are amplitudes at frequencies + const matrixFftOutput = new Array(NUM_VS_BINS).fill(null).map(() => (new Float64Array(fftChunkLength * 2)).fill(-70)); + + const numberSamples = new Uint32Array(NUM_VS_BINS); // Number of samples in each vs value, used to average them later. + + for (let fftChunkIndex = 0; fftChunkIndex + fftChunkLength < flightSamples.samples.length; fftChunkIndex += fftChunkWindow) { + + const fftInput = flightSamples.samples.slice(fftChunkIndex, fftChunkIndex + fftChunkLength); + const psd = this._psd(fftInput, fftChunkLength, 0, 'density'); + psdLength = psd.psdOutput.length; + maxNoise = Math.max(psd.max, maxNoise); + // calculate a bin index and put the fft value in that bin for each field (e.g. eRPM[0], eRPM[1]..) sepparately + for (const vsValueArray of flightSamples.vsValues) { + // Calculate average of the VS values in the chunk + let sumVsValues = 0; + for (let indexVs = fftChunkIndex; indexVs < fftChunkIndex + fftChunkLength; indexVs++) { + sumVsValues += vsValueArray[indexVs]; + } + // Translate the average vs value to a bin index + const avgVsValue = sumVsValues / fftChunkLength; + let vsBinIndex = Math.floor(NUM_VS_BINS * (avgVsValue - flightSamples.minValue) / (flightSamples.maxValue - flightSamples.minValue)); + // ensure that avgVsValue == flightSamples.maxValue does not result in an out of bounds access + if (vsBinIndex === NUM_VS_BINS) { vsBinIndex = NUM_VS_BINS - 1; } + numberSamples[vsBinIndex]++; + + // add the output from the fft to the row given by the vs value bin index + for (let i = 0; i < psd.psdOutput.length; i++) { + matrixFftOutput[vsBinIndex][i] += psd.psdOutput[i]; + } + } + } + + // Divide the values from the fft in each row (vs value bin) by the number of samples in the bin + for (let i = 0; i < NUM_VS_BINS; i++) { + if (numberSamples[i] > 1) { + for (let j = 0; j < matrixFftOutput[i].length; j++) { + matrixFftOutput[i][j] /= numberSamples[i]; + } + } + } + + // The output data needs to be smoothed, the sampling is not perfect + // but after some tests we let the data as is, an we prefer to apply a + // blur algorithm to the heat map image + + const psdData = { + fieldIndex : this._dataBuffer.fieldIndex, + fieldName : this._dataBuffer.fieldName, + fftLength : psdLength, + fftOutput : matrixFftOutput, + maxNoise : maxNoise, + blackBoxRate : this._blackBoxRate, + vsRange : { min: flightSamples.minValue, max: flightSamples.maxValue}, + }; + + return psdData; + +}; GraphSpectrumCalc.dataLoadFrequencyVsThrottle = function() { return this._dataLoadFrequencyVsX(FIELD_THROTTLE_NAME, 0, 100); }; @@ -345,7 +414,7 @@ GraphSpectrumCalc._getVsIndexes = function(vsFieldNames) { return fieldIndexes; }; -GraphSpectrumCalc._getFlightSamplesFreqVsX = function(vsFieldNames, minValue = Infinity, maxValue = -Infinity) { +GraphSpectrumCalc._getFlightSamplesFreqVsX = function(vsFieldNames, minValue = Infinity, maxValue = -Infinity, scaled = true) { const allChunks = this._getFlightChunks(); const vsIndexes = this._getVsIndexes(vsFieldNames); @@ -356,7 +425,11 @@ GraphSpectrumCalc._getFlightSamplesFreqVsX = function(vsFieldNames, minValue = I let samplesCount = 0; for (const chunk of allChunks) { for (let frameIndex = 0; frameIndex < chunk.frames.length; frameIndex++) { - samples[samplesCount] = (this._dataBuffer.curve.lookupRaw(chunk.frames[frameIndex][this._dataBuffer.fieldIndex])); + if (scaled) { + samples[samplesCount] = (this._dataBuffer.curve.lookupRaw(chunk.frames[frameIndex][this._dataBuffer.fieldIndex])); + } else { + samples[samplesCount] = chunk.frames[frameIndex][this._dataBuffer.fieldIndex]; + } for (let i = 0; i < vsIndexes.length; i++) { let vsFieldIx = vsIndexes[i]; let value = chunk.frames[frameIndex][vsFieldIx]; From 12de34e519d82a548cdb0bc734fb280d11dd7c7b Mon Sep 17 00:00:00 2001 From: demvlad Date: Sat, 10 May 2025 20:49:10 +0300 Subject: [PATCH 02/17] Added drawing heat map for PSD values --- src/graph_spectrum_plot.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/graph_spectrum_plot.js b/src/graph_spectrum_plot.js index 2ff35470..b61dd3a7 100644 --- a/src/graph_spectrum_plot.js +++ b/src/graph_spectrum_plot.js @@ -395,7 +395,7 @@ GraphSpectrumPlot._drawPowerSpectralDensityGraph = function (canvasCtx) { canvasCtx.restore(); }; -GraphSpectrumPlot._drawFrequencyVsXGraph = function (canvasCtx) { +GraphSpectrumPlot._drawFrequencyVsXGraph = function (canvasCtx, drawPSD = false) { const PLOTTED_BLACKBOX_RATE = this._fftData.blackBoxRate / this._zoomX; const ACTUAL_MARGIN_LEFT = this._getActualMarginLeft(); @@ -407,7 +407,7 @@ GraphSpectrumPlot._drawFrequencyVsXGraph = function (canvasCtx) { canvasCtx.translate(LEFT, TOP); if (this._cachedDataCanvas == null) { - this._cachedDataCanvas = this._drawHeatMap(); + this._cachedDataCanvas = this._drawHeatMap(drawPSD); } canvasCtx.drawImage(this._cachedDataCanvas, 0, 0, WIDTH, HEIGHT); @@ -467,7 +467,7 @@ GraphSpectrumPlot._drawFrequencyVsXGraph = function (canvasCtx) { } }; -GraphSpectrumPlot._drawHeatMap = function () { +GraphSpectrumPlot._drawHeatMap = function (drawPSD = false) { const THROTTLE_VALUES_SIZE = 100; const SCALE_HEATMAP = 1.3; // Value decided after some tests to be similar to the scale of frequency graph // This value will be maximum color @@ -485,9 +485,16 @@ GraphSpectrumPlot._drawHeatMap = function () { for (let j = 0; j < 100; j++) { // Loop for frequency for (let i = 0; i < this._fftData.fftLength; i++) { - const valuePlot = Math.round( - Math.min(this._fftData.fftOutput[j][i] * fftColorScale, 100) - ); + if (drawPSD) { + const min = -40, max = 10; //limit values dBm + let valuePlot = Math.max(this._fftData.fftOutput[j][i], min); + valuePlot = Math.min(this._fftData.fftOutput[j][i], max); + valuePlot = Math.round((valuePlot - min) * 100 / (max - min)); + } else { + const valuePlot = Math.round( + Math.min(this._fftData.fftOutput[j][i] * fftColorScale, 100) + ); + } // The fillStyle is slow, but I haven't found a way to do this faster... canvasCtx.fillStyle = `hsl(360, 100%, ${valuePlot}%)`; From b9c24b78663e19737a996fb461226feac30b9801 Mon Sep 17 00:00:00 2001 From: demvlad Date: Sat, 10 May 2025 21:11:20 +0300 Subject: [PATCH 03/17] Added switch to select PSD by throttle or RPM charts --- index.html | 4 +- src/graph_spectrum.js | 8 + src/graph_spectrum_calc.js | 11 +- src/graph_spectrum_calc.js.bak | 631 +++++++++++++++++++++++++++++++++ src/graph_spectrum_plot.js | 31 +- 5 files changed, 674 insertions(+), 11 deletions(-) create mode 100644 src/graph_spectrum_calc.js.bak diff --git a/index.html b/index.html index 3f41083f..71b5d086 100644 --- a/index.html +++ b/index.html @@ -458,8 +458,10 @@

Workspace

- + + + diff --git a/src/graph_spectrum.js b/src/graph_spectrum.js index 5d07c588..af5509bd 100644 --- a/src/graph_spectrum.js +++ b/src/graph_spectrum.js @@ -112,6 +112,14 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { fftData = GraphSpectrumCalc.dataLoadFrequencyVsRpm(); break; + case SPECTRUM_TYPE.PSD_VS_THROTTLE: + fftData = GraphSpectrumCalc.dataLoadFrequencyVsThrottle(true); + break; + + case SPECTRUM_TYPE.PSD_VS_RPM: + fftData = GraphSpectrumCalc.dataLoadFrequencyVsRpm(true); + break; + case SPECTRUM_TYPE.PIDERROR_VS_SETPOINT: fftData = GraphSpectrumCalc.dataLoadPidErrorVsSetpoint(); break; diff --git a/src/graph_spectrum_calc.js b/src/graph_spectrum_calc.js index 5cd5ec7b..b8042146 100644 --- a/src/graph_spectrum_calc.js +++ b/src/graph_spectrum_calc.js @@ -287,12 +287,15 @@ GraphSpectrumCalc._dataLoadPowerSpectralDensityVsX = function(vsFieldNames, minV return psdData; }; -GraphSpectrumCalc.dataLoadFrequencyVsThrottle = function() { - return this._dataLoadFrequencyVsX(FIELD_THROTTLE_NAME, 0, 100); + +GraphSpectrumCalc.dataLoadFrequencyVsThrottle = function(drawPSD = false) { + return drawPSD ? this._dataLoadPowerSpectralDensityVsX(FIELD_THROTTLE_NAME, 0, 100) : + this._dataLoadFrequencyVsX(FIELD_THROTTLE_NAME, 0, 100); }; -GraphSpectrumCalc.dataLoadFrequencyVsRpm = function() { - const fftData = this._dataLoadFrequencyVsX(FIELD_RPM_NAMES, 0); +GraphSpectrumCalc.dataLoadFrequencyVsRpm = function(drawPSD = false) { + const fftData = drawPSD ? this._dataLoadPowerSpectralDensityVsX(FIELD_RPM_NAMES, 0) : + this._dataLoadFrequencyVsX(FIELD_RPM_NAMES, 0); fftData.vsRange.max *= 3.333 / this._motorPoles; fftData.vsRange.min *= 3.333 / this._motorPoles; return fftData; diff --git a/src/graph_spectrum_calc.js.bak b/src/graph_spectrum_calc.js.bak new file mode 100644 index 00000000..f29989aa --- /dev/null +++ b/src/graph_spectrum_calc.js.bak @@ -0,0 +1,631 @@ +import { FlightLogFieldPresenter } from "./flightlog_fields_presenter"; + +const + FIELD_THROTTLE_NAME = ['rcCommands[3]'], + FIELD_RPM_NAMES = [ + "eRPM[0]", + "eRPM[1]", + "eRPM[2]", + "eRPM[3]", + "eRPM[4]", + "eRPM[5]", + "eRPM[6]", + "eRPM[7]", + ], + FREQ_VS_THR_CHUNK_TIME_MS = 300, + FREQ_VS_THR_WINDOW_DIVISOR = 6, + MAX_ANALYSER_LENGTH = 300 * 1000 * 1000, // 5min + NUM_VS_BINS = 100, + WARNING_RATE_DIFFERENCE = 0.05, + MAX_RPM_HZ_VALUE = 800, + MAX_RPM_AXIS_GAP = 1.05; + + +export const GraphSpectrumCalc = { + _analyserTimeRange : { + in: 0, + out: MAX_ANALYSER_LENGTH, + }, + _blackBoxRate : 0, + _dataBuffer : { + fieldIndex: 0, + curve: 0, + fieldName: null, + }, + _flightLog : null, + _sysConfig : null, + _motorPoles : null, +}; + +GraphSpectrumCalc.initialize = function(flightLog, sysConfig) { + + this._flightLog = flightLog; + this._sysConfig = sysConfig; + + const gyroRate = (1000000 / this._sysConfig['looptime']).toFixed(0); + this._motorPoles = flightLog.getSysConfig()['motor_poles']; + this._blackBoxRate = gyroRate * this._sysConfig['frameIntervalPNum'] / this._sysConfig['frameIntervalPDenom']; + if (this._sysConfig.pid_process_denom != null) { + this._blackBoxRate = this._blackBoxRate / this._sysConfig.pid_process_denom; + } + this._BetaflightRate = this._blackBoxRate; + + const actualLoggedTime = this._flightLog.getActualLoggedTime(), + length = flightLog.getCurrentLogRowsCount(); + + this._actualeRate = 1e6 * length / actualLoggedTime; + if (Math.abs(this._BetaflightRate - this._actualeRate) / this._actualeRate > WARNING_RATE_DIFFERENCE) + this._blackBoxRate = Math.round(this._actualeRate); + + if (this._BetaflightRate !== this._blackBoxRate) { + $('.actual-lograte').text(this._actualeRate.toFixed(0) + "/" + this._BetaflightRate.toFixed(0)+"Hz"); + return { + actualRate: this._actualeRate, + betaflightRate: this._BetaflightRate, + }; + } else { + $('.actual-lograte').text(""); + } + + return undefined; +}; + +GraphSpectrumCalc.setInTime = function(time) { + this._analyserTimeRange.in = time; + return this._analyserTimeRange.in; +}; + +GraphSpectrumCalc.setOutTime = function(time) { + if ((time - this._analyserTimeRange.in) <= MAX_ANALYSER_LENGTH) { + this._analyserTimeRange.out = time; + } else { + this._analyserTimeRange.out = analyserTimeRange.in + MAX_ANALYSER_LENGTH; + } + return this._analyserTimeRange.out; +}; + +GraphSpectrumCalc.setDataBuffer = function(dataBuffer) { + this._dataBuffer = dataBuffer; + return undefined; +}; + +GraphSpectrumCalc.dataLoadFrequency = function() { + + const flightSamples = this._getFlightSamplesFreq(); + + if (userSettings.analyserHanning) { + this._hanningWindow(flightSamples.samples, flightSamples.count); + } + + //calculate fft + const fftOutput = this._fft(flightSamples.samples); + + // Normalize the result + const fftData = this._normalizeFft(fftOutput, flightSamples.samples.length); + + return fftData; +}; + +GraphSpectrumCalc.dataLoadPSD = function(analyserZoomY) { + const flightSamples = this._getFlightSamplesFreq(false); + + let pointsPerSegment = 512; + const multipiler = Math.floor(1 / analyserZoomY); // 0. ... 10 + if (multipiler == 0) { + pointsPerSegment = 256; + } else if (multipiler > 1) { + pointsPerSegment *= 2 ** Math.floor(multipiler / 2); + } + pointsPerSegment = Math.min(pointsPerSegment, flightSamples.samples.length); + const overlapCount = Math.floor(pointsPerSegment / 2); + + const psd = this._psd(flightSamples.samples, pointsPerSegment, overlapCount); + + const psdData = { + fieldIndex : this._dataBuffer.fieldIndex, + fieldName : this._dataBuffer.fieldName, + psdLength : psd.psdOutput.length, + psdOutput : psd.psdOutput, + blackBoxRate : this._blackBoxRate, + minimum: psd.min, + maximum: psd.max, + maxNoiseIdx: psd.maxNoiseIdx, + }; + return psdData; +}; + +GraphSpectrumCalc._dataLoadFrequencyVsX = function(vsFieldNames, minValue = Infinity, maxValue = -Infinity) { + + const flightSamples = this._getFlightSamplesFreqVsX(vsFieldNames, minValue, maxValue); + + // We divide it into FREQ_VS_THR_CHUNK_TIME_MS FFT chunks, we calculate the average throttle + // for each chunk. We use a moving window to get more chunks available. + const fftChunkLength = this._blackBoxRate * FREQ_VS_THR_CHUNK_TIME_MS / 1000; + const fftChunkWindow = Math.round(fftChunkLength / FREQ_VS_THR_WINDOW_DIVISOR); + + let maxNoise = 0; // Stores the maximum amplitude of the fft over all chunks + // Matrix where each row represents a bin of vs values, and the columns are amplitudes at frequencies + const matrixFftOutput = new Array(NUM_VS_BINS).fill(null).map(() => new Float64Array(fftChunkLength * 2)); + + const numberSamples = new Uint32Array(NUM_VS_BINS); // Number of samples in each vs value, used to average them later. + + const fft = new FFT.complex(fftChunkLength, false); + for (let fftChunkIndex = 0; fftChunkIndex + fftChunkLength < flightSamples.samples.length; fftChunkIndex += fftChunkWindow) { + + const fftInput = flightSamples.samples.slice(fftChunkIndex, fftChunkIndex + fftChunkLength); + let fftOutput = new Float64Array(fftChunkLength * 2); + + // Hanning window applied to input data + if (userSettings.analyserHanning) { + this._hanningWindow(fftInput, fftChunkLength); + } + + fft.simple(fftOutput, fftInput, 'real'); + + fftOutput = fftOutput.slice(0, fftChunkLength); + + // Use only abs values + for (let i = 0; i < fftChunkLength; i++) { + fftOutput[i] = Math.abs(fftOutput[i]); + maxNoise = Math.max(fftOutput[i], maxNoise); + } + + // calculate a bin index and put the fft value in that bin for each field (e.g. eRPM[0], eRPM[1]..) sepparately + for (const vsValueArray of flightSamples.vsValues) { + // Calculate average of the VS values in the chunk + let sumVsValues = 0; + for (let indexVs = fftChunkIndex; indexVs < fftChunkIndex + fftChunkLength; indexVs++) { + sumVsValues += vsValueArray[indexVs]; + } + // Translate the average vs value to a bin index + const avgVsValue = sumVsValues / fftChunkLength; + let vsBinIndex = Math.floor(NUM_VS_BINS * (avgVsValue - flightSamples.minValue) / (flightSamples.maxValue - flightSamples.minValue)); + // ensure that avgVsValue == flightSamples.maxValue does not result in an out of bounds access + if (vsBinIndex === NUM_VS_BINS) { vsBinIndex = NUM_VS_BINS - 1; } + numberSamples[vsBinIndex]++; + + // add the output from the fft to the row given by the vs value bin index + for (let i = 0; i < fftOutput.length; i++) { + matrixFftOutput[vsBinIndex][i] += fftOutput[i]; + } + } + } + + // Divide the values from the fft in each row (vs value bin) by the number of samples in the bin + for (let i = 0; i < NUM_VS_BINS; i++) { + if (numberSamples[i] > 1) { + for (let j = 0; j < matrixFftOutput[i].length; j++) { + matrixFftOutput[i][j] /= numberSamples[i]; + } + } + } + + // The output data needs to be smoothed, the sampling is not perfect + // but after some tests we let the data as is, an we prefer to apply a + // blur algorithm to the heat map image + + const fftData = { + fieldIndex : this._dataBuffer.fieldIndex, + fieldName : this._dataBuffer.fieldName, + fftLength : fftChunkLength, + fftOutput : matrixFftOutput, + maxNoise : maxNoise, + blackBoxRate : this._blackBoxRate, + vsRange : { min: flightSamples.minValue, max: flightSamples.maxValue}, + }; + + return fftData; + +}; + +GraphSpectrumCalc.dataLoadFrequencyVsThrottle = function() { + return this._dataLoadFrequencyVsX(FIELD_THROTTLE_NAME, 0, 100); +}; + +GraphSpectrumCalc.dataLoadFrequencyVsRpm = function() { + const fftData = this._dataLoadFrequencyVsX(FIELD_RPM_NAMES, 0); + fftData.vsRange.max *= 3.333 / this._motorPoles; + fftData.vsRange.min *= 3.333 / this._motorPoles; + return fftData; +}; + +GraphSpectrumCalc.dataLoadPidErrorVsSetpoint = function() { + + // Detect the axis + let axisIndex; + if (this._dataBuffer.fieldName.indexOf('[roll]') >= 0) { + axisIndex = 0; + } else if (this._dataBuffer.fieldName.indexOf('[pitch]') >= 0) { + axisIndex = 1; + } else if (this._dataBuffer.fieldName.indexOf('[yaw]') >= 0) { + axisIndex = 2; + } + + const flightSamples = this._getFlightSamplesPidErrorVsSetpoint(axisIndex); + + // Add the total error by absolute position + const errorBySetpoint = Array.from({length: flightSamples.maxSetpoint + 1}); + const numberOfSamplesBySetpoint = Array.from({length: flightSamples.maxSetpoint + 1}); + + // Initialize + for (let i = 0; i <= flightSamples.maxSetpoint; i++) { + errorBySetpoint[i] = 0; + numberOfSamplesBySetpoint[i] = 0; + } + + // Sum by position + for (let i = 0; i < flightSamples.count; i++) { + + const pidErrorValue = Math.abs(flightSamples.piderror[i]); + const setpointValue = Math.abs(flightSamples.setpoint[i]); + + errorBySetpoint[setpointValue] += pidErrorValue; + numberOfSamplesBySetpoint[setpointValue]++; + } + + // Calculate the media and max values + let maxErrorBySetpoint = 0; + for (let i = 0; i <= flightSamples.maxSetpoint; i++) { + if (numberOfSamplesBySetpoint[i] > 0) { + errorBySetpoint[i] = errorBySetpoint[i] / numberOfSamplesBySetpoint[i]; + if (errorBySetpoint[i] > maxErrorBySetpoint) { + maxErrorBySetpoint = errorBySetpoint[i]; + } + } else { + errorBySetpoint[i] = null; + } + } + + return { + fieldIndex : this._dataBuffer.fieldIndex, + fieldName : this._dataBuffer.fieldName, + axisName : FlightLogFieldPresenter.fieldNameToFriendly(`axisError[${axisIndex}]`), + fftOutput : errorBySetpoint, + fftMaxOutput : maxErrorBySetpoint, + }; + +}; + +GraphSpectrumCalc._getFlightChunks = function() { + + let logStart = 0; + if (this._analyserTimeRange.in) { + logStart = this._analyserTimeRange.in; + } else { + logStart = this._flightLog.getMinTime(); + } + + let logEnd = 0; + if (this._analyserTimeRange.out) { + logEnd = this._analyserTimeRange.out; + } else { + logEnd = this._flightLog.getMaxTime(); + } + + // Limit size + logEnd = (logEnd - logStart <= MAX_ANALYSER_LENGTH)? logEnd : logStart + MAX_ANALYSER_LENGTH; + + const allChunks = this._flightLog.getChunksInTimeRange(logStart, logEnd); + + return allChunks; +}; + +GraphSpectrumCalc._getFlightSamplesFreq = function(scaled = true) { + + const allChunks = this._getFlightChunks(); + + const samples = new Float64Array(MAX_ANALYSER_LENGTH / (1000 * 1000) * this._blackBoxRate); + + // Loop through all the samples in the chunks and assign them to a sample array ready to pass to the FFT. + let samplesCount = 0; + for (const chunk of allChunks) { + for (const frame of chunk.frames) { + if (scaled) { + samples[samplesCount] = this._dataBuffer.curve.lookupRaw(frame[this._dataBuffer.fieldIndex]); + } else { + samples[samplesCount] = frame[this._dataBuffer.fieldIndex]; + } + samplesCount++; + } + } + + return { + samples : samples.slice(0, samplesCount), + count : samplesCount, + }; +}; + +GraphSpectrumCalc._getVsIndexes = function(vsFieldNames) { + const fieldIndexes = []; + for (const fieldName of vsFieldNames) { + if (Object.hasOwn(this._flightLog.getMainFieldIndexes(), fieldName)) { + fieldIndexes.push(this._flightLog.getMainFieldIndexByName(fieldName)); + } + } + return fieldIndexes; +}; + +GraphSpectrumCalc._getFlightSamplesFreqVsX = function(vsFieldNames, minValue = Infinity, maxValue = -Infinity) { + + const allChunks = this._getFlightChunks(); + const vsIndexes = this._getVsIndexes(vsFieldNames); + + const samples = new Float64Array(MAX_ANALYSER_LENGTH / (1000 * 1000) * this._blackBoxRate); + const vsValues = new Array(vsIndexes.length).fill(null).map(() => new Float64Array(MAX_ANALYSER_LENGTH / (1000 * 1000) * this._blackBoxRate)); + + let samplesCount = 0; + for (const chunk of allChunks) { + for (let frameIndex = 0; frameIndex < chunk.frames.length; frameIndex++) { + samples[samplesCount] = (this._dataBuffer.curve.lookupRaw(chunk.frames[frameIndex][this._dataBuffer.fieldIndex])); + for (let i = 0; i < vsIndexes.length; i++) { + let vsFieldIx = vsIndexes[i]; + let value = chunk.frames[frameIndex][vsFieldIx]; + if (vsFieldNames == FIELD_RPM_NAMES) { + const maxRPM = MAX_RPM_HZ_VALUE * this._motorPoles / 3.333; + if (value > maxRPM) { + value = maxRPM; + } + else if (value < 0) { + value = 0; + } + } + vsValues[i][samplesCount] = value; + } + samplesCount++; + } + } + + // Calculate min max average of the VS values in the chunk what will used by spectrum data definition + const fftChunkLength = this._blackBoxRate * FREQ_VS_THR_CHUNK_TIME_MS / 1000; + const fftChunkWindow = Math.round(fftChunkLength / FREQ_VS_THR_WINDOW_DIVISOR); + for (let fftChunkIndex = 0; fftChunkIndex + fftChunkLength < samplesCount; fftChunkIndex += fftChunkWindow) { + for (const vsValueArray of vsValues) { + // Calculate average of the VS values in the chunk + let sumVsValues = 0; + for (let indexVs = fftChunkIndex; indexVs < fftChunkIndex + fftChunkLength; indexVs++) { + sumVsValues += vsValueArray[indexVs]; + } + // Find min max average of the VS values in the chunk + const avgVsValue = sumVsValues / fftChunkLength; + maxValue = Math.max(maxValue, avgVsValue); + minValue = Math.min(minValue, avgVsValue); + } + } + + maxValue *= MAX_RPM_AXIS_GAP; + + if (minValue > maxValue) { + if (minValue == Infinity) { // this should never happen + minValue = 0; + maxValue = 100; + console.log("Invalid minimum value"); + } else { + console.log("Maximum value %f smaller than minimum value %d", maxValue, minValue); + minValue = 0; + maxValue = 100; + } + } + + let slicedVsValues = []; + for (const vsValueArray of vsValues) { + slicedVsValues.push(vsValueArray.slice(0, samplesCount)); + } + + return { + samples : samples.slice(0, samplesCount), + vsValues : slicedVsValues, + count : samplesCount, + minValue : minValue, + maxValue : maxValue, + }; +}; + +GraphSpectrumCalc._getFlightSamplesPidErrorVsSetpoint = function(axisIndex) { + + const allChunks = this._getFlightChunks(); + + // Get the PID Error field + const FIELD_PIDERROR_INDEX = this._flightLog.getMainFieldIndexByName(`axisError[${axisIndex}]`); + const FIELD_SETPOINT_INDEX = this._flightLog.getMainFieldIndexByName(`setpoint[${axisIndex}]`); + + const piderror = new Int16Array(MAX_ANALYSER_LENGTH / (1000 * 1000) * this._blackBoxRate); + const setpoint = new Int16Array(MAX_ANALYSER_LENGTH / (1000 * 1000) * this._blackBoxRate); + + // Loop through all the samples in the chunks and assign them to a sample array. + let samplesCount = 0; + let maxSetpoint = 0; + for (const chunk of allChunks) { + for (const frame of chunk.frames) { + piderror[samplesCount] = frame[FIELD_PIDERROR_INDEX]; + setpoint[samplesCount] = frame[FIELD_SETPOINT_INDEX]; + if (setpoint[samplesCount] > maxSetpoint) { + maxSetpoint = setpoint[samplesCount]; + } + samplesCount++; + } + } + + return { + piderror: piderror.slice(0, samplesCount), + setpoint: setpoint.slice(0, samplesCount), + maxSetpoint, + count: samplesCount, + }; +}; + +GraphSpectrumCalc._hanningWindow = function(samples, size) { + + if (!size) { + size = samples.length; + } + + for(let i=0; i < size; i++) { + samples[i] *= 0.5 * (1-Math.cos((2*Math.PI*i)/(size - 1))); + } +}; + +GraphSpectrumCalc._fft = function(samples, type) { + + if (!type) { + type = 'real'; + } + + const fftLength = samples.length; + const fftOutput = new Float64Array(fftLength * 2); + const fft = new FFT.complex(fftLength, false); + + fft.simple(fftOutput, samples, type); + + return fftOutput; +}; + + +/** + * Makes all the values absolute and returns the index of maxFrequency found + */ +GraphSpectrumCalc._normalizeFft = function(fftOutput, fftLength) { + + if (!fftLength) { + fftLength = fftOutput.length; + } + + // Make all the values absolute, and calculate some useful values (max noise, etc.) + const maxFrequency = (this._blackBoxRate / 2.0); + const noiseLowEndIdx = 100 / maxFrequency * fftLength; + let maxNoiseIdx = 0; + let maxNoise = 0; + + for (let i = 0; i < fftLength; i++) { + fftOutput[i] = Math.abs(fftOutput[i]); + if (i > noiseLowEndIdx && fftOutput[i] > maxNoise) { + maxNoise = fftOutput[i]; + maxNoiseIdx = i; + } + } + + maxNoiseIdx = maxNoiseIdx / fftLength * maxFrequency; + + const fftData = { + fieldIndex : this._dataBuffer.fieldIndex, + fieldName : this._dataBuffer.fieldName, + fftLength : fftLength, + fftOutput : fftOutput, + maxNoiseIdx : maxNoiseIdx, + blackBoxRate : this._blackBoxRate, + }; + + return fftData; +}; + +/** + * Compute PSD for data samples by Welch method follow Python code + */ +GraphSpectrumCalc._psd = function(samples, pointsPerSegment, overlapCount, scaling = 'density') { +// Compute FFT for samples segments + const fftOutput = this._fft_segmented(samples, pointsPerSegment, overlapCount); + + const dataCount = fftOutput[0].length; + const segmentsCount = fftOutput.length; + const psdOutput = new Float64Array(dataCount); + +// Compute power scale coef + let scale = 1; + if (userSettings.analyserHanning) { + const window = Array(pointsPerSegment).fill(1); + this._hanningWindow(window, pointsPerSegment); + if (scaling == 'density') { + let skSum = 0; + for (const value of window) { + skSum += value ** 2; + } + scale = 1 / (this._blackBoxRate * skSum); + } else if (scaling == 'spectrum') { + let sum = 0; + for (const value of window) { + sum += value; + } + scale = 1 / sum ** 2; + } + } else if (scaling == 'density') { + scale = 1 / pointsPerSegment; + } else if (scaling == 'spectrum') { + scale = 1 / pointsPerSegment ** 2; + } + +// Compute average for scaled power + let min = 1e6, + max = -1e6; + // Early exit if no segments were processed + if (segmentsCount === 0) { + return { + psdOutput: new Float64Array(0), + min: 0, + max: 0, + maxNoiseIdx: 0, + }; + } + const maxFrequency = (this._blackBoxRate / 2.0); + const noise50HzIdx = 50 / maxFrequency * dataCount; + const noise3HzIdx = 3 / maxFrequency * dataCount; + let maxNoiseIdx = 0; + let maxNoise = -100; + for (let i = 0; i < dataCount; i++) { + psdOutput[i] = 0.0; + for (let j = 0; j < segmentsCount; j++) { + let p = scale * fftOutput[j][i] ** 2; + if (i != dataCount - 1) { + p *= 2; + } + psdOutput[i] += p; + } + + const min_avg = 1e-7; // limit min value for -70db + let avg = psdOutput[i] / segmentsCount; + avg = Math.max(avg, min_avg); + psdOutput[i] = 10 * Math.log10(avg); + if (i > noise3HzIdx) { // Miss big zero freq magnitude + min = Math.min(psdOutput[i], min); + max = Math.max(psdOutput[i], max); + } + if (i > noise50HzIdx && psdOutput[i] > maxNoise) { + maxNoise = psdOutput[i]; + maxNoiseIdx = i; + } + } + + const maxNoiseFrequency = maxNoiseIdx / dataCount * maxFrequency; + + return { + psdOutput: psdOutput, + min: min, + max: max, + maxNoiseIdx: maxNoiseFrequency, + }; +}; + + +/** + * Compute FFT for samples segments by lenghts as pointsPerSegment with overlapCount overlap points count + */ +GraphSpectrumCalc._fft_segmented = function(samples, pointsPerSegment, overlapCount) { + const samplesCount = samples.length; + let output = []; + for (let i = 0; i <= samplesCount - pointsPerSegment; i += pointsPerSegment - overlapCount) { + const fftInput = samples.slice(i, i + pointsPerSegment); + + if (userSettings.analyserHanning) { + this._hanningWindow(fftInput, pointsPerSegment); + } + + const fftComplex = this._fft(fftInput); + const magnitudes = new Float64Array(pointsPerSegment / 2); + for (let i = 0; i < pointsPerSegment / 2; i++) { + const re = fftComplex[2 * i]; + const im = fftComplex[2 * i + 1]; + magnitudes[i] = Math.hypot(re, im); + } + output.push(magnitudes); + } + + return output; +}; diff --git a/src/graph_spectrum_plot.js b/src/graph_spectrum_plot.js index b61dd3a7..e59068e8 100644 --- a/src/graph_spectrum_plot.js +++ b/src/graph_spectrum_plot.js @@ -18,6 +18,8 @@ export const SPECTRUM_TYPE = { PIDERROR_VS_SETPOINT: 2, FREQ_VS_RPM: 3, POWER_SPECTRAL_DENSITY: 4, + PSD_VS_THROTTLE: 5, + PSD_VS_RPM: 6, }; export const SPECTRUM_OVERDRAW_TYPE = { @@ -169,6 +171,14 @@ GraphSpectrumPlot._drawGraph = function (canvasCtx) { this._drawFrequencyVsXGraph(canvasCtx); break; + case SPECTRUM_TYPE.PSD_VS_THROTTLE: + this._drawFrequencyVsXGraph(canvasCtx, true); + break; + + case SPECTRUM_TYPE.PSD_VS_RPM: + this._drawFrequencyVsXGraph(canvasCtx, true); + break; + case SPECTRUM_TYPE.PIDERROR_VS_SETPOINT: this._drawPidErrorVsSetpointGraph(canvasCtx); break; @@ -441,8 +451,9 @@ GraphSpectrumPlot._drawFrequencyVsXGraph = function (canvasCtx, drawPSD = false) MARGIN_BOTTOM, "Hz" ); - - if (this._spectrumType === SPECTRUM_TYPE.FREQ_VS_THROTTLE) { + + if (this._spectrumType === SPECTRUM_TYPE.FREQ_VS_THROTTLE || + this._spectrumType === SPECTRUM_TYPE.PSD_VS_THROTTLE) { this._drawVerticalGridLines( canvasCtx, LEFT, @@ -453,7 +464,8 @@ GraphSpectrumPlot._drawFrequencyVsXGraph = function (canvasCtx, drawPSD = false) this._fftData.vsRange.max, "%" ); - } else if (this._spectrumType === SPECTRUM_TYPE.FREQ_VS_RPM) { + } else if (this._spectrumType === SPECTRUM_TYPE.FREQ_VS_RPM || + this._spectrumType === SPECTRUM_TYPE.PSD_VS_RPM) { this._drawVerticalGridLines( canvasCtx, LEFT, @@ -485,13 +497,14 @@ GraphSpectrumPlot._drawHeatMap = function (drawPSD = false) { for (let j = 0; j < 100; j++) { // Loop for frequency for (let i = 0; i < this._fftData.fftLength; i++) { + let valuePlot; if (drawPSD) { const min = -40, max = 10; //limit values dBm - let valuePlot = Math.max(this._fftData.fftOutput[j][i], min); + valuePlot = Math.max(this._fftData.fftOutput[j][i], min); valuePlot = Math.min(this._fftData.fftOutput[j][i], max); valuePlot = Math.round((valuePlot - min) * 100 / (max - min)); } else { - const valuePlot = Math.round( + valuePlot = Math.round( Math.min(this._fftData.fftOutput[j][i] * fftColorScale, 100) ); } @@ -1454,7 +1467,9 @@ GraphSpectrumPlot._drawMousePosition = function ( this._spectrumType === SPECTRUM_TYPE.FREQUENCY || this._spectrumType === SPECTRUM_TYPE.FREQ_VS_THROTTLE || this._spectrumType === SPECTRUM_TYPE.FREQ_VS_RPM || - this._spectrumType === SPECTRUM_TYPE.POWER_SPECTRAL_DENSITY + this._spectrumType === SPECTRUM_TYPE.POWER_SPECTRAL_DENSITY || + this._spectrumType === SPECTRUM_TYPE.PSD_VS_THROTTLE || + this._spectrumType === SPECTRUM_TYPE.PSD_VS_RPM ) { // Calculate frequency at mouse const sampleRate = this._fftData.blackBoxRate / this._zoomX; @@ -1481,9 +1496,11 @@ GraphSpectrumPlot._drawMousePosition = function ( let unitLabel; switch (this._spectrumType) { case SPECTRUM_TYPE.FREQ_VS_THROTTLE: + case SPECTRUM_TYPE.PSD_VS_THROTTLE: unitLabel = "%"; break; case SPECTRUM_TYPE.FREQ_VS_RPM: + case SPECTRUM_TYPE.PSD_VS_RPM: unitLabel = "Hz"; break; case SPECTRUM_TYPE.POWER_SPECTRAL_DENSITY: @@ -1564,6 +1581,8 @@ GraphSpectrumPlot._getActualMarginLeft = function () { switch (this._spectrumType) { case SPECTRUM_TYPE.FREQ_VS_THROTTLE: case SPECTRUM_TYPE.FREQ_VS_RPM: + case SPECTRUM_TYPE.PSD_VS_THROTTLE: + case SPECTRUM_TYPE.PSD_VS_RPM: case SPECTRUM_TYPE.POWER_SPECTRAL_DENSITY: actualMarginLeft = this._isFullScreen ? MARGIN_LEFT_FULLSCREEN From a8bfb8c1eaf8a615c49ec60a1b0bb374a22a331d Mon Sep 17 00:00:00 2001 From: demvlad Date: Sat, 10 May 2025 22:04:00 +0300 Subject: [PATCH 04/17] Resolved issue of limit drawing psd value --- src/graph_spectrum_calc.js.bak | 631 --------------------------------- src/graph_spectrum_plot.js | 2 +- 2 files changed, 1 insertion(+), 632 deletions(-) delete mode 100644 src/graph_spectrum_calc.js.bak diff --git a/src/graph_spectrum_calc.js.bak b/src/graph_spectrum_calc.js.bak deleted file mode 100644 index f29989aa..00000000 --- a/src/graph_spectrum_calc.js.bak +++ /dev/null @@ -1,631 +0,0 @@ -import { FlightLogFieldPresenter } from "./flightlog_fields_presenter"; - -const - FIELD_THROTTLE_NAME = ['rcCommands[3]'], - FIELD_RPM_NAMES = [ - "eRPM[0]", - "eRPM[1]", - "eRPM[2]", - "eRPM[3]", - "eRPM[4]", - "eRPM[5]", - "eRPM[6]", - "eRPM[7]", - ], - FREQ_VS_THR_CHUNK_TIME_MS = 300, - FREQ_VS_THR_WINDOW_DIVISOR = 6, - MAX_ANALYSER_LENGTH = 300 * 1000 * 1000, // 5min - NUM_VS_BINS = 100, - WARNING_RATE_DIFFERENCE = 0.05, - MAX_RPM_HZ_VALUE = 800, - MAX_RPM_AXIS_GAP = 1.05; - - -export const GraphSpectrumCalc = { - _analyserTimeRange : { - in: 0, - out: MAX_ANALYSER_LENGTH, - }, - _blackBoxRate : 0, - _dataBuffer : { - fieldIndex: 0, - curve: 0, - fieldName: null, - }, - _flightLog : null, - _sysConfig : null, - _motorPoles : null, -}; - -GraphSpectrumCalc.initialize = function(flightLog, sysConfig) { - - this._flightLog = flightLog; - this._sysConfig = sysConfig; - - const gyroRate = (1000000 / this._sysConfig['looptime']).toFixed(0); - this._motorPoles = flightLog.getSysConfig()['motor_poles']; - this._blackBoxRate = gyroRate * this._sysConfig['frameIntervalPNum'] / this._sysConfig['frameIntervalPDenom']; - if (this._sysConfig.pid_process_denom != null) { - this._blackBoxRate = this._blackBoxRate / this._sysConfig.pid_process_denom; - } - this._BetaflightRate = this._blackBoxRate; - - const actualLoggedTime = this._flightLog.getActualLoggedTime(), - length = flightLog.getCurrentLogRowsCount(); - - this._actualeRate = 1e6 * length / actualLoggedTime; - if (Math.abs(this._BetaflightRate - this._actualeRate) / this._actualeRate > WARNING_RATE_DIFFERENCE) - this._blackBoxRate = Math.round(this._actualeRate); - - if (this._BetaflightRate !== this._blackBoxRate) { - $('.actual-lograte').text(this._actualeRate.toFixed(0) + "/" + this._BetaflightRate.toFixed(0)+"Hz"); - return { - actualRate: this._actualeRate, - betaflightRate: this._BetaflightRate, - }; - } else { - $('.actual-lograte').text(""); - } - - return undefined; -}; - -GraphSpectrumCalc.setInTime = function(time) { - this._analyserTimeRange.in = time; - return this._analyserTimeRange.in; -}; - -GraphSpectrumCalc.setOutTime = function(time) { - if ((time - this._analyserTimeRange.in) <= MAX_ANALYSER_LENGTH) { - this._analyserTimeRange.out = time; - } else { - this._analyserTimeRange.out = analyserTimeRange.in + MAX_ANALYSER_LENGTH; - } - return this._analyserTimeRange.out; -}; - -GraphSpectrumCalc.setDataBuffer = function(dataBuffer) { - this._dataBuffer = dataBuffer; - return undefined; -}; - -GraphSpectrumCalc.dataLoadFrequency = function() { - - const flightSamples = this._getFlightSamplesFreq(); - - if (userSettings.analyserHanning) { - this._hanningWindow(flightSamples.samples, flightSamples.count); - } - - //calculate fft - const fftOutput = this._fft(flightSamples.samples); - - // Normalize the result - const fftData = this._normalizeFft(fftOutput, flightSamples.samples.length); - - return fftData; -}; - -GraphSpectrumCalc.dataLoadPSD = function(analyserZoomY) { - const flightSamples = this._getFlightSamplesFreq(false); - - let pointsPerSegment = 512; - const multipiler = Math.floor(1 / analyserZoomY); // 0. ... 10 - if (multipiler == 0) { - pointsPerSegment = 256; - } else if (multipiler > 1) { - pointsPerSegment *= 2 ** Math.floor(multipiler / 2); - } - pointsPerSegment = Math.min(pointsPerSegment, flightSamples.samples.length); - const overlapCount = Math.floor(pointsPerSegment / 2); - - const psd = this._psd(flightSamples.samples, pointsPerSegment, overlapCount); - - const psdData = { - fieldIndex : this._dataBuffer.fieldIndex, - fieldName : this._dataBuffer.fieldName, - psdLength : psd.psdOutput.length, - psdOutput : psd.psdOutput, - blackBoxRate : this._blackBoxRate, - minimum: psd.min, - maximum: psd.max, - maxNoiseIdx: psd.maxNoiseIdx, - }; - return psdData; -}; - -GraphSpectrumCalc._dataLoadFrequencyVsX = function(vsFieldNames, minValue = Infinity, maxValue = -Infinity) { - - const flightSamples = this._getFlightSamplesFreqVsX(vsFieldNames, minValue, maxValue); - - // We divide it into FREQ_VS_THR_CHUNK_TIME_MS FFT chunks, we calculate the average throttle - // for each chunk. We use a moving window to get more chunks available. - const fftChunkLength = this._blackBoxRate * FREQ_VS_THR_CHUNK_TIME_MS / 1000; - const fftChunkWindow = Math.round(fftChunkLength / FREQ_VS_THR_WINDOW_DIVISOR); - - let maxNoise = 0; // Stores the maximum amplitude of the fft over all chunks - // Matrix where each row represents a bin of vs values, and the columns are amplitudes at frequencies - const matrixFftOutput = new Array(NUM_VS_BINS).fill(null).map(() => new Float64Array(fftChunkLength * 2)); - - const numberSamples = new Uint32Array(NUM_VS_BINS); // Number of samples in each vs value, used to average them later. - - const fft = new FFT.complex(fftChunkLength, false); - for (let fftChunkIndex = 0; fftChunkIndex + fftChunkLength < flightSamples.samples.length; fftChunkIndex += fftChunkWindow) { - - const fftInput = flightSamples.samples.slice(fftChunkIndex, fftChunkIndex + fftChunkLength); - let fftOutput = new Float64Array(fftChunkLength * 2); - - // Hanning window applied to input data - if (userSettings.analyserHanning) { - this._hanningWindow(fftInput, fftChunkLength); - } - - fft.simple(fftOutput, fftInput, 'real'); - - fftOutput = fftOutput.slice(0, fftChunkLength); - - // Use only abs values - for (let i = 0; i < fftChunkLength; i++) { - fftOutput[i] = Math.abs(fftOutput[i]); - maxNoise = Math.max(fftOutput[i], maxNoise); - } - - // calculate a bin index and put the fft value in that bin for each field (e.g. eRPM[0], eRPM[1]..) sepparately - for (const vsValueArray of flightSamples.vsValues) { - // Calculate average of the VS values in the chunk - let sumVsValues = 0; - for (let indexVs = fftChunkIndex; indexVs < fftChunkIndex + fftChunkLength; indexVs++) { - sumVsValues += vsValueArray[indexVs]; - } - // Translate the average vs value to a bin index - const avgVsValue = sumVsValues / fftChunkLength; - let vsBinIndex = Math.floor(NUM_VS_BINS * (avgVsValue - flightSamples.minValue) / (flightSamples.maxValue - flightSamples.minValue)); - // ensure that avgVsValue == flightSamples.maxValue does not result in an out of bounds access - if (vsBinIndex === NUM_VS_BINS) { vsBinIndex = NUM_VS_BINS - 1; } - numberSamples[vsBinIndex]++; - - // add the output from the fft to the row given by the vs value bin index - for (let i = 0; i < fftOutput.length; i++) { - matrixFftOutput[vsBinIndex][i] += fftOutput[i]; - } - } - } - - // Divide the values from the fft in each row (vs value bin) by the number of samples in the bin - for (let i = 0; i < NUM_VS_BINS; i++) { - if (numberSamples[i] > 1) { - for (let j = 0; j < matrixFftOutput[i].length; j++) { - matrixFftOutput[i][j] /= numberSamples[i]; - } - } - } - - // The output data needs to be smoothed, the sampling is not perfect - // but after some tests we let the data as is, an we prefer to apply a - // blur algorithm to the heat map image - - const fftData = { - fieldIndex : this._dataBuffer.fieldIndex, - fieldName : this._dataBuffer.fieldName, - fftLength : fftChunkLength, - fftOutput : matrixFftOutput, - maxNoise : maxNoise, - blackBoxRate : this._blackBoxRate, - vsRange : { min: flightSamples.minValue, max: flightSamples.maxValue}, - }; - - return fftData; - -}; - -GraphSpectrumCalc.dataLoadFrequencyVsThrottle = function() { - return this._dataLoadFrequencyVsX(FIELD_THROTTLE_NAME, 0, 100); -}; - -GraphSpectrumCalc.dataLoadFrequencyVsRpm = function() { - const fftData = this._dataLoadFrequencyVsX(FIELD_RPM_NAMES, 0); - fftData.vsRange.max *= 3.333 / this._motorPoles; - fftData.vsRange.min *= 3.333 / this._motorPoles; - return fftData; -}; - -GraphSpectrumCalc.dataLoadPidErrorVsSetpoint = function() { - - // Detect the axis - let axisIndex; - if (this._dataBuffer.fieldName.indexOf('[roll]') >= 0) { - axisIndex = 0; - } else if (this._dataBuffer.fieldName.indexOf('[pitch]') >= 0) { - axisIndex = 1; - } else if (this._dataBuffer.fieldName.indexOf('[yaw]') >= 0) { - axisIndex = 2; - } - - const flightSamples = this._getFlightSamplesPidErrorVsSetpoint(axisIndex); - - // Add the total error by absolute position - const errorBySetpoint = Array.from({length: flightSamples.maxSetpoint + 1}); - const numberOfSamplesBySetpoint = Array.from({length: flightSamples.maxSetpoint + 1}); - - // Initialize - for (let i = 0; i <= flightSamples.maxSetpoint; i++) { - errorBySetpoint[i] = 0; - numberOfSamplesBySetpoint[i] = 0; - } - - // Sum by position - for (let i = 0; i < flightSamples.count; i++) { - - const pidErrorValue = Math.abs(flightSamples.piderror[i]); - const setpointValue = Math.abs(flightSamples.setpoint[i]); - - errorBySetpoint[setpointValue] += pidErrorValue; - numberOfSamplesBySetpoint[setpointValue]++; - } - - // Calculate the media and max values - let maxErrorBySetpoint = 0; - for (let i = 0; i <= flightSamples.maxSetpoint; i++) { - if (numberOfSamplesBySetpoint[i] > 0) { - errorBySetpoint[i] = errorBySetpoint[i] / numberOfSamplesBySetpoint[i]; - if (errorBySetpoint[i] > maxErrorBySetpoint) { - maxErrorBySetpoint = errorBySetpoint[i]; - } - } else { - errorBySetpoint[i] = null; - } - } - - return { - fieldIndex : this._dataBuffer.fieldIndex, - fieldName : this._dataBuffer.fieldName, - axisName : FlightLogFieldPresenter.fieldNameToFriendly(`axisError[${axisIndex}]`), - fftOutput : errorBySetpoint, - fftMaxOutput : maxErrorBySetpoint, - }; - -}; - -GraphSpectrumCalc._getFlightChunks = function() { - - let logStart = 0; - if (this._analyserTimeRange.in) { - logStart = this._analyserTimeRange.in; - } else { - logStart = this._flightLog.getMinTime(); - } - - let logEnd = 0; - if (this._analyserTimeRange.out) { - logEnd = this._analyserTimeRange.out; - } else { - logEnd = this._flightLog.getMaxTime(); - } - - // Limit size - logEnd = (logEnd - logStart <= MAX_ANALYSER_LENGTH)? logEnd : logStart + MAX_ANALYSER_LENGTH; - - const allChunks = this._flightLog.getChunksInTimeRange(logStart, logEnd); - - return allChunks; -}; - -GraphSpectrumCalc._getFlightSamplesFreq = function(scaled = true) { - - const allChunks = this._getFlightChunks(); - - const samples = new Float64Array(MAX_ANALYSER_LENGTH / (1000 * 1000) * this._blackBoxRate); - - // Loop through all the samples in the chunks and assign them to a sample array ready to pass to the FFT. - let samplesCount = 0; - for (const chunk of allChunks) { - for (const frame of chunk.frames) { - if (scaled) { - samples[samplesCount] = this._dataBuffer.curve.lookupRaw(frame[this._dataBuffer.fieldIndex]); - } else { - samples[samplesCount] = frame[this._dataBuffer.fieldIndex]; - } - samplesCount++; - } - } - - return { - samples : samples.slice(0, samplesCount), - count : samplesCount, - }; -}; - -GraphSpectrumCalc._getVsIndexes = function(vsFieldNames) { - const fieldIndexes = []; - for (const fieldName of vsFieldNames) { - if (Object.hasOwn(this._flightLog.getMainFieldIndexes(), fieldName)) { - fieldIndexes.push(this._flightLog.getMainFieldIndexByName(fieldName)); - } - } - return fieldIndexes; -}; - -GraphSpectrumCalc._getFlightSamplesFreqVsX = function(vsFieldNames, minValue = Infinity, maxValue = -Infinity) { - - const allChunks = this._getFlightChunks(); - const vsIndexes = this._getVsIndexes(vsFieldNames); - - const samples = new Float64Array(MAX_ANALYSER_LENGTH / (1000 * 1000) * this._blackBoxRate); - const vsValues = new Array(vsIndexes.length).fill(null).map(() => new Float64Array(MAX_ANALYSER_LENGTH / (1000 * 1000) * this._blackBoxRate)); - - let samplesCount = 0; - for (const chunk of allChunks) { - for (let frameIndex = 0; frameIndex < chunk.frames.length; frameIndex++) { - samples[samplesCount] = (this._dataBuffer.curve.lookupRaw(chunk.frames[frameIndex][this._dataBuffer.fieldIndex])); - for (let i = 0; i < vsIndexes.length; i++) { - let vsFieldIx = vsIndexes[i]; - let value = chunk.frames[frameIndex][vsFieldIx]; - if (vsFieldNames == FIELD_RPM_NAMES) { - const maxRPM = MAX_RPM_HZ_VALUE * this._motorPoles / 3.333; - if (value > maxRPM) { - value = maxRPM; - } - else if (value < 0) { - value = 0; - } - } - vsValues[i][samplesCount] = value; - } - samplesCount++; - } - } - - // Calculate min max average of the VS values in the chunk what will used by spectrum data definition - const fftChunkLength = this._blackBoxRate * FREQ_VS_THR_CHUNK_TIME_MS / 1000; - const fftChunkWindow = Math.round(fftChunkLength / FREQ_VS_THR_WINDOW_DIVISOR); - for (let fftChunkIndex = 0; fftChunkIndex + fftChunkLength < samplesCount; fftChunkIndex += fftChunkWindow) { - for (const vsValueArray of vsValues) { - // Calculate average of the VS values in the chunk - let sumVsValues = 0; - for (let indexVs = fftChunkIndex; indexVs < fftChunkIndex + fftChunkLength; indexVs++) { - sumVsValues += vsValueArray[indexVs]; - } - // Find min max average of the VS values in the chunk - const avgVsValue = sumVsValues / fftChunkLength; - maxValue = Math.max(maxValue, avgVsValue); - minValue = Math.min(minValue, avgVsValue); - } - } - - maxValue *= MAX_RPM_AXIS_GAP; - - if (minValue > maxValue) { - if (minValue == Infinity) { // this should never happen - minValue = 0; - maxValue = 100; - console.log("Invalid minimum value"); - } else { - console.log("Maximum value %f smaller than minimum value %d", maxValue, minValue); - minValue = 0; - maxValue = 100; - } - } - - let slicedVsValues = []; - for (const vsValueArray of vsValues) { - slicedVsValues.push(vsValueArray.slice(0, samplesCount)); - } - - return { - samples : samples.slice(0, samplesCount), - vsValues : slicedVsValues, - count : samplesCount, - minValue : minValue, - maxValue : maxValue, - }; -}; - -GraphSpectrumCalc._getFlightSamplesPidErrorVsSetpoint = function(axisIndex) { - - const allChunks = this._getFlightChunks(); - - // Get the PID Error field - const FIELD_PIDERROR_INDEX = this._flightLog.getMainFieldIndexByName(`axisError[${axisIndex}]`); - const FIELD_SETPOINT_INDEX = this._flightLog.getMainFieldIndexByName(`setpoint[${axisIndex}]`); - - const piderror = new Int16Array(MAX_ANALYSER_LENGTH / (1000 * 1000) * this._blackBoxRate); - const setpoint = new Int16Array(MAX_ANALYSER_LENGTH / (1000 * 1000) * this._blackBoxRate); - - // Loop through all the samples in the chunks and assign them to a sample array. - let samplesCount = 0; - let maxSetpoint = 0; - for (const chunk of allChunks) { - for (const frame of chunk.frames) { - piderror[samplesCount] = frame[FIELD_PIDERROR_INDEX]; - setpoint[samplesCount] = frame[FIELD_SETPOINT_INDEX]; - if (setpoint[samplesCount] > maxSetpoint) { - maxSetpoint = setpoint[samplesCount]; - } - samplesCount++; - } - } - - return { - piderror: piderror.slice(0, samplesCount), - setpoint: setpoint.slice(0, samplesCount), - maxSetpoint, - count: samplesCount, - }; -}; - -GraphSpectrumCalc._hanningWindow = function(samples, size) { - - if (!size) { - size = samples.length; - } - - for(let i=0; i < size; i++) { - samples[i] *= 0.5 * (1-Math.cos((2*Math.PI*i)/(size - 1))); - } -}; - -GraphSpectrumCalc._fft = function(samples, type) { - - if (!type) { - type = 'real'; - } - - const fftLength = samples.length; - const fftOutput = new Float64Array(fftLength * 2); - const fft = new FFT.complex(fftLength, false); - - fft.simple(fftOutput, samples, type); - - return fftOutput; -}; - - -/** - * Makes all the values absolute and returns the index of maxFrequency found - */ -GraphSpectrumCalc._normalizeFft = function(fftOutput, fftLength) { - - if (!fftLength) { - fftLength = fftOutput.length; - } - - // Make all the values absolute, and calculate some useful values (max noise, etc.) - const maxFrequency = (this._blackBoxRate / 2.0); - const noiseLowEndIdx = 100 / maxFrequency * fftLength; - let maxNoiseIdx = 0; - let maxNoise = 0; - - for (let i = 0; i < fftLength; i++) { - fftOutput[i] = Math.abs(fftOutput[i]); - if (i > noiseLowEndIdx && fftOutput[i] > maxNoise) { - maxNoise = fftOutput[i]; - maxNoiseIdx = i; - } - } - - maxNoiseIdx = maxNoiseIdx / fftLength * maxFrequency; - - const fftData = { - fieldIndex : this._dataBuffer.fieldIndex, - fieldName : this._dataBuffer.fieldName, - fftLength : fftLength, - fftOutput : fftOutput, - maxNoiseIdx : maxNoiseIdx, - blackBoxRate : this._blackBoxRate, - }; - - return fftData; -}; - -/** - * Compute PSD for data samples by Welch method follow Python code - */ -GraphSpectrumCalc._psd = function(samples, pointsPerSegment, overlapCount, scaling = 'density') { -// Compute FFT for samples segments - const fftOutput = this._fft_segmented(samples, pointsPerSegment, overlapCount); - - const dataCount = fftOutput[0].length; - const segmentsCount = fftOutput.length; - const psdOutput = new Float64Array(dataCount); - -// Compute power scale coef - let scale = 1; - if (userSettings.analyserHanning) { - const window = Array(pointsPerSegment).fill(1); - this._hanningWindow(window, pointsPerSegment); - if (scaling == 'density') { - let skSum = 0; - for (const value of window) { - skSum += value ** 2; - } - scale = 1 / (this._blackBoxRate * skSum); - } else if (scaling == 'spectrum') { - let sum = 0; - for (const value of window) { - sum += value; - } - scale = 1 / sum ** 2; - } - } else if (scaling == 'density') { - scale = 1 / pointsPerSegment; - } else if (scaling == 'spectrum') { - scale = 1 / pointsPerSegment ** 2; - } - -// Compute average for scaled power - let min = 1e6, - max = -1e6; - // Early exit if no segments were processed - if (segmentsCount === 0) { - return { - psdOutput: new Float64Array(0), - min: 0, - max: 0, - maxNoiseIdx: 0, - }; - } - const maxFrequency = (this._blackBoxRate / 2.0); - const noise50HzIdx = 50 / maxFrequency * dataCount; - const noise3HzIdx = 3 / maxFrequency * dataCount; - let maxNoiseIdx = 0; - let maxNoise = -100; - for (let i = 0; i < dataCount; i++) { - psdOutput[i] = 0.0; - for (let j = 0; j < segmentsCount; j++) { - let p = scale * fftOutput[j][i] ** 2; - if (i != dataCount - 1) { - p *= 2; - } - psdOutput[i] += p; - } - - const min_avg = 1e-7; // limit min value for -70db - let avg = psdOutput[i] / segmentsCount; - avg = Math.max(avg, min_avg); - psdOutput[i] = 10 * Math.log10(avg); - if (i > noise3HzIdx) { // Miss big zero freq magnitude - min = Math.min(psdOutput[i], min); - max = Math.max(psdOutput[i], max); - } - if (i > noise50HzIdx && psdOutput[i] > maxNoise) { - maxNoise = psdOutput[i]; - maxNoiseIdx = i; - } - } - - const maxNoiseFrequency = maxNoiseIdx / dataCount * maxFrequency; - - return { - psdOutput: psdOutput, - min: min, - max: max, - maxNoiseIdx: maxNoiseFrequency, - }; -}; - - -/** - * Compute FFT for samples segments by lenghts as pointsPerSegment with overlapCount overlap points count - */ -GraphSpectrumCalc._fft_segmented = function(samples, pointsPerSegment, overlapCount) { - const samplesCount = samples.length; - let output = []; - for (let i = 0; i <= samplesCount - pointsPerSegment; i += pointsPerSegment - overlapCount) { - const fftInput = samples.slice(i, i + pointsPerSegment); - - if (userSettings.analyserHanning) { - this._hanningWindow(fftInput, pointsPerSegment); - } - - const fftComplex = this._fft(fftInput); - const magnitudes = new Float64Array(pointsPerSegment / 2); - for (let i = 0; i < pointsPerSegment / 2; i++) { - const re = fftComplex[2 * i]; - const im = fftComplex[2 * i + 1]; - magnitudes[i] = Math.hypot(re, im); - } - output.push(magnitudes); - } - - return output; -}; diff --git a/src/graph_spectrum_plot.js b/src/graph_spectrum_plot.js index e59068e8..643fb7e2 100644 --- a/src/graph_spectrum_plot.js +++ b/src/graph_spectrum_plot.js @@ -501,7 +501,7 @@ GraphSpectrumPlot._drawHeatMap = function (drawPSD = false) { if (drawPSD) { const min = -40, max = 10; //limit values dBm valuePlot = Math.max(this._fftData.fftOutput[j][i], min); - valuePlot = Math.min(this._fftData.fftOutput[j][i], max); + valuePlot = Math.min(valuePlot, max); valuePlot = Math.round((valuePlot - min) * 100 / (max - min)); } else { valuePlot = Math.round( From ebef462bf1228b597f2a0ead21da261bd3a3c9c4 Mon Sep 17 00:00:00 2001 From: demvlad Date: Sat, 10 May 2025 22:17:47 +0300 Subject: [PATCH 05/17] Code style improvement --- src/graph_spectrum_calc.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/graph_spectrum_calc.js b/src/graph_spectrum_calc.js index b8042146..fa793f92 100644 --- a/src/graph_spectrum_calc.js +++ b/src/graph_spectrum_calc.js @@ -110,11 +110,11 @@ GraphSpectrumCalc.dataLoadPSD = function(analyserZoomY) { const flightSamples = this._getFlightSamplesFreq(false); let pointsPerSegment = 512; - const multipiler = Math.floor(1 / analyserZoomY); // 0. ... 10 - if (multipiler == 0) { + const multiplier = Math.floor(1 / analyserZoomY); // 0. ... 10 + if (multiplier == 0) { pointsPerSegment = 256; - } else if (multipiler > 1) { - pointsPerSegment *= 2 ** Math.floor(multipiler / 2); + } else if (multiplier > 1) { + pointsPerSegment *= 2 ** Math.floor(multiplier / 2); } pointsPerSegment = Math.min(pointsPerSegment, flightSamples.samples.length); const overlapCount = Math.floor(pointsPerSegment / 2); From 0c282ba1a8b3d4bdef86c8f8632dab969580cc4a Mon Sep 17 00:00:00 2001 From: demvlad Date: Sat, 10 May 2025 22:30:13 +0300 Subject: [PATCH 06/17] Code refactoring in src/graph_spectrum_calc.js --- src/graph_spectrum_calc.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/graph_spectrum_calc.js b/src/graph_spectrum_calc.js index fa793f92..a014c75e 100644 --- a/src/graph_spectrum_calc.js +++ b/src/graph_spectrum_calc.js @@ -230,7 +230,9 @@ GraphSpectrumCalc._dataLoadPowerSpectralDensityVsX = function(vsFieldNames, minV let maxNoise = 0; // Stores the maximum amplitude of the fft over all chunks let psdLength = 0; // Matrix where each row represents a bin of vs values, and the columns are amplitudes at frequencies - const matrixFftOutput = new Array(NUM_VS_BINS).fill(null).map(() => (new Float64Array(fftChunkLength * 2)).fill(-70)); + const matrixFftOutput = new Array(NUM_VS_BINS) + .fill(null) + .map(() => (new Float64Array(Math.floor(fftChunkLength / 2))).fill(-70)); const numberSamples = new Uint32Array(NUM_VS_BINS); // Number of samples in each vs value, used to average them later. From 01a0c98102cffbac2e78e98d6781d922418609ac Mon Sep 17 00:00:00 2001 From: demvlad Date: Sat, 10 May 2025 22:39:00 +0300 Subject: [PATCH 07/17] Resolved missing this. issue --- src/graph_spectrum_calc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graph_spectrum_calc.js b/src/graph_spectrum_calc.js index a014c75e..c7d0b1f6 100644 --- a/src/graph_spectrum_calc.js +++ b/src/graph_spectrum_calc.js @@ -79,7 +79,7 @@ GraphSpectrumCalc.setOutTime = function(time) { if ((time - this._analyserTimeRange.in) <= MAX_ANALYSER_LENGTH) { this._analyserTimeRange.out = time; } else { - this._analyserTimeRange.out = analyserTimeRange.in + MAX_ANALYSER_LENGTH; + this._analyserTimeRange.out = this._analyserTimeRange.in + MAX_ANALYSER_LENGTH; } return this._analyserTimeRange.out; }; From 7a59f8afdaf6273c6a485659bf722630e97e39ec Mon Sep 17 00:00:00 2001 From: demvlad Date: Sun, 11 May 2025 14:14:49 +0300 Subject: [PATCH 08/17] Added PSD values label at mouse position cursor by Shift key --- src/graph_spectrum_calc.js | 1 - src/graph_spectrum_plot.js | 36 ++++++++++++++++++++++++++++++------ 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/src/graph_spectrum_calc.js b/src/graph_spectrum_calc.js index c7d0b1f6..5bbdf5d7 100644 --- a/src/graph_spectrum_calc.js +++ b/src/graph_spectrum_calc.js @@ -681,7 +681,6 @@ GraphSpectrumCalc._psd = function(samples, pointsPerSegment, overlapCount, scal }; }; - /** * Compute FFT for samples segments by lenghts as pointsPerSegment with overlapCount overlap points count */ diff --git a/src/graph_spectrum_plot.js b/src/graph_spectrum_plot.js index 643fb7e2..502c0183 100644 --- a/src/graph_spectrum_plot.js +++ b/src/graph_spectrum_plot.js @@ -451,7 +451,7 @@ GraphSpectrumPlot._drawFrequencyVsXGraph = function (canvasCtx, drawPSD = false) MARGIN_BOTTOM, "Hz" ); - + if (this._spectrumType === SPECTRUM_TYPE.FREQ_VS_THROTTLE || this._spectrumType === SPECTRUM_TYPE.PSD_VS_THROTTLE) { this._drawVerticalGridLines( @@ -523,6 +523,17 @@ GraphSpectrumPlot._drawHeatMap = function (drawPSD = false) { return heatMapCanvas; }; +GraphSpectrumPlot.getValueFromMatrixFFT = function(frequency, vsArgument) { + const NUM_VS_BINS = 100; // redefinition of value from graph_spectrum_calc.js module! + const matrixFFT = this._fftData; + let vsArgumentIndex = Math.round(NUM_VS_BINS * (vsArgument - matrixFFT.vsRange.min) / (matrixFFT.vsRange.max - matrixFFT.vsRange.min)); + if (vsArgumentIndex === NUM_VS_BINS) { + vsArgumentIndex = NUM_VS_BINS - 1; + } + const freqIndex = Math.round(2 * frequency / matrixFFT.blackBoxRate * (matrixFFT.fftOutput[0].length - 1) ); + return matrixFFT.fftOutput[vsArgumentIndex][freqIndex]; +}; + GraphSpectrumPlot._drawPidErrorVsSetpointGraph = function (canvasCtx) { const ACTUAL_MARGIN_LEFT = this._getActualMarginLeft(); @@ -1463,6 +1474,7 @@ GraphSpectrumPlot._drawMousePosition = function ( lineWidth ) { // X axis + let mouseFrequency = 0; if ( this._spectrumType === SPECTRUM_TYPE.FREQUENCY || this._spectrumType === SPECTRUM_TYPE.FREQ_VS_THROTTLE || @@ -1475,7 +1487,7 @@ GraphSpectrumPlot._drawMousePosition = function ( const sampleRate = this._fftData.blackBoxRate / this._zoomX; const marginLeft = this._getActualMarginLeft(); - const mouseFrequency = + mouseFrequency = ((mouseX - marginLeft) / WIDTH) * (this._fftData.blackBoxRate / this._zoomX / 2); if (mouseFrequency >= 0 && mouseFrequency <= sampleRate) { @@ -1513,12 +1525,12 @@ GraphSpectrumPlot._drawMousePosition = function ( if (unitLabel !== null) { const val_min = this._fftData.vsRange.min; const val_max = this._fftData.vsRange.max; - const mouseValue = (1 - mouseY / HEIGHT) * (val_max - val_min) + val_min; - if (mouseValue >= val_min && mouseValue <= val_max) { - const valueLabel = `${mouseValue.toFixed(0)}${unitLabel}`; + const vsArgValue = (1 - mouseY / HEIGHT) * (val_max - val_min) + val_min; + if (vsArgValue >= val_min && vsArgValue <= val_max) { + const valueLabel = `${vsArgValue.toFixed(0)}${unitLabel}`; this._drawHorizontalMarkerLine( canvasCtx, - mouseValue, + vsArgValue, val_min, val_max, valueLabel, @@ -1528,6 +1540,18 @@ GraphSpectrumPlot._drawMousePosition = function ( stroke, lineWidth ); + + if (this._spectrumType == SPECTRUM_TYPE.PSD_VS_THROTTLE || + this._spectrumType == SPECTRUM_TYPE.PSD_VS_RPM) { + const label = Math.round(this.getValueFromMatrixFFT(mouseFrequency, vsArgValue)).toString() + "dBm/Hz"; + this._drawAxisLabel( + canvasCtx, + label, + mouseX - 25, + mouseY - 4, + "left", + ); + } } } } else if (this._spectrumType === SPECTRUM_TYPE.PIDERROR_VS_SETPOINT) { From c128b2a7bae1e88ee9e3782c0d7c77fc0f3aa212 Mon Sep 17 00:00:00 2001 From: demvlad Date: Sun, 11 May 2025 18:52:52 +0300 Subject: [PATCH 09/17] Added PSD values label on the mouse position cursor by Shift key at the power specral density chart --- src/graph_spectrum_plot.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/graph_spectrum_plot.js b/src/graph_spectrum_plot.js index 502c0183..ee672efe 100644 --- a/src/graph_spectrum_plot.js +++ b/src/graph_spectrum_plot.js @@ -405,6 +405,11 @@ GraphSpectrumPlot._drawPowerSpectralDensityGraph = function (canvasCtx) { canvasCtx.restore(); }; +GraphSpectrumPlot.getPSDbyFreq = function(frequency) { + const freqIndex = Math.round(2 * frequency / this._fftData.blackBoxRate * (this._fftData.psdOutput.length - 1) ); + return this._fftData.psdOutput[freqIndex]; +}; + GraphSpectrumPlot._drawFrequencyVsXGraph = function (canvasCtx, drawPSD = false) { const PLOTTED_BLACKBOX_RATE = this._fftData.blackBoxRate / this._zoomX; @@ -1504,6 +1509,17 @@ GraphSpectrumPlot._drawMousePosition = function ( ); } + if (this._spectrumType === SPECTRUM_TYPE.POWER_SPECTRAL_DENSITY) { + const psdLabel = Math.round(this.getPSDbyFreq(mouseFrequency)).toString() + "dBm/Hz"; + this._drawAxisLabel( + canvasCtx, + psdLabel, + mouseX - 30, + mouseY - 4, + "left", + ); + } + // Y axis let unitLabel; switch (this._spectrumType) { @@ -1547,7 +1563,7 @@ GraphSpectrumPlot._drawMousePosition = function ( this._drawAxisLabel( canvasCtx, label, - mouseX - 25, + mouseX - 30, mouseY - 4, "left", ); From 75468158eeb3fc50cd65b47f3e9884b18a99bc51 Mon Sep 17 00:00:00 2001 From: demvlad Date: Mon, 12 May 2025 12:11:00 +0300 Subject: [PATCH 10/17] Code refactoring --- src/graph_spectrum_calc.js | 34 +++++++++++++++++----------------- src/graph_spectrum_plot.js | 6 +++--- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/graph_spectrum_calc.js b/src/graph_spectrum_calc.js index 5bbdf5d7..c8125e18 100644 --- a/src/graph_spectrum_calc.js +++ b/src/graph_spectrum_calc.js @@ -129,7 +129,7 @@ GraphSpectrumCalc.dataLoadPSD = function(analyserZoomY) { blackBoxRate : this._blackBoxRate, minimum: psd.min, maximum: psd.max, - maxNoiseIdx: psd.maxNoiseIdx, + maxNoiseFrequency: psd.maxNoiseFrequency, }; return psdData; }; @@ -228,11 +228,11 @@ GraphSpectrumCalc._dataLoadPowerSpectralDensityVsX = function(vsFieldNames, minV const fftChunkWindow = Math.round(fftChunkLength / FREQ_VS_THR_WINDOW_DIVISOR); let maxNoise = 0; // Stores the maximum amplitude of the fft over all chunks - let psdLength = 0; + const psdLength = Math.floor(fftChunkLength / 2); // Matrix where each row represents a bin of vs values, and the columns are amplitudes at frequencies - const matrixFftOutput = new Array(NUM_VS_BINS) + const matrixPsdOutput = new Array(NUM_VS_BINS) .fill(null) - .map(() => (new Float64Array(Math.floor(fftChunkLength / 2))).fill(-70)); + .map(() => (new Float64Array(psdLength)).fill(-70)); const numberSamples = new Uint32Array(NUM_VS_BINS); // Number of samples in each vs value, used to average them later. @@ -240,7 +240,6 @@ GraphSpectrumCalc._dataLoadPowerSpectralDensityVsX = function(vsFieldNames, minV const fftInput = flightSamples.samples.slice(fftChunkIndex, fftChunkIndex + fftChunkLength); const psd = this._psd(fftInput, fftChunkLength, 0, 'density'); - psdLength = psd.psdOutput.length; maxNoise = Math.max(psd.max, maxNoise); // calculate a bin index and put the fft value in that bin for each field (e.g. eRPM[0], eRPM[1]..) sepparately for (const vsValueArray of flightSamples.vsValues) { @@ -257,8 +256,8 @@ GraphSpectrumCalc._dataLoadPowerSpectralDensityVsX = function(vsFieldNames, minV numberSamples[vsBinIndex]++; // add the output from the fft to the row given by the vs value bin index - for (let i = 0; i < psd.psdOutput.length; i++) { - matrixFftOutput[vsBinIndex][i] += psd.psdOutput[i]; + for (let i = 0; i < psdLength; i++) { + matrixPsdOutput[vsBinIndex][i] += psd.psdOutput[i]; } } } @@ -266,8 +265,8 @@ GraphSpectrumCalc._dataLoadPowerSpectralDensityVsX = function(vsFieldNames, minV // Divide the values from the fft in each row (vs value bin) by the number of samples in the bin for (let i = 0; i < NUM_VS_BINS; i++) { if (numberSamples[i] > 1) { - for (let j = 0; j < matrixFftOutput[i].length; j++) { - matrixFftOutput[i][j] /= numberSamples[i]; + for (let j = 0; j < psdLength; j++) { + matrixPsdOutput[i][j] /= numberSamples[i]; } } } @@ -280,7 +279,7 @@ GraphSpectrumCalc._dataLoadPowerSpectralDensityVsX = function(vsFieldNames, minV fieldIndex : this._dataBuffer.fieldIndex, fieldName : this._dataBuffer.fieldName, fftLength : psdLength, - fftOutput : matrixFftOutput, + fftOutput : matrixPsdOutput, maxNoise : maxNoise, blackBoxRate : this._blackBoxRate, vsRange : { min: flightSamples.minValue, max: flightSamples.maxValue}, @@ -581,14 +580,14 @@ GraphSpectrumCalc._normalizeFft = function(fftOutput, fftLength) { } } - maxNoiseIdx = maxNoiseIdx / fftLength * maxFrequency; + const maxNoiseFrequency = maxNoiseIdx / fftLength * maxFrequency; const fftData = { fieldIndex : this._dataBuffer.fieldIndex, fieldName : this._dataBuffer.fieldName, fftLength : fftLength, fftOutput : fftOutput, - maxNoiseIdx : maxNoiseIdx, + maxNoiseFrequency : maxNoiseFrequency, blackBoxRate : this._blackBoxRate, }; @@ -639,7 +638,7 @@ GraphSpectrumCalc._psd = function(samples, pointsPerSegment, overlapCount, scal psdOutput: new Float64Array(0), min: 0, max: 0, - maxNoiseIdx: 0, + maxNoiseFrequency: 0, }; } const maxFrequency = (this._blackBoxRate / 2.0); @@ -677,7 +676,7 @@ GraphSpectrumCalc._psd = function(samples, pointsPerSegment, overlapCount, scal psdOutput: psdOutput, min: min, max: max, - maxNoiseIdx: maxNoiseFrequency, + maxNoiseFrequency: maxNoiseFrequency, }; }; @@ -685,7 +684,8 @@ GraphSpectrumCalc._psd = function(samples, pointsPerSegment, overlapCount, scal * Compute FFT for samples segments by lenghts as pointsPerSegment with overlapCount overlap points count */ GraphSpectrumCalc._fft_segmented = function(samples, pointsPerSegment, overlapCount) { - const samplesCount = samples.length; + const samplesCount = samples.length, + fftLength = Math.floor(pointsPerSegment / 2); let output = []; for (let i = 0; i <= samplesCount - pointsPerSegment; i += pointsPerSegment - overlapCount) { const fftInput = samples.slice(i, i + pointsPerSegment); @@ -695,8 +695,8 @@ GraphSpectrumCalc._fft_segmented = function(samples, pointsPerSegment, overlapC } const fftComplex = this._fft(fftInput); - const magnitudes = new Float64Array(pointsPerSegment / 2); - for (let i = 0; i < pointsPerSegment / 2; i++) { + const magnitudes = new Float64Array(fftLength); + for (let i = 0; i < fftLength; i++) { const re = fftComplex[2 * i]; const im = fftComplex[2 * i + 1]; magnitudes[i] = Math.hypot(re, im); diff --git a/src/graph_spectrum_plot.js b/src/graph_spectrum_plot.js index ee672efe..e81e1374 100644 --- a/src/graph_spectrum_plot.js +++ b/src/graph_spectrum_plot.js @@ -392,7 +392,7 @@ GraphSpectrumPlot._drawPowerSpectralDensityGraph = function (canvasCtx) { const offset = 1; this._drawInterestFrequency( canvasCtx, - this._fftData.maxNoiseIdx, + this._fftData.maxNoiseFrequency, PLOTTED_BLACKBOX_RATE, "Max noise", WIDTH, @@ -535,7 +535,7 @@ GraphSpectrumPlot.getValueFromMatrixFFT = function(frequency, vsArgument) { if (vsArgumentIndex === NUM_VS_BINS) { vsArgumentIndex = NUM_VS_BINS - 1; } - const freqIndex = Math.round(2 * frequency / matrixFFT.blackBoxRate * (matrixFFT.fftOutput[0].length - 1) ); + const freqIndex = Math.round(2 * frequency / matrixFFT.blackBoxRate * (matrixFFT.fftLength - 1) ); return matrixFFT.fftOutput[vsArgumentIndex][freqIndex]; }; @@ -983,7 +983,7 @@ GraphSpectrumPlot._drawFiltersAndMarkers = function (canvasCtx) { if (this._spectrumType === SPECTRUM_TYPE.FREQUENCY) { this._drawInterestFrequency( canvasCtx, - this._fftData.maxNoiseIdx, + this._fftData.maxNoiseFrequency, PLOTTED_BLACKBOX_RATE, "Max noise", WIDTH, From effb82014589ce6ed24b92aa3a4b78c6893ac1a9 Mon Sep 17 00:00:00 2001 From: demvlad Date: Mon, 12 May 2025 14:39:50 +0300 Subject: [PATCH 11/17] The background heatmap PSD value is changed from -70 to -100dBm --- src/graph_spectrum_calc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graph_spectrum_calc.js b/src/graph_spectrum_calc.js index c8125e18..75301636 100644 --- a/src/graph_spectrum_calc.js +++ b/src/graph_spectrum_calc.js @@ -232,7 +232,7 @@ GraphSpectrumCalc._dataLoadPowerSpectralDensityVsX = function(vsFieldNames, minV // Matrix where each row represents a bin of vs values, and the columns are amplitudes at frequencies const matrixPsdOutput = new Array(NUM_VS_BINS) .fill(null) - .map(() => (new Float64Array(psdLength)).fill(-70)); + .map(() => (new Float64Array(psdLength)).fill(-100)); const numberSamples = new Uint32Array(NUM_VS_BINS); // Number of samples in each vs value, used to average them later. From 871a0124707e8fd9c491bba02c2a6953bc9da333 Mon Sep 17 00:00:00 2001 From: demvlad Date: Mon, 12 May 2025 19:57:24 +0300 Subject: [PATCH 12/17] Added PSD value to maximal noise PSD marker --- src/graph_spectrum_plot.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/graph_spectrum_plot.js b/src/graph_spectrum_plot.js index e81e1374..cb3899c8 100644 --- a/src/graph_spectrum_plot.js +++ b/src/graph_spectrum_plot.js @@ -406,8 +406,10 @@ GraphSpectrumPlot._drawPowerSpectralDensityGraph = function (canvasCtx) { }; GraphSpectrumPlot.getPSDbyFreq = function(frequency) { - const freqIndex = Math.round(2 * frequency / this._fftData.blackBoxRate * (this._fftData.psdOutput.length - 1) ); - return this._fftData.psdOutput[freqIndex]; + let freqIndex = Math.round(2 * frequency / this._fftData.blackBoxRate * (this._fftData.psdOutput.length - 1) ); + freqIndex = Math.min(freqIndex, this._fftData.psdOutput.length - 1); + freqIndex = Math.max(freqIndex, 0); + return this._fftData.psdOutput.length ? this._fftData.psdOutput[freqIndex] : 0; }; GraphSpectrumPlot._drawFrequencyVsXGraph = function (canvasCtx, drawPSD = false) { @@ -535,7 +537,9 @@ GraphSpectrumPlot.getValueFromMatrixFFT = function(frequency, vsArgument) { if (vsArgumentIndex === NUM_VS_BINS) { vsArgumentIndex = NUM_VS_BINS - 1; } - const freqIndex = Math.round(2 * frequency / matrixFFT.blackBoxRate * (matrixFFT.fftLength - 1) ); + let freqIndex = Math.round(2 * frequency / matrixFFT.blackBoxRate * (matrixFFT.fftLength - 1)); + freqIndex = Math.max(freqIndex, 0); + freqIndex = Math.min(freqIndex, matrixFFT.fftLength - 1); return matrixFFT.fftOutput[vsArgumentIndex][freqIndex]; }; @@ -1274,7 +1278,14 @@ GraphSpectrumPlot._drawInterestFrequency = function ( stroke, lineWidth ) { - const interestLabel = `${label} ${frequency.toFixed(0)}Hz`; + + let interestLabel = ""; + if (this._spectrumType === SPECTRUM_TYPE.POWER_SPECTRAL_DENSITY && label != "" ) { + const psdValue = this.getPSDbyFreq(frequency); + interestLabel = `${label}: (${frequency.toFixed(0)}Hz, ${psdValue.toFixed(0)}dBm/Hz)`; + } else { + interestLabel = `${label} ${frequency.toFixed(0)}Hz`; + } return this._drawVerticalMarkerLine( canvasCtx, frequency, From 30b62b100bf282c08a7e0deb252bb8715ebd8ee9 Mon Sep 17 00:00:00 2001 From: Vladimir Demidov Date: Mon, 12 May 2025 21:16:16 +0300 Subject: [PATCH 13/17] Removed needless spaces Co-authored-by: Mark Haslinghuis --- src/graph_spectrum_plot.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/graph_spectrum_plot.js b/src/graph_spectrum_plot.js index cb3899c8..d1697d4c 100644 --- a/src/graph_spectrum_plot.js +++ b/src/graph_spectrum_plot.js @@ -1523,11 +1523,11 @@ GraphSpectrumPlot._drawMousePosition = function ( if (this._spectrumType === SPECTRUM_TYPE.POWER_SPECTRAL_DENSITY) { const psdLabel = Math.round(this.getPSDbyFreq(mouseFrequency)).toString() + "dBm/Hz"; this._drawAxisLabel( - canvasCtx, - psdLabel, - mouseX - 30, - mouseY - 4, - "left", + canvasCtx, + psdLabel, + mouseX - 30, + mouseY - 4, + "left", ); } From 3877f8441bdce65625b6ad918a28a21cf2fc89db Mon Sep 17 00:00:00 2001 From: demvlad Date: Mon, 12 May 2025 21:30:01 +0300 Subject: [PATCH 14/17] The SPECTRUM_TYPE enum members are reordered --- index.html | 10 +++++----- src/graph_spectrum_plot.js | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/index.html b/index.html index 71b5d086..94cf99f8 100644 --- a/index.html +++ b/index.html @@ -457,11 +457,11 @@

Workspace

diff --git a/src/graph_spectrum_plot.js b/src/graph_spectrum_plot.js index d1697d4c..a4a2eded 100644 --- a/src/graph_spectrum_plot.js +++ b/src/graph_spectrum_plot.js @@ -15,11 +15,11 @@ const BLUR_FILTER_PIXEL = 1, export const SPECTRUM_TYPE = { FREQUENCY: 0, FREQ_VS_THROTTLE: 1, - PIDERROR_VS_SETPOINT: 2, - FREQ_VS_RPM: 3, - POWER_SPECTRAL_DENSITY: 4, - PSD_VS_THROTTLE: 5, - PSD_VS_RPM: 6, + FREQ_VS_RPM: 2, + POWER_SPECTRAL_DENSITY: 3, + PSD_VS_THROTTLE: 4, + PSD_VS_RPM: 5, + PIDERROR_VS_SETPOINT: 6, }; export const SPECTRUM_OVERDRAW_TYPE = { From 6334c89c97bf6c9abc15832302e1f882e5f77134 Mon Sep 17 00:00:00 2001 From: Vladimir Demidov Date: Mon, 12 May 2025 21:51:53 +0300 Subject: [PATCH 15/17] Code style improvement Co-authored-by: Mark Haslinghuis --- src/graph_spectrum_plot.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/graph_spectrum_plot.js b/src/graph_spectrum_plot.js index a4a2eded..93c04267 100644 --- a/src/graph_spectrum_plot.js +++ b/src/graph_spectrum_plot.js @@ -1568,8 +1568,8 @@ GraphSpectrumPlot._drawMousePosition = function ( lineWidth ); - if (this._spectrumType == SPECTRUM_TYPE.PSD_VS_THROTTLE || - this._spectrumType == SPECTRUM_TYPE.PSD_VS_RPM) { + if (this._spectrumType === SPECTRUM_TYPE.PSD_VS_THROTTLE || + this._spectrumType === SPECTRUM_TYPE.PSD_VS_RPM) { const label = Math.round(this.getValueFromMatrixFFT(mouseFrequency, vsArgValue)).toString() + "dBm/Hz"; this._drawAxisLabel( canvasCtx, From 77acf1ed2a0660a51a367cc0efd52c3a8ef8ce3f Mon Sep 17 00:00:00 2001 From: demvlad Date: Mon, 12 May 2025 22:39:07 +0300 Subject: [PATCH 16/17] The samples data slice is removed to prevent spectrum issue for some log files --- src/graph_spectrum_calc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graph_spectrum_calc.js b/src/graph_spectrum_calc.js index 75301636..953986e0 100644 --- a/src/graph_spectrum_calc.js +++ b/src/graph_spectrum_calc.js @@ -403,7 +403,7 @@ GraphSpectrumCalc._getFlightSamplesFreq = function(scaled = true) { } return { - samples : samples.slice(0, samplesCount), + samples : samples, count : samplesCount, }; }; From b1bd76197993514e674894aa28b42441d4cc236d Mon Sep 17 00:00:00 2001 From: demvlad Date: Tue, 13 May 2025 01:18:13 +0300 Subject: [PATCH 17/17] Revert "The samples data slice is removed to prevent spectrum issue for some log files" This reverts commit 77acf1ed2a0660a51a367cc0efd52c3a8ef8ce3f. --- src/graph_spectrum_calc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graph_spectrum_calc.js b/src/graph_spectrum_calc.js index 953986e0..75301636 100644 --- a/src/graph_spectrum_calc.js +++ b/src/graph_spectrum_calc.js @@ -403,7 +403,7 @@ GraphSpectrumCalc._getFlightSamplesFreq = function(scaled = true) { } return { - samples : samples, + samples : samples.slice(0, samplesCount), count : samplesCount, }; };