├── .github └── workflows │ └── build-binaries.yml ├── .gitignore ├── LICENSE ├── README.md ├── build-scripts ├── NSD.UI (linux-x64).bat └── NSD.UI (win-x64).bat ├── docs ├── v1.0.png └── v1.4.png └── source ├── NSD.Benchmarks ├── ComplexFftBenchmark.cs ├── FftTestData.cs ├── GoertzelFilter │ ├── GoertzelFilter1.cs │ └── GoertzelFilterBaseline.cs ├── GoertzelFilterBenchmark.cs ├── NSD.Benchmarks.csproj ├── Program.cs ├── README.md ├── RealFftBenchmark.cs ├── WindowBenchmark.cs └── Windows │ ├── Windows1.cs │ └── WindowsBaseline.cs ├── NSD.Generator ├── NSD.Generator.csproj └── Program.cs ├── NSD.UI ├── .gitignore ├── App.axaml ├── App.axaml.cs ├── LogDecadeMinorTickGenerator.cs ├── MainWindow.axaml ├── MainWindow.axaml.cs ├── MainWindowViewModel.cs ├── NSD.UI.csproj ├── Settings.cs └── runtimeconfig.template.json ├── NSD.sln └── NSD ├── Data types └── Spectrum.cs ├── FFT.cs ├── Filter.cs ├── GoertzelFilter.cs ├── NSD.cs ├── NSD.csproj ├── NsdProcessingException.cs ├── Papers ├── GH_FFT.pdf └── Improved spectrum estimation from digitized time series on a logaritmic frequency axis.pdf ├── Signals.cs └── Windows.cs /.github/workflows/build-binaries.yml: -------------------------------------------------------------------------------- 1 | name: Build binaries 2 | on: [push] 3 | jobs: 4 | build: 5 | runs-on: ${{ matrix.os }} 6 | strategy: 7 | matrix: 8 | dotnet-version: [ '8.0.x' ] 9 | os: [windows-latest] 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Setup .NET Core SDK ${{ matrix.dotnet-version }} 13 | uses: actions/setup-dotnet@v3 14 | with: 15 | dotnet-version: ${{ matrix.dotnet-version }} 16 | 17 | - name: Install dependencies 18 | run: dotnet restore 19 | working-directory: source 20 | 21 | - name: Run win-x64 build 22 | working-directory: build-scripts 23 | run: ./"NSD.UI (win-x64).bat" 24 | - name: Upload win-x64 build 25 | uses: actions/upload-artifact@v4 26 | with: 27 | name: NSD-win-x64 28 | path: builds/NSD.UI/win-x64/NSD-win-x64.exe 29 | 30 | - name: Run linux-x64 build 31 | working-directory: build-scripts 32 | run: ./"NSD.UI (linux-x64).bat" 33 | - name: Upload linux-x64 build 34 | uses: actions/upload-artifact@v4 35 | with: 36 | name: NSD-linux-x64 37 | path: builds/NSD.UI/linux-x64/NSD-linux-x64 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | 352 | builds 353 | tests -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 macaba 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NSD 2 | 3 | Cross-platform UI tool for estimating noise spectral density (NSD) from time-series data. 4 | 5 | This is intended to be an easy-to-use tool which allows anyone to quickly produce NSD charts that are directly comparable to other people's charts if they are produced by the same version of this tool. Customisation and configurability is very limited therefore power users will prefer their own scripts. 6 | 7 | 8 | 9 | ## Input file format 10 | 11 | Input file format is single column CSV with no header. 12 | 13 | Example: 14 | ``` 15 | -5.259715387375001E-07 16 | -4.895393397810999E-07 17 | -5.413877378806E-07 18 | -5.731182876255E-07 19 | -5.228528194452E-07 20 | ``` 21 | 22 | Multiple number formats supported, however not exhaustive. 23 | 24 | ## Using release binaries 25 | 26 | ### Windows 27 | 28 | It may need unblocking on properties dialog 29 | 30 | ![image](https://user-images.githubusercontent.com/1031306/184110097-253fddf3-4037-48ab-867a-c62fa61b87c4.png) 31 | 32 | ### Linux 33 | 34 | ``` 35 | sudo apt-get install -y libgdiplus 36 | chmod +x NSD.linux-x64 37 | ``` 38 | -------------------------------------------------------------------------------- /build-scripts/NSD.UI (linux-x64).bat: -------------------------------------------------------------------------------- 1 | dotnet publish ../source/NSD.UI -r linux-x64 -c Release --output ../builds/NSD.UI/linux-x64/ 2 | ren ..\builds\NSD.UI\linux-x64\NSD.UI NSD-linux-x64 -------------------------------------------------------------------------------- /build-scripts/NSD.UI (win-x64).bat: -------------------------------------------------------------------------------- 1 | dotnet publish ../source/NSD.UI -r win-x64 -c Release --output ../builds/NSD.UI/win-x64/ 2 | ren ..\builds\NSD.UI\win-x64\NSD.UI.exe NSD-win-x64.exe -------------------------------------------------------------------------------- /docs/v1.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macaba/NSD/5b9f022c3fcccb7cad7603d5cf9af9413af43637/docs/v1.0.png -------------------------------------------------------------------------------- /docs/v1.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macaba/NSD/5b9f022c3fcccb7cad7603d5cf9af9413af43637/docs/v1.4.png -------------------------------------------------------------------------------- /source/NSD.Benchmarks/ComplexFftBenchmark.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | using BenchmarkDotNet.Jobs; 3 | using System.Numerics; 4 | 5 | namespace NSD.Benchmarks 6 | { 7 | [SimpleJob(RuntimeMoniker.Net80)] 8 | [MemoryDiagnoser] 9 | [Orderer(BenchmarkDotNet.Order.SummaryOrderPolicy.FastestToSlowest)] 10 | public class ComplexFftBenchmark 11 | { 12 | private Complex[] values_FftFlat; 13 | private Complex[] values_FftSharp; 14 | private Complex[] values_MathNet; 15 | private Complex[] values_AelianFft; 16 | private Complex[] values_NWaveFft; 17 | 18 | private FftFlat.FastFourierTransform fftFlat; 19 | private NWaves.Transforms.Fft64 nWavesFft; 20 | 21 | private double[] values_NWavesFft_Real; 22 | private double[] values_NWavesFft_Img; 23 | 24 | private StreamWriter log; 25 | 26 | //[Params(256, 512, 1024, 2048, 4096, 8192)] 27 | [Params(8192)] 28 | public int Length; 29 | 30 | [GlobalSetup] 31 | public void Setup() 32 | { 33 | values_FftFlat = FftTestData.CreateComplex(Length); 34 | values_FftSharp = FftTestData.CreateComplex(Length); 35 | values_MathNet = FftTestData.CreateComplex(Length); 36 | values_AelianFft = FftTestData.CreateComplex(Length); 37 | values_NWaveFft = FftTestData.CreateComplex(Length); 38 | values_NWavesFft_Real = values_NWaveFft.Select(x => x.Real).ToArray(); 39 | values_NWavesFft_Img = values_NWaveFft.Select(x => x.Imaginary).ToArray(); 40 | 41 | fftFlat = new FftFlat.FastFourierTransform(Length); 42 | 43 | Aelian.FFT.FastFourierTransform.Initialize(); 44 | 45 | nWavesFft = new NWaves.Transforms.Fft64(Length); 46 | 47 | var logPath = Path.Combine("log" + Length + ".txt"); 48 | log = new StreamWriter(logPath); 49 | log.WriteLine("=== BEFORE ==="); 50 | log.WriteLine("FftFlat: " + GetMaxValue(values_FftFlat)); 51 | log.WriteLine("FftSharp: " + GetMaxValue(values_FftSharp)); 52 | log.WriteLine("MathNet: " + GetMaxValue(values_MathNet)); 53 | } 54 | 55 | [GlobalCleanup] 56 | public void Cleanup() 57 | { 58 | log.WriteLine("=== AFTER ==="); 59 | log.WriteLine("FftFlat: " + GetMaxValue(values_FftFlat)); 60 | log.WriteLine("FftSharp: " + GetMaxValue(values_FftSharp)); 61 | log.WriteLine("MathNet: " + GetMaxValue(values_MathNet)); 62 | log.Dispose(); 63 | } 64 | 65 | private static double GetMaxValue(Complex[] data) 66 | { 67 | return data.Select(x => Math.Max(Math.Abs(x.Real), Math.Abs(x.Imaginary))).Max(); 68 | } 69 | 70 | [Benchmark(Baseline = true)] 71 | public void FftFlat() 72 | { 73 | fftFlat.Forward(values_FftFlat); 74 | fftFlat.Inverse(values_FftFlat); 75 | } 76 | 77 | [Benchmark] 78 | public void FftSharp() 79 | { 80 | global::FftSharp.FFT.Forward(values_FftSharp); 81 | global::FftSharp.FFT.Inverse(values_FftSharp); 82 | } 83 | 84 | [Benchmark] 85 | public void MathNet() 86 | { 87 | global::MathNet.Numerics.IntegralTransforms.Fourier.Forward(values_MathNet, global::MathNet.Numerics.IntegralTransforms.FourierOptions.AsymmetricScaling); 88 | global::MathNet.Numerics.IntegralTransforms.Fourier.Inverse(values_MathNet, global::MathNet.Numerics.IntegralTransforms.FourierOptions.AsymmetricScaling); 89 | } 90 | 91 | [Benchmark] 92 | public void AelianFft() 93 | { 94 | Aelian.FFT.FastFourierTransform.FFT(values_AelianFft, true); 95 | Aelian.FFT.FastFourierTransform.FFT(values_AelianFft, false); 96 | } 97 | 98 | [Benchmark] 99 | public void NWaves() 100 | { 101 | nWavesFft.Direct(values_NWavesFft_Real, values_NWavesFft_Img); 102 | nWavesFft.Inverse(values_NWavesFft_Real, values_NWavesFft_Img); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /source/NSD.Benchmarks/FftTestData.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | 3 | namespace NSD.Benchmarks 4 | { 5 | public static class FftTestData 6 | { 7 | public static Complex[] CreateComplex(int length) 8 | { 9 | var random = new Random(42); 10 | var values = new Complex[length]; 11 | for (var i = 0; i < length; i++) 12 | { 13 | values[i] = new Complex(random.NextDouble(), random.NextDouble()); 14 | } 15 | 16 | return values; 17 | } 18 | 19 | public static double[] CreateDouble(int length) 20 | { 21 | var random = new Random(42); 22 | var values = new double[length]; 23 | for (var i = 0; i < length; i++) 24 | { 25 | values[i] = random.NextDouble(); 26 | } 27 | 28 | return values; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /source/NSD.Benchmarks/GoertzelFilter/GoertzelFilter1.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | 3 | namespace NSD 4 | { 5 | public class GoertzelFilter1 6 | { 7 | public double coeff; 8 | public double sine; 9 | public double cosine; 10 | 11 | public GoertzelFilter1(double filterFreq, double sampleFreq) 12 | { 13 | double w = 2 * Math.PI * (filterFreq / sampleFreq); 14 | double wr, wi; 15 | 16 | wr = Math.Cos(w); 17 | wi = Math.Sin(w); 18 | coeff = 2 * wr; 19 | cosine = wr; 20 | sine = wi; 21 | } 22 | 23 | public Complex Process(Span samples) 24 | { 25 | double sprev = 0.0; 26 | double sprev2 = 0.0; 27 | double s, imag, real; 28 | 29 | unsafe 30 | { 31 | fixed (double* samplesP = samples) 32 | { 33 | double* dataPointer = samplesP; 34 | double* endPointer = samplesP + samples.Length; 35 | while(dataPointer < endPointer) 36 | { 37 | s = *dataPointer + coeff * sprev - sprev2; 38 | sprev2 = sprev; 39 | sprev = s; 40 | dataPointer++; 41 | } 42 | } 43 | } 44 | 45 | real = sprev * cosine - sprev2; 46 | imag = -sprev * sine; 47 | 48 | return new Complex(real, imag); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /source/NSD.Benchmarks/GoertzelFilter/GoertzelFilterBaseline.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | 3 | namespace NSD 4 | { 5 | public class GoertzelFilter 6 | { 7 | public double coeff; 8 | public double sine; 9 | public double cosine; 10 | 11 | public GoertzelFilter(double filterFreq, double sampleFreq) 12 | { 13 | double w = 2 * Math.PI * (filterFreq / sampleFreq); 14 | double wr, wi; 15 | 16 | wr = Math.Cos(w); 17 | wi = Math.Sin(w); 18 | coeff = 2 * wr; 19 | cosine = wr; 20 | sine = wi; 21 | } 22 | 23 | public Complex Process(Span samples) 24 | { 25 | double sprev = 0.0; 26 | double sprev2 = 0.0; 27 | double s, imag, real; 28 | 29 | for (int n = 0; n < samples.Length; n++) 30 | { 31 | s = samples[n] + coeff * sprev - sprev2; 32 | sprev2 = sprev; 33 | sprev = s; 34 | } 35 | 36 | real = sprev * cosine - sprev2; 37 | imag = -sprev * sine; 38 | 39 | return new Complex(real, imag); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /source/NSD.Benchmarks/GoertzelFilterBenchmark.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | using BenchmarkDotNet.Jobs; 3 | 4 | namespace NSD.Benchmarks 5 | { 6 | [SimpleJob(RuntimeMoniker.Net80)] 7 | [MemoryDiagnoser] 8 | [Orderer(BenchmarkDotNet.Order.SummaryOrderPolicy.FastestToSlowest)] 9 | public class GoertzelFilterBenchmark 10 | { 11 | private double[] samples; 12 | 13 | [GlobalSetup] 14 | public void Setup() 15 | { 16 | samples = new double[10000000]; 17 | } 18 | 19 | [Benchmark(Baseline = true)] 20 | public void GoertzelFilterBaseline() 21 | { 22 | var filter = new GoertzelFilter(1000, 10000000); 23 | filter.Process(samples); 24 | } 25 | 26 | [Benchmark] 27 | public void GoertzelFilter1() 28 | { 29 | var filter = new GoertzelFilter1(1000, 10000000); 30 | filter.Process(samples); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /source/NSD.Benchmarks/NSD.Benchmarks.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /source/NSD.Benchmarks/Program.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Configs; 2 | using BenchmarkDotNet.Running; 3 | using NSD.Benchmarks; 4 | 5 | DefaultConfig.Instance.WithOptions(ConfigOptions.JoinSummary); 6 | //_ = BenchmarkRunner.Run(); 7 | //_ = BenchmarkRunner.Run(); 8 | //_ = BenchmarkRunner.Run(); 9 | _ = BenchmarkRunner.Run(); 10 | Console.ReadKey(); -------------------------------------------------------------------------------- /source/NSD.Benchmarks/README.md: -------------------------------------------------------------------------------- 1 | Complex: 2 | 3 | | Method | Length | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | 4 | |---------- |------- |----------:|---------:|---------:|------:|--------:|-------:|----------:| 5 | | FftFlat | 8192 | 81.43 μs | 0.242 μs | 0.215 μs | 1.00 | 0.00 | - | - | 6 | | AelianFft | 8192 | 154.61 μs | 0.142 μs | 0.133 μs | 1.90 | 0.01 | - | - | 7 | | MathNet | 8192 | 330.86 μs | 0.500 μs | 0.390 μs | 4.06 | 0.01 | 3.9063 | 50138 B | 8 | | NWaves | 8192 | 389.83 μs | 1.076 μs | 1.007 μs | 4.79 | 0.02 | - | - | 9 | | FftSharp | 8192 | 980.78 μs | 6.862 μs | 6.419 μs | 12.04 | 0.08 | - | - | 10 | 11 | Real: 12 | 13 | | Method | Length | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Gen1 | Gen2 | Allocated | 14 | |---------- |------- |------------:|----------:|---------:|------:|--------:|--------:|--------:|--------:|----------:| 15 | | FftFlat | 8192 | 48.37 μs | 0.640 μs | 0.599 μs | 1.00 | 0.02 | - | - | - | - | 16 | | AelianFft | 8192 | 79.49 μs | 0.962 μs | 0.900 μs | 1.64 | 0.03 | - | - | - | - | 17 | | NWaves | 8192 | 172.38 μs | 1.866 μs | 1.654 μs | 3.56 | 0.05 | - | - | - | - | 18 | | MathNet | 8192 | 357.23 μs | 4.862 μs | 4.548 μs | 7.39 | 0.13 | 38.0859 | 34.1797 | 34.1797 | 314351 B | 19 | | FftSharp | 8192 | 1,048.16 μs | 10.268 μs | 9.102 μs | 21.67 | 0.32 | 41.0156 | 33.2031 | 31.2500 | 393339 B | 20 | -------------------------------------------------------------------------------- /source/NSD.Benchmarks/RealFftBenchmark.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | using BenchmarkDotNet.Jobs; 3 | 4 | namespace NSD.Benchmarks 5 | { 6 | [SimpleJob(RuntimeMoniker.Net80)] 7 | [MemoryDiagnoser] 8 | [Orderer(BenchmarkDotNet.Order.SummaryOrderPolicy.FastestToSlowest)] 9 | public class RealFftBenchmark 10 | { 11 | private double[] values_FftFlat; 12 | private double[] values_FftSharp; 13 | private double[] values_MathNet; 14 | private double[] values_AelianFft; 15 | private double[] values_NWaves; 16 | private double[] values_NWaves_Real; 17 | private double[] values_NWaves_Img; 18 | 19 | private FftFlat.FastFourierTransform fftFlat; 20 | private FftFlat.RealFourierTransform fftFlatReal; 21 | private NWaves.Transforms.RealFft64 nWavesFft; 22 | 23 | private StreamWriter log; 24 | 25 | //[Params(256, 512, 1024, 2048, 4096, 8192)] 26 | [Params(8192)] 27 | public int Length; 28 | 29 | [GlobalSetup] 30 | public void Setup() 31 | { 32 | values_FftFlat = FftTestData.CreateDouble(Length).Append(0.0).Append(0.0).ToArray(); 33 | values_FftSharp = FftTestData.CreateDouble(Length).ToArray(); 34 | values_MathNet = FftTestData.CreateDouble(Length).Append(0.0).Append(0.0).ToArray(); 35 | values_AelianFft = FftTestData.CreateDouble(Length).ToArray(); 36 | values_NWaves = FftTestData.CreateDouble(Length).ToArray(); 37 | values_NWaves_Real = new double[Length]; 38 | values_NWaves_Img = new double[Length]; 39 | 40 | fftFlatReal = new FftFlat.RealFourierTransform(Length); 41 | 42 | Aelian.FFT.FastFourierTransform.Initialize(); 43 | 44 | nWavesFft = new NWaves.Transforms.RealFft64(Length); 45 | 46 | var logPath = Path.Combine("log" + Length + ".txt"); 47 | log = new StreamWriter(logPath); 48 | log.WriteLine("=== BEFORE ==="); 49 | log.WriteLine("FftFlatReal: " + GetMaxValue(values_FftFlat)); 50 | log.WriteLine("FftSharpReal: " + GetMaxValue(values_FftSharp)); 51 | log.WriteLine("MathNetReal: " + GetMaxValue(values_MathNet)); 52 | } 53 | 54 | [GlobalCleanup] 55 | public void Cleanup() 56 | { 57 | log.WriteLine("=== AFTER ==="); 58 | log.WriteLine("FftFlatReal: " + GetMaxValue(values_FftFlat)); 59 | log.WriteLine("FftSharpReal: " + GetMaxValue(values_FftSharp)); 60 | log.WriteLine("MathNetReal: " + GetMaxValue(values_MathNet)); 61 | log.Dispose(); 62 | } 63 | 64 | private static double GetMaxValue(double[] data) 65 | { 66 | return data.Select(x => Math.Abs(x)).Max(); 67 | } 68 | 69 | [Benchmark(Baseline = true)] 70 | public void FftFlat() 71 | { 72 | var spectrum = fftFlatReal.Forward(values_FftFlat); 73 | fftFlatReal.Inverse(spectrum); 74 | } 75 | 76 | [Benchmark] 77 | public void FftSharp() 78 | { 79 | var spectrum = global::FftSharp.FFT.ForwardReal(values_FftSharp); 80 | values_FftSharp = global::FftSharp.FFT.InverseReal(spectrum); 81 | } 82 | 83 | [Benchmark] 84 | public void MathNet() 85 | { 86 | global::MathNet.Numerics.IntegralTransforms.Fourier.ForwardReal(values_MathNet, Length, global::MathNet.Numerics.IntegralTransforms.FourierOptions.AsymmetricScaling); 87 | global::MathNet.Numerics.IntegralTransforms.Fourier.InverseReal(values_MathNet, Length, global::MathNet.Numerics.IntegralTransforms.FourierOptions.AsymmetricScaling); 88 | } 89 | 90 | [Benchmark] 91 | public void AelianFft() 92 | { 93 | Aelian.FFT.FastFourierTransform.RealFFT(values_AelianFft, true); 94 | Aelian.FFT.FastFourierTransform.RealFFT(values_AelianFft, false); 95 | } 96 | 97 | [Benchmark] 98 | public void NWaves() 99 | { 100 | nWavesFft.Direct(values_NWaves, values_NWaves_Real, values_NWaves_Img); 101 | nWavesFft.Inverse(values_NWaves, values_NWaves_Real, values_NWaves_Img); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /source/NSD.Benchmarks/WindowBenchmark.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | using BenchmarkDotNet.Jobs; 3 | 4 | namespace NSD.Benchmarks 5 | { 6 | [SimpleJob(RuntimeMoniker.Net80)] 7 | [MemoryDiagnoser] 8 | [Orderer(BenchmarkDotNet.Order.SummaryOrderPolicy.FastestToSlowest)] 9 | public class WindowBenchmark 10 | { 11 | [Benchmark(Baseline = true)] 12 | public void GoertzelFilterBaseline() 13 | { 14 | Windows.FTNI(100000, out _, out _); 15 | } 16 | 17 | [Benchmark] 18 | public void GoertzelFilter1() 19 | { 20 | Windows1.FTNI(100000, out _, out _); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /source/NSD.Benchmarks/Windows/Windows1.cs: -------------------------------------------------------------------------------- 1 | namespace NSD 2 | { 3 | public static class Windows1 4 | { 5 | /// Optimum overlap in ratio of width, used for Welch method 6 | /// Normalized Equivalent Noise BandWidth with unit of bins 7 | public static Memory FTNI(int width, out double optimumOverlap, out double NENBW) 8 | { 9 | // FTNI - https://holometer.fnal.gov/GH_FFT.pdf 10 | // wj = 0.2810639 − 0.5208972 cos(z) + 0.1980399 cos(2z). 11 | optimumOverlap = 0.656; 12 | NENBW = 2.9656; 13 | Memory window = new double[width]; 14 | int i = 0; 15 | unsafe 16 | { 17 | fixed (double* windowP = window.Span) 18 | { 19 | double* dataPointer = windowP; 20 | double* endPointer = windowP + window.Length; 21 | while (dataPointer < endPointer) 22 | { 23 | double z = (2.0 * Math.PI * i) / width; 24 | double wj = 0.2810639 - (0.5208972 * Math.Cos(z)) + (0.1980399 * Math.Cos(2 * z)); 25 | *dataPointer = wj; 26 | dataPointer++; 27 | i++; 28 | } 29 | } 30 | } 31 | return window; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /source/NSD.Benchmarks/Windows/WindowsBaseline.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NSD 4 | { 5 | public static class Windows 6 | { 7 | /// Optimum overlap in ratio of width, used for Welch method 8 | /// Normalized Equivalent Noise BandWidth with unit of bins 9 | public static Memory HFT95(int width, out double optimumOverlap, out double NENBW) 10 | { 11 | // HFT95 - https://holometer.fnal.gov/GH_FFT.pdf 12 | // wj = 1 − 1.9383379 cos(z) + 1.3045202 cos(2z) − 0.4028270 cos(3z) + 0.0350665 cos(4z). 13 | optimumOverlap = 0.756; 14 | NENBW = 3.8112; 15 | Memory window = new double[width]; 16 | for (int i = 0; i < width; i++) 17 | { 18 | double z = (2.0 * Math.PI * i) / width; 19 | double wj = 1 - (1.9383379 * Math.Cos(z)) + (1.3045202 * Math.Cos(2 * z)) - (0.4028270 * Math.Cos(3 * z)) + (0.0350665 * Math.Cos(4 * z)); 20 | window.Span[i] = wj; 21 | } 22 | return window; 23 | } 24 | 25 | /// Optimum overlap in ratio of width, used for Welch method 26 | /// Normalized Equivalent Noise BandWidth with unit of bins 27 | public static Memory HFT90D(int width, out double optimumOverlap, out double NENBW) 28 | { 29 | // HFT90D - https://holometer.fnal.gov/GH_FFT.pdf 30 | // wj = 1 − 1.942604 cos(z) + 1.340318 cos(2z) − 0.440811 cos(3z) + 0.043097 cos(4z). 31 | optimumOverlap = 0.76; 32 | NENBW = 3.8832; 33 | Memory window = new double[width]; 34 | for (int i = 0; i < width; i++) 35 | { 36 | double z = (2.0 * Math.PI * i) / width; 37 | double wj = 1 - (1.942604 * Math.Cos(z)) + (1.340318 * Math.Cos(2 * z)) - (0.440811 * Math.Cos(3 * z)) + (0.043097 * Math.Cos(4 * z)); 38 | window.Span[i] = wj; 39 | } 40 | 41 | return window; 42 | } 43 | 44 | /// Optimum overlap in ratio of width, used for Welch method 45 | /// Normalized Equivalent Noise BandWidth with unit of bins 46 | public static Memory FTNI(int width, out double optimumOverlap, out double NENBW) 47 | { 48 | // FTNI - https://holometer.fnal.gov/GH_FFT.pdf 49 | // wj = 0.2810639 − 0.5208972 cos(z) + 0.1980399 cos(2z). 50 | optimumOverlap = 0.656; 51 | NENBW = 2.9656; 52 | Memory window = new double[width]; 53 | for (int i = 0; i < width; i++) 54 | { 55 | double z = (2.0 * Math.PI * i) / width; 56 | double wj = 0.2810639 - (0.5208972 * Math.Cos(z)) + (0.1980399 * Math.Cos(2 * z)); 57 | window.Span[i] = wj; 58 | } 59 | return window; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /source/NSD.Generator/NSD.Generator.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /source/NSD.Generator/Program.cs: -------------------------------------------------------------------------------- 1 | using CsvHelper; 2 | using NSD; 3 | using System.Globalization; 4 | 5 | //var noise = Signals.WhiteNoise(1000000, 50, 1e-9); 6 | var noise = Signals.PowerLawGaussian(100000, 1, 1); 7 | 8 | using (StreamWriter writer = new(@$"1nV 1f 50SPS.csv")) 9 | using (CsvWriter csvWrite = new(writer, CultureInfo.InvariantCulture)) 10 | { 11 | foreach (var sample in noise.Span) 12 | { 13 | csvWrite.WriteField(sample); 14 | csvWrite.NextRecord(); 15 | } 16 | } -------------------------------------------------------------------------------- /source/NSD.UI/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # Tye 66 | .tye/ 67 | 68 | # ASP.NET Scaffolding 69 | ScaffoldingReadMe.txt 70 | 71 | # StyleCop 72 | StyleCopReport.xml 73 | 74 | # Files built by Visual Studio 75 | *_i.c 76 | *_p.c 77 | *_h.h 78 | *.ilk 79 | *.meta 80 | *.obj 81 | *.iobj 82 | *.pch 83 | *.pdb 84 | *.ipdb 85 | *.pgc 86 | *.pgd 87 | *.rsp 88 | *.sbr 89 | *.tlb 90 | *.tli 91 | *.tlh 92 | *.tmp 93 | *.tmp_proj 94 | *_wpftmp.csproj 95 | *.log 96 | *.vspscc 97 | *.vssscc 98 | .builds 99 | *.pidb 100 | *.svclog 101 | *.scc 102 | 103 | # Chutzpah Test files 104 | _Chutzpah* 105 | 106 | # Visual C++ cache files 107 | ipch/ 108 | *.aps 109 | *.ncb 110 | *.opendb 111 | *.opensdf 112 | *.sdf 113 | *.cachefile 114 | *.VC.db 115 | *.VC.VC.opendb 116 | 117 | # Visual Studio profiler 118 | *.psess 119 | *.vsp 120 | *.vspx 121 | *.sap 122 | 123 | # Visual Studio Trace Files 124 | *.e2e 125 | 126 | # TFS 2012 Local Workspace 127 | $tf/ 128 | 129 | # Guidance Automation Toolkit 130 | *.gpState 131 | 132 | # ReSharper is a .NET coding add-in 133 | _ReSharper*/ 134 | *.[Rr]e[Ss]harper 135 | *.DotSettings.user 136 | 137 | # TeamCity is a build add-in 138 | _TeamCity* 139 | 140 | # DotCover is a Code Coverage Tool 141 | *.dotCover 142 | 143 | # AxoCover is a Code Coverage Tool 144 | .axoCover/* 145 | !.axoCover/settings.json 146 | 147 | # Coverlet is a free, cross platform Code Coverage Tool 148 | coverage*.json 149 | coverage*.xml 150 | coverage*.info 151 | 152 | # Visual Studio code coverage results 153 | *.coverage 154 | *.coveragexml 155 | 156 | # NCrunch 157 | _NCrunch_* 158 | .*crunch*.local.xml 159 | nCrunchTemp_* 160 | 161 | # MightyMoose 162 | *.mm.* 163 | AutoTest.Net/ 164 | 165 | # Web workbench (sass) 166 | .sass-cache/ 167 | 168 | # Installshield output folder 169 | [Ee]xpress/ 170 | 171 | # DocProject is a documentation generator add-in 172 | DocProject/buildhelp/ 173 | DocProject/Help/*.HxT 174 | DocProject/Help/*.HxC 175 | DocProject/Help/*.hhc 176 | DocProject/Help/*.hhk 177 | DocProject/Help/*.hhp 178 | DocProject/Help/Html2 179 | DocProject/Help/html 180 | 181 | # Click-Once directory 182 | publish/ 183 | 184 | # Publish Web Output 185 | *.[Pp]ublish.xml 186 | *.azurePubxml 187 | # Note: Comment the next line if you want to checkin your web deploy settings, 188 | # but database connection strings (with potential passwords) will be unencrypted 189 | *.pubxml 190 | *.publishproj 191 | 192 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 193 | # checkin your Azure Web App publish settings, but sensitive information contained 194 | # in these scripts will be unencrypted 195 | PublishScripts/ 196 | 197 | # NuGet Packages 198 | *.nupkg 199 | # NuGet Symbol Packages 200 | *.snupkg 201 | # The packages folder can be ignored because of Package Restore 202 | **/[Pp]ackages/* 203 | # except build/, which is used as an MSBuild target. 204 | !**/[Pp]ackages/build/ 205 | # Uncomment if necessary however generally it will be regenerated when needed 206 | #!**/[Pp]ackages/repositories.config 207 | # NuGet v3's project.json files produces more ignorable files 208 | *.nuget.props 209 | *.nuget.targets 210 | 211 | # Microsoft Azure Build Output 212 | csx/ 213 | *.build.csdef 214 | 215 | # Microsoft Azure Emulator 216 | ecf/ 217 | rcf/ 218 | 219 | # Windows Store app package directories and files 220 | AppPackages/ 221 | BundleArtifacts/ 222 | Package.StoreAssociation.xml 223 | _pkginfo.txt 224 | *.appx 225 | *.appxbundle 226 | *.appxupload 227 | 228 | # Visual Studio cache files 229 | # files ending in .cache can be ignored 230 | *.[Cc]ache 231 | # but keep track of directories ending in .cache 232 | !?*.[Cc]ache/ 233 | 234 | # Others 235 | ClientBin/ 236 | ~$* 237 | *~ 238 | *.dbmdl 239 | *.dbproj.schemaview 240 | *.jfm 241 | *.pfx 242 | *.publishsettings 243 | orleans.codegen.cs 244 | 245 | # Including strong name files can present a security risk 246 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 247 | #*.snk 248 | 249 | # Since there are multiple workflows, uncomment next line to ignore bower_components 250 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 251 | #bower_components/ 252 | 253 | # RIA/Silverlight projects 254 | Generated_Code/ 255 | 256 | # Backup & report files from converting an old project file 257 | # to a newer Visual Studio version. Backup files are not needed, 258 | # because we have git ;-) 259 | _UpgradeReport_Files/ 260 | Backup*/ 261 | UpgradeLog*.XML 262 | UpgradeLog*.htm 263 | ServiceFabricBackup/ 264 | *.rptproj.bak 265 | 266 | # SQL Server files 267 | *.mdf 268 | *.ldf 269 | *.ndf 270 | 271 | # Business Intelligence projects 272 | *.rdl.data 273 | *.bim.layout 274 | *.bim_*.settings 275 | *.rptproj.rsuser 276 | *- [Bb]ackup.rdl 277 | *- [Bb]ackup ([0-9]).rdl 278 | *- [Bb]ackup ([0-9][0-9]).rdl 279 | 280 | # Microsoft Fakes 281 | FakesAssemblies/ 282 | 283 | # GhostDoc plugin setting file 284 | *.GhostDoc.xml 285 | 286 | # Node.js Tools for Visual Studio 287 | .ntvs_analysis.dat 288 | node_modules/ 289 | 290 | # Visual Studio 6 build log 291 | *.plg 292 | 293 | # Visual Studio 6 workspace options file 294 | *.opt 295 | 296 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 297 | *.vbw 298 | 299 | # Visual Studio LightSwitch build output 300 | **/*.HTMLClient/GeneratedArtifacts 301 | **/*.DesktopClient/GeneratedArtifacts 302 | **/*.DesktopClient/ModelManifest.xml 303 | **/*.Server/GeneratedArtifacts 304 | **/*.Server/ModelManifest.xml 305 | _Pvt_Extensions 306 | 307 | # Paket dependency manager 308 | .paket/paket.exe 309 | paket-files/ 310 | 311 | # FAKE - F# Make 312 | .fake/ 313 | 314 | # CodeRush personal settings 315 | .cr/personal 316 | 317 | # Python Tools for Visual Studio (PTVS) 318 | __pycache__/ 319 | *.pyc 320 | 321 | # Cake - Uncomment if you are using it 322 | # tools/** 323 | # !tools/packages.config 324 | 325 | # Tabs Studio 326 | *.tss 327 | 328 | # Telerik's JustMock configuration file 329 | *.jmconfig 330 | 331 | # BizTalk build output 332 | *.btp.cs 333 | *.btm.cs 334 | *.odx.cs 335 | *.xsd.cs 336 | 337 | # OpenCover UI analysis results 338 | OpenCover/ 339 | 340 | # Azure Stream Analytics local run output 341 | ASALocalRun/ 342 | 343 | # MSBuild Binary and Structured Log 344 | *.binlog 345 | 346 | # NVidia Nsight GPU debugger configuration file 347 | *.nvuser 348 | 349 | # MFractors (Xamarin productivity tool) working folder 350 | .mfractor/ 351 | 352 | # Local History for Visual Studio 353 | .localhistory/ 354 | 355 | # BeatPulse healthcheck temp database 356 | healthchecksdb 357 | 358 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 359 | MigrationBackup/ 360 | 361 | # Ionide (cross platform F# VS Code tools) working folder 362 | .ionide/ 363 | 364 | # Fody - auto-generated XML schema 365 | FodyWeavers.xsd 366 | 367 | ## 368 | ## Visual studio for Mac 369 | ## 370 | 371 | 372 | # globs 373 | Makefile.in 374 | *.userprefs 375 | *.usertasks 376 | config.make 377 | config.status 378 | aclocal.m4 379 | install-sh 380 | autom4te.cache/ 381 | *.tar.gz 382 | tarballs/ 383 | test-results/ 384 | 385 | # Mac bundle stuff 386 | *.dmg 387 | *.app 388 | 389 | # content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore 390 | # General 391 | .DS_Store 392 | .AppleDouble 393 | .LSOverride 394 | 395 | # Icon must end with two \r 396 | Icon 397 | 398 | 399 | # Thumbnails 400 | ._* 401 | 402 | # Files that might appear in the root of a volume 403 | .DocumentRevisions-V100 404 | .fseventsd 405 | .Spotlight-V100 406 | .TemporaryItems 407 | .Trashes 408 | .VolumeIcon.icns 409 | .com.apple.timemachine.donotpresent 410 | 411 | # Directories potentially created on remote AFP share 412 | .AppleDB 413 | .AppleDesktop 414 | Network Trash Folder 415 | Temporary Items 416 | .apdisk 417 | 418 | # content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore 419 | # Windows thumbnail cache files 420 | Thumbs.db 421 | ehthumbs.db 422 | ehthumbs_vista.db 423 | 424 | # Dump file 425 | *.stackdump 426 | 427 | # Folder config file 428 | [Dd]esktop.ini 429 | 430 | # Recycle Bin used on file shares 431 | $RECYCLE.BIN/ 432 | 433 | # Windows Installer files 434 | *.cab 435 | *.msi 436 | *.msix 437 | *.msm 438 | *.msp 439 | 440 | # Windows shortcuts 441 | *.lnk 442 | 443 | # JetBrains Rider 444 | .idea/ 445 | *.sln.iml 446 | 447 | ## 448 | ## Visual Studio Code 449 | ## 450 | .vscode/* 451 | !.vscode/settings.json 452 | !.vscode/tasks.json 453 | !.vscode/launch.json 454 | !.vscode/extensions.json 455 | -------------------------------------------------------------------------------- /source/NSD.UI/App.axaml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /source/NSD.UI/App.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls.ApplicationLifetimes; 3 | using Avalonia.Markup.Xaml; 4 | using MsBox.Avalonia; 5 | using MsBox.Avalonia.Enums; 6 | using System; 7 | using System.IO; 8 | using System.Threading.Tasks; 9 | 10 | namespace NSD.UI 11 | { 12 | public partial class App : Application 13 | { 14 | public override void Initialize() 15 | { 16 | AvaloniaXamlLoader.Load(this); 17 | SetupExceptionHandling(); 18 | } 19 | 20 | private void SetupExceptionHandling() 21 | { 22 | AppDomain.CurrentDomain.UnhandledException += (s, e) => 23 | LogUnhandledException((Exception)e.ExceptionObject, "AppDomain.CurrentDomain.UnhandledException"); 24 | 25 | TaskScheduler.UnobservedTaskException += (s, e) => 26 | { 27 | LogUnhandledException(e.Exception, "TaskScheduler.UnobservedTaskException"); 28 | e.SetObserved(); 29 | }; 30 | } 31 | 32 | private async void LogUnhandledException(Exception exception, string source) 33 | { 34 | string message = $"Unhandled exception:\n\n{exception}\n"; 35 | File.AppendAllText("error.txt", message); 36 | } 37 | 38 | public override void OnFrameworkInitializationCompleted() 39 | { 40 | if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) 41 | desktop.MainWindow = new MainWindow(); 42 | base.OnFrameworkInitializationCompleted(); 43 | } 44 | 45 | public static int Main(string[] args) 46 | => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); 47 | 48 | public static AppBuilder BuildAvaloniaApp() 49 | => AppBuilder.Configure() 50 | .UsePlatformDetect() 51 | .WithInterFont() 52 | .LogToTrace(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /source/NSD.UI/LogDecadeMinorTickGenerator.cs: -------------------------------------------------------------------------------- 1 | using ScottPlot; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace NSD.UI 7 | { 8 | public class LogDecadeMinorTickGenerator : IMinorTickGenerator 9 | { 10 | public int TicksPerDecade { get; set; } = 10; 11 | 12 | public IEnumerable GetMinorTicks(double[] majorPositions, CoordinateRange visibleRange) 13 | { 14 | var minDecadeTick = Math.Floor(visibleRange.Min); 15 | var maxDecadeTick = Math.Ceiling(visibleRange.Max); 16 | 17 | if(minDecadeTick == maxDecadeTick) 18 | return []; 19 | 20 | // pre-calculate the log-distributed offset positions between major ticks 21 | IEnumerable minorTickOffsets = Enumerable.Range(1, TicksPerDecade - 1) 22 | .Select(x => Math.Log10(x * 10 / TicksPerDecade)); 23 | 24 | // iterate major ticks and collect minor ticks with offsets 25 | List minorTicks = []; 26 | for (double major = minDecadeTick; major <= maxDecadeTick; major += 1) 27 | { 28 | minorTicks.AddRange(minorTickOffsets.Select(offset => major + offset)); 29 | } 30 | 31 | return minorTicks; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /source/NSD.UI/MainWindow.axaml: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | CSV has header? 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | NPLC (50Hz) 62 | NPLC (60Hz) 63 | s 64 | ms 65 | μs 66 | ns 67 | SPS 68 | kSPS 69 | MSPS 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | Logarithmic 80 | Linear 81 | Linear stacking 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 2 95 | 4 96 | 8 97 | 16 98 | 32 99 | 64 100 | 128 101 | 256 102 | 512 103 | 1024 104 | 105 | 106 | 107 | 108 | 109 | 64 110 | 128 111 | 256 112 | 512 113 | 1024 114 | 2048 115 | 4096 116 | 8192 117 | 16384 118 | 32768 119 | 65536 120 | 131072 121 | 262144 122 | 524288 123 | 1048576 124 | 125 | 126 | 127 | 128 | 129 | 64 130 | 128 131 | 256 132 | 512 133 | 1024 134 | 2048 135 | 4096 136 | 8192 137 | 16384 138 | 32768 139 | 65536 140 | 131072 141 | 262144 142 | 524288 143 | 1048576 144 | 145 | 146 | 147 | 64 148 | 128 149 | 256 150 | 512 151 | 1024 152 | 2048 153 | 4096 154 | 8192 155 | 16384 156 | 32768 157 | 65536 158 | 131072 159 | 262144 160 | 524288 161 | 162 | 163 | 164 | 165 | 166 | 167 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | -------------------------------------------------------------------------------- /source/NSD.UI/MainWindow.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using Avalonia.Interactivity; 3 | using CsvHelper.Configuration; 4 | using MsBox.Avalonia; 5 | using ScottPlot.TickGenerators; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Globalization; 9 | using System.IO; 10 | using System.Linq; 11 | using System.Runtime.InteropServices; 12 | using System.Text.RegularExpressions; 13 | using System.Threading.Tasks; 14 | 15 | namespace NSD.UI 16 | { 17 | public partial class MainWindow : Window 18 | { 19 | private readonly MainWindowViewModel viewModel; 20 | private Spectrum spectrum = new(); 21 | private Settings settings; 22 | 23 | public MainWindow() 24 | { 25 | InitializeComponent(); 26 | settings = Settings.Load(); 27 | viewModel = new(settings, this); 28 | DataContext = viewModel; 29 | InitNsdChart(); 30 | } 31 | 32 | public async void BtnSearch_Click(object sender, RoutedEventArgs e) 33 | { 34 | if (!Directory.Exists(viewModel.ProcessWorkingFolder)) 35 | { 36 | var messageBoxStandardWindow = MessageBoxManager.GetMessageBoxStandard("Folder not found", "Search folder not found"); 37 | await messageBoxStandardWindow.ShowAsync(); 38 | return; 39 | } 40 | 41 | viewModel.InputFilePaths.Clear(); 42 | viewModel.InputFileNames.Clear(); 43 | var files = Directory.EnumerateFiles(viewModel.ProcessWorkingFolder, "*.csv"); 44 | foreach (var file in files) 45 | { 46 | viewModel.InputFilePaths.Add(file); 47 | viewModel.InputFileNames.Add(Path.GetFileName(file)); 48 | } 49 | files = Directory.EnumerateFiles(viewModel.ProcessWorkingFolder, "*.f32"); // Hidden functionality that supports F32 bin files 50 | foreach (var file in files) 51 | { 52 | viewModel.InputFilePaths.Add(file); 53 | viewModel.InputFileNames.Add(Path.GetFileName(file)); 54 | } 55 | 56 | viewModel.SelectedInputFileIndex = 0; 57 | } 58 | 59 | public async void btnRun_Click(object sender, RoutedEventArgs e) 60 | { 61 | viewModel.Enabled = false; 62 | try 63 | { 64 | var path = viewModel.GetSelectedInputFilePath(); 65 | if (!File.Exists(path)) 66 | { 67 | viewModel.Status = "Error: Input CSV file not found"; 68 | return; 69 | } 70 | if (!double.TryParse(viewModel.AcquisitionTime, out double acquisitionTime)) 71 | { 72 | viewModel.Status = "Error: Invalid acquisition time value"; 73 | return; 74 | } 75 | double acquisitionTimeSeconds = (string)(viewModel.SelectedAcquisitionTimebaseItem).Content switch 76 | { 77 | "NPLC (50Hz)" => acquisitionTime * (1.0 / 50.0), 78 | "NPLC (60Hz)" => acquisitionTime * (1.0 / 60.0), 79 | "s" => acquisitionTime, 80 | "ms" => acquisitionTime / 1e3, 81 | "μs" => acquisitionTime / 1e6, 82 | "ns" => acquisitionTime / 1e9, 83 | "SPS" => 1.0 / acquisitionTime, 84 | "kSPS" => 1.0 / (acquisitionTime * 1000.0), 85 | "MSPS" => 1.0 / (acquisitionTime * 1000000.0), 86 | _ => throw new ApplicationException("Acquisition time combobox value not handled") 87 | }; 88 | //if (!double.TryParse(viewModel.DataRate, out double dataRateTime)) 89 | //{ 90 | // viewModel.Status = "Error: Invalid data rate value"; 91 | // return; 92 | //} 93 | //double dataRateTimeSeconds = (string)viewModel.SelectedDataRateUnitItem.Content switch 94 | //{ 95 | // "Samples per second" => 1.0 / dataRateTime, 96 | // "Seconds per sample" => dataRateTime, 97 | // _ => throw new ApplicationException("Data rate combobox value not handled") 98 | //}; 99 | 100 | switch ((string)viewModel.SelectedNsdAlgorithm.Content) 101 | { 102 | case "Logarithmic": 103 | break; 104 | case "Linear": 105 | break; 106 | case "Linear stacking": 107 | { 108 | var fftWidth = int.Parse((string)viewModel.SelectedLinearStackingLengthItem.Content); 109 | var stackingFftWidth = int.Parse((string)viewModel.SelectedLinearStackingMinLengthItem.Content); 110 | if (stackingFftWidth >= fftWidth) 111 | { 112 | viewModel.Status = "Error: Invalid minimum stacking FFT width"; 113 | return; 114 | } 115 | break; 116 | } 117 | } 118 | 119 | if (!double.TryParse(viewModel.InputScaling, out double inputScaling)) 120 | { 121 | viewModel.Status = "Error: Invalid input scaling value"; 122 | return; 123 | } 124 | 125 | viewModel.Status = "Status: Loading CSV..."; 126 | 127 | using var stream = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); 128 | List records = []; 129 | DateTimeOffset fileParseStart = DateTimeOffset.UtcNow; 130 | DateTimeOffset fileParseFinish = DateTimeOffset.UtcNow; 131 | 132 | switch (Path.GetExtension(path)) 133 | { 134 | case ".csv": 135 | { 136 | //using var reader = new StreamReader(stream); 137 | //using var csv = new CsvReader(reader, CultureInfo.InvariantCulture); 138 | //var records = await csv.GetRecordsAsync().ToListAsync(); 139 | 140 | fileParseStart = DateTimeOffset.UtcNow; 141 | await Task.Run(() => 142 | { 143 | using var streamReader = new StreamReader(stream); 144 | var csvReader = new NReco.Csv.CsvReader(streamReader, ","); 145 | if (viewModel.CsvHasHeader) 146 | csvReader.Read(); 147 | int columnIndex = viewModel.CsvColumnIndex; 148 | while (csvReader.Read()) 149 | { 150 | var number = double.Parse(csvReader[columnIndex]); 151 | if (number > 1e12) // Catches the overrange samples from DMM6500 152 | continue; 153 | records.Add(number); 154 | } 155 | }); 156 | fileParseFinish = DateTimeOffset.UtcNow; 157 | break; 158 | } 159 | case ".f32": 160 | { 161 | fileParseStart = DateTimeOffset.UtcNow; 162 | await Task.Run(() => 163 | { 164 | const int ChunkSizeBytes = 8 * 1024 * 1024; // 8 MiB 165 | byte[] buffer = new byte[ChunkSizeBytes]; 166 | int bytesRead; 167 | 168 | while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) > 0) 169 | { 170 | // Only process full float32 values (4 bytes each) 171 | int validBytes = bytesRead - (bytesRead % 4); 172 | if (validBytes == 0) 173 | continue; 174 | 175 | ReadOnlySpan byteSpan = buffer.AsSpan(0, validBytes); 176 | ReadOnlySpan floatSpan = MemoryMarshal.Cast(byteSpan); 177 | 178 | foreach (var f32 in floatSpan) 179 | { 180 | records.Add(f32); 181 | } 182 | } 183 | }); 184 | fileParseFinish = DateTimeOffset.UtcNow; 185 | break; 186 | } 187 | } 188 | 189 | if (records.Count == 0) 190 | { 191 | viewModel.Status = "Error: No CSV records found"; 192 | return; 193 | } 194 | 195 | switch ((string)viewModel.SelectedNsdAlgorithm.Content) 196 | { 197 | case "Logarithmic": 198 | break; 199 | case "Linear": 200 | { 201 | var fftWidth = int.Parse((string)viewModel.SelectedLinearLengthItem.Content); 202 | if (fftWidth > records.Count) 203 | { 204 | viewModel.Status = "Error: FFT width is longer than input data"; 205 | return; 206 | } 207 | break; 208 | } 209 | case "Linear stacking": 210 | { 211 | var fftWidth = int.Parse((string)viewModel.SelectedLinearStackingLengthItem.Content); 212 | if (fftWidth > records.Count) 213 | { 214 | viewModel.Status = "Error: FFT width is longer than input data"; 215 | return; 216 | } 217 | break; 218 | } 219 | } 220 | 221 | 222 | //records = Signals.WhiteNoise(100000, sampleRate, 1e-9).ToArray().ToList(); 223 | 224 | viewModel.Status = "Status: Calculating NSD..."; 225 | 226 | for (int i = 0; i < records.Count; i++) 227 | { 228 | records[i] *= inputScaling; 229 | } 230 | 231 | DateTimeOffset nsdComputeStart = DateTimeOffset.UtcNow; 232 | //double spectralValueCorrection = Math.Sqrt(dataRateTimeSeconds / acquisitionTimeSeconds); 233 | //double spectralValueCorrection = 1.0; 234 | //double frequencyBinCorrection = Math.Sqrt(dataRateTimeSeconds / acquisitionTimeSeconds); 235 | //double frequencyBinCorrection = 1.0; 236 | switch ((string)viewModel.SelectedNsdAlgorithm.Content) 237 | { 238 | case "Logarithmic": 239 | { 240 | var pointsPerDecade = int.Parse(viewModel.LogNsdPointsDecade); 241 | var pointsPerDecadeScaling = double.Parse(viewModel.LogNsdPointsDecadeScaling); 242 | var minAverages = int.Parse(viewModel.LogNsdMinAverages); 243 | //var minLength = int.Parse(viewModel.LogNsdMinLength); 244 | var minLength = int.Parse((string)viewModel.SelectedLogNsdMinLength.Content); 245 | var nsd = await Task.Factory.StartNew(() => NSD.Log( 246 | input: records.ToArray(), 247 | sampleRateHz: 1.0 / acquisitionTimeSeconds, 248 | freqMin: viewModel.XMin, 249 | freqMax: viewModel.XMax, 250 | pointsPerDecade, 251 | minAverages, 252 | minLength, 253 | pointsPerDecadeScaling)); 254 | spectrum = nsd; 255 | break; 256 | } 257 | case "Linear": 258 | { 259 | var fftWidth = int.Parse((string)viewModel.SelectedLinearLengthItem.Content); 260 | var nsd = await Task.Factory.StartNew(() => NSD.Linear(input: records.ToArray(), 1.0 / acquisitionTimeSeconds, outputWidth: fftWidth)); 261 | spectrum = nsd; 262 | break; 263 | } 264 | case "Linear stacking": 265 | { 266 | var fftMaxWidth = int.Parse((string)viewModel.SelectedLinearStackingLengthItem.Content); 267 | var fftMinWidth = int.Parse((string)viewModel.SelectedLinearStackingMinLengthItem.Content); 268 | var nsd = await Task.Factory.StartNew(() => NSD.StackedLinear(input: records.ToArray(), 1.0 / acquisitionTimeSeconds, maxWidth: fftMaxWidth, minWidth: fftMinWidth)); 269 | spectrum = nsd; 270 | break; 271 | } 272 | } 273 | DateTimeOffset nsdComputeFinish = DateTimeOffset.UtcNow; 274 | Memory yArray; 275 | if (viewModel.SgFilterChecked) 276 | yArray = new SavitzkyGolayFilter(5, 1).Process(spectrum.Values.Span); 277 | else 278 | yArray = spectrum.Values; 279 | 280 | //for (int i = 0; i < yArray.Length; i++) 281 | //{ 282 | // yArray.Span[i] /= spectralValueCorrection; 283 | //} 284 | 285 | //for (int i = 0; i < spectrum.Frequencies.Length; i++) 286 | //{ 287 | // spectrum.Frequencies.Span[i] *= frequencyBinCorrection; 288 | //} 289 | 290 | UpdateNSDChart(spectrum.Frequencies, yArray); 291 | var fileParseTimeSec = fileParseFinish.Subtract(fileParseStart).TotalSeconds; 292 | var nsdComputeTimeSec = nsdComputeFinish.Subtract(nsdComputeStart).TotalSeconds; 293 | if (spectrum.Stacking > 1) 294 | viewModel.Status = $"Status: Processing complete, {records.Count} input points, averaged {spectrum.Averages} spectrums over {spectrum.Stacking} stacking FFT widths. File parse time: {fileParseTimeSec:F3}s, NSD compute time: {nsdComputeTimeSec:F3}s."; 295 | else 296 | viewModel.Status = $"Status: Processing complete, {records.Count} input points, averaged {spectrum.Averages} spectrums. File parse time: {fileParseTimeSec:F3}s, NSD compute time: {nsdComputeTimeSec:F3}s."; 297 | } 298 | catch (Exception ex) 299 | { 300 | await ShowError("Exception", ex.Message); 301 | } 302 | finally 303 | { 304 | viewModel.Enabled = true; 305 | } 306 | } 307 | 308 | public class NsdSample 309 | { 310 | public double Frequency { get; set; } 311 | public double Noise { get; set; } 312 | } 313 | 314 | public async void BtnGenerate_Click(object sender, RoutedEventArgs e) 315 | { 316 | var outputFilePath = Path.Combine(viewModel.ProcessWorkingFolder, viewModel.OutputFileName); 317 | 318 | CsvConfiguration config = new(CultureInfo.InvariantCulture); 319 | config.Delimiter = ","; 320 | using var writer = new StreamWriter(outputFilePath); 321 | using var csvWriter = new CsvHelper.CsvWriter(writer, config); 322 | csvWriter.WriteHeader(); 323 | csvWriter.NextRecord(); 324 | 325 | for (int i = 0; i < spectrum.Frequencies.Length; i++) 326 | { 327 | csvWriter.WriteField(spectrum.Frequencies.Span[i]); 328 | csvWriter.WriteField(spectrum.Values.Span[i]); 329 | csvWriter.NextRecord(); 330 | } 331 | } 332 | 333 | private void btnSetAxis_Click(object sender, RoutedEventArgs e) 334 | { 335 | if (spectrum != null) 336 | { 337 | Memory yArray; 338 | if (viewModel.SgFilterChecked) 339 | yArray = new SavitzkyGolayFilter(5, 1).Process(spectrum.Values.Span); 340 | else 341 | yArray = spectrum.Values; 342 | UpdateNSDChart(spectrum.Frequencies, yArray); 343 | } 344 | SetChartLimitsAndRefresh(); 345 | } 346 | 347 | public void UpdateNSDChart(Memory x, Memory y) 348 | { 349 | WpfPlot1.Plot.Clear(); 350 | double[] logXs = x.ToArray().Select(pt => Math.Log10(pt)).ToArray(); 351 | double[] logYs = y.ToArray().Select(pt => Math.Log10(pt * 1E9)).ToArray(); 352 | var scatter = WpfPlot1.Plot.Add.ScatterLine(logXs, logYs); 353 | //var scatter = WpfPlot1.Plot.Add.Scatter(logXs, logYs); 354 | if (viewModel.MarkersChecked) 355 | { 356 | scatter.MarkerStyle.Shape = ScottPlot.MarkerShape.FilledCircle; 357 | scatter.MarkerStyle.Size = 3; 358 | scatter.MarkerStyle.IsVisible = true; 359 | } 360 | CommonChartConfig(); 361 | } 362 | 363 | private void InitNsdChart() 364 | { 365 | WpfPlot1.Plot.Clear(); 366 | CommonChartConfig(); 367 | } 368 | 369 | private void CommonChartConfig() 370 | { 371 | static string logTickLabels(double y) => Math.Pow(10, y).ToString(); // "N0" 372 | NumericAutomatic xTickGenerator = new() 373 | { 374 | LabelFormatter = logTickLabels, 375 | MinorTickGenerator = new LogDecadeMinorTickGenerator() { TicksPerDecade = 10 }, 376 | IntegerTicksOnly = true, 377 | TargetTickCount = 10 378 | }; 379 | NumericAutomatic yTickGenerator = new() 380 | { 381 | LabelFormatter = logTickLabels, 382 | MinorTickGenerator = new LogDecadeMinorTickGenerator() { TicksPerDecade = 10 }, 383 | IntegerTicksOnly = true, 384 | TargetTickCount = 10 385 | }; 386 | WpfPlot1.Plot.Axes.Bottom.TickGenerator = xTickGenerator; 387 | WpfPlot1.Plot.Axes.Left.TickGenerator = yTickGenerator; 388 | WpfPlot1.Plot.Axes.Hairline(true); 389 | WpfPlot1.Plot.XLabel("Frequency (Hz)", 14); 390 | WpfPlot1.Plot.YLabel("Noise (nV/rHz)", 14); 391 | WpfPlot1.Plot.Axes.Bottom.Label.Bold = false; 392 | WpfPlot1.Plot.Axes.Left.Label.Bold = false; 393 | WpfPlot1.Plot.Title("NSD estimation", size: 14); 394 | WpfPlot1.Plot.Axes.Title.Label.Bold = false; 395 | WpfPlot1.Plot.Grid.MinorLineWidth = 1; 396 | WpfPlot1.Plot.Grid.MinorLineColor = ScottPlot.Color.FromARGB(0x14000000); 397 | WpfPlot1.Plot.Grid.MajorLineColor = ScottPlot.Color.FromARGB(0x50000000); 398 | SetChartLimitsAndRefresh(); 399 | } 400 | 401 | private void SetChartLimitsAndRefresh() 402 | { 403 | double fudgeFactor = 0.001; 404 | var left = Math.Log10(viewModel.XMin - (viewModel.XMin * fudgeFactor)); 405 | var right = Math.Log10(viewModel.XMax + (viewModel.XMax * fudgeFactor)); 406 | var top = Math.Log10(viewModel.YMax + (viewModel.YMax * fudgeFactor)); 407 | var bottom = Math.Log10(viewModel.YMin - (viewModel.YMin * fudgeFactor)); 408 | WpfPlot1.Plot.Axes.SetLimits(left, right, bottom, top); 409 | WpfPlot1.Refresh(); 410 | } 411 | 412 | private async Task ShowError(string title, string message) 413 | { 414 | var messageBoxStandardWindow = MessageBoxManager.GetMessageBoxStandard(title, message); 415 | await messageBoxStandardWindow.ShowAsync(); 416 | } 417 | } 418 | } 419 | -------------------------------------------------------------------------------- /source/NSD.UI/MainWindowViewModel.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using Avalonia.Media; 3 | using CommunityToolkit.Mvvm.ComponentModel; 4 | using System; 5 | using System.Collections.ObjectModel; 6 | using System.Reflection; 7 | 8 | namespace NSD.UI 9 | { 10 | // https://docs.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/generators/overview 11 | public partial class MainWindowViewModel : ObservableObject 12 | { 13 | // Loaded from settings file 14 | [ObservableProperty] string? processWorkingFolder; 15 | [ObservableProperty] string? acquisitionTime; 16 | 17 | [ObservableProperty] string status = "Status: Idle"; 18 | [ObservableProperty] bool enabled = true; 19 | [ObservableProperty] ObservableCollection inputFilePaths = new(); 20 | [ObservableProperty] ObservableCollection inputFileNames = new(); 21 | [ObservableProperty] int selectedInputFileIndex = -1; 22 | [ObservableProperty] string outputFileName = "output.nsd"; 23 | [ObservableProperty] bool sgFilterChecked = false; 24 | [ObservableProperty] bool markersChecked = false; 25 | [ObservableProperty] IBrush statusBackground = Brushes.WhiteSmoke; 26 | [ObservableProperty] string inputScaling = "1.0"; 27 | [ObservableProperty] string logNsdPointsDecade = "5"; 28 | [ObservableProperty] string logNsdPointsDecadeScaling = "2.0"; 29 | [ObservableProperty] string logNsdMinAverages = "1"; 30 | [ObservableProperty] string logNsdMinLength = "128"; 31 | 32 | public ComboBoxItem? SelectedAcquisitionTimebaseItem { get; set; } 33 | 34 | private ComboBoxItem? selectedNsdAlgorithm; 35 | public ComboBoxItem? SelectedNsdAlgorithm 36 | { 37 | get => selectedNsdAlgorithm; set 38 | { 39 | selectedNsdAlgorithm = value; 40 | switch ((string)selectedNsdAlgorithm.Content) 41 | { 42 | case "Logarithmic": 43 | AlgorithmLog = true; 44 | AlgorithmLin = false; 45 | AlgorithmLinStack = false; 46 | break; 47 | case "Linear": 48 | AlgorithmLog = false; 49 | AlgorithmLin = true; 50 | AlgorithmLinStack = false; 51 | break; 52 | case "Linear stacking": 53 | AlgorithmLog = false; 54 | AlgorithmLin = false; 55 | AlgorithmLinStack = true; 56 | break; 57 | } 58 | } 59 | } 60 | public ComboBoxItem? SelectedLinearLengthItem { get; set; } 61 | public ComboBoxItem? SelectedLinearStackingLengthItem { get; set; } 62 | public ComboBoxItem? SelectedLinearStackingMinLengthItem { get; set; } 63 | public ComboBoxItem? SelectedLogNsdMinLength { get; set; } 64 | [ObservableProperty] bool algorithmLog = true; // Controls visibility of sub-stack panel 65 | [ObservableProperty] bool algorithmLin = false; // Controls visibility of sub-stack panel 66 | [ObservableProperty] bool algorithmLinStack = false; // Controls visibility of sub-stack panel 67 | 68 | public ComboBoxItem? SelectedFileFormatItem { get; set; } 69 | public double XMin { get; set; } = 0.001; 70 | public double XMax { get; set; } = 100; 71 | public double YMin { get; set; } = 0.1; 72 | public double YMax { get; set; } = 100; 73 | public string WindowTitle { get { Version version = Assembly.GetExecutingAssembly().GetName().Version; return "NSD v" + version.Major + "." + version.Minor; } } 74 | [ObservableProperty] bool csvHasHeader = false; 75 | [ObservableProperty] int csvColumnIndex = 0; 76 | 77 | private Settings settings; 78 | 79 | 80 | public MainWindowViewModel(Settings settings, MainWindow window) 81 | { 82 | this.settings = settings; 83 | processWorkingFolder = settings.ProcessWorkingFolder; 84 | acquisitionTime = settings.AcquisitionTime; 85 | 86 | window.cbTime.SelectedIndex = settings.AcquisitionTimeUnit switch 87 | { 88 | "NPLC (50Hz)" => 0, 89 | "NPLC (60Hz)" => 1, 90 | "s" => 2, 91 | "ms" => 3, 92 | "μs" => 4, 93 | "ns" => 5, 94 | "SPS" => 6, 95 | "kSPS" => 7, 96 | "MSPS" => 8, 97 | //_ => throw new Exception("Invalid AcquisitionTimeUnit") 98 | _ => 0 99 | }; 100 | } 101 | 102 | partial void OnProcessWorkingFolderChanged(string? value) 103 | { 104 | settings.ProcessWorkingFolder = value; 105 | settings.Save(); 106 | } 107 | 108 | partial void OnAcquisitionTimeChanged(string? value) 109 | { 110 | settings.AcquisitionTime = value; 111 | settings.Save(); 112 | } 113 | 114 | partial void OnStatusChanged(string value) 115 | { 116 | if (value.Contains("Error")) 117 | { 118 | StatusBackground = Brushes.Red; 119 | } 120 | else 121 | { 122 | StatusBackground = Brushes.WhiteSmoke; 123 | } 124 | } 125 | 126 | public string GetSelectedInputFilePath() 127 | { 128 | if (inputFilePaths.Count > 0 && selectedInputFileIndex < inputFilePaths.Count) 129 | return inputFilePaths[selectedInputFileIndex]; 130 | else 131 | return ""; 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /source/NSD.UI/NSD.UI.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | WinExe 4 | net8.0 5 | 1.4 6 | enable 7 | true 8 | en 9 | true 10 | true 11 | true 12 | partial 13 | true 14 | true 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /source/NSD.UI/Settings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text.Json; 4 | using System.Text.Json.Serialization; 5 | 6 | namespace NSD.UI 7 | { 8 | [JsonSourceGenerationOptions(WriteIndented = true)] 9 | [JsonSerializable(typeof(Settings))] 10 | internal partial class SourceGenerationContext : JsonSerializerContext 11 | { 12 | } 13 | 14 | public class Settings 15 | { 16 | public string? ProcessWorkingFolder { get; set; } 17 | public string? AcquisitionTime { get; set; } 18 | public string? AcquisitionTimeUnit { get; set; } 19 | public string? DataRate { get; set; } 20 | public string? DataRateUnit { get; set; } 21 | 22 | public static Settings Default() 23 | { 24 | return new Settings() 25 | { 26 | ProcessWorkingFolder = Directory.GetCurrentDirectory(), 27 | AcquisitionTime = "1", 28 | AcquisitionTimeUnit = "NPLC (50Hz)", 29 | DataRate = "50", 30 | DataRateUnit = "Samples per second" 31 | }; 32 | } 33 | 34 | public static Settings Load() 35 | { 36 | if (!File.Exists("settings.json")) 37 | return Default(); 38 | var json = File.ReadAllText("settings.json"); 39 | if (json.Contains("SampleRate")) 40 | return Default(); // Ignore old settings file 41 | if (string.IsNullOrWhiteSpace(json)) 42 | return Default(); 43 | var settings = JsonSerializer.Deserialize(json, SourceGenerationContext.Default.Settings); 44 | if (settings != null) 45 | return settings; 46 | else 47 | return Default(); 48 | } 49 | 50 | public void Save() 51 | { 52 | var json = JsonSerializer.Serialize(this, SourceGenerationContext.Default.Settings); 53 | File.WriteAllText("settings.json", json); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /source/NSD.UI/runtimeconfig.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "configProperties": { 3 | "System.Drawing.EnableUnixSupport": true 4 | } 5 | } -------------------------------------------------------------------------------- /source/NSD.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.1.32421.90 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NSD.UI", "NSD.UI\NSD.UI.csproj", "{A5492109-BC61-49D4-A830-300B844F8A67}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NSD", "NSD\NSD.csproj", "{EBA91A52-FAE6-4E77-8714-79CBE5A2EEB8}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NSD.Generator", "NSD.Generator\NSD.Generator.csproj", "{37A8D3A3-4EA6-4EEF-A094-5BFECF33C4C7}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NSD.Benchmarks", "NSD.Benchmarks\NSD.Benchmarks.csproj", "{CAE57A71-6251-4BF3-82F9-9546AB76EC64}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {A5492109-BC61-49D4-A830-300B844F8A67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {A5492109-BC61-49D4-A830-300B844F8A67}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {A5492109-BC61-49D4-A830-300B844F8A67}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {A5492109-BC61-49D4-A830-300B844F8A67}.Release|Any CPU.Build.0 = Release|Any CPU 24 | {EBA91A52-FAE6-4E77-8714-79CBE5A2EEB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {EBA91A52-FAE6-4E77-8714-79CBE5A2EEB8}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {EBA91A52-FAE6-4E77-8714-79CBE5A2EEB8}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {EBA91A52-FAE6-4E77-8714-79CBE5A2EEB8}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {37A8D3A3-4EA6-4EEF-A094-5BFECF33C4C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {37A8D3A3-4EA6-4EEF-A094-5BFECF33C4C7}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {37A8D3A3-4EA6-4EEF-A094-5BFECF33C4C7}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {37A8D3A3-4EA6-4EEF-A094-5BFECF33C4C7}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {CAE57A71-6251-4BF3-82F9-9546AB76EC64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {CAE57A71-6251-4BF3-82F9-9546AB76EC64}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {CAE57A71-6251-4BF3-82F9-9546AB76EC64}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {CAE57A71-6251-4BF3-82F9-9546AB76EC64}.Release|Any CPU.Build.0 = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | GlobalSection(ExtensibilityGlobals) = postSolution 41 | SolutionGuid = {D5FEEFF6-2F41-4325-9E64-D2FBC754142E} 42 | EndGlobalSection 43 | EndGlobal 44 | -------------------------------------------------------------------------------- /source/NSD/Data types/Spectrum.cs: -------------------------------------------------------------------------------- 1 | public class Spectrum 2 | { 3 | public Memory Frequencies { get; set; } 4 | public Memory Values { get; set; } 5 | public int Averages { get; set; } 6 | public int Stacking { get; set; } = 1; 7 | 8 | public static Spectrum FromValues(Memory values, double sampleRate, int averages) 9 | { 10 | int length = (values.Length / 2); 11 | Memory frequencies = new double[length]; 12 | double dT = (sampleRate / values.Length); 13 | for (int i = 0; i < length; i++) 14 | { 15 | frequencies.Span[i] = i * dT; 16 | } 17 | return new Spectrum() { Frequencies = frequencies, Values = values.Slice(0, length), Averages = averages }; 18 | } 19 | 20 | bool trimmedDC = false; 21 | public void TrimDC() 22 | { 23 | if (!trimmedDC) 24 | { 25 | trimmedDC = true; 26 | Frequencies = Frequencies[1..]; 27 | Values = Values[1..]; 28 | } 29 | } 30 | 31 | // It is well known that windowing of data segments is necessary in the WOSA method to reduce the bias 32 | // of the spectral estimate[14]. When calculating onesided spectral estimates containing only positive 33 | // Fourier frequencies windowing causes a bias at low frequency bins—a fact that is also well known: 34 | // one cannot trust the lowest frequency bins on the spectrum analyzer.The bias stems from aliasing of 35 | // power from negative bins and bin zero to the lowest positive frequency bins.Aliasing from bin zero can 36 | // be eliminated by subtracting the mean data value from the segment.Aliasing from negative bins however, cannot be reduced that way.Hence we propose 37 | // not to use the first few frequency bins.The first frequency bin that yields unbiased spectral estimates 38 | // depends on the window function used.The bin is given by the effective half-width of the window 39 | // transfer function. 40 | int trimmedStartBins = 0; 41 | public void TrimStart(int bins) 42 | { 43 | if (trimmedStartBins != 0) 44 | throw new Exception($"TrimStart already called with bins: {trimmedStartBins}"); 45 | trimmedStartBins = bins; 46 | Frequencies = Frequencies.Slice(bins, Frequencies.Length - bins); 47 | Values = Values.Slice(bins, Values.Length - bins); 48 | } 49 | } -------------------------------------------------------------------------------- /source/NSD/FFT.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | 3 | namespace NSD 4 | { 5 | internal class FFT 6 | { 7 | private readonly int length; 8 | private readonly Complex[] complexBuffer; 9 | private readonly FftFlat.FastFourierTransform fft; 10 | 11 | public FFT(int length) 12 | { 13 | this.length = length; 14 | complexBuffer = new Complex[length]; 15 | fft = new FftFlat.FastFourierTransform(length); 16 | } 17 | 18 | public void PSD(ReadOnlyMemory inputData, Memory outputPsd, ReadOnlyMemory window, double sampleRate, double windowS2) 19 | { 20 | if (inputData.Length != length || outputPsd.Length != length || window.Length != length) 21 | throw new ArgumentException("Array lengths don't match"); 22 | 23 | // Apply window to data 24 | for (int i = 0; i < length; i++) 25 | { 26 | complexBuffer[i] = new Complex(inputData.Span[i] * window.Span[i], 0); 27 | } 28 | 29 | // Apply transform 30 | //MathNet.Numerics.IntegralTransforms.Fourier.Forward(fourierData, MathNet.Numerics.IntegralTransforms.FourierOptions.NoScaling); 31 | fft.Forward(complexBuffer); 32 | 33 | // Convert to magnitude spectrum 34 | for (int i = 0; i < length; i++) 35 | { 36 | outputPsd.Span[i] = (2.0 * Math.Pow(complexBuffer[i].Magnitude, 2)) / (sampleRate * windowS2); //"The factor 2 originates from the fact that we presumably use an efficient FFT algorithm that does not compute the redundant results for negative frequencies" 37 | //outputPsd.Span[i] = (Math.Pow(Math.Abs(fourierData[i].Magnitude), 2)) / (sampleRate * s2); 38 | if (double.IsNaN(outputPsd.Span[i]) || outputPsd.Span[i] > 1000000000000) 39 | throw new Exception(); 40 | } 41 | } 42 | 43 | public void PS(ReadOnlyMemory inputData, Memory outputPs, ReadOnlyMemory window, double windowS1) 44 | { 45 | if (inputData.Length != length || outputPs.Length != length || window.Length != length) 46 | throw new ArgumentException("Array lengths don't match"); 47 | 48 | // Apply window to data 49 | for (int i = 0; i < window.Length; i++) 50 | { 51 | complexBuffer[i] = new Complex(inputData.Span[i] * window.Span[i], 0); 52 | } 53 | 54 | // Apply transform 55 | //MathNet.Numerics.IntegralTransforms.Fourier.Forward(fourierData, MathNet.Numerics.IntegralTransforms.FourierOptions.NoScaling); 56 | fft.Forward(complexBuffer); 57 | 58 | // Convert to magnitude spectrum 59 | for (int i = 0; i < inputData.Length; i++) 60 | { 61 | outputPs.Span[i] = (2.0 * Math.Pow(complexBuffer[i].Magnitude, 2)) / (windowS1 * windowS1); //"The factor 2 originates from the fact that we presumably use an efficient FFT algorithm that does not compute the redundant results for negative frequencies" 62 | //outputPsd.Span[i] = (Math.Pow(Math.Abs(fourierData[i].Magnitude), 2)) / (s1 * s1); 63 | if (double.IsNaN(outputPs.Span[i]) || outputPs.Span[i] > 1000000000000) 64 | throw new Exception(); 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /source/NSD/Filter.cs: -------------------------------------------------------------------------------- 1 | using MathNet.Numerics.LinearAlgebra; 2 | using System; 3 | 4 | namespace NSD 5 | { 6 | /// 7 | /// Implements a Savitzky-Golay smoothing filter, as found in [1]. 8 | /// [1] Sophocles J.Orfanidis. 1995. Introduction to Signal Processing. Prentice-Hall, Inc., Upper Saddle River, NJ, USA. 9 | /// 10 | public sealed class SavitzkyGolayFilter 11 | { 12 | private readonly int sidePoints; 13 | 14 | private Matrix coefficients; 15 | 16 | public SavitzkyGolayFilter(int sidePoints, int polynomialOrder) 17 | { 18 | this.sidePoints = sidePoints; 19 | Design(polynomialOrder); 20 | } 21 | 22 | /// 23 | /// Smoothes the input samples. 24 | /// 25 | /// 26 | /// 27 | public double[] Process(Span samples) 28 | { 29 | int length = samples.Length; 30 | double[] output = new double[length]; 31 | int frameSize = (sidePoints << 1) + 1; 32 | double[] frame = samples.Slice(0, frameSize).ToArray(); 33 | 34 | for (int i = 0; i < sidePoints; ++i) 35 | { 36 | output[i] = coefficients.Column(i).DotProduct(Vector.Build.DenseOfArray(frame)); 37 | } 38 | 39 | for (int n = sidePoints; n < length - sidePoints; ++n) 40 | { 41 | Array.ConstrainedCopy(samples.ToArray(), n - sidePoints, frame, 0, frameSize); 42 | output[n] = coefficients.Column(sidePoints).DotProduct(Vector.Build.DenseOfArray(frame)); 43 | } 44 | 45 | Array.ConstrainedCopy(samples.ToArray(), length - frameSize, frame, 0, frameSize); 46 | 47 | for (int i = 0; i < sidePoints; ++i) 48 | { 49 | output[length - sidePoints + i] = coefficients.Column(sidePoints + 1 + i).DotProduct(Vector.Build.Dense(frame)); 50 | } 51 | 52 | return output; 53 | } 54 | 55 | private void Design(int polynomialOrder) 56 | { 57 | double[,] a = new double[(sidePoints << 1) + 1, polynomialOrder + 1]; 58 | 59 | for (int m = -sidePoints; m <= sidePoints; ++m) 60 | { 61 | for (int i = 0; i <= polynomialOrder; ++i) 62 | { 63 | a[m + sidePoints, i] = Math.Pow(m, i); 64 | } 65 | } 66 | 67 | Matrix s = Matrix.Build.DenseOfArray(a); 68 | coefficients = s.Multiply(s.TransposeThisAndMultiply(s).Inverse()).Multiply(s.Transpose()); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /source/NSD/GoertzelFilter.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | 3 | namespace NSD 4 | { 5 | public class GoertzelFilter 6 | { 7 | private readonly double coeff; 8 | private readonly double sine; 9 | private readonly double cosine; 10 | 11 | public GoertzelFilter(double filterFreq, double sampleFreq) 12 | { 13 | double w = 2.0 * Math.PI * (filterFreq / sampleFreq); 14 | cosine = Math.Cos(w); 15 | sine = Math.Sin(w); 16 | coeff = 2.0 * cosine; 17 | } 18 | 19 | public Complex Process(Span samples) 20 | { 21 | double Q0 = 0.0; 22 | double Q1 = 0.0; 23 | double Q2 = 0.0; 24 | 25 | for (int n = 0; n < samples.Length; n++) 26 | { 27 | Q0 = coeff * Q1 - Q2 + samples[n]; 28 | Q2 = Q1; 29 | Q1 = Q0; 30 | } 31 | 32 | var real = Q1 * cosine - Q2; 33 | var imag = -Q1 * sine; 34 | return new Complex(real, imag); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /source/NSD/NSD.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | 3 | namespace NSD 4 | { 5 | public class NSD 6 | { 7 | public static Spectrum Linear(ReadOnlyMemory input, double sampleRate, int outputWidth = 2048) 8 | { 9 | //var window = Windows.HFT90D(outputWidth, out double optimumOverlap, out double NENBW); 10 | var window = Windows.FTNI(outputWidth, out double optimumOverlap, out double NENBW); 11 | //var window = Windows.HFT95(outputWidth, out double optimumOverlap, out double NENBW); 12 | // Switched from HFT90D to FTNI 13 | // FTNI has the useful feature where Math.Ceiling(NENBW) is 1 less than most flaptop windows, 14 | // therefore showing one more usable frequency point at the low frequency end of the spectrum. 15 | var windowS2 = S2(window.Span); 16 | var fft = new FFT(outputWidth); 17 | int startIndex = 0; 18 | int endIndex = outputWidth; 19 | int overlap = (int)(outputWidth * (1.0 - optimumOverlap)); 20 | int spectrumCount = 0; 21 | Memory workBuffer = new double[outputWidth]; 22 | Memory workSpectrum = new double[outputWidth]; 23 | Memory lineFitOutput = new double[outputWidth]; 24 | Memory psdMemory = new double[outputWidth]; 25 | while (endIndex < input.Length) 26 | { 27 | var lineFitInput = input.Slice(startIndex, outputWidth); 28 | SubtractLineFit(lineFitInput, lineFitOutput, workBuffer); 29 | fft.PSD(lineFitOutput, psdMemory, window, sampleRate, windowS2); 30 | AddPSDToWorkSpectrum(psdMemory, workSpectrum); spectrumCount++; 31 | startIndex += overlap; 32 | endIndex += overlap; 33 | } 34 | ConvertWorkSpectrumToAverageVSDInPlace(workSpectrum, spectrumCount); 35 | var nsd = Spectrum.FromValues(workSpectrum, sampleRate, spectrumCount); 36 | //nsd.TrimDC(); // Don't need to trim DC if trimming start/end 37 | nsd.TrimStart((int)Math.Ceiling(NENBW * 2)); 38 | return nsd; 39 | } 40 | 41 | public static Spectrum StackedLinear(Memory input, double sampleRate, int maxWidth = 2048, int minWidth = 64) 42 | { 43 | // Compute all the possible widths between maxWidth & minWidth 44 | List widths = [maxWidth]; 45 | int width = maxWidth; 46 | while (width > minWidth) 47 | { 48 | width /= 2; 49 | widths.Add(width); 50 | } 51 | // Order by smallest to largest 52 | widths.Reverse(); 53 | 54 | // Run parallel NSDs 55 | var spectrums = new Dictionary(); 56 | Parallel.ForEach(widths, new ParallelOptions { MaxDegreeOfParallelism = 8 }, width => 57 | { 58 | spectrums[width] = Linear(input, sampleRate, width); 59 | }); 60 | 61 | // Combine all the NSDs into one 62 | double lowestFrequency = double.MaxValue; 63 | var outputFrequencies = new List(); 64 | var outputValues = new List(); 65 | int averages = 0; 66 | foreach (var computedWidth in widths) 67 | { 68 | var nsd = spectrums[computedWidth]; 69 | averages += nsd.Averages; 70 | for (int i = nsd.Frequencies.Length - 1; i >= 0; i--) 71 | { 72 | if (nsd.Frequencies.Span[i] < lowestFrequency) 73 | { 74 | lowestFrequency = nsd.Frequencies.Span[i]; 75 | outputFrequencies.Add(nsd.Frequencies.Span[i]); 76 | outputValues.Add(nsd.Values.Span[i]); 77 | } 78 | } 79 | } 80 | 81 | // Order by frequencies smallest to largest 82 | outputFrequencies.Reverse(); 83 | outputValues.Reverse(); 84 | return new Spectrum() { Frequencies = outputFrequencies.ToArray(), Values = outputValues.ToArray(), Averages = averages, Stacking = widths.Count }; 85 | } 86 | 87 | private record WelchGoertzelJob(double Frequency, int SpectrumLength, int CalculatedAverages); 88 | public static Spectrum Log(Memory input, double sampleRateHz, double freqMin, double freqMax, int pointsPerDecade, int minimumAverages, int minimumFourierLength, double pointsPerDecadeScaling) 89 | { 90 | if (freqMax <= freqMin) 91 | throw new ArgumentException("freqMax must be greater than freqMin"); 92 | if (pointsPerDecade <= 0 || minimumAverages <= 0 || minimumFourierLength <= 0) 93 | throw new ArgumentException("pointsPerDecade, minimumAverages, and minimumFourierLength must be positive"); 94 | if (sampleRateHz <= 0) 95 | throw new ArgumentException("sampleRateHz must be positive"); 96 | 97 | Windows.FTNI(1, out double optimumOverlap, out double NENBW); 98 | int firstUsableBinForWindow = (int)Math.Ceiling(NENBW); 99 | 100 | // To do: 101 | // For the purposes of the frequencies calculation, round freqMax/freqMin to nearest major decade line. 102 | // This ensures consistency of X-coordinate over various view widths. 103 | double decadeMin = RoundToDecade(freqMin, RoundingMode.Down); 104 | double decadeMax = RoundToDecade(freqMax, RoundingMode.Up); 105 | int decadeMinExponent = (int)Math.Log10(decadeMin); 106 | int decadeMaxExponent = (int)Math.Log10(decadeMax); 107 | 108 | List frequencyList = []; 109 | int pointsPerDecadeScaled = pointsPerDecade; 110 | for (int decadeExponent = decadeMinExponent; decadeExponent < decadeMaxExponent; decadeExponent++) 111 | { 112 | double currentDecadeMin = Math.Pow(10, decadeExponent); 113 | double currentDecadeMax = Math.Pow(10, decadeExponent + 1); 114 | double multiple = Math.Log(currentDecadeMax) - Math.Log(currentDecadeMin); 115 | var decadeFrequencies = Enumerable.Range(0, pointsPerDecadeScaled - 1).Select(i => currentDecadeMin * Math.Exp(i * multiple / (pointsPerDecadeScaled - 1))).ToArray(); 116 | frequencyList.AddRange(decadeFrequencies); 117 | pointsPerDecadeScaled = (int)(pointsPerDecadeScaled * pointsPerDecadeScaling); 118 | } 119 | 120 | double g = Math.Log(decadeMax) - Math.Log(decadeMin); 121 | double[] frequencies = frequencyList.ToArray(); 122 | double[] spectrumResolution = frequencies.Select(freq => freq / firstUsableBinForWindow).ToArray(); 123 | // spectrumResolution contains the 'desired resolutions' for each frequency bin, respecting the rule that we want the first usuable bin for the given window. 124 | int[] spectrumLengths = spectrumResolution.Select(resolution => (int)Math.Round(sampleRateHz / resolution)).ToArray(); 125 | 126 | // Create a job list of valid points to calculate 127 | double nyquistMax = sampleRateHz / 2; 128 | List jobs = []; 129 | for (int i = 0; i < frequencies.Length; i++) 130 | { 131 | if (frequencies[i] > nyquistMax) 132 | continue; 133 | if (TryCalculateAverages(input.Length, spectrumLengths[i], optimumOverlap, out var averages)) 134 | { 135 | if (averages >= minimumAverages) 136 | { 137 | // Increase spectrum length until minimumLength is met, or averages drops below minimumAverages. 138 | // This increases the spectral resolution at the top end of the chart, allowing 50Hz spikes (& similar) to be more visible 139 | var spectrumLength = spectrumLengths[i]; 140 | bool continueLoop = true; 141 | while (continueLoop) 142 | { 143 | if (spectrumLength < minimumFourierLength && averages > minimumAverages) 144 | { 145 | var success = TryCalculateAverages(input.Length, spectrumLength * 2, optimumOverlap, out var newAverages); 146 | if (!success) 147 | break; 148 | if (averages > minimumAverages) 149 | { 150 | spectrumLength *= 2; 151 | averages = newAverages; 152 | continueLoop = true; 153 | } 154 | else 155 | { 156 | continueLoop = false; 157 | break; 158 | } 159 | } 160 | else 161 | { 162 | continueLoop = false; 163 | } 164 | } 165 | jobs.Add(new WelchGoertzelJob(frequencies[i], spectrumLength, averages)); 166 | } 167 | } 168 | } 169 | 170 | var spectrum = new Dictionary(); 171 | for (int i = 0; i < jobs.Count; i++) 172 | { 173 | spectrum[jobs[i].Frequency] = double.NaN; 174 | } 175 | object averageLock = new(); 176 | int cumulativeAverage = 0; 177 | //foreach(var job in jobs) 178 | Parallel.ForEach(jobs, new ParallelOptions { MaxDegreeOfParallelism = 8 }, job => 179 | { 180 | var result = RunWelchGoertzel(input, job.SpectrumLength, job.Frequency, sampleRateHz, out var actualAverages); 181 | if (job.CalculatedAverages != actualAverages) 182 | throw new Exception("Actual averages does not match calculated averages"); 183 | spectrum[job.Frequency] = result; 184 | lock (averageLock) 185 | { 186 | cumulativeAverage += actualAverages; 187 | } 188 | } 189 | ); 190 | 191 | var output = new Spectrum 192 | { 193 | Frequencies = spectrum.Keys.ToArray(), 194 | Values = spectrum.Values.ToArray(), 195 | Averages = cumulativeAverage 196 | }; 197 | return output; 198 | } 199 | 200 | private static void AddPSDToWorkSpectrum(Memory inputPSD, Memory workingMemory) 201 | { 202 | for (int i = 0; i < inputPSD.Length; i++) 203 | { 204 | workingMemory.Span[i] += inputPSD.Span[i]; 205 | } 206 | } 207 | 208 | private static void ConvertWorkSpectrumToAverageVSDInPlace(Memory workingMemory, int count) 209 | { 210 | double divisor = count; 211 | for (int i = 0; i < workingMemory.Length; i++) 212 | { 213 | workingMemory.Span[i] = workingMemory.Span[i] / divisor; 214 | } 215 | 216 | // Convert to VSD 217 | for (int i = 0; i < workingMemory.Length; i++) 218 | { 219 | workingMemory.Span[i] = Math.Sqrt(workingMemory.Span[i]); 220 | } 221 | } 222 | 223 | /// 224 | /// Least-Squares fitting the points (x,y) to a line y : x -> a+b*x, returning its best fitting parameters as (a, b) tuple, where a is the intercept and b the slope. 225 | /// 226 | private static (double A, double B) LineFit(ReadOnlySpan x, ReadOnlySpan y) 227 | { 228 | if (x.Length != y.Length) 229 | { 230 | throw new ArgumentException($"All sample vectors must have the same length."); 231 | } 232 | 233 | if (x.Length <= 1) 234 | { 235 | throw new ArgumentException($"A regression of the requested order requires at least {2} samples. Only {x.Length} samples have been provided."); 236 | } 237 | 238 | // First Pass: Mean (Less robust but faster than ArrayStatistics.Mean) 239 | double mx = 0.0; 240 | double my = 0.0; 241 | for (int i = 0; i < x.Length; i++) 242 | { 243 | mx += x[i]; 244 | my += y[i]; 245 | } 246 | 247 | mx /= x.Length; 248 | my /= y.Length; 249 | 250 | // Second Pass: Covariance/Variance 251 | double covariance = 0.0; 252 | double variance = 0.0; 253 | for (int i = 0; i < x.Length; i++) 254 | { 255 | double diff = x[i] - mx; 256 | covariance += diff * (y[i] - my); 257 | variance += diff * diff; 258 | } 259 | 260 | var b = covariance / variance; 261 | return (my - b * mx, b); 262 | } 263 | 264 | /// 265 | /// Calculate Least-squares line fit and subtract from input, storing in output. Buffer is temporary variable memory. 266 | /// 267 | private static void SubtractLineFit(ReadOnlyMemory input, Memory output, Memory buffer) 268 | { 269 | if (input.Length != output.Length || input.Length != buffer.Length) 270 | throw new ArgumentException("Lengths don't match"); 271 | 272 | var x = buffer.Span; 273 | var y = input.Span; 274 | 275 | for (int i = 0; i < input.Length; i++) 276 | x[i] = i; 277 | 278 | var (A, B) = LineFit(x, y); 279 | var outputSpan = output.Span; 280 | var inputSpan = input.Span; 281 | for (int i = 0; i < input.Length; i++) 282 | { 283 | outputSpan[i] = (inputSpan[i] - (A + B * i)); 284 | } 285 | } 286 | 287 | private static double RunWelchGoertzel(Memory input, int runLength, double frequency, double sampleRateHz, out int spectrumCount2) 288 | { 289 | var window = Windows.FTNI(runLength, out double optimumOverlap, out double NENBW); 290 | double s2 = S2(window.Span); 291 | int startIndex = 0; 292 | int endIndex = runLength; 293 | int overlap = (int)(runLength * (1.0 - optimumOverlap)); 294 | int spectrumCount = 0; 295 | double average = 0; 296 | Memory waveformBuffer = new double[runLength]; 297 | Memory workBuffer = new double[runLength]; 298 | 299 | while (endIndex < input.Length) 300 | { 301 | var lineFitInput = input.Slice(startIndex, runLength); 302 | SubtractLineFit(lineFitInput, waveformBuffer, workBuffer); 303 | for (int i = 0; i < runLength; i++) 304 | { 305 | waveformBuffer.Span[i] = waveformBuffer.Span[i] * window.Span[i]; 306 | } 307 | 308 | var filter = new GoertzelFilter(frequency, sampleRateHz); // Specific form of 1 bin DFT 309 | var power = filter.Process(waveformBuffer.Span); 310 | average += 2.0 * Math.Pow(power.Magnitude, 2) / (sampleRateHz * s2); 311 | spectrumCount++; 312 | startIndex += overlap; 313 | endIndex += overlap; 314 | } 315 | 316 | spectrumCount2 = spectrumCount; 317 | return Math.Sqrt(average / spectrumCount); 318 | } 319 | 320 | private static double S1(ReadOnlySpan window) 321 | { 322 | double sum = 0; 323 | for (int i = 0; i < window.Length; i++) 324 | { 325 | sum += window[i]; 326 | } 327 | return sum; 328 | } 329 | 330 | private static double S2(ReadOnlySpan window) 331 | { 332 | double sumSquared = 0; 333 | for (int i = 0; i < window.Length; i++) 334 | { 335 | sumSquared += Math.Pow(window[i], 2); 336 | } 337 | return sumSquared; 338 | } 339 | 340 | enum RoundingMode { Nearest, Up, Down } 341 | private static double RoundToDecade(double value, RoundingMode mode) 342 | { 343 | if (value <= 0) 344 | throw new ArgumentOutOfRangeException(nameof(value), "Value must be positive."); 345 | 346 | double log10 = Math.Log10(value); 347 | double exponent = mode switch 348 | { 349 | RoundingMode.Nearest => Math.Round(log10), 350 | RoundingMode.Up => Math.Ceiling(log10), 351 | RoundingMode.Down => Math.Floor(log10), 352 | _ => throw new ArgumentOutOfRangeException(nameof(mode), "Invalid rounding mode.") 353 | }; 354 | 355 | return Math.Pow(10, exponent); 356 | } 357 | 358 | private static bool TryCalculateAverages(int dataLength, int spectrumLength, double optimumOverlap, out int averages) 359 | { 360 | averages = 0; 361 | int overlap = (int)(spectrumLength * (1.0 - optimumOverlap)); 362 | if (overlap < 1) 363 | return false; 364 | int endIndex = spectrumLength; 365 | while (endIndex < dataLength) 366 | { 367 | averages++; 368 | endIndex += overlap; 369 | } 370 | return true; 371 | } 372 | } 373 | } 374 | 375 | -------------------------------------------------------------------------------- /source/NSD/NSD.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /source/NSD/NsdProcessingException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NSD 4 | { 5 | public class NsdProcessingException : Exception 6 | { 7 | public NsdProcessingException() { } 8 | public NsdProcessingException(string message) : base(message) { } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /source/NSD/Papers/GH_FFT.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macaba/NSD/5b9f022c3fcccb7cad7603d5cf9af9413af43637/source/NSD/Papers/GH_FFT.pdf -------------------------------------------------------------------------------- /source/NSD/Papers/Improved spectrum estimation from digitized time series on a logaritmic frequency axis.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macaba/NSD/5b9f022c3fcccb7cad7603d5cf9af9413af43637/source/NSD/Papers/Improved spectrum estimation from digitized time series on a logaritmic frequency axis.pdf -------------------------------------------------------------------------------- /source/NSD/Signals.cs: -------------------------------------------------------------------------------- 1 | using MathNet.Numerics.IntegralTransforms; 2 | using System; 3 | using System.Numerics; 4 | 5 | namespace NSD 6 | { 7 | public class Signals 8 | { 9 | public static Memory TenNanoVoltRmsTestSignal() 10 | { 11 | var sine = MathNet.Numerics.Generate.Sinusoidal(1000000, 50, 5.1, 1.41e-8); 12 | for (int i = 0; i < sine.Length; i++) 13 | { 14 | sine[i] = Math.Round(sine[i], 11); 15 | } 16 | return sine; 17 | } 18 | 19 | public static Memory OneVoltRmsTestSignal() 20 | { 21 | var noiseSource = new MathNet.Filtering.DataSources.WhiteGaussianNoiseSource(); 22 | var sine = MathNet.Numerics.Generate.Sinusoidal(1000000, 50, 5.1, 1.41); 23 | for (int i = 0; i < sine.Length; i++) 24 | { 25 | sine[i] = sine[i] + noiseSource.ReadNextSample(); 26 | } 27 | return sine; 28 | } 29 | 30 | public static Memory WhiteNoise(int samples, double sampleRate, double volt) 31 | { 32 | var noiseSource = new MathNet.Filtering.DataSources.WhiteGaussianNoiseSource(0, 1); 33 | Memory data = new double[samples]; 34 | for (int i = 0; i < data.Length; i++) 35 | { 36 | data.Span[i] = noiseSource.ReadNextSample() * volt * Math.Sqrt(sampleRate / 2); 37 | } 38 | return data; 39 | } 40 | 41 | // https://raw.githubusercontent.com/felixpatzelt/colorednoise/master/colorednoise.py 42 | // exponent - 0: gaussian, 1: pink (1/f), 2: brown (1/f^2), -1: blue, -2: violet 43 | public static Memory PowerLawGaussian(int samples, double sampleRate, double exponent) 44 | { 45 | var frequencies = Fourier.FrequencyScale(samples, sampleRate).Take((samples / 2) + 1).ToArray(); 46 | var scalingFactors = frequencies.ToArray(); 47 | for (int i = 1; i < scalingFactors.Length; i++) 48 | { 49 | scalingFactors[i] = Math.Pow(frequencies[i], -exponent / 2.0); 50 | } 51 | scalingFactors[0] = scalingFactors[1]; 52 | 53 | var noiseSource = new MathNet.Filtering.DataSources.WhiteGaussianNoiseSource(); 54 | Complex[] fourierData = new Complex[scalingFactors.Length]; 55 | for (int i = 0; i < fourierData.Length; i++) 56 | { 57 | fourierData[i] = new Complex(noiseSource.ReadNextSample() * scalingFactors[i], noiseSource.ReadNextSample() * scalingFactors[i]); 58 | } 59 | fourierData[0] = new Complex(fourierData[0].Real * Math.Sqrt(2), 0); 60 | 61 | Fourier.Inverse(fourierData); 62 | 63 | double[] data = new double[fourierData.Length]; 64 | for (int i = 0; i < data.Length; i++) 65 | { 66 | data[i] = fourierData[i].Magnitude; 67 | } 68 | 69 | return data; 70 | } 71 | 72 | //public static Memory WhiteWithSlopeNoise(int samples, double sampleRate, double volt, double slopeCornerFrequency, double slope) 73 | //{ 74 | // var white = WhiteNoise(samples, sampleRate, volt); 75 | // //var slope = something; 76 | 77 | // return white; 78 | //} 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /source/NSD/Windows.cs: -------------------------------------------------------------------------------- 1 | namespace NSD 2 | { 3 | public static class Windows 4 | { 5 | /// Optimum overlap in ratio of width, used for Welch method 6 | /// Normalized Equivalent Noise BandWidth with unit of bins 7 | public static Memory HFT95(int width, out double optimumOverlap, out double NENBW) 8 | { 9 | // HFT95 - https://holometer.fnal.gov/GH_FFT.pdf 10 | // wj = 1 − 1.9383379 cos(z) + 1.3045202 cos(2z) − 0.4028270 cos(3z) + 0.0350665 cos(4z). 11 | optimumOverlap = 0.756; 12 | NENBW = 3.8112; 13 | Memory window = new double[width]; 14 | var windowSpan = window.Span; 15 | double angleIncrement = 2.0 * Math.PI / width; 16 | for (int i = 0; i < width; i++) 17 | { 18 | double z = angleIncrement * i; 19 | double wj = 1 - (1.9383379 * Math.Cos(z)) + (1.3045202 * Math.Cos(2 * z)) - (0.4028270 * Math.Cos(3 * z)) + (0.0350665 * Math.Cos(4 * z)); 20 | windowSpan[i] = wj; 21 | } 22 | return window; 23 | } 24 | 25 | /// Optimum overlap in ratio of width, used for Welch method 26 | /// Normalized Equivalent Noise BandWidth with unit of bins 27 | public static Memory HFT90D(int width, out double optimumOverlap, out double NENBW) 28 | { 29 | // HFT90D - https://holometer.fnal.gov/GH_FFT.pdf 30 | // wj = 1 − 1.942604 cos(z) + 1.340318 cos(2z) − 0.440811 cos(3z) + 0.043097 cos(4z). 31 | optimumOverlap = 0.76; 32 | NENBW = 3.8832; 33 | Memory window = new double[width]; 34 | var windowSpan = window.Span; 35 | double angleIncrement = 2.0 * Math.PI / width; 36 | for (int i = 0; i < width; i++) 37 | { 38 | double z = angleIncrement * i; 39 | double wj = 1 - (1.942604 * Math.Cos(z)) + (1.340318 * Math.Cos(2 * z)) - (0.440811 * Math.Cos(3 * z)) + (0.043097 * Math.Cos(4 * z)); 40 | windowSpan[i] = wj; 41 | } 42 | 43 | return window; 44 | } 45 | 46 | /// Optimum overlap in ratio of width, used for Welch method 47 | /// Normalized Equivalent Noise BandWidth with unit of bins 48 | public static Memory FTNI(int width, out double optimumOverlap, out double NENBW) 49 | { 50 | // FTNI - https://holometer.fnal.gov/GH_FFT.pdf 51 | // wj = 0.2810639 − 0.5208972 cos(z) + 0.1980399 cos(2z). 52 | optimumOverlap = 0.656; 53 | NENBW = 2.9656; 54 | Memory window = new double[width]; 55 | var windowSpan = window.Span; 56 | double angleIncrement = 2.0 * Math.PI / width; 57 | for (int i = 0; i < width; i++) 58 | { 59 | double z = angleIncrement * i; 60 | double wj = 0.2810639 - (0.5208972 * Math.Cos(z)) + (0.1980399 * Math.Cos(2 * z)); 61 | windowSpan[i] = wj; 62 | } 63 | return window; 64 | } 65 | } 66 | } 67 | --------------------------------------------------------------------------------