├── .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 | [](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub)
2 | [/statusIcon.svg)](http://teamcity.jetbrains.com/viewType.html?buildTypeId=Epigenome_Tools_Viktor&guest=1)
3 |
4 | [](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 |  | 
66 |  | 
67 |  | 
68 |  | 
69 |  | 
70 |
71 | #### Server
72 |
73 | Array ops/s | FLOPS
74 | ------------|------
75 |  | 
76 |  | 
77 |  | 
78 |  | 
79 |  | 
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 |  | 
93 |  | 
94 |  | 
95 |  | 
96 |
97 | #### Server
98 |
99 | Array ops/s | FLOPS
100 | ------------|------
101 |  | 
102 |  | 
103 |  | 
104 |  | 
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 |
--------------------------------------------------------------------------------