├── .gitignore ├── .travis.yml ├── CHANGES ├── LICENSE ├── README.md ├── build.gradle ├── docs ├── benchmark.md ├── docs.md └── figures │ ├── DotBenchmark_arrayopss_server.png │ ├── DotBenchmark_arrayopss_workstation.png │ ├── DotBenchmark_flops_server.png │ ├── DotBenchmark_flops_workstation.png │ ├── ExpBenchmark_arrayopss_server.png │ ├── ExpBenchmark_arrayopss_workstation.png │ ├── ExpBenchmark_flops_server.png │ ├── ExpBenchmark_flops_workstation.png │ ├── Expm1Benchmark_arrayopss_server.png │ ├── Expm1Benchmark_arrayopss_workstation.png │ ├── Expm1Benchmark_flops_server.png │ ├── Expm1Benchmark_flops_workstation.png │ ├── Log1pBenchmark_arrayopss_server.png │ ├── Log1pBenchmark_arrayopss_workstation.png │ ├── Log1pBenchmark_flops_server.png │ ├── Log1pBenchmark_flops_workstation.png │ ├── LogAddExpBenchmark_arrayopss_server.png │ ├── LogAddExpBenchmark_arrayopss_workstation.png │ ├── LogAddExpBenchmark_flops_server.png │ ├── LogAddExpBenchmark_flops_workstation.png │ ├── LogBenchmark_arrayopss_server.png │ ├── LogBenchmark_arrayopss_workstation.png │ ├── LogBenchmark_flops_server.png │ ├── LogBenchmark_flops_workstation.png │ ├── LogSumExpBenchmark_arrayopss_server.png │ ├── LogSumExpBenchmark_arrayopss_workstation.png │ ├── LogSumExpBenchmark_flops_server.png │ ├── LogSumExpBenchmark_flops_workstation.png │ ├── SDBenchmark_arrayopss_server.png │ ├── SDBenchmark_arrayopss_workstation.png │ ├── SDBenchmark_flops_server.png │ ├── SDBenchmark_flops_workstation.png │ ├── SumBenchmark_arrayopss_server.png │ ├── SumBenchmark_arrayopss_workstation.png │ ├── SumBenchmark_flops_server.png │ └── SumBenchmark_flops_workstation.png ├── gradle.properties ├── gradle ├── boost-simd.gradle ├── disable-static.gradle ├── jni-headers.gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── src ├── loader └── cpp │ └── org_jetbrains_bio_viktor_LoaderKt.cpp ├── main └── kotlin │ └── org │ └── jetbrains │ └── bio │ └── viktor │ ├── DoubleExtensions.kt │ ├── F64Array.kt │ ├── F64DenseFlatArray.kt │ ├── F64FlatArray.kt │ ├── Internals.kt │ ├── Loader.kt │ ├── MoreMath.kt │ ├── NativeSpeedups.kt │ ├── Random.kt │ ├── Searching.kt │ ├── Serialization.kt │ └── Sorting.kt ├── simd ├── cpp │ └── org_jetbrains_bio_viktor_NativeSpeedups.cpp └── headers │ ├── simd_math.hpp │ ├── source.hpp │ └── summing.hpp └── test └── kotlin └── org └── jetbrains └── bio └── viktor ├── BalancedSumTests.kt ├── DoubleExtensionsTest.kt ├── F64ArrayAgainstRTest.kt ├── F64ArrayCreationTest.kt ├── F64ArrayGetSetTests.kt ├── F64ArrayOpsTests.kt ├── F64ArraySlicingTest.kt ├── F64FixedArrayOperationTest.kt ├── MoreMathTests.kt ├── NativeSpeedupTest.kt ├── RandomTests.kt ├── SearchingTests.kt ├── SerializationTests.kt ├── SortingTests.kt ├── TestSupport.kt └── UnsupportedOpTest.kt /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | 4 | build 5 | 6 | .gradle 7 | 8 | *.class 9 | 10 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 11 | hs_err_pid* 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - oraclejdk8 4 | 5 | sudo: true 6 | 7 | addons: 8 | apt: 9 | sources: 10 | - ubuntu-toolchain-r-test 11 | packages: 12 | - cmake 13 | - gcc-5 14 | - g++-5 15 | 16 | install: 17 | - sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-5 90 18 | - sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-5 90 19 | 20 | before_cache: 21 | - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock 22 | - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ 23 | cache: 24 | directories: 25 | - $HOME/.gradle/caches/ 26 | - $HOME/.gradle/wrapper/ 27 | 28 | dist: trusty -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | viktor Changelog 2 | ================ 3 | 4 | Here you can see the full list of changes between each viktor release. 5 | 6 | Version 1.2.0 7 | 8 | Released on November 4, 2021 9 | 10 | - added "fold" and "reduce" methods 11 | - added "combine" and "combineInPlace" methods 12 | - deprecated "size" property in favor of the new "length" property 13 | - changed the distribution method from Bintray to Maven Central 14 | 15 | Version 1.1.0 16 | 17 | Released on February 9, 2021 18 | 19 | - added "transform" and "transformInPlace" methods 20 | - improved performance of multiple operations 21 | - F64FlatArray is specified as return type whenever possible 22 | - types other than F64Array and F64FlatArray are now internal 23 | - improved index and dimensionality checks 24 | - improved test coverage 25 | - removed the benchmarks that were bundled with the project 26 | 27 | Version 1.0.2 28 | ------------- 29 | 30 | Released on January 14, 2021 31 | 32 | - Bumped Kotlin, Gradle, Bintray plugin and NPY versions 33 | 34 | Version 1.0.1 35 | ------------- 36 | 37 | Released on December 4, 2019 38 | 39 | - Replaced log4j logging implementation with slf4j API, since libraries shouldn't enforce logging framework 40 | 41 | 42 | Version 1.0.0 43 | ------------- 44 | 45 | Released on November 19, 2019 46 | 47 | - Simplified signatures 48 | - Added a benchmark JAR task 49 | - Simplified the native methods' signatures 50 | - Removed transpose and reversed 51 | - Introduced unroll mechanics for F64Array 52 | - Serialization uses isFlattenable instead of isDense 53 | - Changed native signature of logAddExp to dst-src syntax 54 | - logAddExp now deals with positive infinities and NaNs 55 | - Corrected guessShape implementation 56 | - Correct JNI copy processing 57 | - Added extensive Markdown documentation 58 | - Reorganized benchmarks 59 | - Removed weightedSum / Mean, transpose, reversed 60 | - Added benchmarking data and description thereof to documentation 61 | - Fixed Travis builds 62 | 63 | Version 0.5.3 64 | ------------- 65 | 66 | Released on January 21, 2019 67 | 68 | - Use the latest Kotlin version 69 | - No more illegal reflective access in Loader 70 | 71 | 72 | Version 0.5.2 73 | ------------- 74 | 75 | Released on January 15, 2019 76 | 77 | - Previous version was MacOS-specific due to outdated instructions in README. 78 | 79 | 80 | Version 0.5.1 81 | ------------- 82 | 83 | Released on December 12, 2018 84 | 85 | - Library dependencies containing '+' symbol replaced with exact versions. This gives more predictable runtime behaviour. 86 | 87 | Version 0.5.0 88 | ------------- 89 | 90 | Released on October 1, 2018 91 | 92 | - Cross-platform build for 64 bit Linux, Windows and MacOS. 93 | 94 | Version 0.4.2 95 | ------------- 96 | 97 | - Fixed `Array<*>.toF64Array`. 98 | 99 | Version 0.4.1 100 | ------------- 101 | 102 | - Fixed `F64Array.V` serialization. 103 | 104 | Version 0.4.0 105 | ------------- 106 | 107 | Released on November 1st 2016 108 | 109 | - Removed special cased `Vector`, `Matrix2` and `Matrix3` classes in 110 | favor of a more generic `F64Array`. 111 | - Fixed serialization of non-dense arrays. 112 | - Allowed slicing an `F64Array` with a custom `step`. 113 | - Added a special attribute `V` for n-d slicing the array. 114 | - Removed `stochastic` and `indexedStochastic`. 115 | - Added `axis` to `F64Array.append`, `F64Array.slice` and `F64Array.reorder`. 116 | - Added `Array<*>.toF64Array` for converting nested arrays to strided ones. 117 | 118 | Version 0.3.5 119 | ------------- 120 | 121 | Bugfix release, released on September 21st 2016 122 | 123 | - Fixed yet another bug, but this time in `StridedMatrix3.toString`. 124 | Like they say, copy-paste is never a good thing. 125 | 126 | Version 0.3.4 127 | ------------- 128 | 129 | Released on September 21st 2016 130 | 131 | - Fixed a bug in `StridedMatrix2.toString`, which incorrectly rendered 132 | large matrices, and unified the implementation with `StridedMatrix3`. 133 | - Added NumPy-compatible serialization support. 134 | - Switched to the first stable release of Boost.SIMD v4.16.9.0. 135 | 136 | Version 0.3.3 137 | ------------- 138 | 139 | Bugfix release, released on September 2nd 2016 140 | 141 | - Fixed a bug in `DenseVector.minusAssign` which called `plusAssign` 142 | if the argument was not dense. 143 | 144 | Version 0.3.2 145 | ------------- 146 | 147 | Bugfix release, released on June 27th 2016 148 | 149 | 150 | Version 0.3.1 151 | ------------- 152 | 153 | Bugfix release, released on June 27th 2016 154 | 155 | - Fixed loading native libraries from the JAR file. 156 | - Fixed error handling on unsupported architectures. 157 | 158 | Version 0.3.0 159 | ------------- 160 | 161 | Released on June 27th 2016 162 | 163 | - Dropped dependency on Yeppp! and embedded a re-worked simdstat. 164 | - Removed `StridedVector.sumSq` in favour of `dot`. 165 | - Switched to balanced summation in pure-Kotlin versions of 166 | `StridedVector.sum` and `dot`. 167 | - Added `StridedVector.sd` for computing unbiased standard deviation. 168 | - Added SIMD speedups for / and /= operations. 169 | - Added `StridedVector.log1p` and `expm1`. 170 | - Fixed a bug in scalar division in expressions of the form 1.0 / v. 171 | - Mirrored `StridedVector` operations in `StridedMatrix2` and 172 | `StridedMatrix3`. 173 | - Extended operator overloads for `Double` to `StridedMatrix2` and 174 | `StidedMatrix3`. 175 | 176 | Version 0.2.3 177 | ------------- 178 | 179 | Released on May 26th 2016 180 | 181 | - Added `StridedVector.append` and `StridedVector.concatenate` for 182 | joining multiple vectors into a single vector. 183 | - Changed `StridedVector.sort`, `argSort` and `reorder` to be extension 184 | functions. 185 | - Exposed `StridedVector.partition`. 186 | - Added `StridedVector.searchSorted` for doing efficient sorted lookups. 187 | - Changed `StridedVector.argMin` and `argMax` to be extension 188 | functions. 189 | 190 | Version 0.2.2 191 | ------------- 192 | 193 | Released on April 29th 2016 194 | 195 | - Added unary operator overloads for `StridedVector`. 196 | - Implemented * and / operations for `StridedVector`. 197 | - Added extra operator overloads for `Double`, so it is now possible to 198 | write (1.0 + v / 2.0). 199 | - Fixed `StridedVector.toString` in case of NaN and infinities. 200 | - Changed `StridedVector.toString` to be more like NumPy for larger vectors. 201 | 202 | Version 0.2.1 203 | ------------- 204 | 205 | Bugfix release, released on April 22nd 2016 206 | 207 | - Changed `StridedVector.quantile` to follow Commons Math implementation. 208 | 209 | Version 0.2.0 210 | ------------- 211 | 212 | Released on April 22nd 2016 213 | 214 | - Changed all `StridedVector` members are now public. 215 | - Changed `StridedVector.size` to be a property instead of a function. 216 | - Removed `StridedVector.wrap`, please use `DoubleArray.asStrided` instead. 217 | - Implemented `StridedVector.dot` for the case when the argument is 218 | also a `StridedVector`. 219 | - Added `StridedVector.quantile` for (amortized) linear-time order 220 | statistic queries. 221 | - Added `StridedVector.shuffle` for randomly permuting vector elements. 222 | 223 | Version 0.1.4 224 | ------------- 225 | 226 | Technical release, released on February 3rd 2016 227 | 228 | - Migrated to Kotlin 1.0.0-rc. 229 | 230 | Version 0.1.3 231 | ------------- 232 | 233 | Technical release, released on February 1st 2016 234 | 235 | - Renamed `StridedVector.sorted` to `argSort` to avoid confusion with 236 | the `sorted` method on primitive arrays. 237 | 238 | Version 0.1.2 239 | ------------- 240 | 241 | Released on December 9th, 2015 242 | 243 | - Overloaded - and / operations for `StridedVector`. 244 | - Removed redundant boxing and copying in `StridedVector.sorted`. 245 | - Fixed indexing in `StridedMatrix3.equals`. 246 | - Added two new operations `StridedVector.mean` and `StridedVector.sumSq`, 247 | which are also applicable to matrices. 248 | 249 | Version 0.1.1 250 | ------------- 251 | 252 | Technical release, released on October 22th, 2015 253 | 254 | - Updated to Kotlin 1.0.0-beta. 255 | - Renamed `_` to `_I` because the latter isn't a valid identifier as 256 | of Kotlin M14. This is a temporary solution. I hope we'll find 257 | a more human-readable workaround eventually. 258 | 259 | Version 0.1.0 260 | ------------- 261 | 262 | Initial release, released on October 15th, 2015 263 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 JetBrains BioLabs 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![JetBrains Research](https://jb.gg/badges/research.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub) 2 | [![tests](http://teamcity.jetbrains.com/app/rest/builds/buildType:(id:Epigenome_Tools_Viktor)/statusIcon.svg)](http://teamcity.jetbrains.com/viewType.html?buildTypeId=Epigenome_Tools_Viktor&guest=1) 3 | 4 | [![Maven Central](https://img.shields.io/maven-central/v/org.jetbrains.bio/viktor.svg?label=Maven%20Central)](https://search.maven.org/search?q=g:%22org.jetbrains.bio%22%20AND%20a:%22viktor%22) 5 | 6 | viktor 7 | ====== 8 | 9 | `viktor` implements a restricted subset of NumPy [ndarray][ndarray] features in 10 | Kotlin. Here are some highlights: 11 | 12 | * A single core data type --- `F64Array`, an n-dimensional primitive array. 13 | * Efficient vectorized operations, which are accelerated using SIMD whenever 14 | possible. 15 | * Semi-sweet syntax. 16 | 17 | ```kotlin 18 | val m = F64Array(4, 3) 19 | m.V[0] = F64Array.full(3, 42.0) // row-view. 20 | m.V[_I, 0] // column-view. 21 | m.V[0] = 42.0 // broadcasting. 22 | m + 0.5 * m // arithmetic operations. 23 | m.V[0].exp() + 1.0 // math functions. 24 | ``` 25 | 26 | [ndarray]: http://docs.scipy.org/doc/numpy/reference/arrays.ndarray.html 27 | 28 | Installation 29 | ------------ 30 | 31 | The latest version of `viktor` is available on [Maven Central][maven-central]. 32 | If you're using Gradle, just add the following to your `build.gradle` dependencies: 33 | ```groovy 34 | implementation 'org.jetbrains.bio:viktor:1.2.0' 35 | ``` 36 | or, equivalently, add the following to your `build.gradle.kts` dependencies: 37 | ```kotlin 38 | implementation("org.jetbrains.bio:viktor:1.2.0") 39 | ``` 40 | With Maven, use the dependency 41 | ```xml 42 | 43 | org.jetbrains.bio 44 | viktor 45 | 1.2.0 46 | 47 | ``` 48 | 49 | Versions older than `1.1.0` can be downloaded from [GitHub Releases][releases]. 50 | 51 | [maven-central]: https://search.maven.org/artifact/org.jetbrains.bio/viktor/1.2.0/jar 52 | [releases]: https://github.com/JetBrains-Research/viktor/releases 53 | 54 | The JAR available on Maven Central currently targets only: 55 | - SSE2 and AVX, 56 | - amd64 / x86-64, 57 | - Linux, Windows and MacOS. 58 | 59 | For any other setup `viktor` would fall back to pure-Kotlin 60 | implementations. If you are interested in SIMD accelerations for a different 61 | architecture, instruction set, or operating system feel free to file an issue to the 62 | [bug tracker][issues]. 63 | 64 | [issues]: https://github.com/JetBrains-Research/viktor/issues 65 | 66 | Logging 67 | ------- 68 | 69 | `viktor` uses [slf4j](http://www.slf4j.org/) logging API to provide error messages. 70 | To see them, you have to add a `slf4j` implementation (also called a binding) 71 | to your project. For example, add the following Gradle dependency to use `log4j`: 72 | ```gradle 73 | dependencies { 74 | compile group: 'org.slf4j', name: 'slf4j-log4j12', version: '1.7.25' 75 | } 76 | ``` 77 | 78 | Building from source 79 | -------------------- 80 | 81 | `viktor` relies on [boost.simd][boost.simd] for implementing SIMD 82 | accelerations. Therefore, you would need a C++11 compiler, 83 | but otherwise the build process is as simple as: 84 | 85 | ```shell 86 | ./gradlew jar 87 | ``` 88 | 89 | Note: don't use `./gradlew assemble`, since it includes the signing of the artifacts 90 | and will fail if the correct credentials are not provided. 91 | 92 | [boost.simd]: https://github.com/JetBrains-Research/boost.simd 93 | 94 | Testing 95 | ------- 96 | 97 | No extra configuration is required for running the tests from Gradle: 98 | 99 | ```shell 100 | ./gradlew test 101 | ``` 102 | 103 | However, you might need to alter `java.library.path` to run the tests from 104 | the IDE. The following Java command line option should work for IDEA 105 | 106 | ```shell 107 | -Djava.library.path=./build/libs 108 | ``` 109 | 110 | Publishing 111 | ---------- 112 | 113 | Publishing to [Maven Central][maven-central] is currently done via a dedicated 114 | build configuration of an internal TeamCity server. This allows us 115 | to deploy a cross-platform version. 116 | 117 | Documentation 118 | ---- 119 | 120 | Visit [viktor Documentation](./docs/docs.md) for an extensive feature overview, 121 | instructive code examples and benchmarking data. 122 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | kotlin_version = '1.4.21' 4 | } 5 | } 6 | 7 | plugins { 8 | id "org.jetbrains.dokka" version "1.4.10.2" 9 | id "org.jetbrains.kotlin.jvm" version "$kotlin_version" 10 | } 11 | 12 | apply plugin: 'cpp' 13 | apply plugin: 'kotlin' 14 | apply plugin: 'maven' 15 | apply plugin: 'org.jetbrains.dokka' 16 | apply plugin: 'signing' 17 | 18 | apply from: "$rootDir/gradle/jni-headers.gradle" 19 | apply from: "$rootDir/gradle/boost-simd.gradle" 20 | apply from: "$rootDir/gradle/disable-static.gradle" 21 | 22 | compileKotlin { 23 | kotlinOptions.jvmTarget = "1.8" 24 | } 25 | 26 | compileTestKotlin { 27 | kotlinOptions.jvmTarget = "1.8" 28 | kotlinOptions { 29 | languageVersion = "1.4" 30 | } 31 | } 32 | 33 | model { 34 | platforms { 35 | linux { 36 | operatingSystem 'linux' 37 | } 38 | osx { 39 | operatingSystem 'osx' 40 | } 41 | windows { 42 | operatingSystem 'windows' 43 | } 44 | } 45 | 46 | toolChains { 47 | visualCpp(VisualCpp) { 48 | /* this is not pretty, but: 49 | 1. Gradle currently doesn't always play nice with Visual Studio 2017 50 | 2. Gradle currently doesn't have any other way to specify Visual Studio version 51 | 3. Without "file://", the build script causes exceptions on Linux, since it tries to convert 52 | this string to Unix path, which leads to a problem with spaces. */ 53 | installDir "file://C:/Program Files (x86)/Microsoft Visual Studio 14.0" 54 | } 55 | 56 | gcc(Gcc) 57 | 58 | clang(Clang) 59 | } 60 | 61 | components { 62 | loader(NativeLibrarySpec) { 63 | sources { 64 | cpp.lib library: 'jniHeaders', linkage: 'api' 65 | } 66 | 67 | targetPlatform 'linux' 68 | targetPlatform 'windows' 69 | targetPlatform 'osx' 70 | 71 | binaries.withType(SharedLibraryBinarySpec) { 72 | def libraryName = System.mapLibraryName('simd.x86_64') 73 | sharedLibraryFile = file("$buildDir/libs/$libraryName") 74 | } 75 | } 76 | 77 | simd(NativeLibrarySpec) { 78 | sources { 79 | cpp.lib library: 'boostSimd', linkage: 'api' 80 | cpp.lib library: 'jniHeaders', linkage: 'api' 81 | } 82 | 83 | flavors { 84 | sse2 85 | avx 86 | } 87 | 88 | targetPlatform 'linux' 89 | targetPlatform 'windows' 90 | targetPlatform 'osx' 91 | targetFlavors 'sse2', 'avx' 92 | 93 | binaries.withType(SharedLibraryBinarySpec) { 94 | switch (flavor) { 95 | case flavors.sse2: if (targetPlatform == 'windows') { 96 | cppCompiler.args '/arch:SSE2' 97 | } else { 98 | cppCompiler.args '-msse2' 99 | }; break 100 | case flavors.avx: if (targetPlatform == 'windows') { 101 | cppCompiler.args '/arch:AVX' 102 | } else { 103 | cppCompiler.args '-mavx' 104 | }; break 105 | } 106 | 107 | def libraryName = System.mapLibraryName("simd.${flavor.name}.x86_64") 108 | sharedLibraryFile = file("$buildDir/libs/$libraryName") 109 | } 110 | } 111 | } 112 | 113 | components { 114 | all { 115 | binaries.all { 116 | cppCompiler.define 'BOOST_DISABLE_ASSERTS' 117 | cppCompiler.define 'NDEBUG' 118 | 119 | if (toolChain in Gcc) { 120 | cppCompiler.args('-std=c++11', '-fno-rtti', '-fno-exceptions', 121 | '-O3', '-Wno-narrowing') 122 | } 123 | if (toolChain in VisualCpp) { 124 | cppCompiler.args('/GR-', '/O2') 125 | } 126 | if (toolChain in Clang) { 127 | cppCompiler.args('-stdlib=libc++', '-std=c++1y') 128 | } 129 | } 130 | } 131 | } 132 | 133 | tasks { 134 | buildAllVariants(Task) { 135 | dependsOn $.binaries.findAll { it.buildable } 136 | } 137 | } 138 | } 139 | 140 | task jniHeaders(dependsOn: compileKotlin) { 141 | def outputDir = file("$buildDir/include") 142 | def jniClasses = [ 143 | 'org.jetbrains.bio.viktor.NativeSpeedups', 144 | 'org.jetbrains.bio.viktor.LoaderKt' 145 | ] 146 | def jniHeaders = jniClasses.collect { 147 | new File(outputDir, it.replace('.', '_') + '.hpp') 148 | } 149 | 150 | inputs.files sourceSets.main.output 151 | outputs.files jniHeaders 152 | 153 | doLast { 154 | outputDir.mkdirs() 155 | [jniClasses, jniHeaders].transpose().each(javah) 156 | } 157 | } 158 | 159 | tasks.withType(CppCompile) { 160 | dependsOn 'jniHeaders' 161 | dependsOn 'installBoostSimd' 162 | } 163 | 164 | repositories { 165 | mavenCentral() 166 | jcenter() 167 | } 168 | 169 | dependencies { 170 | compile 'org.apache.commons:commons-math3:3.6' 171 | compile "org.jetbrains.bio:npy:0.3.5" 172 | compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 173 | compile group: 'org.slf4j', name: 'slf4j-api', version: '1.7.25' 174 | 175 | testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" 176 | testCompile 'junit:junit:4.12' 177 | testCompile group: 'org.slf4j', name: 'slf4j-log4j12', version: '1.7.25' 178 | } 179 | 180 | dokkaJavadoc { 181 | outputDirectory = javadoc.destinationDir 182 | inputs.dir 'src/main/kotlin' 183 | } 184 | 185 | test { 186 | dependsOn 'buildAllVariants' 187 | systemProperty 'java.library.path', "$buildDir/libs" 188 | } 189 | 190 | apply plugin: 'idea' 191 | 192 | idea { 193 | module { 194 | name = 'viktor' 195 | } 196 | } 197 | 198 | jar { 199 | archivesBaseName = 'viktor' 200 | dependsOn 'buildAllVariants' 201 | from "$buildDir/libs" 202 | exclude '*.jar' 203 | } 204 | 205 | task sourcesJar(type: Jar) { 206 | classifier = 'sources' 207 | from sourceSets.main.allSource 208 | } 209 | 210 | task javadocJar(type: Jar, dependsOn: dokkaJavadoc) { 211 | classifier = 'javadoc' 212 | from javadoc.destinationDir 213 | } 214 | 215 | artifacts { 216 | archives sourcesJar, javadocJar 217 | } 218 | 219 | signing { 220 | // multiline environment variables are not fun. 221 | def signingKey = findProperty("signingKey")?.replace("\\n", "\n") 222 | def signingPassword = findProperty("signingPassword") 223 | useInMemoryPgpKeys(signingKey, signingPassword) 224 | sign configurations.archives 225 | } 226 | 227 | uploadArchives { 228 | 229 | doFirst { 230 | assert file("./build/libs/libsimd.x86_64.so").exists() 231 | assert file("./build/libs/libsimd.x86_64.dylib").exists() 232 | assert file("./build/libs/simd.x86_64.dll").exists() 233 | } 234 | 235 | repositories { 236 | mavenDeployer { 237 | def ossrhUsername = findProperty("ossrhUsername") 238 | def ossrhPassword = findProperty("ossrhPassword") 239 | 240 | beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } 241 | 242 | repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") { 243 | authentication(userName: ossrhUsername, password: ossrhPassword) 244 | } 245 | 246 | snapshotRepository(url: "https://oss.sonatype.org/content/repositories/snapshots/") { 247 | authentication(userName: ossrhUsername, password: ossrhPassword) 248 | } 249 | 250 | pom.groupId = 'org.jetbrains.bio' 251 | 252 | pom.project { 253 | name 'viktor' 254 | packaging 'jar' 255 | // optionally artifactId can be defined here 256 | description 'Efficient f64-only ndarray in Kotlin' 257 | url 'https://github.com/JetBrains-Research/viktor' 258 | 259 | scm { 260 | connection 'scm:git:git@github.com:JetBrains-Research/viktor.git' 261 | developerConnection 'scm:git:git@github.com:JetBrains-Research/viktor.git' 262 | url 'https://github.com/JetBrains-Research/viktor' 263 | } 264 | 265 | licenses { 266 | license { 267 | name 'MIT License' 268 | url 'https://github.com/JetBrains-Research/viktor/blob/master/LICENSE' 269 | } 270 | } 271 | 272 | developers { 273 | developer { 274 | id 'dievsky' 275 | name 'Aleksei Dievskii' 276 | email 'alexey.dievsky@jetbrains.com' 277 | } 278 | developer { 279 | id 'slebedev' 280 | name 'Sergei Lebedev' 281 | email 'sergei.a.lebedev@gmail.com' 282 | } 283 | } 284 | } 285 | } 286 | } 287 | } 288 | 289 | wrapper { 290 | gradleVersion = '6.5' 291 | } 292 | -------------------------------------------------------------------------------- /docs/benchmark.md: -------------------------------------------------------------------------------- 1 | # viktor Benchmarks 2 | 3 | We designed a series of microbenchmarks to compare `viktor`'s 4 | native SIMD optimization efficiency to that of a simple Kotlin/Java loop. 5 | 6 | The microbenchmark code can be found in `src/jmh` folder. To run the benchmarks, run 7 | the following commands from `viktor`'s root folder: 8 | ```bash 9 | $ ./gradlew clean assemble benchmarkJar 10 | $ java -jar ./build/libs/viktor-benchmark.jar 11 | ``` 12 | You can add the usual [JMH](https://openjdk.java.net/projects/code-tools/jmh/) 13 | command line arguments to the latter command, e.g. `-o` to specify 14 | the output file or `-t` to control the number of threads. 15 | 16 | ## Benchmark Environment 17 | 18 | We conducted the benchmarking on two machines: 19 | a laptop running on Intel Core i7-6820HQ CPU at 2.70GHz 20 | and a server running on Intel Xeon E5-2690 at 2.90GHz. 21 | The following table summarizes the main features of both: 22 | 23 | machine | laptop | server 24 | --------|--------|------- 25 | CPU | Intel Core i7-6820HQ | Intel Xeon E5-2690 26 | frequency | 2.70GHz | 2.90 GHz 27 | cores1 | 8 | 32 28 | architecture | `amd64` | `amd64` 29 | highest SIMD extension2 | `AVX` | `SSE2` 30 | OS | Ubuntu 18.04.3 LTS | CentOS Linux 7 (Core) 31 | JVM | Java(TM) SE Runtime Environment (build 1.8.0_201-b09) | OpenJDK Runtime Environment (build 1.8.0_242-b08) 32 | 33 | 1 The number of cores shouldn't matter since all benchmarks ran in a single thread. 34 | 35 | 2 The most advanced extension that was used by `viktor`. In reality, the laptop 36 | had `AVX2` and the server had `SSE4.2` as the highest extension. 37 | 38 | ## Benchmark Results 39 | 40 | The following data is provided for informational purposes only. 41 | In each benchmark, we considered arrays of size `1000`, `100_000` and `1_000_000`. 42 | We investigate two metrics: 43 | * `Array ops/s` is the number of times the operation was performed on the entire array 44 | per second. Shown on a logarithmic scale. 45 | * `FLOPS` is the number of equivalent scalar operations performed per second. It is equal 46 | to `Array ops/s` multiplied by `Array size`. Shown on a linear scale. 47 | 48 | ### Math Benchmarks 49 | 50 | The task here was to calculate exponent (logarithm, `expm1`, `log1p` respectively) 51 | of all the elements in a double array. It was done either with a simple loop, 52 | or with a dedicated `viktor` array method (e.g. `expInPlace`). We also evaluated the 53 | efficiency of 54 | [`FastMath`](https://commons.apache.org/proper/commons-math/javadocs/api-3.3/org/apache/commons/math3/util/FastMath.html) 55 | methods compared to Java's built-in `Math`. 56 | 57 | We also measured the performance of `logAddExp` method for adding 58 | two logarithmically stored arrays. It was compared with the scalar `logAddExp` 59 | defined in `viktor`. 60 | 61 | #### Laptop 62 | 63 | Array ops/s | FLOPS 64 | ------------|------ 65 | ![exp laptop array ops/s](./figures/ExpBenchmark_arrayopss_workstation.png) | ![exp laptop flops](./figures/ExpBenchmark_flops_workstation.png) 66 | ![expm1 laptop array ops/s](./figures/Expm1Benchmark_arrayopss_workstation.png) | ![expm1 laptop flops](./figures/Expm1Benchmark_flops_workstation.png) 67 | ![log laptop array ops/s](./figures/LogBenchmark_arrayopss_workstation.png) | ![log laptop flops](./figures/LogBenchmark_flops_workstation.png) 68 | ![log1p laptop array ops/s](./figures/Log1pBenchmark_arrayopss_workstation.png) | ![log1p laptop flops](./figures/Log1pBenchmark_flops_workstation.png) 69 | ![logAddExp laptop array ops/s](./figures/LogAddExpBenchmark_arrayopss_workstation.png) | ![logAddExp laptop flops](./figures/LogAddExpBenchmark_flops_workstation.png) 70 | 71 | #### Server 72 | 73 | Array ops/s | FLOPS 74 | ------------|------ 75 | ![exp server array ops/s](./figures/ExpBenchmark_arrayopss_server.png) | ![exp server flops](./figures/ExpBenchmark_flops_server.png) 76 | ![expm1 server array ops/s](./figures/Expm1Benchmark_arrayopss_server.png) | ![expm1 server flops](./figures/Expm1Benchmark_flops_server.png) 77 | ![log server array ops/s](./figures/LogBenchmark_arrayopss_server.png) | ![log server flops](./figures/LogBenchmark_flops_server.png) 78 | ![log1p server array ops/s](./figures/Log1pBenchmark_arrayopss_server.png) | ![log1p server flops](./figures/Log1pBenchmark_flops_server.png) 79 | ![logAddExp server array ops/s](./figures/LogAddExpBenchmark_arrayopss_server.png) | ![logAddExp server flops](./figures/LogAddExpBenchmark_flops_server.png) 80 | 81 | ### Statistics Benchmarks 82 | 83 | We tested the `sum()`, `sd()` and `logSumExp()` methods here. We also measured 84 | the throughput of a dot product of two arrays (`dot()` method). All these benchmarks 85 | (except for `logSumExp`) don't have a loop-based `FastMath` version since they only 86 | use arithmetic operations in the loop. 87 | 88 | #### Laptop 89 | 90 | Array ops/s | FLOPS 91 | ------------|------ 92 | ![sum laptop array ops/s](./figures/SumBenchmark_arrayopss_workstation.png) | ![sum laptop flops](./figures/SumBenchmark_flops_workstation.png) 93 | ![sd laptop array ops/s](./figures/SDBenchmark_arrayopss_workstation.png) | ![sd laptop flops](./figures/SDBenchmark_flops_workstation.png) 94 | ![logSumExp laptop array ops/s](./figures/LogSumExpBenchmark_arrayopss_workstation.png) | ![logSumExp laptop flops](./figures/LogSumExpBenchmark_flops_workstation.png) 95 | ![dot laptop array ops/s](./figures/DotBenchmark_arrayopss_workstation.png) | ![dot laptop flops](./figures/DotBenchmark_flops_workstation.png) 96 | 97 | #### Server 98 | 99 | Array ops/s | FLOPS 100 | ------------|------ 101 | ![sum server array ops/s](./figures/SumBenchmark_arrayopss_server.png) | ![sum server flops](./figures/SumBenchmark_flops_server.png) 102 | ![sd server array ops/s](./figures/SDBenchmark_arrayopss_server.png) | ![sd server flops](./figures/SDBenchmark_flops_server.png) 103 | ![logSumExp server array ops/s](./figures/LogSumExpBenchmark_arrayopss_server.png) | ![logSumExp server flops](./figures/LogSumExpBenchmark_flops_server.png) 104 | ![dot server array ops/s](./figures/DotBenchmark_arrayopss_server.png) | ![dot server flops](./figures/DotBenchmark_flops_server.png) 105 | 106 | ## Cautious Conclusions 107 | 108 | `viktor` seems to perform better than the 109 | regular scalar computation approach. The difference can reach up to `57x` (`logSumExp` 110 | on `AVX` _vs_ that computed with a `Math` loop). 111 | The only notable exception to that seems to be, curiously, the same exact 112 | `logSumExp` using `FastMath`, which is a little faster than `viktor`'s 113 | `logSumExp()` on `SSE2` on a `1M`-sized array. -------------------------------------------------------------------------------- /docs/docs.md: -------------------------------------------------------------------------------- 1 | # viktor Overview 2 | 3 | * [General Concepts](#general-concepts) 4 | * [Creating Arrays and Accessing Elements](#creating-arrays-and-accessing-elements) 5 | * [Views](#views) 6 | * [Flattenable and Dense Arrays](#flattenable-and-dense-arrays) 7 | * [Copying](#copying) 8 | * [Arithmetic and Mathematics](#arithmetic-and-mathematics) 9 | * [Statistics and Special Methods](#statistics-and-special-methods) 10 | * [Logarithmic Storage](#logarithmic-storage) 11 | * [Efficiency and SIMD](#efficiency-and-simd) 12 | * [Examples](#examples) 13 | 14 | See [README](../README.md) for instructions on how to install `viktor`. 15 | 16 | The section [Examples](#examples) contains instructive code examples with explanations 17 | and can be used as a tutorial. 18 | 19 | See [Benchmarks](./benchmark.md) for informational benchmarking data. 20 | 21 | ## General Concepts 22 | 23 | `viktor` is a Kotlin + JNI library that revolves around a n-dimensional array concept 24 | similar to that of NumPy’s [ndarray](http://docs.scipy.org/doc/numpy/reference/arrays.ndarray.html). 25 | 26 | This array is described by the class `F64Array`. We call 1-dimensional arrays **vectors** 27 | and 2-dimensional ones **matrices**. Regardless of the number of dimensions, an `F64Array` 28 | always stores its data in an ordinary `DoubleArray` (`double[]` for Java speakers). 29 | The elements are laid out in row-major order (also called C-order). For example, 30 | for a 3-dimensional array `a` with the **shape** of 2x3x2, the elements will be laid out 31 | in the following order: 32 | 33 | a[0, 0, 0] === data[0] 34 | a[0, 0, 1] === data[1] 35 | a[0, 1, 0] === data[2] 36 | a[0, 1, 1] === data[3] 37 | a[0, 2, 0] === data[4] 38 | a[0, 2, 1] === data[5] 39 | a[1, 0, 0] === data[6] 40 | a[1, 0, 1] === data[7] 41 | a[1, 1, 0] === data[8] 42 | a[1, 1, 1] === data[9] 43 | a[1, 2, 0] === data[10] 44 | a[1, 2, 1] === data[11] 45 | 46 | It’s easy to see that the element `a[i, j, k]` corresponds to the element 47 | `data[6 * i + 2 * j + 1 * k]`. The values `6`, `2`, `1` are called _strides_, 48 | since they are the distances separating the elements with neighboring indices. 49 | 50 | Two distinct `F64Array`s may be supported by the same underlying `DoubleArray`. 51 | They can point to intersecting or non-intersecting portions of the underlying array. 52 | For example, by calling a viewer getter method `b = a.V[1]` we can obtain 53 | a view of the second 3x2 matrix slice of `a`: 54 | 55 | b[0, 0] === a[1, 0, 0] === data[6] 56 | b[0, 1] === a[1, 0, 1] === data[7] 57 | b[1, 0] === a[1, 1, 0] === data[8] 58 | b[1, 1] === a[1, 1, 1] === data[9] 59 | b[2, 0] === a[1, 2, 0] === data[10] 60 | b[2, 1] === a[1, 2, 1] === data[11] 61 | 62 | For all intents and purposes, `b` is an `F64Array` in its own right. 63 | It doesn't keep any reference to `a`. The element `b[i, j]` corresponds to the element 64 | `data[6 + 2 * i + j]`. Value `6` is called *offset*. Any in-place operation on `b` will 65 | be actually performed on the appropriate region of `data` and thus visible through `a`. 66 | 67 | ## Creating Arrays and Accessing Elements 68 | 69 | ### Accessing elements 70 | The elements can be accessed through standard get/set methods: 71 | ```kotlin 72 | println(a[1, 1, 1]) 73 | a[0, 2, 1] = 42.0 74 | ``` 75 | 76 | The only currently supported element type is `Double` (`double` in Java). 77 | 78 | ### Creating F64Arrays from scratch 79 | There is a number of ways to create an F64Array: 80 | ```kotlin 81 | val a = F64Array(2, 3, 2) // zero-filled 2x3x2 array 82 | val b = F64Array.full(2, 3, 2, init = 3.14) // 2x3x2 array filled with 3.14 83 | val c = F64Array(8, 8) { i, j -> if (i == j) 1.0 else 0.0 } // create an 8x8 unit matrix; this method is available for vectors, matrices and 3D arrays 84 | val d = F64Array.of(3.14, 2.78, 1.41) // creates a vector 85 | ``` 86 | 87 | ### Creating F64Arrays from Kotlin/Java arrays 88 | ```kotlin 89 | val a: DoubleArray = ... 90 | val b = a.asF64Array() // a wrapper method: creates a vector that uses [a] as data storage; only works for vectors 91 | val c: Array> = ... 92 | val d = c.toF64Array() // a copying method: allocates a [DoubleArray] for storage and copies the values from [g] to recreate its structure: g[i][j][k] == h[i, j, k]; only works for matrices and above 93 | ``` 94 | ### Retrieving Kotlin/Java array from F64Array 95 | ```kotlin 96 | val a: F64Array = ... 97 | val b: DoubleArray = a.data // retrieves the underlying storage [DoubleArray]; may or may not represent all of [b]’s elements 98 | val c: DoubleArray = a.toDoubleArray() // a copying method; converts the vector into a [DoubleArray]: a[i] == c[i]; only works for vectors 99 | val d: Array<*> = a.toGenericArray() // a copying method; converts the array to a corresponding Kotlin/Java structure ([Array] for matrices, [Array>] for 3D arrays etc.); only works for matrices and above 100 | val e: Any = a.toArray() // returns either a [DoubleArray] or an [Array<*>], depending on the number of dimensions of [a] 101 | ``` 102 | 103 | ## Views 104 | A **view** of an `F64Array` is another `F64Array` which uses the same `DoubleArray` 105 | for storage. For example, a matrix column is a view of the matrix. 106 | 107 | Each `F64Array` has a special property `V`, called **viewer**. It enables a more idiomatic way 108 | to obtain **views**. In this case, you can use a special `_I` object to signify 109 | "skip this axis", see the examples below. 110 | 111 | ```kotlin 112 | val a = F64Array(2, 3, 2) // 2x3x2 array 113 | val b = a.view(1) // view the second 3x2 matrix slice of [a]: b[i, j] === a[1, i, j] 114 | val b1 = a.V[1] // another way of doing the same 115 | val c = a.view(0, 1) // view a 2x2 matrix such that c[i, j] === a[i, 0, j] 116 | val c1 = a.V[_I, 0] // another way of doing the same; [_I] indicates that the first axis should be skipped 117 | val d = a.view(1, 2) // view a 2x3 matrix such that d[i, i] === a[i, j, 1] 118 | val e = a.along(1) // a sequence of 2x2 views: a.view(0, 1), a.view(1, 1), a.view(2, 1) 119 | ``` 120 | 121 | The viewer also has setter methods: 122 | 123 | ```kotlin 124 | a.V[1] = d // copies the contents of [d] into the second 3x2 matrix slice of [a] 125 | a.V[_I, 0] = 42.0 // replaces the corresponding elements of [a] with the value 42.0 126 | a.V[_I] = 3.14 // replaces all elements of [a] with the value 3.14 127 | ``` 128 | 129 | More sophisticated views can be generated by **slicing**: 130 | 131 | ```kotlin 132 | val f = a.slice(from = 0, to = 2, axis = 1) // f[i, j, k] === a[i, j, k], but [f] has a shape 2x2x2 133 | val g = a.slice(from = 1, to = 3, axis = 1) // g[i, j, k] === a[i, j + 1, k], and [g] has a shape 2x2x2 134 | val h = a.slice(from = 0, step = 2) // h[i, 0, k] === a[i, 0, k], h[i, 1, k] === a[i, 2, k] 135 | ``` 136 | 137 | ## Flattenable and Dense Arrays 138 | Let's explore some views from the [previous section](#views) in detail: 139 | ```kotlin 140 | val a = F64Array(2, 3, 2) // 2x3x2 array 141 | val b = a.view(1) // view the second 3x2 matrix slice of [a]: b[i, j] === a[1, i, j] 142 | val c = a.view(0, 1) // view a 2x2 matrix such that c[i, j] === a[i, 0, j] 143 | val d = a.view(1, 2) // view a 2x3 matrix such that d[i, i] === a[i, j, 1] 144 | ``` 145 | 146 | These three views have different properties. 147 | 148 | b[0, 0] === a[1, 0, 0] === data[6] 149 | b[0, 1] === a[1, 0, 1] === data[7] 150 | b[1, 0] === a[1, 1, 0] === data[8] 151 | b[1, 1] === a[1, 1, 1] === data[9] 152 | b[2, 0] === a[1, 2, 0] === data[10] 153 | b[2, 1] === a[1, 2, 1] === data[11] 154 | 155 | `b` owns a contiguous region of `data`. It is thus called **dense**. 156 | 157 | d[0, 0] === a[0, 0, 1] === data[1] 158 | d[0, 1] === a[0, 1, 1] === data[3] 159 | d[0, 2] === a[0, 2, 1] === data[5] 160 | d[1, 0] === a[1, 0, 1] === data[7] 161 | d[1, 1] === a[1, 1, 1] === data[9] 162 | d[1, 2] === a[1, 2, 1] === data[11] 163 | 164 | `d` doesn't own a contiguous region of `data`, but its entries are equidistant. 165 | This means there is a 6-element vector that owns the exact same elements as `d`. 166 | This vector can be obtained by calling `flatten()`: 167 | ```kotlin 168 | val e = d.flatten() 169 | ``` 170 | 171 | e[0] === d[0, 0] === a[0, 0, 1] === data[1] 172 | e[1] === d[0, 1] === a[0, 1, 1] === data[3] 173 | e[2] === d[0, 2] === a[0, 2, 1] === data[5] 174 | e[3] === d[1, 0] === a[1, 0, 1] === data[7] 175 | e[4] === d[1, 1] === a[1, 1, 1] === data[9] 176 | e[5] === d[1, 2] === a[1, 2, 1] === data[11] 177 | 178 | `b` and `d` are thus both **flattenable** arrays. It can be confirmed by looking 179 | at the property `isFlattenable`. 180 | 181 | c[0, 0] === a[0, 0, 0] === data[0] 182 | c[0, 1] === a[0, 0, 1] === data[1] 183 | c[1, 0] === a[1, 0, 0] === data[6] 184 | c[1, 1] === a[1, 0, 1] === data[7] 185 | 186 | `c` doesn't own a contiguous region of `data`, and its entries are not equidistant. 187 | It is thus **not flattenable**. `c.isFlattenable` will return `false`, and `c.flatten()` 188 | will fail with an exception. 189 | 190 | Flattenable arrays (especially dense ones) generally allow a more efficient traversal, 191 | since all their elements can be accessed in a single loop. 192 | 193 | ### Playing with shape 194 | Flattenable arrays can be easily **reshaped**, provided that their number of elements 195 | stays the same. The reshaped array is a view of the original; it can access 196 | the same elements. 197 | 198 | ```kotlin 199 | val a = F64Array(2, 3, 2) 200 | val b = a.reshape(12) // the same as a.flatten() 201 | val c = a.reshape(2, 6) // essentially flattens the 3x2 matrix slices into 6-element vectors 202 | val d = a.reshape(3, 4) // no elegant interpretation, just the same elements in the same order 203 | ``` 204 | 205 | ## Copying 206 | If desired, you may create a **copy** of an array that is completely separate 207 | from the original. The changes to the copy won't propagate to the original (and vice versa), 208 | because the underlying storage arrays will be different. 209 | 210 | ```kotlin 211 | val a = F64Array.full(2, 3, 2, init = 2.78) 212 | val b = a.copy() 213 | b.fill(3.14) // doesn't affect [a] 214 | ``` 215 | 216 | You can also copy the array contents to another array of the same shape: 217 | 218 | ```kotlin 219 | b.copyTo(a) 220 | a.V[_I] = b // has the exact same effect 221 | ``` 222 | 223 | The copy will always be dense (see [Flattenable and Dense Arrays](#flattenable-and-dense-arrays)), 224 | even if the original was not. 225 | 226 | ## Arithmetic and mathematics 227 | If two arrays have the same exact shape, you can use ordinary arithmetic operators on them: 228 | ```kotlin 229 | val a = F64Array.full(2, 3, 2, init = 2.78) 230 | val b = F64Array.full(2, 3, 2, init = 3.14) 231 | val c = a + b // elementwise addition 232 | val d = a / b // elementwise division 233 | ``` 234 | 235 | These operators are **copying**; they will return a new 2x3x2 array with sums or ratios 236 | respectively. 237 | 238 | ```kotlin 239 | a -= b // in-place elementwise subtraction 240 | b *= b // in-place elementwise multiplication 241 | ``` 242 | 243 | These operators are **in-place**; they will modify the elements of the left part. 244 | 245 | You can also use the verbose methods, for example: 246 | 247 | ```kotlin 248 | val c = a.div(b) 249 | b.timesAssign(b) 250 | ``` 251 | 252 | You can also do the same operations with scalar values, i.e. `Double`s: 253 | 254 | ```kotlin 255 | val e = a * 42.0 256 | val f = 1.0 / b // yes, this works! we have an extension method Double.div(F64Array)! 257 | ``` 258 | 259 | For more sophisticated examples, you have (equally elementwise) mathematical methods 260 | at your disposal: 261 | 262 | ```kotlin 263 | val g = a.exp() // elementwise natural exponent, copying 264 | g.logInPlace() // elementwise natural logarithm, in-place 265 | ``` 266 | 267 | The operations `exp`, `expm1`, `log`, `log1p` are available as both copying 268 | and in-place methods. 269 | 270 | For vectors, we have a scalar multiplication method `dot()`: 271 | 272 | ```kotlin 273 | val v1 = F64Array(1000) 274 | val v2 = F64Array(1000) 275 | val v1v2: Double = v1.dot(v2) 276 | ``` 277 | 278 | ## Statistics and Special Methods 279 | You can calculate some statistical values: 280 | ```kotlin 281 | val a = F64Array(2, 3, 2) 282 | val s: Double = a.sum() // what it says 283 | val m: Double = a.mean() // arithmetic mean 284 | val sd: Double = a.sd() // standard deviation 285 | val aMax = a.max() // maximum value 286 | ``` 287 | 288 | For a vector, there is an in-place method for calculating cumulative sums: 289 | 290 | ```kotlin 291 | val a = F64Array(1000) 292 | a.cumSum() // after this, each element a[n] is equal to the sum a[0] + … + a[n] of the original values of [a] 293 | ``` 294 | 295 | Also for a vector, you can get the index of the maximum / minimum element 296 | using `argMax()` / `argMin()` respectively, as well as an arbitrary quantile: 297 | 298 | ```kotlin 299 | val median = a.quantile(0.5) // note that this is a destructive method, which will shuffle the array 300 | ``` 301 | 302 | You can rescale an array so that its entries sum to one with an in-place method `rescale()`: 303 | 304 | ```kotlin 305 | val a = F64Array.of(3.14, 2.78) 306 | a.rescale() // equivalent to a /= a.sum() but more efficient and precise 307 | ``` 308 | 309 | ## Logarithmic Storage 310 | In statistics and machine learning, it’s frequently useful to store the logarithms of values 311 | instead of the values themselves (e.g. when dealing with distribution density). 312 | The logarithms make multiplication and division easy (just add or subtract the logarithms), 313 | but hinder the summation. To this end, we have a special infix method `logAddExp`: 314 | 315 | ```kotlin 316 | val logA = F64Array(2, 3, 2, init = ln(3.14)) 317 | val logB = F64Array(2, 3, 2, init = ln(2.78)) 318 | val logC = logA logAddExp logB 319 | ``` 320 | 321 | Like the name suggests, `logAddExp` is equivalent to writing 322 | `(logA.exp() + logB.exp()).log()` but generally more precise and efficient 323 | and less memory-consuming. There's also an in-place variant, `logA.logAddExpAssign(logB)`. 324 | 325 | You can also sum the entire logarithmically stored array: 326 | 327 | ```kotlin 328 | val logSum: Double = logA.logSumExp() 329 | ``` 330 | 331 | This is again equivalent to (but better than) `ln(logA.exp().sum())`. 332 | 333 | There is also a version of `rescale()` for logarithmically stored arrays: 334 | 335 | ```kotlin 336 | logA.logRescale() // equivalent to a -= a.logSumExp(), but more efficient and precise 337 | ``` 338 | 339 | ## Efficiency and SIMD 340 | ### Excessive Copying 341 | You might want to keep the number of copying events to a minimum, 342 | especially when working with very large arrays. Copying requires allocation and 343 | (later) disposal of large amounts of memory. 344 | 345 | There is one general tip for less copying: use in-place operations whenever possible. 346 | For example, while 347 | 348 | ```kotlin 349 | val b = (a + 1.0) / 2.0 350 | ``` 351 | 352 | is equivalent to 353 | 354 | ```kotlin 355 | val b = a + 1.0 356 | b /= 2.0 357 | ``` 358 | 359 | the latter approach avoids one copy allocation and disposal by reusing an array. 360 | The effect is even more pronounced with a longer operation chain. 361 | 362 | ### Efficient Traversal 363 | Flattenable arrays can be traversed in a single loop, so they naturally enjoy 364 | faster computations. Try to avoid large non-flattenable arrays. 365 | Moreover, dense arrays benefit from native SIMD optimizations and faster copying, 366 | so these are the natural choice for efficient calculations. 367 | See [Flattenable and Dense Arrays](#flattenable-and-dense-arrays) for more details. 368 | 369 | ### SIMD 370 | `SIMD` stands for "single instruction, multiple data". It’s a broad family 371 | of CPU instruction set extensions, including `MMX`, `SSE`, `AVX` and others. 372 | These extensions allow using extended CPU registers containing multiple data units. 373 | For example, `AVX` defines 256-bit registers that can operate on 374 | four 64-bit floating-point numbers with one instruction. 375 | 376 | `viktor` supports SIMD extensions by providing a set of binary JNI libraries 377 | built specifically for several target platforms and extension sets. 378 | We build and include the following libraries for `amd64` (also called `x86_64`) 379 | instruction set extensions: 380 | 381 | amd64 (x86_64) | SSE2 | AVX 382 | ---------------|------|---- 383 | Windows | + | + 384 | Linux | + | + 385 | macOS | + | + 386 | 387 | These libraries are only compatible with 64-bit JVM. In all other cases, 388 | `viktor` doesn't use JNI, opting for Kotlin/Java operations instead. 389 | 390 | ## Examples 391 | 392 | ### Factorials 393 | 394 | Suppose you want a 1000-element vector `f` such that `f[i] == i!`, i.e. 395 | a vector of factorials. Is there an efficient way to do this with `viktor`? Of course! 396 | 397 | ```kotlin 398 | val f = F64Array(1000) { it.toDouble() } // fill vector f with values so that f[i] == i 399 | f[0] = 1.0 // gotta deal with that edge case 400 | f.logInPlace() // replace all values with their natural logarithms; f[i] == log(i) for i > 0 401 | f.cumSum() // replace all values with cumulative sums; f[i] == sum_{k=1}^i log(k) for i > 0 402 | f.expInPlace() // replace all values with their exponents; f[i] == exp(sum_{k=1}^i log(k)) == prod_{k=1}^i k == i! 403 | ``` 404 | 405 | Bingo! You have a vector of factorials. Sadly, most of these are equal to positive infinity 406 | due to floating-point overflow. You might consider skipping the last exponentiation 407 | and keeping the logarithms of the factorials, which are much less prone to overflow. 408 | See [Logarithmic Storage](#logarithmic-storage) for more details. 409 | 410 | ### Gaussian Density 411 | 412 | Suppose you have a vector `o` of *i.i.d.* random observations, and you want to calculate 413 | the vector of probability density `d` under the standard normal distribution `N(0, 1)`. 414 | Since the density formula is `p(x) = sqrt(2 * pi) * exp(-x^2/2)`, we can do the following: 415 | 416 | ```kotlin 417 | val d = sqrt(2 * PI) * (- o * o / 2.0).exp() 418 | ``` 419 | 420 | This produces the desired results, but is not very efficient: each of the `*`, `/`, `-`, 421 | `exp` and `*` creates a copy, only the last of which is retained as `d`. 422 | That’s four copies too many. Let’s consider a better approach: 423 | 424 | ```kotlin 425 | val d = o * o // d == o^2 426 | d /= -2.0 // d == -o^2 / 2 427 | d.expInPlace() // d == exp(-o^2 / 2) 428 | d *= sqrt(2 * PI) // d == sqrt(2 * pi) * exp(-o^2 / 2) 429 | ``` 430 | 431 | Voila! No extra copies created. However, if some observations have large absolute values, 432 | the exponent might underflow, leaving us with a zero density. It’s thus better to store 433 | and operate on log densities! Since `log p(x) = 1/2 log (2 * pi) - x^2 / 2`, we can write: 434 | 435 | ```kotlin 436 | val logD = o * o 437 | logD /= -2.0 438 | logD += 0.5 * ln(2 * PI) 439 | ``` 440 | 441 | What is the total log likelihood of `o` under the standard normal distribution `N(0, 1)`? 442 | Why, it’s `logD.sum()`, naturally. (Since likelihood is a product of densities, 443 | log likelihood is a sum of log densities.) 444 | 445 | ### Gaussian mixture 446 | 447 | We now suspect our observation vector `o` actually came from a uniform mixture 448 | of two normal distributions, `N(0, 1)` and `N(3, 1)`. What is the total likelihood now? 449 | What is the most probable sequence of components? 450 | 451 | Let's create a matrix `logP` to hold the probabilities of observations 452 | belonging to components: `logP[i, j] = log(P(o_j, c_j = i))`, where `c_j` is the mixture 453 | component responsible for the `j`th observation (either `0` or `1`). Using conditional 454 | probabilities: 455 | 456 | P(o_j, c_j = i) = P(o_j | c_j = i) * P(c_j = i) 457 | 458 | Let's also try to use array-wide operations as much as possible. 459 | 460 | ```kotlin 461 | val oColumn = o.reshape(1, o.size) // create a 1x1000 matrix view of [o] 462 | val logP = F64Array.concatenate(oColumn, oColumn) // concatenate into a 2x1000 array; logP[i, j] == o_j 463 | logP.V[1] -= 3.0 // logP[i, j] == o_j - μ_i 464 | logP *= logP // logP[i, j] == (o_j - μ_i) ^ 2 465 | logP /= -2.0 // logP[i, j] == - (o_j - μ_i) ^ 2 / 2 466 | logP += 0.5 * ln(2 * PI) + ln(0.5) // logP[i, j] == - (o_j - μ_i) ^ 2 / 2 + 1/2 log(2 * pi) + log(1/2) == log(P(o_j | c_j = i) * P(c_j = i)) 467 | ``` 468 | 469 | Now we can answer our questions. The likelihood of an observation is 470 | 471 | P(o_j) = Σ_i P(o_j, c_j = i) 472 | 473 | thus we can easily obtain a vector of log-likelihoods by log-summing the columns of `logP`: 474 | 475 | ```kotlin 476 | val logL = logP.V[0] logAddExp logP.V[1] 477 | ``` 478 | 479 | The total log-likelihood is equal to `logL.sum()`, like in the previous example. 480 | 481 | To obtain the most probable sequence of mixture components, we just need to pick the one 482 | with the greatest probability for each observation: 483 | 484 | ```kotlin 485 | val components = logP.along(1).map { it.argMax() } // a sequence of 0s and 1s 486 | ``` 487 | 488 | This generates a sequence of matrix rows (`along(1)`) and picks the maximum element 489 | index for each row. -------------------------------------------------------------------------------- /docs/figures/DotBenchmark_arrayopss_server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains-Research/viktor/b1c49ca58bc29e645eb8a45ed2d3488cadb67917/docs/figures/DotBenchmark_arrayopss_server.png -------------------------------------------------------------------------------- /docs/figures/DotBenchmark_arrayopss_workstation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains-Research/viktor/b1c49ca58bc29e645eb8a45ed2d3488cadb67917/docs/figures/DotBenchmark_arrayopss_workstation.png -------------------------------------------------------------------------------- /docs/figures/DotBenchmark_flops_server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains-Research/viktor/b1c49ca58bc29e645eb8a45ed2d3488cadb67917/docs/figures/DotBenchmark_flops_server.png -------------------------------------------------------------------------------- /docs/figures/DotBenchmark_flops_workstation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains-Research/viktor/b1c49ca58bc29e645eb8a45ed2d3488cadb67917/docs/figures/DotBenchmark_flops_workstation.png -------------------------------------------------------------------------------- /docs/figures/ExpBenchmark_arrayopss_server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains-Research/viktor/b1c49ca58bc29e645eb8a45ed2d3488cadb67917/docs/figures/ExpBenchmark_arrayopss_server.png -------------------------------------------------------------------------------- /docs/figures/ExpBenchmark_arrayopss_workstation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains-Research/viktor/b1c49ca58bc29e645eb8a45ed2d3488cadb67917/docs/figures/ExpBenchmark_arrayopss_workstation.png -------------------------------------------------------------------------------- /docs/figures/ExpBenchmark_flops_server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains-Research/viktor/b1c49ca58bc29e645eb8a45ed2d3488cadb67917/docs/figures/ExpBenchmark_flops_server.png -------------------------------------------------------------------------------- /docs/figures/ExpBenchmark_flops_workstation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains-Research/viktor/b1c49ca58bc29e645eb8a45ed2d3488cadb67917/docs/figures/ExpBenchmark_flops_workstation.png -------------------------------------------------------------------------------- /docs/figures/Expm1Benchmark_arrayopss_server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains-Research/viktor/b1c49ca58bc29e645eb8a45ed2d3488cadb67917/docs/figures/Expm1Benchmark_arrayopss_server.png -------------------------------------------------------------------------------- /docs/figures/Expm1Benchmark_arrayopss_workstation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains-Research/viktor/b1c49ca58bc29e645eb8a45ed2d3488cadb67917/docs/figures/Expm1Benchmark_arrayopss_workstation.png -------------------------------------------------------------------------------- /docs/figures/Expm1Benchmark_flops_server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains-Research/viktor/b1c49ca58bc29e645eb8a45ed2d3488cadb67917/docs/figures/Expm1Benchmark_flops_server.png -------------------------------------------------------------------------------- /docs/figures/Expm1Benchmark_flops_workstation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains-Research/viktor/b1c49ca58bc29e645eb8a45ed2d3488cadb67917/docs/figures/Expm1Benchmark_flops_workstation.png -------------------------------------------------------------------------------- /docs/figures/Log1pBenchmark_arrayopss_server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains-Research/viktor/b1c49ca58bc29e645eb8a45ed2d3488cadb67917/docs/figures/Log1pBenchmark_arrayopss_server.png -------------------------------------------------------------------------------- /docs/figures/Log1pBenchmark_arrayopss_workstation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains-Research/viktor/b1c49ca58bc29e645eb8a45ed2d3488cadb67917/docs/figures/Log1pBenchmark_arrayopss_workstation.png -------------------------------------------------------------------------------- /docs/figures/Log1pBenchmark_flops_server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains-Research/viktor/b1c49ca58bc29e645eb8a45ed2d3488cadb67917/docs/figures/Log1pBenchmark_flops_server.png -------------------------------------------------------------------------------- /docs/figures/Log1pBenchmark_flops_workstation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains-Research/viktor/b1c49ca58bc29e645eb8a45ed2d3488cadb67917/docs/figures/Log1pBenchmark_flops_workstation.png -------------------------------------------------------------------------------- /docs/figures/LogAddExpBenchmark_arrayopss_server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains-Research/viktor/b1c49ca58bc29e645eb8a45ed2d3488cadb67917/docs/figures/LogAddExpBenchmark_arrayopss_server.png -------------------------------------------------------------------------------- /docs/figures/LogAddExpBenchmark_arrayopss_workstation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains-Research/viktor/b1c49ca58bc29e645eb8a45ed2d3488cadb67917/docs/figures/LogAddExpBenchmark_arrayopss_workstation.png -------------------------------------------------------------------------------- /docs/figures/LogAddExpBenchmark_flops_server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains-Research/viktor/b1c49ca58bc29e645eb8a45ed2d3488cadb67917/docs/figures/LogAddExpBenchmark_flops_server.png -------------------------------------------------------------------------------- /docs/figures/LogAddExpBenchmark_flops_workstation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains-Research/viktor/b1c49ca58bc29e645eb8a45ed2d3488cadb67917/docs/figures/LogAddExpBenchmark_flops_workstation.png -------------------------------------------------------------------------------- /docs/figures/LogBenchmark_arrayopss_server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains-Research/viktor/b1c49ca58bc29e645eb8a45ed2d3488cadb67917/docs/figures/LogBenchmark_arrayopss_server.png -------------------------------------------------------------------------------- /docs/figures/LogBenchmark_arrayopss_workstation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains-Research/viktor/b1c49ca58bc29e645eb8a45ed2d3488cadb67917/docs/figures/LogBenchmark_arrayopss_workstation.png -------------------------------------------------------------------------------- /docs/figures/LogBenchmark_flops_server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains-Research/viktor/b1c49ca58bc29e645eb8a45ed2d3488cadb67917/docs/figures/LogBenchmark_flops_server.png -------------------------------------------------------------------------------- /docs/figures/LogBenchmark_flops_workstation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains-Research/viktor/b1c49ca58bc29e645eb8a45ed2d3488cadb67917/docs/figures/LogBenchmark_flops_workstation.png -------------------------------------------------------------------------------- /docs/figures/LogSumExpBenchmark_arrayopss_server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains-Research/viktor/b1c49ca58bc29e645eb8a45ed2d3488cadb67917/docs/figures/LogSumExpBenchmark_arrayopss_server.png -------------------------------------------------------------------------------- /docs/figures/LogSumExpBenchmark_arrayopss_workstation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains-Research/viktor/b1c49ca58bc29e645eb8a45ed2d3488cadb67917/docs/figures/LogSumExpBenchmark_arrayopss_workstation.png -------------------------------------------------------------------------------- /docs/figures/LogSumExpBenchmark_flops_server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains-Research/viktor/b1c49ca58bc29e645eb8a45ed2d3488cadb67917/docs/figures/LogSumExpBenchmark_flops_server.png -------------------------------------------------------------------------------- /docs/figures/LogSumExpBenchmark_flops_workstation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains-Research/viktor/b1c49ca58bc29e645eb8a45ed2d3488cadb67917/docs/figures/LogSumExpBenchmark_flops_workstation.png -------------------------------------------------------------------------------- /docs/figures/SDBenchmark_arrayopss_server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains-Research/viktor/b1c49ca58bc29e645eb8a45ed2d3488cadb67917/docs/figures/SDBenchmark_arrayopss_server.png -------------------------------------------------------------------------------- /docs/figures/SDBenchmark_arrayopss_workstation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains-Research/viktor/b1c49ca58bc29e645eb8a45ed2d3488cadb67917/docs/figures/SDBenchmark_arrayopss_workstation.png -------------------------------------------------------------------------------- /docs/figures/SDBenchmark_flops_server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains-Research/viktor/b1c49ca58bc29e645eb8a45ed2d3488cadb67917/docs/figures/SDBenchmark_flops_server.png -------------------------------------------------------------------------------- /docs/figures/SDBenchmark_flops_workstation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains-Research/viktor/b1c49ca58bc29e645eb8a45ed2d3488cadb67917/docs/figures/SDBenchmark_flops_workstation.png -------------------------------------------------------------------------------- /docs/figures/SumBenchmark_arrayopss_server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains-Research/viktor/b1c49ca58bc29e645eb8a45ed2d3488cadb67917/docs/figures/SumBenchmark_arrayopss_server.png -------------------------------------------------------------------------------- /docs/figures/SumBenchmark_arrayopss_workstation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains-Research/viktor/b1c49ca58bc29e645eb8a45ed2d3488cadb67917/docs/figures/SumBenchmark_arrayopss_workstation.png -------------------------------------------------------------------------------- /docs/figures/SumBenchmark_flops_server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains-Research/viktor/b1c49ca58bc29e645eb8a45ed2d3488cadb67917/docs/figures/SumBenchmark_flops_server.png -------------------------------------------------------------------------------- /docs/figures/SumBenchmark_flops_workstation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains-Research/viktor/b1c49ca58bc29e645eb8a45ed2d3488cadb67917/docs/figures/SumBenchmark_flops_workstation.png -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | version=1.2.0 -------------------------------------------------------------------------------- /gradle/boost-simd.gradle: -------------------------------------------------------------------------------- 1 | ext { 2 | boostSimdBranch = 'v4.16.9.2' 3 | boostSimdRoot = '4.16.9.2' 4 | def os = org.gradle.internal.os.OperatingSystem.current() 5 | if (os.isMacOsX()) { 6 | cmakeURL = "https://cmake.org/files/v3.12/cmake-3.12.0-Darwin-x86_64.tar.gz" 7 | cmakeFile = "cmake-3.12.0.tar.gz" 8 | cmakeFolder = "cmake-3.12.0-Darwin-x86_64/CMake.app/Contents/bin" 9 | cmakeTarGz = true 10 | } else if (os.isWindows()) { 11 | cmakeURL = "https://cmake.org/files/v3.12/cmake-3.12.0-win64-x64.zip" 12 | cmakeFile = "cmake-3.12.0.zip" 13 | cmakeFolder = "cmake-3.12.0-win64-x64/bin" 14 | cmakeTarGz = false 15 | } else { 16 | // assume it's Linux 17 | cmakeURL = "https://cmake.org/files/v3.12/cmake-3.12.0-Linux-x86_64.tar.gz" 18 | cmakeFile = "cmake-3.12.0.tar.gz" 19 | cmakeFolder = "cmake-3.12.0-Linux-x86_64/bin" 20 | cmakeTarGz = true 21 | } 22 | } 23 | 24 | buildscript { 25 | repositories { 26 | jcenter() 27 | } 28 | 29 | dependencies { 30 | classpath 'de.undercouch:gradle-download-task:3.4.3' 31 | } 32 | } 33 | 34 | apply plugin: de.undercouch.gradle.tasks.download.DownloadTaskPlugin 35 | 36 | model { 37 | repositories { 38 | libs(PrebuiltLibraries) { 39 | boostSimd { 40 | // This doesn't work yet. See 41 | // https://discuss.gradle.org/t/prebuilt-libraries-two-things-i-would-find-useful 42 | // generatedBy installBoostSimd 43 | 44 | headers.srcDir "$buildDir/include" 45 | } 46 | } 47 | } 48 | } 49 | 50 | task downloadCmake(type: de.undercouch.gradle.tasks.download.Download) { 51 | src cmakeURL 52 | dest new File(buildDir, cmakeFile) 53 | acceptAnyCertificate true // for some reason cmake.org certificate can't be verified 54 | overwrite false 55 | } 56 | 57 | task unzipCmake(dependsOn: downloadCmake, type: Copy) { 58 | if (cmakeTarGz) { 59 | from tarTree(resources.gzip(downloadCmake.dest)) 60 | } else { 61 | from zipTree(downloadCmake.dest) 62 | } 63 | into buildDir 64 | outputs.dir new File(buildDir, cmakeFolder) 65 | } 66 | 67 | task downloadBoostSimd(type: de.undercouch.gradle.tasks.download.Download) { 68 | src "https://github.com/JetBrains-Research/boost.simd/archive/refs/tags/${boostSimdBranch}.zip" 69 | dest new File(buildDir, "boost.simd.${boostSimdBranch}.zip") 70 | overwrite false 71 | } 72 | 73 | task unzipBoostSimd(dependsOn: downloadBoostSimd, type: Copy) { 74 | from zipTree(downloadBoostSimd.dest) 75 | into buildDir 76 | outputs.dir new File(buildDir, "boost.simd-$boostSimdRoot") 77 | } 78 | 79 | def nullOutputStream = new OutputStream() { 80 | @Override 81 | public void write(int b) {} 82 | } 83 | 84 | task installBoostSimd(dependsOn: [unzipBoostSimd, unzipCmake]) { 85 | def cmakeDir = new File( 86 | buildDir, "boost.simd-$boostSimdRoot/build") 87 | def cmakeExec = "$buildDir/$cmakeFolder/cmake" 88 | outputs.dir new File(buildDir, 'include/boost') 89 | doLast { 90 | cmakeDir.mkdirs() 91 | 92 | exec { 93 | workingDir cmakeDir 94 | def args = [cmakeExec, '..', '-DUSE_SELF_BOOST=1', 95 | "-DCMAKE_INSTALL_PREFIX=$buildDir"] 96 | def os = org.gradle.internal.os.OperatingSystem.current() 97 | if (os.isWindows()) { 98 | args += ['-G', '"Visual Studio 14 2015"'] 99 | } 100 | commandLine(args) 101 | } 102 | exec { 103 | workingDir cmakeDir 104 | standardOutput = nullOutputStream 105 | commandLine(cmakeExec, '--build', '.', '--target', 'update.boost-header-only') 106 | } 107 | exec { 108 | workingDir cmakeDir 109 | standardOutput = nullOutputStream 110 | commandLine(cmakeExec, '--build', '.', '--target', 'install') 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /gradle/disable-static.gradle: -------------------------------------------------------------------------------- 1 | model { 2 | components { 3 | all { 4 | binaries.withType(StaticLibraryBinarySpec) { 5 | buildable = false 6 | } 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /gradle/jni-headers.gradle: -------------------------------------------------------------------------------- 1 | model { 2 | repositories { 3 | libs(PrebuiltLibraries) { 4 | jniHeaders { 5 | headers.srcDir "$buildDir/include" 6 | } 7 | } 8 | } 9 | 10 | components { 11 | 12 | all { 13 | binaries.all { 14 | def javaHome = org.gradle.internal.jvm.Jvm.current().javaHome 15 | def os = targetPlatform.operatingSystem 16 | if (os.linux) { 17 | cppCompiler.args "-I$javaHome/include" 18 | cppCompiler.args "-I$javaHome/include/linux" 19 | } else if (os.macOsX) { 20 | cppCompiler.args "-I$javaHome/include" 21 | cppCompiler.args "-I$javaHome/include/darwin" 22 | } else if (os.windows) { 23 | cppCompiler.args "/I$javaHome/include" 24 | cppCompiler.args "/I$javaHome/include/win32" 25 | } 26 | } 27 | } 28 | } 29 | } 30 | 31 | ext.javah = { jniClass, jniHeader -> 32 | exec { 33 | executable org.gradle.internal.jvm.Jvm.current() 34 | .getExecutable('javah') 35 | args '-classpath', tasks.compileKotlin.destinationDir 36 | args '-o', jniHeader 37 | args jniClass 38 | } 39 | 40 | assert jniHeader.exists() 41 | println "Generated $jniHeader" 42 | } 43 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains-Research/viktor/b1c49ca58bc29e645eb8a45ed2d3488cadb67917/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto init 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto init 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :init 68 | @rem Get command-line arguments, handling Windows variants 69 | 70 | if not "%OS%" == "Windows_NT" goto win9xME_args 71 | 72 | :win9xME_args 73 | @rem Slurp the command line arguments. 74 | set CMD_LINE_ARGS= 75 | set _SKIP=2 76 | 77 | :win9xME_args_slurp 78 | if "x%~1" == "x" goto execute 79 | 80 | set CMD_LINE_ARGS=%* 81 | 82 | :execute 83 | @rem Setup the command line 84 | 85 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 86 | 87 | 88 | @rem Execute Gradle 89 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 90 | 91 | :end 92 | @rem End local scope for the variables with windows NT shell 93 | if "%ERRORLEVEL%"=="0" goto mainEnd 94 | 95 | :fail 96 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 97 | rem the _cmd.exe /c_ return code! 98 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 99 | exit /b 1 100 | 101 | :mainEnd 102 | if "%OS%"=="Windows_NT" endlocal 103 | 104 | :omega 105 | -------------------------------------------------------------------------------- /src/loader/cpp/org_jetbrains_bio_viktor_LoaderKt.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "org_jetbrains_bio_viktor_LoaderKt.hpp" 4 | 5 | #define JNI_METHOD(rtype, name) \ 6 | JNIEXPORT rtype JNICALL Java_org_jetbrains_bio_viktor_LoaderKt_##name 7 | 8 | JNI_METHOD(jboolean, isAvxSupported)(JNIEnv *env, jclass) { 9 | return boost::simd::avx.is_supported(); 10 | } 11 | 12 | JNI_METHOD(jboolean, isSse2Supported)(JNIEnv *env, jclass) { 13 | return boost::simd::sse2.is_supported(); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jetbrains/bio/viktor/DoubleExtensions.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("nothing_to_inline") 2 | 3 | package org.jetbrains.bio.viktor 4 | 5 | /** 6 | * Operator overloads for [Double] and [F64Array]. 7 | * 8 | * @since 0.2.2 9 | */ 10 | 11 | inline operator fun Double.minus(other: F64Array): F64Array = other.transform { this - it } 12 | 13 | inline operator fun Double.plus(other: F64Array) = other + this 14 | 15 | inline operator fun Double.times(other: F64Array) = other * this 16 | 17 | inline operator fun Double.div(other: F64Array): F64Array = other.transform { this / it } -------------------------------------------------------------------------------- /src/main/kotlin/org/jetbrains/bio/viktor/F64DenseFlatArray.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.bio.viktor 2 | 3 | /** 4 | * A contiguous vector. 5 | * 6 | * @author Sergei Lebedev 7 | * @since 0.1.0 8 | */ 9 | internal sealed class F64DenseFlatArray( 10 | data: DoubleArray, 11 | offset: Int, 12 | size: Int 13 | ) : F64FlatArray(data, offset, 1, size) { 14 | 15 | override fun fill(init: Double) = data.fill(init, offset, offset + length) 16 | 17 | override fun copy(): F64FlatArray { 18 | val copyData = DoubleArray(length) 19 | System.arraycopy(data, offset, copyData, 0, length) 20 | return create(copyData, 0, length) 21 | } 22 | 23 | override fun copyTo(other: F64Array) { 24 | if (other is F64DenseFlatArray) { 25 | checkShape(other) 26 | System.arraycopy(data, offset, other.data, other.offset, length) 27 | } else { 28 | super.copyTo(other) 29 | } 30 | } 31 | 32 | override fun clone(): F64DenseFlatArray = create(data.clone(), offset, length) 33 | 34 | private inline fun denseTransformInPlace(op: (Double) -> Double) { 35 | val dst = data 36 | var dstOffset = offset 37 | val dstEnd = dstOffset + length 38 | while (dstOffset < dstEnd) { 39 | dst[dstOffset] = op.invoke(dst[dstOffset]) 40 | dstOffset++ 41 | } 42 | } 43 | 44 | private inline fun denseTransform(op: (Double) -> Double): F64FlatArray { 45 | val dst = DoubleArray(length) 46 | val src = data 47 | var srcOffset = offset 48 | val length = length 49 | if (srcOffset == 0) { 50 | for (i in 0 until length) { 51 | dst[i] = op.invoke(src[i]) 52 | } 53 | } else { 54 | for (i in 0 until length) { 55 | dst[i] = op.invoke(src[srcOffset]) 56 | srcOffset++ 57 | } 58 | } 59 | return create(dst, 0, this.length) 60 | } 61 | 62 | private inline fun denseEBEInPlace(other: F64DenseFlatArray, op: (Double, Double) -> Double) { 63 | val dst = data 64 | val src = other.data 65 | var dstOffset = offset 66 | var srcOffset = other.offset 67 | val length = length 68 | if (dstOffset == 0 && srcOffset == 0) { 69 | for (i in 0 until length) { 70 | dst[i] = op.invoke(dst[i], src[i]) 71 | } 72 | } else { 73 | val dstEnd = dstOffset + length 74 | while (dstOffset < dstEnd) { 75 | dst[dstOffset] = op.invoke(dst[dstOffset], src[srcOffset]) 76 | dstOffset++ 77 | srcOffset++ 78 | } 79 | } 80 | } 81 | 82 | private inline fun ebeInPlace( 83 | other: F64Array, 84 | op: (Double, Double) -> Double, 85 | superOp: F64Array.(F64Array) -> Unit 86 | ) { 87 | if (other is F64DenseFlatArray) { 88 | checkShape(other) 89 | denseEBEInPlace(other, op) 90 | } else { 91 | this.superOp(other) 92 | } 93 | } 94 | 95 | private inline fun denseEBE(other: F64DenseFlatArray, op: (Double, Double) -> Double): F64DenseFlatArray { 96 | val dst = DoubleArray(length) 97 | val src1 = data 98 | val src2 = other.data 99 | var src1Offset = offset 100 | var src2Offset = other.offset 101 | val length = length 102 | if (src1Offset == 0 && src2Offset == 0) { 103 | for (i in 0 until length) { 104 | dst[i] = op.invoke(src1[i], src2[i]) 105 | } 106 | } else { 107 | for (i in 0 until length) { 108 | dst[i] = op.invoke(src1[src1Offset], src2[src2Offset]) 109 | src1Offset++ 110 | src2Offset++ 111 | } 112 | } 113 | return create(dst, 0, this.length) 114 | } 115 | 116 | private inline fun ebe( 117 | other: F64Array, 118 | op: (Double, Double) -> Double, 119 | superOp: F64Array.(F64Array) -> F64FlatArray 120 | ): F64FlatArray = if (other is F64DenseFlatArray) { 121 | checkShape(other) 122 | denseEBE(other, op) 123 | } else { 124 | this.superOp(other) 125 | } 126 | 127 | override fun transformInPlace(op: (Double) -> Double) = denseTransformInPlace(op) 128 | 129 | override fun transform(op: (Double) -> Double): F64FlatArray = denseTransform(op) 130 | 131 | override fun fold(initial: T, op: (T, Double) -> T): T { 132 | var res = initial 133 | val dst = data 134 | var dstOffset = offset 135 | val dstEnd = dstOffset + length 136 | while (dstOffset < dstEnd) { 137 | res = op.invoke(res, dst[dstOffset]) 138 | dstOffset++ 139 | } 140 | return res 141 | } 142 | 143 | override fun reduce(op: (Double, Double) -> Double): Double { 144 | val dst = data 145 | var dstOffset = offset 146 | val dstEnd = dstOffset + length 147 | var res = dst[dstOffset] 148 | dstOffset++ 149 | while (dstOffset < dstEnd) { 150 | res = op.invoke(res, dst[dstOffset]) 151 | dstOffset++ 152 | } 153 | return res 154 | } 155 | 156 | override fun combineInPlace(other: F64Array, op: (Double, Double) -> Double) = 157 | ebeInPlace(other, op) { super.combineInPlace(it, op) } 158 | 159 | override fun combine(other: F64Array, op: (Double, Double) -> Double): F64FlatArray = 160 | ebe(other, op) { super.combine(other, op) } 161 | 162 | /* Arithmetic */ 163 | 164 | /* Arithmetic binary operations */ 165 | 166 | /* Addition */ 167 | 168 | override fun plusAssign(other: F64Array) = ebeInPlace(other, { a, b -> a + b }, { super.plusAssign(it) }) 169 | 170 | override fun plus(other: F64Array) = ebe(other, { a, b -> a + b }, { super.plus(it) }) 171 | 172 | /* Subtraction */ 173 | 174 | override fun minusAssign(other: F64Array) = ebeInPlace(other, { a, b -> a - b }, { super.minusAssign(it) }) 175 | 176 | override fun minus(other: F64Array) = ebe(other, { a, b -> a - b }, { super.minus(it) }) 177 | 178 | /* Multiplication */ 179 | 180 | override fun timesAssign(other: F64Array) = ebeInPlace(other, { a, b -> a * b }, { super.timesAssign(it) }) 181 | 182 | override fun times(other: F64Array) = ebe(other, { a, b -> a * b }, { super.times(it) }) 183 | 184 | /* Division */ 185 | 186 | override fun divAssign(other: F64Array) = ebeInPlace(other, { a, b -> a / b }, { super.divAssign(it) }) 187 | 188 | override fun div(other: F64Array) = ebe(other, { a, b -> a / b }, { super.div(it) }) 189 | 190 | override fun toDoubleArray() = data.copyOfRange(offset, offset + length) 191 | 192 | companion object { 193 | /** 194 | * We only use SIMD operations on vectors larger than the split boundary. 195 | */ 196 | const val DENSE_SPLIT_SIZE = 16 197 | 198 | internal fun create(data: DoubleArray, offset: Int, size: Int): F64DenseFlatArray { 199 | return if (size <= DENSE_SPLIT_SIZE || !Loader.nativeLibraryLoaded) { 200 | F64SmallDenseArray(data, offset, size) 201 | } else { 202 | F64LargeDenseArray(data, offset, size) 203 | } 204 | } 205 | } 206 | } 207 | 208 | /** 209 | * A contiguous vector of size at most [F64DenseFlatArray.DENSE_SPLIT_SIZE]. 210 | * 211 | * @author Sergei Lebedev 212 | * @since 0.1.0 213 | */ 214 | internal class F64SmallDenseArray( 215 | data: DoubleArray, 216 | offset: Int, 217 | size: Int 218 | ) : F64DenseFlatArray(data, offset, size) 219 | 220 | /** 221 | * A contiguous vector of size at least `[F64DenseFlatArray.DENSE_SPLIT_SIZE] + 1`. 222 | * 223 | * @author Sergei Lebedev 224 | * @since 0.1.0 225 | */ 226 | internal class F64LargeDenseArray( 227 | data: DoubleArray, 228 | offset: Int, 229 | size: Int 230 | ) : F64DenseFlatArray(data, offset, size) { 231 | 232 | override fun sd() = NativeSpeedups.unsafeSD(data, offset, length) 233 | 234 | override fun sum() = NativeSpeedups.unsafeSum(data, offset, length) 235 | 236 | override fun cumSum() { 237 | if (!NativeSpeedups.unsafeCumSum(data, offset, length)) super.cumSum() 238 | } 239 | 240 | override fun min() = NativeSpeedups.unsafeMin(data, offset, length) 241 | 242 | override fun max() = NativeSpeedups.unsafeMax(data, offset, length) 243 | 244 | override fun dot(other: F64Array): Double { 245 | return if (other is F64LargeDenseArray) { 246 | checkShape(other) 247 | NativeSpeedups.unsafeDot(data, offset, other.data, other.offset, length) 248 | } else { 249 | super.dot(other) 250 | } 251 | } 252 | 253 | private inline fun nativeTransform( 254 | nativeOp: (DoubleArray, Int, DoubleArray, Int, Int) -> Boolean, 255 | superOp: F64FlatArray.() -> F64FlatArray 256 | ): F64FlatArray { 257 | val dst = DoubleArray(length) 258 | if (nativeOp(dst, 0, data, offset, length)) { 259 | return create(dst, 0, length) 260 | } 261 | return superOp() 262 | } 263 | 264 | override fun expInPlace() { 265 | if (!NativeSpeedups.unsafeExp(data, offset, data, offset, length)) super.expInPlace() 266 | } 267 | 268 | override fun exp() = nativeTransform(NativeSpeedups::unsafeExp) { super.exp() } 269 | 270 | override fun expm1InPlace() { 271 | if (!NativeSpeedups.unsafeExpm1(data, offset, data, offset, length)) super.expm1InPlace() 272 | } 273 | 274 | override fun expm1() = nativeTransform(NativeSpeedups::unsafeExpm1) { super.expm1() } 275 | 276 | override fun logInPlace() { 277 | if (!NativeSpeedups.unsafeLog(data, offset, data, offset, length)) super.logInPlace() 278 | } 279 | 280 | override fun log() = nativeTransform(NativeSpeedups::unsafeLog) { super.log() } 281 | 282 | override fun log1pInPlace(){ 283 | if (!NativeSpeedups.unsafeLog1p(data, offset, data, offset, length)) super.log1pInPlace() 284 | } 285 | 286 | override fun log1p() = nativeTransform(NativeSpeedups::unsafeLog1p) { super.log1p() } 287 | 288 | override fun logSumExp() = NativeSpeedups.unsafeLogSumExp(data, offset, length) 289 | 290 | override fun logAddExpAssign(other: F64Array) { 291 | if (other is F64LargeDenseArray) { 292 | checkShape(other) 293 | if (NativeSpeedups.unsafeLogAddExp(data, offset, data, offset, other.data, other.offset, length)) return 294 | } 295 | super.logAddExpAssign(other) 296 | } 297 | 298 | override fun logAddExp(other: F64Array): F64FlatArray { 299 | if (other is F64LargeDenseArray) { 300 | checkShape(other) 301 | val res = DoubleArray(length) 302 | if (NativeSpeedups.unsafeLogAddExp(res, 0, data, offset, other.data, other.offset, length)) { 303 | return create(res, 0, length) 304 | } 305 | } 306 | return super.logAddExp(other) 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jetbrains/bio/viktor/F64FlatArray.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.bio.viktor 2 | 3 | import org.apache.commons.math3.util.FastMath 4 | import org.apache.commons.math3.util.Precision 5 | import java.text.DecimalFormat 6 | import kotlin.math.ln 7 | import kotlin.math.ln1p 8 | 9 | /** 10 | * An 1-dimensional specialization of [F64Array]. 11 | * 12 | * @since 0.4.0 13 | */ 14 | open class F64FlatArray protected constructor( 15 | data: DoubleArray, 16 | offset: Int, 17 | stride: Int, 18 | size: Int 19 | ) : F64Array(data, offset, intArrayOf(stride), intArrayOf(size), 1, stride, size) { 20 | 21 | internal open val unsafeGet: (Int) -> Double = { data[it * stride + offset] } 22 | internal open val unsafeSet: (Int, Double) -> Unit = { i, v -> data[i * stride + offset] = v } 23 | 24 | override operator fun get(pos: Int): Double { 25 | checkIndex("pos", pos, length) 26 | return unsafeGet(pos) 27 | } 28 | 29 | override operator fun set(pos: Int, value: Double) { 30 | checkIndex("pos", pos, length) 31 | unsafeSet(pos, value) 32 | } 33 | 34 | override fun flatten() = this 35 | 36 | override fun contains(other: Double): Boolean { 37 | for (pos in 0 until length) { 38 | if (unsafeGet(pos) == other) { 39 | return true 40 | } 41 | } 42 | 43 | return false 44 | } 45 | 46 | override fun along(axis: Int) = unsupported() 47 | 48 | override fun view(index: Int, axis: Int) = unsupported() 49 | 50 | override fun copyTo(other: F64Array) { 51 | val o = checkShape(other) 52 | for (pos in 0 until length) { 53 | o.unsafeSet(pos, unsafeGet(pos)) 54 | } 55 | } 56 | 57 | override fun copy(): F64FlatArray = F64DenseFlatArray.create(toDoubleArray(), 0, length) 58 | 59 | override fun fill(init: Double) { 60 | for (pos in 0 until length) { 61 | unsafeSet(pos, init) 62 | } 63 | } 64 | 65 | override fun reorder(indices: IntArray, axis: Int) { 66 | if (axis == 0) { 67 | reorderInternal(this, indices, axis, 68 | get = { pos -> unsafeGet(pos) }, 69 | set = { pos, value -> unsafeSet(pos, value) }) 70 | } else { 71 | unsupported() 72 | } 73 | } 74 | 75 | override fun dot(other: ShortArray) = balancedSum { unsafeGet(it) * other[it].toDouble() } 76 | 77 | override fun dot(other: IntArray) = balancedSum { unsafeGet(it) * other[it].toDouble() } 78 | 79 | override fun dot(other: F64Array) = balancedSum { unsafeGet(it) * other[it] } 80 | 81 | /** 82 | * Summation algorithm balancing accuracy with throughput. 83 | * 84 | * References 85 | * ---------- 86 | * 87 | * Dalton et al. "SIMDizing pairwise sums", 2014. 88 | */ 89 | private inline fun balancedSum(getter: (Int) -> Double): Double { 90 | var accUnaligned = 0.0 91 | var remaining = length 92 | while (remaining % 4 > 0) { 93 | remaining-- 94 | accUnaligned += getter(remaining) 95 | } 96 | val stack = DoubleArray(31 - 2) 97 | var p = 0 98 | var i = 0 99 | while (i < remaining) { 100 | // Shift. 101 | var v = getter(i) + getter(i + 1) 102 | val w = getter(i + 2) + getter(i + 3) 103 | v += w 104 | 105 | // Reduce. 106 | var bitmask = 4 107 | while (i and bitmask != 0) { 108 | v += stack[--p] 109 | bitmask = bitmask shl 1 110 | } 111 | stack[p++] = v 112 | i += 4 113 | } 114 | var acc = 0.0 115 | while (p > 0) { 116 | acc += stack[--p] 117 | } 118 | return acc + accUnaligned 119 | } 120 | 121 | override fun sum(): Double = balancedSum { unsafeGet(it) } 122 | 123 | override fun cumSum() { 124 | val acc = KahanSum() 125 | for (pos in 0 until length) { 126 | acc += unsafeGet(pos) 127 | unsafeSet(pos, acc.result()) 128 | } 129 | } 130 | 131 | override fun min() = unsafeGet(argMin()) 132 | 133 | override fun argMin(): Int { 134 | var minValue = Double.POSITIVE_INFINITY 135 | var res = 0 136 | for (pos in 0 until length) { 137 | val value = unsafeGet(pos) 138 | if (value <= minValue) { 139 | minValue = value 140 | res = pos 141 | } 142 | } 143 | return res 144 | } 145 | 146 | override fun max() = unsafeGet(argMax()) 147 | 148 | override fun argMax(): Int { 149 | var maxValue = Double.NEGATIVE_INFINITY 150 | var res = 0 151 | for (pos in 0 until length) { 152 | val value = unsafeGet(pos) 153 | if (value >= maxValue) { 154 | maxValue = value 155 | res = pos 156 | } 157 | } 158 | return res 159 | } 160 | 161 | private inline fun flatTransformInPlace(op: (Double) -> Double) { 162 | for (pos in 0 until length) { 163 | unsafeSet(pos, op.invoke(unsafeGet(pos))) 164 | } 165 | } 166 | 167 | private inline fun flatTransform(op: (Double) -> Double): F64FlatArray { 168 | val res = DoubleArray(length) 169 | for (pos in 0 until length) { 170 | res[pos] = op.invoke(unsafeGet(pos)) 171 | } 172 | return create(res) 173 | } 174 | 175 | private inline fun flatEBEInPlace(other: F64Array, op: (Double, Double) -> Double) { 176 | val o = checkShape(other) 177 | for (pos in 0 until length) { 178 | unsafeSet(pos, op.invoke(unsafeGet(pos), o.unsafeGet(pos))) 179 | } 180 | } 181 | 182 | private inline fun flatEBE(other: F64Array, op: (Double, Double) -> Double): F64FlatArray { 183 | val o = checkShape(other) 184 | val res = DoubleArray(length) 185 | for (pos in 0 until length) { 186 | res[pos] = op.invoke(unsafeGet(pos), o.unsafeGet(pos)) 187 | } 188 | return F64DenseFlatArray.create(res, 0, length) 189 | } 190 | 191 | override fun transformInPlace(op: (Double) -> Double) = flatTransformInPlace(op) 192 | 193 | override fun transform(op: (Double) -> Double): F64FlatArray = flatTransform(op) 194 | 195 | override fun fold(initial: T, op: (T, Double) -> T): T { 196 | var res = initial 197 | for (pos in 0 until length) { 198 | res = op(res, unsafeGet(pos)) 199 | } 200 | return res 201 | } 202 | 203 | override fun reduce(op: (Double, Double) -> Double): Double { 204 | var res = unsafeGet(0) 205 | for (pos in 1 until length) { 206 | res = op(res, unsafeGet(pos)) 207 | } 208 | return res 209 | } 210 | 211 | override fun combineInPlace(other: F64Array, op: (Double, Double) -> Double) = flatEBEInPlace(other, op) 212 | 213 | override fun combine(other: F64Array, op: (Double, Double) -> Double): F64FlatArray = flatEBE(other, op) 214 | 215 | /* Mathematics */ 216 | 217 | // FastMath is faster with exp and expm1, but slower with log and log1p 218 | // (confirmed by benchmarks on several JDK and hardware combinations) 219 | 220 | override fun exp() = transform(FastMath::exp) 221 | 222 | override fun expm1() = transform(FastMath::expm1) 223 | 224 | override fun log() = transform(::ln) 225 | 226 | override fun log1p() = transform(::ln1p) 227 | 228 | override fun logSumExp(): Double { 229 | val offset = max() 230 | val acc = KahanSum() 231 | for (pos in 0 until length) { 232 | acc += FastMath.exp(unsafeGet(pos) - offset) 233 | } 234 | return ln(acc.result()) + offset 235 | } 236 | 237 | override fun logAddExpAssign(other: F64Array) = flatEBEInPlace(other) { a, b -> a logAddExp b } 238 | 239 | override fun logAddExp(other: F64Array): F64FlatArray = flatEBE(other) { a, b -> a logAddExp b } 240 | 241 | /* Arithmetic */ 242 | 243 | override fun plusAssign(other: F64Array) = flatEBEInPlace(other) { a, b -> a + b } 244 | 245 | override fun plus(other: F64Array): F64FlatArray = flatEBE(other) { a, b -> a + b } 246 | 247 | override fun minusAssign(other: F64Array) = flatEBEInPlace(other) { a, b -> a - b } 248 | 249 | override fun minus(other: F64Array): F64FlatArray = flatEBE(other) { a, b -> a - b } 250 | 251 | override fun timesAssign(other: F64Array) = flatEBEInPlace(other) { a, b -> a * b } 252 | 253 | override fun times(other: F64Array): F64FlatArray = flatEBE(other) { a, b -> a * b } 254 | 255 | override fun divAssign(other: F64Array) = flatEBEInPlace(other) { a, b -> a / b } 256 | 257 | override fun div(other: F64Array): F64FlatArray = flatEBE(other) { a, b -> a / b } 258 | 259 | protected fun checkShape(other: F64Array): F64FlatArray { 260 | check(this === other || (other is F64FlatArray && shape[0] == other.shape[0])) { 261 | "operands shapes do not match: ${shape.contentToString()} vs ${other.shape.contentToString()}" 262 | } 263 | return other as F64FlatArray 264 | } 265 | 266 | override fun reshape(vararg shape: Int): F64Array { 267 | shape.forEach { require(it > 0) { "shape must be positive but was $it" } } 268 | check(shape.product() == length) { "total size of the new array must be unchanged" } 269 | return when { 270 | this.shape.contentEquals(shape) -> this 271 | else -> { 272 | val reshaped = shape.clone() 273 | reshaped[reshaped.lastIndex] = strides.single() 274 | for (i in reshaped.lastIndex - 1 downTo 0) { 275 | reshaped[i] = reshaped[i + 1] * shape[i + 1] 276 | } 277 | create(data, offset, reshaped, shape) 278 | } 279 | } 280 | } 281 | 282 | override fun asSequence(): Sequence = (0 until length).asSequence().map { unsafeGet(it) } 283 | 284 | override fun clone(): F64FlatArray = F64FlatArray(data.clone(), offset, strides[0], shape[0]) 285 | 286 | override fun toArray() = toDoubleArray() 287 | 288 | override fun toGenericArray() = unsupported() 289 | 290 | override fun toDoubleArray() = DoubleArray(length) { unsafeGet(it) } 291 | 292 | /** 293 | * A version of [DecimalFormat.format] which doesn't produce ? 294 | * for [Double.NaN] and infinities. 295 | */ 296 | private fun DecimalFormat.safeFormat(value: Double) = when { 297 | value.isNaN() -> "nan" 298 | value == Double.POSITIVE_INFINITY -> "inf" 299 | value == Double.NEGATIVE_INFINITY -> "-inf" 300 | else -> format(value) 301 | } 302 | 303 | override fun toString(maxDisplay: Int, format: DecimalFormat): String { 304 | val sb = StringBuilder() 305 | sb.append('[') 306 | 307 | if (maxDisplay < length) { 308 | for (pos in 0 until maxDisplay / 2) { 309 | sb.append(format.safeFormat(this[pos])).append(", ") 310 | } 311 | 312 | sb.append("..., ") 313 | 314 | val leftover = maxDisplay - maxDisplay / 2 315 | for (pos in length - leftover until length) { 316 | sb.append(format.safeFormat(this[pos])) 317 | if (pos < length - 1) { 318 | sb.append(", ") 319 | } 320 | } 321 | } else { 322 | for (pos in 0 until length) { 323 | sb.append(format.safeFormat(this[pos])) 324 | if (pos < length - 1) { 325 | sb.append(", ") 326 | } 327 | } 328 | } 329 | 330 | sb.append(']') 331 | return sb.toString() 332 | } 333 | 334 | override fun equals(other: Any?) = when { 335 | this === other -> true 336 | other !is F64FlatArray -> false // an instance of F64Array can't be flat 337 | length != other.length -> false 338 | else -> (0 until length).all { 339 | Precision.equals(unsafeGet(it), other.unsafeGet(it)) 340 | } 341 | } 342 | 343 | override fun hashCode() = (0 until length).fold(1) { acc, pos -> 344 | // XXX calling #hashCode results in boxing, see KT-7571. 345 | 31 * acc + java.lang.Double.hashCode(unsafeGet(pos)) 346 | } 347 | 348 | companion object { 349 | internal fun create( 350 | data: DoubleArray, 351 | offset: Int = 0, 352 | stride: Int = 1, 353 | size: Int = data.size 354 | ): F64FlatArray { 355 | // require(offset + (size - 1) * stride < data.size) { "not enough data" } 356 | // this check is not needed since we control all invocations of this internal method 357 | require(size > 0) { "empty arrays not supported" } 358 | return if (stride == 1) { 359 | F64DenseFlatArray.create(data, offset, size) 360 | } else { 361 | F64FlatArray(data, offset, stride, size) 362 | } 363 | } 364 | } 365 | } 366 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jetbrains/bio/viktor/Internals.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.bio.viktor 2 | 3 | @Suppress("nothing_to_inline") 4 | inline fun IntArray.product() = fold(1, Int::times) 5 | 6 | internal fun IntArray.remove(pos: Int) = when (pos) { 7 | 0 -> sliceArray(1..lastIndex) 8 | lastIndex -> sliceArray(0 until lastIndex) 9 | else -> sliceArray(0 until pos) + sliceArray(pos + 1..lastIndex) 10 | } 11 | 12 | @Suppress("nothing_to_inline") 13 | internal inline fun checkIndex(label: String, pos: Int, size: Int) { 14 | if (pos < 0 || pos >= size) { 15 | throw IndexOutOfBoundsException("$label must be in [0, $size), but was $pos") 16 | } 17 | } 18 | 19 | @Suppress("nothing_to_inline") 20 | internal inline fun unsupported(): Nothing = throw UnsupportedOperationException() -------------------------------------------------------------------------------- /src/main/kotlin/org/jetbrains/bio/viktor/Loader.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.bio.viktor 2 | 3 | import org.slf4j.LoggerFactory 4 | import java.nio.file.Files 5 | import java.nio.file.Path 6 | import java.nio.file.StandardCopyOption.REPLACE_EXISTING 7 | 8 | internal class ResourceLibrary(private val name: String) { 9 | 10 | fun install() { 11 | val resource = System.mapLibraryName(name) 12 | val inputStream = ResourceLibrary::class.java.getResourceAsStream("/$resource") 13 | if (inputStream != null) { 14 | val libraryPath = LIBRARY_DIR.resolve(resource) 15 | Files.copy(inputStream, libraryPath, REPLACE_EXISTING) 16 | System.load(libraryPath.toString()) 17 | } else { 18 | System.loadLibrary(name) 19 | } 20 | } 21 | 22 | companion object { 23 | private val LIBRARY_DIR: Path by lazy { 24 | val path = Files.createTempDirectory("simd") 25 | Runtime.getRuntime().addShutdownHook(Thread { 26 | path.toFile().deleteRecursively() 27 | }) 28 | path 29 | } 30 | } 31 | } 32 | 33 | internal object Loader { 34 | 35 | private val LOG = LoggerFactory.getLogger(Loader::class.java) 36 | 37 | /** If `true` vector operations will be SIMD-optimized. */ 38 | internal var nativeLibraryLoaded: Boolean = false 39 | private set 40 | 41 | private var optimizationSupported = false 42 | private var architectureSupported = false 43 | 44 | 45 | fun ensureLoaded() {} 46 | 47 | private val arch: String get() { 48 | return when (val arch = System.getProperty("os.arch").toLowerCase()) { 49 | "amd64", "x86_64" -> "x86_64" 50 | else -> error("unsupported architecture: $arch") 51 | } 52 | } 53 | 54 | init { 55 | 56 | try { 57 | architectureSupported = arch.let { true } 58 | ResourceLibrary("simd.$arch").install() 59 | nativeLibraryLoaded = true 60 | 61 | when { 62 | isAvxSupported() -> { 63 | ResourceLibrary("simd.avx.$arch").install() 64 | optimizationSupported = true 65 | } 66 | isSse2Supported() -> { 67 | ResourceLibrary("simd.sse2.$arch").install() 68 | optimizationSupported = true 69 | } 70 | else -> warnNoOptimization() 71 | } 72 | } catch (e: Throwable) { 73 | LOG.info(e.message) 74 | warnNoOptimization() 75 | } 76 | } 77 | 78 | private fun warnNoOptimization() { 79 | if (!architectureSupported) { 80 | LOG.info("SIMD optimization is not available for your architecture, enable debug output for more details.") 81 | LOG.debug( 82 | """Currently supported architectures: x86_64, amd64. 83 | Fallback Kotlin implementation will be used. 84 | Build viktor for your system from source as described in https://github.com/JetBrains-Research/viktor""" 85 | ) 86 | } else if (!nativeLibraryLoaded) { 87 | LOG.info("Couldn't load native SIMD library, enable debug output for more details.") 88 | LOG.debug( 89 | """Native SIMD library couldn't be loaded. 90 | Currently supported operational systems: Linux, Windows, MacOS. 91 | Fallback Kotlin implementation will be used. 92 | Build viktor for your system from source as described in https://github.com/JetBrains-Research/viktor""" 93 | ) 94 | } 95 | else if (!optimizationSupported) { 96 | LOG.info("SIMD optimization is not available for your system, enable debug output for more details.") 97 | LOG.debug( 98 | """No supported SIMD instruction sets were detected on your system. 99 | Currently supported SIMD instruction sets: SSE2, AVX. 100 | Fallback Kotlin implementation will be used. 101 | Build viktor for your system from source as described in https://github.com/JetBrains-Research/viktor""" 102 | ) 103 | } 104 | } 105 | } 106 | 107 | internal external fun isAvxSupported(): Boolean 108 | internal external fun isSse2Supported(): Boolean 109 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jetbrains/bio/viktor/MoreMath.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.bio.viktor 2 | 3 | import org.apache.commons.math3.util.FastMath 4 | import kotlin.math.abs 5 | import kotlin.math.max 6 | 7 | /** 8 | * Evaluates log(exp(a) + exp(b)) using the following trick 9 | * 10 | * log(exp(a) + exp(b)) = a + log(1 + exp(b - a)) 11 | * 12 | * assuming a >= b. 13 | */ 14 | infix fun Double.logAddExp(b: Double): Double { 15 | val a = this 16 | return when { 17 | a.isNaN() || b.isNaN() -> Double.NaN 18 | a.isInfinite() -> if (a < 0) b else a 19 | b.isInfinite() -> if (b < 0) a else b 20 | else -> max(a, b) + StrictMath.log1p(FastMath.exp(-abs(a - b))) 21 | } 22 | } 23 | 24 | fun Sequence.logSumExp(): Double = toList().toDoubleArray().asF64Array().logSumExp() 25 | 26 | /** 27 | * Kahan-Babuska summation. 28 | * 29 | * See https://en.wikipedia.org/wiki/Kahan_summation_algorithm for details. 30 | * 31 | * @author Alexey Dievsky 32 | * @since 0.1.0 33 | */ 34 | class KahanSum @JvmOverloads constructor(private var accumulator: Double = 0.0) { 35 | private var compensator = 0.0 36 | 37 | /** Supplies a number to be added to the accumulator. */ 38 | fun feed(value: Double): KahanSum { 39 | val t = accumulator + value 40 | compensator += if (abs(accumulator) >= abs(value)) { 41 | (accumulator - t) + value 42 | } else { 43 | (value - t) + accumulator 44 | } 45 | 46 | accumulator = t 47 | return this 48 | } 49 | 50 | operator fun plusAssign(value: Double) { 51 | feed(value) // Sweet, so sweet! 52 | } 53 | 54 | /** Returns the sum accumulated so far. */ 55 | fun result() = accumulator + compensator 56 | } 57 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jetbrains/bio/viktor/NativeSpeedups.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.bio.viktor 2 | 3 | internal object NativeSpeedups { 4 | 5 | init { 6 | Loader.ensureLoaded() 7 | } 8 | 9 | external fun unsafeMin(values: DoubleArray, offset: Int, length: Int): Double 10 | 11 | external fun unsafeMax(values: DoubleArray, offset: Int, length: Int): Double 12 | 13 | external fun unsafeExp(dst: DoubleArray, dstOffset: Int, src: DoubleArray, srcOffset: Int, length: Int): Boolean 14 | 15 | external fun unsafeExpm1(dst: DoubleArray, dstOffset: Int, src: DoubleArray, srcOffset: Int, length: Int): Boolean 16 | 17 | external fun unsafeLog(dst: DoubleArray, dstOffset: Int, src: DoubleArray, srcOffset: Int, length: Int): Boolean 18 | 19 | external fun unsafeLog1p(dst: DoubleArray, dstOffset: Int, src: DoubleArray, srcOffset: Int, length: Int): Boolean 20 | 21 | external fun unsafeLogSumExp(src: DoubleArray, srcOffset: Int, length: Int): Double 22 | 23 | external fun unsafeLogAddExp( 24 | dst: DoubleArray, 25 | dstOffset: Int, 26 | src1: DoubleArray, 27 | srcOffset1: Int, 28 | src2: DoubleArray, 29 | srcOffset2: Int, 30 | length: Int 31 | ): Boolean 32 | 33 | external fun unsafeDot( 34 | src1: DoubleArray, 35 | srcOffset1: Int, 36 | src2: DoubleArray, 37 | srcOffset2: Int, 38 | length: Int 39 | ): Double 40 | 41 | external fun unsafeSum(values: DoubleArray, offset: Int, length: Int): Double 42 | 43 | external fun unsafeSD(values: DoubleArray, offset: Int, length: Int): Double 44 | 45 | external fun unsafeCumSum(dest: DoubleArray, destOffset: Int, length: Int): Boolean 46 | } 47 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jetbrains/bio/viktor/Random.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.bio.viktor 2 | 3 | import org.apache.commons.math3.random.MersenneTwister 4 | import org.apache.commons.math3.random.RandomGenerator 5 | import kotlin.math.floor 6 | 7 | private val DEFAULT_RANDOM = MersenneTwister() 8 | 9 | /** 10 | * Randomized linear-time selection algorithm. 11 | * 12 | * See https://en.wikipedia.org/wiki/Quickselect. 13 | * 14 | * @since 0.2.0 15 | */ 16 | internal object QuickSelect { 17 | /** 18 | * Returns the n-th order statistic of a given array. 19 | * 20 | * Invariant: left <= n <= right 21 | */ 22 | tailrec fun select( 23 | values: F64FlatArray, 24 | left: Int, 25 | right: Int, 26 | n: Int, 27 | randomGenerator: RandomGenerator 28 | ): Double { 29 | // assert(n in left..right) unnecessary since we control all invocations 30 | 31 | if (left == right) { 32 | return values[left] 33 | } 34 | 35 | var split = left + randomGenerator.nextInt(right - left + 1) 36 | split = values.partition(split, left, right) 37 | return when { 38 | split == n -> values[n] 39 | split > n -> select(values, left, split - 1, n, randomGenerator) 40 | else -> select(values, split + 1, right, n, randomGenerator) 41 | } 42 | } 43 | } 44 | 45 | /** 46 | * Computes the [q]-th order statistic over this 1-D array. 47 | * 48 | * The implementation follows that of Commons Math. See JavaDoc of 49 | * [org.apache.commons.math3.stat.descriptive.rank.Percentile] for computational details. 50 | * 51 | * The array is modified in-place. Do a [F64Array.copy] of the array 52 | * to avoid mutation if necessary. 53 | * 54 | * @since 0.2.0 55 | */ 56 | fun F64Array.quantile( 57 | q: Double = 0.5, 58 | randomGenerator: RandomGenerator = DEFAULT_RANDOM 59 | ): Double { 60 | check(this is F64FlatArray) { "expected a 1-D array" } 61 | 62 | val pos = (length + 1) * q 63 | val d = pos - floor(pos) 64 | return when { 65 | pos < 1 -> min() 66 | pos >= length -> max() 67 | else -> { 68 | val lo = QuickSelect.select( 69 | this, 0, length - 1, pos.toInt() - 1, randomGenerator 70 | ) 71 | val hi = QuickSelect.select( 72 | this, 0, length - 1, pos.toInt(), randomGenerator 73 | ) 74 | return lo + d * (hi - lo) 75 | } 76 | } 77 | } 78 | 79 | /** 80 | * Randomly permutes the elements of this 1-D array. 81 | * 82 | * See https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle. 83 | * 84 | * @since 0.2.0 85 | */ 86 | fun F64Array.shuffle(randomGenerator: RandomGenerator = DEFAULT_RANDOM) { 87 | check(this is F64FlatArray) { "expected a 1-D array" } 88 | 89 | if (length <= 1) { 90 | return 91 | } 92 | 93 | for (i in 0..length - 2) { 94 | val j = randomGenerator.nextInt(length - i) 95 | swap(i, i + j) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/main/kotlin/org/jetbrains/bio/viktor/Searching.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.bio.viktor 2 | 3 | /** 4 | * Returns the insertion index of [target] into a sorted vector. 5 | * 6 | * If [target] already appears in this vector, the returned 7 | * index is just before the leftmost occurrence of [target]. 8 | * 9 | * @since 0.2.3 10 | */ 11 | fun F64Array.searchSorted(target: Double): Int { 12 | var lo = 0 13 | var hi = length 14 | while (lo < hi) { 15 | val mid = (lo + hi) ushr 1 16 | when { 17 | target <= this[mid] -> hi = mid 18 | else -> lo = mid + 1 19 | } 20 | } 21 | 22 | return lo 23 | } -------------------------------------------------------------------------------- /src/main/kotlin/org/jetbrains/bio/viktor/Serialization.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.bio.viktor 2 | 3 | import org.jetbrains.bio.npy.NpyArray 4 | import org.jetbrains.bio.npy.NpyFile 5 | import org.jetbrains.bio.npy.NpzFile 6 | import java.nio.file.Path 7 | 8 | /** Returns a view of the [NpyArray] as an n-dimensional array. */ 9 | fun NpyArray.asF64Array() = asDoubleArray().asF64Array().reshape(*shape) 10 | 11 | /** Writes a given matrix to [path] in NPY format. */ 12 | fun NpyFile.write(path: Path, a: F64Array) { 13 | val dense = if (a.isFlattenable) a else a.copy() 14 | write(path, dense.flatten().toDoubleArray(), shape = a.shape) 15 | } 16 | 17 | /** Writes a given array into an NPZ file under the specified [name]. */ 18 | fun NpzFile.Writer.write(name: String, a: F64Array) { 19 | val dense = if (a.isFlattenable) a else a.copy() 20 | write(name, dense.flatten().toDoubleArray(), shape = a.shape) 21 | } -------------------------------------------------------------------------------- /src/main/kotlin/org/jetbrains/bio/viktor/Sorting.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.bio.viktor 2 | 3 | import java.util.* 4 | 5 | /** 6 | * Sorts the elements in this 1-D array in in descending order. 7 | * 8 | * The operation is done **in place**. 9 | * 10 | * @param reverse if `true` the elements are sorted in `ascending` order. 11 | * Defaults to `false`. 12 | */ 13 | fun F64Array.sort(reverse: Boolean = false) = reorder(argSort(reverse)) 14 | 15 | /** 16 | * Returns a permutation of indices which makes the 1-D array sorted. 17 | * 18 | * @param reverse see [sort] for details. 19 | */ 20 | fun F64Array.argSort(reverse: Boolean = false): IntArray { 21 | check(this is F64FlatArray) { "expected a 1-D array" } 22 | val comparator = Comparator(IndexedDoubleValue::compareTo) 23 | val indexedValues = Array(length) { IndexedDoubleValue(it, unsafeGet(it)) } 24 | indexedValues.sortWith(if (reverse) comparator.reversed() else comparator) 25 | return IntArray(length) { indexedValues[it].index } 26 | } 27 | 28 | /** A version of [IndexedValue] specialized to [Double]. */ 29 | private data class IndexedDoubleValue(val index: Int, val value: Double) : 30 | Comparable { 31 | override fun compareTo(other: IndexedDoubleValue): Int { 32 | val res = value.compareTo(other.value) 33 | return if (res != 0) { 34 | res 35 | } else { 36 | index.compareTo(other.index) 37 | } 38 | } 39 | } 40 | 41 | internal inline fun reorderInternal( 42 | a: F64Array, 43 | indices: IntArray, 44 | axis: Int, 45 | get: (Int) -> T, 46 | set: (Int, T) -> Unit 47 | ) { 48 | require(indices.size == a.shape[axis]) 49 | 50 | val copy = indices.clone() 51 | for (pos in 0 until a.shape[axis]) { 52 | val value = get(pos) 53 | var j = pos 54 | while (true) { 55 | val k = copy[j] 56 | copy[j] = j 57 | if (k == pos) { 58 | set(j, value) 59 | break 60 | } else { 61 | set(j, get(k)) 62 | j = k 63 | } 64 | } 65 | } 66 | } 67 | 68 | /** 69 | * Partitions the array. 70 | * 71 | * Rearranges the elements in this array in such a way that 72 | * the [p]-th element moves to its position in the sorted copy 73 | * of the array. All elements smaller than the [p]-th element 74 | * are moved before this element, and all elements greater or 75 | * equals to this element are moved behind it. 76 | * 77 | * The operation is done **in place**. 78 | * 79 | * @param p the index of the element to partition by. 80 | * @since 0.2.3 81 | */ 82 | fun F64Array.partition(p: Int) { 83 | check(this is F64FlatArray) { "expected a 1-D array" } 84 | require(p in 0 until length) { "p must be in [0, $length)" } 85 | partition(p, 0, length - 1) 86 | } 87 | 88 | /** 89 | * Helper [partition] extension. 90 | * 91 | * Invariants: p = partition(values, left, right, p) 92 | * for all i < p: 93 | * values[i] < values[p] 94 | * for all i >= p: 95 | * values[i] >= values[p] 96 | * 97 | * @param p the index of the element to partition by. 98 | * @param left start index (inclusive). 99 | * @param right end index (inclusive). 100 | */ 101 | internal fun F64FlatArray.partition(p: Int, left: Int, right: Int): Int { 102 | val pivot = this[p] 103 | swap(p, right) // move to end. 104 | 105 | var ptr = left 106 | for (i in left until right) { 107 | if (this[i] < pivot) { 108 | swap(i, ptr) 109 | ptr++ 110 | } 111 | } 112 | 113 | swap(right, ptr) 114 | return ptr 115 | } 116 | 117 | @Suppress("nothing_to_inline") 118 | internal inline fun F64FlatArray.swap(i: Int, j: Int) { 119 | val tmp = unsafeGet(i) 120 | unsafeSet(i, unsafeGet(j)) 121 | unsafeSet(j, tmp) 122 | } 123 | -------------------------------------------------------------------------------- /src/simd/cpp/org_jetbrains_bio_viktor_NativeSpeedups.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "org_jetbrains_bio_viktor_NativeSpeedups.hpp" 13 | #include "simd_math.hpp" 14 | #include "source.hpp" 15 | #include "summing.hpp" 16 | 17 | #define JNI_METHOD(rtype, name) \ 18 | JNIEXPORT rtype JNICALL Java_org_jetbrains_bio_viktor_NativeSpeedups_##name 19 | 20 | template 21 | jboolean transformTo(JNIEnv *env, 22 | jdoubleArray jdst, jint dst_offset, 23 | jdoubleArray jsrc, jint src_offset, 24 | jint length, UnOp const& op) 25 | { 26 | jboolean dst_is_copy = JNI_FALSE; 27 | jdouble *dst = reinterpret_cast( 28 | env->GetPrimitiveArrayCritical(jdst, &dst_is_copy)); 29 | if (dst_is_copy == JNI_TRUE) return JNI_FALSE; 30 | jdouble *src = reinterpret_cast( 31 | env->GetPrimitiveArrayCritical(jsrc, NULL)); 32 | boost::simd::transform(src + src_offset, 33 | src + src_offset + length, 34 | dst + dst_offset, 35 | op); 36 | env->ReleasePrimitiveArrayCritical(jsrc, src, JNI_ABORT); 37 | env->ReleasePrimitiveArrayCritical(jdst, dst, JNI_ABORT); 38 | return JNI_TRUE; 39 | } 40 | 41 | template 42 | jdouble reduce(JNIEnv *env, 43 | jdoubleArray jsrc, jint src_offset, 44 | jint length, Fold const& f, Empty const& e) 45 | { 46 | jdouble *src = reinterpret_cast( 47 | env->GetPrimitiveArrayCritical(jsrc, NULL)); 48 | jdouble res = boost::simd::reduce( 49 | src + src_offset, src + src_offset + length, e, f, e); 50 | env->ReleasePrimitiveArrayCritical(jsrc, src, JNI_ABORT); 51 | return res; 52 | } 53 | 54 | JNI_METHOD(jdouble, unsafeMin)(JNIEnv *env, jobject, 55 | jdoubleArray jsrc, jint src_offset, 56 | jint length) 57 | { 58 | return reduce(env, jsrc, src_offset, length, boost::simd::min, boost::simd::Inf()); 59 | } 60 | 61 | JNI_METHOD(jdouble, unsafeMax)(JNIEnv *env, jobject, 62 | jdoubleArray jsrc, jint src_offset, 63 | jint length) 64 | { 65 | return reduce(env, jsrc, src_offset, length, boost::simd::max, boost::simd::Minf()); 66 | } 67 | 68 | JNI_METHOD(jboolean, unsafeExp)(JNIEnv *env, jobject, 69 | jdoubleArray jdst, jint dst_offset, 70 | jdoubleArray jsrc, jint src_offset, 71 | jint length) 72 | { 73 | return transformTo(env, jdst, dst_offset, jsrc, src_offset, length, boost::simd::exp); 74 | } 75 | 76 | 77 | JNI_METHOD(jboolean, unsafeExpm1)(JNIEnv *env, jobject, 78 | jdoubleArray jdst, jint dst_offset, 79 | jdoubleArray jsrc, jint src_offset, 80 | jint length) 81 | { 82 | return transformTo(env, jdst, dst_offset, jsrc, src_offset, length, boost::simd::expm1); 83 | } 84 | 85 | JNI_METHOD(jboolean, unsafeLog)(JNIEnv *env, jobject, 86 | jdoubleArray jdst, jint dst_offset, 87 | jdoubleArray jsrc, jint src_offset, 88 | jint length) 89 | { 90 | return transformTo(env, jdst, dst_offset, jsrc, src_offset, length, boost::simd::log); 91 | } 92 | 93 | JNI_METHOD(jboolean, unsafeLog1p)(JNIEnv *env, jobject, 94 | jdoubleArray jdst, jint dst_offset, 95 | jdoubleArray jsrc, jint src_offset, 96 | jint length) 97 | { 98 | return transformTo(env, jdst, dst_offset, jsrc, src_offset, length, boost::simd::log1p); 99 | } 100 | 101 | JNI_METHOD(jdouble, unsafeLogSumExp)(JNIEnv *env, jobject, 102 | jdoubleArray jsrc, jint offset, 103 | jint length) 104 | { 105 | jdouble *src = reinterpret_cast( 106 | env->GetPrimitiveArrayCritical(jsrc, NULL)); 107 | double res = simdmath::logsumexp(src + offset, length); 108 | env->ReleasePrimitiveArrayCritical(jsrc, src, JNI_ABORT); 109 | return res; 110 | } 111 | 112 | namespace { 113 | 114 | struct logaddexp { 115 | template 116 | BOOST_FORCEINLINE T operator() (T const &x, T const &y) const { 117 | T const min = boost::simd::min(x, y); 118 | T const max = boost::simd::max(x, y); 119 | T res = max + boost::simd::log1p(boost::simd::exp(min - max)); 120 | res = boost::simd::if_else(x == boost::simd::Minf(), y, res); 121 | res = boost::simd::if_else(y == boost::simd::Minf(), x, res); 122 | return res; 123 | } 124 | }; 125 | 126 | } 127 | 128 | JNI_METHOD(jboolean, unsafeLogAddExp)(JNIEnv *env, jobject, 129 | jdoubleArray jdst, jint dst_offset, 130 | jdoubleArray jsrc1, jint src1_offset, 131 | jdoubleArray jsrc2, jint src2_offset, 132 | jint length) 133 | { 134 | jboolean dst_is_copy = JNI_FALSE; 135 | jdouble *dst = reinterpret_cast( 136 | env->GetPrimitiveArrayCritical(jdst, &dst_is_copy)); 137 | if (dst_is_copy == JNI_TRUE) return JNI_FALSE; 138 | jdouble *src1 = reinterpret_cast( 139 | env->GetPrimitiveArrayCritical(jsrc1, NULL)); 140 | jdouble *src2 = reinterpret_cast( 141 | env->GetPrimitiveArrayCritical(jsrc2, NULL)); 142 | boost::simd::transform(src1 + src1_offset, 143 | src1 + src1_offset + length, 144 | src2 + src2_offset, 145 | dst + dst_offset, 146 | logaddexp()); 147 | env->ReleasePrimitiveArrayCritical(jsrc2, src2, JNI_ABORT); 148 | env->ReleasePrimitiveArrayCritical(jsrc1, src1, JNI_ABORT); 149 | env->ReleasePrimitiveArrayCritical(jdst, dst, JNI_ABORT); 150 | return JNI_TRUE; 151 | } 152 | 153 | JNI_METHOD(jdouble, unsafeDot)(JNIEnv *env, jobject, 154 | jdoubleArray jsrc1, jint src_offset1, 155 | jdoubleArray jsrc2, jint src_offset2, 156 | jint length) 157 | { 158 | jdouble *src1 = reinterpret_cast( 159 | env->GetPrimitiveArrayCritical(jsrc1, NULL)); 160 | jdouble *src2 = reinterpret_cast( 161 | env->GetPrimitiveArrayCritical(jsrc2, NULL)); 162 | jdouble res = simdmath::dot(src1 + src_offset1, src2 + src_offset2, length); 163 | env->ReleasePrimitiveArrayCritical(jsrc1, src1, JNI_ABORT); 164 | env->ReleasePrimitiveArrayCritical(jsrc2, src2, JNI_ABORT); 165 | return res; 166 | } 167 | 168 | JNI_METHOD(jdouble, unsafeSum)(JNIEnv *env, jobject, 169 | jdoubleArray jvalues, jint offset, jint length) 170 | { 171 | jdouble *values = reinterpret_cast( 172 | env->GetPrimitiveArrayCritical(jvalues, NULL)); 173 | source_1d f(values + offset, length); 174 | double res = balanced_sum(f); 175 | env->ReleasePrimitiveArrayCritical(jvalues, values, JNI_ABORT); 176 | return res; 177 | } 178 | 179 | JNI_METHOD(jdouble, unsafeSD)(JNIEnv *env, jobject, 180 | jdoubleArray jvalues, jint offset, 181 | jint length) 182 | { 183 | jdouble *values = reinterpret_cast( 184 | env->GetPrimitiveArrayCritical(jvalues, NULL)); 185 | source_2d f(values + offset, length); 186 | double res = twin_balanced_sum(f); 187 | env->ReleasePrimitiveArrayCritical(jvalues, values, JNI_ABORT); 188 | return res; 189 | } 190 | 191 | JNI_METHOD(jboolean, unsafeCumSum)(JNIEnv *env, jobject, 192 | jdoubleArray jdst, jint dst_offset, 193 | jint length) 194 | { 195 | jboolean is_copy = JNI_FALSE; 196 | jdouble *dst = reinterpret_cast( 197 | env->GetPrimitiveArrayCritical(jdst, &is_copy)); 198 | if (is_copy == JNI_TRUE) return JNI_FALSE; 199 | source_1d f(dst + dst_offset, dst + dst_offset, length); 200 | cum_sum(f); 201 | env->ReleasePrimitiveArrayCritical(jdst, dst, is_copy == JNI_TRUE ? 0 : JNI_ABORT); 202 | return JNI_TRUE; 203 | } 204 | -------------------------------------------------------------------------------- /src/simd/headers/simd_math.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "source.hpp" 14 | #include "summing.hpp" 15 | 16 | typedef boost::simd::pack pack_double; 17 | 18 | namespace simdmath { 19 | 20 | BOOST_SYMBOL_EXPORT 21 | double logsumexp(double const *src, size_t length) 22 | { 23 | using boost::alignment::is_aligned; 24 | using boost::simd::aligned_load; 25 | size_t const vector_size = pack_double::static_size; 26 | 27 | double offset = boost::simd::reduce( 28 | src, src + length, 29 | boost::simd::Minf(), 30 | boost::simd::max, 31 | boost::simd::Minf()); 32 | pack_double voffset(offset); 33 | 34 | double acc = 0.; 35 | while (length && !is_aligned(src, pack_double::alignment)) { 36 | acc += boost::simd::exp(*(src++) - offset); 37 | --length; 38 | } 39 | while (length % vector_size) { 40 | --length; 41 | acc += boost::simd::exp(src[length] - offset); 42 | } 43 | pack_double vacc = boost::simd::Zero(); 44 | for (size_t i = 0; i < length; i += vector_size) { 45 | vacc += boost::simd::exp(aligned_load(src, i) - voffset); 46 | } 47 | 48 | return boost::simd::log(acc + boost::simd::sum(vacc)) + offset; 49 | } 50 | 51 | BOOST_SYMBOL_EXPORT 52 | double dot(double const *src1, double const *src2, size_t length) 53 | { 54 | source_1d f 55 | = source_1d(src1, src2, length); 56 | return balanced_sum(f); 57 | } 58 | 59 | } /* ::simdmath */ 60 | -------------------------------------------------------------------------------- /src/simd/headers/source.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | typedef boost::simd::pack pack_double; 13 | 14 | struct sum_tag; 15 | struct cum_sum_tag; 16 | struct weighted_sum_tag; 17 | 18 | template 19 | struct source_1d; 20 | 21 | template 22 | size_t static_size() { 23 | return sizeof(T) / sizeof(double); 24 | } 25 | 26 | template<> 27 | struct source_1d 28 | { 29 | source_1d(double const *src, std::size_t length) 30 | : src_(src), length_(length) {} 31 | 32 | template 33 | void procure(T& container) 34 | { 35 | container += boost::simd::aligned_load(src_); 36 | src_ += static_size(); 37 | length_ -= static_size(); 38 | } 39 | 40 | template 41 | bool can_procure(int num, T const& item) const 42 | { 43 | return length_ >= num * static_size(); 44 | } 45 | 46 | bool is_aligned() const 47 | { 48 | return boost::alignment::is_aligned(src_, pack_double::alignment); 49 | } 50 | 51 | private: 52 | double const *src_; 53 | std::size_t length_; 54 | }; 55 | 56 | template<> 57 | struct source_1d 58 | { 59 | source_1d(double const *array, 60 | double const *weights, 61 | std::size_t length) 62 | : src_(array), weights_(weights), length_(length) {} 63 | 64 | template 65 | void procure(T& container) 66 | { 67 | container += (boost::simd::aligned_load(src_) 68 | * boost::simd::load(weights_)); 69 | src_ += static_size(); 70 | weights_ += static_size(); 71 | length_ -= static_size(); 72 | } 73 | 74 | template 75 | bool can_procure(int num, T const& item) const 76 | { 77 | return length_ >= num * static_size(); 78 | } 79 | 80 | bool is_aligned() const 81 | { 82 | return boost::alignment::is_aligned(src_, pack_double::alignment); 83 | } 84 | 85 | private: 86 | double const *src_; 87 | double const *weights_; 88 | std::size_t length_; 89 | }; 90 | 91 | template<> 92 | struct source_1d 93 | { 94 | source_1d(double const *src, 95 | double *dst, 96 | std::size_t length) 97 | : src_(src), dst_(dst), length_(length) {} 98 | 99 | template 100 | void procure(T& container) 101 | { 102 | container = boost::simd::aligned_load(src_); 103 | src_ += static_size(); 104 | length_ -= static_size(); 105 | } 106 | 107 | template 108 | bool can_procure(int num, T const& item) const 109 | { 110 | return length_ >= num * static_size(); 111 | } 112 | 113 | bool is_aligned() const 114 | { 115 | return boost::alignment::is_aligned(src_, pack_double::alignment); 116 | } 117 | 118 | template 119 | void feed(T const value) 120 | { 121 | // TODO: aligned_store? 122 | // TODO: error: 'T' does not refer to a value 123 | boost::simd::store(value, dst_); 124 | dst_ += static_size(); 125 | } 126 | 127 | private: 128 | double const *src_; 129 | double *dst_; 130 | std::size_t length_; 131 | }; 132 | 133 | struct weighted_mean_tag; 134 | struct sd_tag; 135 | 136 | template 137 | struct source_2d; 138 | 139 | template<> 140 | struct source_2d 141 | { 142 | source_2d(double const* array, 143 | double const* weights, 144 | std::size_t length) 145 | : array_(array), weights_(weights), length_(length) {} 146 | 147 | template 148 | void procure(T& vw, T& w) 149 | { 150 | T const value = boost::simd::aligned_load(array_); 151 | T const weight = boost::simd::load(weights_); 152 | vw += value * weight; 153 | w += weight; 154 | array_ += static_size(); 155 | weights_ += static_size(); 156 | length_ -= static_size(); 157 | } 158 | 159 | template 160 | bool can_procure(int num, T const& item) const 161 | { 162 | return length_ >= num * static_size(); 163 | } 164 | 165 | bool is_aligned() const 166 | { 167 | return boost::alignment::is_aligned(array_, pack_double::alignment); 168 | } 169 | 170 | double result(double vw, double w) const 171 | { 172 | return vw / w; 173 | } 174 | 175 | private: 176 | double const* array_; 177 | double const *weights_; 178 | std::size_t length_; 179 | }; 180 | 181 | template<> 182 | struct source_2d 183 | { 184 | source_2d(double const* array, 185 | std::size_t length) 186 | : array_(array), length_(length), initial_length_(length) {} 187 | 188 | template 189 | void procure(T& v2, T& v) 190 | { 191 | T const value = boost::simd::aligned_load(array_); 192 | v2 += value * value; 193 | v += value; 194 | array_ += static_size(); 195 | length_ -= static_size(); 196 | } 197 | 198 | template 199 | bool can_procure(int num, T const& item) const 200 | { 201 | return length_ >= num * static_size(); 202 | } 203 | 204 | bool is_aligned() const 205 | { 206 | return boost::alignment::is_aligned(array_, pack_double::alignment); 207 | } 208 | 209 | double result(double v2, double v) const 210 | { 211 | double const res = (v2 - v * v / initial_length_) 212 | / (initial_length_ - 1); 213 | return (res < 0.) ? 0. : sqrt(res); 214 | } 215 | 216 | private: 217 | double const* array_; 218 | std::size_t length_; 219 | std::size_t const initial_length_; 220 | }; 221 | -------------------------------------------------------------------------------- /src/simd/headers/summing.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "source.hpp" 11 | 12 | typedef boost::simd::pack pack_double; 13 | 14 | template 15 | double balanced_sum(source_1d &f) 16 | { 17 | double res = 0.; 18 | while (f.can_procure(1, res) && !f.is_aligned()) { 19 | f.procure(res); 20 | } 21 | 22 | pack_double stack[62]; 23 | size_t p = 0; 24 | for (size_t iteration = 0; f.can_procure(4, stack[0]); ++iteration) { 25 | pack_double v = boost::simd::Zero(); 26 | f.procure(v); 27 | f.procure(v); 28 | pack_double w = boost::simd::Zero(); 29 | f.procure(w); 30 | f.procure(w); 31 | v += w; 32 | size_t bitmask = 1; 33 | for (; iteration & bitmask; bitmask <<= 1, --p) { 34 | v += stack[p - 1]; 35 | } 36 | stack[p++] = v; 37 | } 38 | pack_double vsum = boost::simd::Zero(); 39 | for (size_t i = p; i > 0; --i) { 40 | vsum += stack[i - 1]; 41 | } 42 | res = std::accumulate(vsum.begin(), vsum.end(), res); 43 | while (f.can_procure(1, res)) { 44 | f.procure(res); 45 | } 46 | return res; 47 | } 48 | 49 | template 50 | double twin_balanced_sum(source_2d& f) 51 | { 52 | double stat1 = 0.; 53 | double stat2 = 0.; 54 | while (f.can_procure(1, stat1) && !f.is_aligned()) { 55 | f.procure(stat1, stat2); 56 | } 57 | 58 | pack_double stack1[62]; 59 | pack_double stack2[62]; 60 | size_t p = 0; 61 | for (size_t iteration = 0; f.can_procure(4, stack1[0]); ++iteration) { 62 | pack_double v1 = boost::simd::Zero(); 63 | pack_double v2 = boost::simd::Zero(); 64 | f.procure(v1, v2); 65 | f.procure(v1, v2); 66 | pack_double w1 = boost::simd::Zero(); 67 | pack_double w2 = boost::simd::Zero(); 68 | f.procure(w1, w2); 69 | f.procure(w1, w2); 70 | v1 += w1; 71 | v2 += w2; 72 | size_t bitmask = 1; 73 | for (; iteration & bitmask; bitmask <<= 1, --p) { 74 | v1 += stack1[p - 1]; 75 | v2 += stack2[p - 1]; 76 | } 77 | stack1[p] = v1; 78 | stack2[p++] = v2; 79 | } 80 | pack_double vsum1 = boost::simd::Zero(); 81 | pack_double vsum2 = boost::simd::Zero(); 82 | for (size_t i = p; i > 0; --i) { 83 | vsum1 += stack1[i - 1]; 84 | vsum2 += stack2[i - 1]; 85 | } 86 | stat1 = std::accumulate(vsum1.begin(), vsum1.end(), stat1); 87 | stat2 = std::accumulate(vsum2.begin(), vsum2.end(), stat2); 88 | while (f.can_procure(1, stat1)) { 89 | f.procure(stat1, stat2); 90 | } 91 | return f.result(stat1, stat2); 92 | } 93 | 94 | inline void kahan_update(double &accumulator, double &compensator, double value) 95 | { 96 | double const new_accumulator = accumulator + value; 97 | double const first_option = (accumulator - new_accumulator) + value; 98 | double const second_option = (value - new_accumulator) + accumulator; 99 | if (std::abs(accumulator) > std::abs(value)) { 100 | compensator += first_option; 101 | } else { 102 | compensator += second_option; 103 | } 104 | accumulator = new_accumulator; 105 | } 106 | 107 | template 108 | void cum_sum(source_1d &f) 109 | { 110 | double accumulator = 0.; 111 | double compensator = 0.; 112 | double value = 0.; 113 | while (f.can_procure(1, value) && !f.is_aligned()) { 114 | f.procure(value); 115 | kahan_update(accumulator, compensator, value); 116 | f.feed(accumulator + compensator); 117 | } 118 | pack_double pack_value1 = boost::simd::Zero(); 119 | pack_double pack_value2 = boost::simd::Zero(); 120 | while (f.can_procure(2, pack_value1)) { 121 | f.procure(pack_value1); 122 | f.procure(pack_value2); 123 | pack_double const pack_cum_sum1 = boost::simd::cumsum(pack_value1); 124 | pack_double const pack_cum_sum2 = boost::simd::cumsum(pack_value2); 125 | pack_double const pack_feed_slice1 = pack_cum_sum1 + (accumulator + compensator); 126 | f.feed(pack_feed_slice1); 127 | kahan_update(accumulator, compensator, pack_cum_sum1[pack_double::static_size - 1]); 128 | pack_double const pack_feed_slice2 = pack_cum_sum2 + (accumulator + compensator); 129 | f.feed(pack_feed_slice2); 130 | kahan_update(accumulator, compensator, pack_cum_sum2[pack_double::static_size - 1]); 131 | } 132 | value = 0.; 133 | while (f.can_procure(1, accumulator)) { 134 | f.procure(value); 135 | kahan_update(accumulator, compensator, value); 136 | f.feed(accumulator + compensator); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/test/kotlin/org/jetbrains/bio/viktor/BalancedSumTests.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.bio.viktor 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Test 5 | import org.junit.runner.RunWith 6 | import org.junit.runners.Parameterized 7 | import org.junit.runners.Parameterized.Parameters 8 | import java.util.* 9 | 10 | @RunWith(Parameterized::class) 11 | class BalancedSumTest(private val size: Int) { 12 | @Test fun accuracy() { 13 | val v = Random().doubles(size.toLong()).toArray().asF64Array() 14 | 15 | val expected = KahanSum() 16 | for (i in 0 until v.length) { 17 | expected.feed(v[i]) 18 | } 19 | 20 | assertEquals(expected.result(), v.sum(), 1e-8) 21 | } 22 | 23 | companion object { 24 | @Parameters(name = "{0}") 25 | @JvmStatic fun `data`() = listOf(32, 64, 100, 500) 26 | } 27 | } 28 | 29 | @RunWith(Parameterized::class) 30 | class BalancedDotTest(private val size: Int) { 31 | @Test fun accuracy() { 32 | val r = Random() 33 | val v = r.doubles(size.toLong()).toArray().asF64Array() 34 | val w = r.doubles(size.toLong()).toArray().asF64Array() 35 | 36 | val expected = KahanSum() 37 | for (i in 0 until v.length) { 38 | expected.feed(v[i] * w[i]) 39 | } 40 | 41 | assertEquals(expected.result(), v.dot(w), 1e-8) 42 | } 43 | 44 | @Test fun intAccuracy() { 45 | val r = Random() 46 | val v = r.doubles(size.toLong()).toArray().asF64Array() 47 | val w = r.ints(-1000, 1000).limit(size.toLong()).toArray() 48 | 49 | val expected = KahanSum() 50 | for (i in 0 until v.length) { 51 | expected.feed(v[i] * w[i]) 52 | } 53 | 54 | assertEquals(expected.result(), v.dot(w), 1e-8) 55 | } 56 | 57 | @Test fun shortAccuracy() { 58 | val r = Random() 59 | val v = r.doubles(size.toLong()).toArray().asF64Array() 60 | // there are no short streams 61 | val w = ShortArray(size) 62 | for (i in 0 until size) { 63 | w[i] = (r.nextInt(2000) - 1000).toShort() 64 | } 65 | val expected = KahanSum() 66 | for (i in 0 until v.length) { 67 | expected.feed(v[i] * w[i]) 68 | } 69 | 70 | assertEquals(expected.result(), v.dot(w), 1e-8) 71 | } 72 | 73 | companion object { 74 | @Parameters(name = "{0}") 75 | @JvmStatic fun `data`() = listOf(32, 64, 100, 500) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/test/kotlin/org/jetbrains/bio/viktor/DoubleExtensionsTest.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.bio.viktor 2 | 3 | import org.junit.Test 4 | import kotlin.test.assertEquals 5 | 6 | class DoubleExtensionsTest { 7 | @Test fun plusVector() { 8 | val v = F64Array(10) { it.toDouble() } 9 | val incremented = F64Array(10) { it + 1.0 } 10 | assertEquals(incremented, 1.0 + v) 11 | } 12 | 13 | @Test fun plusMatrix() { 14 | val m = F64Array(10, 2) { i, j -> i + 2.0 * j } 15 | val incremented = F64Array(10, 2) { i, j -> m[i, j] + 1.0 } 16 | assertEquals(incremented, 1.0 + m) 17 | } 18 | 19 | @Test fun minusVector() { 20 | val v = F64Array(10) { it.toDouble() } 21 | val reversed = F64Array(10) { (9 - it).toDouble() } 22 | assertEquals(reversed, 9.0 - v) 23 | } 24 | 25 | @Test fun minusMatrix() { 26 | val m = F64Array(10, 2) { i, j -> i + 2.0 * j } 27 | val decremented = F64Array(10, 2) { i, j -> 42.0 - m[i, j] } 28 | assertEquals(decremented, 42.0 - m) 29 | } 30 | 31 | @Test fun timesVector() { 32 | val v = F64Array(10) { it.toDouble() } 33 | val scaled = F64Array(10) { it * 42.0 } 34 | assertEquals(scaled, 42.0 * v) 35 | } 36 | 37 | @Test fun timesMatrix() { 38 | val m = F64Array(10, 2) { i, j -> i + 2.0 * j } 39 | val decremented = F64Array(10, 2) { i, j -> 42.0 * m[i, j] } 40 | assertEquals(decremented, 42.0 * m) 41 | } 42 | 43 | @Test fun divVector() { 44 | val v = F64Array(10) { it.toDouble() } 45 | val scaled = F64Array(10) { 1.0 / it } 46 | assertEquals(scaled, 1.0 / v) 47 | } 48 | 49 | @Test fun divMatrix() { 50 | val m = F64Array(10, 2) { i, j -> i + 2.0 * j } 51 | val decremented = F64Array(10, 2) { i, j -> 42.0 / m[i, j] } 52 | assertEquals(decremented, 42.0 / m) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/test/kotlin/org/jetbrains/bio/viktor/F64ArrayAgainstRTest.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.bio.viktor 2 | 3 | import org.junit.Assert 4 | import org.junit.Test 5 | 6 | class F64ArrayAgainstRTest { 7 | @Test 8 | fun whole() { 9 | val v = VALUES.asF64Array() 10 | Assert.assertEquals(18.37403, v.sum(), 1E-5) 11 | Assert.assertEquals(1.837403, v.mean(), 1E-6) 12 | Assert.assertEquals(0.8286257, v.sd(), 1E-7) 13 | } 14 | 15 | @Test 16 | fun slices() { 17 | val v = VALUES.asF64Array(offset = 3, size = 4) 18 | Assert.assertEquals(8.292786, v.sum(), 1E-6) 19 | Assert.assertEquals(2.073197, v.mean(), 1E-6) 20 | Assert.assertEquals(1.016512, v.sd(), 1E-6) 21 | } 22 | 23 | @Test 24 | fun weighted() { 25 | val v = VALUES.asF64Array() 26 | val w = WEIGHTS.asF64Array() 27 | Assert.assertEquals(8.417747, v.dot(w), 1E-6) 28 | } 29 | 30 | @Test 31 | fun weightedSlices() { 32 | val v = VALUES.asF64Array(offset = 3, size = 4) 33 | val w = WEIGHTS.asF64Array(offset = 2, size = 4) 34 | Assert.assertEquals(2.363317, v.dot(w), 1E-6) 35 | } 36 | 37 | companion object { 38 | /** 39 | * The VALUES were produced by R command "rgamma(10, 4, 2)" 40 | * The WEIGHTS were produced by R command "runif(10)" 41 | * The expected statistics were calculated in R 42 | */ 43 | private val VALUES = doubleArrayOf( 44 | 1.5409738, 2.6926526, 0.8159389, 2.5009070, 3.2777667, 45 | 1.5157005, 0.9984120, 2.3274278, 1.7286019, 0.9756442 46 | ) 47 | private val WEIGHTS = doubleArrayOf( 48 | 0.04437868, 0.93508668, 0.09091827, 0.17638019, 0.86624410, 49 | 0.24522868, 0.85157408, 0.17318330, 0.07582913, 0.73878585 50 | ) 51 | } 52 | } -------------------------------------------------------------------------------- /src/test/kotlin/org/jetbrains/bio/viktor/F64ArrayCreationTest.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.bio.viktor 2 | 3 | import org.apache.commons.math3.util.Precision 4 | import org.junit.Assert.assertArrayEquals 5 | import org.junit.Assert.assertEquals 6 | import org.junit.Test 7 | import kotlin.test.assertTrue 8 | 9 | class F64ArrayCreationTest { 10 | @Test fun specialization() { 11 | assertTrue(F64FlatArray.create(doubleArrayOf(1.0), stride = 10) !is F64DenseFlatArray) 12 | assertTrue(F64FlatArray.create(doubleArrayOf(1.0)) is F64DenseFlatArray) 13 | assertTrue(F64FlatArray.create(doubleArrayOf(1.0, 2.0), offset = 1, size = 1) is F64DenseFlatArray) 14 | } 15 | 16 | @Test fun of() { 17 | assertArrayEquals( 18 | doubleArrayOf(1.0), F64Array.of(1.0).toDoubleArray(), Precision.EPSILON 19 | ) 20 | assertArrayEquals( 21 | doubleArrayOf(1.0, 2.0), F64Array.of(1.0, 2.0).toDoubleArray(), Precision.EPSILON 22 | ) 23 | assertArrayEquals( 24 | doubleArrayOf(1.0, 2.0, 3.0), F64Array.of(1.0, 2.0, 3.0).toDoubleArray(), Precision.EPSILON 25 | ) 26 | } 27 | 28 | @Test fun asF64Array() { 29 | assertEquals(F64Array.of(1.0), doubleArrayOf(1.0).asF64Array()) 30 | assertEquals( 31 | F64Array.of(3.0), doubleArrayOf(1.0, 2.0, 3.0).asF64Array(offset = 2, size = 1) 32 | ) 33 | } 34 | 35 | @Test fun asF64ArrayView() { 36 | val values = doubleArrayOf(1.0, 2.0, 3.0) 37 | val v = values.asF64Array(offset = 2, size = 1) 38 | v[0] = 42.0 39 | assertArrayEquals(doubleArrayOf(1.0, 2.0, 42.0), values, Precision.EPSILON) 40 | } 41 | 42 | @Test fun toF64Array() { 43 | assertEquals( 44 | F64Array.of(1.0, 2.0).reshape(1, 2), 45 | arrayOf(doubleArrayOf(1.0, 2.0)).toF64Array() 46 | ) 47 | assertEquals( 48 | F64Array.of(1.0, 2.0).reshape(2, 1), 49 | arrayOf(doubleArrayOf(1.0), doubleArrayOf(2.0)).toF64Array() 50 | ) 51 | 52 | assertEquals( 53 | F64Array.of( 54 | 1.0, 2.0, 3.0, 55 | 4.0, 5.0, 6.0 56 | ).reshape(2, 3), 57 | arrayOf( 58 | doubleArrayOf(1.0, 2.0, 3.0), 59 | doubleArrayOf(4.0, 5.0, 6.0) 60 | ).toF64Array() 61 | ) 62 | assertEquals( 63 | F64Array.of( 64 | 1.0, 2.0, 3.0, 65 | 4.0, 5.0, 6.0 66 | ).reshape(3, 2), 67 | arrayOf( 68 | doubleArrayOf(1.0, 2.0), 69 | doubleArrayOf(3.0, 4.0), 70 | doubleArrayOf(5.0, 6.0) 71 | ).toF64Array() 72 | ) 73 | 74 | assertEquals( 75 | F64Array.of( 76 | 1.0, 2.0, 3.0, 4.0, 77 | 5.0, 6.0, 7.0, 8.0 78 | ).reshape(2, 2, 2), 79 | arrayOf( 80 | arrayOf( 81 | doubleArrayOf(1.0, 2.0), 82 | doubleArrayOf(3.0, 4.0) 83 | ), 84 | arrayOf( 85 | doubleArrayOf(5.0, 6.0), 86 | doubleArrayOf(7.0, 8.0) 87 | ) 88 | ).toF64Array()) 89 | } 90 | 91 | @Test fun invoke() { 92 | assertEquals( 93 | F64Array.of(1.0, 2.0, 3.0), 94 | F64Array(3) { it + 1.0 } 95 | ) 96 | } 97 | 98 | @Test fun full() { 99 | val v = F64Array.full(2, 42.0) 100 | assertEquals(2, v.length) 101 | assertEquals(F64Array.of(42.0, 42.0), v) 102 | } 103 | 104 | @Test fun concatenateFlat() { 105 | assertEquals( 106 | F64Array.of(1.0, 2.0, 3.0, 4.0, 5.0), 107 | F64Array.concatenate( 108 | F64Array.of(1.0, 2.0), 109 | F64Array.of(3.0), 110 | F64Array.of(4.0, 5.0) 111 | ) 112 | ) 113 | } 114 | 115 | @Test fun appendFlat() { 116 | assertEquals( 117 | F64Array.of(1.0, 2.0, 3.0, 4.0, 5.0), 118 | F64Array.of(1.0, 2.0).append(F64Array.of(3.0, 4.0, 5.0)) 119 | ) 120 | } 121 | 122 | @Test fun appendMatrix0() { 123 | assertEquals( 124 | F64Array.of( 125 | 1.0, 2.0, 126 | 3.0, 4.0, 127 | 42.0, 42.0 128 | ).reshape(3, 2), 129 | F64Array.of( 130 | 1.0, 2.0, 131 | 3.0, 4.0 132 | ).reshape(2, 2).append(F64Array.of(42.0, 42.0).reshape(1, 2))) 133 | } 134 | 135 | @Test fun appendMatrix1() { 136 | assertEquals( 137 | F64Array.of( 138 | 1.0, 2.0, 42.0, 139 | 3.0, 4.0, 42.0 140 | ).reshape(2, 3), 141 | F64Array.of( 142 | 1.0, 2.0, 143 | 3.0, 4.0 144 | ).reshape(2, 2).append(F64Array.of(42.0, 42.0).reshape(2, 1), axis = 1) 145 | ) 146 | } 147 | 148 | @Test(expected = IllegalArgumentException::class) fun appendMismatch() { 149 | F64Array.of( 150 | 1.0, 2.0, 151 | 3.0, 4.0 152 | ).reshape(2, 2).append(F64Array.of(42.0, 42.0).reshape(2, 1), axis = 0) 153 | } 154 | 155 | @Test fun copy() { 156 | val v = F64Array.of(1.0, 2.0, 3.0) 157 | val copy = v.copy() 158 | assertEquals(v, copy) 159 | v[0] = 42.0 160 | assertEquals(1.0, copy[0], Precision.EPSILON) 161 | } 162 | 163 | @Test fun reshapeNonFlat() { 164 | val v = F64Array.of( 165 | 1.0, 2.0, 166 | 3.0, 4.0 167 | ) 168 | assertEquals(v.reshape(4, 1), v.reshape(2, 2).reshape(4, 1)) 169 | } 170 | 171 | @Test(expected = IllegalStateException::class) fun reshapeSizeMismatch() { 172 | F64Array.of( 173 | 1.0, 2.0, 174 | 3.0, 4.0 175 | ).reshape(3, 2) 176 | } 177 | 178 | @Test(expected = IllegalArgumentException::class) fun reshapeNegativeShape() { 179 | F64Array.of( 180 | 1.0, 2.0, 181 | 3.0, 4.0 182 | ).reshape(-2, -2) 183 | } 184 | } -------------------------------------------------------------------------------- /src/test/kotlin/org/jetbrains/bio/viktor/F64ArrayGetSetTests.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.bio.viktor 2 | 3 | import org.apache.commons.math3.util.Precision 4 | import org.junit.Assert.assertEquals 5 | import org.junit.Test 6 | import org.junit.runner.RunWith 7 | import org.junit.runners.Parameterized 8 | import org.junit.runners.Parameterized.Parameters 9 | import java.lang.IllegalArgumentException 10 | import kotlin.test.assertFailsWith 11 | import kotlin.test.assertNotEquals 12 | 13 | @RunWith(Parameterized::class) 14 | class F64FlatArrayGetSetTest( 15 | private val values: DoubleArray, 16 | private val offset: Int, 17 | size: Int, 18 | private val stride: Int 19 | ) { 20 | 21 | private val v = F64FlatArray.create(values, offset, stride, size) 22 | 23 | @Test fun get() { 24 | for (i in 0 until v.length) { 25 | assertEquals(values[offset + i * stride], v[i], Precision.EPSILON) 26 | } 27 | } 28 | 29 | @Test(expected = IndexOutOfBoundsException::class) fun getOutOfBounds() { 30 | v[v.length] 31 | } 32 | 33 | @Test fun set() { 34 | for (i in 0 until v.length) { 35 | val copy = v.copy() 36 | copy[i] = 42.0 37 | assertEquals(42.0, copy[i], Precision.EPSILON) 38 | 39 | // Ensure all other elements are unchanged. 40 | for (j in 0 until v.length) { 41 | if (j == i) { 42 | continue 43 | } 44 | 45 | assertEquals("$i/$j", v[j], copy[j], Precision.EPSILON) 46 | } 47 | } 48 | } 49 | 50 | @Test(expected = IndexOutOfBoundsException::class) fun setOutOfBounds() { 51 | v[v.length] = 42.0 52 | } 53 | 54 | @Test fun setMagicScalar() { 55 | val copy = v.copy() 56 | copy.V[_I] = 42.0 57 | 58 | assertEquals(F64Array.full(copy.length, 42.0), copy) 59 | } 60 | 61 | @Test fun setMagicVector() { 62 | val other = F64Array.full(v.length, 42.0) 63 | val copy = v.copy() 64 | copy.V[_I] = other 65 | 66 | assertEquals(other, copy) 67 | } 68 | 69 | companion object { 70 | @Parameters(name = "F64Array({1}, {2}, {3})") 71 | @JvmStatic fun `data`() = listOf( 72 | // Normal case. 73 | arrayOf(doubleArrayOf(1.0, 2.0, 3.0), 0, 3, 1), 74 | // Offset and stride. 75 | arrayOf(doubleArrayOf(1.0, 2.0, 3.0, 4.0, 5.0, 6.0), 1, 1, 2) 76 | ) 77 | } 78 | } 79 | 80 | class F64MatrixGetSetTest { 81 | @Test fun get() { 82 | val m = F64Array.of( 83 | 0.0, 1.0, 84 | 2.0, 3.0, 85 | 4.0, 5.0 86 | ).reshape(3, 2) 87 | 88 | assertEquals(0.0, m[0, 0], Precision.EPSILON) 89 | assertEquals(1.0, m[0, 1], Precision.EPSILON) 90 | assertEquals(2.0, m[1, 0], Precision.EPSILON) 91 | assertEquals(3.0, m[1, 1], Precision.EPSILON) 92 | assertEquals(4.0, m[2, 0], Precision.EPSILON) 93 | assertEquals(5.0, m[2, 1], Precision.EPSILON) 94 | 95 | assertFailsWith { 96 | m[42, 42] 97 | } 98 | assertFailsWith { 99 | m[0, 4] 100 | } 101 | } 102 | 103 | @Test fun set() { 104 | val m = F64Array.of( 105 | 0.0, 1.0, 106 | 2.0, 3.0, 107 | 4.0, 5.0 108 | ).reshape(3, 2) 109 | val copy = m.copy() 110 | copy[0, 1] = 42.0 111 | assertEquals(42.0, copy[0, 1], Precision.EPSILON) 112 | 113 | assertFailsWith { 114 | m[42, 42] = 100500.0 115 | } 116 | assertFailsWith { 117 | m[0, 4] = 42.0 118 | } 119 | } 120 | 121 | @Test fun setMagicRowScalar() { 122 | val m = F64Array.of( 123 | 0.0, 1.0, 124 | 2.0, 3.0, 125 | 4.0, 5.0 126 | ).reshape(3, 2) 127 | val copy = m.copy() 128 | copy.V[0] = 42.0 129 | assertEquals(F64Array.full(copy.shape[1], 42.0), copy.V[0]) 130 | } 131 | 132 | @Test fun setMagicRowVector() { 133 | val m = F64Array.of( 134 | 0.0, 1.0, 135 | 2.0, 3.0, 136 | 4.0, 5.0 137 | ).reshape(3, 2) 138 | val copy = m.copy() 139 | val v = F64Array.full(copy.shape[1], 42.0) 140 | copy.V[0] = v 141 | assertEquals(v, copy.V[0]) 142 | 143 | for (r in 1 until copy.shape[0]) { 144 | assertNotEquals(v, copy.V[r]) 145 | assertEquals(m.V[r], copy.V[r]) 146 | } 147 | } 148 | 149 | @Test fun setMagicColumnScalar() { 150 | val m = F64Array.of( 151 | 0.0, 1.0, 152 | 2.0, 3.0, 153 | 4.0, 5.0 154 | ).reshape(3, 2) 155 | val copy = m.copy() 156 | copy.V[_I, 0] = 42.0 157 | assertEquals(F64Array.full(copy.shape[0], 42.0), copy.V[_I, 0]) 158 | } 159 | 160 | @Test fun setMagicColumnVector() { 161 | val m = F64Array.of( 162 | 0.0, 1.0, 163 | 2.0, 3.0, 164 | 4.0, 5.0 165 | ).reshape(3, 2) 166 | val copy = m.copy() 167 | val v = F64Array.full(copy.shape[0], 42.0) 168 | copy.V[_I, 0] = v 169 | assertEquals(v, copy.V[_I, 0]) 170 | 171 | for (c in 1 until copy.shape[1]) { 172 | assertNotEquals(v, copy.V[_I, c]) 173 | assertEquals(m.V[_I, c], copy.V[_I, c]) 174 | } 175 | } 176 | 177 | @Test fun setMagicMatrix() { 178 | val m = F64Array.of( 179 | 0.0, 1.0, 180 | 2.0, 3.0, 181 | 4.0, 5.0 182 | ).reshape(3, 1, 2) 183 | 184 | val copy = m.copy() 185 | val replacement = F64Array.full(m.shape[1], m.shape[2], init = 42.0) 186 | copy.V[0] = replacement 187 | assertEquals(replacement, copy.V[0]) 188 | 189 | for (d in 1 until m.shape[0]) { 190 | assertNotEquals(replacement, copy.V[d]) 191 | assertEquals(m.V[d], copy.V[d]) 192 | } 193 | } 194 | 195 | @Test fun setMagicArray() { 196 | val m = F64Array.of( 197 | 0.0, 1.0, 198 | 2.0, 3.0, 199 | 4.0, 5.0 200 | ).reshape(3, 1, 2) 201 | val copy = m.copy() 202 | val replacement = m.copy().apply { fill(42.0) } 203 | copy.V[_I] = replacement 204 | assertEquals(replacement, copy) 205 | assertNotEquals(replacement, m) 206 | } 207 | 208 | @Test fun setMagicMatrixViaScalar() { 209 | val m = F64Array.of( 210 | 0.0, 1.0, 211 | 2.0, 3.0, 212 | 4.0, 5.0 213 | ).reshape(3, 1, 2) 214 | 215 | val copy1 = m.copy() 216 | copy1.V[0] = 42.0 217 | val copy2 = m.copy() 218 | copy2.V[0] = F64Array.full(m.shape[1], m.shape[2], init = 42.0) 219 | assertEquals(copy1, copy2) 220 | } 221 | 222 | @Test fun setMagicArrayViaScalar() { 223 | val m = F64Array.of( 224 | 0.0, 1.0, 225 | 2.0, 3.0, 226 | 4.0, 5.0 227 | ).reshape(3, 1, 2) 228 | val copy = m.copy() 229 | copy.V[_I] = 42.0 230 | assertNotEquals(copy, m) 231 | m.fill(42.0) 232 | assertEquals(copy, m) 233 | } 234 | } 235 | 236 | class F64ArrayGetSetTest { 237 | @Test fun get3D() { 238 | val value = 42.0 239 | val m = F64Array.full(init = value, shape = intArrayOf(2, 2, 2)) 240 | assertEquals(value, m[0, 0, 0], 0.0) 241 | assertEquals(value, m[1, 1, 1], 0.0) 242 | assertFailsWith { 243 | m[0, 1, 2] 244 | } 245 | assertFailsWith { 246 | m[2, 1, 0] 247 | } 248 | assertFailsWith { 249 | m[0] = value 250 | } 251 | assertFailsWith { 252 | m[0, 0] 253 | } 254 | assertFailsWith { 255 | m[0, 0, 0, 0] 256 | } 257 | assertFailsWith { 258 | m.V[0, 0, 0] 259 | } 260 | } 261 | 262 | @Test fun set3D() { 263 | val value = 42.0 264 | val m = F64Array.full(init = value - 1, shape = intArrayOf(2, 2, 2)) 265 | m[0, 0, 0] = value 266 | assertEquals(value, m[0, 0, 0], 0.0) 267 | assertNotEquals(value, m[1, 1, 1]) 268 | assertFailsWith { 269 | m[0, 1, 2] = value 270 | } 271 | assertFailsWith { 272 | m[2, 1, 0] = value 273 | } 274 | assertFailsWith { 275 | m[0] = value 276 | } 277 | assertFailsWith { 278 | m[0, 0] = value 279 | } 280 | assertFailsWith { 281 | m[0, 0, 0, 0] = value 282 | } 283 | } 284 | 285 | @Test fun get4D() { 286 | val value = 42.0 287 | val m = F64Array.full(init = value, shape = intArrayOf(2, 2, 2, 2)) 288 | assertEquals(value, m[0, 1, 0, 1], 0.0) 289 | assertEquals(value, m[1, 0, 1, 0], 0.0) 290 | assertFailsWith { 291 | m[0, 1, 2, 3] 292 | } 293 | assertFailsWith { 294 | m[3, 2, 1, 0] 295 | } 296 | assertFailsWith { 297 | m[0] 298 | } 299 | assertFailsWith { 300 | m[0, 0, 0] 301 | } 302 | assertFailsWith { 303 | m[0, 0, 0, 0, 0] 304 | } 305 | assertFailsWith { 306 | m.V[0, 0, 0, 0] 307 | } 308 | } 309 | 310 | @Test fun set4D() { 311 | val value = 42.0 312 | val m = F64Array.full(init = value - 1, shape = intArrayOf(2, 2, 2, 2)) 313 | m[0, 1, 0, 1] = value 314 | assertEquals(value, m[0, 1, 0, 1], 0.0) 315 | assertNotEquals(value, m[1, 0, 1, 0]) 316 | assertFailsWith { 317 | m[0, 1, 2, 3] = value 318 | } 319 | assertFailsWith { 320 | m[3, 2, 1, 0] = value 321 | } 322 | assertFailsWith { 323 | m[0] = value 324 | } 325 | assertFailsWith { 326 | m[0, 0, 0] = value 327 | } 328 | assertFailsWith { 329 | m[0, 0, 0, 0, 0] = value 330 | } 331 | } 332 | } -------------------------------------------------------------------------------- /src/test/kotlin/org/jetbrains/bio/viktor/F64ArrayOpsTests.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.bio.viktor 2 | 3 | import org.apache.commons.math3.special.Beta 4 | import org.apache.commons.math3.special.Gamma 5 | import org.apache.commons.math3.stat.StatUtils 6 | import org.apache.commons.math3.util.FastMath 7 | import org.junit.Assert.assertArrayEquals 8 | import org.junit.Assert.assertEquals 9 | import org.junit.Test 10 | import org.junit.runner.RunWith 11 | import org.junit.runners.Parameterized 12 | import org.junit.runners.Parameterized.Parameters 13 | import java.util.* 14 | import java.util.stream.DoubleStream 15 | import java.util.stream.IntStream 16 | import kotlin.math.sqrt 17 | import kotlin.test.assertNotEquals 18 | import kotlin.test.assertTrue 19 | 20 | @RunWith(Parameterized::class) 21 | class F64BasicArrayOperationTest(private val v: F64Array) { 22 | @Test fun contains() { 23 | v.asSequence().forEach { 24 | assertTrue(it in v) 25 | } 26 | } 27 | 28 | @Test fun equals() { 29 | assertEquals(v, v) 30 | assertEquals(v, v.copy()) 31 | assertEquals(v.copy(), v) 32 | 33 | if (v.nDim == 1) { 34 | assertEquals(v, v.toDoubleArray().asF64Array()) 35 | } else { 36 | assertEquals(v, v.toGenericArray().toF64Array()) 37 | } 38 | 39 | assertNotEquals(v, gappedArray(2..4)) 40 | assertNotEquals(v, gappedArray(1..30)) 41 | } 42 | 43 | @Test fun fill() { 44 | val copy = v.copy() 45 | copy.fill(42.0) 46 | assertEquals(F64Array.full(*v.shape, init = 42.0), copy) 47 | } 48 | 49 | companion object { 50 | @Parameters(name = "{0}") 51 | @JvmStatic fun `data`() = CASES 52 | } 53 | } 54 | 55 | @RunWith(Parameterized::class) 56 | class F64ArrayOperationTest(private val v: F64Array) { 57 | 58 | /* Unary array operations */ 59 | 60 | private fun doTestUnary( 61 | copyOp: F64Array.() -> F64Array, 62 | inplaceOp: (F64Array.() -> Unit)?, 63 | scalarOp: (Double) -> Double, 64 | delta: Double, 65 | opName: String 66 | ) { 67 | val vCopyOp = v.checkConstant(copyOp) 68 | assertArrayEquals("$opName array shape", v.shape, vCopyOp.shape) 69 | v.asSequence().map(scalarOp).zip(vCopyOp.asSequence()).forEach { (e, a) -> 70 | assertEquals("$opName copy", e, a, delta) 71 | } 72 | if (inplaceOp != null) { 73 | val vInplaceOp = v.clone().apply(inplaceOp) 74 | assertEquals("$opName in-place", vCopyOp, vInplaceOp) 75 | } 76 | } 77 | 78 | @Test fun transform() = doTestUnary( 79 | { transform(Gamma::logGamma) }, 80 | { transformInPlace(Gamma::logGamma) }, 81 | Gamma::logGamma, 82 | EXACT_DELTA, 83 | "transform (log-gamma)" 84 | ) 85 | @Test fun exp() = doTestUnary(F64Array::exp, F64Array::expInPlace, FastMath::exp, PRECISE_DELTA, "exp") 86 | @Test fun expm1() = doTestUnary(F64Array::expm1, F64Array::expm1InPlace, FastMath::expm1, PRECISE_DELTA, "expm1") 87 | @Test fun log() = doTestUnary(F64Array::log, F64Array::logInPlace, Math::log, PRECISE_DELTA, "log") 88 | @Test fun log1p() = doTestUnary(F64Array::log1p, F64Array::log1pInPlace, Math::log1p, PRECISE_DELTA, "log1p") 89 | @Test fun unaryMinus() = doTestUnary( 90 | F64Array::unaryMinus, { transformInPlace { -it } }, Double::unaryMinus, EXACT_DELTA, "unaryMinus" 91 | ) 92 | 93 | /* Binary array operations */ 94 | 95 | private fun doTestBinary( 96 | copyOp: F64Array.(F64Array) -> F64Array, 97 | inplaceOp: F64Array.(F64Array) -> Unit, 98 | scalarOp: (Double, Double) -> Double, 99 | commutative: Boolean, 100 | delta: Double, 101 | opName: String 102 | ) { 103 | val random = Random() 104 | val other = DoubleArray(v.shape.product()) { random.nextDouble() }.asF64Array().reshape(*v.shape) 105 | val voCopy = v.checkConstant(other, copyOp) 106 | val ovCopy = other.checkConstant(v, copyOp) 107 | val voInPlace = v.clone().apply { inplaceOp(other) } 108 | val ovInPlace = other.clone().apply { inplaceOp(v) } 109 | val voActual = v.asSequence().zip(other.asSequence()).map { (a, b) -> scalarOp(a, b) } 110 | val ovActual = other.asSequence().zip(v.asSequence()).map { (a, b) -> scalarOp(a, b) } 111 | 112 | voCopy.asSequence().zip(voActual).forEach { (a, e) -> 113 | assertEquals("v $opName other (copy)", e, a, delta) 114 | } 115 | ovCopy.asSequence().zip(ovActual).forEach { (a, e) -> 116 | assertEquals("other $opName v (copy)", e, a, delta) 117 | } 118 | 119 | assertEquals("v $opName other (in-place)", voCopy, voInPlace) 120 | assertEquals("other $opName v (in-place)", ovCopy, ovInPlace) 121 | 122 | if (commutative) { 123 | assertEquals("v $opName other != other $opName v", voCopy, ovCopy) 124 | } 125 | } 126 | 127 | @Test fun combine() = doTestBinary( 128 | { other -> combine(other) { a, b -> Beta.logBeta(a, b) } }, 129 | { other -> combineInPlace(other) { a, b -> Beta.logBeta(a, b) } }, 130 | { a, b -> Beta.logBeta(a, b) }, 131 | true, 132 | DELTA, 133 | "combine (log-beta)" 134 | ) 135 | 136 | @Test fun plus() = doTestBinary( 137 | F64Array::plus, F64Array::plusAssign, Double::plus, true, EXACT_DELTA, "plus" 138 | ) 139 | @Test fun minus() = doTestBinary( 140 | F64Array::minus, F64Array::minusAssign, Double::minus, false, EXACT_DELTA, "minus" 141 | ) 142 | @Test fun times() = doTestBinary( 143 | F64Array::times, F64Array::timesAssign, Double::times, true, EXACT_DELTA, "times" 144 | ) 145 | @Test fun div() = doTestBinary( 146 | F64Array::div, F64Array::divAssign, Double::div, false, EXACT_DELTA, "div" 147 | ) 148 | @Test fun logAddExp() = doTestBinary( 149 | F64Array::logAddExp, F64Array::logAddExpAssign, Double::logAddExp, true, DELTA, "logAddExp" 150 | ) 151 | 152 | /* Binary array-scalar operations */ 153 | 154 | private fun doTestBinaryArrayScalar( 155 | copyOp: (F64Array, Double) -> F64Array, 156 | inplaceOp: (F64Array, Double) -> Unit, 157 | scalarOp: (Double, Double) -> Double, 158 | delta: Double, 159 | opName: String 160 | ) { 161 | val scalar = 42.0 162 | doTestUnary({ copyOp(this, scalar) }, { inplaceOp(this, scalar) }, { scalarOp(it, scalar) }, delta, opName) 163 | } 164 | 165 | private fun doTestBinaryScalarArray( 166 | copyOp: (Double, F64Array) -> F64Array, 167 | scalarOp: (Double, Double) -> Double, 168 | delta: Double, 169 | opName: String 170 | ) { 171 | val scalar = 42.0 172 | doTestUnary({ copyOp(scalar, this) }, null, { scalarOp(scalar, it) }, delta, opName) 173 | } 174 | 175 | private fun doTestBinaryScalar( 176 | asCopyOp: (F64Array, Double) -> F64Array, 177 | saCopyOp: (Double, F64Array) -> F64Array, 178 | asInplaceOp: (F64Array, Double) -> Unit, 179 | scalarOp: (Double, Double) -> Double, 180 | commutative: Boolean, 181 | delta: Double, 182 | opName: String 183 | ) { 184 | doTestBinaryArrayScalar(asCopyOp, asInplaceOp, scalarOp, delta, opName) 185 | doTestBinaryScalarArray(saCopyOp, scalarOp, delta, opName) 186 | if (commutative) { 187 | assertEquals("$opName (scalar) should be commutative", asCopyOp(v, 42.0), saCopyOp(42.0, v)) 188 | } 189 | } 190 | 191 | @Test fun plusScalar() = doTestBinaryScalar( 192 | F64Array::plus, Double::plus, F64Array::plusAssign, Double::plus, 193 | true, EXACT_DELTA, "plusScalar" 194 | ) 195 | @Test fun minusScalar() = doTestBinaryScalar( 196 | F64Array::minus, Double::minus, F64Array::minusAssign, Double::minus, 197 | false, EXACT_DELTA, "plusScalar" 198 | ) 199 | @Test fun timesScalar() = doTestBinaryScalar( 200 | F64Array::times, Double::times, F64Array::timesAssign, Double::times, 201 | true, EXACT_DELTA, "timesScalar" 202 | ) 203 | @Test fun divScalar() = doTestBinaryScalar( 204 | F64Array::div, Double::div, F64Array::divAssign, Double::div, 205 | false, EXACT_DELTA, "plusScalar" 206 | ) 207 | 208 | /* Fold (reduce) array operations */ 209 | 210 | private fun doTestFold( 211 | arrayOp: (F64Array) -> Double, 212 | scalarOp: (Double, Double) -> Double, 213 | initial: Double, 214 | delta: Double, 215 | opName: String 216 | ) { 217 | val actual = v.checkConstant(arrayOp) 218 | val expected = v.asSequence().fold(initial, scalarOp) 219 | assertEquals(opName, expected, actual, delta) 220 | } 221 | 222 | @Test fun fold() = doTestFold( 223 | { it.fold(1.0, Double::times) }, 224 | Double::times, 225 | 1.0, 226 | EXACT_DELTA, 227 | "fold (product)" 228 | ) 229 | @Test fun reduce() = doTestFold( 230 | { it.reduce(Double::times) }, 231 | Double::times, 232 | 1.0, 233 | EXACT_DELTA, 234 | "reduce (product)" 235 | ) 236 | @Test fun max() = doTestFold(F64Array::max, Math::max, Double.NEGATIVE_INFINITY, EXACT_DELTA, "max") 237 | @Test fun min() = doTestFold(F64Array::min, Math::min, Double.POSITIVE_INFINITY, EXACT_DELTA, "min") 238 | @Test fun sum() = doTestFold(F64Array::sum, Double::plus, 0.0, DELTA, "plus") 239 | @Test fun logSumExp() = doTestFold( 240 | F64Array::logSumExp, Double::logAddExp, Double.NEGATIVE_INFINITY, DELTA, "logSumExp" 241 | ) 242 | 243 | /* Other array operations */ 244 | 245 | @Test fun unaryPlus() = assertEquals(v, v.checkConstant { unaryPlus() }) 246 | 247 | @Test fun rescale() = assertEquals(1.0, v.clone().apply { rescale() }.sum(), DELTA) 248 | 249 | @Test fun logRescale() = assertEquals(0.0, v.clone().apply { logRescale() }.logSumExp(), DELTA) 250 | 251 | @Test fun mean() = 252 | assertEquals(v.asSequence().sum() / v.shape.product(), v.checkConstant(F64Array::mean), DELTA) 253 | 254 | companion object { 255 | @Parameters(name = "{0}") 256 | @JvmStatic fun `data`() = CASES 257 | } 258 | } 259 | 260 | @RunWith(Parameterized::class) 261 | class F64FlatArrayOperationTest(private val v: F64FlatArray) { 262 | /* Flat array operations */ 263 | 264 | @Test fun argMax() = assertEquals(v.max(), v[v.checkConstant { argMax() }], EXACT_DELTA) 265 | 266 | @Test fun argMin() = assertEquals(v.min(), v[v.checkConstant { argMin() }], EXACT_DELTA) 267 | 268 | @Test fun sd() = assertEquals(sqrt(StatUtils.variance(v.toDoubleArray())), v.checkConstant(F64Array::sd), DELTA) 269 | 270 | @Test fun cumSum() { 271 | val actual = v.clone().apply { cumSum() } 272 | val acc = KahanSum() 273 | for (i in 0 until v.length) { 274 | acc.feed(v[i]) 275 | assertEquals(acc.result(), actual[i], DELTA) 276 | } 277 | } 278 | 279 | @Test fun dot() { 280 | val random = Random() 281 | val other = DoubleArray(v.length) { random.nextDouble() }.asF64Array() 282 | val voActual = v.checkConstant(other, F64Array::dot) 283 | val ovActual = other.checkConstant(v, F64Array::dot) 284 | val voExpected = v.asSequence().zip(other.asSequence()).map { (a, b) -> a * b }.sum() 285 | val ovExpected = other.asSequence().zip(v.asSequence()).map { (a, b) -> b * a }.sum() 286 | assertEquals("v dot other (copy)", voExpected, voActual, DELTA) 287 | assertEquals("other dot v (copy)", ovExpected, ovActual, DELTA) 288 | assertEquals("v dot other != other dot v", voActual, ovActual, DELTA) 289 | } 290 | 291 | companion object { 292 | @Parameters(name = "{0}") 293 | @JvmStatic fun `data`() = CASES.filterIsInstance() 294 | } 295 | } 296 | 297 | /** 298 | * Choosing a suitable delta for double comparison is tricky. Some of the operations being tested 299 | * have slightly different implementation (like exponent or logarithm), some are executed out-of-order 300 | * (like sum), so you can't always expect the results to be exactly the same. 301 | */ 302 | private const val DELTA = 1E-13 303 | private const val PRECISE_DELTA = 1E-15 304 | private const val EXACT_DELTA = 0.0 305 | 306 | private const val LARGE_SIZE = F64DenseFlatArray.DENSE_SPLIT_SIZE + 1 307 | 308 | private val CASES = listOf( 309 | // Gapped. 310 | gappedArray(1..3), 311 | // Gapped large. 312 | gappedArray(1..LARGE_SIZE), 313 | // Dense small. 314 | doubleArrayOf(4.0, 5.0, 6.0).asF64Array(), 315 | // Dense large. 316 | Random().doubles(LARGE_SIZE.toLong()).toArray().asF64Array(), 317 | // Dense large subarray. 318 | Random().doubles(3L * LARGE_SIZE).toArray() 319 | .asF64Array(LARGE_SIZE, LARGE_SIZE), 320 | // Non-flattenable array. 321 | Random().doubles(4L * 3 * LARGE_SIZE).toArray().asF64Array() 322 | .reshape(4, 3, LARGE_SIZE).view(1, 1) 323 | ) 324 | 325 | internal fun gappedArray(r: IntRange): F64Array { 326 | // The NaN gaps are there for two reasons: 327 | // 328 | // 1. to ensure 'offset' and 'stride' are used correctly, 329 | // 2. to force the use of fallback implementation. 330 | val values = IntStream.range(r.first, r.last + 1) 331 | .mapToDouble(Int::toDouble) 332 | .flatMap { DoubleStream.of(Double.NaN, it) } 333 | .toArray() 334 | return F64FlatArray.create(values, offset = 1, size = r.last + 1 - r.first, stride = 2) 335 | } 336 | 337 | private fun F64Array.checkConstant(copyOp: F64Array.() -> R): R { 338 | val copy = copy() 339 | val res = copyOp() 340 | assertEquals("copying operation changed the array", copy, this) 341 | return res 342 | } 343 | 344 | private fun F64Array.checkConstant(other: F64Array, copyOp: F64Array.(F64Array) -> R): R { 345 | val copy = copy() 346 | val res = copyOp(other) 347 | assertEquals("copying operation changed the array", copy, this) 348 | return res 349 | } -------------------------------------------------------------------------------- /src/test/kotlin/org/jetbrains/bio/viktor/F64ArraySlicingTest.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.bio.viktor 2 | 3 | import org.junit.Assert.assertArrayEquals 4 | import org.junit.Assert.assertEquals 5 | import org.junit.Test 6 | import kotlin.test.assertFailsWith 7 | 8 | class F64FlatArraySlicingTest { 9 | 10 | @Test fun slice() { 11 | val v = F64Array.of(1.0, 2.0, 3.0) 12 | val slice = v.slice(1, 2) 13 | assertEquals(1, slice.length) 14 | assertEquals(F64Array.of(2.0), slice) 15 | 16 | slice[0] = 42.0 17 | assertEquals(F64Array.of(1.0, 42.0, 3.0), v) 18 | } 19 | 20 | @Test fun sliceMatrix() { 21 | val m = F64Array.of( 22 | 1.0, 2.0, 3.0, 23 | 4.0, 5.0, 6.0 24 | ).reshape(2, 3) 25 | assertEquals( 26 | F64Array.of(1.0, 2.0, 3.0).reshape(1, 3), 27 | m.slice(0, 1) 28 | ) 29 | assertEquals( 30 | F64Array.of(4.0, 5.0, 6.0).reshape(1, 3), 31 | m.slice(1, 2) 32 | ) 33 | assertEquals( 34 | F64Array.of( 35 | 1.0, 36 | 4.0 37 | ).reshape(2, 1), 38 | m.slice(0, 1, axis = 1) 39 | ) 40 | assertEquals( 41 | F64Array.of( 42 | 2.0, 3.0, 43 | 5.0, 6.0 44 | ).reshape(2, 2), 45 | m.slice(1, 3, axis = 1) 46 | ) 47 | } 48 | 49 | @Test fun sliceWithStep() { 50 | val v = F64Array.of(1.0, 2.0, 3.0, 4.0, 5.0, 6.0) 51 | 52 | v.slice(step = 2).let { 53 | assertEquals(3, it.length) 54 | assertEquals(F64Array.of(1.0, 3.0, 5.0), it) 55 | } 56 | 57 | v.slice(1, step = 2).let { 58 | assertEquals(3, it.length) 59 | assertEquals(F64Array.of(2.0, 4.0, 6.0), it) 60 | } 61 | 62 | v.slice(1, step = 3).let { 63 | assertEquals(2, it.length) 64 | assertEquals(F64Array.of(2.0, 5.0), it) 65 | } 66 | 67 | v.slice(1, step = 4).let { 68 | assertEquals(2, it.length) 69 | assertEquals(F64Array.of(2.0, 6.0), it) 70 | } 71 | } 72 | 73 | @Test(expected = IllegalStateException::class) fun sliceOutOfBounds() { 74 | F64Array(7).slice(10, 42) 75 | } 76 | 77 | @Test(expected = IllegalArgumentException::class) fun sliceFromNegative() { 78 | F64Array(7).slice(-1, 5) 79 | } 80 | 81 | @Test(expected = IllegalArgumentException::class) fun sliceToBeforeFrom() { 82 | F64Array(7).slice(3, 1) 83 | } 84 | 85 | @Test(expected = IllegalArgumentException::class) fun sliceStepNegative() { 86 | F64Array(7).slice(3, 5, -1) 87 | } 88 | 89 | @Test(expected = IllegalArgumentException::class) fun sliceInvalidAxis() { 90 | F64Array(7).slice(1, 3, axis = 1) 91 | } 92 | } 93 | 94 | class F64ArraySlicing { 95 | 96 | @Test fun rowView() { 97 | val m = F64Array.of( 98 | 0.0, 1.0, 99 | 2.0, 3.0, 100 | 4.0, 5.0 101 | ).reshape(3, 2) 102 | assertEquals(F64Array.of(0.0, 1.0), m.V[0]) 103 | assertEquals(F64Array.of(2.0, 3.0), m.V[1]) 104 | assertEquals(F64Array.of(4.0, 5.0), m.V[2]) 105 | 106 | assertFailsWith { m.V[42] } 107 | } 108 | 109 | @Test fun columnView() { 110 | val m = F64Array.of( 111 | 0.0, 1.0, 112 | 2.0, 3.0, 113 | 4.0, 5.0 114 | ).reshape(3, 2) 115 | 116 | assertEquals(F64Array.of(0.0, 2.0, 4.0), m.V[_I, 0]) 117 | assertEquals(F64Array.of(1.0, 3.0, 5.0), m.V[_I, 1]) 118 | 119 | assertFailsWith { m.V[_I, 42] } 120 | } 121 | 122 | @Test fun view() { 123 | val m = F64Array.of( 124 | 0.0, 1.0, 125 | 2.0, 3.0, 126 | 4.0, 5.0 127 | ).reshape(3, 1, 2) 128 | 129 | assertEquals(F64Array.of(0.0, 1.0).reshape(1, 2), m.V[0]) 130 | assertEquals(F64Array.of(2.0, 3.0).reshape(1, 2), m.V[1]) 131 | assertEquals(F64Array.of(4.0, 5.0).reshape(1, 2), m.V[2]) 132 | 133 | assertFailsWith { m.V[42] } 134 | } 135 | 136 | @Test fun reshape2() { 137 | val v = F64Array.of(0.0, 1.0, 2.0, 3.0, 4.0, 5.0) 138 | assertArrayEquals( 139 | arrayOf( 140 | doubleArrayOf(0.0, 1.0, 2.0), 141 | doubleArrayOf(3.0, 4.0, 5.0) 142 | ), 143 | v.reshape(2, 3).toGenericArray() 144 | ) 145 | assertArrayEquals( 146 | arrayOf( 147 | doubleArrayOf(0.0, 1.0), 148 | doubleArrayOf(2.0, 3.0), 149 | doubleArrayOf(4.0, 5.0) 150 | ), 151 | v.reshape(3, 2).toGenericArray() 152 | ) 153 | } 154 | 155 | @Test fun reshape2WithStride() { 156 | val v = F64FlatArray.create( 157 | doubleArrayOf( 158 | 0.0, 1.0, 2.0, 3.0, 159 | 4.0, 5.0, 6.0, 7.0 160 | ), 161 | 0, size = 4, stride = 2 162 | ) 163 | assertArrayEquals( 164 | arrayOf(doubleArrayOf(0.0, 2.0), doubleArrayOf(4.0, 6.0)), 165 | v.reshape(2, 2).toGenericArray() 166 | ) 167 | } 168 | 169 | @Test fun reshape3() { 170 | val v = F64Array.of( 171 | 0.0, 1.0, 172 | 2.0, 3.0, 173 | 4.0, 5.0 174 | ) 175 | assertArrayEquals( 176 | arrayOf( 177 | arrayOf(doubleArrayOf(0.0, 1.0)), 178 | arrayOf(doubleArrayOf(2.0, 3.0)), 179 | arrayOf(doubleArrayOf(4.0, 5.0)) 180 | ), 181 | v.reshape(3, 1, 2).toGenericArray() 182 | ) 183 | assertArrayEquals( 184 | arrayOf( 185 | arrayOf(doubleArrayOf(0.0), doubleArrayOf(1.0)), 186 | arrayOf(doubleArrayOf(2.0), doubleArrayOf(3.0)), 187 | arrayOf(doubleArrayOf(4.0), doubleArrayOf(5.0)) 188 | ), 189 | v.reshape(3, 2, 1).toGenericArray() 190 | ) 191 | } 192 | 193 | 194 | @Test fun reshape3WithStride() { 195 | val v = F64FlatArray.create(doubleArrayOf(0.0, 1.0, 2.0, 3.0, 196 | 4.0, 5.0, 6.0, 7.0), 197 | 0, size = 4, stride = 2) 198 | assertArrayEquals( 199 | arrayOf( 200 | arrayOf(doubleArrayOf(0.0, 2.0)), 201 | arrayOf(doubleArrayOf(4.0, 6.0)) 202 | ), 203 | v.reshape(2, 1, 2).toGenericArray() 204 | ) 205 | assertArrayEquals( 206 | arrayOf( 207 | arrayOf(doubleArrayOf(0.0), doubleArrayOf(2.0)), 208 | arrayOf(doubleArrayOf(4.0), doubleArrayOf(6.0)) 209 | ), 210 | v.reshape(2, 2, 1).toGenericArray() 211 | ) 212 | } 213 | 214 | @Test fun along0() { 215 | val a = F64Array.of( 216 | 1.0, 2.0, 3.0, 217 | 4.0, 5.0, 6.0 218 | ).reshape(2, 3) 219 | 220 | a.along(0).forEach { it /= it[0] } 221 | assertEquals( 222 | F64Array.of( 223 | 1.0, 2.0, 3.0, 224 | 1.0, 5.0 / 4.0, 6.0 / 4.0 225 | ).reshape(2, 3), 226 | a 227 | ) 228 | } 229 | 230 | @Test fun along1() { 231 | val a = F64Array.of(1.0, 2.0, 3.0, 232 | 4.0, 5.0, 6.0).reshape(2, 3) 233 | 234 | a.along(1).forEach { it /= it[1] } 235 | assertEquals( 236 | F64Array.of( 237 | 1.0 / 4.0, 2.0 / 5.0, 3.0 / 6.0, 238 | 1.0, 1.0, 1.0 239 | ).reshape(2, 3), 240 | a 241 | ) 242 | } 243 | 244 | @Test fun view2() { 245 | val a = DoubleArray(8) { it.toDouble() }.asF64Array().reshape(2, 2, 2) 246 | val aView = a.view(0, 1) 247 | assertEquals(F64Array.of(0.0, 1.0, 4.0, 5.0).reshape(2, 2), aView) 248 | aView.expInPlace() 249 | assertEquals(F64Array.of(2.0, 3.0, 6.0, 7.0).reshape(2, 2), a.view(1, 1)) 250 | } 251 | } -------------------------------------------------------------------------------- /src/test/kotlin/org/jetbrains/bio/viktor/F64FixedArrayOperationTest.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.bio.viktor 2 | 3 | import org.junit.Assert 4 | import org.junit.Test 5 | import kotlin.test.assertNotEquals 6 | 7 | class F64FixedArrayOperationTest { 8 | 9 | @Test fun equals() { 10 | val m = F64Array(2, 3, 4) { i, j, k -> 1.0 * i + 2 * j + 3 * k } 11 | 12 | Assert.assertEquals(m, m) 13 | Assert.assertEquals(m, m.copy()) 14 | assertNotEquals(m, m.exp()) 15 | assertNotEquals(m.toArray(), m) 16 | assertNotEquals(m, m.flatten()) 17 | } 18 | 19 | @Test fun toString2() { 20 | Assert.assertEquals("[[0], [0]]", F64Array(2, 1).toString()) 21 | Assert.assertEquals("[[0, 0]]", F64Array(1, 2).toString()) 22 | } 23 | 24 | @Test fun toString2Large() { 25 | val v = F64Array(1024) { it.toDouble() } 26 | Assert.assertEquals( 27 | "[[0, 1], [2, 3], ..., [1020, 1021], [1022, 1023]]", 28 | v.reshape(512, 2).toString(4) 29 | ) 30 | Assert.assertEquals( 31 | "[[0, 1, ..., 510, 511], [512, 513, ..., 1022, 1023]]", 32 | v.reshape(2, 512).toString(4) 33 | ) 34 | } 35 | 36 | @Test fun toString3() { 37 | Assert.assertEquals("[[[0]]]", F64Array(1, 1, 1).toString()) 38 | Assert.assertEquals( 39 | "[[[0], [0]], [[0], [0]], [[0], [0]]]", 40 | F64Array(3, 2, 1).toString() 41 | ) 42 | } 43 | 44 | @Test fun toStringNormal() { 45 | Assert.assertEquals("[42]", F64Array.of(42.0).toString()) 46 | Assert.assertEquals("[0, 1, 2, 3]", gappedArray(0..3).toString()) 47 | } 48 | 49 | @Test fun toStringLarge() { 50 | val v = gappedArray(0..1023) 51 | Assert.assertEquals("[0, ..., 1023]", v.toString(2)) 52 | Assert.assertEquals("[0, ..., 1022, 1023]", v.toString(3)) 53 | Assert.assertEquals("[0, 1, ..., 1022, 1023]", v.toString(4)) 54 | } 55 | 56 | @Test fun toStringNanInf() { 57 | val v = F64Array.of( 58 | Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, 42.0 59 | ) 60 | Assert.assertEquals("[nan, inf, -inf, 42]", v.toString()) 61 | } 62 | 63 | } -------------------------------------------------------------------------------- /src/test/kotlin/org/jetbrains/bio/viktor/MoreMathTests.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.bio.viktor 2 | 3 | import org.junit.Test 4 | import java.util.* 5 | import kotlin.math.abs 6 | import kotlin.test.assertEquals 7 | import kotlin.test.assertTrue 8 | 9 | class MoreMathTest { 10 | @Test fun testLogAddExpEdgeCases() { 11 | val r = Random() 12 | val logx = -abs(r.nextDouble()) 13 | 14 | assertEquals(logx, Double.NEGATIVE_INFINITY logAddExp logx) 15 | assertEquals(logx, logx logAddExp Double.NEGATIVE_INFINITY) 16 | assertEquals(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY logAddExp Double.NEGATIVE_INFINITY) 17 | 18 | assertEquals(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY logAddExp logx) 19 | assertEquals(Double.POSITIVE_INFINITY, logx logAddExp Double.POSITIVE_INFINITY) 20 | assertEquals(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY logAddExp Double.POSITIVE_INFINITY) 21 | 22 | assertEquals(Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY logAddExp Double.POSITIVE_INFINITY) 23 | assertEquals(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY logAddExp Double.NEGATIVE_INFINITY) 24 | 25 | assertIsNan(Double.NaN logAddExp logx) 26 | assertIsNan(Double.NaN logAddExp Double.NEGATIVE_INFINITY) 27 | assertIsNan(Double.NaN logAddExp Double.POSITIVE_INFINITY) 28 | assertIsNan(Double.NaN logAddExp Double.NaN) 29 | assertIsNan(logx logAddExp Double.NaN) 30 | assertIsNan(Double.NEGATIVE_INFINITY logAddExp Double.NaN) 31 | assertIsNan(Double.POSITIVE_INFINITY logAddExp Double.NaN) 32 | } 33 | 34 | private fun assertIsNan(x: Double) { 35 | assertTrue(x.isNaN(), "Expected NaN but got $x") 36 | } 37 | } 38 | 39 | class KahanSumTest { 40 | @Test fun testPrecision() { 41 | val bigNumber = 10000000 42 | for (d in 9..15) { 43 | // note that in each case 1/d is not precisely representable as a double, 44 | // which is bound to lead to accumulating rounding errors. 45 | val oneDth = 1.0 / d 46 | val preciseSum = KahanSum() 47 | var impreciseSum = 0.0 48 | for (i in 0 until bigNumber * d) { 49 | preciseSum += oneDth 50 | impreciseSum += oneDth 51 | } 52 | 53 | val imprecision = abs(impreciseSum - bigNumber) 54 | val precision = abs(preciseSum.result() - bigNumber) 55 | assertTrue( 56 | imprecision >= precision, 57 | "Kahan's algorithm yielded worse precision than ordinary summation: " + 58 | "$precision is greater than $imprecision" 59 | ) 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /src/test/kotlin/org/jetbrains/bio/viktor/NativeSpeedupTest.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.bio.viktor 2 | 3 | import org.junit.Test 4 | import kotlin.test.assertTrue 5 | 6 | class NativeSpeedupTest { 7 | @Test 8 | fun nativeSpeedupEnabled() { 9 | assertTrue( 10 | Loader.nativeLibraryLoaded, 11 | """ 12 | Native optimizations disabled. 13 | If running as a project: have you added -Djava.library.path=./build/libs to JVM options? 14 | If running from a JAR: is your system supported? 15 | """ 16 | ) 17 | } 18 | } -------------------------------------------------------------------------------- /src/test/kotlin/org/jetbrains/bio/viktor/RandomTests.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.bio.viktor 2 | 3 | import org.apache.commons.math3.stat.StatUtils 4 | import org.apache.commons.math3.util.CombinatoricsUtils 5 | import org.apache.commons.math3.util.Precision 6 | import org.junit.Assert.assertEquals 7 | import org.junit.Test 8 | import org.junit.runner.RunWith 9 | import org.junit.runners.Parameterized 10 | import org.junit.runners.Parameterized.Parameters 11 | import java.util.* 12 | 13 | class QuickSelectTest { 14 | @Test fun quantileRandom() { 15 | val values = Random().doubles(1024).toArray().asF64Array() 16 | for (i in 0 until values.length) { 17 | val q = (i.toDouble() + 1) / values.length 18 | assertEquals(StatUtils.percentile(values.data, q * 100), 19 | values.quantile(q), Precision.EPSILON) 20 | } 21 | } 22 | 23 | @Test(expected = IllegalArgumentException::class) fun quantileEmpty() { 24 | doubleArrayOf().asF64Array().quantile(0.5) 25 | } 26 | 27 | @Test fun quantileSingleton() { 28 | val values = doubleArrayOf(42.0).asF64Array() 29 | assertEquals(42.0, values.quantile(0.0), Precision.EPSILON) 30 | assertEquals(42.0, values.quantile(0.6), Precision.EPSILON) 31 | assertEquals(42.0, values.quantile(1.0), Precision.EPSILON) 32 | } 33 | 34 | @Test fun quantileLarge() { 35 | val values = Random().doubles(1 shl 16).toArray().asF64Array() 36 | assertEquals(values.max(), values.quantile(1.0), Precision.EPSILON) 37 | assertEquals(values.min(), values.quantile(0.0), Precision.EPSILON) 38 | assertEquals(values.quantile(0.5), values.quantile(), Precision.EPSILON) 39 | } 40 | } 41 | 42 | @RunWith(Parameterized::class) 43 | class QuickSelectAgainstR(private val q: Double, 44 | private val expected: Double) { 45 | private val values = doubleArrayOf( 46 | -0.488945108002711, 0.873275572589175, 0.847986479658537, 47 | -1.59886444167727, 0.249354016468346, -0.689076738071403, 48 | 0.0543457714544607, -0.0692308243756736, -1.25989203795668, 49 | 0.382121372812074, -0.009470319810494, -1.8054972148897, 50 | 0.067861610814957, 0.468446958114462, 0.224353680059208, 51 | -0.245225307498896, 0.535946008237022, 0.415292903147929, 52 | 0.891363596131771, -0.729720196710022, 0.163686611195071, 53 | -0.146685146346365, -0.854292328562661, 0.56457235804922, 54 | -0.722260803213915, -0.174921113221112, -1.07241799924636, 55 | -0.522823322920814, 0.885244973408295, -0.346516108433445, 56 | -1.24309985605676, -0.750971331806947, -0.0623048403054132, 57 | 1.08201477922608, -0.350437623032381, 1.60138339981255, 58 | -0.269227746609915, 0.567394813999463, -0.994662221866329, 59 | 0.872948172503996, -0.413080049221564, -1.09568693094285, 60 | 2.85855335947479, -0.366668919549065, -0.689957269702223, 61 | -0.732641734640687, 0.892014735418404, 0.493166028474261, 62 | 0.25428368704835, -0.676160478225966, 0.997101813953564, 63 | 0.168415857016069, -0.464640453789841, 0.732682558827146, 64 | -0.299061308130272, -0.305758044447607, 0.47688050755864, 65 | -0.323423915437573, -1.07443801087424, 1.01833105965806, 66 | 0.106573790832308, -0.0368140390438757, -1.22410154910053, 67 | -0.623308734987264).asF64Array() 68 | 69 | // R uses a different approximation scheme, so we can only compare 70 | // the first to significant digits. 71 | @Test fun agreement() = assertEquals(expected, values.quantile(q), 1e-2) 72 | 73 | companion object { 74 | @JvmStatic 75 | @Parameters(name = "{0}") 76 | fun `data`() = listOf( 77 | arrayOf(0.0, -1.8054972), 78 | arrayOf(0.25, -0.6793895), 79 | arrayOf(0.5, -0.1079580), 80 | arrayOf(0.75, 0.4809519), 81 | arrayOf(1.0, 2.8585534)) 82 | } 83 | } 84 | 85 | class ShuffleTest { 86 | @Test fun distribution() { 87 | val values = doubleArrayOf(0.0, 1.0, 2.0, 3.0).asF64Array() 88 | val counts = HashMap() 89 | 90 | val nFactorial = CombinatoricsUtils.factorial(4).toInt() 91 | for (i in 0..5000 * nFactorial) { 92 | values.shuffle() 93 | val p = values.copy() 94 | counts[p] = (counts[p] ?: 0) + 1 95 | } 96 | 97 | assertEquals(nFactorial, counts.size) 98 | 99 | val total = counts.values.sum() 100 | for (count in counts.values) { 101 | val p = count.toDouble() / total 102 | assertEquals(1.0 / nFactorial, p, 1e-2) 103 | } 104 | } 105 | 106 | @Test fun trivial() { 107 | val values = F64Array.of(3.14) 108 | val stored = values.copy() 109 | values.shuffle() 110 | assertEquals(stored, values) 111 | } 112 | } -------------------------------------------------------------------------------- /src/test/kotlin/org/jetbrains/bio/viktor/SearchingTests.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.bio.viktor 2 | 3 | import org.junit.Test 4 | import kotlin.test.assertEquals 5 | 6 | class SearchingTests { 7 | @Test fun singleLess() { 8 | assertEquals(0, F64Array.of(42.0).searchSorted(0.0)) 9 | assertEquals(0, F64Array.of(42.0, 42.0, 42.0).searchSorted(0.0)) 10 | } 11 | 12 | @Test fun singleGreater() { 13 | assertEquals(1, F64Array.of(0.0).searchSorted(42.0)) 14 | assertEquals(3, F64Array.of(0.0, 0.0, 0.0).searchSorted(42.0)) 15 | } 16 | 17 | @Test fun duplicates() { 18 | assertEquals(0, F64Array.of(0.0, 0.0, 0.0).searchSorted(0.0)) 19 | } 20 | } -------------------------------------------------------------------------------- /src/test/kotlin/org/jetbrains/bio/viktor/SerializationTests.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.bio.viktor 2 | 3 | import org.jetbrains.bio.npy.NpyFile 4 | import org.jetbrains.bio.npy.NpzFile 5 | import org.junit.Test 6 | import kotlin.test.assertEquals 7 | 8 | class TestReadWriteNpy { 9 | @Test fun vector() = withTempFile("v", ".npy") { path -> 10 | val v = F64Array.of(1.0, 2.0, 3.0, 4.0) 11 | NpyFile.write(path, v) 12 | assertEquals(v, NpyFile.read(path).asF64Array()) 13 | } 14 | 15 | @Test fun matrix2() = withTempFile("m2", ".npy") { path -> 16 | val m = F64Array.of(1.0, 2.0, 3.0, 4.0, 5.0, 6.0).reshape(2, 3) 17 | NpyFile.write(path, m) 18 | assertEquals(m, NpyFile.read(path).asF64Array()) 19 | } 20 | 21 | @Test fun matrix3() = withTempFile("m3", ".npy") { path -> 22 | val m = F64Array.of(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0) 23 | .reshape(1, 4, 2) 24 | NpyFile.write(path, m) 25 | assertEquals(m, NpyFile.read(path).asF64Array()) 26 | } 27 | 28 | @Test fun nonFlattenable() = withTempFile("nf", ".npy") { path -> 29 | val m = F64Array.of( 30 | 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0 31 | ).reshape(2, 2, 2).view(1, 1) 32 | NpyFile.write(path, m) 33 | assertEquals(m, NpyFile.read(path).asF64Array()) 34 | } 35 | } 36 | 37 | class TestReadWriteNpz { 38 | @Test fun combined() { 39 | val v = F64Array.of(1.0, 2.0, 3.0, 4.0) 40 | val m2 = F64Array.of(1.0, 2.0, 3.0, 4.0, 5.0, 6.0).reshape(2, 3) 41 | val m3 = F64Array.of( 42 | 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0 43 | ).reshape(1, 4, 2) 44 | 45 | val nf = F64Array.of( 46 | 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0 47 | ).reshape(2, 2, 2).view(1, 1) 48 | 49 | withTempFile("vm2m3", ".npz") { path -> 50 | NpzFile.write(path).use { 51 | it.write("v", v) 52 | it.write("m2", m2) 53 | it.write("m3", m3) 54 | it.write("nf", nf) 55 | } 56 | 57 | NpzFile.read(path).use { 58 | assertEquals(v, it["v"].asF64Array()) 59 | assertEquals(m2, it["m2"].asF64Array()) 60 | assertEquals(m3, it["m3"].asF64Array()) 61 | assertEquals(nf, it["nf"].asF64Array()) 62 | } 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /src/test/kotlin/org/jetbrains/bio/viktor/SortingTests.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.bio.viktor 2 | 3 | import org.apache.commons.math3.util.Precision 4 | import org.junit.Assert.assertArrayEquals 5 | import org.junit.Assert.assertEquals 6 | import org.junit.Test 7 | import java.util.* 8 | import kotlin.test.assertTrue 9 | 10 | class SortingTests { 11 | @Test fun partition() { 12 | assertEquals( 13 | F64Array.of(1.0, 2.0, 3.0, 4.0), 14 | F64Array.of(3.0, 4.0, 2.0, 1.0).apply { partition(2) } 15 | ) 16 | } 17 | 18 | @Test fun partitionInternal() { 19 | val values = F64Array.of(0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0) 20 | val length = values.length 21 | for (p in 0 until length) { 22 | values.shuffle() 23 | val pivot = values[p] 24 | val split = values.partition(p, 0, length - 1) 25 | for (i in 0 until split) { 26 | assertTrue(values[i] < pivot, "<") 27 | } 28 | 29 | for (i in split until length) { 30 | assertTrue(values[i] >= pivot, ">=") 31 | } 32 | } 33 | } 34 | 35 | @Test fun sort() { 36 | val values = Random().doubles().limit(100).toArray() 37 | val v = values.clone().asF64Array() 38 | v.sort() 39 | assertArrayEquals(values.sortedArray(), v.toDoubleArray(), 40 | Precision.EPSILON) 41 | } 42 | 43 | @Test fun argSort() { 44 | val v = F64Array.of(42.0, 2.0, -1.0, 0.0, 4.0, 2.0) 45 | val indices = v.argSort() 46 | val copy = v.toDoubleArray() 47 | copy.sort() 48 | 49 | for ((i, j) in indices.withIndex()) { 50 | assertEquals(copy[i], v[j], Precision.EPSILON) 51 | } 52 | } 53 | 54 | @Test fun argSortReverse() { 55 | val v = F64Array.of(42.0, 2.0, -1.0, 0.0, 4.0, 2.0) 56 | val indices = v.argSort(reverse = true) 57 | val copy = v.toDoubleArray() 58 | copy.sort() 59 | 60 | for ((i, j) in indices.withIndex()) { 61 | assertEquals(copy[copy.size - 1 - i], v[j], Precision.EPSILON) 62 | } 63 | } 64 | 65 | @Test fun argSortWithNaN() { 66 | val values = doubleArrayOf(42.0, 2.0, -1.0, 0.0, 4.0, 2.0) 67 | val indices = values.asF64Array().argSort() 68 | assertArrayEquals(intArrayOf(2, 3, 1, 5, 4, 0), indices) 69 | 70 | val v = F64FlatArray.create( 71 | doubleArrayOf( 72 | Double.NaN, Double.NaN, // Prefix. 73 | 42.0, Double.NaN, 2.0, 74 | Double.NaN, -1.0, 75 | Double.NaN, 0.0, 76 | Double.NaN, 4.0, 77 | Double.NaN, 2.0 78 | ), 79 | offset = 2, size = values.size, stride = 2 80 | ) 81 | v.reorder(indices) 82 | assertArrayEquals( 83 | doubleArrayOf(-1.0, 0.0, 2.0, 2.0, 4.0, 42.0), 84 | v.toDoubleArray(), Precision.EPSILON 85 | ) 86 | } 87 | 88 | @Test fun reorderFlat() { 89 | val v = F64Array.of(1.0, 2.0, 3.0) 90 | v.reorder(intArrayOf(2, 1, 0)) 91 | assertEquals(F64Array.of(3.0, 2.0, 1.0), v) 92 | } 93 | 94 | @Test fun reorderMatrix0() { 95 | val m = F64Array.of( 96 | 1.0, 2.0, 3.0, 97 | 4.0, 5.0, 6.0 98 | ).reshape(2, 3) 99 | m.reorder(intArrayOf(1, 0)) 100 | assertEquals( 101 | F64Array.of( 102 | 4.0, 5.0, 6.0, 103 | 1.0, 2.0, 3.0 104 | ).reshape(2, 3), 105 | m 106 | ) 107 | } 108 | 109 | @Test fun reorderMatrix1() { 110 | val m = F64Array.of( 111 | 1.0, 2.0, 3.0, 112 | 4.0, 5.0, 6.0 113 | ).reshape(2, 3) 114 | m.reorder(intArrayOf(2, 1, 0), axis = 1) 115 | assertEquals( 116 | F64Array.of( 117 | 3.0, 2.0, 1.0, 118 | 6.0, 5.0, 4.0 119 | ).reshape(2, 3), 120 | m 121 | ) 122 | } 123 | 124 | @Test(expected = IllegalArgumentException::class) fun reorderMatrix2() { 125 | val m = F64Array.of( 126 | 1.0, 2.0, 3.0, 127 | 4.0, 5.0, 6.0 128 | ).reshape(2, 3) 129 | m.reorder(intArrayOf(2, 1, 0), axis = 0) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/test/kotlin/org/jetbrains/bio/viktor/TestSupport.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.bio.viktor 2 | 3 | import java.io.IOException 4 | import java.nio.file.Files 5 | import java.nio.file.Path 6 | 7 | internal inline fun withTempFile(prefix: String, suffix: String, 8 | block: (Path) -> Unit) { 9 | val path = Files.createTempFile(prefix, suffix) 10 | try { 11 | block(path) 12 | } finally { 13 | try { 14 | Files.delete(path) 15 | } catch (e: IOException) { 16 | // Mmaped buffer not yet garbage collected. Leave it to the VM. 17 | path.toFile().deleteOnExit() 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/test/kotlin/org/jetbrains/bio/viktor/UnsupportedOpTest.kt: -------------------------------------------------------------------------------- 1 | package org.jetbrains.bio.viktor 2 | 3 | import org.junit.Test 4 | 5 | class UnsupportedOpTest { 6 | 7 | @Test(expected = UnsupportedOperationException::class) 8 | fun invalidArray() { 9 | arrayOf("foo", "bar").toF64Array() 10 | } 11 | 12 | @Test(expected = IllegalStateException::class) 13 | fun emptyArray() { 14 | emptyArray().toF64Array() 15 | } 16 | 17 | @Test(expected = UnsupportedOperationException::class) 18 | fun flatArrayToGeneric() { 19 | F64Array.of(1.0, 2.0, 3.0).toGenericArray() 20 | } 21 | 22 | @Test(expected = UnsupportedOperationException::class) 23 | fun flatArrayAlong() { 24 | F64Array.of(1.0, 2.0, 3.0).along(0) 25 | } 26 | 27 | @Test(expected = UnsupportedOperationException::class) 28 | fun flatArrayView() { 29 | F64Array.of(1.0, 2.0, 3.0).view(0, 0) 30 | } 31 | 32 | @Test(expected = UnsupportedOperationException::class) 33 | fun flatArrayReorder() { 34 | F64Array.of(1.0, 2.0, 3.0).reorder(intArrayOf(2, 1, 0), 1) 35 | } 36 | 37 | @Test(expected = IllegalArgumentException::class) 38 | fun indicesSizeReorder() { 39 | F64Array.of(1.0, 2.0, 3.0).reorder(intArrayOf(1, 0)) 40 | } 41 | 42 | @Test(expected = IllegalStateException::class) 43 | fun mismatchedArray1() { 44 | arrayOf(doubleArrayOf(1.0), doubleArrayOf(2.0, 3.0)).toF64Array() 45 | } 46 | 47 | @Test(expected = IllegalStateException::class) 48 | fun mismatchedArray2() { 49 | arrayOf( 50 | arrayOf(doubleArrayOf(1.0)), 51 | arrayOf(doubleArrayOf(2.0), doubleArrayOf(3.0)) 52 | ).toF64Array() 53 | } 54 | 55 | @Test(expected = IllegalStateException::class) 56 | fun mixedTypeArray1() { 57 | arrayOf(doubleArrayOf(1.0), arrayOf(doubleArrayOf(2.0, 3.0))).toF64Array() 58 | } 59 | 60 | @Test(expected = IllegalStateException::class) 61 | fun mixedTypeArray2() { 62 | arrayOf(arrayOf(doubleArrayOf(1.0, 2.0)), doubleArrayOf(3.0)).toF64Array() 63 | } 64 | 65 | @Test(expected = IllegalStateException::class) 66 | fun nullsArray1() { 67 | arrayOf(doubleArrayOf(1.0, 2.0, 3.0), null).toF64Array() 68 | } 69 | 70 | @Test(expected = IllegalStateException::class) 71 | fun nullsArray2() { 72 | arrayOf(arrayOf(doubleArrayOf(1.0, 2.0, 3.0)), null).toF64Array() 73 | } 74 | 75 | @Test(expected = IllegalStateException::class) 76 | fun nonFlattenable() { 77 | val m = F64Array.full(init = 42.0, shape = intArrayOf(2, 3, 2)).V[_I, 1] 78 | m.flatten() 79 | } 80 | 81 | } 82 | --------------------------------------------------------------------------------