Skip to content

Commit 1f858ea

Browse files
committed
Improvements for log NSD algorithm
1 parent 2626439 commit 1f858ea

File tree

6 files changed

+166
-60
lines changed

6 files changed

+166
-60
lines changed

source/NSD.UI/MainWindow.axaml

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,11 @@
6565
<ComboBoxItem>μs</ComboBoxItem>
6666
<ComboBoxItem>ns</ComboBoxItem>
6767
</ComboBox>
68-
<Button Content="--&gt;" VerticalAlignment="Stretch" Click="btnTimeToRate_Click" Margin="12,0,0,0"></Button>
69-
<Button Content="&lt;--" VerticalAlignment="Stretch" Click="btnRateToTime_Click" Margin="2,0,0,0"></Button>
70-
<Label VerticalAlignment="Center" Margin="6,0,0,0">Data rate</Label>
71-
<TextBox Watermark="Data rate..." Text="{Binding DataRate}" IsEnabled="{Binding Enabled}" MinWidth="120"></TextBox>
72-
<ComboBox x:Name="cbRate" SelectedIndex="0" SelectedItem="{Binding SelectedDataRateUnitItem, Mode=OneWayToSource}" IsEnabled="{Binding Enabled}" HorizontalAlignment="Stretch" VerticalContentAlignment="Center" MinWidth="100" Margin="2,0,0,0">
68+
<Button IsVisible="false" Content="--&gt;" VerticalAlignment="Stretch" Click="btnTimeToRate_Click" Margin="12,0,0,0"></Button>
69+
<Button IsVisible="false" Content="&lt;--" VerticalAlignment="Stretch" Click="btnRateToTime_Click" Margin="2,0,0,0"></Button>
70+
<Label IsVisible="false" VerticalAlignment="Center" Margin="6,0,0,0">Data rate</Label>
71+
<TextBox IsVisible="false" Watermark="Data rate..." Text="{Binding DataRate}" IsEnabled="{Binding Enabled}" MinWidth="120"></TextBox>
72+
<ComboBox IsVisible="false" x:Name="cbRate" SelectedIndex="0" SelectedItem="{Binding SelectedDataRateUnitItem, Mode=OneWayToSource}" IsEnabled="{Binding Enabled}" HorizontalAlignment="Stretch" VerticalContentAlignment="Center" MinWidth="100" Margin="2,0,0,0">
7373
<ComboBoxItem>Samples per second</ComboBoxItem>
7474
<ComboBoxItem>Seconds per sample</ComboBoxItem>
7575
</ComboBox>
@@ -87,10 +87,12 @@
8787
</ComboBox>
8888

8989
<StackPanel x:Name="spLogarithmic" Orientation="Horizontal" Margin="6,0,0,0" IsVisible="{Binding AlgorithmLog}">
90-
<Label VerticalAlignment="Stretch" VerticalContentAlignment="Center">Minimum averages</Label>
91-
<TextBox Text="{Binding LogNsdMinAverages}" IsEnabled="{Binding Enabled}" MinWidth="30" Width="100" HorizontalAlignment="Left"></TextBox>
9290
<Label VerticalAlignment="Stretch" VerticalContentAlignment="Center" Margin="6,0,0,0">Points per decade</Label>
93-
<TextBox Text="{Binding LogNsdPointsDecade}" IsEnabled="{Binding Enabled}" MinWidth="30" Width="100" HorizontalAlignment="Left"></TextBox>
91+
<TextBox Text="{Binding LogNsdPointsDecade}" IsEnabled="{Binding Enabled}" MinWidth="30" Width="50" HorizontalAlignment="Left"></TextBox>
92+
<Label VerticalAlignment="Stretch" VerticalContentAlignment="Center">Minimum averages</Label>
93+
<TextBox Text="{Binding LogNsdMinAverages}" IsEnabled="{Binding Enabled}" MinWidth="30" Width="50" HorizontalAlignment="Left"></TextBox>
94+
<Label VerticalAlignment="Stretch" VerticalContentAlignment="Center">Minimum spectrum length</Label>
95+
<TextBox Text="{Binding LogNsdMinLength}" IsEnabled="{Binding Enabled}" MinWidth="30" Width="50" HorizontalAlignment="Left"></TextBox>
9496
</StackPanel>
9597
<StackPanel x:Name="spLinear" Orientation="Horizontal" Margin="6,0,0,0" IsVisible="{Binding AlgorithmLin}">
9698
<Label VerticalAlignment="Stretch" VerticalContentAlignment="Center">FFT length</Label>

source/NSD.UI/MainWindow.axaml.cs

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public MainWindow()
2222
{
2323
InitializeComponent();
2424
settings = Settings.Load();
25-
viewModel = new(settings);
25+
viewModel = new(settings, this);
2626
DataContext = viewModel;
2727
InitNsdChart();
2828
}
@@ -73,17 +73,17 @@ public async void btnRun_Click(object sender, RoutedEventArgs e)
7373
"ns" => acquisitionTime / 1e9,
7474
_ => throw new ApplicationException("Acquisition time combobox value not handled")
7575
};
76-
if (!double.TryParse(viewModel.DataRate, out double dataRateTime))
77-
{
78-
viewModel.Status = "Error: Invalid data rate value";
79-
return;
80-
}
81-
double dataRateTimeSeconds = (string)viewModel.SelectedDataRateUnitItem.Content switch
82-
{
83-
"Samples per second" => 1.0 / dataRateTime,
84-
"Seconds per sample" => dataRateTime,
85-
_ => throw new ApplicationException("Data rate combobox value not handled")
86-
};
76+
//if (!double.TryParse(viewModel.DataRate, out double dataRateTime))
77+
//{
78+
// viewModel.Status = "Error: Invalid data rate value";
79+
// return;
80+
//}
81+
//double dataRateTimeSeconds = (string)viewModel.SelectedDataRateUnitItem.Content switch
82+
//{
83+
// "Samples per second" => 1.0 / dataRateTime,
84+
// "Seconds per sample" => dataRateTime,
85+
// _ => throw new ApplicationException("Data rate combobox value not handled")
86+
//};
8787

8888
switch ((string)viewModel.SelectedNsdAlgorithm.Content)
8989
{
@@ -185,9 +185,17 @@ await Task.Run(() =>
185185
{
186186
case "Logarithmic":
187187
{
188-
var minAverages = int.Parse(viewModel.LogNsdMinAverages);
189188
var pointsPerDecade = int.Parse(viewModel.LogNsdPointsDecade);
190-
var nsd = await Task.Factory.StartNew(() => NSD.Log(input: records.ToArray(), 1.0 / acquisitionTimeSeconds, viewModel.XMin, viewModel.XMax, pointsPerDecade, minAverages));
189+
var minAverages = int.Parse(viewModel.LogNsdMinAverages);
190+
var minLength = int.Parse(viewModel.LogNsdMinLength);
191+
var nsd = await Task.Factory.StartNew(() => NSD.Log(
192+
input: records.ToArray(),
193+
sampleRateHz: 1.0 / acquisitionTimeSeconds,
194+
freqMin: viewModel.XMin,
195+
freqMax: viewModel.XMax,
196+
pointsPerDecade,
197+
minAverages,
198+
minLength));
191199
spectrum = nsd;
192200
break;
193201
}

source/NSD.UI/MainWindowViewModel.cs

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
using Avalonia.Media;
33
using CommunityToolkit.Mvvm.ComponentModel;
44
using System;
5-
using System.Collections.Generic;
65
using System.Collections.ObjectModel;
76
using System.Reflection;
87

@@ -25,9 +24,10 @@ public partial class MainWindowViewModel : ObservableObject
2524
[ObservableProperty] bool sgFilterChecked = false;
2625
[ObservableProperty] IBrush statusBackground = Brushes.WhiteSmoke;
2726
[ObservableProperty] string inputScaling = "1.0";
28-
[ObservableProperty] string logNsdMinAverages = "30";
2927
[ObservableProperty] string logNsdPointsDecade = "20";
30-
28+
[ObservableProperty] string logNsdMinAverages = "1";
29+
[ObservableProperty] string logNsdMinLength = "256";
30+
3131
public ComboBoxItem? SelectedAcquisitionTimebaseItem { get; set; }
3232
public ComboBoxItem? SelectedDataRateUnitItem { get; set; }
3333

@@ -37,7 +37,7 @@ public ComboBoxItem? SelectedNsdAlgorithm
3737
get => selectedNsdAlgorithm; set
3838
{
3939
selectedNsdAlgorithm = value;
40-
switch((string)selectedNsdAlgorithm.Content)
40+
switch ((string)selectedNsdAlgorithm.Content)
4141
{
4242
case "Logarithmic":
4343
AlgorithmLog = true;
@@ -65,8 +65,8 @@ public ComboBoxItem? SelectedNsdAlgorithm
6565
[ObservableProperty] bool algorithmLinStack = false; // Controls visibility of sub-stack panel
6666

6767
public ComboBoxItem? SelectedFileFormatItem { get; set; }
68-
public double XMin { get; set; } = 0.01;
69-
public double XMax { get; set; } = 10;
68+
public double XMin { get; set; } = 0.001;
69+
public double XMax { get; set; } = 100;
7070
public double YMin { get; set; } = 0.1;
7171
public double YMax { get; set; } = 100;
7272
public string WindowTitle { get { Version version = Assembly.GetExecutingAssembly().GetName().Version; return "NSD v" + version.Major + "." + version.Minor; } }
@@ -76,12 +76,35 @@ public ComboBoxItem? SelectedNsdAlgorithm
7676
private Settings settings;
7777

7878

79-
public MainWindowViewModel(Settings settings)
79+
public MainWindowViewModel(Settings settings, MainWindow window)
8080
{
8181
this.settings = settings;
8282
processWorkingFolder = settings.ProcessWorkingFolder;
8383
acquisitionTime = settings.AcquisitionTime;
84-
dataRate = settings.DataRate;
84+
85+
switch (settings.AcquisitionTimeUnit)
86+
{
87+
case "NPLC (50Hz)":
88+
window.cbTime.SelectedIndex = 0;
89+
break;
90+
case "NPLC (60Hz)":
91+
window.cbTime.SelectedIndex = 1;
92+
break;
93+
case "s":
94+
window.cbTime.SelectedIndex = 2;
95+
break;
96+
case "ms":
97+
window.cbTime.SelectedIndex = 3;
98+
break;
99+
case "μs":
100+
window.cbTime.SelectedIndex = 4;
101+
break;
102+
case "ns":
103+
window.cbTime.SelectedIndex = 5;
104+
break;
105+
}
106+
//dataRate = settings.DataRate;
107+
//SelectedDataRateUnitItem = settings.DataRateUnit;
85108
}
86109

87110
partial void OnProcessWorkingFolderChanged(string? value)

source/NSD.UI/NSD.UI.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<PropertyGroup>
33
<OutputType>WinExe</OutputType>
44
<TargetFramework>net8.0</TargetFramework>
5-
<Version>1.3</Version>
5+
<Version>1.4</Version>
66
<Nullable>enable</Nullable>
77
<InvariantGlobalization>true</InvariantGlobalization>
88
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>

source/NSD.UI/Settings.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public static Settings Default()
2525
{
2626
ProcessWorkingFolder = Directory.GetCurrentDirectory(),
2727
AcquisitionTime = "1",
28-
AcquisitionTimeUnit = "NPLC",
28+
AcquisitionTimeUnit = "NPLC (50Hz)",
2929
DataRate = "50",
3030
DataRateUnit = "Samples per second"
3131
};

source/NSD/NSD.cs

Lines changed: 101 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using System.Data;
2-
using System.Diagnostics;
32

43
namespace NSD
54
{
@@ -84,52 +83,93 @@ public static Spectrum StackedLinear(Memory<double> input, double sampleRate, in
8483
return new Spectrum() { Frequencies = outputFrequencies.ToArray(), Values = outputValues.ToArray(), Averages = averages, Stacking = widths.Count };
8584
}
8685

87-
public static Spectrum Log(Memory<double> input, double sampleRateHz, double freqMin, double freqMax, int pointsPerDecade, int minimumAverages)
86+
private record WelchGoertzelJob(double Frequency, int SpectrumLength, int CalculatedAverages);
87+
public static Spectrum Log(Memory<double> input, double sampleRateHz, double freqMin, double freqMax, int pointsPerDecade, int minimumAverages, int minimumLength)
8888
{
8989
if (freqMax <= freqMin)
9090
throw new ArgumentException("freqMax must be greater than freqMin");
91-
if (pointsPerDecade <= 0 || minimumAverages <= 0)
92-
throw new ArgumentException("pointsPerDecade, and minimumAverages must be positive");
91+
if (pointsPerDecade <= 0 || minimumAverages <= 0 || minimumLength <= 0)
92+
throw new ArgumentException("pointsPerDecade, minimumAverages, and minimumLength must be positive");
9393
if (sampleRateHz <= 0)
9494
throw new ArgumentException("sampleRateHz must be positive");
95-
if (freqMin < sampleRateHz / input.Length)
96-
freqMin = sampleRateHz / input.Length;
97-
if (freqMax > sampleRateHz / 2)
98-
freqMax = sampleRateHz / 2;
9995

10096
Windows.FTNI(1, out double optimumOverlap, out double NENBW);
10197
int firstUsableBinForWindow = (int)Math.Ceiling(NENBW);
102-
double decades = Math.Log10(freqMax / freqMin);
98+
99+
// To do:
100+
// For the purposes of the frequencies calculation, round freqMax/freqMin to nearest major decade line.
101+
// This ensures consistency of X-coordinate over various view widths.
102+
double decadeMin = RoundToDecade(freqMin, RoundingMode.Down);
103+
double decadeMax = RoundToDecade(freqMax, RoundingMode.Up);
104+
double decades = Math.Log10(decadeMax / decadeMin);
103105
int desiredNumberOfPoints = (int)(decades * pointsPerDecade) + 1; // + 1 to get points on the decade grid lines
104106

105-
double g = Math.Log(freqMax) - Math.Log(freqMin);
106-
double[] frequencies = Enumerable.Range(0, desiredNumberOfPoints).Select(j => freqMin * Math.Exp(j * g / (desiredNumberOfPoints - 1))).ToArray();
107+
double g = Math.Log(decadeMax) - Math.Log(decadeMin);
108+
double[] frequencies = Enumerable.Range(0, desiredNumberOfPoints).Select(j => decadeMin * Math.Exp(j * g / (desiredNumberOfPoints - 1))).ToArray();
107109
double[] spectrumResolution = frequencies.Select(freq => freq / firstUsableBinForWindow).ToArray();
108-
// spectrumResolution contains the 'desired resolutions' for each frequency bin, given the rule that we want the first usuable bin in the flat-top'd data.
110+
// spectrumResolution contains the 'desired resolutions' for each frequency bin, respecting the rule that we want the first usuable bin for the given window.
111+
112+
int[] spectrumLengths = spectrumResolution.Select(resolution => (int)Math.Round(sampleRateHz / resolution)).ToArray();
109113

110-
int[] spectrumLength = spectrumResolution.Select(val => (int)Math.Round(sampleRateHz / val)).ToArray(); // Segment lengths
111-
//double[] actualSpectrumResolution = spectrumLength.Select(val => sampleRateHz / val).ToArray(); // Actual resolution
112-
//double[] binNumber = frequencies.Select((val, index) => val / actualSpectrumResolution[index]).ToArray(); // Fourier tranform bin number (maybe validate that it doesn't deviate beyond +/-10%?)
113-
int[] estimatedAverages = spectrumLength.Select(val => (int)((input.Length - val) / (val * (1.0 - optimumOverlap)))).ToArray();
114+
// Create a job list of valid points to calculate
115+
double nyquistMax = sampleRateHz / 2;
116+
List<WelchGoertzelJob> jobs = [];
117+
for (int i = 0; i < frequencies.Length; i++)
118+
{
119+
if (frequencies[i] > nyquistMax)
120+
continue;
121+
if (TryCalculateAverages(input.Length, spectrumLengths[i], optimumOverlap, out var averages))
122+
{
123+
if (averages >= minimumAverages)
124+
{
125+
// Increase spectrum length until minimumLength is met, or averages drops below minimumAverages.
126+
// This increases the spectral resolution at the top end of the chart, allowing 50Hz spikes (& similar) to be more visible
127+
var spectrumLength = spectrumLengths[i];
128+
bool continueLoop = true;
129+
while (continueLoop)
130+
{
131+
if (spectrumLength < minimumLength && averages > minimumAverages)
132+
{
133+
var success = TryCalculateAverages(input.Length, spectrumLength * 2, optimumOverlap, out var newAverages);
134+
if (!success)
135+
break;
136+
if (averages > minimumAverages)
137+
{
138+
spectrumLength *= 2;
139+
averages = newAverages;
140+
continueLoop = true;
141+
}
142+
else
143+
{
144+
continueLoop = false;
145+
break;
146+
}
147+
148+
}
149+
else
150+
{
151+
continueLoop = false;
152+
}
153+
}
154+
jobs.Add(new WelchGoertzelJob(frequencies[i], spectrumLength, averages));
155+
}
156+
}
157+
}
114158

115159
var spectrum = new Dictionary<double, double>();
116-
var indices = Enumerable.Range(0, desiredNumberOfPoints).ToArray();
117-
for(int i = 0; i < frequencies.Length; i++)
160+
for (int i = 0; i < jobs.Count; i++)
118161
{
119-
spectrum[frequencies[i]] = double.NaN;
162+
spectrum[jobs[i].Frequency] = double.NaN;
120163
}
121164
object averageLock = new();
122165
int cumulativeAverage = 0;
123-
//for (int i = 0; i < desiredNumberOfPoints; i++)
124-
//foreach(var i in indices)
125-
Parallel.ForEach(indices, new ParallelOptions { MaxDegreeOfParallelism = 8 }, i =>
166+
//foreach(var job in jobs)
167+
Parallel.ForEach(jobs, new ParallelOptions { MaxDegreeOfParallelism = 8 }, job =>
126168
{
127-
if (estimatedAverages[i] < minimumAverages)
128-
return;
129-
var result = RunWelchGoertzel(input, spectrumLength[i], frequencies[i], sampleRateHz, out var actualAverages);
130-
if (estimatedAverages[i] != actualAverages)
131-
Debug.WriteLine($"{estimatedAverages[i]} {actualAverages}");
132-
spectrum[frequencies[i]] = result;
169+
var result = RunWelchGoertzel(input, job.SpectrumLength, job.Frequency, sampleRateHz, out var actualAverages);
170+
if (job.CalculatedAverages != actualAverages)
171+
throw new Exception("Actual averages does not match calculated averages");
172+
spectrum[job.Frequency] = result;
133173
lock (averageLock)
134174
{
135175
cumulativeAverage += actualAverages;
@@ -285,6 +325,39 @@ private static double S2(ReadOnlySpan<double> window)
285325
}
286326
return sumSquared;
287327
}
328+
329+
enum RoundingMode { Nearest, Up, Down }
330+
private static double RoundToDecade(double value, RoundingMode mode)
331+
{
332+
if (value <= 0)
333+
throw new ArgumentOutOfRangeException(nameof(value), "Value must be positive.");
334+
335+
double log10 = Math.Log10(value);
336+
double exponent = mode switch
337+
{
338+
RoundingMode.Nearest => Math.Round(log10),
339+
RoundingMode.Up => Math.Ceiling(log10),
340+
RoundingMode.Down => Math.Floor(log10),
341+
_ => throw new ArgumentOutOfRangeException(nameof(mode), "Invalid rounding mode.")
342+
};
343+
344+
return Math.Pow(10, exponent);
345+
}
346+
347+
private static bool TryCalculateAverages(int dataLength, int spectrumLength, double optimumOverlap, out int averages)
348+
{
349+
averages = 0;
350+
int overlap = (int)(spectrumLength * (1.0 - optimumOverlap));
351+
if (overlap < 1)
352+
return false;
353+
int endIndex = spectrumLength;
354+
while (endIndex < dataLength)
355+
{
356+
averages++;
357+
endIndex += overlap;
358+
}
359+
return true;
360+
}
288361
}
289362
}
290363

0 commit comments

Comments
 (0)