├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── dev ├── example1.png ├── example2.png ├── example3.png ├── fit.gif ├── fit2.gif └── icon │ ├── icon.bat │ ├── icon.ico │ ├── icon.png │ └── icon.svg └── src ├── SwarmFit.Benchmarks ├── Program.cs ├── Simple.cs ├── SwarmFit.Benchmarks.csproj └── run.bat ├── SwarmFit.Demo ├── Form1.Designer.cs ├── Form1.cs ├── Form1.resx ├── Plotting.cs ├── Program.cs └── SwarmFit.Demo.csproj ├── SwarmFit.Tests ├── Animation.cs ├── KnownSolutions.cs ├── Plotting.cs ├── Quickstart.cs ├── RandomNumberGeneratorTests.cs └── SwarmFit.Tests.csproj ├── SwarmFit.sln └── SwarmFit ├── Extensions.cs ├── FitSolution.cs ├── IFunction.cs ├── IRandomNumberGenerator.cs ├── ParameterLimits.cs ├── Particle.cs ├── QuickFit.cs ├── RandomNumberGenerators ├── CryptoRandom.cs ├── SystemRandom.cs └── XorShift.cs ├── SampleData.cs ├── StandardFunction.cs ├── StandardFunctions ├── ExpDec1.cs ├── ExpDecay1.cs └── Linear.cs ├── SwarmFit.csproj ├── SwarmFitter.cs ├── icon.png └── nuget-readme.md /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | pull_request: 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Setup dotnet 14 | uses: actions/setup-dotnet@v3 15 | - name: Setup Validator 16 | run: dotnet tool install --global dotnet-validate --version 0.0.1-preview.304 17 | - name: Restore 18 | run: dotnet restore ./src 19 | - name: Build 20 | run: dotnet build ./src 21 | - name: Test 22 | run: dotnet test ./src 23 | - name: Pack 24 | run: dotnet pack ./src/SwarmFit/ -o packages 25 | - name: Validate Packages 26 | run: dotnet validate package local ./packages/*.nupkg 27 | - name: Setup NuGet 28 | if: github.ref == 'refs/heads/main' 29 | uses: nuget/setup-nuget@v2 30 | with: 31 | nuget-api-key: ${{ secrets.NUGET_API_KEY }} 32 | - name: Deploy NuGet Packages 33 | if: github.ref == 'refs/heads/main' 34 | run: nuget push packages/*.nupkg -SkipDuplicate -Source https://api.nuget.org/v3/index.json -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /packages/ 2 | 3 | ## Ignore Visual Studio temporary files, build results, and 4 | ## files generated by popular Visual Studio add-ons. 5 | ## 6 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore 7 | 8 | # User-specific files 9 | *.rsuser 10 | *.suo 11 | *.user 12 | *.userosscache 13 | *.sln.docstates 14 | 15 | # User-specific files (MonoDevelop/Xamarin Studio) 16 | *.userprefs 17 | 18 | # Mono auto generated files 19 | mono_crash.* 20 | 21 | # Build results 22 | [Dd]ebug/ 23 | [Dd]ebugPublic/ 24 | [Rr]elease/ 25 | [Rr]eleases/ 26 | x64/ 27 | x86/ 28 | [Ww][Ii][Nn]32/ 29 | [Aa][Rr][Mm]/ 30 | [Aa][Rr][Mm]64/ 31 | bld/ 32 | [Bb]in/ 33 | [Oo]bj/ 34 | [Ll]og/ 35 | [Ll]ogs/ 36 | 37 | # Visual Studio 2015/2017 cache/options directory 38 | .vs/ 39 | # Uncomment if you have tasks that create the project's static files in wwwroot 40 | #wwwroot/ 41 | 42 | # Visual Studio 2017 auto generated files 43 | Generated\ Files/ 44 | 45 | # MSTest test Results 46 | [Tt]est[Rr]esult*/ 47 | [Bb]uild[Ll]og.* 48 | 49 | # NUnit 50 | *.VisualState.xml 51 | TestResult.xml 52 | nunit-*.xml 53 | 54 | # Build Results of an ATL Project 55 | [Dd]ebugPS/ 56 | [Rr]eleasePS/ 57 | dlldata.c 58 | 59 | # Benchmark Results 60 | BenchmarkDotNet.Artifacts/ 61 | 62 | # .NET Core 63 | project.lock.json 64 | project.fragment.lock.json 65 | artifacts/ 66 | 67 | # ASP.NET Scaffolding 68 | ScaffoldingReadMe.txt 69 | 70 | # StyleCop 71 | StyleCopReport.xml 72 | 73 | # Files built by Visual Studio 74 | *_i.c 75 | *_p.c 76 | *_h.h 77 | *.ilk 78 | *.meta 79 | *.obj 80 | *.iobj 81 | *.pch 82 | *.pdb 83 | *.ipdb 84 | *.pgc 85 | *.pgd 86 | *.rsp 87 | *.sbr 88 | *.tlb 89 | *.tli 90 | *.tlh 91 | *.tmp 92 | *.tmp_proj 93 | *_wpftmp.csproj 94 | *.log 95 | *.tlog 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 6 auto-generated project file (contains which files were open etc.) 300 | *.vbp 301 | 302 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 303 | *.dsw 304 | *.dsp 305 | 306 | # Visual Studio 6 technical files 307 | *.ncb 308 | *.aps 309 | 310 | # Visual Studio LightSwitch build output 311 | **/*.HTMLClient/GeneratedArtifacts 312 | **/*.DesktopClient/GeneratedArtifacts 313 | **/*.DesktopClient/ModelManifest.xml 314 | **/*.Server/GeneratedArtifacts 315 | **/*.Server/ModelManifest.xml 316 | _Pvt_Extensions 317 | 318 | # Paket dependency manager 319 | .paket/paket.exe 320 | paket-files/ 321 | 322 | # FAKE - F# Make 323 | .fake/ 324 | 325 | # CodeRush personal settings 326 | .cr/personal 327 | 328 | # Python Tools for Visual Studio (PTVS) 329 | __pycache__/ 330 | *.pyc 331 | 332 | # Cake - Uncomment if you are using it 333 | # tools/** 334 | # !tools/packages.config 335 | 336 | # Tabs Studio 337 | *.tss 338 | 339 | # Telerik's JustMock configuration file 340 | *.jmconfig 341 | 342 | # BizTalk build output 343 | *.btp.cs 344 | *.btm.cs 345 | *.odx.cs 346 | *.xsd.cs 347 | 348 | # OpenCover UI analysis results 349 | OpenCover/ 350 | 351 | # Azure Stream Analytics local run output 352 | ASALocalRun/ 353 | 354 | # MSBuild Binary and Structured Log 355 | *.binlog 356 | 357 | # NVidia Nsight GPU debugger configuration file 358 | *.nvuser 359 | 360 | # MFractors (Xamarin productivity tool) working folder 361 | .mfractor/ 362 | 363 | # Local History for Visual Studio 364 | .localhistory/ 365 | 366 | # Visual Studio History (VSHistory) files 367 | .vshistory/ 368 | 369 | # BeatPulse healthcheck temp database 370 | healthchecksdb 371 | 372 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 373 | MigrationBackup/ 374 | 375 | # Ionide (cross platform F# VS Code tools) working folder 376 | .ionide/ 377 | 378 | # Fody - auto-generated XML schema 379 | FodyWeavers.xsd 380 | 381 | # VS Code files for those working on multiple tools 382 | .vscode/* 383 | !.vscode/settings.json 384 | !.vscode/tasks.json 385 | !.vscode/launch.json 386 | !.vscode/extensions.json 387 | *.code-workspace 388 | 389 | # Local History for Visual Studio Code 390 | .history/ 391 | 392 | # Windows Installer files from build outputs 393 | *.cab 394 | *.msi 395 | *.msix 396 | *.msm 397 | *.msp 398 | 399 | # JetBrains Rider 400 | *.sln.iml 401 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # SwarmFit 0.0.3 2 | _Published to NuGet on 2024-09-24_ 3 | * SwarmFitter: Improved performance by minimizing allocations (#1) @neon-sunset 4 | * Renamed classes and arguments to favor `parameters` instead of `variables` 5 | * SwarmFitter: Use a customizable `IRandomNumberGenerator` with a default that maximizes performance 6 | * IFunction: Created an interface to help display documented common functions with known typical parameters 7 | * SwarmFit: Refactored to support successive iterative parameter improvements 8 | * QuickFit: Improved default parameters and end optimizing when successive iterations no longer improve the fit 9 | 10 | # SwarmFit 0.0.2 11 | _Published to NuGet on 2024-09-14_ 12 | * Published to test automated deployment 13 | 14 | # SwarmFit 0.0.1 15 | _Published to NuGet on 2024-09-14_ 16 | * Initial Release -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Scott W Harden 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 | # SwarmFit 2 | 3 | [![CI](https://github.com/swharden/SwarmFit/actions/workflows/ci.yaml/badge.svg)](https://github.com/swharden/SwarmFit/actions/workflows/ci.yaml) 4 | 5 | **SwarmFit is a .NET package for fitting curves to X/Y data** points using [particle swarm optimization](https://en.wikipedia.org/wiki/Particle_swarm_optimization). Unlike other gradient decent strategies, finding the derivative of the error function is not required. SwarmFit can be used to calculate best fit curves for arbitrary equations that use any number of parameters. 6 | 7 | ![](dev/fit2.gif) 8 | 9 | ## Quickstart 10 | 11 | ```cs 12 | // data points to fit 13 | double[] xs = [1, 2, 3, 4, 5]; 14 | double[] ys = [304, 229, 174, 134, 111]; 15 | 16 | // a custom function to fit to the data using any number of parameters 17 | static double MyFitFunc(double x, double[] parameters) 18 | { 19 | double a = parameters[0]; 20 | double b = parameters[1]; 21 | double c = parameters[2]; 22 | return a + b * Math.Exp(x * c); 23 | } 24 | 25 | // the minimum and maximum value for each parameter 26 | double[] minVars = [-100, -5000, -10]; 27 | double[] maxVars = [100, 5000, 10]; 28 | 29 | // perform the fit with general purpose settings 30 | double[] solution = QuickFit.Solve(xs, ys, MyFitFunc, minVars, maxVars); 31 | 32 | // display the solution 33 | double a = solution[0]; 34 | double b = solution[1]; 35 | double c = solution[2]; 36 | Console.WriteLine($"Y = {a} + {b} * e^(x * {c})"); 37 | ``` 38 | 39 | ## Graphical Demo 40 | 41 | A sample application is included with this repository which generates random data and fits it using common curve functions. This application demonstrates advanced features such as fitter fine-tuning, iterative error logging, charting, etc. 42 | 43 | ![](dev/example3.png) -------------------------------------------------------------------------------- /dev/example1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/SwarmFit/60fcc85184b405ef65de551261d6cd89f2d73b46/dev/example1.png -------------------------------------------------------------------------------- /dev/example2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/SwarmFit/60fcc85184b405ef65de551261d6cd89f2d73b46/dev/example2.png -------------------------------------------------------------------------------- /dev/example3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/SwarmFit/60fcc85184b405ef65de551261d6cd89f2d73b46/dev/example3.png -------------------------------------------------------------------------------- /dev/fit.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/SwarmFit/60fcc85184b405ef65de551261d6cd89f2d73b46/dev/fit.gif -------------------------------------------------------------------------------- /dev/fit2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/SwarmFit/60fcc85184b405ef65de551261d6cd89f2d73b46/dev/fit2.gif -------------------------------------------------------------------------------- /dev/icon/icon.bat: -------------------------------------------------------------------------------- 1 | ::magick convert icon.png -define icon:auto-resize=16,48,256 -compress zip icon.ico 2 | magick convert icon.png -define icon:auto-resize=48 -compress zip icon.ico 3 | pause -------------------------------------------------------------------------------- /dev/icon/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/SwarmFit/60fcc85184b405ef65de551261d6cd89f2d73b46/dev/icon/icon.ico -------------------------------------------------------------------------------- /dev/icon/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/SwarmFit/60fcc85184b405ef65de551261d6cd89f2d73b46/dev/icon/icon.png -------------------------------------------------------------------------------- /dev/icon/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 20 | 22 | 30 | 34 | 35 | 38 | 47 | 48 | 56 | 60 | 61 | 64 | 73 | 74 | 82 | 86 | 87 | 90 | 99 | 100 | 108 | 112 | 113 | 116 | 125 | 126 | 127 | 149 | 152 | 153 | 155 | 156 | 158 | image/svg+xml 159 | 161 | 162 | 163 | 164 | 165 | 169 | 178 | 181 | 184 | 188 | 192 | 196 | 200 | 201 | 202 | 207 | 208 | 209 | -------------------------------------------------------------------------------- /src/SwarmFit.Benchmarks/Program.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Running; 2 | 3 | BenchmarkSwitcher 4 | .FromAssembly(typeof(Program).Assembly) 5 | .Run(args); 6 | -------------------------------------------------------------------------------- /src/SwarmFit.Benchmarks/Simple.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | 3 | namespace SwarmFit.Benchmarks; 4 | 5 | [ShortRunJob] 6 | [MemoryDiagnoser] 7 | public class Simple 8 | { 9 | [Benchmark] 10 | public void SolveDemo() 11 | { 12 | // data points to fit 13 | double[] xs = [1, 2, 3, 4, 5]; 14 | double[] ys = [304, 229, 174, 134, 111]; 15 | 16 | // a custom function to fit to the data using any number of parameters 17 | static double MyFitFunc(double x, double[] parameters) 18 | { 19 | double a = parameters[0]; 20 | double b = parameters[1]; 21 | double c = parameters[2]; 22 | return a + b * Math.Exp(x * c); 23 | } 24 | 25 | // the minimum and maximum value for each parameter 26 | double[] minVars = [-100, -5000, -10]; 27 | double[] maxVars = [100, 5000, 10]; 28 | 29 | // perform the fit with general purpose settings 30 | double[] solution = QuickFit.Solve(xs, ys, MyFitFunc, minVars, maxVars); 31 | 32 | // display the solution 33 | double a = solution[0]; 34 | double b = solution[1]; 35 | double c = solution[2]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/SwarmFit.Benchmarks/SwarmFit.Benchmarks.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | 9 | false 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/SwarmFit.Benchmarks/run.bat: -------------------------------------------------------------------------------- 1 | dotnet run --configuration release --filter * 2 | pause -------------------------------------------------------------------------------- /src/SwarmFit.Demo/Form1.Designer.cs: -------------------------------------------------------------------------------- 1 | namespace SwarmFit.Demo; 2 | 3 | partial class Form1 4 | { 5 | /// 6 | /// Required designer variable. 7 | /// 8 | private System.ComponentModel.IContainer components = null; 9 | 10 | /// 11 | /// Clean up any resources being used. 12 | /// 13 | /// true if managed resources should be disposed; otherwise, false. 14 | protected override void Dispose(bool disposing) 15 | { 16 | if (disposing && (components != null)) 17 | { 18 | components.Dispose(); 19 | } 20 | base.Dispose(disposing); 21 | } 22 | 23 | #region Windows Form Designer generated code 24 | 25 | /// 26 | /// Required method for Designer support - do not modify 27 | /// the contents of this method with the code editor. 28 | /// 29 | private void InitializeComponent() 30 | { 31 | formsPlot1 = new ScottPlot.WinForms.FormsPlot(); 32 | comboFormula = new ComboBox(); 33 | btnRandomize = new Button(); 34 | checkBox1 = new CheckBox(); 35 | btnStep = new Button(); 36 | btnSolve = new Button(); 37 | richTextBox1 = new RichTextBox(); 38 | btnImprove = new Button(); 39 | btnZeroError = new Button(); 40 | btnImproveStop = new Button(); 41 | SuspendLayout(); 42 | // 43 | // formsPlot1 44 | // 45 | formsPlot1.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; 46 | formsPlot1.DisplayScale = 1F; 47 | formsPlot1.Location = new Point(12, 166); 48 | formsPlot1.Name = "formsPlot1"; 49 | formsPlot1.Size = new Size(804, 421); 50 | formsPlot1.TabIndex = 8; 51 | // 52 | // comboFormula 53 | // 54 | comboFormula.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; 55 | comboFormula.DropDownStyle = ComboBoxStyle.DropDownList; 56 | comboFormula.FormattingEnabled = true; 57 | comboFormula.Location = new Point(12, 12); 58 | comboFormula.Name = "comboFormula"; 59 | comboFormula.Size = new Size(804, 23); 60 | comboFormula.TabIndex = 9; 61 | // 62 | // btnRandomize 63 | // 64 | btnRandomize.Location = new Point(12, 41); 65 | btnRandomize.Name = "btnRandomize"; 66 | btnRandomize.Size = new Size(97, 41); 67 | btnRandomize.TabIndex = 10; 68 | btnRandomize.Text = "Randomize"; 69 | btnRandomize.UseVisualStyleBackColor = true; 70 | // 71 | // checkBox1 72 | // 73 | checkBox1.AutoSize = true; 74 | checkBox1.Location = new Point(115, 54); 75 | checkBox1.Name = "checkBox1"; 76 | checkBox1.Size = new Size(56, 19); 77 | checkBox1.TabIndex = 14; 78 | checkBox1.Text = "Noise"; 79 | checkBox1.UseVisualStyleBackColor = true; 80 | // 81 | // btnStep 82 | // 83 | btnStep.Location = new Point(177, 41); 84 | btnStep.Name = "btnStep"; 85 | btnStep.Size = new Size(97, 41); 86 | btnStep.TabIndex = 15; 87 | btnStep.Text = "Iterate Once"; 88 | btnStep.UseVisualStyleBackColor = true; 89 | // 90 | // btnSolve 91 | // 92 | btnSolve.Location = new Point(280, 41); 93 | btnSolve.Name = "btnSolve"; 94 | btnSolve.Size = new Size(97, 41); 95 | btnSolve.TabIndex = 16; 96 | btnSolve.Text = "Iterate 100 Times"; 97 | btnSolve.UseVisualStyleBackColor = true; 98 | // 99 | // richTextBox1 100 | // 101 | richTextBox1.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; 102 | richTextBox1.BackColor = SystemColors.Control; 103 | richTextBox1.BorderStyle = BorderStyle.None; 104 | richTextBox1.Location = new Point(12, 88); 105 | richTextBox1.Name = "richTextBox1"; 106 | richTextBox1.ReadOnly = true; 107 | richTextBox1.Size = new Size(804, 72); 108 | richTextBox1.TabIndex = 17; 109 | richTextBox1.Text = ""; 110 | // 111 | // btnImprove 112 | // 113 | btnImprove.Location = new Point(383, 41); 114 | btnImprove.Name = "btnImprove"; 115 | btnImprove.Size = new Size(97, 41); 116 | btnImprove.TabIndex = 18; 117 | btnImprove.Text = "Iterate Until Improved"; 118 | btnImprove.UseVisualStyleBackColor = true; 119 | // 120 | // btnZeroError 121 | // 122 | btnZeroError.Location = new Point(486, 42); 123 | btnZeroError.Name = "btnZeroError"; 124 | btnZeroError.Size = new Size(97, 41); 125 | btnZeroError.TabIndex = 19; 126 | btnZeroError.Text = "Iterate Until Zero Error"; 127 | btnZeroError.UseVisualStyleBackColor = true; 128 | // 129 | // button1 130 | // 131 | btnImproveStop.Location = new Point(589, 42); 132 | btnImproveStop.Name = "button1"; 133 | btnImproveStop.Size = new Size(129, 41); 134 | btnImproveStop.TabIndex = 20; 135 | btnImproveStop.Text = "Iterate Until Improvements Stop"; 136 | btnImproveStop.UseVisualStyleBackColor = true; 137 | // 138 | // Form1 139 | // 140 | AutoScaleDimensions = new SizeF(7F, 15F); 141 | AutoScaleMode = AutoScaleMode.Font; 142 | ClientSize = new Size(828, 599); 143 | Controls.Add(btnImproveStop); 144 | Controls.Add(btnZeroError); 145 | Controls.Add(btnImprove); 146 | Controls.Add(richTextBox1); 147 | Controls.Add(btnSolve); 148 | Controls.Add(btnStep); 149 | Controls.Add(checkBox1); 150 | Controls.Add(btnRandomize); 151 | Controls.Add(comboFormula); 152 | Controls.Add(formsPlot1); 153 | Name = "Form1"; 154 | StartPosition = FormStartPosition.CenterScreen; 155 | Text = "SwarmFit Demo"; 156 | ResumeLayout(false); 157 | PerformLayout(); 158 | } 159 | 160 | #endregion 161 | private ScottPlot.WinForms.FormsPlot formsPlot1; 162 | private ComboBox comboFormula; 163 | private Button btnRandomize; 164 | private CheckBox checkBox1; 165 | private Button btnStep; 166 | private Button btnSolve; 167 | private RichTextBox richTextBox1; 168 | private Button btnImprove; 169 | private Button btnZeroError; 170 | private Button btnImproveStop; 171 | } -------------------------------------------------------------------------------- /src/SwarmFit.Demo/Form1.cs: -------------------------------------------------------------------------------- 1 | namespace SwarmFit.Demo; 2 | 3 | public partial class Form1 : Form 4 | { 5 | double[] Xs = []; 6 | double[] Ys = []; 7 | 8 | SwarmFitter? Fitter; 9 | 10 | readonly IFunction[] StandardFunctions = StandardFunction.GetAll(); 11 | 12 | IFunction SelectedFunction => StandardFunctions[comboFormula.SelectedIndex]; 13 | 14 | public Form1() 15 | { 16 | InitializeComponent(); 17 | StandardFunctions.ToList().ForEach(x => comboFormula.Items.Add($"{x.Name} ({x.Formula()})")); 18 | comboFormula.SelectedIndex = 0; 19 | comboFormula.SelectedIndexChanged += (s, e) => Randomize(); 20 | btnRandomize.Click += (s, e) => Randomize(); 21 | btnStep.Click += (s, e) => Iterate(1); 22 | btnSolve.Click += (s, e) => Iterate(100); 23 | btnImprove.Click += (s, e) => IterateUntilImproved(); 24 | btnZeroError.Click += (s, e) => IterateUntilNoError(); 25 | btnImproveStop.Click += (s, e) => IterateUntilImprovementsStop(); 26 | checkBox1.CheckedChanged += (s, e) => Randomize(); 27 | Load += (s, e) => Randomize(); 28 | } 29 | 30 | void Iterate(int count) 31 | { 32 | if (Fitter is null) 33 | return; 34 | 35 | FitSolution solution = Fitter.Solve(count); 36 | DisplayResult(solution); 37 | } 38 | 39 | void IterateUntilImproved() 40 | { 41 | if (Fitter is null) 42 | return; 43 | 44 | // iterate until a single improvement is achieved, but set the maximum limit anyway to 45 | // a very large number to prevent an infinite loop if a perfect solution has already been found. 46 | FitSolution solution = Fitter.Solve(maxIterations: 1_000_000, maxImprovements: 1); 47 | DisplayResult(solution); 48 | } 49 | 50 | void IterateUntilNoError() 51 | { 52 | if (Fitter is null) 53 | return; 54 | 55 | // iterate until a single improvement is achieved, but set the maximum limit anyway to 56 | // a very large number to prevent an infinite loop if a perfect solution has already been found. 57 | FitSolution solution = Fitter.Solve(maxIterations: 1_000_000, errorThreshold: 0); 58 | DisplayResult(solution); 59 | } 60 | 61 | void IterateUntilImprovementsStop() 62 | { 63 | if (Fitter is null) 64 | return; 65 | 66 | // iterate until a single improvement is achieved, but set the maximum limit anyway to 67 | // a very large number to prevent an infinite loop if a perfect solution has already been found. 68 | while (true) 69 | { 70 | int originalImprovements = Fitter.ImprovementCount; 71 | Fitter.Solve(maxIterations: 1000, errorThreshold: 0); 72 | if (Fitter.ImprovementCount == originalImprovements) 73 | { 74 | break; 75 | } 76 | } 77 | 78 | DisplayResult(Fitter.BestSolution); 79 | } 80 | 81 | void Randomize() 82 | { 83 | double[] parameters = SelectedFunction.TypicalParameterLimits.Select(x => x.Random(Random.Shared)).ToArray(); 84 | 85 | int count = 10; 86 | Xs = ScottPlot.Generate.Range(SelectedFunction.TypicalXRange.min, SelectedFunction.TypicalXRange.max, n: count).ToArray(); 87 | Ys = Xs.Select(x => SelectedFunction.Function.Invoke(x, parameters)).ToArray(); 88 | 89 | if (checkBox1.Checked) 90 | { 91 | double ySpan = Ys.Max() - Ys.Min(); 92 | double amount = 0.2; 93 | for (int i = 0; i < Ys.Length; i++) 94 | { 95 | Ys[i] += ySpan * amount * (Random.Shared.NextDouble() - .5) * 2; 96 | } 97 | } 98 | 99 | Plotting.PlotDataPoints(formsPlot1.Plot, Xs, Ys); 100 | formsPlot1.Plot.Title(SelectedFunction.Formula()); 101 | formsPlot1.Refresh(); 102 | 103 | Fitter = new(Xs, Ys, SelectedFunction.Function, SelectedFunction.TypicalParameterLimits); 104 | } 105 | 106 | void DisplayResult(FitSolution solution) 107 | { 108 | if (Fitter is null) 109 | return; 110 | 111 | richTextBox1.Text = $"Calculation time: {solution.Elapsed.TotalMilliseconds:N2} msec\n" + 112 | $"Iterations: {solution.Iterations:N0}\n" + 113 | $"Improvements: {solution.ImprovementCount:N0}\n" + 114 | $"Error: {solution.Error}"; 115 | 116 | Plotting.PlotFitCurve(formsPlot1.Plot, SelectedFunction, solution); 117 | formsPlot1.Plot.Title(SelectedFunction.Formula(solution.Parameters)); 118 | formsPlot1.Refresh(); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/SwarmFit.Demo/Form1.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | -------------------------------------------------------------------------------- /src/SwarmFit.Demo/Plotting.cs: -------------------------------------------------------------------------------- 1 | using ScottPlot; 2 | 3 | namespace SwarmFit.Demo; 4 | 5 | internal static class Plotting 6 | { 7 | public static void PlotDataPoints(Plot plot, double[] xs, double[] ys) 8 | { 9 | plot.Clear(); 10 | var pts = plot.Add.Markers(xs, ys); 11 | pts.MarkerSize = 10; 12 | plot.Axes.AutoScale(); 13 | } 14 | 15 | public static void PlotFitCurve(Plot plot, IFunction function, FitSolution solution) 16 | { 17 | plot.Clear(); 18 | 19 | double fitXMin = plot.Axes.Bottom.Min; 20 | double fitXMax = plot.Axes.Bottom.Max; 21 | double[] fitXs = Generate.Range(fitXMin, fitXMax, (fitXMax - fitXMin) / 100); 22 | double[] fitYs = fitXs.Select(x => function.Function.Invoke(x, solution.Parameters)).ToArray(); 23 | 24 | var sl = plot.Add.ScatterLine(fitXs, fitYs); 25 | sl.LineWidth = 2; 26 | sl.Color = Colors.Black; 27 | sl.LinePattern = LinePattern.DenselyDashed; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/SwarmFit.Demo/Program.cs: -------------------------------------------------------------------------------- 1 | namespace SwarmFit.Demo; 2 | 3 | static class Program 4 | { 5 | /// 6 | /// The main entry point for the application. 7 | /// 8 | [STAThread] 9 | static void Main() 10 | { 11 | // To customize application configuration such as set high DPI settings or default font, 12 | // see https://aka.ms/applicationconfiguration. 13 | ApplicationConfiguration.Initialize(); 14 | Application.Run(new Form1()); 15 | } 16 | } -------------------------------------------------------------------------------- /src/SwarmFit.Demo/SwarmFit.Demo.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | WinExe 5 | net8.0-windows 6 | enable 7 | true 8 | enable 9 | true 10 | NU1701 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/SwarmFit.Tests/Animation.cs: -------------------------------------------------------------------------------- 1 | using ScottPlot; 2 | 3 | namespace SwarmFit.Tests; 4 | 5 | public class Animation 6 | { 7 | [Ignore("only used for creating graphics for the website")] 8 | [Test] 9 | public void Test_Fit_Rainbow() 10 | { 11 | 12 | static double MyFunc(double x, double[] parameters) 13 | { 14 | double a = parameters[0]; 15 | double b = parameters[1]; 16 | double c = parameters[2]; 17 | return a + b * Math.Exp(-x * c); 18 | } 19 | 20 | double[] parameters = [5, 7, .7]; 21 | double[] xs = ScottPlot.Generate.Consecutive(10, first: 1); 22 | double[] ys = xs.Select(x => MyFunc(x, parameters)).ToArray(); 23 | ScottPlot.Generate.AddNoiseInPlace(xs, .3); 24 | ScottPlot.Generate.AddNoiseInPlace(ys, .3); 25 | 26 | ParameterLimits[] limits = [ 27 | new(0, 20), 28 | new(0, 20), 29 | new (.1, 10) 30 | ]; 31 | 32 | SwarmFitter fitter = new(xs, ys, MyFunc, limits); 33 | 34 | Plot plot = new(); 35 | 36 | var dataPoints = plot.Add.Markers(fitter.Xs, fitter.Ys); 37 | dataPoints.MarkerSize = 15; 38 | dataPoints.MarkerShape = MarkerShape.OpenCircle; 39 | dataPoints.MarkerLineWidth = 2; 40 | dataPoints.Color = Colors.Black; 41 | 42 | plot.Axes.AutoScale(); 43 | plot.Axes.ZoomOut(1.5, 1.5); 44 | double[] fitXs = Generate.Range(plot.Axes.Bottom.Range.Min, plot.Axes.Bottom.Range.Max, plot.Axes.Bottom.Range.Span / 100); 45 | ScottPlot.Colormaps.Turbo cmap = new(); 46 | 47 | int improvements = 150; 48 | for (int i = 0; i < improvements; i++) 49 | { 50 | FitSolution solution = fitter.Solve(maxImprovements: 1); 51 | 52 | double a = solution.Parameters[0]; 53 | double b = solution.Parameters[1]; 54 | double c = solution.Parameters[2]; 55 | string formula = $"Y = {a:N3} + {b:N3} * e^(-x * {c:N3})"; 56 | plot.Title(formula); 57 | 58 | plot.Clear(); 59 | double[] fitYs = fitXs.Select(x => MyFunc(x, solution.Parameters)).ToArray(); 60 | var tempLine = plot.Add.ScatterLine(fitXs, fitYs); 61 | tempLine.Color = cmap.GetColor(i, improvements, .2, 1).WithAlpha(.7); 62 | tempLine.LineWidth = 4; 63 | 64 | plot.SavePng($"animation/test-{i:0000}.png", 400, 300); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/SwarmFit.Tests/KnownSolutions.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | 3 | namespace SwarmFit.Tests; 4 | 5 | public class KnownSolutions 6 | { 7 | [Test] 8 | public void Test_Fit_Values() 9 | { 10 | double[] parameters = [5, 7, 0.3]; 11 | static double MyFunc(double x, double[] p) => p[0] + p[1] * Math.Exp(p[2] * x); 12 | 13 | double[] xs = [7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18]; 14 | double[] ys = xs.Select(x => MyFunc(x, parameters)).ToArray(); 15 | 16 | // Define limits for each variable 17 | double[] minVars = [0, 0, 0]; 18 | double[] maxVars = [10, 10, 1]; 19 | 20 | // Find a solution for the best parameters to fit the curve to the data points 21 | double[] solution = QuickFit.Solve(xs, ys, MyFunc, minVars, maxVars); 22 | 23 | solution[0].Should().BeApproximately(parameters[0], 1e-5); 24 | solution[1].Should().BeApproximately(parameters[1], 1e-5); 25 | solution[2].Should().BeApproximately(parameters[2], 1e-5); 26 | 27 | Plotting.PlotFit(xs, ys, MyFunc, solution).SavePng("Test_Fit_Values.png", 400, 300); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/SwarmFit.Tests/Plotting.cs: -------------------------------------------------------------------------------- 1 | namespace SwarmFit.Tests; 2 | 3 | internal static class Plotting 4 | { 5 | public static ScottPlot.Plot PlotFit(double[] xs, double[] ys, Func func, double[] solution) 6 | { 7 | ScottPlot.Plot plot = new(); 8 | 9 | var marks = plot.Add.ScatterPoints(xs, ys); 10 | marks.MarkerSize = 10; 11 | plot.Axes.AutoScale(); 12 | 13 | var limits = plot.Axes.GetLimits(); 14 | double[] fitXs = ScottPlot.Generate.Range(limits.Left, limits.Right, limits.HorizontalSpan / 100); 15 | double[] fitYs = fitXs.Select(x => func(x, solution)).ToArray(); 16 | var line = plot.Add.ScatterLine(fitXs, fitYs); 17 | line.LineWidth = 2; 18 | 19 | return plot; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/SwarmFit.Tests/Quickstart.cs: -------------------------------------------------------------------------------- 1 | using ScottPlot; 2 | 3 | namespace SwarmFit.Tests; 4 | 5 | public class Quickstart 6 | { 7 | [Test] 8 | public void Test_Fit_Quickstart() 9 | { 10 | // data points to fit 11 | double[] xs = [1, 2, 3, 4, 5]; 12 | double[] ys = [304, 229, 174, 134, 111]; 13 | 14 | // define a fit function using any number of variables. 15 | static double MyFunc(double x, double[] vars) 16 | { 17 | // Y = A + B * e^(x*C) 18 | return vars[0] + vars[1] * Math.Exp(vars[2] * x); 19 | } 20 | 21 | // define the minimum and maximum value for each variable 22 | double[] minVars = [-100, -5000, -10]; 23 | double[] maxVars = [100, 5000, 10]; 24 | 25 | // perform the fit 26 | double[] solution = QuickFit.Solve(xs, ys, MyFunc, minVars, maxVars); 27 | 28 | // display the solution 29 | double a = solution[0]; 30 | double b = solution[1]; 31 | double c = solution[2]; 32 | Console.WriteLine($"Y = {a:N2} + {b:N2} * e^(x * {c:N2})"); 33 | } 34 | 35 | [Test] 36 | public void Test_Logo() 37 | { 38 | double[] xs = [7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18]; 39 | double[] ys = [304.08994, 229.13878, 173.71886, 135.75499, 40 | 111.096794, 94.25109, 81.55578, 71.30187, 41 | 62.146603, 54.212032, 49.20715, 46.765743]; 42 | 43 | static double Exponential3P(double x, double[] parameters) 44 | { 45 | return parameters[0] * Math.Exp(parameters[1] * x) + parameters[2]; 46 | } 47 | 48 | ParameterLimits[] limits = [new(-5000, 5000), new(-100, 100), new(-100, 100)]; 49 | SwarmFitter fitter = new(xs, ys, Exponential3P, limits); 50 | FitSolution solution = fitter.Solve(10_000); 51 | 52 | double[] fitYs = xs.Select(x => Exponential3P(x, solution.Parameters)).ToArray(); 53 | 54 | Plot plot = new(); 55 | var data = plot.Add.Markers(xs, ys); 56 | data.LegendText = "Data Points"; 57 | 58 | var fit = plot.Add.ScatterLine(xs, fitYs); 59 | fit.LegendText = "Fitted Curve"; 60 | fit.LineWidth = 2; 61 | fit.LinePattern = LinePattern.DenselyDashed; 62 | fit.LineColor = Colors.Black; 63 | 64 | plot.Legend.Alignment = Alignment.UpperRight; 65 | 66 | plot.Title($"Y = {solution.Parameters[2]:0.00} + {solution.Parameters[0]:0.00} * e^({solution.Parameters[1]:0.00} * x)"); 67 | plot.Axes.Title.Label.Bold = false; 68 | 69 | var saved = plot.SavePng("test.png", 400, 300); 70 | Console.WriteLine(saved); 71 | //saved.LaunchInBrowser(); 72 | } 73 | } -------------------------------------------------------------------------------- /src/SwarmFit.Tests/RandomNumberGeneratorTests.cs: -------------------------------------------------------------------------------- 1 | namespace SwarmFit.Tests; 2 | 3 | class RandomNumberGeneratorTests 4 | { 5 | [Test] 6 | public void Test_SystemRandom_ValuesAreUnique() 7 | { 8 | // NOTE: a duplicate is found by luck after 66k iterations 9 | AssertNoDuplicates(new RandomNumberGenerators.SystemRandom(randomSeed: false), 50_000); 10 | } 11 | 12 | [Test] 13 | public void Test_XorShift_ValuesAreUnique() 14 | { 15 | AssertNoDuplicates(new RandomNumberGenerators.XorShift(), 100_000); 16 | } 17 | 18 | private static void AssertNoDuplicates(IRandomNumberGenerator RNG, int count) 19 | { 20 | HashSet seen = []; 21 | for (int i = 0; i < count; i++) 22 | { 23 | double value = RNG.NextDouble(); 24 | if (seen.Contains(value)) 25 | throw new InvalidOperationException($"duplicate number found after {i} iterations"); 26 | seen.Add(value); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/SwarmFit.Tests/SwarmFit.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | false 9 | true 10 | 11 | 12 | 13 | 14 | all 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | 17 | 18 | 19 | 20 | 21 | all 22 | runtime; build; native; contentfiles; analyzers; buildtransitive 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/SwarmFit.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.10.35122.118 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SwarmFit", "SwarmFit\SwarmFit.csproj", "{3B3A28AA-135A-493E-99D9-CC5625D24346}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SwarmFit.Demo", "SwarmFit.Demo\SwarmFit.Demo.csproj", "{C663E7B3-03C5-43B9-B92F-4FA9970B2DA8}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SwarmFit.Tests", "SwarmFit.Tests\SwarmFit.Tests.csproj", "{5B93F4D3-B9F2-495A-830E-63CB96A3A699}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SwarmFit.Benchmarks", "SwarmFit.Benchmarks\SwarmFit.Benchmarks.csproj", "{0F1DA296-6DFC-4C07-9172-2EAB50CA6A45}" 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 | {3B3A28AA-135A-493E-99D9-CC5625D24346}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {3B3A28AA-135A-493E-99D9-CC5625D24346}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {3B3A28AA-135A-493E-99D9-CC5625D24346}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {3B3A28AA-135A-493E-99D9-CC5625D24346}.Release|Any CPU.Build.0 = Release|Any CPU 24 | {C663E7B3-03C5-43B9-B92F-4FA9970B2DA8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {C663E7B3-03C5-43B9-B92F-4FA9970B2DA8}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {C663E7B3-03C5-43B9-B92F-4FA9970B2DA8}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {C663E7B3-03C5-43B9-B92F-4FA9970B2DA8}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {5B93F4D3-B9F2-495A-830E-63CB96A3A699}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {5B93F4D3-B9F2-495A-830E-63CB96A3A699}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {5B93F4D3-B9F2-495A-830E-63CB96A3A699}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {5B93F4D3-B9F2-495A-830E-63CB96A3A699}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {0F1DA296-6DFC-4C07-9172-2EAB50CA6A45}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {0F1DA296-6DFC-4C07-9172-2EAB50CA6A45}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {0F1DA296-6DFC-4C07-9172-2EAB50CA6A45}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {0F1DA296-6DFC-4C07-9172-2EAB50CA6A45}.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 = {E45BA962-1CFA-4982-9EF1-FAB43E624A03} 42 | EndGlobalSection 43 | EndGlobal 44 | -------------------------------------------------------------------------------- /src/SwarmFit/Extensions.cs: -------------------------------------------------------------------------------- 1 | namespace SwarmFit; 2 | 3 | public static class Extensions 4 | { 5 | 6 | } 7 | -------------------------------------------------------------------------------- /src/SwarmFit/FitSolution.cs: -------------------------------------------------------------------------------- 1 | namespace SwarmFit; 2 | 3 | public class FitSolution(double[] parameters, double error, TimeSpan elapsed, int iterations, int improvements) 4 | { 5 | public double[] Parameters { get; } = [.. parameters]; 6 | public double Error { get; } = error; 7 | public TimeSpan Elapsed { get; } = elapsed; 8 | public int Iterations { get; } = iterations; 9 | public int ImprovementCount { get; } = improvements; 10 | }; -------------------------------------------------------------------------------- /src/SwarmFit/IFunction.cs: -------------------------------------------------------------------------------- 1 | namespace SwarmFit; 2 | 3 | /// 4 | /// Classes implement this to pair a function with information that describes it 5 | /// 6 | public interface IFunction 7 | { 8 | /// 9 | /// Common name for the formula 10 | /// 11 | string Name { get; } 12 | 13 | /// 14 | /// Text description of the formula (e.g., Y = m * X + b) 15 | /// 16 | string Formula(); 17 | 18 | /// 19 | /// Text description of the formula using the given parameters 20 | /// 21 | string Formula(double[] parameters, int precision = 3); 22 | 23 | /// 24 | /// Number of parameters to use when calling 25 | /// 26 | int ParameterCount { get; } 27 | 28 | /// 29 | /// Typical parameter ranges that may be used to evaluate the shape of the curve 30 | /// 31 | public ParameterLimits[] TypicalParameterLimits { get; } 32 | 33 | /// 34 | /// Typical horizontal range used to evaluate the shape of the curve 35 | /// 36 | public (double min, double max) TypicalXRange { get; } 37 | 38 | /// 39 | /// The function that returns a Y given an X and a collection of parameters (with a length equal to ) 40 | /// 41 | Func Function { get; } 42 | } 43 | -------------------------------------------------------------------------------- /src/SwarmFit/IRandomNumberGenerator.cs: -------------------------------------------------------------------------------- 1 | namespace SwarmFit; 2 | 3 | public interface IRandomNumberGenerator 4 | { 5 | /// 6 | /// Return a random double between 0 and 1 7 | /// 8 | public double NextDouble(); 9 | } 10 | -------------------------------------------------------------------------------- /src/SwarmFit/ParameterLimits.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace SwarmFit; 4 | 5 | public readonly struct ParameterLimits(double min, double max) 6 | { 7 | public double Min { get; } = Math.Min(min, max); 8 | public double Max { get; } = Math.Max(min, max); 9 | public double Mid => (Min + Max) / 2; 10 | public double Span => Max - Min; 11 | public double Random(IRandomNumberGenerator rand) => Span * rand.NextDouble() + Min; 12 | public double Random(Random rand) => Span * rand.NextDouble() + Min; 13 | 14 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 15 | public double Clamp(double value) 16 | { 17 | if (value < Min) return Min; 18 | else if (value > Max) return Max; 19 | else return value; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/SwarmFit/Particle.cs: -------------------------------------------------------------------------------- 1 | namespace SwarmFit; 2 | 3 | public class Particle 4 | { 5 | public double[] Positions; 6 | public double[] BestPositions; 7 | public double[] Velocities; 8 | 9 | public double Error; 10 | public double BestError; 11 | 12 | public Particle(double[] pos, double err, double[] vel, double[] bestPos, double bestErr) 13 | { 14 | Positions = new double[pos.Length]; 15 | pos.CopyTo(Positions, 0); 16 | Error = err; 17 | Velocities = new double[vel.Length]; 18 | vel.CopyTo(Velocities, 0); 19 | BestPositions = new double[bestPos.Length]; 20 | bestPos.CopyTo(BestPositions, 0); 21 | BestError = bestErr; 22 | } 23 | 24 | public void RandomizePositions(IRandomNumberGenerator rand, ParameterLimits[] limits) 25 | { 26 | double[] positions = Positions; 27 | for (int i = 0; i < positions.Length; i++) 28 | { 29 | positions[i] = limits[i].Random(rand); 30 | } 31 | positions.AsSpan().CopyTo(BestPositions); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/SwarmFit/QuickFit.cs: -------------------------------------------------------------------------------- 1 | namespace SwarmFit; 2 | 3 | public static class QuickFit 4 | { 5 | /// 6 | /// This function provides a simple API for fitting parameters of a function to find the best for for a collection of X/Y data points. 7 | /// The fitter will attempt continue to optimize parameters until 500 iterations yields no further improvement. 8 | /// The swarm fitter has many configuration options which are not available when calling this function, so advanced users 9 | /// are encouraged to instantiate a and interact it with directly to find the ideal solution. 10 | /// 11 | /// horizontal data values to fit 12 | /// vertical data values to fit 13 | /// a user defined function which calculates Y given X according to a collection of parameters 14 | /// minimum possible value for each parameter 15 | /// maximum possible value for each parameter 16 | /// Number of particles to use for fitting 17 | /// Fitting will be aborted after this number of total iterations is performed 18 | /// optimized parameters for the curve 19 | /// 20 | public static double[] Solve(double[] xs, double[] ys, Func func, double[] parameterMins, double[] parameterMaxes, int numParticles = 10, int maxIterations = 10_000) 21 | { 22 | if (parameterMins.Length != parameterMaxes.Length) 23 | { 24 | throw new ArgumentException($"{nameof(parameterMins)} and {nameof(parameterMaxes)} must have equal length"); 25 | } 26 | 27 | ParameterLimits[] limits = Enumerable 28 | .Range(0, parameterMins.Length) 29 | .Select(x => new ParameterLimits(parameterMins[x], parameterMaxes[x])) 30 | .ToArray(); 31 | 32 | SwarmFitter fitter = new(xs, ys, func, limits, numParticles); 33 | 34 | while (fitter.IterationCount < maxIterations) 35 | { 36 | int originalImprovementCount = fitter.ImprovementCount; 37 | FitSolution solution = fitter.Solve(maxIterations: 500); 38 | if (fitter.ImprovementCount == originalImprovementCount) 39 | break; 40 | } 41 | 42 | return fitter.BestSolution.Parameters; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/SwarmFit/RandomNumberGenerators/CryptoRandom.cs: -------------------------------------------------------------------------------- 1 | namespace SwarmFit.RandomNumberGenerators; 2 | 3 | public class CruptoRandom : IRandomNumberGenerator 4 | { 5 | private readonly System.Security.Cryptography.RandomNumberGenerator Rand = System.Security.Cryptography.RandomNumberGenerator.Create(); 6 | 7 | readonly byte[] bytes = new byte[sizeof(int)]; 8 | 9 | public double NextDouble() 10 | { 11 | Rand.GetBytes(bytes); 12 | return BitConverter.ToInt32(bytes, 0) & (int.MaxValue - 1); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/SwarmFit/RandomNumberGenerators/SystemRandom.cs: -------------------------------------------------------------------------------- 1 | namespace SwarmFit.RandomNumberGenerators; 2 | 3 | public class SystemRandom(bool randomSeed = false) : IRandomNumberGenerator 4 | { 5 | private readonly Random Rand = new(randomSeed ? GetRandomSeed() : 0); 6 | 7 | private static int GetRandomSeed() 8 | { 9 | System.Security.Cryptography.RandomNumberGenerator RNG = System.Security.Cryptography.RandomNumberGenerator.Create(); 10 | byte[] data = new byte[sizeof(int)]; 11 | RNG.GetBytes(data); 12 | return BitConverter.ToInt32(data, 0) & (int.MaxValue - 1); 13 | } 14 | 15 | public double NextDouble() 16 | { 17 | return Rand.NextDouble(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/SwarmFit/RandomNumberGenerators/XorShift.cs: -------------------------------------------------------------------------------- 1 | namespace SwarmFit.RandomNumberGenerators; 2 | 3 | // See: https://en.wikipedia.org/wiki/Xorshift 4 | // Note: 2463534242 in binary is 10010010110101101000110010100010 which has a nice balance of 1s and 0s 5 | public class XorShift(uint seed = 2463534242) : IRandomNumberGenerator 6 | { 7 | private uint Rand = seed; 8 | 9 | public uint Next() 10 | { 11 | Rand ^= Rand << 13; 12 | Rand ^= Rand >> 17; 13 | Rand ^= Rand << 5; 14 | return Rand; 15 | } 16 | 17 | public double NextDouble() 18 | { 19 | return (double)Next() / uint.MaxValue; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/SwarmFit/SampleData.cs: -------------------------------------------------------------------------------- 1 | namespace SwarmFit; 2 | 3 | public static class SampleData 4 | { 5 | public static double[] Range(double min, double max, double delta) 6 | { 7 | int count = (int)((max - min) / delta) + 1; 8 | double[] result = new double[count]; 9 | 10 | for (int i = 0; i < count; i++) 11 | { 12 | result[i] = min + i * delta; 13 | } 14 | 15 | return result; 16 | } 17 | 18 | public static double[] Exponential(double[] xs, double a, double b, double c) 19 | { 20 | double[] ys = new double[xs.Length]; 21 | 22 | for (int i = 0; i < xs.Length; i++) 23 | { 24 | ys[i] = a + b * Math.Exp(xs[i] * c); 25 | } 26 | 27 | return ys; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/SwarmFit/StandardFunction.cs: -------------------------------------------------------------------------------- 1 | namespace SwarmFit; 2 | 3 | public static class StandardFunction 4 | { 5 | public static IFunction[] GetAll() 6 | { 7 | return System.Reflection.Assembly.GetExecutingAssembly() 8 | .GetTypes() 9 | .Where(x => x.IsClass) 10 | .Where(x => !x.IsAbstract) 11 | .Where(x => x.GetInterfaces().Contains(typeof(IFunction))) 12 | .Where(x => x.GetConstructors().Where(x => x.GetParameters().Length == 0).Any()) 13 | .Select(x => (IFunction)Activator.CreateInstance(x)!) 14 | .ToArray(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/SwarmFit/StandardFunctions/ExpDec1.cs: -------------------------------------------------------------------------------- 1 | namespace SwarmFit.StandardFunctions; 2 | 3 | // https://www.originlab.com/doc/en/Origin-Help/ExpDec1-FitFunc 4 | public class ExpDec1 : IFunction 5 | { 6 | public string Name => "One-phase exponential decay function with time constant parameter"; 7 | public string Formula() => "Y = y0 + a * Exp(-x/t)"; 8 | public string Formula(double[] parameters, int precision = 3) 9 | { 10 | double y0 = Math.Round(parameters[0], precision); 11 | double a = Math.Round(parameters[1], precision); 12 | double t = Math.Round(parameters[2], precision); 13 | return $"Y = {y0} + {a} * Exp(-x/{t})"; 14 | } 15 | 16 | public int ParameterCount => 3; 17 | public (double min, double max) TypicalXRange => (1, 10); 18 | public Func Function { get; } = (double x, double[] parameters) => 19 | { 20 | double y0 = parameters[0]; 21 | double a = parameters[1]; 22 | double t = parameters[2]; 23 | return y0 + a + Math.Exp(-x / t); 24 | }; 25 | 26 | public ParameterLimits[] TypicalParameterLimits => [ 27 | new ParameterLimits(0, 10), // y0 28 | new ParameterLimits(1, 10), // a 29 | new ParameterLimits(.1, 5), // t 30 | ]; 31 | } 32 | -------------------------------------------------------------------------------- /src/SwarmFit/StandardFunctions/ExpDecay1.cs: -------------------------------------------------------------------------------- 1 | namespace SwarmFit.StandardFunctions; 2 | 3 | // https://www.originlab.com/doc/en/Origin-Help/ExpDecay1-FitFunc 4 | public class ExpDecay1 : IFunction 5 | { 6 | public string Name => "One-phase exponential decay function with time offset"; 7 | public string Formula() => "Y = y0 + a * Exp(-(x-x0)/t)"; 8 | public string Formula(double[] parameters, int precision = 3) 9 | { 10 | double y0 = Math.Round(parameters[0], precision); 11 | double a = Math.Round(parameters[1], precision); 12 | double x0 = Math.Round(parameters[2], precision); 13 | double t = Math.Round(parameters[3], precision); 14 | return $"Y = {y0} + {a} * Exp(-(x-{x0})/{t})"; 15 | } 16 | 17 | public int ParameterCount => 4; 18 | public (double min, double max) TypicalXRange => (1, 10); 19 | public Func Function { get; } = (double x, double[] parameters) => 20 | { 21 | double y0 = parameters[0]; 22 | double a = parameters[1]; 23 | double x0 = parameters[0]; 24 | double t = parameters[1]; 25 | return y0 + a + Math.Exp(-(x - x0) / t); 26 | }; 27 | 28 | public ParameterLimits[] TypicalParameterLimits => [ 29 | new ParameterLimits(0, 10), // y0 30 | new ParameterLimits(1, 10), // a 31 | new ParameterLimits(1, 10), // x0 32 | new ParameterLimits(.1, 5), // t 33 | ]; 34 | } 35 | -------------------------------------------------------------------------------- /src/SwarmFit/StandardFunctions/Linear.cs: -------------------------------------------------------------------------------- 1 | namespace SwarmFit.StandardFunctions; 2 | 3 | public class Linear : IFunction 4 | { 5 | public string Name => "Linear"; 6 | public string Formula() => "Y=m*X+b"; 7 | public string Formula(double[] p, int precision = 3) 8 | { 9 | double m = Math.Round(p[0], precision); 10 | double b = Math.Round(p[1], precision); 11 | return $"Y={m}*X+{b}"; 12 | } 13 | 14 | public int ParameterCount => 2; 15 | public (double min, double max) TypicalXRange => (-10, 10); 16 | public Func Function { get; } = (double x, double[] parameters) => 17 | { 18 | double m = parameters[0]; 19 | double b = parameters[1]; 20 | return m * x * + b; 21 | }; 22 | public ParameterLimits[] TypicalParameterLimits => [ 23 | new(-10, 10), // m 24 | new(-10, 10), // b 25 | ]; 26 | } 27 | -------------------------------------------------------------------------------- /src/SwarmFit/SwarmFit.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0;net8.0 5 | enable 6 | enable 7 | 12 8 | SwarmFit 9 | 0.0.3 10 | MIT 11 | Scott Harden 12 | Harden Technologies, LLC 13 | Copyright 2024 (c) Scott Harden / Harden Technologies, LLC 14 | A .NET standard library for fitting curves to data using particle swarm optimization 15 | curve fit fitting curve-fit curve-fitting math equation function 16 | https://github.com/swharden/SwarmFit 17 | https://github.com/swharden/SwarmFit.git 18 | git 19 | icon.png 20 | readme.md 21 | true 22 | snupkg 23 | true 24 | true 25 | true 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | all 36 | runtime; build; native; contentfiles; analyzers; buildtransitive 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/SwarmFit/SwarmFitter.cs: -------------------------------------------------------------------------------- 1 | /* Adapted from "Particle Swarm Optimization Using C#" by James McCaffrey, 2 | * published in Visual Studio Magazine on 11/25/2013 and other texts: 3 | * https://visualstudiomagazine.com/Articles/2013/11/01/Particle-Swarm-Optimization.aspx 4 | * https://ieeexplore.ieee.org/document/9376550 5 | * https://www.mathworks.com/matlabcentral/fileexchange/58895-ppso?s_tid=blogs_rc_5 6 | */ 7 | 8 | using System.Diagnostics; 9 | using System.Runtime.CompilerServices; 10 | 11 | namespace SwarmFit; 12 | 13 | public class SwarmFitter 14 | { 15 | /// 16 | /// X values (provided by the user) that the parameters of will be optimized to fit. 17 | /// 18 | public double[] Xs { get; } 19 | 20 | /// 21 | /// Y values (provided by the user) that the parameters of will be optimized to fit 22 | /// 23 | public double[] Ys { get; } 24 | 25 | /// 26 | /// The function (provided by the user) that parameters will be optimized for to fit and 27 | /// 28 | public Func Function { get; } 29 | 30 | /// 31 | /// Bounds for each parameter used when randomizing particle positions. 32 | /// 33 | private ParameterLimits[] ParameterLimits { get; } 34 | 35 | /// 36 | /// Number of parameters to be optimized. 37 | /// This must be consistent with the size of and the 38 | /// length of the parameter array consumes. 39 | /// 40 | public int ParameterCount => ParameterLimits.Length; 41 | 42 | /// 43 | /// Source of randomness used for randomizing particle placement and inertial angle 44 | /// 45 | public IRandomNumberGenerator Rand { get; set; } = new RandomNumberGenerators.XorShift(); 46 | 47 | 48 | public double InertiaWeight { get; set; } = 0.729; 49 | public double LocalWeight { get; set; } = 1.49445; 50 | public double GlobalWeight { get; set; } = 1.49445; 51 | 52 | /// 53 | /// Fractional chance that a particle will be killed and randomized after an iteration 54 | /// 55 | public double ParticleDeathProbability = 0.01; 56 | 57 | /// 58 | /// Number of particles in the field 59 | /// 60 | public int ParticleCount => Particles.Length; 61 | 62 | private Particle[] Particles { get; } 63 | double[] BestParameters { get; } 64 | double BestError = double.MaxValue; 65 | 66 | readonly Stopwatch Stopwatch = new(); 67 | 68 | /// 69 | /// Total time the solver was running 70 | /// 71 | public TimeSpan CalculationTime => Stopwatch.Elapsed; 72 | 73 | /// 74 | /// Total number of best fit curves identified including the present one 75 | /// 76 | public int ImprovementCount { get; private set; } = 0; 77 | 78 | /// 79 | /// Total number of times the entire particle field was iterated forward 80 | /// 81 | public int IterationCount { get; private set; } = 0; 82 | 83 | public FitSolution BestSolution => new(BestParameters, BestError, CalculationTime, IterationCount, ImprovementCount); 84 | 85 | public SwarmFitter(double[] xs, double[] ys, Func func, ParameterLimits[] limits, int numParticles = 5) 86 | { 87 | if (xs.Length != ys.Length) 88 | throw new ArgumentException($"{nameof(xs)} and {nameof(ys)} must have equal length"); 89 | 90 | Xs = xs; 91 | Ys = ys; 92 | Function = func; 93 | ParameterLimits = limits; 94 | Particles = new Particle[numParticles]; 95 | BestParameters = new double[ParameterCount]; 96 | } 97 | 98 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 99 | double GetError(double[] parameters) 100 | { 101 | double error = 0; 102 | double[] xs = Xs; 103 | double[] ys = Ys; 104 | 105 | for (int i = 0; i < xs.Length; i++) 106 | { 107 | double predictedY = Function(xs[i], parameters); 108 | double actualY = ys[i]; 109 | double diff = Math.Abs(predictedY - actualY); 110 | error += diff; 111 | } 112 | 113 | return error; 114 | } 115 | 116 | /// 117 | /// Progress toward an ideal solution. 118 | /// This method may be called multiple times. 119 | /// 120 | /// Stop trying to improve the fit if this number of particle progressions has been reached 121 | /// Stop trying to improve the fit if this number of additional improvements has been made 122 | /// Stop trying to improve the fit if the total error no longer exceeds this value 123 | public FitSolution Solve(int maxIterations = 1000, int maxImprovements = int.MaxValue, float errorThreshold = 0) 124 | { 125 | Stopwatch.Start(); 126 | 127 | // Randomize all particles only if this is the first time running the solver. 128 | // This way the solve method may be called multiple times to improve the solution. 129 | if (IterationCount == 0) 130 | { 131 | RandomizeAll(); 132 | } 133 | 134 | int improvementLimit = maxImprovements == int.MaxValue ? maxImprovements : ImprovementCount + maxImprovements; 135 | 136 | for (int i = 0; i < maxIterations; i++) 137 | { 138 | IterateAllParticles(); 139 | if (ImprovementCount >= improvementLimit || BestError <= errorThreshold) 140 | break; 141 | } 142 | 143 | Stopwatch.Stop(); 144 | 145 | return BestSolution; 146 | } 147 | 148 | private void RandomizeAll() 149 | { 150 | double initialVelocityRandomness = 0.01; 151 | for (int i = 0; i < Particles.Length; i++) 152 | { 153 | double[] randomPositions = ParameterLimits.Select(x => x.Random(Rand)).ToArray(); 154 | double error = GetError(randomPositions); 155 | double[] randomVelocities = ParameterLimits.Select(x => x.Random(Rand) * x.Span * initialVelocityRandomness).ToArray(); 156 | Particles[i] = new Particle(randomPositions, error, randomVelocities, randomPositions, error); 157 | 158 | if (Particles[i].Error < BestError) 159 | { 160 | BestError = Particles[i].Error; 161 | Particles[i].Positions.AsSpan().CopyTo(BestParameters); 162 | } 163 | } 164 | } 165 | 166 | private void IterateAllParticles() 167 | { 168 | double originalBestError = BestError; 169 | 170 | foreach (Particle particle in Particles) 171 | { 172 | IterateParticle(particle); 173 | } 174 | 175 | IterationCount++; 176 | if (BestError != originalBestError) 177 | { 178 | ImprovementCount++; 179 | } 180 | } 181 | 182 | private void IterateParticle(Particle particle) 183 | { 184 | double[] positions = particle.Positions; 185 | double[] bestPositions = particle.BestPositions; 186 | double[] velocities = particle.Velocities; 187 | 188 | for (int j = 0; j < velocities.Length; j++) 189 | { 190 | double inertia = InertiaWeight * velocities[j]; 191 | double local = LocalWeight * Rand.NextDouble() * (bestPositions[j] - positions[j]); 192 | double global = GlobalWeight * Rand.NextDouble() * (BestParameters[j] - positions[j]); 193 | velocities[j] = inertia + local + global; 194 | } 195 | 196 | for (int j = 0; j < positions.Length; j++) 197 | { 198 | positions[j] = positions[j] + velocities[j]; 199 | positions[j] = ParameterLimits[j].Clamp(positions[j]); 200 | } 201 | 202 | particle.Error = GetError(positions); 203 | 204 | if (particle.Error < particle.BestError) 205 | { 206 | positions.AsSpan().CopyTo(bestPositions); 207 | particle.BestError = particle.Error; 208 | } 209 | 210 | if (particle.Error < BestError) 211 | { 212 | positions.AsSpan().CopyTo(BestParameters); 213 | BestError = particle.BestError; 214 | } 215 | 216 | bool isBestParticle = particle.BestError == BestError; 217 | 218 | if (!isBestParticle && (Rand.NextDouble() < ParticleDeathProbability)) 219 | { 220 | particle.RandomizePositions(Rand, ParameterLimits); 221 | particle.Error = GetError(particle.Positions); 222 | particle.BestError = particle.Error; 223 | 224 | if (particle.Error < BestError) 225 | { 226 | BestError = particle.Error; 227 | positions.AsSpan().CopyTo(BestParameters); 228 | } 229 | } 230 | } 231 | } -------------------------------------------------------------------------------- /src/SwarmFit/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swharden/SwarmFit/60fcc85184b405ef65de551261d6cd89f2d73b46/src/SwarmFit/icon.png -------------------------------------------------------------------------------- /src/SwarmFit/nuget-readme.md: -------------------------------------------------------------------------------- 1 | # SwarmFit 2 | 3 | **SwarmFit is a .NET package for fitting curves to X/Y data** points using particle swarm optimization. Unlike other gradient decent strategies, finding the derivative of the error function is not required. SwarmFit can be used to calculate best fit curves for arbitrary equations that use any number of parameters. 4 | 5 | ![](https://raw.githubusercontent.com/swharden/SwarmFit/main/dev/fit.gif) 6 | 7 | ## Quickstart 8 | 9 | ```cs 10 | // data points to fit 11 | double[] xs = [1, 2, 3, 4, 5]; 12 | double[] ys = [304, 229, 174, 134, 111]; 13 | 14 | // define a fit function using any number of parameters 15 | static double MyFunc(double x, double[] parameters) 16 | { 17 | double a = parameters[0]; 18 | double b = parameters[1]; 19 | double c = parameters[2]; 20 | return a + b * Math.Exp(x * c); 21 | } 22 | 23 | // define the minimum and maximum value for each parameter 24 | double[] paramMins = [-100, -5000, -10]; 25 | double[] paramMaxs = [100, 5000, 10]; 26 | 27 | // perform the fit 28 | double[] solution = QuickFit.Solve(xs, ys, MyFunc, minParams, maxParams); 29 | 30 | // display the solution 31 | double a = solution[0]; 32 | double b = solution[1]; 33 | double c = solution[2]; 34 | Console.WriteLine($"Y = {a} + {b} * e^(x * {c})"); 35 | ``` 36 | 37 | ## Documentation 38 | 39 | See [https://github.com/swharden/SwarmFit/](https://github.com/swharden/SwarmFit/) for additional code and documentation --------------------------------------------------------------------------------