Skip to content

Commit cd62b52

Browse files
demvladmituritsynhaslinghuis
authored
Added Power Spectral Density curves at the spectrum chart (Completion of #820 PR) (#826)
* Added computing of PSD - Power Spectral Density for spectrum chart * Added PSD curves at spectrum chart * Removed redundant check TODO code * Code issues are resolved * PSD caption is edited in index.html Co-authored-by: MikeNomatter <mikhail-turicyn@yandex.ru> * Removed non execute condition. The FFT output is allways even value * Powers scale computing code refactoring * Draw the all points of PSD data * Resolved issue of magnitude computing * Added vertical grid lines * Added grid with db value captions * The PSD Y axises range is alligned by value 10db/Hz * Added setup points per segment by using vertical slider (Y-scale slider) * maximal num per segment count value is limited by data samples count * The names variable are changed to JS code style * Added the maximal noise frequency label * The 'Max motor noise' label is renamed to 'Max noise' * Added mouse marker at the PSD chart * Zeroes frequency magnitudes removed from curves min-max range * Decreased spectrums charts plot transparent * Code style improvement * Max noise definition low frequency limit is changed from 100Hz to 50Hz for PSD chart * Added horizont mouse position marker line at PSD chart * The max noise label is shifted down at PSD chart * The PSD chart updates immediately after Y scale sliders change * PSD unit label is changed at dBm/Hz * The minimum PSD value is set as -70dBm * Code style improvement Co-authored-by: Mark Haslinghuis <mark@numloq.nl> * Code style improvement Co-authored-by: Mark Haslinghuis <mark@numloq.nl> * Code style improvement * Added checking for missing samples segment data Co-authored-by: Mark Haslinghuis <mark@numloq.nl> * Code style improvement Co-authored-by: Mark Haslinghuis <mark@numloq.nl> * Resolved wrong variable name * Resolved missing coma * Resolved "for" loop condition issue, when num per segment value is equal samples count * Code style improvement --------- Co-authored-by: MikeNomatter <mikhail-turicyn@yandex.ru> Co-authored-by: Mark Haslinghuis <mark@numloq.nl>
1 parent 20cec88 commit cd62b52

File tree

4 files changed

+260
-14
lines changed

4 files changed

+260
-14
lines changed

index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,7 @@ <h4>Workspace</h4>
459459
<option value="1">Freq. vs Throttle</option>
460460
<option value="3">Freq. vs RPM</option>
461461
<option value="2">Error vs Setpoint</option>
462+
<option value="4">Power spectral density</option>
462463
</select>
463464
</div>
464465

src/graph_spectrum.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,10 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) {
116116
fftData = GraphSpectrumCalc.dataLoadPidErrorVsSetpoint();
117117
break;
118118

119+
case SPECTRUM_TYPE.POWER_SPECTRAL_DENSITY:
120+
fftData = GraphSpectrumCalc.dataLoadPSD(analyserZoomY);
121+
break;
122+
119123
case SPECTRUM_TYPE.FREQUENCY:
120124
default:
121125
fftData = GraphSpectrumCalc.dataLoadFrequency();
@@ -187,6 +191,11 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) {
187191
debounce(100, function () {
188192
analyserZoomY = 1 / (analyserZoomYElem.val() / 100);
189193
GraphSpectrumPlot.setZoom(analyserZoomX, analyserZoomY);
194+
// Recalculate PSD with updated samples per segment count
195+
if (userSettings.spectrumType == SPECTRUM_TYPE.POWER_SPECTRAL_DENSITY) {
196+
dataLoad();
197+
GraphSpectrumPlot.setData(fftData, userSettings.spectrumType);
198+
}
190199
that.refresh();
191200
})
192201
)

src/graph_spectrum_calc.js

Lines changed: 146 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,33 @@ GraphSpectrumCalc.dataLoadFrequency = function() {
106106
return fftData;
107107
};
108108

109+
GraphSpectrumCalc.dataLoadPSD = function(analyserZoomY) {
110+
const flightSamples = this._getFlightSamplesFreq(false);
111+
112+
let pointsPerSegment = 512;
113+
const multipiler = Math.floor(1 / analyserZoomY); // 0. ... 10
114+
if (multipiler == 0) {
115+
pointsPerSegment = 256;
116+
} else if (multipiler > 1) {
117+
pointsPerSegment *= 2 ** Math.floor(multipiler / 2);
118+
}
119+
pointsPerSegment = Math.min(pointsPerSegment, flightSamples.samples.length);
120+
const overlapCount = Math.floor(pointsPerSegment / 2);
121+
122+
const psd = this._psd(flightSamples.samples, pointsPerSegment, overlapCount);
123+
124+
const psdData = {
125+
fieldIndex : this._dataBuffer.fieldIndex,
126+
fieldName : this._dataBuffer.fieldName,
127+
psdLength : psd.psdOutput.length,
128+
psdOutput : psd.psdOutput,
129+
blackBoxRate : this._blackBoxRate,
130+
minimum: psd.min,
131+
maximum: psd.max,
132+
maxNoiseIdx: psd.maxNoiseIdx,
133+
};
134+
return psdData;
135+
};
109136

110137
GraphSpectrumCalc._dataLoadFrequencyVsX = function(vsFieldNames, minValue = Infinity, maxValue = -Infinity) {
111138

@@ -283,7 +310,7 @@ GraphSpectrumCalc._getFlightChunks = function() {
283310
return allChunks;
284311
};
285312

286-
GraphSpectrumCalc._getFlightSamplesFreq = function() {
313+
GraphSpectrumCalc._getFlightSamplesFreq = function(scaled = true) {
287314

288315
const allChunks = this._getFlightChunks();
289316

@@ -293,7 +320,11 @@ GraphSpectrumCalc._getFlightSamplesFreq = function() {
293320
let samplesCount = 0;
294321
for (const chunk of allChunks) {
295322
for (const frame of chunk.frames) {
296-
samples[samplesCount] = (this._dataBuffer.curve.lookupRaw(frame[this._dataBuffer.fieldIndex]));
323+
if (scaled) {
324+
samples[samplesCount] = this._dataBuffer.curve.lookupRaw(frame[this._dataBuffer.fieldIndex]);
325+
} else {
326+
samples[samplesCount] = frame[this._dataBuffer.fieldIndex];
327+
}
297328
samplesCount++;
298329
}
299330
}
@@ -485,3 +516,116 @@ GraphSpectrumCalc._normalizeFft = function(fftOutput, fftLength) {
485516

486517
return fftData;
487518
};
519+
520+
/**
521+
* Compute PSD for data samples by Welch method follow Python code
522+
*/
523+
GraphSpectrumCalc._psd = function(samples, pointsPerSegment, overlapCount, scaling = 'density') {
524+
// Compute FFT for samples segments
525+
const fftOutput = this._fft_segmented(samples, pointsPerSegment, overlapCount);
526+
527+
const dataCount = fftOutput[0].length;
528+
const segmentsCount = fftOutput.length;
529+
const psdOutput = new Float64Array(dataCount);
530+
531+
// Compute power scale coef
532+
let scale = 1;
533+
if (userSettings.analyserHanning) {
534+
const window = Array(pointsPerSegment).fill(1);
535+
this._hanningWindow(window, pointsPerSegment);
536+
if (scaling == 'density') {
537+
let skSum = 0;
538+
for (const value of window) {
539+
skSum += value ** 2;
540+
}
541+
scale = 1 / (this._blackBoxRate * skSum);
542+
} else if (scaling == 'spectrum') {
543+
let sum = 0;
544+
for (const value of window) {
545+
sum += value;
546+
}
547+
scale = 1 / sum ** 2;
548+
}
549+
} else if (scaling == 'density') {
550+
scale = 1 / pointsPerSegment;
551+
} else if (scaling == 'spectrum') {
552+
scale = 1 / pointsPerSegment ** 2;
553+
}
554+
555+
// Compute average for scaled power
556+
let min = 1e6,
557+
max = -1e6;
558+
// Early exit if no segments were processed
559+
if (segmentsCount === 0) {
560+
return {
561+
psdOutput: new Float64Array(0),
562+
min: 0,
563+
max: 0,
564+
maxNoiseIdx: 0,
565+
};
566+
}
567+
const maxFrequency = (this._blackBoxRate / 2.0);
568+
const noise50HzIdx = 50 / maxFrequency * dataCount;
569+
const noise3HzIdx = 3 / maxFrequency * dataCount;
570+
let maxNoiseIdx = 0;
571+
let maxNoise = -100;
572+
for (let i = 0; i < dataCount; i++) {
573+
psdOutput[i] = 0.0;
574+
for (let j = 0; j < segmentsCount; j++) {
575+
let p = scale * fftOutput[j][i] ** 2;
576+
if (i != dataCount - 1) {
577+
p *= 2;
578+
}
579+
psdOutput[i] += p;
580+
}
581+
582+
const min_avg = 1e-7; // limit min value for -70db
583+
let avg = psdOutput[i] / segmentsCount;
584+
avg = Math.max(avg, min_avg);
585+
psdOutput[i] = 10 * Math.log10(avg);
586+
if (i > noise3HzIdx) { // Miss big zero freq magnitude
587+
min = Math.min(psdOutput[i], min);
588+
max = Math.max(psdOutput[i], max);
589+
}
590+
if (i > noise50HzIdx && psdOutput[i] > maxNoise) {
591+
maxNoise = psdOutput[i];
592+
maxNoiseIdx = i;
593+
}
594+
}
595+
596+
const maxNoiseFrequency = maxNoiseIdx / dataCount * maxFrequency;
597+
598+
return {
599+
psdOutput: psdOutput,
600+
min: min,
601+
max: max,
602+
maxNoiseIdx: maxNoiseFrequency,
603+
};
604+
};
605+
606+
607+
/**
608+
* Compute FFT for samples segments by lenghts as pointsPerSegment with overlapCount overlap points count
609+
*/
610+
GraphSpectrumCalc._fft_segmented = function(samples, pointsPerSegment, overlapCount) {
611+
const samplesCount = samples.length;
612+
let output = [];
613+
for (let i = 0; i <= samplesCount - pointsPerSegment; i += pointsPerSegment - overlapCount) {
614+
const fftInput = samples.slice(i, i + pointsPerSegment);
615+
616+
if (userSettings.analyserHanning) {
617+
this._hanningWindow(fftInput, pointsPerSegment);
618+
}
619+
620+
const fftComplex = this._fft(fftInput);
621+
const magnitudes = new Float64Array(pointsPerSegment / 2);
622+
for (let i = 0; i < pointsPerSegment / 2; i++) {
623+
const re = fftComplex[2 * i];
624+
const im = fftComplex[2 * i + 1];
625+
magnitudes[i] = Math.hypot(re, im);
626+
}
627+
output.push(magnitudes);
628+
}
629+
630+
return output;
631+
};

0 commit comments

Comments
 (0)