Skip to content

Commit 966e0cd

Browse files
committed
Added Power spectral density curves chart
1 parent 7905a91 commit 966e0cd

File tree

4 files changed

+242
-22
lines changed

4 files changed

+242
-22
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: 128 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@ GraphSpectrumCalc._getFlightChunks = function() {
325325
return allChunks;
326326
};
327327

328-
GraphSpectrumCalc._getFlightSamplesFreq = function() {
328+
GraphSpectrumCalc._getFlightSamplesFreq = function(scaled = true) {
329329

330330
const allChunks = this._getFlightChunks();
331331

@@ -335,7 +335,11 @@ GraphSpectrumCalc._getFlightSamplesFreq = function() {
335335
let samplesCount = 0;
336336
for (const chunk of allChunks) {
337337
for (const frame of chunk.frames) {
338-
samples[samplesCount] = (this._dataBuffer.curve.lookupRaw(frame[this._dataBuffer.fieldIndex]));
338+
if (scaled) {
339+
samples[samplesCount] = this._dataBuffer.curve.lookupRaw(frame[this._dataBuffer.fieldIndex]);
340+
} else {
341+
samples[samplesCount] = frame[this._dataBuffer.fieldIndex];
342+
}
339343
samplesCount++;
340344
}
341345
}
@@ -423,13 +427,14 @@ GraphSpectrumCalc._getFlightSamplesFreqVsX = function(vsFieldNames, minValue = I
423427
for (const vsValueArray of vsValues) {
424428
slicedVsValues.push(vsValueArray.slice(0, samplesCount));
425429
}
430+
426431
return {
427-
samples : samples.slice(0, samplesCount),
428-
vsValues : slicedVsValues,
429-
count : samplesCount,
430-
minValue : minValue,
431-
maxValue : maxValue,
432-
};
432+
samples : samples.slice(0, samplesCount),
433+
vsValues : slicedVsValues,
434+
count : samplesCount,
435+
minValue : minValue,
436+
maxValue : maxValue,
437+
};
433438
};
434439

435440
GraphSpectrumCalc._getFlightSamplesPidErrorVsSetpoint = function(axisIndex) {
@@ -458,8 +463,8 @@ GraphSpectrumCalc._getFlightSamplesPidErrorVsSetpoint = function(axisIndex) {
458463
}
459464

460465
return {
461-
piderror,
462-
setpoint,
466+
piderror: piderror.slice(0, samplesCount),
467+
setpoint: setpoint.slice(0, samplesCount),
463468
maxSetpoint,
464469
count: samplesCount,
465470
};
@@ -526,3 +531,116 @@ GraphSpectrumCalc._normalizeFft = function(fftOutput) {
526531

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

0 commit comments

Comments
 (0)