├── .gitignore ├── LICENSE ├── Output-Color.png ├── Output-Label.png ├── ReadMe.md ├── Tester ├── MNIST-LabelledVectorArray-60000x100.msgpack ├── Program.cs └── Tester.csproj ├── UMAP.sln ├── UMAP ├── DefaultRandomGenerator.cs ├── DistanceCalculation.cs ├── FastRandom.cs ├── Heaps.cs ├── IProvideRandomValues.cs ├── InternalsVisibleTo.cs ├── NNDescent.cs ├── SIMD.cs ├── SIMDInt.cs ├── SparseMatrix.cs ├── ThreadSafeFastRandom.cs ├── Tree.cs ├── UMAP.csproj ├── Umap.cs └── Utils.cs ├── UnitTests ├── DeterministicRandomGenerator.cs ├── Prando.cs ├── SparseMatrixTests.cs ├── UmapTests.cs ├── UnitTestData.cs └── UnitTests.csproj └── build-nuget.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | *.binlog 2 | ## Ignore Visual Studio temporary files, build results, and 3 | ## files generated by popular Visual Studio add-ons. 4 | ## 5 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 6 | 7 | # User-specific files 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Visual Studio 2015 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | wwwroot/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | # .NET Core 47 | project.lock.json 48 | project.fragment.lock.json 49 | artifacts/ 50 | **/Properties/launchSettings.json 51 | 52 | *_i.c 53 | *_p.c 54 | *_i.h 55 | *.ilk 56 | *.meta 57 | *.obj 58 | *.pch 59 | *.pdb 60 | *.pgc 61 | *.pgd 62 | *.rsp 63 | *.sbr 64 | *.tlb 65 | *.tli 66 | *.tlh 67 | *.tmp 68 | *.tmp_proj 69 | *.log 70 | *.vspscc 71 | *.vssscc 72 | .builds 73 | *.pidb 74 | *.svclog 75 | *.scc 76 | 77 | # Chutzpah Test files 78 | _Chutzpah* 79 | 80 | # Visual C++ cache files 81 | ipch/ 82 | *.aps 83 | *.ncb 84 | *.opendb 85 | *.opensdf 86 | *.sdf 87 | *.cachefile 88 | *.VC.db 89 | *.VC.VC.opendb 90 | 91 | # Visual Studio profiler 92 | *.psess 93 | *.vsp 94 | *.vspx 95 | *.sap 96 | 97 | # TFS 2012 Local Workspace 98 | $tf/ 99 | 100 | # Guidance Automation Toolkit 101 | *.gpState 102 | 103 | # ReSharper is a .NET coding add-in 104 | _ReSharper*/ 105 | *.[Rr]e[Ss]harper 106 | *.DotSettings.user 107 | 108 | # JustCode is a .NET coding add-in 109 | .JustCode 110 | 111 | # TeamCity is a build add-in 112 | _TeamCity* 113 | 114 | # DotCover is a Code Coverage Tool 115 | *.dotCover 116 | 117 | # Visual Studio code coverage results 118 | *.coverage 119 | *.coveragexml 120 | 121 | # NCrunch 122 | _NCrunch_* 123 | .*crunch*.local.xml 124 | nCrunchTemp_* 125 | 126 | # MightyMoose 127 | *.mm.* 128 | AutoTest.Net/ 129 | 130 | # Web workbench (sass) 131 | .sass-cache/ 132 | 133 | # Installshield output folder 134 | [Ee]xpress/ 135 | 136 | # DocProject is a documentation generator add-in 137 | DocProject/buildhelp/ 138 | DocProject/Help/*.HxT 139 | DocProject/Help/*.HxC 140 | DocProject/Help/*.hhc 141 | DocProject/Help/*.hhk 142 | DocProject/Help/*.hhp 143 | DocProject/Help/Html2 144 | DocProject/Help/html 145 | 146 | # Click-Once directory 147 | publish/ 148 | 149 | # Publish Web Output 150 | *.[Pp]ublish.xml 151 | *.azurePubxml 152 | # TODO: Comment the next line if you want to checkin your web deploy settings 153 | # but database connection strings (with potential passwords) will be unencrypted 154 | *.pubxml 155 | *.publishproj 156 | 157 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 158 | # checkin your Azure Web App publish settings, but sensitive information contained 159 | # in these scripts will be unencrypted 160 | PublishScripts/ 161 | 162 | # NuGet Packages 163 | *.nupkg 164 | # The packages folder can be ignored because of Package Restore 165 | **/packages/* 166 | # except build/, which is used as an MSBuild target. 167 | !**/packages/build/ 168 | # Uncomment if necessary however generally it will be regenerated when needed 169 | #!**/packages/repositories.config 170 | # NuGet v3's project.json files produces more ignorable files 171 | *.nuget.props 172 | *.nuget.targets 173 | 174 | # Microsoft Azure Build Output 175 | csx/ 176 | *.build.csdef 177 | 178 | # Microsoft Azure Emulator 179 | ecf/ 180 | rcf/ 181 | 182 | # Windows Store app package directories and files 183 | AppPackages/ 184 | BundleArtifacts/ 185 | Package.StoreAssociation.xml 186 | _pkginfo.txt 187 | 188 | # Visual Studio cache files 189 | # files ending in .cache can be ignored 190 | *.[Cc]ache 191 | # but keep track of directories ending in .cache 192 | !*.[Cc]ache/ 193 | 194 | # Others 195 | ClientBin/ 196 | ~$* 197 | *~ 198 | *.dbmdl 199 | *.dbproj.schemaview 200 | *.jfm 201 | *.pfx 202 | *.publishsettings 203 | orleans.codegen.cs 204 | 205 | # Since there are multiple workflows, uncomment next line to ignore bower_components 206 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 207 | #bower_components/ 208 | 209 | # RIA/Silverlight projects 210 | Generated_Code/ 211 | 212 | # Backup & report files from converting an old project file 213 | # to a newer Visual Studio version. Backup files are not needed, 214 | # because we have git ;-) 215 | _UpgradeReport_Files/ 216 | Backup*/ 217 | UpgradeLog*.XML 218 | UpgradeLog*.htm 219 | 220 | # SQL Server files 221 | *.mdf 222 | *.ldf 223 | *.ndf 224 | 225 | # Business Intelligence projects 226 | *.rdl.data 227 | *.bim.layout 228 | *.bim_*.settings 229 | 230 | # Microsoft Fakes 231 | FakesAssemblies/ 232 | 233 | # GhostDoc plugin setting file 234 | *.GhostDoc.xml 235 | 236 | # Node.js Tools for Visual Studio 237 | .ntvs_analysis.dat 238 | node_modules/ 239 | 240 | # Typescript v1 declaration files 241 | typings/ 242 | 243 | # Visual Studio 6 build log 244 | *.plg 245 | 246 | # Visual Studio 6 workspace options file 247 | *.opt 248 | 249 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 250 | *.vbw 251 | 252 | # Visual Studio LightSwitch build output 253 | **/*.HTMLClient/GeneratedArtifacts 254 | **/*.DesktopClient/GeneratedArtifacts 255 | **/*.DesktopClient/ModelManifest.xml 256 | **/*.Server/GeneratedArtifacts 257 | **/*.Server/ModelManifest.xml 258 | _Pvt_Extensions 259 | 260 | # Paket dependency manager 261 | .paket/paket.exe 262 | paket-files/ 263 | 264 | # FAKE - F# Make 265 | .fake/ 266 | 267 | # JetBrains Rider 268 | .idea/ 269 | *.sln.iml 270 | 271 | # CodeRush 272 | .cr/ 273 | 274 | # Python Tools for Visual Studio (PTVS) 275 | __pycache__/ 276 | *.pyc 277 | 278 | # Cake - Uncomment if you are using it 279 | # tools/** 280 | # !tools/packages.config 281 | 282 | # Telerik's JustMock configuration file 283 | *.jmconfig 284 | 285 | # BizTalk build output 286 | *.btp.cs 287 | *.btm.cs 288 | *.odx.cs 289 | *.xsd.cs 290 | 291 | # Temporary projects folder 292 | DoNotCommit/ 293 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Curiosity Gmbh 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Output-Color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/curiosity-ai/umap-sharp/912769f067c20648d3f310d8cffad9dd156e486f/Output-Color.png -------------------------------------------------------------------------------- /Output-Label.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/curiosity-ai/umap-sharp/912769f067c20648d3f310d8cffad9dd156e486f/Output-Label.png -------------------------------------------------------------------------------- /ReadMe.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://dev.azure.com/curiosity-ai/mosaik/_apis/build/status/umap-sharp?branchName=master)](https://dev.azure.com/curiosity-ai/mosaik/_build/latest?definitionId=6&branchName=master) 2 | 3 | 4 | 5 | 6 | # UMAP C# 7 | 8 | This is a C# reimplementation of the [JavaScript version](https://github.com/PAIR-code/umap-js), which was based upon the [Python version](https://github.com/lmcinnes/umap). 9 | 10 | "Uniform Manifold Approximation and Projection (UMAP) is a dimension reduction technique that can be used for visualisation similarly to t-SNE, but also for general non-linear dimension reduction" - if you have a set of vectors representing document or entities then you might use the algorithm to reduce those vectors to two or three dimensions in order to plot them and explore clusters. 11 | 12 | ## Installation 13 | 14 | [![Nuget](https://img.shields.io/nuget/v/UMAP.svg?maxAge=0&colorB=brightgreen)](https://www.nuget.org/packages/UMAP) 15 | 16 | Install via [NuGet](https://www.nuget.org/packages/UMAP): 17 | 18 | ``` 19 | Install-Package UMAP 20 | ``` 21 | 22 | ## Usage 23 | 24 | Instantiate a **Umap** instance, pass the array of vectors to the "InitializeFit" method, receive a recommended number of epochs to use from "InitializeFit", call the "Step" method this many times and then request the resulting (reduced dimension) vectors from the "GetEmbedding" method. The vectors passed to "InitializeFit" must all be of the same length. The vectors returned from "GetEmbedding" will be in the same order as the vectors passed to "InitializeFit" (so if you have labels relating to the source vectors then you can apply those labels to the embedding vectors). 25 | 26 | ```csharp 27 | // It doesn't matter where this data comes from, so long as it is a 28 | // float[][] and every nested array has the same length 29 | float[][] vectors = .. 30 | 31 | // Calculate embedding vectors using the default configuration 32 | var umap = new Umap(); 33 | var numberOfEpochs = umap.InitializeFit(vectors); 34 | for (var i = 0; i < numberOfEpochs; i++) 35 | umap.Step(); 36 | 37 | // This will be a float[][] where each nested array has two elements 38 | // because the default Umap configuration generates 2D embeddings 39 | var embeddings = umap.GetEmbedding(); 40 | ``` 41 | 42 | ## Configuration options 43 | 44 | | Umap ctor argument | Description | Default | 45 | | - | - | - | 46 | | `dimensions` | The number of dimensions to project the data to (commonly 2 or 3) | 2 47 | | `distanceFn` | A custom distance function to use | `Umap.DistanceFunctions.Cosine` | 48 | | `random` | A pseudo-random-number generator for controlling stochastic processes | `DefaultRandomGenerator.Instance` (unit tests use a fixed seed generator that disables parallelisation of the calculation | 49 | | `numberOfNeighbors` | The number of nearest neighbors to construct the fuzzy manifold in `InitializeFit` | 15 | 50 | | `customNumberOfEpochs` | If you wish to call Step a number of times other than that recommended by `InitializeFit` then it must be specified here The number of nearest neighbors to construct the fuzzy manifold in `InitializeFit` | null | 51 | | `progressReporter` | An optional delegate (`Action`) that will be called during processing with a rough estimate of progress (from 0 to 1) | null | 52 | 53 | If the input vectors are all normalized and you want to project to three dimensions then you might use: 54 | 55 | ```csharp 56 | var umap = new Umap( 57 | distance: Umap.DistanceFunctions.CosineForNormalizedVectors, 58 | dimensions: 3 59 | ); 60 | ``` 61 | ## Parallelization support 62 | 63 | This project uses a similar approach as Facebook's [fastText](https://github.com/facebookresearch/fastText) for lock-free multi-threaded optimization, by first [randomizing the order](https://github.com/curiosity-ai/umap-csharp/blob/ac636d76110f7cf8946976174c01a5609e0601eb/UMAP/Umap.cs#L291) each point is passed to the optimizer, and then, if using a thread-safe number generator, [running each optimization step multi-threaded](https://github.com/curiosity-ai/umap-csharp/blob/ac636d76110f7cf8946976174c01a5609e0601eb/UMAP/Umap.cs#L403). The assumption here is that collisions when [writing](https://github.com/curiosity-ai/umap-csharp/blob/ac636d76110f7cf8946976174c01a5609e0601eb/UMAP/Umap.cs#L424) to the projected embeddings vector will only happen at a very low probability, and will have minimum impact on the final results. 64 | 65 | If it is not desirable for multiple threads to be used then `DefaultRandomGenerator.DisableThreading` may be provided as the **Umap**'s "random" constructor argument. 66 | 67 | ## A complete example 68 | 69 | The "Tester" project is a console application that loads vectors that represent the [MNIST](http://yann.lecun.com/exdb/mnist/) data resized to 10x10 images and generates two images from it. One ("Output-Label.png") a visualisation that draw the labels (the numeric digit that exist vector represents, in this case) and the second ("Output-Color.png") a visualisation that plots each vector as a circle with a colour corresponding to the label. 70 | 71 | ![Text-labelled output](Output-Label.png) 72 | 73 | ![Color-labelled output](Output-Color.png) 74 | 75 | To see how it looks in three dimensions, see [this CodePen example](https://codepen.io/anon/pen/XLamda) - this library was used to calculate embedding vectors from MNIST, which were then used to generate JavaScript to render the visualisation in 3D using [Plotly](https://plot.ly/javascript/). 76 | -------------------------------------------------------------------------------- /Tester/MNIST-LabelledVectorArray-60000x100.msgpack: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/curiosity-ai/umap-sharp/912769f067c20648d3f310d8cffad9dd156e486f/Tester/MNIST-LabelledVectorArray-60000x100.msgpack -------------------------------------------------------------------------------- /Tester/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Drawing; 4 | using System.Drawing.Drawing2D; 5 | using System.Drawing.Text; 6 | using System.IO; 7 | using System.Linq; 8 | using MessagePack; 9 | using UMAP; 10 | 11 | namespace Tester 12 | { 13 | class Program 14 | { 15 | static void Main() 16 | { 17 | // Note: The MNIST data here consist of normalized vectors (so the CosineForNormalizedVectors distance function can be safely used) 18 | var data = MessagePackSerializer.Deserialize(File.ReadAllBytes("MNIST-LabelledVectorArray-60000x100.msgpack")); 19 | data = data.Take(10_000).ToArray(); 20 | 21 | var timer = Stopwatch.StartNew(); 22 | var umap = new Umap(distance: Umap.DistanceFunctions.CosineForNormalizedVectors); 23 | 24 | Console.WriteLine("Initialize fit.."); 25 | var nEpochs = umap.InitializeFit(data.Select(entry => entry.Vector).ToArray()); 26 | Console.WriteLine("- Done"); 27 | Console.WriteLine(); 28 | Console.WriteLine("Calculating.."); 29 | for (var i = 0; i < nEpochs; i++) 30 | { 31 | umap.Step(); 32 | if ((i % 10) == 0) 33 | { 34 | Console.WriteLine($"- Completed {i + 1} of {nEpochs}"); 35 | } 36 | } 37 | Console.WriteLine("- Done"); 38 | var embeddings = umap.GetEmbedding() 39 | .Select(vector => new { X = vector[0], Y = vector[1] }) 40 | .ToArray(); 41 | timer.Stop(); 42 | Console.WriteLine("Time taken: " + timer.Elapsed); 43 | 44 | // Fit the vectors to a 0-1 range (this isn't necessary if feeding these values down from a server to a browser to draw with Plotly because ronend because Plotly scales the axes to the data) 45 | var minX = embeddings.Min(vector => vector.X); 46 | var rangeX = embeddings.Max(vector => vector.X) - minX; 47 | var minY = embeddings.Min(vector => vector.Y); 48 | var rangeY = embeddings.Max(vector => vector.Y) - minY; 49 | var scaledEmbeddings = embeddings 50 | .Select(vector => new { X = (vector.X - minX) / rangeX, Y = (vector.Y - minY) / rangeY }) 51 | .ToArray(); 52 | 53 | const int width = 1600; 54 | const int height = 1200; 55 | using (var bitmap = new Bitmap(width, height)) 56 | { 57 | using (var g = Graphics.FromImage(bitmap)) 58 | { 59 | g.FillRectangle(Brushes.DarkBlue, 0, 0, width, height); 60 | g.SmoothingMode = SmoothingMode.HighQuality; 61 | g.TextRenderingHint = TextRenderingHint.ClearTypeGridFit; 62 | g.InterpolationMode = InterpolationMode.HighQualityBicubic; 63 | g.PixelOffsetMode = PixelOffsetMode.HighQuality; 64 | using (var font = new Font("Tahoma", 6)) 65 | { 66 | foreach (var (vector, uid) in scaledEmbeddings.Zip(data, (vector, entry) => (vector, entry.UID))) 67 | { 68 | g.DrawString(uid, font, Brushes.White, vector.X * width, vector.Y * height); 69 | } 70 | } 71 | } 72 | bitmap.Save("Output-Label.png"); 73 | } 74 | 75 | var colors = "#006400,#00008b,#b03060,#ff4500,#ffd700,#7fff00,#00ffff,#ff00ff,#6495ed,#ffdab9" 76 | .Split(',') 77 | .Select(c => ColorTranslator.FromHtml(c)) 78 | .Select(c => new SolidBrush(c)) 79 | .ToArray(); 80 | using (var bitmap = new Bitmap(width, height)) 81 | { 82 | using (var g = Graphics.FromImage(bitmap)) 83 | { 84 | g.FillRectangle(Brushes.White, 0, 0, width, height); 85 | g.SmoothingMode = SmoothingMode.HighQuality; 86 | g.InterpolationMode = InterpolationMode.HighQualityBicubic; 87 | g.PixelOffsetMode = PixelOffsetMode.HighQuality; 88 | foreach (var (vector, uid) in scaledEmbeddings.Zip(data, (vector, entry) => (vector, entry.UID))) 89 | { 90 | g.FillEllipse(colors[int.Parse(uid)], vector.X * width, vector.Y * height, 5, 5); 91 | } 92 | } 93 | bitmap.Save("Output-Color.png"); 94 | } 95 | 96 | Console.WriteLine("Generated visualisation images"); 97 | Console.WriteLine("Press [Enter] to terminate.."); 98 | Console.ReadLine(); 99 | } 100 | } 101 | 102 | [MessagePackObject] 103 | public sealed class LabelledVector 104 | { 105 | [Key(0)] public string UID; 106 | [Key(1)] public float[] Vector; 107 | } 108 | } -------------------------------------------------------------------------------- /Tester/Tester.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net7.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | PreserveNewest 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /UMAP.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.28803.156 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tester", "Tester\Tester.csproj", "{A994212B-A717-4ADC-AA26-2CDC40FE48D2}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UMAP", "UMAP\UMAP.csproj", "{52501C71-FC97-41D4-9FDB-379E27271039}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTests", "UnitTests\UnitTests.csproj", "{CD6B3A5D-1308-49EE-8F0D-D97EB9FAB451}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {52501C71-FC97-41D4-9FDB-379E27271039}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {52501C71-FC97-41D4-9FDB-379E27271039}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {52501C71-FC97-41D4-9FDB-379E27271039}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {52501C71-FC97-41D4-9FDB-379E27271039}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {A994212B-A717-4ADC-AA26-2CDC40FE48D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {A994212B-A717-4ADC-AA26-2CDC40FE48D2}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {A994212B-A717-4ADC-AA26-2CDC40FE48D2}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {A994212B-A717-4ADC-AA26-2CDC40FE48D2}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {CD6B3A5D-1308-49EE-8F0D-D97EB9FAB451}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {CD6B3A5D-1308-49EE-8F0D-D97EB9FAB451}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {CD6B3A5D-1308-49EE-8F0D-D97EB9FAB451}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {CD6B3A5D-1308-49EE-8F0D-D97EB9FAB451}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {8E039A54-0D46-4C22-B8B5-37A1306EEC30} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /UMAP/DefaultRandomGenerator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace UMAP 5 | { 6 | public sealed class DefaultRandomGenerator : IProvideRandomValues 7 | { 8 | /// 9 | /// This is the default configuration (it supports the optimization process to be executed on multiple threads) 10 | /// 11 | public static DefaultRandomGenerator Instance { get; } = new DefaultRandomGenerator(allowParallel: true); 12 | 13 | /// 14 | /// This uses the same random number generator but forces the optimization process to run on a single thread (which may be desirable if multiple requests may be processed concurrently 15 | /// or if it is otherwise not desirable to let a single request access all of the CPUs) 16 | /// 17 | public static DefaultRandomGenerator DisableThreading { get; } = new DefaultRandomGenerator(allowParallel: false); 18 | 19 | private DefaultRandomGenerator(bool allowParallel) => IsThreadSafe = allowParallel; 20 | 21 | public bool IsThreadSafe { get; } 22 | 23 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 24 | public int Next(int minValue, int maxValue) => ThreadSafeFastRandom.Next(minValue, maxValue); 25 | 26 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 27 | public float NextFloat() => ThreadSafeFastRandom.NextFloat(); 28 | 29 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 30 | public void NextFloats(Span buffer) => ThreadSafeFastRandom.NextFloats(buffer); 31 | } 32 | } -------------------------------------------------------------------------------- /UMAP/DistanceCalculation.cs: -------------------------------------------------------------------------------- 1 | namespace UMAP 2 | { 3 | public delegate float DistanceCalculation(float[] x, float[] y); 4 | } -------------------------------------------------------------------------------- /UMAP/FastRandom.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace UMAP 4 | { 5 | /// 6 | /// A fast random number generator for .NET, from https://www.codeproject.com/Articles/9187/A-fast-equivalent-for-System-Random 7 | /// Colin Green, January 2005 8 | /// 9 | /// September 4th 2005 10 | /// Added NextBytesUnsafe() - commented out by default. 11 | /// Fixed bug in Reinitialise() - y,z and w variables were not being reset. 12 | /// 13 | /// Key points: 14 | /// 1) Based on a simple and fast xor-shift pseudo random number generator (RNG) specified in: 15 | /// Marsaglia, George. (2003). Xorshift RNGs. 16 | /// http://www.jstatsoft.org/v08/i14/xorshift.pdf 17 | /// 18 | /// This particular implementation of xorshift has a period of 2^128-1. See the above paper to see 19 | /// how this can be easily extened if you need a longer period. At the time of writing I could find no 20 | /// information on the period of System.Random for comparison. 21 | /// 22 | /// 2) Faster than System.Random. Up to 8x faster, depending on which methods are called. 23 | /// 24 | /// 3) Direct replacement for System.Random. This class implements all of the methods that System.Random 25 | /// does plus some additional methods. The like named methods are functionally equivalent. 26 | /// 27 | /// 4) Allows fast re-initialisation with a seed, unlike System.Random which accepts a seed at construction 28 | /// time which then executes a relatively expensive initialisation routine. This provides a vast speed improvement 29 | /// if you need to reset the pseudo-random number sequence many times, e.g. if you want to re-generate the same 30 | /// sequence many times. An alternative might be to cache random numbers in an array, but that approach is limited 31 | /// by memory capacity and the fact that you may also want a large number of different sequences cached. Each sequence 32 | /// can each be represented by a single seed value (int) when using FastRandom. 33 | /// 34 | /// Notes. 35 | /// A further performance improvement can be obtained by declaring local variables as static, thus avoiding 36 | /// re-allocation of variables on each call. However care should be taken if multiple instances of 37 | /// FastRandom are in use or if being used in a multi-threaded environment. 38 | /// 39 | /// 40 | internal class FastRandom 41 | { 42 | // The +1 ensures NextDouble doesn't generate 1.0 43 | const float FLOAT_UNIT_INT = 1.0f / ((float)int.MaxValue + 1.0f); 44 | 45 | const double REAL_UNIT_INT = 1.0 / ((double)int.MaxValue + 1.0); 46 | const double REAL_UNIT_UINT = 1.0 / ((double)uint.MaxValue + 1.0); 47 | const uint Y = 842502087, Z = 3579807591, W = 273326509; 48 | 49 | uint x, y, z, w; 50 | 51 | /// 52 | /// Initialises a new instance using time dependent seed. 53 | /// 54 | public FastRandom() 55 | { 56 | // Initialise using the system tick count. 57 | Reinitialise(Environment.TickCount); 58 | } 59 | 60 | /// 61 | /// Initialises a new instance using an int value as seed. 62 | /// This constructor signature is provided to maintain compatibility with 63 | /// System.Random 64 | /// 65 | public FastRandom(int seed) 66 | { 67 | Reinitialise(seed); 68 | } 69 | 70 | /// 71 | /// Reinitialises using an int value as a seed. 72 | /// 73 | public void Reinitialise(int seed) 74 | { 75 | // The only stipulation stated for the xorshift RNG is that at least one of 76 | // the seeds x,y,z,w is non-zero. We fulfill that requirement by only allowing 77 | // resetting of the x seed 78 | x = (uint)seed; 79 | y = Y; 80 | z = Z; 81 | w = W; 82 | } 83 | 84 | /// 85 | /// Generates a random int over the range 0 to int.MaxValue-1. 86 | /// MaxValue is not generated in order to remain functionally equivalent to System.Random.Next(). 87 | /// This does slightly eat into some of the performance gain over System.Random, but not much. 88 | /// For better performance see: 89 | /// 90 | /// Call NextInt() for an int over the range 0 to int.MaxValue. 91 | /// 92 | /// Call NextUInt() and cast the result to an int to generate an int over the full Int32 value range 93 | /// including negative values. 94 | /// 95 | public int Next() 96 | { 97 | uint t = (x ^ (x << 11)); 98 | x = y; y = z; z = w; 99 | w = (w ^ (w >> 19)) ^ (t ^ (t >> 8)); 100 | 101 | // Handle the special case where the value int.MaxValue is generated. This is outside of 102 | // the range of permitted values, so we therefore call Next() to try again. 103 | uint rtn = w & 0x7FFFFFFF; 104 | if (rtn == 0x7FFFFFFF) 105 | { 106 | return Next(); 107 | } 108 | 109 | return (int)rtn; 110 | } 111 | 112 | /// 113 | /// Generates a random int over the range 0 to upperBound-1, and not including upperBound. 114 | /// 115 | public int Next(int upperBound) 116 | { 117 | if (upperBound < 0) 118 | { 119 | throw new ArgumentOutOfRangeException("upperBound", upperBound, "upperBound must be >=0"); 120 | } 121 | 122 | uint t = (x ^ (x << 11)); 123 | x = y; y = z; z = w; 124 | 125 | // The explicit int cast before the first multiplication gives better performance. 126 | // See comments in NextDouble. 127 | return (int)((REAL_UNIT_INT * (int)(0x7FFFFFFF & (w = (w ^ (w >> 19)) ^ (t ^ (t >> 8))))) * upperBound); 128 | } 129 | 130 | /// 131 | /// Generates a random int over the range lowerBound to upperBound-1, and not including upperBound. 132 | /// upperBound must be >= lowerBound. lowerBound may be negative. 133 | /// 134 | public int Next(int lowerBound, int upperBound) 135 | { 136 | if (lowerBound > upperBound) 137 | { 138 | throw new ArgumentOutOfRangeException("upperBound", upperBound, "upperBound must be >=lowerBound"); 139 | } 140 | 141 | uint t = (x ^ (x << 11)); 142 | x = y; y = z; z = w; 143 | 144 | // The explicit int cast before the first multiplication gives better performance. 145 | // See comments in NextDouble. 146 | int range = upperBound - lowerBound; 147 | if (range < 0) 148 | { // If range is <0 then an overflow has occured and must resort to using long integer arithmetic instead (slower). 149 | // We also must use all 32 bits of precision, instead of the normal 31, which again is slower. 150 | return lowerBound + (int)((REAL_UNIT_UINT * (double)(w = (w ^ (w >> 19)) ^ (t ^ (t >> 8)))) * (double)((long)upperBound - (long)lowerBound)); 151 | } 152 | 153 | // 31 bits of precision will suffice if range<=int.MaxValue. This allows us to cast to an int and gain 154 | // a little more performance. 155 | return lowerBound + (int)((REAL_UNIT_INT * (double)(int)(0x7FFFFFFF & (w = (w ^ (w >> 19)) ^ (t ^ (t >> 8))))) * (double)range); 156 | } 157 | 158 | /// 159 | /// Generates a random double. Values returned are from 0.0 up to but not including 1.0. 160 | /// 161 | public double NextDouble() 162 | { 163 | uint t = (x ^ (x << 11)); 164 | x = y; y = z; z = w; 165 | 166 | // Here we can gain a 2x speed improvement by generating a value that can be cast to 167 | // an int instead of the more easily available uint. If we then explicitly cast to an 168 | // int the compiler will then cast the int to a double to perform the multiplication, 169 | // this final cast is a lot faster than casting from a uint to a double. The extra cast 170 | // to an int is very fast (the allocated bits remain the same) and so the overall effect 171 | // of the extra cast is a significant performance improvement. 172 | // 173 | // Also note that the loss of one bit of precision is equivalent to what occurs within 174 | // System.Random. 175 | return (REAL_UNIT_INT * (int)(0x7FFFFFFF & (w = (w ^ (w >> 19)) ^ (t ^ (t >> 8))))); 176 | } 177 | 178 | /// 179 | /// Generates a random double. Values returned are from 0.0 up to but not including 1.0. 180 | /// 181 | public float NextFloat() 182 | { 183 | uint x = this.x, y = this.y, z = this.z, w = this.w; 184 | uint t = (x ^ (x << 11)); 185 | x = y; y = z; z = w; 186 | w = (w ^ (w >> 19)) ^ (t ^ (t >> 8)); 187 | var value = FLOAT_UNIT_INT * (int)(0x7FFFFFFF & w); 188 | this.x = x; this.y = y; this.z = z; this.w = w; 189 | return value; 190 | } 191 | 192 | /// 193 | /// Fills the provided byte array with random floats. 194 | /// 195 | public void NextFloats(Span buffer) 196 | { 197 | uint x = this.x, y = this.y, z = this.z, w = this.w; 198 | int i = 0; 199 | uint t; 200 | for (int bound = buffer.Length; i < bound;) 201 | { 202 | t = (x ^ (x << 11)); 203 | x = y; y = z; z = w; 204 | w = (w ^ (w >> 19)) ^ (t ^ (t >> 8)); 205 | 206 | buffer[i++] = FLOAT_UNIT_INT * (int)(0x7FFFFFFF & w); 207 | } 208 | 209 | this.x = x; this.y = y; this.z = z; this.w = w; 210 | } 211 | 212 | 213 | /// 214 | /// Fills the provided byte array with random bytes. 215 | /// This method is functionally equivalent to System.Random.NextBytes(). 216 | /// 217 | public void NextBytes(byte[] buffer) 218 | { 219 | // Fill up the bulk of the buffer in chunks of 4 bytes at a time. 220 | uint x = this.x, y = this.y, z = this.z, w = this.w; 221 | int i = 0; 222 | uint t; 223 | for (int bound = buffer.Length - 3; i < bound;) 224 | { 225 | // Generate 4 bytes. 226 | // Increased performance is achieved by generating 4 random bytes per loop. 227 | // Also note that no mask needs to be applied to zero out the higher order bytes before 228 | // casting because the cast ignores thos bytes. Thanks to Stefan Troschütz for pointing this out. 229 | t = (x ^ (x << 11)); 230 | x = y; y = z; z = w; 231 | w = (w ^ (w >> 19)) ^ (t ^ (t >> 8)); 232 | 233 | buffer[i++] = (byte)w; 234 | buffer[i++] = (byte)(w >> 8); 235 | buffer[i++] = (byte)(w >> 16); 236 | buffer[i++] = (byte)(w >> 24); 237 | } 238 | 239 | // Fill up any remaining bytes in the buffer. 240 | if (i < buffer.Length) 241 | { 242 | // Generate 4 bytes. 243 | t = (x ^ (x << 11)); 244 | x = y; y = z; z = w; 245 | w = (w ^ (w >> 19)) ^ (t ^ (t >> 8)); 246 | 247 | buffer[i++] = (byte)w; 248 | if (i < buffer.Length) 249 | { 250 | buffer[i++] = (byte)(w >> 8); 251 | if (i < buffer.Length) 252 | { 253 | buffer[i++] = (byte)(w >> 16); 254 | if (i < buffer.Length) 255 | { 256 | buffer[i] = (byte)(w >> 24); 257 | } 258 | } 259 | } 260 | } 261 | this.x = x; this.y = y; this.z = z; this.w = w; 262 | } 263 | 264 | /// 265 | /// Fills the provided byte array with random bytes. 266 | /// This method is functionally equivalent to System.Random.NextBytes(). 267 | /// 268 | public void NextBytes(Span buffer) 269 | { 270 | // Fill up the bulk of the buffer in chunks of 4 bytes at a time. 271 | uint x = this.x, y = this.y, z = this.z, w = this.w; 272 | int i = 0; 273 | uint t; 274 | for (int bound = buffer.Length - 3; i < bound;) 275 | { 276 | // Generate 4 bytes. 277 | // Increased performance is achieved by generating 4 random bytes per loop. 278 | // Also note that no mask needs to be applied to zero out the higher order bytes before 279 | // casting because the cast ignores thos bytes. Thanks to Stefan Troschütz for pointing this out. 280 | t = (x ^ (x << 11)); 281 | x = y; y = z; z = w; 282 | w = (w ^ (w >> 19)) ^ (t ^ (t >> 8)); 283 | 284 | buffer[i++] = (byte)w; 285 | buffer[i++] = (byte)(w >> 8); 286 | buffer[i++] = (byte)(w >> 16); 287 | buffer[i++] = (byte)(w >> 24); 288 | } 289 | 290 | // Fill up any remaining bytes in the buffer. 291 | if (i < buffer.Length) 292 | { 293 | // Generate 4 bytes. 294 | t = (x ^ (x << 11)); 295 | x = y; y = z; z = w; 296 | w = (w ^ (w >> 19)) ^ (t ^ (t >> 8)); 297 | 298 | buffer[i++] = (byte)w; 299 | if (i < buffer.Length) 300 | { 301 | buffer[i++] = (byte)(w >> 8); 302 | if (i < buffer.Length) 303 | { 304 | buffer[i++] = (byte)(w >> 16); 305 | if (i < buffer.Length) 306 | { 307 | buffer[i] = (byte)(w >> 24); 308 | } 309 | } 310 | } 311 | } 312 | this.x = x; this.y = y; this.z = z; this.w = w; 313 | } 314 | 315 | /// 316 | /// Generates a uint. Values returned are over the full range of a uint, 317 | /// uint.MinValue to uint.MaxValue, inclusive. 318 | /// 319 | /// This is the fastest method for generating a single random number because the underlying 320 | /// random number generator algorithm generates 32 random bits that can be cast directly to 321 | /// a uint. 322 | /// 323 | public uint NextUInt() 324 | { 325 | uint t = (x ^ (x << 11)); 326 | x = y; y = z; z = w; 327 | return (w = (w ^ (w >> 19)) ^ (t ^ (t >> 8))); 328 | } 329 | 330 | /// 331 | /// Generates a random int over the range 0 to int.MaxValue, inclusive. 332 | /// This method differs from Next() only in that the range is 0 to int.MaxValue 333 | /// and not 0 to int.MaxValue-1. 334 | /// 335 | /// The slight difference in range means this method is slightly faster than Next() 336 | /// but is not functionally equivalent to System.Random.Next(). 337 | /// 338 | public int NextInt() 339 | { 340 | uint t = (x ^ (x << 11)); 341 | x = y; y = z; z = w; 342 | return (int)(0x7FFFFFFF & (w = (w ^ (w >> 19)) ^ (t ^ (t >> 8)))); 343 | } 344 | 345 | 346 | // Buffer 32 bits in bitBuffer, return 1 at a time, keep track of how many have been returned 347 | // with bitBufferIdx. 348 | uint bitBuffer; 349 | uint bitMask = 1; 350 | 351 | /// 352 | /// Generates a single random bit. 353 | /// This method's performance is improved by generating 32 bits in one operation and storing them 354 | /// ready for future calls. 355 | /// 356 | public bool NextBool() 357 | { 358 | if (bitMask == 1) 359 | { 360 | // Generate 32 more bits. 361 | uint t = (x ^ (x << 11)); 362 | x = y; y = z; z = w; 363 | bitBuffer = w = (w ^ (w >> 19)) ^ (t ^ (t >> 8)); 364 | 365 | // Reset the bitMask that tells us which bit to read next. 366 | bitMask = 0x80000000; 367 | return (bitBuffer & bitMask) == 0; 368 | } 369 | 370 | return (bitBuffer & (bitMask >>= 1)) == 0; 371 | } 372 | } 373 | } -------------------------------------------------------------------------------- /UMAP/Heaps.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace UMAP 6 | { 7 | internal static class Heaps 8 | { 9 | /// 10 | /// Constructor for the heap objects. The heaps are used for approximate nearest neighbor search, maintaining a list of potential neighbors sorted by their distance.We also flag if potential neighbors 11 | /// are newly added to the list or not.Internally this is stored as a single array; the first axis determines whether we are looking at the array of candidate indices, the array of distances, or the 12 | /// flag array for whether elements are new or not.Each of these arrays are of shape (``nPoints``, ``size``) 13 | /// 14 | public static Heap MakeHeap(int nPoints, int size) 15 | { 16 | var heap = new Heap(); 17 | heap.Add(MakeArrays(-1)); 18 | heap.Add(MakeArrays(float.MaxValue)); 19 | heap.Add(MakeArrays(0)); 20 | return heap; 21 | 22 | float[][] MakeArrays(float fillValue) => Utils.Empty(nPoints).Select(_ => Utils.Filled(size, fillValue)).ToArray(); 23 | } 24 | 25 | /// 26 | /// Push a new element onto the heap. The heap stores potential neighbors for each data point.The ``row`` parameter determines which data point we are addressing, the ``weight`` determines the distance 27 | /// (for heap sorting), the ``index`` is the element to add, and the flag determines whether this is to be considered a new addition. 28 | /// 29 | public static int HeapPush(Heap heap, int row, float weight, int index, int flag) 30 | { 31 | var indices = heap[0][row]; 32 | var weights = heap[1][row]; 33 | if (weight >= weights[0]) 34 | { 35 | return 0; 36 | } 37 | 38 | // Break if we already have this element. 39 | for (var i = 0; i < indices.Length; i++) 40 | { 41 | if (index == indices[i]) 42 | { 43 | return 0; 44 | } 45 | } 46 | 47 | return UncheckedHeapPush(heap, row, weight, index, flag); 48 | } 49 | 50 | /// 51 | /// Push a new element onto the heap. The heap stores potential neighbors for each data point. The ``row`` parameter determines which data point we are addressing, the ``weight`` determines the distance 52 | /// (for heap sorting), the ``index`` is the element to add, and the flag determines whether this is to be considered a new addition. 53 | /// 54 | public static int UncheckedHeapPush(Heap heap, int row, float weight, int index, int flag) 55 | { 56 | var indices = heap[0][row]; 57 | var weights = heap[1][row]; 58 | var isNew = heap[2][row]; 59 | if (weight >= weights[0]) 60 | { 61 | return 0; 62 | } 63 | 64 | // Insert val at position zero 65 | weights[0] = weight; 66 | indices[0] = index; 67 | isNew[0] = flag; 68 | 69 | // Descend the heap, swapping values until the max heap criterion is met 70 | var i = 0; 71 | int iSwap; 72 | while (true) 73 | { 74 | var ic1 = 2 * i + 1; 75 | var ic2 = ic1 + 1; 76 | var heapShape2 = heap[0][0].Length; 77 | if (ic1 >= heapShape2) 78 | { 79 | break; 80 | } 81 | else if (ic2 >= heapShape2) 82 | { 83 | if (weights[ic1] > weight) 84 | { 85 | iSwap = ic1; 86 | } 87 | else 88 | { 89 | break; 90 | } 91 | } 92 | else if (weights[ic1] >= weights[ic2]) 93 | { 94 | if (weight < weights[ic1]) 95 | { 96 | iSwap = ic1; 97 | } 98 | else 99 | { 100 | break; 101 | } 102 | } 103 | else 104 | { 105 | if (weight < weights[ic2]) 106 | { 107 | iSwap = ic2; 108 | } 109 | else 110 | { 111 | break; 112 | } 113 | } 114 | weights[i] = weights[iSwap]; 115 | indices[i] = indices[iSwap]; 116 | isNew[i] = isNew[iSwap]; 117 | i = iSwap; 118 | } 119 | weights[i] = weight; 120 | indices[i] = index; 121 | isNew[i] = flag; 122 | return 1; 123 | } 124 | 125 | /// 126 | /// Build a heap of candidate neighbors for nearest neighbor descent. For each vertex the candidate neighbors are any current neighbors, and any vertices that have the vertex as one of their nearest neighbors. 127 | /// 128 | public static Heap BuildCandidates(Heap currentGraph, int nVertices, int nNeighbors, int maxCandidates, IProvideRandomValues random) 129 | { 130 | var candidateNeighbors = MakeHeap(nVertices, maxCandidates); 131 | for (var i = 0; i < nVertices; i++) 132 | { 133 | for (var j = 0; j < nNeighbors; j++) 134 | { 135 | if (currentGraph[0][i][j] < 0) 136 | { 137 | continue; 138 | } 139 | 140 | var idx = (int)currentGraph[0][i][j]; // TOOD: Should Heap be int values instead of float? 141 | var isn = (int)currentGraph[2][i][j]; // TOOD: Should Heap be int values instead of float? 142 | var d = random.NextFloat(); 143 | HeapPush(candidateNeighbors, i, d, idx, isn); 144 | HeapPush(candidateNeighbors, idx, d, i, isn); 145 | currentGraph[2][i][j] = 0; 146 | } 147 | } 148 | return candidateNeighbors; 149 | } 150 | 151 | /// 152 | /// Given an array of heaps (of indices and weights), unpack the heap out to give and array of sorted lists of indices and weights by increasing weight. This is effectively just the second half of heap sort 153 | /// (the first half not being required since we already have the data in a heap). 154 | /// 155 | public static (int[][] indices, float[][] weights) DeHeapSort(Heap heap) 156 | { 157 | // Note: The comment on this method doesn't seem to quite fit with the method signature (where a single Heap is provided, not an array of Heaps) 158 | var indices = heap[0]; 159 | var weights = heap[1]; 160 | for (var i = 0; i < indices.Length; i++) 161 | { 162 | var indHeap = indices[i]; 163 | var distHeap = weights[i]; 164 | for (var j = 0; j < indHeap.Length - 1; j++) 165 | { 166 | var indHeapIndex = indHeap.Length - j - 1; 167 | var distHeapIndex = distHeap.Length - j - 1; 168 | 169 | var temp1 = indHeap[0]; 170 | indHeap[0] = indHeap[indHeapIndex]; 171 | indHeap[indHeapIndex] = temp1; 172 | 173 | var temp2 = distHeap[0]; 174 | distHeap[0] = distHeap[distHeapIndex]; 175 | distHeap[distHeapIndex] = temp2; 176 | 177 | SiftDown(distHeap, indHeap, distHeapIndex, 0); 178 | } 179 | } 180 | var indicesAsInts = indices.Select(floatArray => floatArray.Select(value => (int)value).ToArray()).ToArray(); 181 | return (indicesAsInts, weights); 182 | } 183 | 184 | /// 185 | /// Restore the heap property for a heap with an out of place element at position ``elt``. This works with a heap pair where heap1 carries the weights and heap2 holds the corresponding elements. 186 | /// 187 | private static void SiftDown(float[] heap1, float[] heap2, int ceiling, int elt) 188 | { 189 | while (elt * 2 + 1 < ceiling) 190 | { 191 | var leftChild = elt * 2 + 1; 192 | var rightChild = leftChild + 1; 193 | var swap = elt; 194 | 195 | if (heap1[swap] < heap1[leftChild]) 196 | { 197 | swap = leftChild; 198 | } 199 | 200 | if (rightChild < ceiling && heap1[swap] < heap1[rightChild]) 201 | { 202 | swap = rightChild; 203 | } 204 | 205 | if (swap == elt) 206 | { 207 | break; 208 | } 209 | else 210 | { 211 | var temp1 = heap1[elt]; 212 | heap1[elt] = heap1[swap]; 213 | heap1[swap] = temp1; 214 | 215 | var temp2 = heap2[elt]; 216 | heap2[elt] = heap2[swap]; 217 | heap2[swap] = temp2; 218 | 219 | elt = swap; 220 | } 221 | } 222 | } 223 | 224 | /// 225 | /// Search the heap for the smallest element that is still flagged 226 | /// 227 | public static int SmallestFlagged(Heap heap, int row) 228 | { 229 | var ind = heap[0][row]; 230 | var dist = heap[1][row]; 231 | var flag = heap[2][row]; 232 | var minDist = float.MaxValue; 233 | var resultIndex = -1; 234 | for (var i = 0; i > ind.Length; i++) 235 | { 236 | if ((flag[i] == 1) && (dist[i] < minDist)) 237 | { 238 | minDist = dist[i]; 239 | resultIndex = i; 240 | } 241 | } 242 | if (resultIndex >= 0) 243 | { 244 | flag[resultIndex] = 0; 245 | return (int)Math.Floor(ind[resultIndex]); 246 | } 247 | else 248 | { 249 | return -1; 250 | } 251 | } 252 | 253 | public sealed class Heap 254 | { 255 | private readonly List _values; 256 | public Heap() => _values = new List(); 257 | 258 | public float[][] this[int index] { get => _values[index]; } 259 | 260 | public void Add(float[][] value) => _values.Add(value); 261 | } 262 | } 263 | } -------------------------------------------------------------------------------- /UMAP/IProvideRandomValues.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace UMAP 5 | { 6 | public interface IProvideRandomValues 7 | { 8 | bool IsThreadSafe { get; } 9 | 10 | /// 11 | /// Generates a random float. Values returned are from 0.0 up to but not including 1.0. 12 | /// 13 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 14 | float NextFloat(); 15 | 16 | /// 17 | /// Fills the elements of a specified array of bytes with random numbers. 18 | /// 19 | /// An array of bytes to contain random numbers. 20 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 21 | void NextFloats(Span buffer); 22 | 23 | /// 24 | /// Returns a random integer that is within a specified range. 25 | /// 26 | /// The inclusive lower bound of the random number returned. 27 | /// The exclusive upper bound of the random number returned. maxValue must be greater than or equal to minValue. 28 | /// A 32-bit signed integer greater than or equal to minValue and less than maxValue; that is, the range of return values includes minValue but not maxValue. If minValue 29 | // equals maxValue, minValue is returned. 30 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 31 | int Next(int minValue, int maxValue); 32 | } 33 | } -------------------------------------------------------------------------------- /UMAP/InternalsVisibleTo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("UMAP.UnitTests")] -------------------------------------------------------------------------------- /UMAP/NNDescent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using static UMAP.Heaps; 3 | 4 | namespace UMAP 5 | { 6 | internal static class NNDescent 7 | { 8 | public delegate (int[][] indices, float[][] weights) NNDescentFn( 9 | float[][] data, 10 | int[][] leafArray, 11 | int nNeighbors, 12 | int nIters = 10, 13 | int maxCandidates = 50, 14 | float delta = 0.001f, 15 | float rho = 0.5f, 16 | bool rpTreeInit = true, 17 | Action startingIteration = null 18 | ); 19 | 20 | /// 21 | /// Create a version of nearest neighbor descent. 22 | /// 23 | public static NNDescentFn MakeNNDescent(DistanceCalculation distanceFn, IProvideRandomValues random) 24 | { 25 | return (data, leafArray, nNeighbors, nIters, maxCandidates, delta, rho, rpTreeInit, startingIteration) => 26 | { 27 | var nVertices = data.Length; 28 | var currentGraph = MakeHeap(data.Length, nNeighbors); 29 | for (var i = 0; i < data.Length; i++) 30 | { 31 | var indices = Utils.RejectionSample(nNeighbors, data.Length, random); 32 | for (var j = 0; j < indices.Length; j++) 33 | { 34 | var d = distanceFn(data[i], data[indices[j]]); 35 | HeapPush(currentGraph, i, d, indices[j], 1); 36 | HeapPush(currentGraph, indices[j], d, i, 1); 37 | } 38 | } 39 | if (rpTreeInit) 40 | { 41 | for (var n = 0; n < leafArray.Length; n++) 42 | { 43 | for (var i = 0; i < leafArray[n].Length; i++) 44 | { 45 | if (leafArray[n][i] < 0) 46 | { 47 | break; 48 | } 49 | 50 | for (var j = i + 1; j < leafArray[n].Length; j++) 51 | { 52 | if (leafArray[n][j] < 0) 53 | { 54 | break; 55 | } 56 | 57 | var d = distanceFn(data[leafArray[n][i]], data[leafArray[n][j]]); 58 | HeapPush(currentGraph, leafArray[n][i], d, leafArray[n][j], 1); 59 | HeapPush(currentGraph, leafArray[n][j], d, leafArray[n][i], 1); 60 | } 61 | } 62 | } 63 | } 64 | for (var n = 0; n < nIters; n++) 65 | { 66 | startingIteration?.Invoke(n, nIters); 67 | var candidateNeighbors = BuildCandidates(currentGraph, nVertices, nNeighbors, maxCandidates, random); 68 | var c = 0; 69 | for (var i = 0; i < nVertices; i++) 70 | { 71 | for (var j = 0; j < maxCandidates; j++) 72 | { 73 | var p = (int)Math.Floor(candidateNeighbors[0][i][j]); 74 | if ((p < 0) || (random.NextFloat() < rho)) 75 | { 76 | continue; 77 | } 78 | 79 | for (var k = 0; k < maxCandidates; k++) 80 | { 81 | var q = (int)Math.Floor(candidateNeighbors[0][i][k]); 82 | var cj = candidateNeighbors[2][i][j]; 83 | var ck = candidateNeighbors[2][i][k]; 84 | if (q < 0 || ((cj == 0) && (ck == 0))) 85 | { 86 | continue; 87 | } 88 | 89 | var d = distanceFn(data[p], data[q]); 90 | c += HeapPush(currentGraph, p, d, q, 1); 91 | c += HeapPush(currentGraph, q, d, p, 1); 92 | } 93 | } 94 | } 95 | if (c <= delta * nNeighbors * data.Length) 96 | { 97 | break; 98 | } 99 | } 100 | return DeHeapSort(currentGraph); 101 | }; 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /UMAP/SIMD.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Numerics; 3 | using System.Runtime.CompilerServices; 4 | 5 | namespace UMAP 6 | { 7 | internal static class SIMD 8 | { 9 | private static readonly int _vs1 = Vector.Count; 10 | private static readonly int _vs2 = 2 * Vector.Count; 11 | private static readonly int _vs3 = 3 * Vector.Count; 12 | private static readonly int _vs4 = 4 * Vector.Count; 13 | 14 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 15 | public static float Magnitude(ref float[] vec) => (float)Math.Sqrt(DotProduct(ref vec, ref vec)); 16 | 17 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 18 | public static float Euclidean(ref float[] lhs, ref float[] rhs) 19 | { 20 | float result = 0f; 21 | 22 | var count = lhs.Length; 23 | var offset = 0; 24 | Vector diff; 25 | while (count >= _vs4) 26 | { 27 | diff = new Vector(lhs, offset) - new Vector(rhs, offset); result += Vector.Dot(diff, diff); 28 | diff = new Vector(lhs, offset + _vs1) - new Vector(rhs, offset + _vs1); result += Vector.Dot(diff, diff); 29 | diff = new Vector(lhs, offset + _vs2) - new Vector(rhs, offset + _vs2); result += Vector.Dot(diff, diff); 30 | diff = new Vector(lhs, offset + _vs3) - new Vector(rhs, offset + _vs3); result += Vector.Dot(diff, diff); 31 | if (count == _vs4) 32 | { 33 | return result; 34 | } 35 | 36 | count -= _vs4; 37 | offset += _vs4; 38 | } 39 | 40 | if (count >= _vs2) 41 | { 42 | diff = new Vector(lhs, offset) - new Vector(rhs, offset); result += Vector.Dot(diff, diff); 43 | diff = new Vector(lhs, offset + _vs1) - new Vector(rhs, offset + _vs1); result += Vector.Dot(diff, diff); 44 | if (count == _vs2) 45 | { 46 | return result; 47 | } 48 | 49 | count -= _vs2; 50 | offset += _vs2; 51 | } 52 | if (count >= _vs1) 53 | { 54 | diff = new Vector(lhs, offset) - new Vector(rhs, offset); result += Vector.Dot(diff, diff); 55 | if (count == _vs1) 56 | { 57 | return result; 58 | } 59 | 60 | count -= _vs1; 61 | offset += _vs1; 62 | } 63 | if (count > 0) 64 | { 65 | while (count > 0) 66 | { 67 | var d = (lhs[offset] - rhs[offset]); 68 | result += d * d; 69 | offset++; count--; 70 | } 71 | } 72 | return result; 73 | } 74 | 75 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 76 | public static void Add(ref float[] lhs, float f) 77 | { 78 | var count = lhs.Length; 79 | var offset = 0; 80 | var v = new Vector(f); 81 | while (count >= _vs4) 82 | { 83 | (new Vector(lhs, offset) + v).CopyTo(lhs, offset); 84 | (new Vector(lhs, offset + _vs1) + v).CopyTo(lhs, offset + _vs1); 85 | (new Vector(lhs, offset + _vs2) + v).CopyTo(lhs, offset + _vs2); 86 | (new Vector(lhs, offset + _vs3) + v).CopyTo(lhs, offset + _vs3); 87 | if (count == _vs4) 88 | { 89 | return; 90 | } 91 | 92 | count -= _vs4; 93 | offset += _vs4; 94 | } 95 | if (count >= _vs2) 96 | { 97 | (new Vector(lhs, offset) + v).CopyTo(lhs, offset); 98 | (new Vector(lhs, offset + _vs1) + v).CopyTo(lhs, offset + _vs1); 99 | if (count == _vs2) 100 | { 101 | return; 102 | } 103 | 104 | count -= _vs2; 105 | offset += _vs2; 106 | } 107 | if (count >= _vs1) 108 | { 109 | (new Vector(lhs, offset) + v).CopyTo(lhs, offset); 110 | if (count == _vs1) 111 | { 112 | return; 113 | } 114 | 115 | count -= _vs1; 116 | offset += _vs1; 117 | } 118 | if (count > 0) 119 | { 120 | while (count > 0) 121 | { 122 | lhs[offset] += f; 123 | offset++; count--; 124 | } 125 | } 126 | } 127 | 128 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 129 | public static void Multiply(ref float[] lhs, float f) 130 | { 131 | var count = lhs.Length; 132 | var offset = 0; 133 | while (count >= _vs4) 134 | { 135 | (new Vector(lhs, offset) * f).CopyTo(lhs, offset); 136 | (new Vector(lhs, offset + _vs1) * f).CopyTo(lhs, offset + _vs1); 137 | (new Vector(lhs, offset + _vs2) * f).CopyTo(lhs, offset + _vs2); 138 | (new Vector(lhs, offset + _vs3) * f).CopyTo(lhs, offset + _vs3); 139 | if (count == _vs4) 140 | { 141 | return; 142 | } 143 | 144 | count -= _vs4; 145 | offset += _vs4; 146 | } 147 | if (count >= _vs2) 148 | { 149 | (new Vector(lhs, offset) * f).CopyTo(lhs, offset); 150 | (new Vector(lhs, offset + _vs1) * f).CopyTo(lhs, offset + _vs1); 151 | if (count == _vs2) 152 | { 153 | return; 154 | } 155 | 156 | count -= _vs2; 157 | offset += _vs2; 158 | } 159 | if (count >= _vs1) 160 | { 161 | (new Vector(lhs, offset) * f).CopyTo(lhs, offset); 162 | if (count == _vs1) 163 | { 164 | return; 165 | } 166 | 167 | count -= _vs1; 168 | offset += _vs1; 169 | } 170 | if (count > 0) 171 | { 172 | while (count > 0) 173 | { 174 | lhs[offset] *= f; 175 | offset++; count--; 176 | } 177 | } 178 | } 179 | 180 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 181 | public static float DotProduct(ref float[] lhs, ref float[] rhs) 182 | { 183 | var result = 0f; 184 | var count = lhs.Length; 185 | var offset = 0; 186 | while (count >= _vs4) 187 | { 188 | result += Vector.Dot(new Vector(lhs, offset), new Vector(rhs, offset)); 189 | result += Vector.Dot(new Vector(lhs, offset + _vs1), new Vector(rhs, offset + _vs1)); 190 | result += Vector.Dot(new Vector(lhs, offset + _vs2), new Vector(rhs, offset + _vs2)); 191 | result += Vector.Dot(new Vector(lhs, offset + _vs3), new Vector(rhs, offset + _vs3)); 192 | if (count == _vs4) 193 | { 194 | return result; 195 | } 196 | 197 | count -= _vs4; 198 | offset += _vs4; 199 | } 200 | if (count >= _vs2) 201 | { 202 | result += Vector.Dot(new Vector(lhs, offset), new Vector(rhs, offset)); 203 | result += Vector.Dot(new Vector(lhs, offset + _vs1), new Vector(rhs, offset + _vs1)); 204 | if (count == _vs2) 205 | { 206 | return result; 207 | } 208 | 209 | count -= _vs2; 210 | offset += _vs2; 211 | } 212 | if (count >= _vs1) 213 | { 214 | result += Vector.Dot(new Vector(lhs, offset), new Vector(rhs, offset)); 215 | if (count == _vs1) 216 | { 217 | return result; 218 | } 219 | 220 | count -= _vs1; 221 | offset += _vs1; 222 | } 223 | if (count > 0) 224 | { 225 | while (count > 0) 226 | { 227 | result += lhs[offset] * rhs[offset]; 228 | offset++; count--; 229 | } 230 | } 231 | return result; 232 | } 233 | } 234 | } -------------------------------------------------------------------------------- /UMAP/SIMDInt.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace UMAP 5 | { 6 | internal static class SIMDint 7 | { 8 | private static readonly int _vs1 = Vector.Count; 9 | private static readonly int _vs2 = 2 * Vector.Count; 10 | private static readonly int _vs3 = 3 * Vector.Count; 11 | private static readonly int _vs4 = 4 * Vector.Count; 12 | 13 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 14 | public static void Zero(ref int[] lhs) 15 | { 16 | var count = lhs.Length; 17 | var offset = 0; 18 | 19 | while (count >= _vs4) 20 | { 21 | Vector.Zero.CopyTo(lhs, offset); 22 | Vector.Zero.CopyTo(lhs, offset + _vs1); 23 | Vector.Zero.CopyTo(lhs, offset + _vs2); 24 | Vector.Zero.CopyTo(lhs, offset + _vs3); 25 | if (count == _vs4) 26 | { 27 | return; 28 | } 29 | 30 | count -= _vs4; 31 | offset += _vs4; 32 | } 33 | 34 | if (count >= _vs2) 35 | { 36 | Vector.Zero.CopyTo(lhs, offset); 37 | Vector.Zero.CopyTo(lhs, offset + _vs1); 38 | if (count == _vs2) 39 | { 40 | return; 41 | } 42 | 43 | count -= _vs2; 44 | offset += _vs2; 45 | } 46 | if (count >= _vs1) 47 | { 48 | Vector.Zero.CopyTo(lhs, offset); 49 | if (count == _vs1) 50 | { 51 | return; 52 | } 53 | 54 | count -= _vs1; 55 | offset += _vs1; 56 | } 57 | if (count > 0) 58 | { 59 | while (count > 0) 60 | { 61 | lhs[offset] = 0; 62 | offset++; count--; 63 | } 64 | } 65 | } 66 | 67 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 68 | public static void Uniform(ref float[] data, float a, IProvideRandomValues random) 69 | { 70 | float a2 = 2 * a; 71 | float an = -a; 72 | random.NextFloats(data); 73 | SIMD.Multiply(ref data, a2); 74 | SIMD.Add(ref data, an); 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /UMAP/SparseMatrix.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.CompilerServices; 5 | 6 | namespace UMAP 7 | { 8 | internal sealed class SparseMatrix 9 | { 10 | private readonly Dictionary _entries; 11 | public SparseMatrix(IEnumerable rows, IEnumerable cols, IEnumerable values, (int rows, int cols) dims) : this(Combine(rows, cols, values), dims) { } 12 | private SparseMatrix(IEnumerable<(int row, int col, float value)> entries, (int rows, int cols) dims) 13 | { 14 | Dims = dims; 15 | _entries = new Dictionary(); 16 | foreach (var (entry, index) in entries.Select((entry, index) => (entry, index))) 17 | { 18 | CheckDims(entry.row, entry.col); 19 | _entries[new RowCol(entry.row, entry.col)] = entry.value; 20 | } 21 | } 22 | private SparseMatrix(Dictionary entries, (int, int) dims) 23 | { 24 | Dims = dims; 25 | _entries = entries; 26 | } 27 | 28 | private static IEnumerable<(int row, int col, float value)> Combine(IEnumerable rows, IEnumerable cols, IEnumerable values) 29 | { 30 | var rowsArray = rows.ToArray(); 31 | var colsArray = cols.ToArray(); 32 | var valuesArray = values.ToArray(); 33 | if ((rowsArray.Length != valuesArray.Length) || (colsArray.Length != valuesArray.Length)) 34 | { 35 | throw new ArgumentException($"The input lists {nameof(rows)}, {nameof(cols)} and {nameof(values)} must all have the same number of elements"); 36 | } 37 | 38 | for (var i = 0; i < valuesArray.Length; i++) 39 | { 40 | yield return (rowsArray[i], colsArray[i], valuesArray[i]); 41 | } 42 | } 43 | 44 | public (int rows, int cols) Dims { get; } 45 | 46 | public void Set(int row, int col, float value) 47 | { 48 | CheckDims(row, col); 49 | _entries[new RowCol(row, col)] = value; 50 | } 51 | 52 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 53 | public float Get(int row, int col, float defaultValue = 0) 54 | { 55 | CheckDims(row, col); 56 | return _entries.TryGetValue(new RowCol(row, col), out var v) ? v : defaultValue; 57 | } 58 | 59 | public IEnumerable<(int row, int col, float value)> GetAll() => _entries.Select(kv => (kv.Key.Row, kv.Key.Col, kv.Value)); 60 | 61 | public IEnumerable GetRows() => _entries.Keys.Select(k => k.Row); 62 | public IEnumerable GetCols() => _entries.Keys.Select(k => k.Col); 63 | public IEnumerable GetValues() => _entries.Values; 64 | 65 | public void ForEach(Action fn) 66 | { 67 | foreach (var kv in _entries) 68 | { 69 | fn(kv.Value, kv.Key.Row, kv.Key.Col); 70 | } 71 | } 72 | 73 | public SparseMatrix Map(Func fn) => Map((value, row, col) => fn(value)); 74 | 75 | public SparseMatrix Map(Func fn) 76 | { 77 | var newEntries = _entries.ToDictionary(kv => kv.Key, kv => fn(kv.Value, kv.Key.Row, kv.Key.Col)); 78 | return new SparseMatrix(newEntries, Dims); 79 | } 80 | 81 | public float[][] ToArray() 82 | { 83 | var output = Enumerable.Range(0, Dims.rows).Select(_ => new float[Dims.cols]).ToArray(); 84 | foreach (var kv in _entries) 85 | { 86 | output[kv.Key.Row][kv.Key.Col] = kv.Value; 87 | } 88 | 89 | return output; 90 | } 91 | 92 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 93 | private void CheckDims(int row, int col) 94 | { 95 | #if DEBUG 96 | if ((row >= Dims.rows) || (col >= Dims.cols)) 97 | { 98 | throw new Exception("array index out of bounds"); 99 | } 100 | #endif 101 | } 102 | 103 | public SparseMatrix Transpose() 104 | { 105 | var dims = (Dims.cols, Dims.rows); 106 | var entries = new Dictionary(_entries.Count); 107 | foreach (var entry in _entries) 108 | { 109 | entries[new RowCol(entry.Key.Col, entry.Key.Row)] = entry.Value; 110 | } 111 | 112 | return new SparseMatrix(entries, dims); 113 | } 114 | 115 | /// 116 | /// Element-wise multiplication of two matrices 117 | /// 118 | public SparseMatrix PairwiseMultiply(SparseMatrix other) 119 | { 120 | var newEntries = new Dictionary(_entries.Count); 121 | foreach (var kv in _entries) 122 | { 123 | if (other._entries.TryGetValue(kv.Key, out var v)) 124 | { 125 | newEntries[kv.Key] = kv.Value * v; 126 | } 127 | } 128 | return new SparseMatrix(newEntries, Dims); 129 | } 130 | 131 | /// 132 | /// Element-wise addition of two matrices 133 | /// 134 | public SparseMatrix Add(SparseMatrix other) => ElementWiseWith(other, (x, y) => x + y); 135 | 136 | /// 137 | /// Element-wise subtraction of two matrices 138 | /// 139 | public SparseMatrix Subtract(SparseMatrix other) => ElementWiseWith(other, (x, y) => x - y); 140 | 141 | /// 142 | /// Scalar multiplication of a matrix 143 | /// 144 | public SparseMatrix MultiplyScalar(float scalar) => Map((value, row, cols) => value * scalar); 145 | 146 | /// 147 | /// Helper function for element-wise operations 148 | /// 149 | private SparseMatrix ElementWiseWith(SparseMatrix other, Func op) 150 | { 151 | var newEntries = new Dictionary(_entries.Count); 152 | foreach (var k in _entries.Keys.Union(other._entries.Keys)) 153 | { 154 | newEntries[k] = op( 155 | _entries.TryGetValue(k, out var x) ? x : 0f, 156 | other._entries.TryGetValue(k, out var y) ? y : 0f 157 | ); 158 | } 159 | return new SparseMatrix(newEntries, Dims); 160 | } 161 | 162 | /// 163 | /// Helper function for getting data, indices, and indptr arrays from a sparse matrix to follow csr matrix conventions. Super inefficient (and kind of defeats the purpose of this convention) 164 | /// but a lot of the ported python tree search logic depends on this data format. 165 | /// 166 | public (int[] indices, float[] values, int[] indptr) GetCSR() 167 | { 168 | var entries = new List<(float value, int row, int col)>(); 169 | ForEach((value, row, col) => entries.Add((value, row, col))); 170 | entries.Sort((a, b) => 171 | { 172 | if (a.row == b.row) 173 | { 174 | return a.col - b.col; 175 | } 176 | 177 | return a.row - b.row; 178 | }); 179 | 180 | var indices = new List(); 181 | var values = new List(); 182 | var indptr = new List(); 183 | var currentRow = -1; 184 | for (var i = 0; i < entries.Count; i++) 185 | { 186 | var (value, row, col) = entries[i]; 187 | if (row != currentRow) 188 | { 189 | currentRow = row; 190 | indptr.Add(i); 191 | } 192 | indices.Add(col); 193 | values.Add(value); 194 | } 195 | return (indices.ToArray(), values.ToArray(), indptr.ToArray()); 196 | } 197 | 198 | private struct RowCol : IEquatable 199 | { 200 | public RowCol(int row, int col) 201 | { 202 | Row = row; 203 | Col = col; 204 | } 205 | 206 | public int Row { get; } 207 | public int Col { get; } 208 | 209 | // 2019-06-24 DWR: Structs get default Equals and GetHashCode implementations but they can be slow - having these versions makes the code run much quicker 210 | // and it seems a good practice to throw in IEquatable to avoid boxing when Equals is called 211 | public bool Equals(RowCol other) => (other.Row == Row) && (other.Col == Col); 212 | public override bool Equals(object obj) => (obj is RowCol rc) && rc.Equals(this); 213 | public override int GetHashCode() // Courtesy of https://stackoverflow.com/a/263416/3813189 214 | { 215 | unchecked // Overflow is fine, just wrap 216 | { 217 | int hash = 17; 218 | hash = hash * 23 + Row; 219 | hash = hash * 23 + Col; 220 | return hash; 221 | } 222 | } 223 | } 224 | } 225 | } -------------------------------------------------------------------------------- /UMAP/ThreadSafeFastRandom.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace UMAP 5 | { 6 | internal static class ThreadSafeFastRandom 7 | { 8 | private static readonly Random _global = new Random(); 9 | 10 | [ThreadStatic] 11 | private static FastRandom _local; 12 | 13 | private static int GetGlobalSeed() 14 | { 15 | int seed; 16 | lock (_global) 17 | { 18 | seed = _global.Next(); 19 | } 20 | return seed; 21 | } 22 | 23 | /// 24 | /// Returns a non-negative random integer. 25 | /// 26 | /// A 32-bit signed integer that is greater than or equal to 0 and less than System.Int32.MaxValue. 27 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 28 | public static int Next() 29 | { 30 | var inst = _local; 31 | if (inst is null) 32 | { 33 | int seed; 34 | seed = GetGlobalSeed(); 35 | _local = inst = new FastRandom(seed); 36 | } 37 | return inst.Next(); 38 | } 39 | 40 | /// 41 | /// Returns a non-negative random integer that is less than the specified maximum. 42 | /// 43 | /// The exclusive upper bound of the random number to be generated. maxValue must be greater than or equal to 0. 44 | /// A 32-bit signed integer that is greater than or equal to 0, and less than maxValue; that is, the range of return values ordinarily includes 0 but not maxValue. However, 45 | // if maxValue equals 0, maxValue is returned. 46 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 47 | public static int Next(int maxValue) 48 | { 49 | var inst = _local; 50 | if (inst is null) 51 | { 52 | int seed; 53 | seed = GetGlobalSeed(); 54 | _local = inst = new FastRandom(seed); 55 | } 56 | int ans; 57 | do 58 | { 59 | ans = inst.Next(maxValue); 60 | } while (ans == maxValue); 61 | 62 | return ans; 63 | } 64 | 65 | /// 66 | /// Returns a random integer that is within a specified range. 67 | /// 68 | /// The inclusive lower bound of the random number returned. 69 | /// The exclusive upper bound of the random number returned. maxValue must be greater than or equal to minValue. 70 | /// A 32-bit signed integer greater than or equal to minValue and less than maxValue; that is, the range of return values includes minValue but not maxValue. If minValue 71 | // equals maxValue, minValue is returned. 72 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 73 | public static int Next(int minValue, int maxValue) 74 | { 75 | var inst = _local; 76 | if (inst is null) 77 | { 78 | int seed; 79 | seed = GetGlobalSeed(); 80 | _local = inst = new FastRandom(seed); 81 | } 82 | return inst.Next(minValue, maxValue); 83 | } 84 | 85 | /// 86 | /// Generates a random float. Values returned are from 0.0 up to but not including 1.0. 87 | /// 88 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 89 | public static float NextFloat() 90 | { 91 | var inst = _local; 92 | if (inst is null) 93 | { 94 | int seed; 95 | seed = GetGlobalSeed(); 96 | _local = inst = new FastRandom(seed); 97 | } 98 | return inst.NextFloat(); 99 | } 100 | 101 | /// 102 | /// Fills the elements of a specified array of bytes with random numbers. 103 | /// 104 | /// An array of bytes to contain random numbers. 105 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 106 | public static void NextFloats(Span buffer) 107 | { 108 | var inst = _local; 109 | if (inst is null) 110 | { 111 | int seed; 112 | seed = GetGlobalSeed(); 113 | _local = inst = new FastRandom(seed); 114 | } 115 | inst.NextFloats(buffer); 116 | } 117 | } 118 | } -------------------------------------------------------------------------------- /UMAP/Tree.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace UMAP 6 | { 7 | internal static class Tree 8 | { 9 | /// 10 | /// Construct a random projection tree based on ``data`` with leaves of size at most ``leafSize`` 11 | /// 12 | public static RandomProjectionTreeNode MakeTree(float[][] data, int leafSize, int n, IProvideRandomValues random) 13 | { 14 | var indices = Enumerable.Range(0, data.Length).ToArray(); 15 | return MakeEuclideanTree(data, indices, leafSize, n, random); 16 | } 17 | 18 | private static RandomProjectionTreeNode MakeEuclideanTree(float[][] data, int[] indices, int leafSize, int q, IProvideRandomValues random) 19 | { 20 | if (indices.Length > leafSize) 21 | { 22 | var (indicesLeft, indicesRight, hyperplaneVector, hyperplaneOffset) = EuclideanRandomProjectionSplit(data, indices, random); 23 | var leftChild = MakeEuclideanTree(data, indicesLeft, leafSize, q + 1, random); 24 | var rightChild = MakeEuclideanTree(data, indicesRight, leafSize, q + 1, random); 25 | return new RandomProjectionTreeNode { Indices = indices, LeftChild = leftChild, RightChild = rightChild, IsLeaf = false, Hyperplane = hyperplaneVector, Offset = hyperplaneOffset }; 26 | } 27 | else 28 | { 29 | return new RandomProjectionTreeNode { Indices = indices, LeftChild = null, RightChild = null, IsLeaf = true, Hyperplane = Array.Empty(), Offset = 0 }; 30 | } 31 | } 32 | 33 | public static FlatTree FlattenTree(RandomProjectionTreeNode tree, int leafSize) 34 | { 35 | var nNodes = NumNodes(tree); 36 | var nLeaves = NumLeaves(tree); 37 | 38 | // TODO[umap-js]: Verify that sparse code is not relevant... 39 | var hyperplanes = Utils.Range(nNodes).Select(_ => new float[tree.Hyperplane.Length]).ToArray(); 40 | 41 | var offsets = new float[nNodes]; 42 | var children = Utils.Range(nNodes).Select(_ => new[] { -1, -1 }).ToArray(); 43 | var indices = Utils.Range(nLeaves).Select(_ => Utils.Range(leafSize).Select(___ => -1).ToArray()).ToArray(); 44 | RecursiveFlatten(tree, hyperplanes, offsets, children, indices, 0, 0); 45 | return new FlatTree { Hyperplanes = hyperplanes, Offsets = offsets, Children = children, Indices = indices }; 46 | } 47 | 48 | /// 49 | /// Given a set of ``indices`` for data points from ``data``, create a random hyperplane to split the data, returning two arrays indices that fall on either side of the hyperplane. This is 50 | /// the basis for a random projection tree, which simply uses this splitting recursively. This particular split uses euclidean distance to determine the hyperplane and which side each data 51 | /// sample falls on. 52 | /// 53 | private static (int[] indicesLeft, int[] indicesRight, float[] hyperplaneVector, float hyperplaneOffset) EuclideanRandomProjectionSplit(float[][] data, int[] indices, IProvideRandomValues random) 54 | { 55 | var dim = data[0].Length; 56 | 57 | // Select two random points, set the hyperplane between them 58 | var leftIndex = random.Next(0, indices.Length); 59 | var rightIndex = random.Next(0, indices.Length); 60 | rightIndex += (leftIndex == rightIndex) ? 1 : 0; 61 | rightIndex %= indices.Length; 62 | var left = indices[leftIndex]; 63 | var right = indices[rightIndex]; 64 | 65 | // Compute the normal vector to the hyperplane (the vector between the two points) and the offset from the origin 66 | var hyperplaneOffset = 0f; 67 | var hyperplaneVector = new float[dim]; 68 | for (var i = 0; i < hyperplaneVector.Length; i++) 69 | { 70 | hyperplaneVector[i] = data[left][i] - data[right][i]; 71 | hyperplaneOffset -= (hyperplaneVector[i] * (data[left][i] + data[right][i])) / 2; 72 | } 73 | 74 | // For each point compute the margin (project into normal vector) 75 | // If we are on lower side of the hyperplane put in one pile, otherwise put it in the other pile (if we hit hyperplane on the nose, flip a coin) 76 | var nLeft = 0; 77 | var nRight = 0; 78 | var side = new int[indices.Length]; 79 | for (var i = 0; i < indices.Length; i++) 80 | { 81 | var margin = hyperplaneOffset; 82 | for (var d = 0; d < dim; d++) 83 | { 84 | margin += hyperplaneVector[d] * data[indices[i]][d]; 85 | } 86 | 87 | if (margin == 0) 88 | { 89 | side[i] = random.Next(0, 2); 90 | if (side[i] == 0) 91 | { 92 | nLeft += 1; 93 | } 94 | else 95 | { 96 | nRight += 1; 97 | } 98 | } 99 | else if (margin > 0) 100 | { 101 | side[i] = 0; 102 | nLeft += 1; 103 | } 104 | else 105 | { 106 | side[i] = 1; 107 | nRight += 1; 108 | } 109 | } 110 | 111 | // Now that we have the counts, allocate arrays 112 | var indicesLeft = new int[nLeft]; 113 | var indicesRight = new int[nRight]; 114 | 115 | // Populate the arrays with indices according to which side they fell on 116 | nLeft = 0; 117 | nRight = 0; 118 | for (var i = 0; i < side.Length; i++) 119 | { 120 | if (side[i] == 0) 121 | { 122 | indicesLeft[nLeft] = indices[i]; 123 | nLeft += 1; 124 | } 125 | else 126 | { 127 | indicesRight[nRight] = indices[i]; 128 | nRight += 1; 129 | } 130 | } 131 | 132 | return (indicesLeft, indicesRight, hyperplaneVector, hyperplaneOffset); 133 | } 134 | 135 | private static (int nodeNum, int leafNum) RecursiveFlatten(RandomProjectionTreeNode tree, float[][] hyperplanes, float[] offsets, int[][] children, int[][] indices, int nodeNum, int leafNum) 136 | { 137 | if (tree.IsLeaf) 138 | { 139 | children[nodeNum][0] = -leafNum; 140 | 141 | // TODO[umap-js]: Triple check this operation corresponds to 142 | // indices[leafNum : tree.indices.shape[0]] = tree.indices 143 | tree.Indices.CopyTo(indices[leafNum], 0); 144 | leafNum += 1; 145 | return (nodeNum, leafNum); 146 | } 147 | else 148 | { 149 | hyperplanes[nodeNum] = tree.Hyperplane; 150 | offsets[nodeNum] = tree.Offset; 151 | children[nodeNum][0] = nodeNum + 1; 152 | var oldNodeNum = nodeNum; 153 | 154 | var res = RecursiveFlatten( 155 | tree.LeftChild, 156 | hyperplanes, 157 | offsets, 158 | children, 159 | indices, 160 | nodeNum + 1, 161 | leafNum 162 | ); 163 | nodeNum = res.nodeNum; 164 | leafNum = res.leafNum; 165 | 166 | children[oldNodeNum][1] = nodeNum + 1; 167 | 168 | res = RecursiveFlatten( 169 | tree.RightChild, 170 | hyperplanes, 171 | offsets, 172 | children, 173 | indices, 174 | nodeNum + 1, 175 | leafNum 176 | ); 177 | return (res.nodeNum, res.leafNum); 178 | } 179 | } 180 | 181 | private static int NumNodes(RandomProjectionTreeNode tree) => tree.IsLeaf ? 1 : (1 + NumNodes(tree.LeftChild) + NumNodes(tree.RightChild)); 182 | 183 | private static int NumLeaves(RandomProjectionTreeNode tree) => tree.IsLeaf ? 1 : (1 + NumLeaves(tree.LeftChild) + NumLeaves(tree.RightChild)); 184 | 185 | /// 186 | /// Generate an array of sets of candidate nearest neighbors by constructing a random projection forest and taking the leaves of all the trees. Any given tree has leaves that are 187 | /// a set of potential nearest neighbors.Given enough trees the set of all such leaves gives a good likelihood of getting a good set of nearest neighbors in composite. Since such 188 | /// a random projection forest is inexpensive to compute, this can be a useful means of seeding other nearest neighbor algorithms. 189 | /// 190 | public static int[][] MakeLeafArray(FlatTree[] forest) 191 | { 192 | if (forest.Length > 0) 193 | { 194 | var output = new List(); 195 | foreach (var tree in forest) 196 | { 197 | foreach (var entry in tree.Indices) 198 | { 199 | output.Add(entry); 200 | } 201 | } 202 | return output.ToArray(); 203 | } 204 | else 205 | { 206 | return new[] { new[] { -1 } }; 207 | } 208 | } 209 | 210 | /// 211 | /// Searches a flattened rp-tree for a point 212 | /// 213 | public static int[] SearchFlatTree(float[] point, FlatTree tree, IProvideRandomValues random) 214 | { 215 | var node = 0; 216 | while (tree.Children[node][0] > 0) 217 | { 218 | var side = SelectSide(tree.Hyperplanes[node], tree.Offsets[node], point, random); 219 | if (side == 0) 220 | { 221 | node = tree.Children[node][0]; 222 | } 223 | else 224 | { 225 | node = tree.Children[node][1]; 226 | } 227 | } 228 | var index = -1 * tree.Children[node][0]; 229 | return tree.Indices[index]; 230 | } 231 | 232 | /// 233 | /// Select the side of the tree to search during flat tree search 234 | /// 235 | private static int SelectSide(float[] hyperplane, float offset, float[] point, IProvideRandomValues random) 236 | { 237 | var margin = offset; 238 | for (var d = 0; d < point.Length; d++) 239 | { 240 | margin += hyperplane[d] * point[d]; 241 | } 242 | 243 | if (margin == 0) 244 | { 245 | return random.Next(0, 2); 246 | } 247 | else if (margin > 0) 248 | { 249 | return 0; 250 | } 251 | else 252 | { 253 | return 1; 254 | } 255 | } 256 | 257 | public sealed class FlatTree 258 | { 259 | public float[][] Hyperplanes { get; set; } 260 | public float[] Offsets { get; set; } 261 | public int[][] Children { get; set; } 262 | public int[][] Indices { get; set; } 263 | } 264 | 265 | public sealed class RandomProjectionTreeNode 266 | { 267 | public bool IsLeaf { get; set; } 268 | public int[] Indices { get; set; } 269 | public RandomProjectionTreeNode LeftChild { get; set; } 270 | public RandomProjectionTreeNode RightChild { get; set; } 271 | public float[] Hyperplane { get; set; } 272 | public float Offset { get; set; } 273 | } 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /UMAP/UMAP.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | UMAP 5 | netstandard2.0;netstandard2.1;net7.0 6 | UMAP 7 | UMAP 8 | Curiosity GmbH 9 | Curiosity GmbH 10 | 11 | C# implementation of "Uniform Manifold Approximation and Projection" (UMAP) for embedding projections. 12 | MIT 13 | https://github.com/curiosity-ai/umap-sharp 14 | https://github.com/curiosity-ai/umap-sharp 15 | UMAP, embeddings projection, dimensionality reduction, TSNE, data science, embeddings, word2vec 16 | true 17 | (c) Copyright 2022 Curiosity GmbH - all right reserved 18 | false 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /UMAP/Umap.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.CompilerServices; 5 | using System.Threading.Tasks; 6 | using static UMAP.NNDescent; 7 | using static UMAP.Tree; 8 | 9 | namespace UMAP 10 | { 11 | public sealed class Umap 12 | { 13 | private const float SMOOTH_K_TOLERANCE = 1e-5f; 14 | private const float MIN_K_DIST_SCALE = 1e-3f; 15 | 16 | private readonly float _learningRate = 1f; 17 | private readonly float _localConnectivity = 1f; 18 | private readonly float _minDist = 0.1f; 19 | private readonly int _negativeSampleRate = 5; 20 | private readonly float _repulsionStrength = 1; 21 | private readonly float _setOpMixRatio = 1; 22 | private readonly float _spread = 1; 23 | 24 | private readonly DistanceCalculation _distanceFn; 25 | private readonly IProvideRandomValues _random; 26 | private readonly int _nNeighbors; 27 | private readonly int? _customNumberOfEpochs; 28 | private readonly ProgressReporter _progressReporter; 29 | 30 | // KNN state (can be precomputed and supplied via initializeFit) 31 | private int[][] _knnIndices = null; 32 | private float[][] _knnDistances = null; 33 | 34 | // Internal graph connectivity representation 35 | private SparseMatrix _graph = null; 36 | private float[][] _x = null; 37 | private bool _isInitialized = false; 38 | private FlatTree[] _rpForest = new FlatTree[0]; 39 | 40 | // Projected embedding 41 | private float[] _embedding; 42 | private readonly OptimizationState _optimizationState; 43 | 44 | /// 45 | /// The progress will be a value from 0 to 1 that indicates approximately how much of the processing has been completed 46 | /// 47 | public delegate void ProgressReporter(float progress); 48 | 49 | public Umap( 50 | DistanceCalculation distance = null, 51 | IProvideRandomValues random = null, 52 | int dimensions = 2, 53 | int numberOfNeighbors = 15, 54 | int? customNumberOfEpochs = null, 55 | ProgressReporter progressReporter = null) 56 | { 57 | if ((customNumberOfEpochs != null) && (customNumberOfEpochs <= 0)) 58 | { 59 | throw new ArgumentOutOfRangeException(nameof(customNumberOfEpochs), "if non-null then must be a positive value"); 60 | } 61 | 62 | _distanceFn = distance ?? DistanceFunctions.Cosine; 63 | _random = random ?? DefaultRandomGenerator.Instance; 64 | _nNeighbors = numberOfNeighbors; 65 | _optimizationState = new OptimizationState { Dim = dimensions }; 66 | _customNumberOfEpochs = customNumberOfEpochs; 67 | _progressReporter = progressReporter; 68 | } 69 | 70 | /// 71 | /// Initializes fit by computing KNN and a fuzzy simplicial set, as well as initializing the projected embeddings. Sets the optimization state ahead of optimization steps. 72 | /// Returns the number of epochs to be used for the SGD optimization. 73 | /// 74 | public int InitializeFit(float[][] x) 75 | { 76 | // We don't need to reinitialize if we've already initialized for this data 77 | if ((_x == x) && _isInitialized) 78 | { 79 | return GetNEpochs(); 80 | } 81 | 82 | // For large quantities of data (which is where the progress estimating is more useful), InitializeFit takes at least 80% of the total time (the calls to Step are 83 | // completed much more quickly AND they naturally lend themselves to granular progress updates; one per loop compared to the recommended number of epochs) 84 | ProgressReporter initializeFitProgressReporter = (_progressReporter is null) ? (progress => { }) : ScaleProgressReporter(_progressReporter, 0, 0.8f); 85 | 86 | _x = x; 87 | if ((_knnIndices is null) && (_knnDistances is null)) 88 | { 89 | // This part of the process very roughly accounts for 1/3 of the work 90 | (_knnIndices, _knnDistances) = NearestNeighbors(x, ScaleProgressReporter(initializeFitProgressReporter, 0, 0.3f)); 91 | } 92 | 93 | // This part of the process very roughly accounts for 2/3 of the work (the reamining work is in the Step calls) 94 | _graph = FuzzySimplicialSet(x, _nNeighbors, _setOpMixRatio, ScaleProgressReporter(initializeFitProgressReporter, 0.3f, 1)); 95 | 96 | var (head, tail, epochsPerSample) = InitializeSimplicialSetEmbedding(); 97 | 98 | // Set the optimization routine state 99 | _optimizationState.Head = head; 100 | _optimizationState.Tail = tail; 101 | _optimizationState.EpochsPerSample = epochsPerSample; 102 | 103 | // Now, initialize the optimization steps 104 | InitializeOptimization(); 105 | PrepareForOptimizationLoop(); 106 | _isInitialized = true; 107 | 108 | return GetNEpochs(); 109 | } 110 | 111 | public float[][] GetEmbedding() 112 | { 113 | var final = new float[_optimizationState.NVertices][]; 114 | Span span = _embedding.AsSpan(); 115 | for (int i = 0; i < _optimizationState.NVertices; i++) 116 | { 117 | final[i] = span.Slice(i * _optimizationState.Dim, _optimizationState.Dim).ToArray(); 118 | } 119 | return final; 120 | } 121 | 122 | /// 123 | /// Gets the number of epochs for optimizing the projection - NOTE: This heuristic differs from the python version 124 | /// 125 | private int GetNEpochs() 126 | { 127 | if (_customNumberOfEpochs != null) 128 | { 129 | return _customNumberOfEpochs.Value; 130 | } 131 | 132 | var length = _graph.Dims.rows; 133 | if (length <= 2500) 134 | { 135 | return 500; 136 | } 137 | else if (length <= 5000) 138 | { 139 | return 400; 140 | } 141 | else if (length <= 7500) 142 | { 143 | return 300; 144 | } 145 | else 146 | { 147 | return 200; 148 | } 149 | } 150 | 151 | /// 152 | /// Compute the ``nNeighbors`` nearest points for each data point in ``X`` - this may be exact, but more likely is approximated via nearest neighbor descent. 153 | /// 154 | internal (int[][] knnIndices, float[][] knnDistances) NearestNeighbors(float[][] x, ProgressReporter progressReporter) 155 | { 156 | var metricNNDescent = MakeNNDescent(_distanceFn, _random); 157 | progressReporter(0.05f); 158 | var nTrees = 5 + Round(Math.Sqrt(x.Length) / 20); 159 | var nIters = Math.Max(5, (int)Math.Floor(Math.Round(Math.Log(x.Length, 2)))); 160 | progressReporter(0.1f); 161 | var leafSize = Math.Max(10, _nNeighbors); 162 | var forestProgressReporter = ScaleProgressReporter(progressReporter, 0.1f, 0.4f); 163 | _rpForest = Enumerable.Range(0, nTrees) 164 | .Select(i => 165 | { 166 | forestProgressReporter((float)i / nTrees); 167 | return FlattenTree(MakeTree(x, leafSize, i, _random), leafSize); 168 | }) 169 | .ToArray(); 170 | var leafArray = MakeLeafArray(_rpForest); 171 | progressReporter(0.45f); 172 | var nnDescendProgressReporter = ScaleProgressReporter(progressReporter, 0.5f, 1); 173 | return metricNNDescent(x, leafArray, _nNeighbors, nIters, startingIteration: (i, max) => nnDescendProgressReporter((float)i / max)); 174 | 175 | // Handle python3 rounding down from 0.5 discrpancy 176 | int Round(double n) => (n == 0.5) ? 0 : (int)Math.Floor(Math.Round(n)); 177 | } 178 | 179 | /// 180 | /// Given a set of data X, a neighborhood size, and a measure of distance compute the fuzzy simplicial set(here represented as a fuzzy graph in the form of a sparse matrix) associated 181 | /// to the data. This is done by locally approximating geodesic distance at each point, creating a fuzzy simplicial set for each such point, and then combining all the local fuzzy 182 | /// simplicial sets into a global one via a fuzzy union. 183 | /// 184 | private SparseMatrix FuzzySimplicialSet(float[][] x, int nNeighbors, float setOpMixRatio, ProgressReporter progressReporter) 185 | { 186 | var knnIndices = _knnIndices ?? new int[0][]; 187 | var knnDistances = _knnDistances ?? new float[0][]; 188 | progressReporter(0.1f); 189 | var (sigmas, rhos) = SmoothKNNDistance(knnDistances, nNeighbors, _localConnectivity); 190 | progressReporter(0.2f); 191 | var (rows, cols, vals) = ComputeMembershipStrengths(knnIndices, knnDistances, sigmas, rhos); 192 | progressReporter(0.3f); 193 | var sparseMatrix = new SparseMatrix(rows, cols, vals, (x.Length, x.Length)); 194 | var transpose = sparseMatrix.Transpose(); 195 | var prodMatrix = sparseMatrix.PairwiseMultiply(transpose); 196 | progressReporter(0.4f); 197 | var a = sparseMatrix.Add(transpose).Subtract(prodMatrix); 198 | progressReporter(0.5f); 199 | var b = a.MultiplyScalar(setOpMixRatio); 200 | progressReporter(0.6f); 201 | var c = prodMatrix.MultiplyScalar(1 - setOpMixRatio); 202 | progressReporter(0.7f); 203 | var result = b.Add(c); 204 | progressReporter(0.8f); 205 | return result; 206 | } 207 | 208 | private static (float[] sigmas, float[] rhos) SmoothKNNDistance(float[][] distances, int k, float localConnectivity = 1, int nIter = 64, float bandwidth = 1) 209 | { 210 | var target = Math.Log(k, 2) * bandwidth; // TODO: Use Math.Log2 (when update framework to a version that supports it) or consider a pre-computed table 211 | var rho = new float[distances.Length]; 212 | var result = new float[distances.Length]; 213 | for (var i = 0; i < distances.Length; i++) 214 | { 215 | var lo = 0f; 216 | var hi = float.MaxValue; 217 | var mid = 1f; 218 | 219 | // TODO[umap-js]: This is very inefficient, but will do for now. FIXME 220 | var ithDistances = distances[i]; 221 | var nonZeroDists = ithDistances.Where(d => d > 0).ToArray(); 222 | if (nonZeroDists.Length >= localConnectivity) 223 | { 224 | var index = (int)Math.Floor(localConnectivity); 225 | var interpolation = localConnectivity - index; 226 | if (index > 0) 227 | { 228 | rho[i] = nonZeroDists[index - 1]; 229 | if (interpolation > SMOOTH_K_TOLERANCE) 230 | { 231 | rho[i] += interpolation * (nonZeroDists[index] - nonZeroDists[index - 1]); 232 | } 233 | } 234 | else 235 | { 236 | rho[i] = interpolation * nonZeroDists[0]; 237 | } 238 | } 239 | else if (nonZeroDists.Length > 0) 240 | { 241 | rho[i] = Utils.Max(nonZeroDists); 242 | } 243 | 244 | for (var n = 0; n < nIter; n++) 245 | { 246 | var psum = 0.0; 247 | for (var j = 1; j < distances[i].Length; j++) 248 | { 249 | var d = distances[i][j] - rho[i]; 250 | if (d > 0) 251 | { 252 | psum += Math.Exp(-(d / mid)); 253 | } 254 | else 255 | { 256 | psum += 1.0; 257 | } 258 | } 259 | if (Math.Abs(psum - target) < SMOOTH_K_TOLERANCE) 260 | { 261 | break; 262 | } 263 | 264 | if (psum > target) 265 | { 266 | hi = mid; 267 | mid = (lo + hi) / 2; 268 | } 269 | else 270 | { 271 | lo = mid; 272 | if (hi == float.MaxValue) 273 | { 274 | mid *= 2; 275 | } 276 | else 277 | { 278 | mid = (lo + hi) / 2; 279 | } 280 | } 281 | } 282 | 283 | result[i] = mid; 284 | 285 | // TODO[umap-js]: This is very inefficient, but will do for now. FIXME 286 | if (rho[i] > 0) 287 | { 288 | var meanIthDistances = Utils.Mean(ithDistances); 289 | if (result[i] < MIN_K_DIST_SCALE * meanIthDistances) 290 | { 291 | result[i] = MIN_K_DIST_SCALE * meanIthDistances; 292 | } 293 | } 294 | else 295 | { 296 | var meanDistances = Utils.Mean(distances.Select(Utils.Mean).ToArray()); 297 | if (result[i] < MIN_K_DIST_SCALE * meanDistances) 298 | { 299 | result[i] = MIN_K_DIST_SCALE * meanDistances; 300 | } 301 | } 302 | } 303 | return (result, rho); 304 | } 305 | 306 | private static (int[] rows, int[] cols, float[] vals) ComputeMembershipStrengths(int[][] knnIndices, float[][] knnDistances, float[] sigmas, float[] rhos) 307 | { 308 | var nSamples = knnIndices.Length; 309 | var nNeighbors = knnIndices[0].Length; 310 | 311 | var rows = new int[nSamples * nNeighbors]; 312 | var cols = new int[nSamples * nNeighbors]; 313 | var vals = new float[nSamples * nNeighbors]; 314 | for (var i = 0; i < nSamples; i++) 315 | { 316 | for (var j = 0; j < nNeighbors; j++) 317 | { 318 | if (knnIndices[i][j] == -1) 319 | { 320 | continue; // We didn't get the full knn for i 321 | } 322 | 323 | float val; 324 | if (knnIndices[i][j] == i) 325 | { 326 | val = 0; 327 | } 328 | else if (knnDistances[i][j] - rhos[i] <= 0.0) 329 | { 330 | val = 1; 331 | } 332 | else 333 | { 334 | val = (float)Math.Exp(-((knnDistances[i][j] - rhos[i]) / sigmas[i])); 335 | } 336 | 337 | rows[i * nNeighbors + j] = i; 338 | cols[i * nNeighbors + j] = knnIndices[i][j]; 339 | vals[i * nNeighbors + j] = val; 340 | } 341 | } 342 | return (rows, cols, vals); 343 | } 344 | 345 | /// 346 | /// Initialize a fuzzy simplicial set embedding, using a specified initialisation method and then minimizing the fuzzy set cross entropy between the 1-skeletons of the high and low 347 | /// dimensional fuzzy simplicial sets. 348 | /// 349 | private (int[] head, int[] tail, float[] epochsPerSample) InitializeSimplicialSetEmbedding() 350 | { 351 | var nEpochs = GetNEpochs(); 352 | var graphMax = 0f; 353 | foreach (var value in _graph.GetValues()) 354 | { 355 | if (graphMax < value) 356 | { 357 | graphMax = value; 358 | } 359 | } 360 | 361 | var graph = _graph.Map(value => (value < graphMax / nEpochs) ? 0 : value); 362 | 363 | // We're not computing the spectral initialization in this implementation until we determine a better eigenvalue/eigenvector computation approach 364 | 365 | _embedding = new float[graph.Dims.rows * _optimizationState.Dim]; 366 | SIMDint.Uniform(ref _embedding, 10, _random); 367 | 368 | // Get graph data in ordered way... 369 | var weights = new List(); 370 | var head = new List(); 371 | var tail = new List(); 372 | foreach (var (row, col, value) in graph.GetAll()) 373 | { 374 | if (value != 0) 375 | { 376 | weights.Add(value); 377 | tail.Add(row); 378 | head.Add(col); 379 | } 380 | } 381 | ShuffleTogether(head, tail, weights); 382 | return (head.ToArray(), tail.ToArray(), MakeEpochsPerSample(weights.ToArray(), nEpochs)); 383 | } 384 | 385 | private void ShuffleTogether(List list, List other, List weights) 386 | { 387 | int n = list.Count; 388 | if (other.Count != n) { throw new Exception(); } 389 | while (n > 1) 390 | { 391 | n--; 392 | int k = _random.Next(0, n + 1); 393 | T value = list[k]; 394 | list[k] = list[n]; 395 | list[n] = value; 396 | 397 | T2 otherValue = other[k]; 398 | other[k] = other[n]; 399 | other[n] = otherValue; 400 | 401 | T3 weightsValue = weights[k]; 402 | weights[k] = weights[n]; 403 | weights[n] = weightsValue; 404 | } 405 | } 406 | 407 | private static float[] MakeEpochsPerSample(float[] weights, int nEpochs) 408 | { 409 | var result = Utils.Filled(weights.Length, -1); 410 | var max = Utils.Max(weights); 411 | foreach (var (n, i) in weights.Select((w, i) => ((w / max) * nEpochs, i))) 412 | { 413 | if (n > 0) 414 | { 415 | result[i] = nEpochs / n; 416 | } 417 | } 418 | return result; 419 | } 420 | 421 | private void InitializeOptimization() 422 | { 423 | // Initialized in initializeSimplicialSetEmbedding() 424 | var head = _optimizationState.Head; 425 | var tail = _optimizationState.Tail; 426 | var epochsPerSample = _optimizationState.EpochsPerSample; 427 | 428 | var nEpochs = GetNEpochs(); 429 | var nVertices = _graph.Dims.cols; 430 | 431 | var (a, b) = FindABParams(_spread, _minDist); 432 | 433 | _optimizationState.Head = head; 434 | _optimizationState.Tail = tail; 435 | _optimizationState.EpochsPerSample = epochsPerSample; 436 | _optimizationState.A = a; 437 | _optimizationState.B = b; 438 | _optimizationState.NEpochs = nEpochs; 439 | _optimizationState.NVertices = nVertices; 440 | } 441 | 442 | internal static (float a, float b) FindABParams(float spread, float minDist) 443 | { 444 | // 2019-06-21 DWR: If we need to support other spread, minDist values then we might be able to use the LM implementation in Accord.NET but I'll hard code values that relate to the default configuration for now 445 | if ((spread != 1) || (minDist != 0.1f)) 446 | { 447 | throw new ArgumentException($"Currently, the {nameof(FindABParams)} method only supports spread, minDist values of 1, 0.1 (the Levenberg-Marquardt algorithm is required to process other values"); 448 | } 449 | 450 | return (1.5694704762346365f, 0.8941996053733949f); 451 | } 452 | 453 | private void PrepareForOptimizationLoop() 454 | { 455 | // Hyperparameters 456 | var repulsionStrength = _repulsionStrength; 457 | var learningRate = _learningRate; 458 | var negativeSampleRate = _negativeSampleRate; 459 | 460 | var epochsPerSample = _optimizationState.EpochsPerSample; 461 | 462 | var dim = _optimizationState.Dim; 463 | 464 | var epochsPerNegativeSample = epochsPerSample.Select(e => e / negativeSampleRate).ToArray(); 465 | var epochOfNextNegativeSample = epochsPerNegativeSample.ToArray(); 466 | var epochOfNextSample = epochsPerSample.ToArray(); 467 | 468 | _optimizationState.EpochOfNextSample = epochOfNextSample; 469 | _optimizationState.EpochOfNextNegativeSample = epochOfNextNegativeSample; 470 | _optimizationState.EpochsPerNegativeSample = epochsPerNegativeSample; 471 | 472 | _optimizationState.MoveOther = true; 473 | _optimizationState.InitialAlpha = learningRate; 474 | _optimizationState.Alpha = learningRate; 475 | _optimizationState.Gamma = repulsionStrength; 476 | _optimizationState.Dim = dim; 477 | } 478 | 479 | /// 480 | /// Manually step through the optimization process one epoch at a time 481 | /// 482 | public int Step() 483 | { 484 | var currentEpoch = _optimizationState.CurrentEpoch; 485 | var numberOfEpochsToComplete = GetNEpochs(); 486 | if (currentEpoch < numberOfEpochsToComplete) 487 | { 488 | OptimizeLayoutStep(currentEpoch); 489 | if (_progressReporter is object) 490 | { 491 | // InitializeFit roughly approximately takes 80% of the processing time for large quantities of data, leaving 20% for the Step iterations - the progress reporter 492 | // calls made here are based on the assumption that Step will be called the recommended number of times (the number-of-epochs value returned from InitializeFit) 493 | ScaleProgressReporter(_progressReporter, 0.8f, 1)((float)currentEpoch / numberOfEpochsToComplete); 494 | } 495 | } 496 | return _optimizationState.CurrentEpoch; 497 | } 498 | 499 | /// 500 | /// Improve an embedding using stochastic gradient descent to minimize the fuzzy set cross entropy between the 1-skeletons of the high dimensional and low dimensional fuzzy simplicial sets. 501 | /// In practice this is done by sampling edges based on their membership strength(with the (1-p) terms coming from negative sampling similar to word2vec). 502 | /// 503 | private void OptimizeLayoutStep(int n) 504 | { 505 | if (_random.IsThreadSafe) 506 | { 507 | Parallel.For(0, _optimizationState.EpochsPerSample.Length, Iterate); 508 | } 509 | else 510 | { 511 | for (var i = 0; i < _optimizationState.EpochsPerSample.Length; i++) 512 | { 513 | Iterate(i); 514 | } 515 | } 516 | 517 | _optimizationState.Alpha = _optimizationState.InitialAlpha * (1f - n / _optimizationState.NEpochs); 518 | _optimizationState.CurrentEpoch += 1; 519 | 520 | void Iterate(int i) 521 | { 522 | if (_optimizationState.EpochOfNextSample[i] >= n) 523 | { 524 | return; 525 | } 526 | 527 | Span embeddingSpan = _embedding.AsSpan(); 528 | 529 | int j = _optimizationState.Head[i]; 530 | int k = _optimizationState.Tail[i]; 531 | 532 | var current = embeddingSpan.Slice(j * _optimizationState.Dim, _optimizationState.Dim); 533 | var other = embeddingSpan.Slice(k * _optimizationState.Dim, _optimizationState.Dim); 534 | 535 | var distSquared = RDist(current, other); 536 | var gradCoeff = 0f; 537 | 538 | if (distSquared > 0) 539 | { 540 | gradCoeff = -2 * _optimizationState.A * _optimizationState.B * (float)Math.Pow(distSquared, _optimizationState.B - 1); 541 | gradCoeff /= _optimizationState.A * (float)Math.Pow(distSquared, _optimizationState.B) + 1; 542 | } 543 | 544 | const float clipValue = 4f; 545 | for (var d = 0; d < _optimizationState.Dim; d++) 546 | { 547 | var gradD = Clip(gradCoeff * (current[d] - other[d]), clipValue); 548 | current[d] += gradD * _optimizationState.Alpha; 549 | if (_optimizationState.MoveOther) 550 | { 551 | other[d] += -gradD * _optimizationState.Alpha; 552 | } 553 | } 554 | 555 | _optimizationState.EpochOfNextSample[i] += _optimizationState.EpochsPerSample[i]; 556 | 557 | var nNegSamples = (int)Math.Floor((double)(n - _optimizationState.EpochOfNextNegativeSample[i]) / _optimizationState.EpochsPerNegativeSample[i]); 558 | 559 | for (var p = 0; p < nNegSamples; p++) 560 | { 561 | k = _random.Next(0, _optimizationState.NVertices); 562 | other = embeddingSpan.Slice(k * _optimizationState.Dim, _optimizationState.Dim); 563 | distSquared = RDist(current, other); 564 | gradCoeff = 0f; 565 | if (distSquared > 0) 566 | { 567 | gradCoeff = 2 * _optimizationState.Gamma * _optimizationState.B; 568 | gradCoeff *= _optimizationState.GetDistanceFactor(distSquared); //Preparation for future work for interpolating the table before optimizing 569 | } 570 | else if (j == k) 571 | { 572 | continue; 573 | } 574 | 575 | for (var d = 0; d < _optimizationState.Dim; d++) 576 | { 577 | var gradD = 4f; 578 | if (gradCoeff > 0) 579 | { 580 | gradD = Clip(gradCoeff * (current[d] - other[d]), clipValue); 581 | } 582 | 583 | current[d] += gradD * _optimizationState.Alpha; 584 | } 585 | } 586 | 587 | _optimizationState.EpochOfNextNegativeSample[i] += nNegSamples * _optimizationState.EpochsPerNegativeSample[i]; 588 | } 589 | } 590 | 591 | /// 592 | /// Reduced Euclidean distance 593 | /// 594 | private static float RDist(Span x, Span y) 595 | { 596 | //return Mosaik.Core.SIMD.Euclidean(ref x, ref y); 597 | var distSquared = 0f; 598 | for (var i = 0; i < x.Length; i++) 599 | { 600 | var d = x[i] - y[i]; 601 | distSquared += d * d; 602 | } 603 | return distSquared; 604 | } 605 | 606 | /// 607 | /// Standard clamping of a value into a fixed range 608 | /// 609 | private static float Clip(float x, float clipValue) 610 | { 611 | if (x > clipValue) 612 | { 613 | return clipValue; 614 | } 615 | else if (x < -clipValue) 616 | { 617 | return -clipValue; 618 | } 619 | else 620 | { 621 | return x; 622 | } 623 | } 624 | 625 | private static ProgressReporter ScaleProgressReporter(ProgressReporter progressReporter, float start, float end) 626 | { 627 | var range = end - start; 628 | return progress => progressReporter((range * progress) + start); 629 | } 630 | 631 | public static class DistanceFunctions 632 | { 633 | public static float Cosine(float[] lhs, float[] rhs) 634 | { 635 | return 1 - (SIMD.DotProduct(ref lhs, ref rhs) / (SIMD.Magnitude(ref lhs) * SIMD.Magnitude(ref rhs))); 636 | } 637 | 638 | public static float CosineForNormalizedVectors(float[] lhs, float[] rhs) 639 | { 640 | return 1 - SIMD.DotProduct(ref lhs, ref rhs); 641 | } 642 | 643 | public static float Euclidean(float[] lhs, float[] rhs) 644 | { 645 | return (float)Math.Sqrt(SIMD.Euclidean(ref lhs, ref rhs)); // TODO: Replace with netcore3 MathF class when the framework is available 646 | } 647 | } 648 | 649 | private sealed class OptimizationState 650 | { 651 | public int CurrentEpoch = 0; 652 | public int[] Head = new int[0]; 653 | public int[] Tail = new int[0]; 654 | public float[] EpochsPerSample = new float[0]; 655 | public float[] EpochOfNextSample = new float[0]; 656 | public float[] EpochOfNextNegativeSample= new float[0]; 657 | public float[] EpochsPerNegativeSample = new float[0]; 658 | public bool MoveOther = true; 659 | public float InitialAlpha = 1; 660 | public float Alpha = 1; 661 | public float Gamma = 1; 662 | public float A = 1.5769434603113077f; 663 | public float B = 0.8950608779109733f; 664 | public int Dim = 2; 665 | public int NEpochs = 500; 666 | public int NVertices = 0; 667 | 668 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 669 | public float GetDistanceFactor(float distSquared) => 1f / ((0.001f + distSquared) * (float)(A * Math.Pow(distSquared, B) + 1)); 670 | } 671 | } 672 | } -------------------------------------------------------------------------------- /UMAP/Utils.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | 3 | namespace UMAP 4 | { 5 | internal static class Utils 6 | { 7 | /// 8 | /// Creates an empty array 9 | /// 10 | public static float[] Empty(int n) => new float[n]; 11 | 12 | /// 13 | /// Creates an array filled with index values 14 | /// 15 | public static float[] Range(int n) => Enumerable.Range(0, n).Select(i => (float)i).ToArray(); 16 | 17 | /// 18 | /// Creates an array filled with a specific value 19 | /// 20 | public static float[] Filled(int count, float value) => Enumerable.Range(0, count).Select(i => value).ToArray(); 21 | 22 | /// 23 | /// Returns the mean of an array 24 | /// 25 | public static float Mean(float[] input) => input.Sum() / input.Length; 26 | 27 | /// 28 | /// Returns the maximum value of an array 29 | /// 30 | public static float Max(float[] input) => input.Max(); 31 | 32 | /// 33 | /// Generate nSamples many integers from 0 to poolSize such that no integer is selected twice.The duplication constraint is achieved via rejection sampling. 34 | /// 35 | public static int[] RejectionSample(int nSamples, int poolSize, IProvideRandomValues random) 36 | { 37 | if(poolSize < nSamples) 38 | { 39 | nSamples = poolSize; 40 | } 41 | var result = new int[nSamples]; 42 | for (var i = 0; i < nSamples; i++) 43 | { 44 | var rejectSample = true; 45 | while (rejectSample) 46 | { 47 | var j = random.Next(0, poolSize); 48 | var broken = false; 49 | for (var k = 0; k < i; k++) 50 | { 51 | if (j == result[k]) 52 | { 53 | broken = true; 54 | break; 55 | } 56 | } 57 | if (!broken) 58 | { 59 | rejectSample = false; 60 | } 61 | 62 | result[i] = j; 63 | } 64 | } 65 | return result; 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /UnitTests/DeterministicRandomGenerator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace UMAP.UnitTests 4 | { 5 | public sealed class DeterministicRandomGenerator : IProvideRandomValues 6 | { 7 | private readonly Prando _rnd; 8 | public DeterministicRandomGenerator(int seed) => _rnd = new Prando(seed); 9 | 10 | public bool IsThreadSafe => false; 11 | 12 | public int Next(int minValue, int maxValue) => _rnd.Next(minValue, maxValue); 13 | 14 | public float NextFloat() => _rnd.NextFloat(); 15 | 16 | public void NextFloats(Span buffer) 17 | { 18 | for (var i = 0; i < buffer.Length; i++) 19 | { 20 | buffer[i] = _rnd.NextFloat(); 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /UnitTests/Prando.cs: -------------------------------------------------------------------------------- 1 | namespace UMAP.UnitTests 2 | { 3 | // See https://github.com/zeh/prando/blob/master/src/Prando.ts 4 | public sealed class Prando 5 | { 6 | private readonly int _seed; 7 | private int _value; 8 | public Prando(int seed) 9 | { 10 | _seed = GetSafeSeed(seed); 11 | _value = _seed; 12 | } 13 | 14 | /// 15 | /// Generates a pseudo-random number between a lower (inclusive) and a higher (exclusive) bounds 16 | /// 17 | public float NextFloat(float min = 0, float pseudoMax = 1) 18 | { 19 | Recalculate(); 20 | return Map(_value, int.MinValue, int.MaxValue, min, pseudoMax); 21 | } 22 | 23 | /// 24 | /// Returns a random integer that is within a specified range. 25 | /// 26 | /// The inclusive lower bound of the random number returned. 27 | /// The exclusive upper bound of the random number returned. maxValue must be greater than or equal to minValue. 28 | /// A 32-bit signed integer greater than or equal to minValue and less than maxValue; that is, the range of return values includes minValue but not maxValue. If minValue 29 | // equals maxValue, minValue is returned. 30 | public int Next(int minValue, int maxValue) 31 | { 32 | Recalculate(); 33 | return (int)Map(_value, int.MinValue, int.MaxValue, minValue, maxValue); 34 | } 35 | 36 | private void Recalculate() 37 | { 38 | _value = XorShift(_value); 39 | } 40 | 41 | private static float Map(int val, int minFrom, int maxFrom, float minTo, float maxTo) 42 | { 43 | var availableRange = (float)maxFrom - minFrom; // Perform the calculation as float because it will overflow if it's done in Int32 space 44 | var distanceOfValueIntoRange = (float)val - minFrom; 45 | return (distanceOfValueIntoRange / availableRange) * (maxTo - minTo) + minTo; 46 | } 47 | 48 | private static int XorShift(int value) 49 | { 50 | // Xorshift*32 51 | // Based on George Marsaglia's work: http://www.jstatsoft.org/v08/i14/paper 52 | value ^= value << 13; 53 | value ^= value >> 17; 54 | value ^= value << 5; 55 | return value; 56 | } 57 | 58 | private static int GetSafeSeed(int seed) => (seed == 0) ? 1 : seed; 59 | } 60 | } -------------------------------------------------------------------------------- /UnitTests/SparseMatrixTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Xunit; 3 | 4 | namespace UMAP.UnitTests 5 | { 6 | public static class SparseMatrixTests 7 | { 8 | [Fact] 9 | public static void ConstructsSpareMatrixFromRowsColsVals() 10 | { 11 | var rows = new[] { 0, 0, 1, 1 }; 12 | var cols = new[] { 0, 1, 0, 1 }; 13 | var vals = new[] { 1f, 2f, 3f, 4f }; 14 | var dims = (2, 2); 15 | var matrix = new SparseMatrix(rows, cols, vals, dims); 16 | Assert.Equal(rows, matrix.GetRows()); 17 | Assert.Equal(cols, matrix.GetCols()); 18 | Assert.Equal(vals, matrix.GetValues()); 19 | Assert.Equal(2, matrix.Dims.rows); 20 | Assert.Equal(2, matrix.Dims.cols); 21 | } 22 | 23 | [Fact] 24 | public static void HasGetSetMethods() 25 | { 26 | var matrix = GetTestMatrix(); 27 | Assert.Equal(2, matrix.Get(0, 1)); 28 | matrix.Set(0, 1, 9); 29 | Assert.Equal(9, matrix.Get(0, 1)); 30 | } 31 | 32 | [Fact] 33 | public static void HasMapMethod() 34 | { 35 | var matrix = GetTestMatrix(); 36 | var newMatrix = matrix.Map(value => value + 1); 37 | Assert.Equal(new[] { new[] { 2f, 3f }, new[] { 4f, 5f } }, newMatrix.ToArray()); 38 | } 39 | 40 | [Fact] 41 | public static void HasForEachMethod() 42 | { 43 | var rows = new[] { 0, 1 }; 44 | var cols = new[] { 0, 0 }; 45 | var vals = new[] { 1f, 3f }; 46 | var dims = (2, 2); 47 | var matrix = new SparseMatrix(rows, cols, vals, dims); 48 | var entries = new List(); 49 | matrix.ForEach((value, row, col) => entries.Add(new float[] { value, row, col })); 50 | Assert.Equal(new[] { new[] { 1f, 0f, 0f }, new[] { 3f, 1f, 0f } }, entries.ToArray()); 51 | } 52 | 53 | [Fact] 54 | public static void TransposeMethod() => Assert.Equal(new[] { new[] { 1f, 3f }, new[] { 2f, 4f } }, GetTestMatrix().Transpose().ToArray()); 55 | 56 | [Fact] 57 | public static void PairwiseMultiplyMethod() => Assert.Equal(new[] { new[] { 1f, 4f }, new[] { 9f, 16f } }, GetTestMatrix().PairwiseMultiply(GetTestMatrix()).ToArray()); 58 | 59 | [Fact] 60 | public static void AddMethod() => Assert.Equal(new[] { new[] { 2f, 4f }, new[] { 6f, 8f } }, GetTestMatrix().Add(GetTestMatrix()).ToArray()); 61 | 62 | [Fact] 63 | public static void SubtractMethod() => Assert.Equal(new[] { new[] { 0f, 0f }, new[] { 0f, 0f } }, GetTestMatrix().Subtract(GetTestMatrix()).ToArray()); 64 | 65 | [Fact] 66 | public static void ScalarMultiplyMethod() => Assert.Equal(new[] { new[] { 3f, 6f }, new[] { 9f, 12f } }, GetTestMatrix().MultiplyScalar(3).ToArray()); 67 | 68 | [Fact] 69 | public static void GetCSRMethod() 70 | { 71 | var (indices, values, indptr) = GetNormalizationTestMatrix().GetCSR(); 72 | Assert.Equal(new[] { 0, 1, 2, 0, 1, 2, 0, 1, 2 }, indices); 73 | Assert.Equal(new[] { 1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f }, values); 74 | Assert.Equal(new[] { 0, 3, 6 }, indptr); 75 | } 76 | 77 | private static SparseMatrix GetNormalizationTestMatrix() 78 | { 79 | var rows = new[] { 0, 0, 0, 1, 1, 1, 2, 2, 2 }; 80 | var cols = new[] { 0, 1, 2, 0, 1, 2, 0, 1, 2 }; 81 | var vals = new[] { 1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f }; 82 | var dims = (3, 3); 83 | return new SparseMatrix(rows, cols, vals, dims); 84 | } 85 | 86 | private static SparseMatrix GetTestMatrix() 87 | { 88 | var rows = new[] { 0, 0, 1, 1 }; 89 | var cols = new[] { 0, 1, 0, 1 }; 90 | var vals = new[] { 1f, 2f, 3f, 4f }; 91 | var dims = (2, 2); 92 | return new SparseMatrix(rows, cols, vals, dims); 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /UnitTests/UmapTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Xunit; 4 | using static UMAP.UnitTests.UnitTestData; 5 | 6 | namespace UMAP.UnitTests 7 | { 8 | public static class UmapTests 9 | { 10 | [Fact] 11 | public static void StepMethod2D() 12 | { 13 | var umap = new Umap(random: new DeterministicRandomGenerator(42)); 14 | var nEpochs = umap.InitializeFit(TestData); 15 | for (var i = 0; i < nEpochs; i++) 16 | { 17 | umap.Step(); 18 | } 19 | 20 | var embedding = umap.GetEmbedding(); 21 | Assert.Equal(500, nEpochs); 22 | AssertNestedFloatArraysEquivalent(TestResults2D, embedding); 23 | } 24 | 25 | [Fact] 26 | public static void StepMethod3D() 27 | { 28 | var umap = new Umap(random: new DeterministicRandomGenerator(42), dimensions: 3); 29 | var nEpochs = umap.InitializeFit(TestData); 30 | for (var i = 0; i < nEpochs; i++) 31 | { 32 | umap.Step(); 33 | } 34 | 35 | var embedding = umap.GetEmbedding(); 36 | Assert.Equal(500, nEpochs); 37 | AssertNestedFloatArraysEquivalent(TestResults3D, embedding); 38 | } 39 | 40 | [Fact] 41 | public static void FindsNearestNeighbors() 42 | { 43 | var nNeighbors = 10; 44 | var umap = new Umap(random: new DeterministicRandomGenerator(42), numberOfNeighbors: nNeighbors); 45 | var (knnIndices, knnDistances) = umap.NearestNeighbors(TestData, progress => { }); 46 | 47 | Assert.Equal(knnDistances.Length, TestData.Length); 48 | Assert.Equal(knnIndices.Length, TestData.Length); 49 | 50 | Assert.Equal(knnDistances[0].Length, nNeighbors); 51 | Assert.Equal(knnIndices[0].Length, nNeighbors); 52 | } 53 | 54 | [Fact] 55 | public static void FindsABParamsUsingLevenbergMarquardtForDefaultSettings() 56 | { 57 | const float expectedA = 1.5769434603113077f; 58 | const float expectedB = 0.8950608779109733f; 59 | 60 | var (a, b) = Umap.FindABParams(1, 0.1f); 61 | Assert.True(AreCloseEnough(a, expectedA)); 62 | Assert.True(AreCloseEnough(b, expectedB)); 63 | 64 | bool AreCloseEnough(float x, float y) => Math.Abs(x - y) < 0.01; 65 | } 66 | 67 | private static void AssertNestedFloatArraysEquivalent(float[][] expected, float[][] actual) 68 | { 69 | Assert.Equal(expected.Length, actual.Length); 70 | foreach (var (expectedRow, actualRow) in expected.Zip(actual, (expectedRow, actualRow) => (expectedRow, actualRow))) 71 | { 72 | Assert.Equal(expectedRow.Length, actualRow.Length); 73 | foreach (var (expectedValue, actualValue) in expectedRow.Zip(actualRow, (expectedValue, actualValue) => (expectedValue, actualValue))) 74 | { 75 | Assert.True(Math.Abs(expectedValue - actualValue) < 1e-5); 76 | } 77 | } 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /UnitTests/UnitTestData.cs: -------------------------------------------------------------------------------- 1 | namespace UMAP.UnitTests 2 | { 3 | public static class UnitTestData 4 | { 5 | public static float[][] TestData { get; } = new[] 6 | { 7 | new float[] { 0, 0, 5, 13, 9, 1, 0, 0, 0, 0, 13, 15, 10, 15, 5, 0, 0, 3, 15, 2, 0, 11, 8, 0, 0, 4, 12, 0, 0, 8, 8, 0, 0, 5, 8, 0, 0, 9, 8, 0, 0, 4, 11, 0, 1, 12, 7, 0, 0, 2, 14, 5, 10, 12, 0, 0, 0, 0, 6, 13, 10, 0, 0, 0 }, 8 | new float[] { 0, 0, 0, 12, 13, 5, 0, 0, 0, 0, 0, 11, 16, 9, 0, 0, 0, 0, 3, 15, 16, 6, 0, 0, 0, 7, 15, 16, 16, 2, 0, 0, 0, 0, 1, 16, 16, 3, 0, 0, 0, 0, 1, 16, 16, 6, 0, 0, 0, 0, 1, 16, 16, 6, 0, 0, 0, 0, 0, 11, 16, 10, 0, 0 }, 9 | new float[] { 0, 0, 0, 4, 15, 12, 0, 0, 0, 0, 3, 16, 15, 14, 0, 0, 0, 0, 8, 13, 8, 16, 0, 0, 0, 0, 1, 6, 15, 11, 0, 0, 0, 1, 8, 13, 15, 1, 0, 0, 0, 9, 16, 16, 5, 0, 0, 0, 0, 3, 13, 16, 16, 11, 5, 0, 0, 0, 0, 3, 11, 16, 9, 0 }, 10 | new float[] { 0, 0, 7, 15, 13, 1, 0, 0, 0, 8, 13, 6, 15, 4, 0, 0, 0, 2, 1, 13, 13, 0, 0, 0, 0, 0, 2, 15, 11, 1, 0, 0, 0, 0, 0, 1, 12, 12, 1, 0, 0, 0, 0, 0, 1, 10, 8, 0, 0, 0, 8, 4, 5, 14, 9, 0, 0, 0, 7, 13, 13, 9, 0, 0 }, 11 | new float[] { 0, 0, 0, 1, 11, 0, 0, 0, 0, 0, 0, 7, 8, 0, 0, 0, 0, 0, 1, 13, 6, 2, 2, 0, 0, 0, 7, 15, 0, 9, 8, 0, 0, 5, 16, 10, 0, 16, 6, 0, 0, 4, 15, 16, 13, 16, 1, 0, 0, 0, 0, 3, 15, 10, 0, 0, 0, 0, 0, 2, 16, 4, 0, 0 }, 12 | new float[] { 0, 0, 12, 10, 0, 0, 0, 0, 0, 0, 14, 16, 16, 14, 0, 0, 0, 0, 13, 16, 15, 10, 1, 0, 0, 0, 11, 16, 16, 7, 0, 0, 0, 0, 0, 4, 7, 16, 7, 0, 0, 0, 0, 0, 4, 16, 9, 0, 0, 0, 5, 4, 12, 16, 4, 0, 0, 0, 9, 16, 16, 10, 0, 0 }, 13 | new float[] { 0, 0, 0, 12, 13, 0, 0, 0, 0, 0, 5, 16, 8, 0, 0, 0, 0, 0, 13, 16, 3, 0, 0, 0, 0, 0, 14, 13, 0, 0, 0, 0, 0, 0, 15, 12, 7, 2, 0, 0, 0, 0, 13, 16, 13, 16, 3, 0, 0, 0, 7, 16, 11, 15, 8, 0, 0, 0, 1, 9, 15, 11, 3, 0 }, 14 | new float[] { 0, 0, 7, 8, 13, 16, 15, 1, 0, 0, 7, 7, 4, 11, 12, 0, 0, 0, 0, 0, 8, 13, 1, 0, 0, 4, 8, 8, 15, 15, 6, 0, 0, 2, 11, 15, 15, 4, 0, 0, 0, 0, 0, 16, 5, 0, 0, 0, 0, 0, 9, 15, 1, 0, 0, 0, 0, 0, 13, 5, 0, 0, 0, 0 }, 15 | new float[] { 0, 0, 9, 14, 8, 1, 0, 0, 0, 0, 12, 14, 14, 12, 0, 0, 0, 0, 9, 10, 0, 15, 4, 0, 0, 0, 3, 16, 12, 14, 2, 0, 0, 0, 4, 16, 16, 2, 0, 0, 0, 3, 16, 8, 10, 13, 2, 0, 0, 1, 15, 1, 3, 16, 8, 0, 0, 0, 11, 16, 15, 11, 1, 0 }, 16 | new float[] { 0, 0, 11, 12, 0, 0, 0, 0, 0, 2, 16, 16, 16, 13, 0, 0, 0, 3, 16, 12, 10, 14, 0, 0, 0, 1, 16, 1, 12, 15, 0, 0, 0, 0, 13, 16, 9, 15, 2, 0, 0, 0, 0, 3, 0, 9, 11, 0, 0, 0, 0, 0, 9, 15, 4, 0, 0, 0, 9, 12, 13, 3, 0, 0 }, 17 | new float[] { 0, 0, 1, 9, 15, 11, 0, 0, 0, 0, 11, 16, 8, 14, 6, 0, 0, 2, 16, 10, 0, 9, 9, 0, 0, 1, 16, 4, 0, 8, 8, 0, 0, 4, 16, 4, 0, 8, 8, 0, 0, 1, 16, 5, 1, 11, 3, 0, 0, 0, 12, 12, 10, 10, 0, 0, 0, 0, 1, 10, 13, 3, 0, 0 }, 18 | new float[] { 0, 0, 0, 0, 14, 13, 1, 0, 0, 0, 0, 5, 16, 16, 2, 0, 0, 0, 0, 14, 16, 12, 0, 0, 0, 1, 10, 16, 16, 12, 0, 0, 0, 3, 12, 14, 16, 9, 0, 0, 0, 0, 0, 5, 16, 15, 0, 0, 0, 0, 0, 4, 16, 14, 0, 0, 0, 0, 0, 1, 13, 16, 1, 0 }, 19 | new float[] { 0, 0, 5, 12, 1, 0, 0, 0, 0, 0, 15, 14, 7, 0, 0, 0, 0, 0, 13, 1, 12, 0, 0, 0, 0, 2, 10, 0, 14, 0, 0, 0, 0, 0, 2, 0, 16, 1, 0, 0, 0, 0, 0, 6, 15, 0, 0, 0, 0, 0, 9, 16, 15, 9, 8, 2, 0, 0, 3, 11, 8, 13, 12, 4 }, 20 | new float[] { 0, 2, 9, 15, 14, 9, 3, 0, 0, 4, 13, 8, 9, 16, 8, 0, 0, 0, 0, 6, 14, 15, 3, 0, 0, 0, 0, 11, 14, 2, 0, 0, 0, 0, 0, 2, 15, 11, 0, 0, 0, 0, 0, 0, 2, 15, 4, 0, 0, 1, 5, 6, 13, 16, 6, 0, 0, 2, 12, 12, 13, 11, 0, 0 }, 21 | new float[] { 0, 0, 0, 8, 15, 1, 0, 0, 0, 0, 1, 14, 13, 1, 1, 0, 0, 0, 10, 15, 3, 15, 11, 0, 0, 7, 16, 7, 1, 16, 8, 0, 0, 9, 16, 13, 14, 16, 5, 0, 0, 1, 10, 15, 16, 14, 0, 0, 0, 0, 0, 1, 16, 10, 0, 0, 0, 0, 0, 10, 15, 4, 0, 0 }, 22 | new float[] { 0, 5, 12, 13, 16, 16, 2, 0, 0, 11, 16, 15, 8, 4, 0, 0, 0, 8, 14, 11, 1, 0, 0, 0, 0, 8, 16, 16, 14, 0, 0, 0, 0, 1, 6, 6, 16, 0, 0, 0, 0, 0, 0, 5, 16, 3, 0, 0, 0, 1, 5, 15, 13, 0, 0, 0, 0, 4, 15, 16, 2, 0, 0, 0 }, 23 | new float[] { 0, 0, 0, 8, 15, 1, 0, 0, 0, 0, 0, 12, 14, 0, 0, 0, 0, 0, 3, 16, 7, 0, 0, 0, 0, 0, 6, 16, 2, 0, 0, 0, 0, 0, 7, 16, 16, 13, 5, 0, 0, 0, 15, 16, 9, 9, 14, 0, 0, 0, 3, 14, 9, 2, 16, 2, 0, 0, 0, 7, 15, 16, 11, 0 }, 24 | new float[] { 0, 0, 1, 8, 15, 10, 0, 0, 0, 3, 13, 15, 14, 14, 0, 0, 0, 5, 10, 0, 10, 12, 0, 0, 0, 0, 3, 5, 15, 10, 2, 0, 0, 0, 16, 16, 16, 16, 12, 0, 0, 1, 8, 12, 14, 8, 3, 0, 0, 0, 0, 10, 13, 0, 0, 0, 0, 0, 0, 11, 9, 0, 0, 0 }, 25 | new float[] { 0, 0, 10, 7, 13, 9, 0, 0, 0, 0, 9, 10, 12, 15, 2, 0, 0, 0, 4, 11, 10, 11, 0, 0, 0, 0, 1, 16, 10, 1, 0, 0, 0, 0, 12, 13, 4, 0, 0, 0, 0, 0, 12, 1, 12, 0, 0, 0, 0, 1, 10, 2, 14, 0, 0, 0, 0, 0, 11, 14, 5, 0, 0, 0 }, 26 | new float[] { 0, 0, 6, 14, 4, 0, 0, 0, 0, 0, 11, 16, 10, 0, 0, 0, 0, 0, 8, 14, 16, 2, 0, 0, 0, 0, 1, 12, 12, 11, 0, 0, 0, 0, 0, 0, 0, 11, 3, 0, 0, 0, 0, 0, 0, 5, 11, 0, 0, 0, 1, 4, 4, 7, 16, 2, 0, 0, 7, 16, 16, 13, 11, 1 }, 27 | new float[] { 0, 0, 3, 13, 11, 7, 0, 0, 0, 0, 11, 16, 16, 16, 2, 0, 0, 4, 16, 9, 1, 14, 2, 0, 0, 4, 16, 0, 0, 16, 2, 0, 0, 0, 16, 1, 0, 12, 8, 0, 0, 0, 15, 9, 0, 13, 6, 0, 0, 0, 9, 14, 9, 14, 1, 0, 0, 0, 2, 12, 13, 4, 0, 0 }, 28 | new float[] { 0, 0, 0, 2, 16, 16, 2, 0, 0, 0, 0, 4, 16, 16, 2, 0, 0, 1, 4, 12, 16, 12, 0, 0, 0, 7, 16, 16, 16, 12, 0, 0, 0, 0, 3, 10, 16, 14, 0, 0, 0, 0, 0, 8, 16, 12, 0, 0, 0, 0, 0, 6, 16, 16, 2, 0, 0, 0, 0, 2, 12, 15, 4, 0 }, 29 | new float[] { 0, 0, 8, 16, 5, 0, 0, 0, 0, 1, 13, 11, 16, 0, 0, 0, 0, 0, 10, 0, 13, 3, 0, 0, 0, 0, 3, 1, 16, 1, 0, 0, 0, 0, 0, 9, 12, 0, 0, 0, 0, 0, 3, 15, 5, 0, 0, 0, 0, 0, 14, 15, 8, 8, 3, 0, 0, 0, 7, 12, 12, 12, 13, 1 }, 30 | new float[] { 0, 1, 8, 12, 15, 14, 4, 0, 0, 3, 11, 8, 8, 12, 12, 0, 0, 0, 0, 0, 2, 13, 7, 0, 0, 0, 0, 2, 15, 12, 1, 0, 0, 0, 0, 0, 13, 5, 0, 0, 0, 0, 0, 0, 9, 13, 0, 0, 0, 0, 7, 8, 14, 15, 0, 0, 0, 0, 14, 15, 11, 2, 0, 0 }, 31 | new float[] { 0, 0, 0, 0, 12, 2, 0, 0, 0, 0, 0, 6, 14, 1, 0, 0, 0, 0, 4, 16, 7, 8, 0, 0, 0, 0, 13, 9, 0, 16, 6, 0, 0, 6, 16, 10, 11, 16, 0, 0, 0, 0, 5, 10, 13, 16, 0, 0, 0, 0, 0, 0, 6, 16, 0, 0, 0, 0, 0, 0, 12, 8, 0, 0 }, 32 | new float[] { 0, 0, 12, 8, 8, 7, 0, 0, 0, 3, 16, 16, 11, 7, 0, 0, 0, 2, 14, 1, 0, 0, 0, 0, 0, 5, 14, 5, 0, 0, 0, 0, 0, 2, 15, 16, 9, 0, 0, 0, 0, 0, 0, 2, 16, 2, 0, 0, 0, 0, 4, 8, 16, 4, 0, 0, 0, 0, 11, 14, 9, 0, 0, 0 }, 33 | new float[] { 0, 0, 1, 13, 14, 3, 0, 0, 0, 0, 8, 16, 13, 2, 0, 0, 0, 2, 16, 16, 3, 0, 0, 0, 0, 3, 16, 12, 1, 0, 0, 0, 0, 5, 16, 14, 5, 0, 0, 0, 0, 3, 16, 16, 16, 16, 6, 0, 0, 1, 14, 16, 16, 16, 12, 0, 0, 0, 3, 12, 15, 14, 7, 0 }, 34 | new float[] { 0, 0, 0, 8, 14, 14, 2, 0, 0, 0, 0, 6, 10, 15, 11, 0, 0, 0, 0, 0, 0, 14, 10, 0, 0, 2, 8, 11, 12, 16, 8, 0, 0, 8, 16, 16, 16, 16, 7, 0, 0, 0, 0, 0, 11, 15, 1, 0, 0, 0, 0, 9, 16, 7, 0, 0, 0, 0, 0, 12, 13, 1, 0, 0 }, 35 | new float[] { 0, 0, 10, 11, 4, 0, 0, 0, 0, 0, 10, 15, 13, 13, 1, 0, 0, 0, 8, 11, 0, 14, 4, 0, 0, 0, 0, 13, 15, 13, 0, 0, 0, 1, 11, 16, 16, 0, 0, 0, 0, 1, 15, 3, 9, 10, 0, 0, 0, 0, 14, 6, 15, 10, 0, 0, 0, 0, 8, 14, 7, 1, 0, 0 }, 36 | new float[] { 0, 0, 9, 13, 7, 0, 0, 0, 0, 0, 12, 16, 16, 2, 0, 0, 0, 0, 12, 13, 16, 6, 0, 0, 0, 0, 6, 16, 16, 14, 0, 0, 0, 0, 0, 0, 2, 16, 3, 0, 0, 0, 0, 0, 0, 9, 10, 0, 0, 0, 3, 7, 12, 14, 16, 2, 0, 0, 7, 12, 12, 12, 11, 0 }, 37 | new float[] { 0, 0, 10, 14, 11, 3, 0, 0, 0, 4, 16, 13, 6, 14, 1, 0, 0, 4, 16, 2, 0, 11, 7, 0, 0, 8, 16, 0, 0, 10, 5, 0, 0, 8, 16, 0, 0, 14, 4, 0, 0, 8, 16, 0, 1, 16, 1, 0, 0, 4, 16, 1, 11, 15, 0, 0, 0, 0, 11, 16, 12, 3, 0, 0 }, 38 | new float[] { 0, 0, 2, 13, 8, 0, 0, 0, 0, 0, 6, 16, 16, 6, 0, 0, 0, 0, 5, 15, 13, 11, 0, 0, 0, 0, 0, 7, 16, 15, 0, 0, 0, 0, 0, 0, 0, 14, 3, 0, 0, 0, 0, 0, 0, 7, 11, 0, 0, 0, 0, 3, 4, 4, 16, 2, 0, 0, 2, 15, 13, 14, 13, 2 }, 39 | new float[] { 0, 2, 13, 16, 16, 16, 11, 0, 0, 5, 16, 10, 5, 4, 1, 0, 0, 6, 16, 7, 3, 0, 0, 0, 0, 9, 16, 16, 16, 6, 0, 0, 0, 3, 8, 4, 11, 15, 0, 0, 0, 0, 0, 1, 12, 15, 0, 0, 0, 0, 4, 13, 16, 6, 0, 0, 0, 2, 16, 15, 8, 0, 0, 0 }, 40 | new float[] { 0, 6, 13, 5, 8, 8, 1, 0, 0, 8, 16, 16, 16, 16, 6, 0, 0, 6, 16, 9, 6, 4, 0, 0, 0, 6, 16, 16, 15, 5, 0, 0, 0, 0, 4, 5, 15, 12, 0, 0, 0, 0, 0, 3, 16, 9, 0, 0, 0, 1, 8, 13, 15, 3, 0, 0, 0, 4, 16, 15, 3, 0, 0, 0 }, 41 | new float[] { 0, 0, 0, 5, 14, 2, 0, 0, 0, 0, 1, 13, 11, 0, 0, 0, 0, 0, 5, 16, 2, 0, 0, 0, 0, 0, 6, 15, 5, 0, 0, 0, 0, 1, 15, 16, 15, 11, 1, 0, 0, 2, 13, 14, 1, 12, 9, 0, 0, 0, 4, 16, 7, 13, 9, 0, 0, 0, 0, 5, 16, 15, 3, 0 }, 42 | new float[] { 0, 3, 15, 8, 8, 6, 0, 0, 0, 4, 16, 16, 16, 13, 2, 0, 0, 3, 16, 9, 2, 0, 0, 0, 0, 2, 16, 16, 15, 3, 0, 0, 0, 0, 7, 6, 12, 9, 0, 0, 0, 0, 0, 1, 14, 10, 0, 0, 0, 0, 5, 14, 15, 2, 0, 0, 0, 1, 15, 14, 1, 0, 0, 0 }, 43 | new float[] { 0, 0, 6, 14, 10, 2, 0, 0, 0, 0, 15, 15, 13, 15, 3, 0, 0, 2, 16, 10, 0, 13, 9, 0, 0, 1, 16, 5, 0, 12, 5, 0, 0, 0, 16, 3, 0, 13, 6, 0, 0, 1, 15, 5, 6, 13, 1, 0, 0, 0, 16, 11, 14, 10, 0, 0, 0, 0, 7, 16, 11, 1, 0, 0 }, 44 | new float[] { 0, 0, 13, 10, 1, 0, 0, 0, 0, 5, 16, 14, 7, 0, 0, 0, 0, 4, 16, 8, 14, 0, 0, 0, 0, 2, 14, 16, 16, 6, 0, 0, 0, 0, 1, 4, 9, 13, 1, 0, 0, 0, 0, 0, 0, 13, 6, 0, 0, 0, 5, 8, 5, 9, 14, 0, 0, 0, 13, 13, 15, 16, 13, 0 }, 45 | new float[] { 0, 0, 7, 7, 13, 16, 4, 0, 0, 0, 13, 13, 6, 12, 7, 0, 0, 0, 10, 4, 10, 11, 1, 0, 0, 0, 8, 16, 10, 0, 0, 0, 0, 3, 14, 16, 0, 0, 0, 0, 0, 8, 8, 11, 5, 0, 0, 0, 0, 4, 10, 9, 8, 0, 0, 0, 0, 1, 11, 16, 6, 0, 0, 0 }, 46 | new float[] { 0, 1, 9, 16, 13, 7, 0, 0, 0, 7, 14, 4, 10, 12, 0, 0, 0, 6, 15, 9, 16, 11, 0, 0, 0, 0, 9, 11, 7, 14, 0, 0, 0, 0, 0, 0, 0, 15, 2, 0, 0, 0, 0, 0, 0, 11, 6, 0, 0, 3, 13, 8, 5, 14, 5, 0, 0, 0, 9, 14, 13, 10, 1, 0 }, 47 | new float[] { 0, 0, 11, 10, 12, 4, 0, 0, 0, 0, 12, 13, 9, 16, 1, 0, 0, 0, 7, 13, 11, 16, 0, 0, 0, 0, 1, 16, 14, 4, 0, 0, 0, 0, 10, 16, 13, 0, 0, 0, 0, 0, 14, 7, 12, 7, 0, 0, 0, 4, 14, 4, 12, 13, 0, 0, 0, 1, 11, 14, 12, 4, 0, 0 }, 48 | new float[] { 0, 0, 0, 9, 15, 1, 0, 0, 0, 0, 4, 16, 12, 0, 0, 0, 0, 0, 15, 14, 2, 11, 3, 0, 0, 4, 16, 9, 4, 16, 10, 0, 0, 9, 16, 11, 13, 16, 2, 0, 0, 0, 9, 16, 16, 14, 0, 0, 0, 0, 0, 8, 16, 6, 0, 0, 0, 0, 0, 9, 16, 2, 0, 0 }, 49 | new float[] { 0, 0, 0, 0, 12, 5, 0, 0, 0, 0, 0, 2, 16, 12, 0, 0, 0, 0, 1, 12, 16, 11, 0, 0, 0, 2, 12, 16, 16, 10, 0, 0, 0, 6, 11, 5, 15, 6, 0, 0, 0, 0, 0, 1, 16, 9, 0, 0, 0, 0, 0, 2, 16, 11, 0, 0, 0, 0, 0, 3, 16, 8, 0, 0 }, 50 | new float[] { 0, 0, 0, 9, 15, 12, 0, 0, 0, 0, 4, 7, 7, 14, 0, 0, 0, 0, 0, 0, 0, 13, 3, 0, 0, 4, 9, 8, 10, 13, 1, 0, 0, 4, 16, 15, 16, 16, 6, 0, 0, 0, 0, 0, 14, 3, 0, 0, 0, 0, 0, 9, 12, 0, 0, 0, 0, 0, 0, 11, 7, 0, 0, 0 }, 51 | new float[] { 0, 0, 9, 16, 16, 16, 5, 0, 0, 1, 14, 10, 8, 16, 8, 0, 0, 0, 0, 0, 7, 16, 3, 0, 0, 3, 8, 11, 15, 16, 11, 0, 0, 8, 16, 16, 15, 11, 3, 0, 0, 0, 2, 16, 7, 0, 0, 0, 0, 0, 8, 16, 1, 0, 0, 0, 0, 0, 13, 10, 0, 0, 0, 0 }, 52 | new float[] { 0, 0, 9, 16, 13, 6, 0, 0, 0, 0, 6, 5, 16, 16, 0, 0, 0, 0, 0, 8, 15, 5, 0, 0, 0, 0, 0, 5, 14, 3, 0, 0, 0, 0, 0, 0, 9, 15, 2, 0, 0, 0, 0, 0, 0, 11, 12, 0, 0, 0, 4, 8, 11, 15, 12, 0, 0, 0, 11, 14, 12, 8, 0, 0 }, 53 | new float[] { 0, 1, 15, 4, 0, 0, 0, 0, 0, 2, 16, 16, 16, 14, 2, 0, 0, 6, 16, 11, 8, 8, 3, 0, 0, 5, 16, 11, 5, 0, 0, 0, 0, 0, 11, 14, 14, 1, 0, 0, 0, 0, 0, 5, 16, 7, 0, 0, 0, 0, 6, 16, 16, 4, 0, 0, 0, 0, 14, 14, 4, 0, 0, 0 }, 54 | new float[] { 0, 0, 0, 1, 11, 9, 0, 0, 0, 0, 0, 7, 16, 13, 0, 0, 0, 0, 4, 14, 16, 9, 0, 0, 0, 10, 16, 11, 16, 8, 0, 0, 0, 0, 0, 3, 16, 6, 0, 0, 0, 0, 0, 3, 16, 8, 0, 0, 0, 0, 0, 5, 16, 10, 0, 0, 0, 0, 0, 2, 14, 6, 0, 0 }, 55 | new float[] { 0, 0, 2, 15, 13, 3, 0, 0, 0, 0, 10, 15, 11, 15, 0, 0, 0, 3, 16, 6, 0, 10, 0, 0, 0, 4, 16, 8, 0, 3, 8, 0, 0, 8, 14, 3, 0, 4, 8, 0, 0, 3, 15, 1, 0, 3, 7, 0, 0, 0, 14, 11, 6, 14, 5, 0, 0, 0, 4, 12, 15, 6, 0, 0 }, 56 | new float[] { 0, 0, 1, 15, 13, 1, 0, 0, 0, 0, 7, 16, 14, 8, 0, 0, 0, 8, 12, 9, 2, 13, 2, 0, 0, 7, 9, 1, 0, 6, 6, 0, 0, 5, 9, 0, 0, 3, 9, 0, 0, 0, 15, 2, 0, 8, 12, 0, 0, 0, 9, 15, 13, 16, 6, 0, 0, 0, 0, 13, 14, 8, 0, 0 }, 57 | new float[] { 0, 0, 0, 5, 14, 12, 2, 0, 0, 0, 7, 15, 8, 14, 4, 0, 0, 0, 6, 2, 3, 13, 1, 0, 0, 0, 0, 1, 13, 4, 0, 0, 0, 0, 1, 11, 9, 0, 0, 0, 0, 8, 16, 13, 0, 0, 0, 0, 0, 5, 14, 16, 11, 2, 0, 0, 0, 0, 0, 6, 12, 13, 3, 0 }, 58 | new float[] { 0, 0, 0, 3, 15, 10, 1, 0, 0, 0, 0, 11, 10, 16, 4, 0, 0, 0, 0, 12, 1, 15, 6, 0, 0, 0, 0, 3, 4, 15, 4, 0, 0, 0, 0, 6, 15, 6, 0, 0, 0, 4, 15, 16, 9, 0, 0, 0, 0, 0, 13, 16, 15, 9, 3, 0, 0, 0, 0, 4, 9, 14, 7, 0 }, 59 | new float[] { 0, 0, 3, 12, 16, 16, 6, 0, 0, 0, 10, 11, 7, 16, 11, 0, 0, 0, 0, 0, 2, 14, 10, 0, 0, 5, 11, 8, 9, 16, 3, 0, 0, 9, 16, 16, 16, 16, 9, 0, 0, 1, 4, 9, 16, 6, 0, 0, 0, 0, 0, 11, 14, 0, 0, 0, 0, 0, 4, 16, 5, 0, 0, 0 }, 60 | new float[] { 0, 0, 4, 8, 16, 5, 0, 0, 0, 0, 9, 16, 8, 11, 0, 0, 0, 0, 5, 10, 0, 13, 2, 0, 0, 0, 0, 13, 4, 15, 2, 0, 0, 0, 0, 9, 16, 8, 0, 0, 0, 0, 8, 15, 14, 5, 0, 0, 0, 0, 16, 5, 14, 4, 0, 0, 0, 0, 6, 16, 12, 1, 0, 0 }, 61 | new float[] { 0, 0, 0, 1, 14, 14, 3, 0, 0, 0, 0, 10, 11, 13, 8, 0, 0, 0, 0, 7, 0, 13, 8, 0, 0, 0, 0, 0, 7, 15, 1, 0, 0, 4, 8, 12, 15, 4, 0, 0, 0, 6, 16, 16, 6, 0, 0, 0, 0, 0, 2, 12, 12, 4, 2, 0, 0, 0, 0, 1, 13, 16, 5, 0 }, 62 | new float[] { 0, 0, 2, 14, 15, 5, 0, 0, 0, 0, 10, 16, 16, 15, 1, 0, 0, 3, 16, 10, 10, 16, 4, 0, 0, 5, 16, 0, 0, 14, 6, 0, 0, 5, 16, 6, 0, 12, 7, 0, 0, 1, 15, 13, 4, 13, 6, 0, 0, 0, 11, 16, 16, 15, 0, 0, 0, 0, 2, 11, 13, 4, 0, 0 }, 63 | new float[] { 0, 0, 0, 0, 12, 13, 1, 0, 0, 0, 0, 8, 16, 15, 2, 0, 0, 0, 10, 16, 16, 12, 0, 0, 0, 4, 16, 16, 16, 13, 0, 0, 0, 4, 7, 4, 16, 6, 0, 0, 0, 0, 0, 1, 16, 8, 0, 0, 0, 0, 0, 1, 16, 8, 0, 0, 0, 0, 0, 0, 12, 12, 0, 0 }, 64 | new float[] { 0, 0, 0, 1, 9, 11, 0, 0, 0, 0, 0, 13, 16, 16, 0, 0, 0, 0, 0, 12, 7, 14, 0, 0, 0, 0, 0, 0, 14, 7, 0, 0, 0, 0, 5, 12, 12, 0, 0, 0, 0, 7, 16, 16, 6, 0, 0, 0, 0, 4, 9, 13, 16, 11, 4, 0, 0, 0, 0, 0, 9, 13, 3, 0 }, 65 | new float[] { 0, 0, 0, 10, 13, 1, 0, 0, 0, 1, 11, 12, 7, 0, 0, 0, 0, 2, 16, 12, 0, 0, 0, 0, 0, 4, 16, 11, 0, 0, 0, 0, 0, 4, 16, 15, 8, 4, 0, 0, 0, 4, 16, 16, 13, 16, 6, 0, 0, 0, 7, 16, 7, 13, 14, 0, 0, 0, 0, 7, 15, 15, 5, 0 }, 66 | new float[] { 0, 1, 10, 15, 11, 1, 0, 0, 0, 3, 8, 8, 11, 12, 0, 0, 0, 0, 0, 5, 14, 15, 1, 0, 0, 0, 0, 11, 15, 2, 0, 0, 0, 0, 0, 4, 15, 2, 0, 0, 0, 0, 0, 0, 12, 10, 0, 0, 0, 0, 3, 4, 10, 16, 1, 0, 0, 0, 13, 16, 15, 10, 0, 0 }, 67 | new float[] { 0, 0, 10, 15, 14, 4, 0, 0, 0, 0, 4, 6, 13, 16, 2, 0, 0, 0, 0, 3, 16, 9, 0, 0, 0, 0, 0, 1, 16, 6, 0, 0, 0, 0, 0, 0, 10, 12, 0, 0, 0, 0, 0, 0, 1, 16, 4, 0, 0, 1, 9, 5, 6, 16, 7, 0, 0, 0, 14, 12, 15, 11, 2, 0 }, 68 | new float[] { 0, 0, 6, 13, 16, 6, 0, 0, 0, 3, 16, 14, 15, 16, 1, 0, 0, 0, 5, 0, 8, 16, 2, 0, 0, 0, 0, 0, 8, 16, 3, 0, 0, 3, 15, 16, 16, 16, 9, 0, 0, 5, 13, 14, 16, 11, 3, 0, 0, 0, 0, 12, 15, 1, 0, 0, 0, 0, 4, 16, 7, 0, 0, 0 }, 69 | new float[] { 0, 0, 14, 16, 14, 6, 0, 0, 0, 0, 7, 10, 16, 16, 3, 0, 0, 0, 0, 5, 16, 16, 1, 0, 0, 0, 0, 2, 16, 8, 0, 0, 0, 0, 0, 0, 12, 13, 1, 0, 0, 0, 0, 0, 4, 16, 7, 0, 0, 0, 5, 9, 14, 16, 7, 0, 0, 0, 13, 16, 16, 10, 1, 0 }, 70 | new float[] { 0, 3, 16, 16, 14, 7, 1, 0, 0, 1, 9, 9, 15, 16, 4, 0, 0, 0, 0, 7, 16, 12, 1, 0, 0, 0, 0, 9, 16, 2, 0, 0, 0, 0, 0, 3, 15, 7, 0, 0, 0, 0, 0, 0, 9, 15, 0, 0, 0, 1, 10, 10, 16, 16, 3, 0, 0, 2, 13, 16, 12, 5, 0, 0 }, 71 | new float[] { 0, 0, 0, 6, 16, 4, 0, 0, 0, 0, 1, 13, 15, 1, 0, 0, 0, 1, 11, 16, 5, 0, 0, 0, 0, 8, 16, 10, 0, 10, 6, 0, 0, 12, 16, 8, 9, 16, 12, 0, 0, 2, 15, 16, 16, 16, 7, 0, 0, 0, 0, 4, 16, 11, 0, 0, 0, 0, 0, 7, 16, 3, 0, 0 }, 72 | new float[] { 0, 0, 0, 9, 10, 0, 0, 0, 0, 0, 7, 16, 7, 0, 0, 0, 0, 0, 13, 13, 1, 0, 0, 0, 0, 0, 15, 7, 0, 0, 0, 0, 0, 4, 16, 15, 12, 7, 0, 0, 0, 2, 16, 12, 4, 11, 10, 0, 0, 0, 8, 14, 5, 9, 14, 0, 0, 0, 0, 6, 12, 14, 9, 0 }, 73 | new float[] { 0, 0, 0, 10, 11, 0, 0, 0, 0, 0, 9, 16, 6, 0, 0, 0, 0, 0, 15, 13, 0, 0, 0, 0, 0, 0, 14, 10, 0, 0, 0, 0, 0, 1, 15, 12, 8, 2, 0, 0, 0, 0, 12, 16, 16, 16, 10, 1, 0, 0, 7, 16, 12, 12, 16, 4, 0, 0, 0, 9, 15, 12, 5, 0 }, 74 | new float[] { 0, 0, 5, 14, 0, 0, 0, 0, 0, 0, 12, 9, 0, 0, 0, 0, 0, 0, 15, 3, 0, 0, 0, 0, 0, 1, 16, 0, 0, 0, 0, 0, 0, 1, 16, 2, 7, 4, 0, 0, 0, 3, 16, 16, 16, 16, 9, 0, 0, 0, 15, 15, 4, 10, 16, 0, 0, 0, 4, 14, 16, 12, 7, 0 }, 75 | new float[] { 0, 0, 0, 9, 9, 0, 0, 0, 0, 0, 3, 16, 9, 0, 0, 0, 0, 3, 14, 10, 0, 2, 0, 0, 0, 10, 16, 5, 7, 15, 1, 0, 0, 2, 11, 15, 16, 13, 1, 0, 0, 0, 0, 7, 16, 3, 0, 0, 0, 0, 0, 6, 15, 0, 0, 0, 0, 0, 0, 4, 16, 5, 0, 0 }, 76 | new float[] { 0, 0, 6, 12, 13, 6, 0, 0, 0, 6, 16, 9, 12, 16, 2, 0, 0, 7, 16, 9, 15, 13, 0, 0, 0, 0, 11, 15, 16, 4, 0, 0, 0, 0, 0, 12, 10, 0, 0, 0, 0, 0, 3, 16, 4, 0, 0, 0, 0, 0, 1, 16, 2, 0, 0, 0, 0, 0, 6, 11, 0, 0, 0, 0 }, 77 | new float[] { 0, 0, 0, 0, 14, 7, 0, 0, 0, 0, 0, 13, 16, 9, 0, 0, 0, 0, 10, 16, 16, 7, 0, 0, 0, 7, 16, 8, 16, 2, 0, 0, 0, 1, 5, 6, 16, 6, 0, 0, 0, 0, 0, 4, 16, 6, 0, 0, 0, 0, 0, 2, 16, 6, 0, 0, 0, 0, 0, 0, 12, 11, 0, 0 }, 78 | new float[] { 0, 1, 13, 15, 12, 12, 5, 0, 0, 4, 16, 8, 8, 6, 0, 0, 0, 7, 13, 0, 0, 0, 0, 0, 0, 8, 15, 13, 15, 7, 0, 0, 0, 1, 6, 5, 8, 12, 0, 0, 0, 0, 0, 0, 12, 11, 0, 0, 0, 0, 2, 13, 14, 1, 0, 0, 0, 3, 14, 10, 1, 0, 0, 0 }, 79 | new float[] { 0, 0, 1, 13, 10, 0, 0, 0, 0, 7, 16, 16, 16, 7, 0, 0, 0, 8, 16, 13, 10, 15, 0, 0, 0, 8, 16, 2, 2, 15, 3, 0, 0, 5, 15, 2, 0, 12, 7, 0, 0, 1, 15, 6, 2, 16, 3, 0, 0, 0, 11, 15, 13, 16, 0, 0, 0, 0, 1, 15, 14, 8, 0, 0 }, 80 | new float[] { 0, 1, 12, 13, 4, 0, 0, 0, 0, 4, 16, 16, 16, 3, 0, 0, 0, 4, 16, 16, 16, 10, 0, 0, 0, 0, 6, 16, 14, 16, 0, 0, 0, 0, 0, 0, 0, 16, 4, 0, 0, 0, 0, 0, 0, 13, 7, 0, 0, 1, 2, 3, 7, 14, 10, 0, 0, 2, 12, 16, 14, 12, 3, 0 }, 81 | new float[] { 0, 0, 13, 13, 8, 2, 0, 0, 0, 5, 16, 16, 16, 12, 0, 0, 0, 1, 15, 12, 0, 0, 0, 0, 0, 0, 12, 13, 7, 1, 0, 0, 0, 0, 8, 16, 16, 12, 0, 0, 0, 0, 0, 4, 9, 16, 3, 0, 0, 0, 1, 5, 14, 15, 1, 0, 0, 0, 10, 16, 16, 6, 0, 0 }, 82 | new float[] { 0, 0, 0, 0, 9, 13, 0, 0, 0, 0, 0, 2, 16, 16, 1, 0, 0, 0, 0, 5, 9, 15, 0, 0, 0, 0, 0, 0, 5, 14, 0, 0, 0, 0, 0, 3, 15, 7, 0, 0, 0, 7, 16, 16, 11, 0, 0, 0, 0, 0, 11, 14, 16, 7, 3, 0, 0, 0, 0, 0, 9, 15, 9, 0 }, 83 | new float[] { 0, 3, 5, 14, 13, 6, 0, 0, 0, 9, 16, 12, 10, 12, 0, 0, 0, 6, 16, 3, 12, 11, 0, 0, 0, 1, 13, 10, 16, 6, 0, 0, 0, 0, 10, 16, 10, 0, 0, 0, 0, 1, 15, 16, 10, 0, 0, 0, 0, 0, 16, 12, 16, 0, 0, 0, 0, 0, 3, 15, 16, 5, 0, 0 }, 84 | new float[] { 0, 0, 0, 0, 11, 15, 4, 0, 0, 0, 0, 3, 16, 16, 12, 0, 0, 0, 0, 8, 14, 16, 12, 0, 0, 0, 0, 5, 10, 16, 6, 0, 0, 1, 7, 11, 16, 13, 0, 0, 0, 9, 16, 16, 14, 1, 0, 0, 0, 3, 8, 14, 16, 9, 0, 0, 0, 0, 0, 1, 11, 16, 12, 0 }, 85 | new float[] { 0, 0, 10, 12, 10, 0, 0, 0, 0, 3, 16, 16, 16, 4, 0, 0, 0, 7, 15, 3, 8, 13, 0, 0, 0, 8, 12, 0, 0, 14, 1, 0, 0, 8, 12, 0, 0, 7, 8, 0, 0, 5, 13, 0, 0, 4, 8, 0, 0, 0, 14, 8, 0, 10, 8, 0, 0, 0, 7, 12, 13, 12, 4, 0 }, 86 | new float[] { 0, 0, 4, 14, 11, 0, 0, 0, 0, 3, 15, 15, 16, 9, 0, 0, 0, 8, 13, 0, 3, 15, 1, 0, 0, 8, 12, 0, 0, 8, 6, 0, 0, 8, 12, 0, 0, 8, 8, 0, 0, 5, 13, 1, 0, 8, 8, 0, 0, 2, 15, 14, 12, 15, 6, 0, 0, 0, 5, 16, 15, 8, 0, 0 }, 87 | new float[] { 0, 0, 0, 1, 14, 13, 1, 0, 0, 0, 0, 1, 16, 16, 3, 0, 0, 5, 11, 15, 16, 16, 0, 0, 0, 4, 15, 16, 16, 15, 0, 0, 0, 0, 0, 8, 16, 7, 0, 0, 0, 0, 0, 10, 16, 3, 0, 0, 0, 0, 0, 8, 16, 6, 0, 0, 0, 0, 0, 2, 13, 15, 2, 0 }, 88 | new float[] { 0, 0, 3, 14, 16, 14, 0, 0, 0, 0, 13, 13, 13, 16, 2, 0, 0, 0, 1, 0, 9, 15, 0, 0, 0, 0, 9, 12, 15, 16, 10, 0, 0, 4, 16, 16, 16, 11, 3, 0, 0, 0, 4, 9, 14, 2, 0, 0, 0, 0, 2, 15, 9, 0, 0, 0, 0, 0, 4, 13, 1, 0, 0, 0 }, 89 | new float[] { 0, 0, 0, 10, 15, 3, 0, 0, 0, 0, 7, 16, 11, 0, 0, 0, 0, 0, 13, 15, 1, 0, 0, 0, 0, 0, 15, 11, 0, 0, 0, 0, 0, 0, 16, 13, 8, 1, 0, 0, 0, 0, 15, 16, 16, 15, 6, 0, 0, 0, 10, 16, 14, 16, 14, 2, 0, 0, 1, 9, 15, 16, 11, 0 }, 90 | new float[] { 0, 2, 13, 15, 10, 4, 0, 0, 0, 0, 5, 4, 13, 15, 2, 0, 0, 0, 0, 0, 11, 16, 4, 0, 0, 0, 0, 0, 16, 12, 0, 0, 0, 0, 0, 0, 13, 11, 0, 0, 0, 0, 0, 0, 8, 13, 0, 0, 0, 1, 6, 8, 14, 12, 0, 0, 0, 2, 12, 14, 11, 1, 0, 0 }, 91 | new float[] { 0, 1, 13, 15, 2, 0, 0, 0, 0, 6, 15, 15, 9, 0, 0, 0, 0, 9, 8, 10, 13, 0, 0, 0, 0, 5, 3, 12, 12, 0, 0, 0, 0, 0, 3, 16, 6, 0, 0, 0, 0, 5, 15, 15, 1, 0, 0, 0, 0, 6, 16, 15, 12, 12, 11, 0, 0, 1, 11, 13, 16, 16, 12, 0 }, 92 | new float[] { 0, 0, 0, 1, 16, 5, 0, 0, 0, 0, 0, 5, 16, 11, 0, 0, 0, 0, 0, 12, 16, 11, 0, 0, 0, 7, 12, 16, 16, 7, 0, 0, 0, 4, 8, 12, 16, 4, 0, 0, 0, 0, 0, 9, 16, 2, 0, 0, 0, 0, 0, 10, 16, 2, 0, 0, 0, 0, 0, 3, 13, 5, 0, 0 }, 93 | new float[] { 0, 0, 2, 7, 15, 13, 1, 0, 0, 0, 14, 12, 9, 14, 8, 0, 0, 0, 2, 0, 0, 12, 8, 0, 0, 0, 0, 0, 0, 13, 6, 0, 0, 5, 16, 16, 16, 16, 5, 0, 0, 2, 5, 7, 13, 14, 2, 0, 0, 0, 0, 1, 15, 5, 0, 0, 0, 0, 0, 11, 9, 0, 0, 0 }, 94 | new float[] { 0, 0, 0, 9, 16, 4, 0, 0, 0, 1, 9, 16, 13, 2, 0, 0, 0, 14, 16, 14, 8, 0, 0, 0, 1, 15, 15, 5, 16, 9, 0, 0, 0, 5, 16, 16, 16, 8, 0, 0, 0, 0, 2, 13, 16, 1, 0, 0, 0, 0, 0, 11, 13, 0, 0, 0, 0, 0, 0, 11, 13, 0, 0, 0 }, 95 | new float[] { 0, 0, 0, 10, 11, 0, 0, 0, 0, 0, 3, 16, 10, 0, 0, 0, 0, 0, 8, 16, 0, 0, 0, 0, 0, 0, 12, 14, 0, 0, 0, 0, 0, 0, 14, 16, 15, 6, 0, 0, 0, 0, 12, 16, 12, 15, 6, 0, 0, 0, 7, 16, 10, 13, 14, 0, 0, 0, 0, 9, 13, 11, 6, 0 }, 96 | new float[] { 0, 0, 13, 16, 15, 4, 0, 0, 0, 0, 9, 8, 13, 16, 3, 0, 0, 0, 0, 0, 13, 16, 7, 0, 0, 0, 0, 1, 16, 12, 0, 0, 0, 0, 0, 0, 15, 10, 0, 0, 0, 0, 0, 0, 8, 15, 0, 0, 0, 0, 3, 6, 15, 16, 7, 0, 0, 0, 15, 16, 16, 11, 1, 0 }, 97 | new float[] { 0, 0, 0, 1, 12, 8, 1, 0, 0, 0, 0, 4, 16, 16, 1, 0, 0, 0, 1, 13, 16, 11, 0, 0, 0, 1, 11, 16, 16, 12, 0, 0, 0, 2, 12, 8, 16, 10, 0, 0, 0, 0, 0, 0, 15, 8, 0, 0, 0, 0, 0, 4, 16, 4, 0, 0, 0, 0, 0, 3, 13, 4, 0, 0 }, 98 | new float[] { 0, 4, 14, 16, 16, 12, 1, 0, 0, 2, 12, 7, 14, 16, 6, 0, 0, 0, 0, 5, 16, 10, 0, 0, 0, 0, 0, 4, 16, 7, 0, 0, 0, 0, 0, 4, 16, 6, 0, 0, 0, 0, 0, 1, 15, 11, 0, 0, 0, 1, 8, 10, 16, 10, 0, 0, 0, 5, 16, 16, 15, 1, 0, 0 }, 99 | new float[] { 0, 0, 9, 13, 14, 5, 0, 0, 0, 4, 16, 10, 13, 16, 0, 0, 0, 0, 13, 15, 14, 16, 1, 0, 0, 0, 0, 3, 7, 16, 3, 0, 0, 0, 0, 0, 4, 16, 0, 0, 0, 0, 0, 0, 1, 16, 3, 0, 0, 1, 15, 5, 8, 16, 2, 0, 0, 0, 7, 15, 16, 9, 0, 0 }, 100 | new float[] { 0, 0, 0, 11, 16, 5, 0, 0, 0, 0, 0, 10, 16, 5, 0, 0, 0, 0, 4, 16, 16, 5, 0, 0, 0, 11, 16, 16, 16, 3, 0, 0, 0, 5, 8, 14, 16, 2, 0, 0, 0, 0, 0, 14, 16, 2, 0, 0, 0, 0, 0, 11, 16, 2, 0, 0, 0, 0, 0, 8, 16, 8, 0, 0 }, 101 | new float[] { 0, 0, 3, 12, 16, 10, 0, 0, 0, 2, 14, 12, 12, 12, 0, 0, 0, 5, 10, 0, 10, 11, 0, 0, 0, 0, 0, 1, 14, 9, 2, 0, 0, 0, 8, 16, 16, 16, 10, 0, 0, 0, 6, 16, 13, 7, 0, 0, 0, 0, 0, 16, 5, 0, 0, 0, 0, 0, 5, 13, 0, 0, 0, 0 }, 102 | new float[] { 0, 0, 0, 11, 16, 8, 0, 0, 0, 0, 6, 16, 13, 3, 0, 0, 0, 0, 8, 16, 8, 0, 0, 0, 0, 0, 13, 16, 2, 0, 0, 0, 0, 0, 15, 16, 5, 0, 0, 0, 0, 2, 16, 16, 16, 5, 0, 0, 0, 1, 10, 16, 16, 14, 0, 0, 0, 0, 0, 12, 16, 15, 0, 0 }, 103 | new float[] { 0, 1, 9, 16, 15, 10, 0, 0, 0, 6, 16, 8, 7, 16, 3, 0, 0, 0, 11, 14, 16, 11, 1, 0, 0, 1, 13, 16, 6, 0, 0, 0, 0, 8, 15, 16, 3, 0, 0, 0, 0, 5, 14, 10, 11, 0, 0, 0, 0, 0, 15, 7, 16, 3, 0, 0, 0, 0, 11, 16, 8, 0, 0, 0 }, 104 | new float[] { 0, 0, 0, 3, 14, 1, 0, 0, 0, 0, 0, 13, 12, 1, 0, 0, 0, 0, 7, 16, 5, 3, 0, 0, 0, 3, 15, 11, 5, 16, 2, 0, 0, 5, 16, 11, 11, 16, 6, 0, 0, 0, 6, 12, 16, 13, 3, 0, 0, 0, 0, 1, 15, 7, 0, 0, 0, 0, 0, 2, 16, 7, 0, 0 }, 105 | new float[] { 0, 2, 15, 16, 16, 13, 2, 0, 0, 1, 10, 8, 14, 16, 8, 0, 0, 0, 0, 0, 16, 15, 1, 0, 0, 0, 0, 0, 16, 8, 0, 0, 0, 0, 0, 0, 14, 14, 0, 0, 0, 0, 0, 0, 11, 16, 1, 0, 0, 2, 14, 13, 16, 16, 3, 0, 0, 2, 15, 16, 14, 5, 0, 0 }, 106 | new float[] { 0, 0, 1, 15, 13, 0, 0, 0, 0, 0, 1, 16, 16, 5, 0, 0, 0, 0, 7, 16, 16, 0, 0, 0, 0, 0, 13, 16, 13, 0, 0, 0, 0, 7, 16, 16, 13, 0, 0, 0, 0, 1, 11, 16, 13, 0, 0, 0, 0, 0, 2, 16, 16, 0, 0, 0, 0, 0, 1, 14, 16, 3, 0, 0 } 107 | }; 108 | 109 | public static float[][] TestResults2D { get; } = new[] 110 | { 111 | new[] { -8.320373f, -8.604719f }, 112 | new[] { -6.942148f, -0.03944469f }, 113 | new[] { -1.092286f, 1.906214f }, 114 | new[] { 2.383298f, -7.798354f }, 115 | new[] { -8.162592f, 0.1028088f }, 116 | new[] { 1.558339f, -7.567441f }, 117 | new[] { -10.3906f, -6.220805f }, 118 | new[] { -4.229552f, -0.8153143f }, 119 | new[] { 0.3562826f, -4.661168f }, 120 | new[] { 2.555051f, -5.847264f }, 121 | new[] { -10.33069f, -6.624838f }, 122 | new[] { -3.6533f, 2.108036f }, 123 | new[] { -3.096949f, -4.276262f }, 124 | new[] { 2.476444f, -6.036654f }, 125 | new[] { -9.375031f, -1.486822f }, 126 | new[] { 2.013881f, 0.9525841f }, 127 | new[] { -8.461199f, -3.771342f }, 128 | new[] { -3.878093f, -0.8050613f }, 129 | new[] { 0.0299643f, -2.961395f }, 130 | new[] { 3.635397f, -7.161431f }, 131 | new[] { -9.967746f, -8.844633f }, 132 | new[] { -4.345988f, 3.402549f }, 133 | new[] { -3.072233f, -4.107821f }, 134 | new[] { -0.7761965f, -7.623389f }, 135 | new[] { -8.42043f, -0.6327686f }, 136 | new[] { 1.048605f, -1.818133f }, 137 | new[] { -7.379044f, -1.838265f }, 138 | new[] { -0.4553163f, 3.050899f }, 139 | new[] { -1.101518f, -3.858194f }, 140 | new[] { 3.605124f, -6.74646f }, 141 | new[] { -9.542397f, -7.786929f }, 142 | new[] { 3.133594f, -6.191055f }, 143 | new[] { 0.3724804f, -2.874243f }, 144 | new[] { -0.6005926f, -2.119896f }, 145 | new[] { -8.691688f, -2.395538f }, 146 | new[] { 1.425107f, -2.553241f }, 147 | new[] { -10.09815f, -7.712442f }, 148 | new[] { 3.694135f, -9.443636f }, 149 | new[] { 0.3023378f, -1.885989f }, 150 | new[] { 1.821165f, -7.569468f }, 151 | new[] { -0.4740453f, -2.820369f }, 152 | new[] { -2.917798f, -4.574456f }, 153 | new[] { -3.023664f, 2.418785f }, 154 | new[] { -4.523185f, 1.134553f }, 155 | new[] { -3.280892f, -2.116279f }, 156 | new[] { 1.835311f, -6.21517f }, 157 | new[] { 0.92663f, -2.790078f }, 158 | new[] { -5.06945f, 1.891056f }, 159 | new[] { -8.99618f, -8.296493f }, 160 | new[] { -9.162386f, -7.874556f }, 161 | new[] { -0.9411381f, 0.1705438f }, 162 | new[] { -0.6540645f, 1.114049f }, 163 | new[] { -2.289296f, -0.3754147f }, 164 | new[] { -5.034694f, -2.917206f }, 165 | new[] { -0.4209626f, 0.4161775f }, 166 | new[] { -10.10551f, -8.50801f }, 167 | new[] { -4.251418f, 1.541349f }, 168 | new[] { 0.3834115f, 1.729182f }, 169 | new[] { -5.800233f, -4.948131f }, 170 | new[] { 1.783736f, -8.373639f }, 171 | new[] { 2.528492f, -7.550576f }, 172 | new[] { -4.048687f, -1.248089f }, 173 | new[] { 5.781887f, -5.729897f }, 174 | new[] { 1.403556f, -7.284789f }, 175 | new[] { -7.933628f, -2.150665f }, 176 | new[] { -13.62819f, -7.003048f }, 177 | new[] { -8.11895f, -1.999082f }, 178 | new[] { -7.597252f, -1.9403f }, 179 | new[] { -6.086762f, -0.4432608f }, 180 | new[] { -1.71025f, -2.867901f }, 181 | new[] { -3.227082f, 2.749957f }, 182 | new[] { -1.91261f, 0.4374519f }, 183 | new[] { -8.702655f, -8.698884f }, 184 | new[] { 2.903067f, -8.556941f }, 185 | new[] { -0.6866481f, -2.321998f }, 186 | new[] { -0.2834932f, 1.332285f }, 187 | new[] { 1.016448f, -3.636409f }, 188 | new[] { -1.609478f, 1.211246f }, 189 | new[] { -8.742628f, -9.264371f }, 190 | new[] { -6.971736f, -9.665169f }, 191 | new[] { -3.031928f, 5.492709f }, 192 | new[] { -2.214855f, -0.08604397f }, 193 | new[] { -7.526623f, -3.313847f }, 194 | new[] { 1.901841f, -7.604033f }, 195 | new[] { -2.37418f, -4.171429f }, 196 | new[] { -3.826939f, 3.371283f }, 197 | new[] { -4.516561f, -1.038555f }, 198 | new[] { -5.203784f, 1.150155f }, 199 | new[] { -8.362083f, -2.397606f }, 200 | new[] { 2.093331f, -8.55346f }, 201 | new[] { -2.761242f, 2.643443f }, 202 | new[] { 2.646401f, -6.870502f }, 203 | new[] { 1.693492f, -6.775913f }, 204 | new[] { -3.050454f, 2.4635f }, 205 | new[] { -4.373929f, -1.468093f }, 206 | new[] { -8.735733f, -2.878323f }, 207 | new[] { -0.6633761f, -3.03056f }, 208 | new[] { -6.715734f, -1.238603f }, 209 | new[] { 1.43748f, -7.30011f }, 210 | new[] { -8.397482f, -0.9207776f } 211 | }; 212 | 213 | public static float[][] TestResults3D { get; } = new[] 214 | { 215 | new[] { 0.3296421f, -6.753858f, -8.818388f }, 216 | new[] { 4.616915f, -5.549636f, -0.9464225f }, 217 | new[] { 1.912133f, -4.816415f, 1.669457f }, 218 | new[] { -1.434986f, -5.38502f, -2.660372f }, 219 | new[] { 5.79124f, -4.837191f, -1.353609f }, 220 | new[] { -4.128863f, -4.122072f, -4.167237f }, 221 | new[] { 2.658624f, -5.587315f, -3.73626f }, 222 | new[] { 3.415799f, -2.091272f, -1.595504f }, 223 | new[] { 2.338245f, -7.289652f, -0.8838012f }, 224 | new[] { -5.242035f, -3.85071f, -5.191285f }, 225 | new[] { 1.809993f, -5.426212f, -10.46675f }, 226 | new[] { 3.770066f, -4.756917f, 0.4359012f }, 227 | new[] { -1.006244f, -4.897892f, -3.595196f }, 228 | new[] { -1.008714f, -2.672057f, -4.91155f }, 229 | new[] { 5.54211f, -4.821457f, -0.8330045f }, 230 | new[] { 1.074861f, -3.557968f, -1.624334f }, 231 | new[] { 3.626313f, -4.933278f, -5.380041f }, 232 | new[] { 3.595803f, -2.586318f, -0.4920848f }, 233 | new[] { 0.3084172f, -3.438377f, -2.611337f }, 234 | new[] { -2.536091f, -4.925086f, -3.508682f }, 235 | new[] { 1.995182f, -4.750051f, -9.732018f }, 236 | new[] { 2.116572f, -5.56626f, 1.09675f }, 237 | new[] { 0.590396f, -4.792588f, -3.006315f }, 238 | new[] { -2.239354f, -3.060114f, -4.604927f }, 239 | new[] { 5.122916f, -4.962685f, -1.897424f }, 240 | new[] { 0.9867913f, -3.70471f, -1.842693f }, 241 | new[] { 3.997803f, -5.339355f, -4.867959f }, 242 | new[] { 3.853042f, -3.048736f, -1.196938f }, 243 | new[] { 1.269793f, -4.030499f, -0.7389124f }, 244 | new[] { -4.027319f, -4.308586f, -3.982872f }, 245 | new[] { 2.293057f, -5.402081f, -10.94966f }, 246 | new[] { -3.299832f, -4.502176f, -3.235481f }, 247 | new[] { 1.76854f, -3.388761f, -3.745355f }, 248 | new[] { 0.01418269f, -3.167543f, -2.78126f }, 249 | new[] { 3.032885f, -4.717833f, -5.197276f }, 250 | new[] { -1.263622f, -2.744089f, 1.44622f }, 251 | new[] { 2.601995f, -5.784561f, -11.19244f }, 252 | new[] { -3.831325f, -3.475907f, -4.441282f }, 253 | new[] { 0.3756016f, -4.048975f, -1.416553f }, 254 | new[] { -3.185873f, -4.954843f, -2.946358f }, 255 | new[] { 1.969193f, -3.699514f, -0.6256625f }, 256 | new[] { 4.216197f, -4.414992f, -2.045035f }, 257 | new[] { 3.546402f, -5.694018f, 0.8498759f }, 258 | new[] { 3.510612f, -2.504033f, -1.699088f }, 259 | new[] { 2.644541f, -2.103311f, -3.200837f }, 260 | new[] { -2.70559f, -4.919746f, -3.061578f }, 261 | new[] { 0.7669074f, -3.060825f, -1.181533f }, 262 | new[] { 3.425015f, -4.688536f, 0.9474523f }, 263 | new[] { 1.887939f, -5.215654f, -10.05782f }, 264 | new[] { 2.145744f, -6.386961f, -11.46865f }, 265 | new[] { 1.368508f, -4.679376f, 1.921977f }, 266 | new[] { 2.797714f, -3.973244f, 2.705537f }, 267 | new[] { 3.288908f, -2.586312f, -1.685063f }, 268 | new[] { 1.416532f, -4.518889f, -0.6887631f }, 269 | new[] { 2.912766f, -3.896601f, 2.797744f }, 270 | new[] { 1.980949f, -5.612195f, -8.725646f }, 271 | new[] { 3.873387f, -5.097413f, 0.7784522f }, 272 | new[] { 4.534695f, -7.309093f, 0.3521543f }, 273 | new[] { 3.433252f, -4.974471f, -5.562697f }, 274 | new[] { -1.508732f, -3.42938f, -4.163044f }, 275 | new[] { -1.619553f, -3.886419f, -3.12351f }, 276 | new[] { 3.725511f, -1.583363f, -2.260959f }, 277 | new[] { -1.283567f, -4.402247f, -3.869999f }, 278 | new[] { -0.8045936f, -4.727701f, -4.596708f }, 279 | new[] { 8.298201f, -2.012852f, -1.90397f }, 280 | new[] { 2.277084f, -4.464949f, -5.348395f }, 281 | new[] { 3.364753f, -4.120842f, -5.214706f }, 282 | new[] { 1.326963f, -7.035489f, -5.047671f }, 283 | new[] { 5.769221f, -5.092606f, -1.788288f }, 284 | new[] { -0.9384711f, -4.859257f, -1.638373f }, 285 | new[] { 1.547893f, -2.003042f, 3.408768f }, 286 | new[] { 0.8960452f, -3.770396f, -1.931583f }, 287 | new[] { 2.667089f, -5.881826f, -11.07828f }, 288 | new[] { -2.959897f, -4.284044f, -4.300431f }, 289 | new[] { 0.1739722f, -3.555531f, -1.617135f }, 290 | new[] { 1.703177f, -4.386927f, 2.287134f }, 291 | new[] { 0.7687496f, -3.598454f, -0.9895182f }, 292 | new[] { 4.587348f, -4.00893f, 0.2220344f }, 293 | new[] { 2.643583f, -5.901285f, -11.68189f }, 294 | new[] { 2.465163f, -5.82519f, -11.5582f }, 295 | new[] { 3.830516f, -7.378181f, 0.2881624f }, 296 | new[] { 4.119965f, -2.678119f, -1.026754f }, 297 | new[] { 3.487442f, -3.562086f, -4.975732f }, 298 | new[] { -1.440468f, -3.660691f, -4.017163f }, 299 | new[] { 0.8626004f, -4.595687f, -3.882763f }, 300 | new[] { 3.896677f, -7.20416f, 0.06009799f }, 301 | new[] { 2.383678f, -3.051026f, -1.048198f }, 302 | new[] { 4.187432f, -5.705504f, -1.661127f }, 303 | new[] { 2.831393f, -5.174496f, -4.938772f }, 304 | new[] { 2.593388f, -0.2970088f, -7.808298f }, 305 | new[] { 3.817667f, -4.770395f, 0.7046201f }, 306 | new[] { -0.7714103f, -3.456257f, -3.731102f }, 307 | new[] { -2.623014f, -5.40233f, -3.25167f }, 308 | new[] { 3.445163f, -5.699363f, -1.178799f }, 309 | new[] { 2.935046f, -0.4999955f, -0.2860188f }, 310 | new[] { 3.531482f, -5.201192f, -5.803703f }, 311 | new[] { 0.2635803f, -3.656114f, -1.731611f }, 312 | new[] { 5.480093f, -4.955137f, -1.228168f }, 313 | new[] { -1.730953f, -3.181008f, -4.338359f }, 314 | new[] { 2.814848f, -5.972325f, -2.887124f } 315 | }; 316 | } 317 | } -------------------------------------------------------------------------------- /UnitTests/UnitTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp2.1 5 | 6 | false 7 | 8 | 9 | 10 | UMAP.UnitTests 11 | 12 | 13 | 14 | 15 | 16 | 17 | all 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /build-nuget.yaml: -------------------------------------------------------------------------------- 1 | variables: 2 | project: './UMAP/UMAP.csproj' 3 | buildConfiguration: 'Release' 4 | 5 | trigger: 6 | - master 7 | 8 | pool: 9 | vmImage: 'windows-latest' 10 | 11 | steps: 12 | - task: NuGetToolInstaller@1 13 | 14 | - task: UseDotNet@2 15 | displayName: 'Use .NET 7.0 SDK' 16 | inputs: 17 | packageType: sdk 18 | version: 7.x 19 | includePreviewVersions: false 20 | installationPath: $(Agent.ToolsDirectory)\dotnet 21 | 22 | - task: DotNetCoreCLI@2 23 | inputs: 24 | command: 'restore' 25 | projects: '$(project)' 26 | displayName: 'restore nuget' 27 | 28 | - task: DotNetCoreCLI@2 29 | inputs: 30 | command: 'build' 31 | projects: '$(project)' 32 | arguments: '-c $(buildConfiguration) /p:Version=1.0.$(build.buildId) /p:LangVersion=latest' 33 | 34 | - task: DotNetCoreCLI@2 35 | inputs: 36 | command: 'pack' 37 | packagesToPack: '$(project)' 38 | versioningScheme: 'off' 39 | configuration: '$(buildConfiguration)' 40 | buildProperties: 'Version="1.0.$(build.buildId)";LangVersion="latest"' 41 | nobuild: true 42 | 43 | - task: NuGetCommand@2 44 | inputs: 45 | command: 'push' 46 | packagesToPush: '**/*.nupkg' 47 | nuGetFeedType: 'external' 48 | publishFeedCredentials: 'nuget-curiosity-org' 49 | displayName: 'push nuget' 50 | --------------------------------------------------------------------------------