├── .travis.yml ├── LICENSE ├── README.md ├── benchmarks_test.go ├── binary.go ├── binary_test.go ├── blas ├── README.md ├── axpy.go ├── axpy_amd64.s ├── benchmarks_test.go ├── doc.go ├── dot.go ├── dot_amd64.s ├── level1.go ├── level1_test.go ├── level2.go ├── level2_test.go ├── level3.go ├── level3_test.go ├── matrix.go ├── matrix_test.go └── stubs_amd64.go ├── cholesky.go ├── cholesky_test.go ├── compressed.go ├── compressed_arith.go ├── compressed_arith_test.go ├── compressed_test.go ├── coordinate.go ├── coordinate_test.go ├── diagonal.go ├── diagonal_test.go ├── dictionaryofkeys.go ├── dictionaryofkeys_test.go ├── doc.go ├── example_test.go ├── go.mod ├── go.sum ├── matrix.go ├── matrix_test.go ├── persistence.go ├── persistence_test.go ├── pool.go ├── sparse_test.go ├── vector.go └── vector_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - "1.13.x" 5 | - "1.14.x" 6 | - tip 7 | 8 | before_install: 9 | - go get -t -v ./... 10 | 11 | script: 12 | - go test -coverprofile=coverage.txt -covermode=atomic ./... 13 | 14 | after_success: 15 | - bash <(curl -s https://codecov.io/bash) 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 James Bowman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sparse matrix formats 2 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 3 | [![GoDoc](https://godoc.org/github.com/james-bowman/sparse?status.svg)](https://godoc.org/github.com/james-bowman/sparse) 4 | [![Build Status](https://travis-ci.org/james-bowman/sparse.svg?branch=master)](https://travis-ci.org/james-bowman/sparse) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/james-bowman/sparse)](https://goreportcard.com/report/github.com/james-bowman/sparse) 6 | [![codecov](https://codecov.io/gh/james-bowman/sparse/branch/master/graph/badge.svg)](https://codecov.io/gh/james-bowman/sparse) 7 | [![Mentioned in Awesome Go](https://awesome.re/mentioned-badge-flat.svg)](https://github.com/avelino/awesome-go) 8 | [![Sourcegraph](https://sourcegraph.com/github.com/james-bowman/sparse/-/badge.svg)](https://sourcegraph.com/github.com/james-bowman/sparse?badge) 9 | 10 | Implementations of selected sparse matrix formats for linear algebra supporting scientific and machine learning applications. Compatible with the APIs in the [Gonum](http://www.gonum.org/) package and interoperable with Gonum dense matrix types. 11 | 12 | ## Overview 13 | 14 | Machine learning applications typically model entities as vectors of numerical features so that they may be compared and analysed quantitively. Typically the majority of the elements in these vectors are zeros. In the case of text mining applications, each document within a corpus is represented as a vector and its features represent the vocabulary of unique words. A corpus of several thousand documents might utilise a vocabulary of hundreds of thousands (or perhaps even millions) of unique words but each document will typically only contain a couple of hundred unique words. This means the number of non-zero values in the matrix might only be around 1%. 15 | 16 | Sparse matrix formats capitalise on this premise by only storing the non-zero values thereby reducing both storage/memory requirements and processing effort for manipulating the data. 17 | 18 | ## Features 19 | 20 | * Implementations of [Sparse BLAS](http://www.netlib.org/blas/blast-forum/chapter3.pdf) standard routines. 21 | * Compatible with [Gonum's APIs](https://godoc.org/gonum.org/v1/gonum/mat) and interoperable with Gonum's dense matrix types. 22 | * Implemented Formats: 23 | * Sparse Matrix Formats: 24 | * [DOK (Dictionary Of Keys)](https://en.wikipedia.org/wiki/Sparse_matrix#Dictionary_of_keys_(DOK)) format 25 | * [COO (COOrdinate)](https://en.wikipedia.org/wiki/Sparse_matrix#Coordinate_list_(COO)) format (sometimes referred to as 'triplet') 26 | * [CSR (Compressed Sparse Row)](https://en.wikipedia.org/wiki/Sparse_matrix#Compressed_sparse_row_(CSR,_CRS_or_Yale_format)) format 27 | * [CSC (Compressed Sparse Column)](https://en.wikipedia.org/wiki/Sparse_matrix#Compressed_sparse_column_(CSC_or_CCS)) format 28 | * [DIA (DIAgonal)](https://en.wikipedia.org/wiki/Sparse_matrix#Diagonal) format 29 | * sparse vectors 30 | * Other Formats: 31 | * [Binary (Bit) vectors](https://en.wikipedia.org/wiki/Bit_array) and matrices 32 | * Matrix multiplication, addition and subtraction and vector dot products. 33 | 34 | ## Usage 35 | 36 | The sparse matrices in this package implement the Gonum `Matrix` interface and so are fully interoperable and mutually compatible with the Gonum APIs and dense matrix types. 37 | 38 | ``` go 39 | // Construct a new 3x2 DOK (Dictionary Of Keys) matrix 40 | dokMatrix := sparse.NewDOK(3, 2) 41 | 42 | // Populate it with some non-zero values 43 | dokMatrix.Set(0, 0, 5) 44 | dokMatrix.Set(2, 1, 7) 45 | 46 | // Demonstrate accessing values (could use Gonum's mat.Formatted() 47 | // function to pretty print but this demonstrates element access) 48 | m, n := dokMatrix.Dims() 49 | for i := 0; i < m; i++ { 50 | for j := 0; j < n; j++ { 51 | fmt.Printf("%.0f,", dokMatrix.At(i, j)) 52 | } 53 | fmt.Printf("\n") 54 | } 55 | 56 | // Convert DOK matrix to CSR (Compressed Sparse Row) matrix 57 | // just for fun (not required for upcoming multiplication operation) 58 | csrMatrix := dokMatrix.ToCSR() 59 | 60 | // Create a random 2x3 COO (COOrdinate) matrix with 61 | // density of 0.5 (half the elements will be non-zero) 62 | cooMatrix := sparse.Random(sparse.COOFormat, 2, 3, 0.5) 63 | 64 | // Convert CSR matrix to Gonum mat.Dense matrix just for fun 65 | // (not required for upcoming multiplication operation) 66 | // then transpose so it is the right shape/dimensions for 67 | // multiplication with the original CSR matrix 68 | denseMatrix := csrMatrix.ToDense().T() 69 | 70 | // Multiply the 2 matrices together and store the result in the 71 | // sparse receiver (multiplication with sparse product) 72 | var csrProduct sparse.CSR 73 | csrProduct.Mul(csrMatrix, cooMatrix) 74 | 75 | // As an alternative, use the sparse BLAS routines for efficient 76 | // sparse matrix multiplication with a Gonum mat.Dense product 77 | // (multiplication with dense product) 78 | denseProduct := sparse.MulMatMat(false, 1, csrMatrix, denseMatrix, nil) 79 | ``` 80 | 81 | ## Installation 82 | 83 | With Go installed, package installation is performed using go get. 84 | 85 | ``` 86 | go get -u github.com/james-bowman/sparse/... 87 | ``` 88 | 89 | ## Acknowledgements 90 | 91 | * [Gonum](http://www.gonum.org/) 92 | * [Netlib. BLAS. Chapter 3: Sparse BLAS](http://www.netlib.org/blas/blast-forum/chapter3.pdf) 93 | * J.R. Gilbert, C. Moler, and R. Schreiber. Sparse matrices in 94 | MATLAB: Design and implementation. SIAM Journal on Matrix Analysis and 95 | Applications, 13:333–356, 1992. 96 | * F.G. Gustavson. Some basic techniques for solving sparse systems 97 | of linear equations. In D.J. Rose and R.A. Willoughby, eds., Sparse Matrices and 98 | Their Applications, 41–52, New York: Plenum Press, 1972. 99 | * F.G. Gustavson. Efficient algorithm to perform sparse matrix 100 | multiplication. IBM Technical Disclosure Bulletin, 20:1262–1264, 1977. 101 | * [Wikipedia. Sparse Matrix](https://en.wikipedia.org/wiki/Sparse_matrix) 102 | * [A. Fog. 2. Optimizing subroutines in assembly language An optimization guide for x86 platforms, 1996.](https://www.agner.org/optimize/optimizing_assembly.pdf) 103 | 104 | ## See Also 105 | 106 | * [gonum/gonum](https://github.com/gonum/gonum) 107 | 108 | ## License 109 | 110 | MIT -------------------------------------------------------------------------------- /benchmarks_test.go: -------------------------------------------------------------------------------- 1 | package sparse 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "testing" 7 | 8 | "gonum.org/v1/gonum/mat" 9 | ) 10 | 11 | type Product interface { 12 | Mul(a, b mat.Matrix) 13 | Add(a, b mat.Matrix) 14 | } 15 | 16 | var products = []Product{ 17 | &mat.Dense{}, 18 | &CSR{}, 19 | } 20 | 21 | var densities = []float32{ 22 | 0.01, 23 | //0.1, 24 | 0.4, 25 | // 0.6, 26 | } 27 | 28 | type MatMultiplyer interface { 29 | Mul(a, b mat.Matrix) 30 | } 31 | 32 | func benchmarkMatrixMultiplication(target MatMultiplyer, lhs mat.Matrix, rhs mat.Matrix, b *testing.B) { 33 | for n := 0; n < b.N; n++ { 34 | target.Mul(lhs, rhs) 35 | } 36 | } 37 | 38 | // DIAgonal Multiplication 39 | 40 | func BenchmarkMulLargeCSRDIACSR(b *testing.B) { 41 | t := CreateCSR(0, 0, nil).(*CSR) 42 | lhs := RandomDIA(500, 600) 43 | rhs := Random(CSRFormat, 600, 500, 0.01) 44 | benchmarkMatrixMultiplication(t, lhs, rhs, b) 45 | } 46 | 47 | func BenchmarkMulLargeCSRCSRDIA(b *testing.B) { 48 | t := CreateCSR(0, 0, nil).(*CSR) 49 | lhs := Random(CSRFormat, 500, 600, 0.01) 50 | rhs := RandomDIA(600, 500) 51 | benchmarkMatrixMultiplication(t, lhs, rhs, b) 52 | } 53 | 54 | func BenchmarkMulLargeCSRDIACSC(b *testing.B) { 55 | t := CreateCSR(0, 0, nil).(*CSR) 56 | lhs := RandomDIA(500, 600) 57 | rhs := Random(CSCFormat, 600, 500, 0.01) 58 | benchmarkMatrixMultiplication(t, lhs, rhs, b) 59 | } 60 | 61 | func BenchmarkMulLargeCSRCSCDIA(b *testing.B) { 62 | t := CreateCSR(0, 0, nil).(*CSR) 63 | lhs := Random(CSCFormat, 500, 600, 0.01) 64 | rhs := RandomDIA(600, 500) 65 | benchmarkMatrixMultiplication(t, lhs, rhs, b) 66 | } 67 | 68 | func BenchmarkMulLargeCSRDIADense(b *testing.B) { 69 | t := CreateCSR(0, 0, nil).(*CSR) 70 | lhs := RandomDIA(500, 600) 71 | rhs := Random(DenseFormat, 600, 500, 0.01) 72 | benchmarkMatrixMultiplication(t, lhs, rhs, b) 73 | } 74 | 75 | func BenchmarkMulLargeCSRDenseDIA(b *testing.B) { 76 | t := CreateCSR(0, 0, nil).(*CSR) 77 | lhs := Random(DenseFormat, 500, 600, 0.01) 78 | rhs := RandomDIA(600, 500) 79 | benchmarkMatrixMultiplication(t, lhs, rhs, b) 80 | } 81 | 82 | func RandomDIA(r, c int) *DIA { 83 | var min int 84 | if r < c { 85 | min = r 86 | } else { 87 | min = c 88 | } 89 | data := make([]float64, min) 90 | for i := range data { 91 | data[i] = rand.Float64() 92 | } 93 | return NewDIA(r, c, data) 94 | } 95 | 96 | func BenchmarkAdd(b *testing.B) { 97 | var dimensions = []struct { 98 | ar, ac, br, bc int 99 | }{ 100 | //{ar: 5, ac: 6, br: 5, bc: 6}, 101 | {ar: 500, ac: 600, br: 500, bc: 600}, 102 | } 103 | 104 | benchmarks := []struct { 105 | name string 106 | a MatrixType 107 | b MatrixType 108 | }{ 109 | { 110 | name: "CSR+CSR", 111 | a: CSRFormat, 112 | b: CSRFormat, 113 | }, 114 | { 115 | name: "CSR+Dense", 116 | a: CSRFormat, 117 | b: DenseFormat, 118 | }, 119 | { 120 | name: "Dense+CSR", 121 | a: DenseFormat, 122 | b: CSRFormat, 123 | }, 124 | { 125 | name: "Dense+Dense", 126 | a: DenseFormat, 127 | b: DenseFormat, 128 | }, 129 | { 130 | name: "CSR+CSC", 131 | a: CSRFormat, 132 | b: CSCFormat, 133 | }, 134 | { 135 | name: "CSC+CSC", 136 | a: CSCFormat, 137 | b: CSCFormat, 138 | }, 139 | } 140 | 141 | for _, dims := range dimensions { 142 | for _, density := range densities { 143 | for _, c := range products { 144 | for _, bench := range benchmarks { 145 | if cs, resetable := c.(mat.Reseter); resetable { 146 | cs.Reset() 147 | } 148 | aMat := Random(bench.a, dims.ar, dims.ac, density) 149 | bMat := Random(bench.b, dims.br, dims.bc, density) 150 | 151 | b.Run(fmt.Sprintf("%dx%d (%.2f) %T=%s", dims.ar, dims.bc, density, c, bench.name), func(b *testing.B) { 152 | for i := 0; i < b.N; i++ { 153 | c.Add(aMat, bMat) 154 | } 155 | }) 156 | } 157 | } 158 | } 159 | } 160 | } 161 | 162 | func BenchmarkMul(b *testing.B) { 163 | var dimensions = []struct { 164 | ar, ac, br, bc int 165 | }{ 166 | //{ar: 5, ac: 6, br: 6, bc: 5}, 167 | {ar: 500, ac: 600, br: 600, bc: 500}, 168 | } 169 | 170 | benchmarks := []struct { 171 | name string 172 | a MatrixType 173 | b MatrixType 174 | }{ 175 | { 176 | name: "Dense*Dense", 177 | a: DenseFormat, 178 | b: DenseFormat, 179 | }, 180 | { 181 | name: "Dense*CSR", 182 | a: CSRFormat, 183 | b: DenseFormat, 184 | }, 185 | { 186 | name: "CSR*Dense", 187 | a: CSRFormat, 188 | b: DenseFormat, 189 | }, 190 | { 191 | name: "CSR*CSR", 192 | a: CSRFormat, 193 | b: CSRFormat, 194 | }, 195 | { 196 | name: "CSR*CSC", 197 | a: CSRFormat, 198 | b: CSCFormat, 199 | }, 200 | { 201 | name: "CSC*CSR", 202 | a: CSCFormat, 203 | b: CSRFormat, 204 | }, 205 | { 206 | name: "CSC*CSC", 207 | a: CSCFormat, 208 | b: CSCFormat, 209 | }, 210 | { 211 | name: "CSR*DOK", 212 | a: CSRFormat, 213 | b: DOKFormat, 214 | }, 215 | { 216 | name: "Dense*DOK", 217 | a: DenseFormat, 218 | b: DOKFormat, 219 | }, 220 | { 221 | name: "DOK*DOK", 222 | a: DOKFormat, 223 | b: DOKFormat, 224 | }, 225 | // { 226 | // name: "CSR*COO", 227 | // a: CSRFormat, 228 | // b: COOFormat, 229 | // }, 230 | } 231 | 232 | for _, dims := range dimensions { 233 | for _, density := range densities { 234 | for _, c := range products { 235 | for _, bench := range benchmarks { 236 | if cs, resetable := c.(mat.Reseter); resetable { 237 | cs.Reset() 238 | } 239 | aMat := Random(bench.a, dims.ar, dims.ac, density) 240 | bMat := Random(bench.b, dims.br, dims.bc, density) 241 | 242 | b.Run(fmt.Sprintf("%dx%d (%.2f) %T=%s", dims.ar, dims.bc, density, c, bench.name), func(b *testing.B) { 243 | for i := 0; i < b.N; i++ { 244 | c.Mul(aMat, bMat) 245 | } 246 | }) 247 | } 248 | } 249 | } 250 | } 251 | } 252 | 253 | func BenchmarkBLASMulMatMat(b *testing.B) { 254 | var dimensions = []struct { 255 | ar, ac, br, bc int 256 | }{ 257 | //{ar: 5, ac: 6, br: 6, bc: 5}, 258 | {ar: 500, ac: 600, br: 600, bc: 500}, 259 | } 260 | benchmarks := []struct { 261 | name string 262 | transA bool 263 | alpha float64 264 | a MatrixType 265 | b MatrixType 266 | }{ 267 | { 268 | name: "CSRxDense", 269 | transA: false, 270 | alpha: 1, 271 | a: CSRFormat, 272 | b: DenseFormat, 273 | }, 274 | { 275 | name: "CSCxDense", 276 | transA: false, 277 | alpha: 1, 278 | a: CSCFormat, 279 | b: DenseFormat, 280 | }, 281 | { 282 | name: "COOxDense", 283 | transA: false, 284 | alpha: 1, 285 | a: COOFormat, 286 | b: DenseFormat, 287 | }, 288 | { 289 | name: "DOKxDense", 290 | transA: false, 291 | alpha: 1, 292 | a: DOKFormat, 293 | b: DenseFormat, 294 | }, 295 | { 296 | name: "CSRxCSC", 297 | transA: false, 298 | alpha: 1, 299 | a: CSRFormat, 300 | b: CSCFormat, 301 | }, 302 | { 303 | name: "CSRxCSR", 304 | transA: false, 305 | alpha: 1, 306 | a: CSRFormat, 307 | b: CSRFormat, 308 | }, 309 | { 310 | name: "CSRxCOO", 311 | transA: false, 312 | alpha: 1, 313 | a: CSRFormat, 314 | b: COOFormat, 315 | }, 316 | { 317 | name: "CSCxCSC", 318 | transA: false, 319 | alpha: 1, 320 | a: CSCFormat, 321 | b: CSCFormat, 322 | }, 323 | { 324 | name: "CSCxCSR", 325 | transA: false, 326 | alpha: 1, 327 | a: CSCFormat, 328 | b: CSRFormat, 329 | }, 330 | { 331 | name: "CSCxCOO", 332 | transA: false, 333 | alpha: 1, 334 | a: CSCFormat, 335 | b: COOFormat, 336 | }, 337 | } 338 | 339 | for _, dims := range dimensions { 340 | for _, density := range densities { 341 | for _, bench := range benchmarks { 342 | cMat := mat.NewDense(dims.ar, dims.bc, nil) 343 | aMat := Random(bench.a, dims.ar, dims.ac, density).(BlasCompatibleSparser) 344 | bMat := Random(bench.b, dims.br, dims.bc, density) 345 | 346 | c := cMat.RawMatrix() 347 | for i := range c.Data { 348 | c.Data[i] = 0 349 | } 350 | 351 | b.Run(fmt.Sprintf("%dx%d (%.2f) %s", dims.ar, dims.bc, density, bench.name), func(b *testing.B) { 352 | for i := 0; i < b.N; i++ { 353 | cMat = MulMatMat(bench.transA, 1, aMat, bMat, cMat) 354 | } 355 | }) 356 | } 357 | } 358 | } 359 | } 360 | 361 | // dot is a package level variable to hold the result of dot benchmark to prevent 362 | // compiler from optimising out the call. 363 | var dot float64 364 | 365 | func BenchmarkDot(b *testing.B) { 366 | rnd := rand.New(rand.NewSource(0)) 367 | population := 0.01 368 | dim := 100000 369 | 370 | adata := make([]float64, dim) 371 | bdata := make([]float64, dim) 372 | 373 | pop := int(float64(dim) * population) 374 | for i := 1; i <= pop; i++ { 375 | adata[rnd.Intn(dim)] = float64(i) 376 | bdata[rnd.Intn(dim)] = float64(i) 377 | } 378 | 379 | benchmarks := []struct { 380 | name string 381 | af vector 382 | bf vector 383 | fn func(mat.Vector, mat.Vector) float64 384 | }{ 385 | {name: "Mat Dense Dense", af: denseVec, bf: denseVec, fn: mat.Dot}, 386 | {name: "Mat Sparse Sparse", af: sparseVec, bf: sparseVec, fn: mat.Dot}, 387 | {name: "Mat Dense Sparse", af: denseVec, bf: sparseVec, fn: mat.Dot}, 388 | {name: "Mat Sparse Dense", af: denseVec, bf: sparseVec, fn: mat.Dot}, 389 | 390 | {name: "Sparse Sparse Sparse", af: sparseVec, bf: sparseVec, fn: Dot}, 391 | {name: "Sparse Sparse Dense", af: sparseVec, bf: denseVec, fn: Dot}, 392 | {name: "Sparse Dense Sparse", af: denseVec, bf: sparseVec, fn: Dot}, 393 | {name: "Sparse Dense Dense", af: denseVec, bf: denseVec, fn: Dot}, 394 | } 395 | 396 | for _, bench := range benchmarks { 397 | av := bench.af(adata) 398 | bv := bench.bf(bdata) 399 | 400 | b.Run(bench.name, func(b *testing.B) { 401 | for n := 0; n < b.N; n++ { 402 | dot = bench.fn(av, bv) 403 | } 404 | }) 405 | } 406 | } 407 | 408 | type vector func([]float64) mat.Vector 409 | 410 | func sparseVec(s []float64) mat.Vector { 411 | var data []float64 412 | var ind []int 413 | 414 | for i, v := range s { 415 | if v != 0 { 416 | data = append(data, v) 417 | ind = append(ind, i) 418 | } 419 | } 420 | return NewVector(len(s), ind, data) 421 | } 422 | 423 | func denseVec(s []float64) mat.Vector { 424 | return mat.NewVecDense(len(s), s) 425 | } 426 | 427 | func BenchmarkNorm(b *testing.B) { 428 | ind := []int{0, 100, 200, 300, 400, 500, 600, 700, 800, 900} 429 | data := []float64{2, 2, 2, 2, 2, 2, 2, 2, 2, 2} 430 | vec := NewVector(1000, ind, data) 431 | 432 | benchmarks := []struct { 433 | name string 434 | f func(float64) float64 435 | }{ 436 | {name: "norm", f: vec.Norm}, 437 | } 438 | 439 | var v float64 440 | for _, bench := range benchmarks { 441 | b.Run(bench.name, func(b *testing.B) { 442 | for n := 0; n < b.N; n++ { 443 | v = bench.f(2) 444 | } 445 | }) 446 | } 447 | _ = v 448 | } 449 | 450 | var traceResult float64 451 | 452 | func BenchmarkTraceCS(b *testing.B) { 453 | formats := []MatrixType{CSRFormat, CSCFormat} 454 | for s := 100; s <= 1600; s *= 2 { 455 | for _, density := range densities { 456 | for _, format := range formats { 457 | a := Random(format, s, s, density) 458 | b.Run(fmt.Sprintf("BenchTraceCS%d/%f-%d", format, density, s), func(b *testing.B) { 459 | for n := 0; n < b.N; n++ { 460 | traceResult = mat.Trace(a) 461 | } 462 | }) 463 | } 464 | } 465 | } 466 | } 467 | 468 | func BenchmarkTraceDIA(b *testing.B) { 469 | for s := 100; s <= 1600; s *= 2 { 470 | a := RandomDIA(s, s) 471 | b.Run(fmt.Sprintf("BenchTraceDIA-%d", s), func(b *testing.B) { 472 | for n := 0; n < b.N; n++ { 473 | traceResult = mat.Trace(a) 474 | } 475 | }) 476 | } 477 | } 478 | -------------------------------------------------------------------------------- /binary.go: -------------------------------------------------------------------------------- 1 | package sparse 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "unsafe" 7 | 8 | "gonum.org/v1/gonum/mat" 9 | ) 10 | 11 | var ( 12 | _ mat.Matrix = (*BinaryVec)(nil) 13 | _ mat.Vector = (*BinaryVec)(nil) 14 | 15 | _ mat.Matrix = (*Binary)(nil) 16 | _ mat.ColViewer = (*Binary)(nil) 17 | ) 18 | 19 | const ( 20 | // maxLen is the biggest slice/array len one can create on a 32/64b platform. 21 | //bitLen = ^uint(0) >> 1 22 | maxDataLen = ^uint(0) 23 | 24 | // the wordSize of a binary vector. Bits are stored in slices of words. 25 | wordSize = uint(64) 26 | 27 | // log2WordSize is the binary logarithm (log base 2) of (wordSize) 28 | log2WordSize = uint(6) 29 | ) 30 | 31 | // BinaryVec is a Binary Vector or Bit Vector type. This is useful 32 | // for representing vectors of features with a binary state (1 or 0). 33 | // Although part of the sparse package, this type is not sparse itself 34 | // and stores all bits even 0s. However, as it makes use of 64 bit 35 | // integers to store each set of 64 bits and then bitwise operators to 36 | // manipulate the elements it will be more efficient in terms of both 37 | // storage requirements and performance than a slice of bool values 38 | // (8 bits per element) or even a typical Dense matrix of float64 39 | // elements. A compressed bitmap scheme could be used to take advantage 40 | // of sparseness but may have an associated overhead. 41 | type BinaryVec struct { 42 | length int 43 | data []uint64 44 | } 45 | 46 | // NewBinaryVec creates a new BitSet with a hint that length bits will be required 47 | func NewBinaryVec(length int) *BinaryVec { 48 | maxSize := (maxDataLen - wordSize + uint(1)) 49 | if uint(length) > maxSize { 50 | panic(fmt.Errorf("sparse: Requested bit length of Binary vector (%d) too large. %d is the maximum allowed", length, maxSize)) 51 | } 52 | elements := int((uint(length) + (wordSize - 1)) >> log2WordSize) 53 | 54 | vec := &BinaryVec{ 55 | length: length, 56 | data: make([]uint64, elements), 57 | } 58 | 59 | return vec 60 | } 61 | 62 | // DistanceFrom is the number of bits that are different between the 63 | // receiver and rhs i.e. 64 | // recevier = 1001001 65 | // rhs = 1010101 66 | // Distance = 3 67 | // because there are three bits that are different between the 2 68 | // binary vectors. This is sometimes referred to as the `Hamming 69 | // distance` or `Matching distance`. In this case, the distance 70 | // is not normalised and is simply the raw count of differences. To 71 | // normalise the value simply divide this value by the vector's length. 72 | func (b *BinaryVec) DistanceFrom(rhs *BinaryVec) int { 73 | differences := uint64(0) 74 | for i, word := range b.data { 75 | differences += popcount(word ^ rhs.data[i]) 76 | } 77 | return int(differences) 78 | } 79 | 80 | // Dims returns the dimensions of the matrix as the number of rows, columns. 81 | // As this is a vector, the second value representing the number of columns 82 | // will be 1. This method is part of the Gonum mat.Matrix interface 83 | func (b *BinaryVec) Dims() (int, int) { 84 | return b.Len(), 1 85 | } 86 | 87 | // At returns the value of the element at row i and column j. 88 | // As this is a vector (only one column), j must be 0 otherwise the 89 | // method panics. This method is part of the Gonum mat.Matrix interface. 90 | func (b *BinaryVec) At(i, j int) float64 { 91 | if i < 0 || i >= b.length { 92 | panic(mat.ErrRowAccess) 93 | } 94 | if j != 0 { 95 | panic(mat.ErrColAccess) 96 | } 97 | 98 | if b.bitIsSet(i) { 99 | return 1.0 100 | } 101 | return 0.0 102 | } 103 | 104 | // AtVec returns the value of the element at row i. This method will panic if 105 | // i > Len(). This method is part of the Gonum mat.Vector interface. 106 | func (b *BinaryVec) AtVec(i int) float64 { 107 | if i < 0 || i >= b.length { 108 | panic(mat.ErrRowAccess) 109 | } 110 | 111 | if b.bitIsSet(i) { 112 | return 1.0 113 | } 114 | return 0.0 115 | } 116 | 117 | // T performs an implicit transpose by returning the receiver inside a Transpose. 118 | // This method is part of the Gonum mat.Matrix interface 119 | func (b *BinaryVec) T() mat.Matrix { 120 | return mat.TransposeVec{Vector: b} 121 | } 122 | 123 | // NNZ returns the Number of Non-Zero elements (bits). This is the number of set 124 | // bits (represented by 1s rather than 0s) in the vector. This is also known as the 125 | // `Hamming weight` or `population count` (popcount). 126 | func (b *BinaryVec) NNZ() int { 127 | nnz := uint64(0) 128 | for _, word := range b.data { 129 | nnz += popcount(word) 130 | } 131 | return int(nnz) 132 | } 133 | 134 | // Len returns the length of the vector or the total number of elements. This method 135 | // is part of the Gonum mat.Vector interface 136 | func (b *BinaryVec) Len() int { 137 | return b.length 138 | } 139 | 140 | // BitIsSet tests whether the element (bit) at position i is set (equals 1) and 141 | // returns true if so. If the element (bit) is not set or has been unset (equal 142 | // to 0) the the method will return false. The method will panic if i is greater 143 | // than Len(). 144 | func (b *BinaryVec) BitIsSet(i int) bool { 145 | if i < 0 || i >= b.length { 146 | panic(mat.ErrRowAccess) 147 | } 148 | return b.bitIsSet(i) 149 | } 150 | 151 | // bitIsSet tests whether the element (bit) at position i is set (equals 1) and 152 | // returns true if so. If the element (bit) is not set or has been unset (equal 153 | // to 0) the the method will return false. 154 | func (b *BinaryVec) bitIsSet(i int) bool { 155 | return b.data[i>>log2WordSize]&(1<<(uint(i)&(wordSize-1))) != 0 156 | } 157 | 158 | // SetBit sets the bit at the specified index (i) to 1. If the bit is already set 159 | // there are no adverse effects. The method will panic if index is larger 160 | // than Len() 161 | func (b *BinaryVec) SetBit(i int) { 162 | if i < 0 || i >= b.length { 163 | panic(mat.ErrRowAccess) 164 | } 165 | b.setBit(i) 166 | } 167 | 168 | // setBit sets the bit at the specified index (i) to 1. If the bit is already set 169 | // there are no adverse effects. 170 | func (b *BinaryVec) setBit(i int) { 171 | b.data[i>>log2WordSize] |= 1 << (uint(i) & (wordSize - 1)) 172 | } 173 | 174 | // UnsetBit unsets the bit at the specified index (i) (sets it to 0). If the bit 175 | // is already unset or has simply never been set (default bit values are 0) 176 | // there are no adverse effects. The method will panic if index is larger 177 | // than Len() 178 | func (b *BinaryVec) UnsetBit(i int) { 179 | if i < 0 || i >= b.length { 180 | panic(mat.ErrRowAccess) 181 | } 182 | b.unsetBit(i) 183 | } 184 | 185 | // unsetBit unsets the bit at the specified index (i) (sets it to 0). If the bit 186 | // is already unset or has simply never been set (default bit values are 0) 187 | // there are no adverse effects. 188 | func (b *BinaryVec) unsetBit(i int) { 189 | b.data[i>>log2WordSize] &^= 1 << (uint(i) & (wordSize - 1)) 190 | } 191 | 192 | // Set sets the element of the matrix located at row i and column j to 1 if v != 0 193 | // or 0 otherwise. Set will panic if specified values for i or j fall outside the 194 | // dimensions of the matrix. 195 | func (b *BinaryVec) Set(i int, j int, v float64) { 196 | if i < 0 || i >= b.length { 197 | panic(mat.ErrRowAccess) 198 | } 199 | if j != 0 { 200 | panic(mat.ErrColAccess) 201 | } 202 | 203 | if v != 0 { 204 | b.setBit(i) 205 | return 206 | } 207 | b.unsetBit(i) 208 | } 209 | 210 | // SetVec sets the element of the vector located at row i to 1 if v != 0 211 | // or 0 otherwise. The method will panic if i is greater than Len(). 212 | func (b *BinaryVec) SetVec(i int, v float64) { 213 | if i < 0 || i >= b.length { 214 | panic(mat.ErrRowAccess) 215 | } 216 | 217 | if v != 0 { 218 | b.setBit(i) 219 | return 220 | } 221 | b.unsetBit(i) 222 | } 223 | 224 | // SliceToUint64 returns a new uint64. 225 | // The returned matrix starts at element from of the receiver and extends 226 | // to - from rows. The final row in the resulting matrix is to-1. 227 | // Slice panics with ErrIndexOutOfRange if the slice is outside the capacity 228 | // of the receiver. 229 | func (b *BinaryVec) SliceToUint64(from, to int) uint64 { 230 | if from < 0 || to <= from || to > b.length || to-from > 64 { 231 | panic(mat.ErrIndexOutOfRange) 232 | } 233 | 234 | var result uint64 235 | var k uint64 236 | for i := from; i < to; i++ { 237 | if b.bitIsSet(i) { 238 | result |= 1 << k 239 | } 240 | k++ 241 | } 242 | 243 | return result 244 | } 245 | 246 | // String will output the vector as a string representation of its bits 247 | // This method implements the fmt.Stringer interface. 248 | func (b BinaryVec) String() string { 249 | buf := bytes.NewBuffer(make([]byte, 0, b.Len())) 250 | 251 | width := b.length % int(wordSize) 252 | if width == 0 { 253 | width = 64 254 | } 255 | 256 | fmt.Fprintf(buf, fmt.Sprintf("%%0%db", width), b.data[len(b.data)-1]) 257 | for i := len(b.data) - 2; i >= 0; i-- { 258 | fmt.Fprintf(buf, "%064b", b.data[i]) 259 | } 260 | 261 | s := buf.Bytes() 262 | return *(*string)(unsafe.Pointer(&s)) 263 | } 264 | 265 | // Format outputs the vector to f and allows the output format 266 | // to be specified. Supported values of c are `x`, `X`, `b`` and `s` 267 | // to format the bits of the vector as a hex digit or binary digit string. 268 | // `s` (the default format) will output as binary digits. 269 | // Please refer to the fmt package documentation for more information. 270 | // This method implements the fmt.Formatter interface. 271 | func (b BinaryVec) Format(f fmt.State, c rune) { 272 | var buf bytes.Buffer 273 | var format string 274 | var leadFormat string 275 | switch c { 276 | case 'x': 277 | format = ".%x" 278 | leadFormat = "%x" 279 | case 'X': 280 | format = ".%X" 281 | leadFormat = "%X" 282 | case 'b': 283 | f.Write([]byte(b.String())) 284 | return 285 | case 's': 286 | f.Write([]byte(b.String())) 287 | return 288 | default: 289 | panic(fmt.Errorf("sparse: unsupported format verb '%c' for Binary vector", c)) 290 | } 291 | fmt.Fprintf(&buf, leadFormat, b.data[len(b.data)-1]) 292 | for i := len(b.data) - 2; i >= 0; i-- { 293 | fmt.Fprintf(&buf, format, b.data[i]) 294 | } 295 | f.Write(buf.Bytes()) 296 | } 297 | 298 | // popcount calculates the population count of the vector (also known 299 | // as `Hamming weight`). This uses fewer arithmetic operations than 300 | // any other known implementation on machines with fast multiplication. 301 | // Thanks to Wikipedia and Hacker's Delight. 302 | func popcount(x uint64) (n uint64) { 303 | x -= (x >> 1) & 0x5555555555555555 304 | x = (x>>2)&0x3333333333333333 + x&0x3333333333333333 305 | x += x >> 4 306 | x &= 0x0f0f0f0f0f0f0f0f 307 | x *= 0x0101010101010101 308 | return x >> 56 309 | } 310 | 311 | // Binary is a Binary Matrix or Bit Matrix type. 312 | // Although part of the sparse package, this type is not sparse itself 313 | // and stores all bits even 0s. However, as it makes use of 64 bit 314 | // integers to store each set of 64 bits and then bitwise operators to 315 | // manipulate the elements it will be more efficient in terms of both 316 | // storage requirements and performance than a slice of bool values 317 | // (8 bits per element) or even a typical Dense matrix of float64 318 | // elements. A compressed bitmap scheme could be used to take advantage 319 | // of sparseness but may have an associated overhead. 320 | type Binary struct { 321 | r, c int 322 | cols []BinaryVec 323 | } 324 | 325 | // NewBinary constructs a new Binary matrix or r rows and c columns. 326 | // If vecs is not nil, it will be used as the underlying binary column vectors. 327 | // If vecs is nil, new storage will be allocated. 328 | func NewBinary(r, c int, vecs []BinaryVec) *Binary { 329 | if vecs == nil { 330 | vecs = make([]BinaryVec, c) 331 | for i := 0; i < c; i++ { 332 | vecs[i] = *NewBinaryVec(r) 333 | } 334 | } 335 | 336 | return &Binary{r: r, c: c, cols: vecs} 337 | } 338 | 339 | // Dims returns the dimensions of the matrix as the number of rows, columns. 340 | // This method is part of the Gonum mat.Matrix interface 341 | func (b *Binary) Dims() (int, int) { 342 | return b.r, b.c 343 | } 344 | 345 | // At returns the value of the element at row i and column k. 346 | // i (row) and j (col) must be within the dimensions of the matrix otherwise the 347 | // method panics. This method is part of the Gonum mat.Matrix interface. 348 | func (b *Binary) At(i int, j int) float64 { 349 | if j < 0 || j >= b.c { 350 | panic(mat.ErrColAccess) 351 | } 352 | return b.cols[j].AtVec(i) 353 | } 354 | 355 | // T performs an implicit transpose by returning the receiver inside a Transpose. 356 | // This method is part of the Gonum mat.Matrix interface 357 | func (b *Binary) T() mat.Matrix { 358 | return mat.Transpose{Matrix: b} 359 | } 360 | 361 | // ColView returns the mat.Vector representing the column j. This vector will 362 | // be a BinaryVec and will share the same storage as the matrix so any changes 363 | // to the vector will be reflected in the matrix and vice versa. 364 | // if j is outside the dimensions of the matrix the method will panic. 365 | func (b *Binary) ColView(j int) mat.Vector { 366 | if j < 0 || j >= b.c { 367 | panic(mat.ErrColAccess) 368 | } 369 | return &b.cols[j] 370 | } 371 | -------------------------------------------------------------------------------- /binary_test.go: -------------------------------------------------------------------------------- 1 | package sparse 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "testing" 7 | ) 8 | 9 | func createBinVec(bitString string) (*BinaryVec, error) { 10 | bits := []rune(bitString) 11 | vec := NewBinaryVec(len(bits)) 12 | 13 | for i, bit := range bits { 14 | f, err := strconv.ParseFloat(string(bit), 64) 15 | if err != nil { 16 | return vec, err 17 | } 18 | vec.Set((len(bits)-1)-i, 0, f) 19 | } 20 | 21 | return vec, nil 22 | } 23 | 24 | func TestBinaryVectorManipulation(t *testing.T) { 25 | tests := []struct { 26 | bits string 27 | }{ 28 | { 29 | bits: "000000000000000000000000000000000000000000000000000000000000001", 30 | }, 31 | { 32 | bits: "100000000000000000000000000000000000000000000000000000000000000", 33 | }, 34 | { 35 | bits: "000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000001", 36 | }, 37 | } 38 | 39 | for ti, test := range tests { 40 | bits := []rune(test.bits) 41 | 42 | vec, err := createBinVec(test.bits) 43 | if err != nil { 44 | t.Errorf("Test %d: Failed to parse bits because %v", ti, err) 45 | } 46 | 47 | if len(bits) != vec.Len() { 48 | t.Errorf("Test %d: Vector is the wrong length, expected %d but received %d", ti, len(bits), vec.Len()) 49 | } 50 | 51 | for i, bit := range bits { 52 | b := vec.At((len(bits)-1)-i, 0) 53 | f, err := strconv.ParseFloat(string(bit), 64) 54 | if err != nil { 55 | t.Errorf("Test %d: Failed to parse bit (%s) as float because %v", ti, string(bit), err) 56 | } 57 | if b != f { 58 | t.Errorf("Test %d: Failed to get bit expected %s (%f) but received %f", ti, string(bit), b, f) 59 | } 60 | } 61 | 62 | } 63 | } 64 | 65 | func TestBinaryFormat(t *testing.T) { 66 | tests := []struct { 67 | bits string 68 | format rune 69 | expected string 70 | }{ 71 | { // 64 bits (1 whole word) 72 | bits: "0000000000000000000000000000000000000000000000000000000000000001", 73 | format: 's', 74 | expected: "0000000000000000000000000000000000000000000000000000000000000001", 75 | }, 76 | { // 128 bits (2 whole words) 77 | bits: "00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001", 78 | format: 's', 79 | expected: "00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001", 80 | }, 81 | { // 66 bits (2 words) 82 | bits: "010000000000000000000000000000000000000000000000000000000000000001", 83 | format: 's', 84 | expected: "010000000000000000000000000000000000000000000000000000000000000001", 85 | }, 86 | { // 37 bits (1 word) 87 | bits: "0000000000000000000000000000000001001", 88 | format: 'b', 89 | expected: "0000000000000000000000000000000001001", 90 | }, 91 | { // 64 bits (1 whole word) 92 | bits: "1000000000000000000000000000000000000000000000000000000000001001", 93 | format: 'b', 94 | expected: "1000000000000000000000000000000000000000000000000000000000001001", 95 | }, 96 | { // 89 bits (2 words) 97 | bits: "00000000000000000000000100000000000000000000000000000000000000000000000000000000000000001", 98 | format: 'b', 99 | expected: "00000000000000000000000100000000000000000000000000000000000000000000000000000000000000001", 100 | }, 101 | { // 65 bits (2 words) 102 | bits: "10000000000000000000000000000000000000000000000000000000000000001", 103 | format: 'b', 104 | expected: "10000000000000000000000000000000000000000000000000000000000000001", 105 | }, 106 | { 107 | bits: "0000000000000000000000000000000000000000000000000000000000000001", 108 | format: 'x', 109 | expected: "1", 110 | }, 111 | { 112 | bits: "0000000000000000000000000000000000000000000000000000000000001101", 113 | format: 'x', 114 | expected: "d", 115 | }, 116 | { 117 | bits: "0000000000000000000000000000000000000000000000000000000000001101", 118 | format: 'X', 119 | expected: "D", 120 | }, 121 | { 122 | bits: "10000000000000000000000000000000000000000000000000000000000001101", 123 | format: 'X', 124 | expected: "1.D", 125 | }, 126 | } 127 | 128 | for ti, test := range tests { 129 | vec, err := createBinVec(test.bits) 130 | if err != nil { 131 | t.Errorf("Test %d: Failed to parse bits because %v", ti, err) 132 | } 133 | 134 | out := fmt.Sprintf(fmt.Sprintf("%%%c", test.format), vec) 135 | 136 | if test.expected != out { 137 | t.Errorf("Test %d: Binary Format failed, expected '%s' but received '%s'\n", ti, test.expected, out) 138 | } 139 | } 140 | } 141 | 142 | func TestBinaryNNZ(t *testing.T) { 143 | tests := []struct { 144 | bits string 145 | nnz int 146 | }{ 147 | { 148 | bits: "0000000000000000000000000000000000000000000000000000000000000001", 149 | nnz: 1, 150 | }, 151 | { 152 | bits: "0000100000000000000000000000110000000001000000010000000000000001", 153 | nnz: 6, 154 | }, 155 | { 156 | bits: "10000000000000000000000010000000000000000010000000000110000000001000000000000000000000000000000000000000000000000000000000000011", 157 | nnz: 8, 158 | }, 159 | } 160 | 161 | for ti, test := range tests { 162 | vec, err := createBinVec(test.bits) 163 | if err != nil { 164 | t.Errorf("Test %d: Failed to parse bits because %v", ti, err) 165 | } 166 | 167 | if test.nnz != vec.NNZ() { 168 | t.Errorf("Test %d: NNZ incorrect, expected %d but received %d", ti, test.nnz, vec.NNZ()) 169 | } 170 | } 171 | } 172 | 173 | func TestBinaryDistance(t *testing.T) { 174 | tests := []struct { 175 | bitsa string 176 | bitsb string 177 | distance int 178 | }{ 179 | { 180 | bitsa: "0000000000000000000000000000000000000000000000000000000000000001", 181 | bitsb: "0000000000000000000000000000000000000000000000000000000000000000", 182 | distance: 1, 183 | }, 184 | { 185 | bitsa: "0000000000000000000000000000000000000000000000000000000000000000", 186 | bitsb: "0000000000000000000000000000000000000000000000000000000000000001", 187 | distance: 1, 188 | }, 189 | { 190 | bitsa: "0001000000000000000000000000010110000000000000000000000000000001", 191 | bitsb: "0000000000000000000000000000001010000000000000000000000000000000", 192 | distance: 5, 193 | }, 194 | } 195 | 196 | for ti, test := range tests { 197 | veca, err := createBinVec(test.bitsa) 198 | if err != nil { 199 | t.Errorf("Test %d: Failed to parse bitsa because %v", ti, err) 200 | } 201 | vecb, err := createBinVec(test.bitsb) 202 | if err != nil { 203 | t.Errorf("Test %d: Failed to parse bitsb because %v", ti, err) 204 | } 205 | 206 | distance := veca.DistanceFrom(vecb) 207 | 208 | if test.distance != distance { 209 | t.Errorf("Test %d: Distance incorrect, expected %d but received %d", ti, test.distance, distance) 210 | } 211 | } 212 | } 213 | 214 | func TestBinarySliceToUint64(t *testing.T) { 215 | tests := []struct { 216 | bits string 217 | from int 218 | to int 219 | expectedSlice string 220 | }{ 221 | { 222 | bits: "0000000000000000000000000000000000111001101", 223 | from: 0, 224 | to: 8, 225 | expectedSlice: "11001101", 226 | }, 227 | { 228 | bits: "110101010000", 229 | from: 3, 230 | to: 11, 231 | expectedSlice: "10101010", 232 | }, 233 | { 234 | bits: "0101010100000000000000010001001101", 235 | from: 10, 236 | to: 11, 237 | expectedSlice: "1", 238 | }, 239 | { 240 | bits: "0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000100", 241 | from: 2, 242 | to: 66, 243 | expectedSlice: "1000000000000000000000000000000000000000000000000000000000000001", 244 | }, 245 | } 246 | 247 | for ti, test := range tests { 248 | vec, err := createBinVec(test.bits) 249 | 250 | if err != nil { 251 | t.Errorf("Test %d: failed because %v\n", ti, err) 252 | } 253 | 254 | val := vec.SliceToUint64(test.from, test.to) 255 | 256 | result := fmt.Sprintf("%b", val) 257 | 258 | if test.expectedSlice != result { 259 | t.Errorf("Test %d: Expected %s but received %s\n", ti, test.expectedSlice, result) 260 | } 261 | } 262 | } 263 | 264 | func TestBinary(t *testing.T) { 265 | tests := []struct { 266 | b *Binary 267 | }{ 268 | { 269 | b: NewBinary(4, 3, []BinaryVec{ 270 | {length: 4, data: []uint64{1}}, 271 | {length: 4, data: []uint64{15}}, 272 | {length: 4, data: []uint64{10}}, 273 | }), 274 | }, 275 | } 276 | 277 | for ti, test := range tests { 278 | r, c := test.b.Dims() 279 | tb := test.b.T() 280 | tr, tc := tb.Dims() 281 | 282 | if tr != c && tc != r { 283 | t.Errorf("Test %d: Dimensions of transpose (%dx%d) do not match original (%dx%d)", ti, tr, tc, r, c) 284 | } 285 | 286 | for j := 0; j < c; j++ { 287 | v := test.b.ColView(j).(*BinaryVec) 288 | for i := 0; i < r; i++ { 289 | vv := v.BitIsSet(i) 290 | bv := test.b.At(i, j) == 1 291 | if vv != bv { 292 | t.Errorf("Test %d: Values don't match - Expected %t but found %t", ti, vv, bv) 293 | } 294 | } 295 | } 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /blas/README.md: -------------------------------------------------------------------------------- 1 | # Sparse matrix formats 2 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 3 | [![GoDoc](https://godoc.org/github.com/james-bowman/sparse/blas?status.svg)](https://godoc.org/github.com/james-bowman/sparse/blas) 4 | [![Sourcegraph](https://sourcegraph.com/github.com/james-bowman/sparse/blas/-/badge.svg)](https://sourcegraph.com/github.com/james-bowman/sparse/blas?badge) 5 | 6 | Implementation of sparse BLAS (Basic Linear Algebra Subprograms) routines in Go for 7 | sparse matrix arithmetic. See http://www.netlib.org/blas/blast-forum/chapter3.pdf for more details. Includes optimised assembler implementations of key kernel operations (vector dot product and Axpy operations). 8 | 9 | ## Indicative Benchmarks 10 | 11 | | Operation | Pure Go | Assembler | 12 | | -------------------------------- | ------------ | ------------ | 13 | | Dusdot (with increment/stride) | 1340 ns/op | 978 ns/op | 14 | | Dusdot (unitary) | 1215 ns/op | 662 ns/op | 15 | | Dusaxpy (with increment/stride) | 1944 ns/op | 1769 ns/op | 16 | | Dusaxpy (unitary) | 1091 ns/op | 979 ns/op | 17 | -------------------------------------------------------------------------------- /blas/axpy.go: -------------------------------------------------------------------------------- 1 | // +build !amd64 noasm appengine safe 2 | 3 | package blas 4 | 5 | // Dusaxpy (Sparse update (y <- alpha * x + y)) scales the sparse vector x by 6 | // alpha and adds the result to the dense vector y. indx is used as the index 7 | // values to gather and incy as the stride for y. 8 | func Dusaxpy(alpha float64, x []float64, indx []int, y []float64, incy int) { 9 | for i, index := range indx { 10 | y[index*incy] += alpha * x[i] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /blas/axpy_amd64.s: -------------------------------------------------------------------------------- 1 | //+build !noasm,!appengine,!safe 2 | 3 | #include "textflag.h" 4 | 5 | // func Dusaxpy(alpha float64, x []float64, indx []int, y []float64, incy int) 6 | TEXT ·Dusaxpy(SB), NOSPLIT, $0 7 | MOVSD alpha+0(FP), X0 8 | MOVQ x+8(FP), SI 9 | MOVQ indx+32(FP), R8 10 | MOVQ indx+40(FP), AX 11 | MOVQ y+56(FP), CX 12 | MOVQ incy+80(FP), DI 13 | 14 | MOVAPS X0, X1 // alpha in X0 and X1 for pipelining 15 | XORL R9, R9 16 | 17 | SUBQ $4, AX // len(indx)-4 18 | 19 | LEAQ (SI)(AX*8), R14 // R14 = &indx[len(indx)-4] 20 | LEAQ (R8)(AX*8), R15 // R15 = &indx[len(indx)-4] 21 | 22 | SUBQ AX, R9 23 | JG tailstart 24 | 25 | loop: 26 | MOVSD (R14)(R9*8), X2 // X2 := x[i] 27 | MOVSD 8(R14)(R9*8), X3 // X3 := x[i+1] 28 | MOVSD 16(R14)(R9*8), X4 // X4 := x[i+2] 29 | MOVSD 24(R14)(R9*8), X5 // X5 := x[i+3] 30 | 31 | MOVQ (R15)(R9*8), R10 // R10 := indx[i] 32 | MOVQ 8(R15)(R9*8), R11 // R11 := indx[i+1] 33 | MOVQ 16(R15)(R9*8), R12 // R12 := indx[i+2] 34 | MOVQ 24(R15)(R9*8), R13 // R13 := indx[i+3] 35 | 36 | IMULQ DI, R10 // R10 *= incy 37 | IMULQ DI, R11 // R11 *= incy 38 | IMULQ DI, R12 // R12 *= incy 39 | IMULQ DI, R13 // R13 *= incy 40 | 41 | MULSD X0, X2 42 | MULSD X1, X3 43 | MULSD X0, X4 44 | MULSD X1, X5 45 | 46 | ADDSD (CX)(R10*8), X2 47 | ADDSD (CX)(R11*8), X3 48 | ADDSD (CX)(R12*8), X4 49 | ADDSD (CX)(R13*8), X5 50 | 51 | MOVSD X2, (CX)(R10*8) 52 | MOVSD X3, (CX)(R11*8) 53 | MOVSD X4, (CX)(R12*8) 54 | MOVSD X5, (CX)(R13*8) 55 | 56 | ADDQ $4, R9 // i += 4 57 | JLE loop 58 | 59 | tailstart: 60 | SUBQ $4, R9 61 | JNS end 62 | 63 | tail: 64 | // one more y[indx[i]*incy] += alpha * x[i] 65 | MOVSD 32(R14)(R9*8), X2 // X1 := x[i : i+1] 66 | MOVQ 32(R15)(R9*8), R10 // R10 := indx[i] 67 | IMULQ DI, R10 // R10 *= incy 68 | MULSD X0, X2 69 | ADDSD (CX)(R10*8), X2 70 | MOVSD X2, (CX)(R10*8) 71 | 72 | ADDQ $1, R9 73 | JS tail 74 | 75 | end: 76 | RET 77 | -------------------------------------------------------------------------------- /blas/benchmarks_test.go: -------------------------------------------------------------------------------- 1 | package blas 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "golang.org/x/exp/rand" 8 | 9 | "gonum.org/v1/gonum/stat/sampleuv" 10 | ) 11 | 12 | func BenchmarkAxpy(b *testing.B) { 13 | nnz := 1000 14 | dim := 10000 15 | x := make([]float64, nnz) 16 | indx := make([]int, nnz) 17 | y := make([]float64, (dim)*(dim)) 18 | 19 | rnd := rand.New(rand.NewSource(0)) 20 | 21 | sampleuv.WithoutReplacement(indx, dim, rnd) 22 | 23 | for i := range x { 24 | x[i] = rnd.Float64() 25 | } 26 | 27 | for i := range y { 28 | y[i] = rnd.Float64() 29 | } 30 | 31 | inputs := []struct { 32 | name string 33 | alpha float64 34 | x []float64 35 | indx []int 36 | y []float64 37 | incy int 38 | }{ 39 | { 40 | name: "inc", 41 | alpha: 1, 42 | x: x, 43 | indx: indx, 44 | y: y, 45 | incy: dim, 46 | }, 47 | { 48 | name: "unitary", 49 | alpha: 1, 50 | x: x, 51 | indx: indx, 52 | y: y[:dim], 53 | incy: 1, 54 | }, 55 | } 56 | 57 | benchmarks := []struct { 58 | name string 59 | f func(alpha float64, x []float64, indx []int, y []float64, incy int) 60 | }{ 61 | { 62 | name: "Naive Go", 63 | f: func(alpha float64, x []float64, indx []int, y []float64, incy int) { 64 | for i, index := range indx { 65 | y[index*incy] += alpha * x[i] 66 | } 67 | }, 68 | }, 69 | { 70 | name: "Asm", 71 | f: Dusaxpy, 72 | }, 73 | } 74 | 75 | for _, input := range inputs { 76 | for _, bench := range benchmarks { 77 | b.Run(fmt.Sprintf("%s %s", input.name, bench.name), func(b *testing.B) { 78 | for i := 0; i < b.N; i++ { 79 | bench.f(input.alpha, input.x, input.indx, input.y, input.incy) 80 | } 81 | }) 82 | } 83 | } 84 | } 85 | 86 | func BenchmarkDot(b *testing.B) { 87 | nnz := 1000 88 | dim := 10000 89 | x := make([]float64, nnz) 90 | indx := make([]int, nnz) 91 | y := make([]float64, (dim)*(dim)) 92 | 93 | rnd := rand.New(rand.NewSource(0)) 94 | 95 | sampleuv.WithoutReplacement(indx, dim, rnd) 96 | 97 | for i := range x { 98 | x[i] = rnd.Float64() 99 | } 100 | 101 | for i := range y { 102 | y[i] = rnd.Float64() 103 | } 104 | 105 | inputs := []struct { 106 | name string 107 | x []float64 108 | indx []int 109 | y []float64 110 | incy int 111 | }{ 112 | { 113 | name: "inc", 114 | x: x, 115 | indx: indx, 116 | y: y, 117 | incy: dim, 118 | }, 119 | { 120 | name: "unitary", 121 | x: x, 122 | indx: indx, 123 | y: y[:dim], 124 | incy: 1, 125 | }, 126 | } 127 | 128 | benchmarks := []struct { 129 | name string 130 | f func(x []float64, indx []int, y []float64, incy int) float64 131 | }{ 132 | { 133 | name: "Naive Go", 134 | f: func(x []float64, indx []int, y []float64, incy int) (dot float64) { 135 | for i, index := range indx { 136 | dot += x[i] * y[index*incy] 137 | } 138 | return 139 | }, 140 | }, 141 | { 142 | name: "Asm", 143 | f: Dusdot, 144 | }, 145 | } 146 | 147 | for _, input := range inputs { 148 | for _, bench := range benchmarks { 149 | b.Run(fmt.Sprintf("%s %s", input.name, bench.name), func(b *testing.B) { 150 | for i := 0; i < b.N; i++ { 151 | bench.f(input.x, input.indx, input.y, input.incy) 152 | } 153 | }) 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /blas/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package blas provides implementations of sparse BLAS (Basic Linear Algebra Subprograms) routines 3 | for sparse matrix arithmetic and solving sparse linear systems. 4 | 5 | See http://www.netlib.org/blas/blast-forum/chapter3.pdf for further information. 6 | */ 7 | package blas 8 | -------------------------------------------------------------------------------- /blas/dot.go: -------------------------------------------------------------------------------- 1 | // +build !amd64 noasm appengine safe 2 | 3 | package blas 4 | 5 | // Dusdot (Sparse dot product (r <- x^T * y)) calculates the dot product of 6 | // sparse vector x and dense vector y. indx is used as the index 7 | // values to gather and incy as the stride for y. 8 | func Dusdot(x []float64, indx []int, y []float64, incy int) (dot float64) { 9 | for i, index := range indx { 10 | dot += x[i] * y[index*incy] 11 | } 12 | return 13 | } 14 | -------------------------------------------------------------------------------- /blas/dot_amd64.s: -------------------------------------------------------------------------------- 1 | //+build !noasm,!appengine,!safe 2 | 3 | #include "textflag.h" 4 | 5 | // func Dusdot(x []float64, indx []int, y []float64, incy int) (dot float64) 6 | TEXT ·Dusdot(SB), NOSPLIT, $0 7 | MOVQ x+0(FP), R8 8 | MOVQ indx+24(FP), SI 9 | MOVQ y+48(FP), DX 10 | MOVQ indx+32(FP), AX // len(indx) 11 | MOVQ incy+72(FP), CX 12 | 13 | XORPS X0, X0 // 2 accumulators for reduced dependencies and 14 | XORPS X9, X9 // better software pipelining 15 | 16 | SUBQ $4, AX 17 | JL tailstart 18 | 19 | loop: 20 | MOVQ (SI), R10 21 | MOVQ 8(SI), R11 22 | MOVQ 16(SI), R12 23 | MOVQ 24(SI), R13 24 | 25 | MOVUPD (R8), X1 // load packed pairs of elements from x into SSE registers 26 | MOVUPD 16(R8), X3 27 | 28 | IMULQ CX, R10 // multiply indx (indices of x) by incy (stride for y) to 29 | IMULQ CX, R11 // calculate corresponding indexes for y 30 | IMULQ CX, R12 31 | IMULQ CX, R13 32 | 33 | MOVLPD (DX)(R10*8), X2 // pack scattered elements of y into SSE registers (gather) 34 | MOVHPD (DX)(R11*8), X2 35 | MOVLPD (DX)(R12*8), X4 36 | MOVHPD (DX)(R13*8), X4 37 | 38 | MULPD X2, X1 39 | MULPD X4, X3 40 | 41 | ADDPD X1, X0 42 | ADDPD X3, X9 43 | 44 | ADDQ $32, SI // increment pointers by 4*sizeOf(float64) 45 | ADDQ $32, R8 46 | 47 | SUBQ $4, AX 48 | JGE loop 49 | 50 | tailstart: 51 | ADDQ $4, AX 52 | JE end 53 | 54 | tail: 55 | // process remaining elements individually where length is not divisible by 4 56 | MOVQ (SI), R10 57 | MOVSD (R8), X1 58 | IMULQ CX, R10 59 | 60 | MULSD (DX)(R10*8), X1 61 | ADDSD X1, X0 62 | 63 | ADDQ $8, SI 64 | ADDQ $8, R8 65 | 66 | SUBQ $1, AX 67 | JNE tail 68 | 69 | end: 70 | ADDPD X9, X0 // add accumulators together 71 | MOVSD X0, X7 // unpack SSE register and add 2 values together 72 | UNPCKHPD X0, X0 73 | ADDSD X7, X0 74 | 75 | MOVSD X0, dot+80(FP) 76 | RET 77 | -------------------------------------------------------------------------------- /blas/level1.go: -------------------------------------------------------------------------------- 1 | package blas 2 | 3 | // Dusga (Sparse gather (x <- y|x)) gathers entries from the dense vector 4 | // y into the sparse vector x using indx as the index values to gather 5 | // and incy as the stride for y. 6 | func Dusga(y []float64, incy int, x []float64, indx []int) { 7 | for i, index := range indx { 8 | x[i] = y[index*incy] 9 | } 10 | } 11 | 12 | // Dusgz (Sparse gather and zero (x <- y|x, y|x <- 0)) gathers entries 13 | // from the dense vector y into the sparse vector x 14 | // (as Usga()) and then sets the corresponding elements of y (y[indx[i]]) 15 | // to 0. indx is used as the index values to gather and incy as the stride 16 | // for y. 17 | func Dusgz(y []float64, incy int, x []float64, indx []int) { 18 | for i, index := range indx { 19 | x[i] = y[index*incy] 20 | y[index*incy] = 0 21 | } 22 | } 23 | 24 | // Dussc (Sparse scatter (y|x <- x)) scatters enries into the dense vector y 25 | // from the sparse vector x using indx as the index values to scatter to 26 | // and incy as the stride for y. The function will panic if x and idx are 27 | // different lengths. 28 | func Dussc(x []float64, y []float64, incy int, indx []int) { 29 | for i, index := range indx { 30 | y[index*incy] = x[i] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /blas/level1_test.go: -------------------------------------------------------------------------------- 1 | package blas 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestDusdot(t *testing.T) { 8 | tests := []struct { 9 | x []float64 10 | indx []int 11 | y []float64 12 | incy int 13 | expected float64 14 | }{ 15 | { // 1 16 | x: []float64{1, 3, 4}, 17 | indx: []int{0, 2, 3}, 18 | y: []float64{1, 2, 3, 4}, 19 | incy: 1, 20 | expected: 26, 21 | }, 22 | { // 2 23 | x: []float64{1, 3, 4, 5}, 24 | indx: []int{0, 2, 3, 4}, 25 | y: []float64{1, 2, 3, 4, 5}, 26 | incy: 1, 27 | expected: 51, 28 | }, 29 | { // 3 30 | x: []float64{1, 3, 4, 5, 6}, 31 | indx: []int{0, 2, 3, 4, 5}, 32 | y: []float64{1, 2, 3, 4, 5, 6}, 33 | incy: 1, 34 | expected: 87, 35 | }, 36 | { // 4 37 | x: []float64{1, 3, 4, 5, 6, 7, 8, 9}, 38 | indx: []int{0, 2, 3, 4, 5, 6, 7, 8, 9}, 39 | y: []float64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, 40 | incy: 1, 41 | expected: 281, 42 | }, 43 | { // 5 44 | x: []float64{1, 3, 4, 5}, 45 | indx: []int{0, 2, 3, 4}, 46 | y: []float64{1, 2, 3, 4, 5}, 47 | incy: 1, 48 | expected: 51, 49 | }, 50 | { // 6 51 | x: []float64{1, 3, 4}, 52 | indx: []int{0, 2, 3}, 53 | y: []float64{ 54 | 1, 5, 5, 5, 55 | 2, 5, 5, 5, 56 | 3, 5, 5, 5, 57 | 4, 5, 5, 5, 58 | }, 59 | incy: 4, 60 | expected: 26, 61 | }, 62 | { // 7 63 | x: []float64{1, 3, 4, 5, 6}, 64 | indx: []int{0, 2, 3, 4, 5}, 65 | y: []float64{ 66 | 1, 5, 5, 5, 67 | 2, 5, 5, 5, 68 | 3, 5, 5, 5, 69 | 4, 5, 5, 5, 70 | 5, 5, 5, 5, 71 | 6, 5, 5, 5, 72 | }, 73 | incy: 4, 74 | expected: 87, 75 | }, 76 | { // 8 77 | x: []float64{}, 78 | indx: []int{}, 79 | y: []float64{1, 2, 3, 4}, 80 | incy: 1, 81 | expected: 0, 82 | }, 83 | { // 9 84 | x: []float64{2}, 85 | indx: []int{2}, 86 | y: []float64{1, 2, 3, 4}, 87 | incy: 1, 88 | expected: 6, 89 | }, 90 | { // 10 91 | x: []float64{1, 2}, 92 | indx: []int{0, 2}, 93 | y: []float64{1, 2, 3, 4}, 94 | incy: 1, 95 | expected: 7, 96 | }, 97 | { // 11 98 | x: []float64{3, 4, 5}, 99 | indx: []int{0, 1, 3}, 100 | y: []float64{1, 2, 3, 4}, 101 | incy: 1, 102 | expected: 31, 103 | }, 104 | } 105 | 106 | for ti, test := range tests { 107 | dot := Dusdot(test.x, test.indx, test.y, test.incy) 108 | 109 | if dot != test.expected { 110 | t.Errorf("Test %d: Wanted %f but received %f", ti+1, test.expected, dot) 111 | } 112 | } 113 | } 114 | 115 | func TestDusaxpy(t *testing.T) { 116 | tests := []struct { 117 | alpha float64 118 | x []float64 119 | indx []int 120 | y []float64 121 | incy int 122 | expected []float64 123 | }{ 124 | { 125 | alpha: 1, 126 | x: []float64{1, 3, 4}, 127 | indx: []int{0, 2, 3}, 128 | y: []float64{0, 0, 0, 0}, 129 | incy: 1, 130 | expected: []float64{1, 0, 3, 4}, 131 | }, 132 | { 133 | alpha: 1, 134 | x: []float64{1, 3, 4}, 135 | indx: []int{0, 2, 3}, 136 | y: []float64{1, 2, 3, 4}, 137 | incy: 1, 138 | expected: []float64{2, 2, 6, 8}, 139 | }, 140 | { 141 | alpha: 2, 142 | x: []float64{1, 3, 4}, 143 | indx: []int{0, 2, 3}, 144 | y: []float64{ 145 | 1, 5, 5, 5, 146 | 2, 5, 5, 5, 147 | 3, 5, 5, 5, 148 | 4, 5, 5, 5, 149 | }, 150 | incy: 4, 151 | expected: []float64{ 152 | 3, 5, 5, 5, 153 | 2, 5, 5, 5, 154 | 9, 5, 5, 5, 155 | 12, 5, 5, 5, 156 | }, 157 | }, 158 | } 159 | 160 | for ti, test := range tests { 161 | Dusaxpy(test.alpha, test.x, test.indx, test.y, test.incy) 162 | 163 | for i, y := range test.y { 164 | if y != test.expected[i] { 165 | t.Errorf("Test %d: Wanted %f at %d but received %f", ti+1, test.expected[i], i, y) 166 | } 167 | } 168 | } 169 | } 170 | 171 | func TestDusga(t *testing.T) { 172 | tests := []struct { 173 | x []float64 174 | indx []int 175 | y []float64 176 | incy int 177 | expected []float64 178 | }{ 179 | { 180 | x: []float64{0, 0, 0}, 181 | indx: []int{0, 2, 3}, 182 | y: []float64{1, 2, 3, 4}, 183 | incy: 1, 184 | expected: []float64{1, 3, 4}, 185 | }, 186 | { 187 | x: []float64{5, 5, 5}, 188 | indx: []int{0, 2, 3}, 189 | y: []float64{ 190 | 1, 5, 5, 5, 191 | 5, 5, 5, 5, 192 | 3, 5, 5, 5, 193 | 4, 5, 5, 5, 194 | }, 195 | incy: 4, 196 | expected: []float64{1, 3, 4}, 197 | }, 198 | } 199 | 200 | for ti, test := range tests { 201 | Dusga(test.y, test.incy, test.x, test.indx) 202 | 203 | for i := range test.indx { 204 | if test.x[i] != test.expected[i] { 205 | t.Errorf("Test %d: Wanted %f at %d but received %f", ti+1, test.expected[i], i, test.x[i]) 206 | } 207 | } 208 | } 209 | } 210 | 211 | func TestDusgz(t *testing.T) { 212 | tests := []struct { 213 | x []float64 214 | indx []int 215 | y []float64 216 | incy int 217 | expected []float64 218 | }{ 219 | { 220 | x: []float64{0, 0, 0}, 221 | indx: []int{0, 2, 3}, 222 | y: []float64{1, 2, 3, 4}, 223 | incy: 1, 224 | expected: []float64{1, 3, 4}, 225 | }, 226 | { 227 | x: []float64{5, 5, 5}, 228 | indx: []int{0, 2, 3}, 229 | y: []float64{ 230 | 1, 5, 5, 5, 231 | 5, 5, 5, 5, 232 | 3, 5, 5, 5, 233 | 4, 5, 5, 5, 234 | }, 235 | incy: 4, 236 | expected: []float64{1, 3, 4}, 237 | }, 238 | } 239 | 240 | for ti, test := range tests { 241 | Dusgz(test.y, test.incy, test.x, test.indx) 242 | 243 | for i, v := range test.indx { 244 | if test.x[i] != test.expected[i] { 245 | t.Errorf("Test %d: Wanted %f at %d but received %f", ti+1, test.expected[i], i, test.x[i]) 246 | } 247 | if test.y[v*test.incy] != 0 { 248 | t.Errorf("Test %d: Expected %d element zeroed", ti+1, i) 249 | } 250 | } 251 | } 252 | } 253 | 254 | func TestDussc(t *testing.T) { 255 | tests := []struct { 256 | x []float64 257 | indx []int 258 | y []float64 259 | incy int 260 | expected []float64 261 | }{ 262 | { 263 | x: []float64{1, 3, 4}, 264 | indx: []int{0, 2, 3}, 265 | y: []float64{0, 0, 0, 0}, 266 | incy: 1, 267 | expected: []float64{1, 0, 3, 4}, 268 | }, 269 | { 270 | x: []float64{1, 3, 4}, 271 | indx: []int{0, 2, 3}, 272 | y: []float64{ 273 | 5, 5, 5, 5, 274 | 5, 5, 5, 5, 275 | 5, 5, 5, 5, 276 | 5, 5, 5, 5, 277 | }, 278 | incy: 4, 279 | expected: []float64{ 280 | 1, 5, 5, 5, 281 | 5, 5, 5, 5, 282 | 3, 5, 5, 5, 283 | 4, 5, 5, 5, 284 | }, 285 | }, 286 | } 287 | 288 | for ti, test := range tests { 289 | Dussc(test.x, test.y, test.incy, test.indx) 290 | 291 | for i, y := range test.y { 292 | if y != test.expected[i] { 293 | t.Errorf("Test %d: Wanted %f at %d but received %f", ti+1, test.expected[i], i, y) 294 | } 295 | } 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /blas/level2.go: -------------------------------------------------------------------------------- 1 | package blas 2 | 3 | // Dusmv (sparse matrix / vector multiply (y <- alpha * A * x + y Or y <- alpha * A^T * x + y)) 4 | // multiplies a dense vector x by sparse matrix a (or its transpose), and adds it 5 | // to the dense vector y. transA is a boolean indicating whether to transpose (true) a. 6 | // alpha is used to scale a and incx and incy represent the span to be used for indexing into 7 | // vectors x and y respectively. 8 | func Dusmv(transA bool, alpha float64, a *SparseMatrix, x []float64, incx int, y []float64, incy int) { 9 | r := a.I 10 | 11 | if alpha == 0 { 12 | return 13 | } 14 | 15 | if transA { 16 | for i := 0; i < r; i++ { 17 | begin, end := a.Indptr[i], a.Indptr[i+1] 18 | Dusaxpy(alpha*x[i*incx], a.Data[begin:end], a.Ind[begin:end], y, incy) 19 | } 20 | } else { 21 | for i := 0; i < r; i++ { 22 | begin, end := a.Indptr[i], a.Indptr[i+1] 23 | y[i*incy] += alpha * Dusdot(a.Data[begin:end], a.Ind[begin:end], x, incx) 24 | } 25 | } 26 | } 27 | 28 | // Dussv (sparse triangular solve (x <- alpha * T^-1 * x Or x <- alpha * T^T * x)) solves 29 | // a system of equations where x is a dense vector and T is a triangular sparse matrix. 30 | //func Dussv(transT bool, alpha float64, t *SparseMatrix, x []float64, incx int) { 31 | 32 | //} 33 | -------------------------------------------------------------------------------- /blas/level2_test.go: -------------------------------------------------------------------------------- 1 | package blas 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestDusmv(t *testing.T) { 8 | tests := []struct { 9 | transA bool 10 | alpha float64 11 | a *SparseMatrix 12 | x []float64 13 | incx int 14 | y []float64 15 | incy int 16 | expected []float64 17 | }{ 18 | { 19 | transA: false, 20 | alpha: 0, 21 | a: &SparseMatrix{ 22 | I: 3, J: 4, 23 | Indptr: []int{0, 2, 2, 5}, 24 | Ind: []int{0, 2, 0, 1, 3}, 25 | Data: []float64{1, 2, 3, 4, 5}, 26 | }, 27 | // 1, 0, 2, 0, 28 | // 0, 0, 0, 0, 29 | // 3, 4, 0, 5, 30 | x: []float64{1, 2, 3, 4}, 31 | incx: 1, 32 | y: []float64{0, 0, 0}, 33 | incy: 1, 34 | expected: []float64{0, 0, 0}, 35 | }, 36 | { 37 | transA: false, 38 | alpha: 1, 39 | a: &SparseMatrix{ 40 | I: 3, J: 4, 41 | Indptr: []int{0, 2, 2, 5}, 42 | Ind: []int{0, 2, 0, 1, 3}, 43 | Data: []float64{1, 2, 3, 4, 5}, 44 | }, 45 | // 1, 0, 2, 0, 46 | // 0, 0, 0, 0, 47 | // 3, 4, 0, 5, 48 | x: []float64{1, 2, 3, 4}, 49 | incx: 1, 50 | y: []float64{0, 0, 0}, 51 | incy: 1, 52 | expected: []float64{7, 0, 31}, 53 | }, 54 | { 55 | transA: true, 56 | alpha: 1, 57 | a: &SparseMatrix{ 58 | I: 4, J: 3, 59 | Indptr: []int{0, 2, 3, 4, 5}, 60 | Ind: []int{0, 2, 2, 0, 2}, 61 | Data: []float64{1, 3, 4, 2, 5}, 62 | }, 63 | // 1 0 3 64 | // 0 0 4 65 | // 2 0 0 66 | // 0 0 5 67 | x: []float64{1, 2, 3, 4}, 68 | incx: 1, 69 | y: []float64{0, 0, 0}, 70 | incy: 1, 71 | expected: []float64{7, 0, 31}, 72 | }, 73 | { 74 | transA: false, 75 | alpha: 2, 76 | a: &SparseMatrix{ 77 | I: 3, J: 4, 78 | Indptr: []int{0, 2, 2, 5}, 79 | Ind: []int{0, 2, 0, 1, 3}, 80 | Data: []float64{1, 2, 3, 4, 5}, 81 | }, 82 | // 1, 0, 2, 0, 83 | // 0, 0, 0, 0, 84 | // 3, 4, 0, 5, 85 | x: []float64{ 86 | 1, 5, 87 | 2, 5, 88 | 3, 5, 89 | 4, 5, 90 | }, 91 | incx: 2, 92 | y: []float64{ 93 | 1, 5, 5, 5, 94 | 2, 5, 5, 5, 95 | 3, 5, 5, 5, 96 | }, 97 | incy: 4, 98 | expected: []float64{ 99 | 15, 5, 5, 5, 100 | 2, 5, 5, 5, 101 | 65, 5, 5, 5, 102 | }, 103 | }, 104 | { 105 | transA: true, 106 | alpha: 2, 107 | a: &SparseMatrix{ 108 | I: 4, J: 3, 109 | Indptr: []int{0, 2, 3, 4, 5}, 110 | Ind: []int{0, 2, 2, 0, 2}, 111 | Data: []float64{1, 3, 4, 2, 5}, 112 | }, 113 | // 1 0 3 114 | // 0 0 4 115 | // 2 0 0 116 | // 0 0 5 117 | x: []float64{ 118 | 1, 5, 119 | 2, 5, 120 | 3, 5, 121 | 4, 5, 122 | }, 123 | incx: 2, 124 | y: []float64{ 125 | 1, 5, 5, 5, 126 | 2, 5, 5, 5, 127 | 3, 5, 5, 5, 128 | }, 129 | incy: 4, 130 | expected: []float64{ 131 | 15, 5, 5, 5, 132 | 2, 5, 5, 5, 133 | 65, 5, 5, 5, 134 | }, 135 | }, 136 | } 137 | 138 | for ti, test := range tests { 139 | Dusmv(test.transA, test.alpha, test.a, test.x, test.incx, test.y, test.incy) 140 | 141 | for i, v := range test.expected { 142 | if v != test.y[i] { 143 | t.Errorf("Test %d: Expected %f at %d but received %f", ti, v, i, test.y[i]) 144 | } 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /blas/level3.go: -------------------------------------------------------------------------------- 1 | package blas 2 | 3 | // Dusmm (Sparse matrix multiply (C <- alpha * A * B + C Or C <- alpha * A^T * B + C)) 4 | // multiplies a dense matrix B by a sparse matrix A (or its transpose), and 5 | // adds it to a dense matrix operand C. C is modified to hold the result of 6 | // operation. k Represents the number of columns in matrices B and C and ldb and ldc 7 | // are the spans to be used for indexing into matrices B and C respectively. 8 | func Dusmm(transA bool, k int, alpha float64, a *SparseMatrix, b []float64, ldb int, c []float64, ldc int) { 9 | // A is m x n, B is n x k, C is m x k 10 | if alpha == 0 { 11 | return 12 | } 13 | 14 | // Perform k matvecs: i-th column of C gets A*(i-th column of B) 15 | for i := 0; i < k; i++ { 16 | Dusmv(transA, alpha, a, b[i:], ldb, c[i:], ldc) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /blas/level3_test.go: -------------------------------------------------------------------------------- 1 | package blas 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | // A is m x n, B is n x k, C is m x k 8 | func TestDusmm(t *testing.T) { 9 | tests := []struct { 10 | transA bool 11 | alpha float64 12 | a *SparseMatrix 13 | br, bc int 14 | bData []float64 15 | cr, cc int 16 | cData []float64 17 | er, ec int 18 | eData []float64 19 | }{ 20 | { 21 | transA: false, 22 | alpha: 0, 23 | a: &SparseMatrix{ 24 | I: 3, J: 4, 25 | Ind: []int{0, 2, 1, 2, 3}, 26 | Indptr: []int{0, 2, 2, 5}, 27 | Data: []float64{1, 2, 3, 4, 5}, 28 | }, 29 | br: 4, bc: 5, 30 | bData: []float64{ 31 | 1, 2, 3, 4, 5, 32 | 6, 7, 8, 9, 1, 33 | 2, 3, 4, 5, 6, 34 | 7, 8, 9, 0, 1, 35 | }, 36 | cr: 3, cc: 5, 37 | cData: []float64{ 38 | 0, 0, 0, 0, 0, 39 | 0, 0, 0, 0, 0, 40 | 0, 0, 0, 0, 0, 41 | }, 42 | er: 3, ec: 5, 43 | eData: []float64{ 44 | 0, 0, 0, 0, 0, 45 | 0, 0, 0, 0, 0, 46 | 0, 0, 0, 0, 0, 47 | }, 48 | }, 49 | { 50 | transA: false, 51 | alpha: 1, 52 | a: &SparseMatrix{ 53 | I: 3, J: 4, 54 | Ind: []int{0, 2, 1, 2, 3}, 55 | Indptr: []int{0, 2, 2, 5}, 56 | Data: []float64{1, 2, 3, 4, 5}, 57 | }, 58 | br: 4, bc: 5, 59 | bData: []float64{ 60 | 1, 2, 3, 4, 5, 61 | 6, 7, 8, 9, 1, 62 | 2, 3, 4, 5, 6, 63 | 7, 8, 9, 0, 1, 64 | }, 65 | cr: 3, cc: 5, 66 | cData: []float64{ 67 | 0, 0, 0, 0, 0, 68 | 0, 0, 0, 0, 0, 69 | 0, 0, 0, 0, 0, 70 | }, 71 | er: 3, ec: 5, 72 | eData: []float64{ 73 | 5, 8, 11, 14, 17, 74 | 0, 0, 0, 0, 0, 75 | 61, 73, 85, 47, 32, 76 | }, 77 | }, 78 | { 79 | transA: true, 80 | alpha: 1, 81 | a: &SparseMatrix{ 82 | I: 4, J: 3, 83 | Ind: []int{0, 2, 0, 2, 2}, 84 | Indptr: []int{0, 1, 2, 4, 5}, 85 | Data: []float64{1, 3, 2, 4, 5}, 86 | }, 87 | br: 4, bc: 5, 88 | bData: []float64{ 89 | 1, 2, 3, 4, 5, 90 | 6, 7, 8, 9, 1, 91 | 2, 3, 4, 5, 6, 92 | 7, 8, 9, 0, 1, 93 | }, 94 | cr: 3, cc: 5, 95 | cData: []float64{ 96 | 0, 0, 0, 0, 0, 97 | 0, 0, 0, 0, 0, 98 | 0, 0, 0, 0, 0, 99 | }, 100 | er: 3, ec: 5, 101 | eData: []float64{ 102 | 5, 8, 11, 14, 17, 103 | 0, 0, 0, 0, 0, 104 | 61, 73, 85, 47, 32, 105 | }, 106 | }, 107 | { 108 | transA: false, 109 | alpha: 2, 110 | a: &SparseMatrix{ 111 | I: 3, J: 4, 112 | Ind: []int{0, 2, 1, 2, 3}, 113 | Indptr: []int{0, 2, 2, 5}, 114 | Data: []float64{1, 2, 3, 4, 5}, 115 | }, 116 | br: 4, bc: 5, 117 | bData: []float64{ 118 | 1, 2, 3, 4, 5, 119 | 6, 7, 8, 9, 1, 120 | 2, 3, 4, 5, 6, 121 | 7, 8, 9, 0, 1, 122 | }, 123 | cr: 3, cc: 5, 124 | cData: []float64{ 125 | 1, 2, 3, 4, 5, 126 | 6, 7, 8, 9, 10, 127 | 11, 12, 13, 14, 15, 128 | }, 129 | er: 3, ec: 5, 130 | eData: []float64{ 131 | 11, 18, 25, 32, 39, 132 | 6, 7, 8, 9, 10, 133 | 133, 158, 183, 108, 79, 134 | }, 135 | }, 136 | { 137 | transA: true, 138 | alpha: 2, 139 | a: &SparseMatrix{ 140 | I: 4, J: 3, 141 | Ind: []int{0, 2, 0, 2, 2}, 142 | Indptr: []int{0, 1, 2, 4, 5}, 143 | Data: []float64{1, 3, 2, 4, 5}, 144 | }, 145 | br: 4, bc: 5, 146 | bData: []float64{ 147 | 1, 2, 3, 4, 5, 148 | 6, 7, 8, 9, 1, 149 | 2, 3, 4, 5, 6, 150 | 7, 8, 9, 0, 1, 151 | }, 152 | cr: 3, cc: 5, 153 | cData: []float64{ 154 | 1, 2, 3, 4, 5, 155 | 6, 7, 8, 9, 10, 156 | 11, 12, 13, 14, 15, 157 | }, 158 | er: 3, ec: 5, 159 | eData: []float64{ 160 | 11, 18, 25, 32, 39, 161 | 6, 7, 8, 9, 10, 162 | 133, 158, 183, 108, 79, 163 | }, 164 | }, 165 | } 166 | 167 | for ti, test := range tests { 168 | Dusmm(test.transA, test.ec, test.alpha, test.a, test.bData, test.bc, test.cData, test.cc) 169 | 170 | for i, v := range test.eData { 171 | if v != test.cData[i] { 172 | t.Errorf("Test %d: Failed at index %d, expected %f but receied %f", ti+1, i, v, test.cData[i]) 173 | } 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /blas/matrix.go: -------------------------------------------------------------------------------- 1 | package blas 2 | 3 | import ( 4 | "gonum.org/v1/gonum/floats/scalar" 5 | "gonum.org/v1/gonum/mat" 6 | ) 7 | 8 | // SparseMatrix represents the common structure for representing compressed sparse 9 | // matrix formats e.g. CSR (Compressed Sparse Row) or CSC (Compressed Sparse Column) 10 | type SparseMatrix struct { 11 | I, J int 12 | Indptr []int 13 | Ind []int 14 | Data []float64 15 | } 16 | 17 | // At returns the element of the matrix located at coordinate i, j. 18 | func (m *SparseMatrix) At(i, j int) float64 { 19 | if uint(i) < 0 || uint(i) >= uint(m.I) { 20 | panic("sparse/blas: index out of range") 21 | } 22 | if uint(j) < 0 || uint(j) >= uint(m.J) { 23 | panic("sparse/blas: index out of range") 24 | } 25 | 26 | for k := m.Indptr[i]; k < m.Indptr[i+1]; k++ { 27 | if m.Ind[k] == j { 28 | return m.Data[k] 29 | } 30 | } 31 | 32 | return 0 33 | } 34 | 35 | // Set is a generic method to set a matrix element. Note: setting a non-zero element to zero 36 | // does not remove the element from the sparcity pattern but will actually store a zero value. 37 | func (m *SparseMatrix) Set(i, j int, v float64) { 38 | if uint(i) < 0 || uint(i) >= uint(m.I) { 39 | panic("sparse/blas: index out of range") 40 | } 41 | if uint(j) < 0 || uint(j) >= uint(m.J) { 42 | panic("sparse/blas: index out of range") 43 | } 44 | 45 | for k := m.Indptr[i]; k < m.Indptr[i+1]; k++ { 46 | if m.Ind[k] == j { 47 | // if element(i, j) is already a non-zero value then simply update the existing 48 | // value without altering the sparsity pattern 49 | m.Data[k] = v 50 | return 51 | } 52 | } 53 | 54 | if v == 0 { 55 | // don't bother storing new zero values 56 | return 57 | } 58 | 59 | // element(i, j) doesn't exist in current sparsity pattern and is beyond the last 60 | // non-zero element of a row/col or an empty row/col - so add it 61 | m.insert(i, j, v, m.Indptr[i+1]) 62 | } 63 | 64 | // insert inserts a new non-zero element into the sparse matrix, updating the 65 | // sparsity pattern 66 | func (m *SparseMatrix) insert(i int, j int, v float64, insertionPoint int) { 67 | m.Ind = append(m.Ind, 0) 68 | copy(m.Ind[insertionPoint+1:], m.Ind[insertionPoint:]) 69 | m.Ind[insertionPoint] = j 70 | 71 | m.Data = append(m.Data, 0) 72 | copy(m.Data[insertionPoint+1:], m.Data[insertionPoint:]) 73 | m.Data[insertionPoint] = v 74 | 75 | for n := i + 1; n <= m.I; n++ { 76 | m.Indptr[n]++ 77 | } 78 | } 79 | 80 | func (m *SparseMatrix) nnzWithin(epsilon float64) int { 81 | count := 0 82 | for _, v := range m.Data { 83 | if !scalar.EqualWithinAbs(v, 0, epsilon) { 84 | count++ 85 | } 86 | } 87 | return count 88 | } 89 | 90 | // Cull returns a new SparseMatrix with all entries within epsilon of 0 removed. 91 | func (m *SparseMatrix) Cull(epsilon float64) *SparseMatrix { 92 | nMajor := len(m.Indptr) 93 | targetNNZ := m.nnzWithin(epsilon) 94 | newIndPtr := make([]int, nMajor) 95 | newInd := make([]int, targetNNZ) 96 | newData := make([]float64, targetNNZ) 97 | curPos := 0 98 | for major := 0; major < nMajor-1; major++ { 99 | startIdx := m.Indptr[major] 100 | endIdx := m.Indptr[major+1] 101 | newIndPtr[major] = curPos 102 | for minor := startIdx; minor < endIdx; minor++ { 103 | col := m.Ind[minor] 104 | v := m.Data[minor] 105 | if !scalar.EqualWithinAbs(v, 0, epsilon) { 106 | newInd[curPos] = col 107 | newData[curPos] = v 108 | curPos++ 109 | } 110 | } 111 | } 112 | if curPos != targetNNZ { 113 | panic(mat.ErrShape) 114 | } 115 | newIndPtr[nMajor-1] = curPos 116 | return &SparseMatrix{ 117 | I: m.I, J: m.J, 118 | Indptr: newIndPtr, 119 | Ind: newInd, 120 | Data: newData, 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /blas/matrix_test.go: -------------------------------------------------------------------------------- 1 | package blas 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestSparseMatrixSet(t *testing.T) { 8 | var tests = []struct { 9 | r, c int 10 | data []float64 11 | i, j int 12 | v float64 13 | result []float64 14 | }{ 15 | { // 0 at start of matrix set to non-zero 16 | r: 3, c: 4, 17 | data: []float64{ 18 | 0, 0, 0, 0, 19 | 0, 2, 1, 0, 20 | 0, 0, 3, 6, 21 | }, 22 | i: 0, j: 0, 23 | v: 5, 24 | result: []float64{ 25 | 5, 0, 0, 0, 26 | 0, 2, 1, 0, 27 | 0, 0, 3, 6, 28 | }, 29 | }, 30 | { // 0 as first element of row set to non-zero 31 | r: 3, c: 4, 32 | data: []float64{ 33 | 1, 0, 0, 0, 34 | 0, 2, 1, 0, 35 | 0, 0, 3, 6, 36 | }, 37 | i: 2, j: 0, 38 | v: 5, 39 | result: []float64{ 40 | 1, 0, 0, 0, 41 | 0, 2, 1, 0, 42 | 5, 0, 3, 6, 43 | }, 44 | }, 45 | { // 0 as first non-zero element of row set to non-zero 46 | r: 3, c: 4, 47 | data: []float64{ 48 | 1, 0, 0, 0, 49 | 0, 2, 1, 0, 50 | 0, 0, 3, 6, 51 | }, 52 | i: 2, j: 1, 53 | v: 5, 54 | result: []float64{ 55 | 1, 0, 0, 0, 56 | 0, 2, 1, 0, 57 | 0, 5, 3, 6, 58 | }, 59 | }, 60 | { // 0 as non-zero element in middle of row/col set to non-zero 61 | r: 3, c: 4, 62 | data: []float64{ 63 | 1, 0, 0, 0, 64 | 0, 2, 0, 7, 65 | 0, 0, 3, 6, 66 | }, 67 | i: 1, j: 2, 68 | v: 5, 69 | result: []float64{ 70 | 1, 0, 0, 0, 71 | 0, 2, 5, 7, 72 | 0, 0, 3, 6, 73 | }, 74 | }, 75 | { // non-zero value updated 76 | r: 3, c: 4, 77 | data: []float64{ 78 | 1, 0, 0, 0, 79 | 0, 2, 0, 0, 80 | 0, 0, 3, 6, 81 | }, 82 | i: 2, j: 2, 83 | v: 5, 84 | result: []float64{ 85 | 1, 0, 0, 0, 86 | 0, 2, 0, 0, 87 | 0, 0, 5, 6, 88 | }, 89 | }, 90 | { // 0 at end of row set to non-zero 91 | r: 3, c: 4, 92 | data: []float64{ 93 | 1, 0, 0, 0, 94 | 0, 2, 1, 0, 95 | 0, 0, 3, 0, 96 | }, 97 | i: 2, j: 3, 98 | v: 5, 99 | result: []float64{ 100 | 1, 0, 0, 0, 101 | 0, 2, 1, 0, 102 | 0, 0, 3, 5, 103 | }, 104 | }, 105 | { // 0 on all zero row/column set to non-zero 106 | r: 3, c: 4, 107 | data: []float64{ 108 | 1, 0, 2, 0, 109 | 0, 0, 0, 0, 110 | 0, 0, 3, 6, 111 | }, 112 | i: 1, j: 1, 113 | v: 5, 114 | result: []float64{ 115 | 1, 0, 2, 0, 116 | 0, 5, 0, 0, 117 | 0, 0, 3, 6, 118 | }, 119 | }, 120 | { // non-zero on otherwise all zero row/column set to zero 121 | r: 3, c: 4, 122 | data: []float64{ 123 | 1, 0, 2, 0, 124 | 0, 5, 0, 0, 125 | 0, 0, 3, 6, 126 | }, 127 | i: 1, j: 1, 128 | v: 0, 129 | result: []float64{ 130 | 1, 0, 2, 0, 131 | 0, 0, 0, 0, 132 | 0, 0, 3, 6, 133 | }, 134 | }, 135 | { // zero on otherwise all zero row/column set to zero 136 | r: 3, c: 4, 137 | data: []float64{ 138 | 1, 0, 2, 0, 139 | 0, 0, 0, 0, 140 | 0, 0, 3, 6, 141 | }, 142 | i: 1, j: 1, 143 | v: 0, 144 | result: []float64{ 145 | 1, 0, 2, 0, 146 | 0, 0, 0, 0, 147 | 0, 0, 3, 6, 148 | }, 149 | }, 150 | } 151 | 152 | for ti, test := range tests { 153 | matrix := SparseMatrix{ 154 | I: test.r, J: test.c, 155 | } 156 | matrix.Indptr = make([]int, test.r+1) 157 | for i := 0; i < test.r; i++ { 158 | for j := 0; j < test.c; j++ { 159 | v := test.data[i*test.c+j] 160 | if v != 0 { 161 | matrix.Ind = append(matrix.Ind, j) 162 | matrix.Data = append(matrix.Data, v) 163 | } 164 | } 165 | matrix.Indptr[i+1] = len(matrix.Data) 166 | } 167 | 168 | matrix.Set(test.i, test.j, test.v) 169 | 170 | for i := 0; i < test.r; i++ { 171 | for j := 0; j < test.c; j++ { 172 | if matrix.At(i, j) != test.result[i*test.c+j] { 173 | t.Errorf("Test %d: Expected %f at %d,%d but found %f", ti+1, test.result[i*test.c+j], i, j, matrix.At(i, j)) 174 | } 175 | } 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /blas/stubs_amd64.go: -------------------------------------------------------------------------------- 1 | //+build !noasm,!appengine,!safe 2 | 3 | package blas 4 | 5 | // Dusdot (Sparse dot product (r <- x^T * y)) calculates the dot product of 6 | // sparse vector x and dense vector y. indx is used as the index 7 | // values to gather and incy as the stride for y. 8 | func Dusdot(x []float64, indx []int, y []float64, incy int) (dot float64) 9 | 10 | // Dusaxpy (Sparse update (y <- alpha * x + y)) scales the sparse vector x by 11 | // alpha and adds the result to the dense vector y. indx is used as the index 12 | // values to gather and incy as the stride for y. 13 | func Dusaxpy(alpha float64, x []float64, indx []int, y []float64, incy int) 14 | -------------------------------------------------------------------------------- /cholesky.go: -------------------------------------------------------------------------------- 1 | package sparse 2 | 3 | import ( 4 | "math" 5 | 6 | "gonum.org/v1/gonum/floats" 7 | "gonum.org/v1/gonum/mat" 8 | ) 9 | 10 | // Cholesky shadows the gonum mat.Cholesly type 11 | type Cholesky struct { 12 | // internal representation is CSR in lower triangular form 13 | chol *CSR 14 | 15 | // some operations use a columnar version 16 | cholc *CSC 17 | cond float64 18 | } 19 | 20 | // Dims of the matrix 21 | func (ch *Cholesky) Dims() (r, c int) { 22 | return ch.chol.Dims() 23 | } 24 | 25 | // Symmetric of matrix 26 | func (ch *Cholesky) Symmetric() int { 27 | r, _ := ch.Dims() 28 | return r 29 | } 30 | 31 | func min(a, b int) int { 32 | if a < b { 33 | return a 34 | } 35 | return b 36 | } 37 | 38 | // At from the matrix 39 | func (ch *Cholesky) At(i, j int) float64 { 40 | var val float64 41 | ri := ch.chol.RowView(i).(*Vector) 42 | rj := ch.chol.RowView(j).(*Vector) 43 | // FIXME: check types 44 | val = dotSparseSparseNoSortBefore(ri, rj, nil, min(i, j)+1) 45 | return val 46 | } 47 | 48 | // T is the same as symmetric 49 | func (ch *Cholesky) T() mat.Matrix { 50 | return ch 51 | } 52 | 53 | func newCSR(r, c int) *CSR { 54 | // FIXME: creating a CSR directly leads to panics 55 | coo := NewCOO(r, c, nil, nil, nil) 56 | return coo.ToCSR() 57 | } 58 | 59 | func newCSC(r, c int) *CSC { 60 | // FIXME: creating a CSC directly leads to panics 61 | coo := NewCOO(r, c, nil, nil, nil) 62 | return coo.ToCSC() 63 | } 64 | 65 | // Det returns the determinant of the factored matrix 66 | func (ch *Cholesky) Det() float64 { 67 | return math.Exp(ch.LogDet()) 68 | } 69 | 70 | // LogDet returns ln(determinant) of the factored matrix 71 | func (ch *Cholesky) LogDet() float64 { 72 | det := 0.0 73 | for i := 0; i < ch.Symmetric(); i++ { 74 | det += 2 * math.Log(ch.chol.At(i, i)) 75 | } 76 | return det 77 | } 78 | 79 | // Factorize a CSR 80 | // the CSR must be symmetric positive-definite or this won't work 81 | // FIXME: enforce sym positive definite 82 | func (ch *Cholesky) Factorize(a *CSR) { 83 | r, c := a.Dims() 84 | if r != c { 85 | panic(mat.ErrShape) 86 | } 87 | ch.chol = newCSR(r, c) 88 | cholCSR(a, ch.chol) 89 | } 90 | 91 | // LTo returns the factored matrix in lower-triangular form as a CSR 92 | func (ch *Cholesky) LTo(dst *CSR) { 93 | r, c := ch.chol.Dims() 94 | rDst, cDst := dst.Dims() 95 | if r != rDst || c != cDst { 96 | panic(mat.ErrShape) 97 | } 98 | ch.chol.DoNonZero(func(i, j int, v float64) { 99 | dst.Set(i, j, v) 100 | }) 101 | } 102 | 103 | func (ch *Cholesky) buildCholC() { 104 | if ch.cholc == nil { 105 | r := ch.Symmetric() 106 | ch.cholc = newCSC(r, r) 107 | ch.chol.DoNonZero(func(i, j int, v float64) { 108 | ch.cholc.Set(i, j, v) 109 | }) 110 | } 111 | } 112 | 113 | // SolveVecTo shadows Cholesky.SolveVecTo 114 | // dst is Dense as this doesn't make any sense with sparse solutions 115 | func (ch *Cholesky) SolveVecTo(dst *mat.VecDense, b mat.Vector) error { 116 | r := ch.Symmetric() 117 | dstLen := dst.Len() 118 | if r != dstLen { 119 | panic(mat.ErrShape) 120 | } 121 | 122 | // we are going to need to scan down columns too 123 | ch.buildCholC() 124 | 125 | // textbook setup and approach: 126 | // Ax=b 127 | // LLtx=b 128 | // L is ch.Chol 129 | 130 | // forward substitute 131 | // Ly=b 132 | y := mat.NewVecDense(r, nil) 133 | for i := 0; i < r; i++ { 134 | denom := ch.chol.At(i, i) 135 | k := b.AtVec(i) 136 | sum := 0.0 137 | ch.chol.DoRowNonZero(i, func(x, z int, v float64) { 138 | if z < i { 139 | sum += y.AtVec(z) * v 140 | } 141 | }) 142 | y.SetVec(i, (k-sum)/denom) 143 | } 144 | 145 | // backward substitute 146 | // Lt x=y 147 | for i := r - 1; i >= 0; i-- { 148 | denom := ch.chol.At(i, i) 149 | k := y.AtVec(i) 150 | sum := 0.0 151 | ch.cholc.DoColNonZero(i, func(x, z int, v float64) { 152 | if x > i { 153 | sum += dst.AtVec(x) * v 154 | } 155 | }) 156 | dst.SetVec(i, (k-sum)/denom) 157 | } 158 | 159 | return nil 160 | } 161 | 162 | // SolveTo goes column-by-column and applies SolveVecTo 163 | func (ch *Cholesky) SolveTo(dst *mat.Dense, b mat.Matrix) error { 164 | rows, cols := b.Dims() 165 | n := ch.Symmetric() 166 | if dst.IsEmpty() { 167 | dst.ReuseAs(n, cols) 168 | } 169 | bv, bHasColView := b.(mat.ColViewer) 170 | for c := 0; c < cols; c++ { 171 | dstView := dst.ColView(c).(*mat.VecDense) 172 | if bHasColView { 173 | cv := bv.ColView(c) 174 | ch.SolveVecTo(dstView, cv) 175 | } else { 176 | cv := mat.NewVecDense(rows, nil) 177 | ch.SolveVecTo(dstView, cv) 178 | } 179 | } 180 | return nil 181 | } 182 | 183 | // basic textbook "dot product" algo, here for comparison against the 184 | // sparse version 185 | func cholSimple(matrix mat.Matrix, lower *mat.TriDense) { 186 | r, _ := matrix.Dims() 187 | 188 | for i := 0; i < r; i++ { 189 | for j := 0; j <= i; j++ { 190 | var sum float64 191 | if i == j { 192 | for k := 0; k < j; k++ { 193 | sum += math.Pow(lower.At(j, k), 2) 194 | } 195 | lower.SetTri(j, j, math.Sqrt(matrix.At(j, j)-sum)) 196 | } else { 197 | for k := 0; k < j; k++ { 198 | sum += lower.At(i, k) * lower.At(j, k) 199 | } 200 | lower.SetTri(i, j, (matrix.At(i, j)-sum)/lower.At(j, j)) 201 | } 202 | } 203 | } 204 | } 205 | 206 | // the core sparse factoring algo 207 | // this is simply the textbook "dot product" algo using a sparse dot 208 | func cholCSR(matrix *CSR, lower *CSR) { 209 | r, _ := matrix.Dims() 210 | 211 | for i := 0; i < r; i++ { 212 | if matrix.RowNNZ(i) == 0 { 213 | continue 214 | } 215 | for j := 0; j <= i; j++ { 216 | iRow := lower.RowView(i) 217 | iRowS, iRowIsSparse := iRow.(*Vector) 218 | jRow := lower.RowView(j) 219 | jRowS, jRowIsSparse := jRow.(*Vector) 220 | if !iRowIsSparse || !jRowIsSparse { 221 | panic(mat.ErrShape) 222 | } 223 | if i == j { 224 | sum := floats.Dot(jRowS.data, jRowS.data) 225 | if sum == 0.0 && matrix.At(i, i) == 0.0 { 226 | continue 227 | } 228 | lower.Set(j, j, math.Sqrt(matrix.At(i, i)-sum)) 229 | } else { 230 | rowDotSum := dotSparseSparseNoSort(iRowS, jRowS, nil) 231 | if rowDotSum == 0.0 && matrix.At(i, j) == 0.0 { 232 | continue 233 | } 234 | lower.Set(i, j, (matrix.At(i, j)-rowDotSum)/lower.At(j, j)) 235 | } 236 | } 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /cholesky_test.go: -------------------------------------------------------------------------------- 1 | package sparse 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "testing" 7 | 8 | "golang.org/x/exp/rand" 9 | 10 | "gonum.org/v1/gonum/mat" 11 | "gonum.org/v1/gonum/stat/distmat" 12 | ) 13 | 14 | func TestCholesky(t *testing.T) { 15 | t.Parallel() 16 | for _, test := range []struct { 17 | a *mat.SymDense 18 | 19 | cond float64 20 | want *mat.TriDense 21 | posdef bool 22 | }{ 23 | { 24 | a: mat.NewSymDense(3, []float64{ 25 | 4, 1, 1, 26 | 0, 2, 3, 27 | 0, 0, 6, 28 | }), 29 | cond: 37, 30 | want: mat.NewTriDense(3, true, []float64{ 31 | 2, 0.5, 0.5, 32 | 0, 1.3228756555322954, 2.0788046015507495, 33 | 0, 0, 1.195228609334394, 34 | }), 35 | posdef: true, 36 | }, 37 | } { 38 | if !cholMatches(test.a) { 39 | t.Error("chol mismatch") 40 | } 41 | } 42 | iters := 16 43 | src := rand.NewSource(1) 44 | for i := 0; i < iters; i++ { 45 | n := 128 46 | frac := 0.05 47 | m := randomSymDensePosDefinite(n, frac, src) 48 | if !cholMatches(m) { 49 | t.Error("mismatch on random matrix") 50 | } 51 | } 52 | } 53 | 54 | func TestCholeskySolveVecTo(t *testing.T) { 55 | t.Parallel() 56 | for idx, test := range []struct { 57 | a *mat.SymDense 58 | b *mat.VecDense 59 | ans *mat.VecDense 60 | }{ 61 | { 62 | a: mat.NewSymDense(2, []float64{ 63 | 1, 0, 64 | 0, 1, 65 | }), 66 | b: mat.NewVecDense(2, []float64{5, 6}), 67 | ans: mat.NewVecDense(2, []float64{5, 6}), 68 | }, 69 | { 70 | a: mat.NewSymDense(3, []float64{ 71 | 53, 59, 37, 72 | 0, 83, 71, 73 | 0, 0, 101, 74 | }), 75 | b: mat.NewVecDense(3, []float64{5, 6, 7}), 76 | ans: mat.NewVecDense(3, []float64{0.20745069393718094, -0.17421475529583694, 0.11577794010226464}), 77 | }, 78 | } { 79 | var chol mat.Cholesky 80 | ok := chol.Factorize(test.a) 81 | if !ok { 82 | t.Fatal("unexpected Cholesky factorization failure: not positive definite") 83 | } 84 | 85 | var x mat.VecDense 86 | err := chol.SolveVecTo(&x, test.b) 87 | if err != nil { 88 | t.Errorf("unexpected error from Cholesky solve: %v", err) 89 | } 90 | if !mat.EqualApprox(&x, test.ans, 1e-12) { 91 | t.Error("incorrect Cholesky solve solution") 92 | } 93 | 94 | var ans mat.VecDense 95 | ans.MulVec(test.a, &x) 96 | if !mat.EqualApprox(&ans, test.b, 1e-12) { 97 | t.Error("incorrect Cholesky solve solution product") 98 | } 99 | 100 | // if !cholMatches(test.a) { 101 | // t.Error("chol mismatch in solvevecto test") 102 | // } 103 | 104 | n := test.a.Symmetric() 105 | aCOO := matToCOO(test.a, 1e-10) 106 | aCSR := aCOO.ToCSR() 107 | var sc Cholesky 108 | sc.Factorize(aCSR) 109 | xs := mat.NewVecDense(n, nil) 110 | sc.SolveVecTo(xs, test.b) 111 | if !mat.EqualApprox(xs, test.ans, 1e-12) { 112 | t.Error("incorrect sparse Cholesky solution", idx) 113 | } 114 | } 115 | } 116 | 117 | func TestCholeskyAt(t *testing.T) { 118 | t.Parallel() 119 | for _, test := range []*mat.SymDense{ 120 | mat.NewSymDense(3, []float64{ 121 | 53, 59, 37, 122 | 59, 83, 71, 123 | 37, 71, 101, 124 | }), 125 | } { 126 | var chol Cholesky 127 | csr := matToCSR(test, 1e-8) 128 | chol.Factorize(csr) 129 | n := test.Symmetric() 130 | cn := chol.Symmetric() 131 | if cn != n { 132 | t.Errorf("Cholesky size does not match. Got %d, want %d", cn, n) 133 | } 134 | for i := 0; i < n; i++ { 135 | for j := 0; j < n; j++ { 136 | got := chol.At(i, j) 137 | want := test.At(i, j) 138 | if math.Abs(got-want) > 1e-12 { 139 | t.Errorf("Cholesky at does not match at %d, %d. Got %v, want %v", i, j, got, want) 140 | } 141 | } 142 | } 143 | } 144 | } 145 | 146 | func TestCholeskySolveTo(t *testing.T) { 147 | t.Parallel() 148 | for _, test := range []struct { 149 | a *mat.SymDense 150 | b *mat.Dense 151 | ans *mat.Dense 152 | }{ 153 | { 154 | a: mat.NewSymDense(2, []float64{ 155 | 1, 0, 156 | 0, 1, 157 | }), 158 | b: mat.NewDense(2, 1, []float64{5, 6}), 159 | ans: mat.NewDense(2, 1, []float64{5, 6}), 160 | }, 161 | { 162 | a: mat.NewSymDense(3, []float64{ 163 | 53, 59, 37, 164 | 0, 83, 71, 165 | 37, 71, 101, 166 | }), 167 | b: mat.NewDense(3, 1, []float64{5, 6, 7}), 168 | ans: mat.NewDense(3, 1, []float64{0.20745069393718094, -0.17421475529583694, 0.11577794010226464}), 169 | }, 170 | } { 171 | var chol Cholesky 172 | csr := matToCSR(test.a, 1e-8) 173 | chol.Factorize(csr) 174 | 175 | var x mat.Dense 176 | err := chol.SolveTo(&x, test.b) 177 | if err != nil { 178 | t.Errorf("unexpected error from Cholesky solve: %v", err) 179 | } 180 | if !mat.EqualApprox(&x, test.ans, 1e-12) { 181 | t.Error("incorrect Cholesky solve solution") 182 | } 183 | 184 | var ans mat.Dense 185 | ans.Mul(test.a, &x) 186 | if !mat.EqualApprox(&ans, test.b, 1e-12) { 187 | t.Error("incorrect Cholesky solve solution product") 188 | } 189 | } 190 | } 191 | 192 | func cholMatches(a *mat.SymDense) bool { 193 | _, n := a.Dims() 194 | var chol mat.Cholesky 195 | 196 | ok := chol.Factorize(a) 197 | if !ok { 198 | fmt.Println("cannot factorize") 199 | return false 200 | } 201 | var L mat.TriDense 202 | chol.LTo(&L) 203 | simpleRes := mat.NewTriDense(n, false, nil) 204 | cholSimple(a, simpleRes) 205 | if !mat.EqualApprox(&L, simpleRes, 1e-10) { 206 | return false 207 | } 208 | coo := NewCOO(n, n, nil, nil, nil) 209 | csrRes := coo.ToCSR() 210 | aCOO := matToCOO(a, 1e-10) 211 | aCSR := aCOO.ToCSR() 212 | cholCSR(aCSR, csrRes) 213 | if !mat.EqualApprox(&L, csrRes, 1e-10) { 214 | for i := 0; i < n; i++ { 215 | for j := 0; j < n; j++ { 216 | fmt.Println(L.At(i, j), csrRes.At(i, j)) 217 | } 218 | } 219 | return false 220 | } 221 | 222 | return true 223 | } 224 | 225 | // computes a permutation matrix where each non-zero is rand.Float64() instead of 1 226 | // and then return aat of that 227 | func randomScaledPermutationMatrixAAT(n int, src rand.Source) *mat.SymDense { 228 | // p contains a permutation matrix 229 | p := mat.NewDense(n, n, nil) 230 | u := distmat.NewUniformPermutation(src) 231 | u.PermTo(p) 232 | 233 | // s is a scale matrix with rnd.Float64() down the diagonal 234 | s := mat.NewDense(n, n, nil) 235 | rnd := rand.New(src) 236 | for i := 0; i < n; i++ { 237 | s.Set(i, i, rnd.Float64()) 238 | } 239 | 240 | // a = s p 241 | // compute aat and convert to symdense 242 | a := mat.NewDense(n, n, nil) 243 | a.Mul(p, s) 244 | at := a.T() 245 | aat := mat.NewDense(n, n, nil) 246 | aat.Mul(a, at) 247 | aatSym := mat.NewSymDense(n, aat.RawMatrix().Data) 248 | return aatSym 249 | } 250 | 251 | func randomSymDensePosDefinite(n int, fracNZ float64, src rand.Source) *mat.SymDense { 252 | ok := false 253 | for !ok { 254 | m := randomSymDensePosDefiniteInternal(n, fracNZ, src) 255 | var chol mat.Cholesky 256 | ok = chol.Factorize(m) 257 | if ok { 258 | return m 259 | } 260 | } 261 | return nil 262 | } 263 | 264 | func randomSymDensePosDefiniteInternal(n int, fracNZ float64, src rand.Source) *mat.SymDense { 265 | rnd := rand.New(src) 266 | m := mat.NewDense(n, n, nil) 267 | nnz := int(float64(n) * float64(n) * fracNZ) 268 | rList := make([]int, nnz) 269 | cList := make([]int, nnz) 270 | for i := range rList { 271 | rList[i] = rnd.Intn(n) 272 | cList[i] = rnd.Intn(n) 273 | } 274 | for i := range rList { 275 | r := rList[i] 276 | c := cList[i] 277 | m.Set(r, c, rnd.Float64()) 278 | m.Set(c, r, rnd.Float64()) 279 | } 280 | mt := m.T() 281 | mmt := mat.NewDense(n, n, nil) 282 | mmt.Mul(m, mt) 283 | mmtSym := mat.NewSymDense(n, mmt.RawMatrix().Data) 284 | return mmtSym 285 | } 286 | 287 | func matToCOO(m mat.Matrix, tol float64) *COO { 288 | r, c := m.Dims() 289 | newMat := NewCOO(r, c, nil, nil, nil) 290 | for i := 0; i < r; i++ { 291 | for j := 0; j < c; j++ { 292 | v := m.At(i, j) 293 | if v > tol || v < -tol { 294 | newMat.Set(i, j, v) 295 | } 296 | } 297 | } 298 | // nSize := float64(r * c) 299 | // nnz := float64(newMat.NNZ()) 300 | // ratio := nnz / nSize 301 | // fmt.Println("frac=", ratio) 302 | return newMat 303 | } 304 | 305 | func matToCSR(m mat.Matrix, tol float64) *CSR { 306 | coo := matToCOO(m, tol) 307 | return coo.ToCSR() 308 | } 309 | 310 | func BenchmarkCholSimple800(b *testing.B) { cholSimpleBench(b, 800, 0) } 311 | 312 | func BenchmarkCholGoNum400S3(b *testing.B) { cholGoNumBench(b, 400, 3.0/400) } 313 | func BenchmarkCholGoNum400S5(b *testing.B) { cholGoNumBench(b, 400, 5.0/400) } 314 | func BenchmarkCholGoNum400S7(b *testing.B) { cholGoNumBench(b, 400, 7.0/400) } 315 | func BenchmarkCholGoNum400S9(b *testing.B) { cholGoNumBench(b, 400, 9.0/400) } 316 | func BenchmarkCholGoNum400(b *testing.B) { cholGoNumBench(b, 400, 0.0) } 317 | func BenchmarkCholGoNum800(b *testing.B) { cholGoNumBench(b, 800, 0.0) } 318 | func BenchmarkCholGoNum1600(b *testing.B) { cholGoNumBench(b, 1600, 0.0) } 319 | func BenchmarkCholGoNum3200(b *testing.B) { cholGoNumBench(b, 3200, 0.0) } 320 | func BenchmarkCholGoNum6400(b *testing.B) { cholGoNumBench(b, 6400, 0.0) } 321 | 322 | //func BenchmarkCholGoNum12800(b *testing.B) { cholGoNumBench(b, 12800, 0.0) } 323 | 324 | func BenchmarkCholSparse400S3(b *testing.B) { sparseCholBench(b, 400, 3.0/400) } 325 | func BenchmarkCholSparse400S5(b *testing.B) { sparseCholBench(b, 400, 5.0/400) } 326 | func BenchmarkCholSparse400S7(b *testing.B) { sparseCholBench(b, 400, 7.0/400) } 327 | func BenchmarkCholSparse400S9(b *testing.B) { sparseCholBench(b, 400, 9.0/400) } 328 | func BenchmarkCholSparse400(b *testing.B) { sparseCholBench(b, 400, 0.0) } 329 | func BenchmarkCholSparse800(b *testing.B) { sparseCholBench(b, 800, 0.0) } 330 | func BenchmarkCholSparse1600(b *testing.B) { sparseCholBench(b, 1600, 0.0) } 331 | func BenchmarkCholSparse3200(b *testing.B) { sparseCholBench(b, 3200, 0.0) } 332 | func BenchmarkCholSparse6400(b *testing.B) { sparseCholBench(b, 6400, 0.0) } 333 | 334 | //func BenchmarkCholSparse12800(b *testing.B) { sparseCholBench(b, 12800, 0.0) } 335 | 336 | func cholGoNumBench(b *testing.B, size int, frac float64) { 337 | src := rand.NewSource(1) 338 | m := randomScaledPermutationMatrixAAT(size, src) 339 | if frac != 0.0 { 340 | m = randomSymDensePosDefinite(size, frac, src) 341 | } 342 | b.ResetTimer() 343 | for i := 0; i < b.N; i++ { 344 | var chol mat.Cholesky 345 | chol.Factorize(m) 346 | } 347 | } 348 | 349 | func cholSimpleBench(b *testing.B, size int, frac float64) { 350 | src := rand.NewSource(1) 351 | m := randomScaledPermutationMatrixAAT(size, src) 352 | if frac != 0.0 { 353 | m = randomSymDensePosDefinite(size, frac, src) 354 | } 355 | simpleRes := mat.NewTriDense(size, false, nil) 356 | b.ResetTimer() 357 | for i := 0; i < b.N; i++ { 358 | cholSimple(m, simpleRes) 359 | } 360 | } 361 | 362 | func sparseCholBench(b *testing.B, size int, frac float64) { 363 | src := rand.NewSource(1) 364 | mDense := randomScaledPermutationMatrixAAT(size, src) 365 | if frac != 0.0 { 366 | mDense = randomSymDensePosDefinite(size, frac, src) 367 | } 368 | coo := NewCOO(size, size, nil, nil, nil) 369 | csrRes := coo.ToCSR() 370 | aCSR := matToCSR(mDense, 1e-8) 371 | b.ResetTimer() 372 | for i := 0; i < b.N; i++ { 373 | cholCSR(aCSR, csrRes) 374 | } 375 | } 376 | -------------------------------------------------------------------------------- /compressed_arith.go: -------------------------------------------------------------------------------- 1 | package sparse 2 | 3 | import ( 4 | "github.com/james-bowman/sparse/blas" 5 | "gonum.org/v1/gonum/mat" 6 | ) 7 | 8 | // MulMatRawVec computes the matrix vector product between lhs and rhs and stores 9 | // the result in out 10 | func MulMatRawVec(lhs *CSR, rhs []float64, out []float64) { 11 | m, n := lhs.Dims() 12 | if len(rhs) != n || len(out) != m { 13 | panic(mat.ErrShape) 14 | } 15 | 16 | blas.Dusmv(false, 1, lhs.RawMatrix(), rhs, 1, out, 1) 17 | } 18 | 19 | // MulVecTo performs matrix vector multiplication (dst+=A*x or dst+=A^T*x), where A is 20 | // the receiver, and stores the result in dst. MulVecTo panics if ac != len(x) or 21 | // ar != len(dst) 22 | func (c *CSR) MulVecTo(dst []float64, trans bool, x []float64) { 23 | ar, ac := c.Dims() 24 | if trans { 25 | ar, ac = ac, ar 26 | } 27 | if ac != len(x) || ar != len(dst) { 28 | panic(mat.ErrShape) 29 | } 30 | 31 | blas.Dusmv(trans, 1, c.RawMatrix(), x, 1, dst, 1) 32 | } 33 | 34 | // MulVecTo performs matrix vector multiplication (dst+=A*x or dst+=A^T*x), where A is 35 | // the receiver, and stores the result in dst. MulVecTo panics if ac != len(x) or 36 | // ar != len(dst) 37 | func (c *CSC) MulVecTo(dst []float64, trans bool, x []float64) { 38 | c.T().(*CSR).MulVecTo(dst, !trans, x) 39 | } 40 | 41 | // temporaryWorkspace returns a new CSR matrix w with the size of r x c with 42 | // initial capacity allocated for nnz non-zero elements and 43 | // returns a callback to defer which performs cleanup at the return of the call. 44 | // This should be used when a method receiver is the same pointer as an input argument. 45 | func (c *CSR) temporaryWorkspace(row, col, nnz int, clear bool) (w *CSR, restore func()) { 46 | w = getWorkspace(row, col, nnz, clear) 47 | return w, func() { 48 | c.cloneCSR(w) 49 | putWorkspace(w) 50 | } 51 | } 52 | 53 | // spalloc ensures appropriate storage is allocated for the receiver sparse matrix 54 | // ensuring it is row * col dimensions and checking for any overlap or aliasing 55 | // between operands a or b with c in which case a temporary isolated workspace is 56 | // allocated and the returned value isTemp is true with restore representing a 57 | // function to clean up and restore the workspace once finished. 58 | func (c *CSR) spalloc(a mat.Matrix, b mat.Matrix) (m *CSR, isTemp bool, restore func()) { 59 | var nnz int 60 | m = c 61 | row, _ := a.Dims() 62 | _, col := b.Dims() 63 | 64 | lSp, lIsSp := a.(Sparser) 65 | rSp, rIsSp := b.(Sparser) 66 | if lIsSp && rIsSp { 67 | nnz = lSp.NNZ() + rSp.NNZ() 68 | } else { 69 | // assume 10% of elements will be non-zero 70 | nnz = row * col / 10 71 | } 72 | 73 | if c.checkOverlap(a) || c.checkOverlap(b) { 74 | if !c.IsZero() && (row != c.matrix.I || col != c.matrix.J) { 75 | panic(mat.ErrShape) 76 | } 77 | m, restore = c.temporaryWorkspace(row, col, nnz, true) 78 | isTemp = true 79 | } else { 80 | c.reuseAs(row, col, nnz, true) 81 | } 82 | 83 | return 84 | } 85 | 86 | // Mul takes the matrix product of the supplied matrices a and b and stores the result 87 | // in the receiver. Some specific optimisations are available for operands of certain 88 | // sparse formats e.g. CSR * CSR uses Gustavson Algorithm (ACM 1978) for fast 89 | // sparse matrix multiplication. 90 | // If the number of columns does not equal the number of rows in b, Mul will panic. 91 | func (c *CSR) Mul(a, b mat.Matrix) { 92 | ar, ac := a.Dims() 93 | br, bc := b.Dims() 94 | 95 | if ac != br { 96 | panic(mat.ErrShape) 97 | } 98 | 99 | if m, temp, restore := c.spalloc(a, b); temp { 100 | defer restore() 101 | c = m 102 | } 103 | 104 | lhs, isLCsr := a.(*CSR) 105 | rhs, isRCsr := b.(*CSR) 106 | if isLCsr && isRCsr { 107 | // handle CSR * CSR 108 | c.mulCSRCSR(lhs, rhs) 109 | return 110 | } 111 | if dia, ok := a.(*DIA); ok { 112 | if diaB, okB := b.(*DIA); okB { 113 | // handle DIA * DIA 114 | c.mulDIADIA(dia, diaB) 115 | return 116 | } 117 | if isRCsr { 118 | // handle DIA * CSR 119 | c.mulDIACSR(dia, rhs, false) 120 | return 121 | } 122 | // handle DIA * mat.Matrix 123 | c.mulDIAMat(dia, b, false) 124 | return 125 | } 126 | if dia, ok := b.(*DIA); ok { 127 | if isLCsr { 128 | // handle CSR * DIA 129 | c.mulDIACSR(dia, lhs, true) 130 | return 131 | } 132 | // handle mat.Matrix * DIA 133 | c.mulDIAMat(dia, a, true) 134 | return 135 | } 136 | 137 | srcA, isLSparse := a.(TypeConverter) 138 | srcB, isRSparse := b.(TypeConverter) 139 | if isLSparse { 140 | if isRSparse { 141 | // handle Sparser * Sparser 142 | c.mulCSRCSR(srcA.ToCSR(), srcB.ToCSR()) 143 | return 144 | } 145 | // handle Sparser * mat.Matrix 146 | c.mulCSRMat(srcA.ToCSR(), b) 147 | return 148 | } 149 | if isRSparse { 150 | // handle mat.Matrix * Sparser 151 | c.mulMatCSR(a, srcB.ToCSR()) 152 | return 153 | } 154 | 155 | // TODO: consider applying loop interchange to use Axpy kernel and loop invariant 156 | // motion to move a out of nested inner loop with a if != 0 test around inner 157 | // nested loop 158 | 159 | // handle mat.Matrix * mat.Matrix 160 | row := getFloats(ac, false) 161 | var v float64 162 | for i := 0; i < ar; i++ { 163 | for ci := range row { 164 | row[ci] = a.At(i, ci) 165 | } 166 | for j := 0; j < bc; j++ { 167 | v = 0 168 | for ci, e := range row { 169 | if e != 0 { 170 | v += e * b.At(ci, j) 171 | } 172 | } 173 | if v != 0 { 174 | c.matrix.Ind = append(c.matrix.Ind, j) 175 | c.matrix.Data = append(c.matrix.Data, v) 176 | } 177 | } 178 | c.matrix.Indptr[i+1] = len(c.matrix.Ind) 179 | } 180 | putFloats(row) 181 | } 182 | 183 | // mulCSRCSR handles CSR = CSR * CSR using Gustavson Algorithm (ACM 1978) 184 | func (c *CSR) mulCSRCSR(lhs *CSR, rhs *CSR) { 185 | ar, _ := lhs.Dims() 186 | _, bc := rhs.Dims() 187 | spa := NewSPA(bc) 188 | 189 | for i := 0; i < ar; i++ { 190 | for k := lhs.matrix.Indptr[i]; k < lhs.matrix.Indptr[i+1]; k++ { 191 | begin := rhs.matrix.Indptr[lhs.matrix.Ind[k]] 192 | end := rhs.matrix.Indptr[lhs.matrix.Ind[k]+1] 193 | spa.Scatter(rhs.matrix.Data[begin:end], rhs.matrix.Ind[begin:end], lhs.matrix.Data[k], &c.matrix.Ind) 194 | } 195 | spa.GatherAndZero(&c.matrix.Data, &c.matrix.Ind) 196 | c.matrix.Indptr[i+1] = len(c.matrix.Ind) 197 | } 198 | } 199 | 200 | // mulCSRMat handles CSR = CSR * mat.Matrix IJK 201 | func (c *CSR) mulCSRMat(lhs *CSR, b mat.Matrix) { 202 | ar, _ := lhs.Dims() 203 | _, bc := b.Dims() 204 | var start, end int 205 | 206 | if bd, isDense := b.(mat.RawMatrixer); isDense { 207 | braw := bd.RawMatrix() 208 | // handle case where matrix A is CSR and matrix B is mat.Dense 209 | for i := 0; i < ar; i++ { 210 | start, end = lhs.matrix.Indptr[i], lhs.matrix.Indptr[i+1] 211 | for j := 0; j < bc; j++ { 212 | // Dot kernel 213 | c.matrix.Ind = append(c.matrix.Ind, j) 214 | c.matrix.Data = append(c.matrix.Data, blas.Dusdot(lhs.matrix.Data[start:end], lhs.matrix.Ind[start:end], braw.Data[j:], braw.Stride)) 215 | } 216 | c.matrix.Indptr[i+1] = len(c.matrix.Ind) 217 | } 218 | return 219 | } 220 | 221 | var ind []int 222 | var data []float64 223 | var v float64 224 | // handle case where matrix A is CSR (matrix B can be any implementation of mat.Matrix) 225 | for i := 0; i < ar; i++ { 226 | start, end = lhs.matrix.Indptr[i], lhs.matrix.Indptr[i+1] 227 | ind, data = lhs.matrix.Ind[start:end], lhs.matrix.Data[start:end] 228 | for j := 0; j < bc; j++ { 229 | v = 0 230 | // Dot kernel 231 | for k, idx := range ind { 232 | v += data[k] * b.At(idx, j) 233 | } 234 | if v != 0.0 { 235 | c.matrix.Ind = append(c.matrix.Ind, j) 236 | c.matrix.Data = append(c.matrix.Data, v) 237 | } 238 | } 239 | c.matrix.Indptr[i+1] = len(c.matrix.Ind) 240 | } 241 | } 242 | 243 | // mulMatCSR handles CSR = mat.Matrix * CSR 244 | func (c *CSR) mulMatCSR(a mat.Matrix, rhs *CSR) { 245 | ar, ac := a.Dims() 246 | _, bc := rhs.Dims() 247 | 248 | spa := NewSPA(bc) 249 | 250 | for i := 0; i < ar; i++ { 251 | for k := 0; k < ac; k++ { 252 | // Axpy kernel using SPA 253 | s := a.At(i, k) 254 | if s != 0.0 { 255 | start, end := rhs.matrix.Indptr[k], rhs.matrix.Indptr[k+1] 256 | spa.Scatter(rhs.matrix.Data[start:end], rhs.matrix.Ind[start:end], s, &c.matrix.Ind) 257 | } 258 | } 259 | spa.GatherAndZero(&c.matrix.Data, &c.matrix.Ind) 260 | c.matrix.Indptr[i+1] = len(c.matrix.Ind) 261 | } 262 | } 263 | 264 | // mulDIACSR handles CSR = DIA * CSR (or CSR = CSR * DIA if trans == true) 265 | func (c *CSR) mulDIACSR(dia *DIA, other *CSR, trans bool) { 266 | diagonal := dia.Diagonal() 267 | if trans { 268 | for i := 0; i < c.matrix.I; i++ { 269 | var v float64 270 | for k := other.matrix.Indptr[i]; k < other.matrix.Indptr[i+1]; k++ { 271 | if other.matrix.Ind[k] < len(diagonal) { 272 | v = other.matrix.Data[k] * diagonal[other.matrix.Ind[k]] 273 | if v != 0 { 274 | c.matrix.Ind = append(c.matrix.Ind, other.matrix.Ind[k]) 275 | c.matrix.Data = append(c.matrix.Data, v) 276 | } 277 | } 278 | } 279 | c.matrix.Indptr[i+1] = len(c.matrix.Ind) 280 | } 281 | } else { 282 | for i := 0; i < c.matrix.I; i++ { 283 | var v float64 284 | for k := other.matrix.Indptr[i]; k < other.matrix.Indptr[i+1]; k++ { 285 | if i < len(diagonal) { 286 | v = other.matrix.Data[k] * diagonal[i] 287 | if v != 0 { 288 | c.matrix.Ind = append(c.matrix.Ind, other.matrix.Ind[k]) 289 | c.matrix.Data = append(c.matrix.Data, v) 290 | } 291 | } 292 | } 293 | c.matrix.Indptr[i+1] = len(c.matrix.Ind) 294 | } 295 | } 296 | } 297 | 298 | // mulDIAMat handles CSR = DIA * mat.Matrix (or CSR = mat.Matrix * DIA if trans == true) 299 | func (c *CSR) mulDIAMat(dia *DIA, other mat.Matrix, trans bool) { 300 | _, cols := other.Dims() 301 | diagonal := dia.Diagonal() 302 | 303 | if trans { 304 | for i := 0; i < c.matrix.I; i++ { 305 | var v float64 306 | for k := 0; k < cols; k++ { 307 | if k < len(diagonal) { 308 | v = other.At(i, k) * diagonal[k] 309 | if v != 0 { 310 | c.matrix.Ind = append(c.matrix.Ind, k) 311 | c.matrix.Data = append(c.matrix.Data, v) 312 | } 313 | } 314 | } 315 | c.matrix.Indptr[i+1] = len(c.matrix.Ind) 316 | } 317 | } else { 318 | for i := 0; i < c.matrix.I; i++ { 319 | var v float64 320 | for k := 0; k < cols; k++ { 321 | if i < len(diagonal) { 322 | v = other.At(i, k) * diagonal[i] 323 | if v != 0 { 324 | c.matrix.Ind = append(c.matrix.Ind, k) 325 | c.matrix.Data = append(c.matrix.Data, v) 326 | } 327 | } 328 | } 329 | c.matrix.Indptr[i+1] = len(c.matrix.Ind) 330 | } 331 | } 332 | } 333 | 334 | // mulDIADIA multiplies two diagonal matrices 335 | func (c *CSR) mulDIADIA(a, b *DIA) { 336 | _, ac := a.Dims() 337 | br, _ := b.Dims() 338 | aDiagonal := a.Diagonal() 339 | bDiagonal := a.Diagonal() 340 | if ac != br { 341 | panic(mat.ErrShape) 342 | } 343 | for i := 0; i < br; i++ { 344 | var v float64 345 | v = aDiagonal[i] * bDiagonal[i] 346 | if v != 0 { 347 | c.matrix.Ind = append(c.matrix.Ind, i) 348 | c.matrix.Data = append(c.matrix.Data, v) 349 | } 350 | c.matrix.Indptr[i+1] = i + 1 351 | } 352 | } 353 | 354 | // addDIADIA add two diagonal matrices 355 | func (c *CSR) addDIADIA(a, b *DIA, alpha, beta float64) { 356 | ar, ac := a.Dims() 357 | br, bc := b.Dims() 358 | aDiagonal := a.Diagonal() 359 | bDiagonal := a.Diagonal() 360 | if ac != bc { 361 | panic(mat.ErrShape) 362 | } 363 | if ar != br { 364 | panic(mat.ErrShape) 365 | } 366 | for i := 0; i < br; i++ { 367 | var v float64 368 | v = aDiagonal[i]*alpha + bDiagonal[i]*beta 369 | c.matrix.Ind = append(c.matrix.Ind, i) 370 | c.matrix.Data = append(c.matrix.Data, v) 371 | c.matrix.Indptr[i+1] = i + 1 372 | } 373 | } 374 | 375 | // Sub subtracts matrix b from a and stores the result in the receiver. 376 | // If matrices a and b are not the same shape then the method will panic. 377 | func (c *CSR) Sub(a, b mat.Matrix) { 378 | c.addScaled(a, b, 1, -1) 379 | } 380 | 381 | // Add adds matrices a and b together and stores the result in the receiver. 382 | // If matrices a and b are not the same shape then the method will panic. 383 | func (c *CSR) Add(a, b mat.Matrix) { 384 | c.addScaled(a, b, 1, 1) 385 | } 386 | 387 | // addScaled adds matrices a and b scaling them by a and b respectively before hand. 388 | func (c *CSR) addScaled(a mat.Matrix, b mat.Matrix, alpha float64, beta float64) { 389 | ar, ac := a.Dims() 390 | br, bc := b.Dims() 391 | 392 | if ar != br || ac != bc { 393 | panic(mat.ErrShape) 394 | } 395 | 396 | if m, temp, restore := c.spalloc(a, b); temp { 397 | defer restore() 398 | c = m 399 | } 400 | 401 | // special case both diagonal 402 | lDIA, lIsDIA := a.(*DIA) 403 | rDIA, rIsDIA := b.(*DIA) 404 | if lIsDIA && rIsDIA { 405 | c.addDIADIA(lDIA, rDIA, alpha, beta) 406 | return 407 | } 408 | 409 | // and then one or both csr 410 | lCsr, lIsCsr := a.(*CSR) 411 | rCsr, rIsCsr := b.(*CSR) 412 | if lIsCsr && rIsCsr { 413 | c.addCSRCSR(lCsr, rCsr, alpha, beta) 414 | return 415 | } 416 | if lIsCsr { 417 | c.addCSR(lCsr, b, alpha, beta) 418 | return 419 | } 420 | if rIsCsr { 421 | c.addCSR(rCsr, a, beta, alpha) 422 | return 423 | } 424 | // dumb addition with no sparcity optimisations/savings 425 | for i := 0; i < ar; i++ { 426 | for j := 0; j < ac; j++ { 427 | v := alpha*a.At(i, j) + beta*b.At(i, j) 428 | if v != 0 { 429 | c.matrix.Ind = append(c.matrix.Ind, j) 430 | c.matrix.Data = append(c.matrix.Data, v) 431 | } 432 | } 433 | c.matrix.Indptr[i+1] = len(c.matrix.Ind) 434 | } 435 | } 436 | 437 | // addCSR adds a CSR matrix to any implementation of mat.Matrix and stores the 438 | // result in the receiver. 439 | func (c *CSR) addCSR(csr *CSR, other mat.Matrix, alpha float64, beta float64) { 440 | ar, ac := csr.Dims() 441 | spa := NewSPA(ac) 442 | a := csr.RawMatrix() 443 | 444 | if dense, isDense := other.(mat.RawMatrixer); isDense { 445 | for i := 0; i < ar; i++ { 446 | begin := csr.matrix.Indptr[i] 447 | end := csr.matrix.Indptr[i+1] 448 | rawOther := dense.RawMatrix() 449 | r := rawOther.Data[i*rawOther.Stride : i*rawOther.Stride+rawOther.Cols] 450 | spa.AccumulateDense(r, beta, &c.matrix.Ind) 451 | spa.Scatter(a.Data[begin:end], a.Ind[begin:end], alpha, &c.matrix.Ind) 452 | spa.GatherAndZero(&c.matrix.Data, &c.matrix.Ind) 453 | c.matrix.Indptr[i+1] = len(c.matrix.Ind) 454 | } 455 | } else { 456 | for i := 0; i < ar; i++ { 457 | begin := csr.matrix.Indptr[i] 458 | end := csr.matrix.Indptr[i+1] 459 | for j := 0; j < ac; j++ { 460 | v := other.At(i, j) 461 | if v != 0 { 462 | spa.ScatterValue(v, j, beta, &c.matrix.Ind) 463 | } 464 | } 465 | spa.Scatter(a.Data[begin:end], a.Ind[begin:end], alpha, &c.matrix.Ind) 466 | spa.GatherAndZero(&c.matrix.Data, &c.matrix.Ind) 467 | c.matrix.Indptr[i+1] = len(c.matrix.Ind) 468 | } 469 | } 470 | } 471 | 472 | // addCSRCSR adds 2 CSR matrices together storing the result in the receiver. 473 | // Matrices a and b are scaled by alpha and beta respectively before addition. 474 | // This method is specially optimised to take advantage of the sparsity patterns 475 | // of the 2 CSR matrices. 476 | func (c *CSR) addCSRCSR(lhs *CSR, rhs *CSR, alpha float64, beta float64) { 477 | ar, ac := lhs.Dims() 478 | a := lhs.RawMatrix() 479 | b := rhs.RawMatrix() 480 | spa := NewSPA(ac) 481 | 482 | var begin, end int 483 | for i := 0; i < ar; i++ { 484 | begin, end = a.Indptr[i], a.Indptr[i+1] 485 | spa.Scatter(a.Data[begin:end], a.Ind[begin:end], alpha, &c.matrix.Ind) 486 | 487 | begin, end = b.Indptr[i], b.Indptr[i+1] 488 | spa.Scatter(b.Data[begin:end], b.Ind[begin:end], beta, &c.matrix.Ind) 489 | 490 | spa.GatherAndZero(&c.matrix.Data, &c.matrix.Ind) 491 | c.matrix.Indptr[i+1] = len(c.matrix.Ind) 492 | } 493 | } 494 | 495 | // SPA is a SParse Accumulator used to construct the results of sparse 496 | // arithmetic operations in linear time. 497 | type SPA struct { 498 | // w contains flags for indices containing non-zero values 499 | w []int 500 | 501 | // x contains all the values in dense representation (including zero values) 502 | y []float64 503 | 504 | // nnz is the Number of Non-Zero elements 505 | nnz int 506 | 507 | // generation is used to compare values of w to see if they have been set 508 | // in the current row (generation). This avoids needing to reset all values 509 | // during the GatherAndZero operation at the end of 510 | // construction for each row/column vector. 511 | generation int 512 | } 513 | 514 | // NewSPA creates a new SParse Accumulator of length n. If accumulating 515 | // rows for a CSR matrix then n should be equal to the number of columns 516 | // in the resulting matrix. 517 | func NewSPA(n int) *SPA { 518 | return &SPA{ 519 | w: make([]int, n), 520 | y: make([]float64, n), 521 | } 522 | } 523 | 524 | // ScatterVec accumulates the sparse vector x by multiplying the elements 525 | // by alpha and adding them to the corresponding elements in the SPA 526 | // (SPA += alpha * x) 527 | func (s *SPA) ScatterVec(x *Vector, alpha float64, ind *[]int) { 528 | s.Scatter(x.data, x.ind, alpha, ind) 529 | } 530 | 531 | // Scatter accumulates the sparse vector x by multiplying the elements by 532 | // alpha and adding them to the corresponding elements in the SPA (SPA += alpha * x) 533 | func (s *SPA) Scatter(x []float64, indx []int, alpha float64, ind *[]int) { 534 | for i, index := range indx { 535 | s.ScatterValue(x[i], index, alpha, ind) 536 | } 537 | } 538 | 539 | // ScatterValue accumulates a single value by multiplying the value by alpha 540 | // and adding it to the corresponding element in the SPA (SPA += alpha * x) 541 | func (s *SPA) ScatterValue(val float64, index int, alpha float64, ind *[]int) { 542 | if s.w[index] < s.generation+1 { 543 | s.w[index] = s.generation + 1 544 | *ind = append(*ind, index) 545 | s.y[index] = alpha * val 546 | } else { 547 | s.y[index] += alpha * val 548 | } 549 | } 550 | 551 | // AccumulateDense accumulates the dense vector x by multiplying the non-zero elements 552 | // by alpha and adding them to the corresponding elements in the SPA (SPA += alpha * x) 553 | // This is the dense version of the Scatter method for sparse vectors. 554 | func (s *SPA) AccumulateDense(x []float64, alpha float64, ind *[]int) { 555 | for i, val := range x { 556 | if val != 0 { 557 | s.ScatterValue(val, i, alpha, ind) 558 | } 559 | } 560 | } 561 | 562 | // Gather gathers the non-zero values from the SPA and appends them to 563 | // end of the supplied sparse vector. 564 | func (s SPA) Gather(data *[]float64, ind *[]int) { 565 | for _, index := range (*ind)[s.nnz:] { 566 | *data = append(*data, s.y[index]) 567 | } 568 | } 569 | 570 | // GatherAndZero gathers the non-zero values from the SPA and appends them 571 | // to the end of the supplied sparse vector. The SPA is also zeroed 572 | // ready to start accumulating the next row/column vector. 573 | func (s *SPA) GatherAndZero(data *[]float64, ind *[]int) { 574 | s.Gather(data, ind) 575 | 576 | s.nnz = len(*ind) 577 | s.generation++ 578 | } 579 | -------------------------------------------------------------------------------- /compressed_test.go: -------------------------------------------------------------------------------- 1 | package sparse 2 | 3 | import ( 4 | "testing" 5 | 6 | "gonum.org/v1/gonum/floats/scalar" 7 | "gonum.org/v1/gonum/mat" 8 | ) 9 | 10 | func TestCSRCSCTranspose(t *testing.T) { 11 | var tests = []struct { 12 | r, c int 13 | data []float64 14 | er, ec int 15 | result []float64 16 | }{ 17 | { 18 | r: 3, c: 4, 19 | data: []float64{ 20 | 1, 0, 0, 0, 21 | 0, 2, 0, 0, 22 | 0, 0, 3, 6, 23 | }, 24 | er: 4, ec: 3, 25 | result: []float64{ 26 | 1, 0, 0, 27 | 0, 2, 0, 28 | 0, 0, 3, 29 | 0, 0, 6, 30 | }, 31 | }, 32 | } 33 | 34 | for ti, test := range tests { 35 | t.Logf("**** Test Run %d.\n", ti+1) 36 | 37 | expected := mat.NewDense(test.er, test.ec, test.result) 38 | 39 | csr := CreateCSR(test.r, test.c, test.data).(*CSR) 40 | csc := CreateCSC(test.r, test.c, test.data).(*CSC) 41 | 42 | if !mat.Equal(expected, csr.T()) { 43 | t.Logf("CSR is:\n%v\n", mat.Formatted(csr)) 44 | t.Logf("For CSR^T, Expected:\n%v\n but received:\n%v\n", mat.Formatted(expected), mat.Formatted(csr.T())) 45 | t.Fail() 46 | } 47 | if !mat.Equal(expected, csc.T()) { 48 | t.Logf("CSC is:\n%v\n", mat.Formatted(csc)) 49 | t.Logf("For CSC^T, Expected:\n%v\n but received:\n%v\n", mat.Formatted(expected), mat.Formatted(csc.T())) 50 | t.Fail() 51 | } 52 | 53 | } 54 | } 55 | 56 | func TestCSRCSCConversion(t *testing.T) { 57 | r, c := 3, 4 58 | data := []float64{ 59 | 1, 0, 0, 7, 60 | 0, 2, 4, 0, 61 | 3, 0, 3, 6, 62 | } 63 | 64 | var tests = []struct { 65 | desc string 66 | r, c int 67 | data []float64 68 | create MatrixCreator 69 | convert func(a TypeConverter) mat.Matrix 70 | }{ 71 | { 72 | "CSR -> CSC", 73 | r, c, 74 | data, 75 | CreateCSR, 76 | func(a TypeConverter) mat.Matrix { return a.ToCSC() }, 77 | }, 78 | { 79 | "CSC -> CSR", 80 | r, c, 81 | data, 82 | CreateCSC, 83 | func(a TypeConverter) mat.Matrix { return a.ToCSR() }, 84 | }, 85 | { 86 | "CSR -> COO", 87 | r, c, 88 | data, 89 | CreateCSR, 90 | func(a TypeConverter) mat.Matrix { return a.ToCOO() }, 91 | }, 92 | { 93 | "CSC -> COO", 94 | r, c, 95 | data, 96 | CreateCSC, 97 | func(a TypeConverter) mat.Matrix { return a.ToCOO() }, 98 | }, 99 | { 100 | "CSR -> DOK", 101 | r, c, 102 | data, 103 | CreateCSR, 104 | func(a TypeConverter) mat.Matrix { return a.ToDOK() }, 105 | }, 106 | { 107 | "CSC -> DOK", 108 | r, c, 109 | data, 110 | CreateCSC, 111 | func(a TypeConverter) mat.Matrix { return a.ToDOK() }, 112 | }, 113 | { 114 | "CSR -> Dense", 115 | r, c, 116 | data, 117 | CreateCSR, 118 | func(a TypeConverter) mat.Matrix { return a.ToDense() }, 119 | }, 120 | { 121 | "CSC -> Dense", 122 | r, c, 123 | data, 124 | CreateCSC, 125 | func(a TypeConverter) mat.Matrix { return a.ToDense() }, 126 | }, 127 | { 128 | "CSR -> CSC 2", 129 | 5, 4, 130 | []float64{ 131 | 1, 0, 0, 7, 132 | 0, 0, 0, 0, 133 | 0, 0, 0, 0, 134 | 0, 0, 7, 0, 135 | 0, 0, 0, 0, 136 | }, 137 | CreateCSR, 138 | func(a TypeConverter) mat.Matrix { return a.ToCSC() }, 139 | }, 140 | { 141 | "CSC -> CSR 2", 142 | 5, 4, 143 | []float64{ 144 | 1, 0, 0, 7, 145 | 0, 0, 0, 0, 146 | 0, 0, 0, 0, 147 | 0, 0, 7, 0, 148 | 0, 0, 0, 0, 149 | }, 150 | CreateCSC, 151 | func(a TypeConverter) mat.Matrix { return a.ToCSR() }, 152 | }, 153 | } 154 | 155 | for ti, test := range tests { 156 | t.Logf("**** Test Run %d. %s\n", ti+1, test.desc) 157 | 158 | d := mat.NewDense(r, c, data) 159 | 160 | a := test.create(r, c, data) 161 | b := test.convert(a.(TypeConverter)) 162 | 163 | if !mat.Equal(d, b) { 164 | t.Logf("d : %v\n", a) 165 | t.Logf("B : %v\n", b) 166 | t.Logf("Expected:\n%v\n but received:\n%v\n", mat.Formatted(d), mat.Formatted(b)) 167 | t.Fail() 168 | } 169 | // check has not mutated original matrix 170 | if !mat.Equal(a, b) { 171 | t.Logf("A : %v\n", a) 172 | t.Logf("B : %v\n", b) 173 | t.Logf("Expected:\n%v\n but received:\n%v\n", mat.Formatted(a), mat.Formatted(b)) 174 | t.Fail() 175 | } 176 | } 177 | } 178 | 179 | func TestCSRCSCSet(t *testing.T) { 180 | var tests = []struct { 181 | r, c int 182 | data []float64 183 | i, j int 184 | v float64 185 | result []float64 186 | }{ 187 | { // 0 at start of matrix set to non-zero 188 | r: 3, c: 4, 189 | data: []float64{ 190 | 0, 0, 0, 0, 191 | 0, 2, 1, 0, 192 | 0, 0, 3, 6, 193 | }, 194 | i: 0, j: 0, 195 | v: 5, 196 | result: []float64{ 197 | 5, 0, 0, 0, 198 | 0, 2, 1, 0, 199 | 0, 0, 3, 6, 200 | }, 201 | }, 202 | { // 0 as first element of row set to non-zero 203 | r: 3, c: 4, 204 | data: []float64{ 205 | 1, 0, 0, 0, 206 | 0, 2, 1, 0, 207 | 0, 0, 3, 6, 208 | }, 209 | i: 2, j: 0, 210 | v: 5, 211 | result: []float64{ 212 | 1, 0, 0, 0, 213 | 0, 2, 1, 0, 214 | 5, 0, 3, 6, 215 | }, 216 | }, 217 | { // 0 as first non-zero element of row set to non-zero 218 | r: 3, c: 4, 219 | data: []float64{ 220 | 1, 0, 0, 0, 221 | 0, 2, 1, 0, 222 | 0, 0, 3, 6, 223 | }, 224 | i: 2, j: 1, 225 | v: 5, 226 | result: []float64{ 227 | 1, 0, 0, 0, 228 | 0, 2, 1, 0, 229 | 0, 5, 3, 6, 230 | }, 231 | }, 232 | { // 0 as non-zero element in middle of row/col set to non-zero 233 | r: 3, c: 4, 234 | data: []float64{ 235 | 1, 0, 0, 0, 236 | 0, 2, 0, 7, 237 | 0, 0, 3, 6, 238 | }, 239 | i: 1, j: 2, 240 | v: 5, 241 | result: []float64{ 242 | 1, 0, 0, 0, 243 | 0, 2, 5, 7, 244 | 0, 0, 3, 6, 245 | }, 246 | }, 247 | { // non-zero value updated 248 | r: 3, c: 4, 249 | data: []float64{ 250 | 1, 0, 0, 0, 251 | 0, 2, 0, 0, 252 | 0, 0, 3, 6, 253 | }, 254 | i: 2, j: 2, 255 | v: 5, 256 | result: []float64{ 257 | 1, 0, 0, 0, 258 | 0, 2, 0, 0, 259 | 0, 0, 5, 6, 260 | }, 261 | }, 262 | { // 0 at end of row set to non-zero 263 | r: 3, c: 4, 264 | data: []float64{ 265 | 1, 0, 0, 0, 266 | 0, 2, 1, 0, 267 | 0, 0, 3, 0, 268 | }, 269 | i: 2, j: 3, 270 | v: 5, 271 | result: []float64{ 272 | 1, 0, 0, 0, 273 | 0, 2, 1, 0, 274 | 0, 0, 3, 5, 275 | }, 276 | }, 277 | { // 0 on all zero row/column set to non-zero 278 | r: 3, c: 4, 279 | data: []float64{ 280 | 1, 0, 2, 0, 281 | 0, 0, 0, 0, 282 | 0, 0, 3, 6, 283 | }, 284 | i: 1, j: 1, 285 | v: 5, 286 | result: []float64{ 287 | 1, 0, 2, 0, 288 | 0, 5, 0, 0, 289 | 0, 0, 3, 6, 290 | }, 291 | }, 292 | } 293 | 294 | for ti, test := range tests { 295 | t.Logf("**** Test Run %d.\n", ti+1) 296 | 297 | expected := mat.NewDense(test.r, test.c, test.result) 298 | 299 | csr := CreateCSR(test.r, test.c, test.data).(*CSR) 300 | csc := CreateCSC(test.r, test.c, test.data).(*CSC) 301 | 302 | csr.Set(test.i, test.j, test.v) 303 | csc.Set(test.i, test.j, test.v) 304 | 305 | if !mat.Equal(expected, csr) { 306 | t.Logf("For CSR.Set(), Expected:\n%v\n but received:\n%v\n", mat.Formatted(expected), mat.Formatted(csr)) 307 | t.Fail() 308 | } 309 | if !mat.Equal(expected, csc) { 310 | t.Logf("For CSC.Set(), Expected:\n%v\n but received:\n%v\n", mat.Formatted(expected), mat.Formatted(csc)) 311 | t.Fail() 312 | } 313 | } 314 | } 315 | 316 | func TestCSRCSCRowColView(t *testing.T) { 317 | var tests = []struct { 318 | r, c int 319 | data []float64 320 | }{ 321 | { 322 | r: 3, c: 4, 323 | data: []float64{ 324 | 1, 0, 0, 0, 325 | 0, 2, 0, 0, 326 | 0, 0, 3, 6, 327 | }, 328 | }, 329 | { 330 | r: 3, c: 4, 331 | data: []float64{ 332 | 1, 0, 0, 0, 333 | 0, 0, 0, 0, 334 | 0, 0, 3, 0, 335 | }, 336 | }, 337 | } 338 | 339 | for ti, test := range tests { 340 | t.Logf("**** Test Run %d.\n", ti+1) 341 | 342 | dense := mat.NewDense(test.r, test.c, test.data) 343 | csr := CreateCSR(test.r, test.c, test.data).(*CSR) 344 | csc := CreateCSC(test.r, test.c, test.data).(*CSC) 345 | 346 | for i := 0; i < test.r; i++ { 347 | row := csr.RowView(i) 348 | for k := 0; k < row.Len(); k++ { 349 | if row.At(k, 0) != test.data[i*test.c+k] { 350 | t.Logf("ROW: Vector = \n%v\nElement %d = %f was not element %d, %d from \n%v\n", mat.Formatted(row), k, row.At(k, 0), i, k, mat.Formatted(dense)) 351 | t.Fail() 352 | } 353 | } 354 | } 355 | 356 | for j := 0; j < test.c; j++ { 357 | col := csc.ColView(j) 358 | for k := 0; k < col.Len(); k++ { 359 | if col.At(k, 0) != test.data[k*test.c+j] { 360 | t.Logf("COL: Vector = \n%v\nElement %d = %f was not element %d, %d from \n%v\n", mat.Formatted(col), k, col.At(k, 0), k, j, mat.Formatted(dense)) 361 | t.Fail() 362 | } 363 | } 364 | } 365 | } 366 | } 367 | 368 | func TestCSRCSCDoNonZero(t *testing.T) { 369 | var tests = []struct { 370 | r, c int 371 | data []float64 372 | }{ 373 | { 374 | r: 3, c: 3, 375 | data: []float64{ 376 | 1, 3, 6, 377 | 0, 2, 0, 378 | 1, 0, 3, 379 | }, 380 | }, 381 | { 382 | r: 3, c: 4, 383 | data: []float64{ 384 | 1, 0, 0, 4, 385 | 0, 0, 0, 0, 386 | 1, 0, 3, 8, 387 | }, 388 | }, 389 | { 390 | r: 4, c: 3, 391 | data: []float64{ 392 | 1, 0, 0, 393 | 0, 0, 0, 394 | 0, 0, 3, 395 | 4, 0, 8, 396 | }, 397 | }, 398 | } 399 | creatorFuncs := map[string]MatrixCreator{ 400 | "csr": CreateCSR, 401 | "csc": CreateCSC, 402 | } 403 | 404 | for creatorKey, creator := range creatorFuncs { 405 | for ti, test := range tests { 406 | t.Logf("**** Test Run %d. using %s\n", ti+1, creatorKey) 407 | 408 | matrix := creator(test.r, test.c, test.data).(Sparser) 409 | 410 | var nnz int 411 | matrix.DoNonZero(func(i, j int, v float64) { 412 | if testv := test.data[i*test.c+j]; testv == 0 || testv != v { 413 | t.Logf("Expected %f at (%d, %d) but received %f\n", v, i, j, testv) 414 | t.Fail() 415 | } 416 | nnz++ 417 | }) 418 | 419 | if nnz != matrix.NNZ() { 420 | t.Logf("Expected %d Non Zero elements but found %d", nnz, matrix.NNZ()) 421 | t.Fail() 422 | } 423 | } 424 | } 425 | } 426 | 427 | func TestCSTrace(t *testing.T) { 428 | var tests = []struct { 429 | s int 430 | theType MatrixType 431 | density float32 432 | }{ 433 | { 434 | s: 8, 435 | theType: CSRFormat, 436 | density: 0.1, 437 | }, 438 | { 439 | s: 8, 440 | theType: CSCFormat, 441 | density: 0.1, 442 | }, 443 | { 444 | s: 80, 445 | theType: CSRFormat, 446 | density: 0.75, 447 | }, 448 | { 449 | s: 80, 450 | theType: CSCFormat, 451 | density: 0.75, 452 | }, 453 | } 454 | for _, test := range tests { 455 | m := Random(test.theType, test.s, test.s, test.density) 456 | tr := mat.Trace(m) 457 | var checkTr float64 458 | for i := 0; i < test.s; i++ { 459 | checkTr += m.At(i, i) 460 | } 461 | if !scalar.EqualWithinAbs(tr, checkTr, 1e-13) { 462 | t.Logf("trace mismatch: %f vs %f", tr, checkTr) 463 | t.Fail() 464 | } 465 | 466 | } 467 | } 468 | 469 | func TestCSRCSCCull(t *testing.T) { 470 | var tests = []struct { 471 | r, c int 472 | data []float64 473 | nnz int 474 | epsilon float64 475 | nnzE int 476 | expected []float64 477 | }{ 478 | { 479 | r: 3, c: 4, 480 | data: []float64{ 481 | 1, 0, 0, 0, 482 | 0, 2, 0, 0, 483 | 0, 0, 3, 6, 484 | }, 485 | nnz: 4, 486 | epsilon: 0.0, 487 | nnzE: 4, 488 | expected: []float64{ 489 | 1, 0, 0, 0, 490 | 0, 2, 0, 0, 491 | 0, 0, 3, 6, 492 | }, 493 | }, 494 | { 495 | r: 3, c: 4, 496 | data: []float64{ 497 | 1, 0, 0, 0, 498 | 0, 2, 0, 0, 499 | 0, 0, 3, 6, 500 | }, 501 | nnz: 4, 502 | epsilon: 2.5, 503 | nnzE: 2, 504 | expected: []float64{ 505 | 0, 0, 0, 0, 506 | 0, 0, 0, 0, 507 | 0, 0, 3, 6, 508 | }, 509 | }, 510 | } 511 | 512 | for ti, test := range tests { 513 | t.Logf("**** Test Run %d.\n", ti+1) 514 | 515 | expected := mat.NewDense(test.r, test.c, test.expected) 516 | csr := CreateCSR(test.r, test.c, test.data).(*CSR) 517 | csc := CreateCSC(test.r, test.c, test.data).(*CSC) 518 | 519 | nnzCSR := csr.NNZ() 520 | nnzCSC := csc.NNZ() 521 | 522 | if nnzCSR != test.nnz { 523 | t.Logf("CSR NNZ is %d vs %d", nnzCSR, test.nnz) 524 | t.Fail() 525 | } 526 | if nnzCSC != test.nnz { 527 | t.Logf("CSC NNZ is %d vs %d", nnzCSC, test.nnz) 528 | t.Fail() 529 | } 530 | 531 | csrLen := len(csr.matrix.Data) 532 | cscLen := len(csc.matrix.Data) 533 | if csrLen != test.nnz { 534 | t.Logf("CSR data length incorrect: %d, %d", csrLen, test.nnz) 535 | t.Fail() 536 | } 537 | if cscLen != test.nnz { 538 | t.Logf("CSC data length incorrect: %d, %d", cscLen, test.nnz) 539 | t.Fail() 540 | } 541 | 542 | csr.Cull(test.epsilon) 543 | csc.Cull(test.epsilon) 544 | 545 | if !mat.Equal(csr, expected) { 546 | t.Logf("Expected:\n%v\n but received:\n%v\n", mat.Formatted(expected), mat.Formatted(csr)) 547 | t.Fail() 548 | } 549 | if !mat.Equal(csc, expected) { 550 | t.Logf("Expected:\n%v\n but received:\n%v\n", mat.Formatted(expected), mat.Formatted(csc)) 551 | t.Fail() 552 | } 553 | 554 | nnzECSR := csr.NNZ() 555 | nnzECSC := csc.NNZ() 556 | if nnzECSR != test.nnzE { 557 | t.Logf("CSR NNZE is %d vs %d", nnzECSR, test.nnzE) 558 | t.Fail() 559 | } 560 | if nnzECSC != test.nnzE { 561 | t.Logf("CSC NNZE is %d vs %d", nnzECSC, test.nnzE) 562 | t.Fail() 563 | } 564 | 565 | csrELen := len(csr.matrix.Data) 566 | cscELen := len(csc.matrix.Data) 567 | if csrELen != test.nnzE { 568 | t.Logf("CSR data length incorrect: %d, %d", csrELen, test.nnzE) 569 | t.Fail() 570 | } 571 | if cscELen != test.nnzE { 572 | t.Logf("CSC data length incorrect: %d, %d", cscELen, test.nnzE) 573 | t.Fail() 574 | } 575 | 576 | csr2 := CreateCSR(test.r, test.c, test.data).(*CSR) 577 | csc2 := CreateCSC(test.r, test.c, test.data).(*CSC) 578 | cscT := csc2.T().(*CSR) 579 | csrT := csr2.T().(*CSC) 580 | nnzCSRT := csrT.NNZ() 581 | nnzCSCT := cscT.NNZ() 582 | 583 | csrT.Cull(test.epsilon) 584 | cscT.Cull(test.epsilon) 585 | 586 | nnzECSRT := csrT.NNZ() 587 | nnzECSCT := cscT.NNZ() 588 | 589 | if nnzCSRT != test.nnz { 590 | t.Logf("CSRT NNZ is %d vs %d", nnzCSRT, test.nnz) 591 | t.Fail() 592 | } 593 | if nnzECSRT != test.nnzE { 594 | t.Logf("CSRT NNZE is %d vs %d", nnzECSRT, test.nnzE) 595 | t.Fail() 596 | } 597 | if nnzCSCT != test.nnz { 598 | t.Logf("CSCT NNZ is %d vs %d", nnzCSCT, test.nnz) 599 | t.Fail() 600 | } 601 | if nnzECSCT != test.nnzE { 602 | t.Logf("CSCT NNZE is %d vs %d", nnzECSCT, test.nnzE) 603 | t.Fail() 604 | } 605 | } 606 | } 607 | -------------------------------------------------------------------------------- /coordinate.go: -------------------------------------------------------------------------------- 1 | package sparse 2 | 3 | import ( 4 | "github.com/james-bowman/sparse/blas" 5 | "gonum.org/v1/gonum/mat" 6 | ) 7 | 8 | var ( 9 | _ Sparser = (*COO)(nil) 10 | _ TypeConverter = (*COO)(nil) 11 | _ mat.Mutable = (*COO)(nil) 12 | ) 13 | 14 | // COO is a COOrdinate format sparse matrix implementation (sometimes called `Triplet` format) and implements the 15 | // Matrix interface from gonum/matrix. This allows large sparse (mostly zero-valued) matrices to be stored 16 | // efficiently in memory (only storing non-zero values). COO matrices are good for constructing sparse matrices 17 | // initially and very good at converting to CSR and CSC formats but poor for arithmetic operations. As this 18 | // type implements the gonum mat.Matrix interface, it may be used with any of the Gonum mat functions that 19 | // accept Matrix types as parameters in place of other matrix types included in the Gonum mat package e.g. mat.Dense. 20 | type COO struct { 21 | r int 22 | c int 23 | rows []int 24 | cols []int 25 | data []float64 26 | } 27 | 28 | // NewCOO creates a new COOrdinate format sparse matrix. 29 | // The matrix is initialised to the size of the specified r * c dimensions (rows * columns) 30 | // with the specified slices containing either nil or containing rows and cols indexes of non-zero elements 31 | // and the non-zero data values themselves respectively. If not nil, the supplied slices will be used as the 32 | // backing storage to the matrix so changes to values of the slices will be reflected in the created matrix 33 | // and vice versa. 34 | func NewCOO(r int, c int, rows []int, cols []int, data []float64) *COO { 35 | if uint(r) < 0 { 36 | panic(mat.ErrRowAccess) 37 | } 38 | if uint(c) < 0 { 39 | panic(mat.ErrColAccess) 40 | } 41 | 42 | coo := &COO{r: r, c: c} 43 | 44 | if rows != nil || cols != nil || data != nil { 45 | if rows != nil && cols != nil && data != nil { 46 | coo.rows = rows 47 | coo.cols = cols 48 | coo.data = data 49 | } else { 50 | panic(mat.ErrRowAccess) 51 | } 52 | } 53 | 54 | return coo 55 | } 56 | 57 | // NNZ returns the number of stored data elements. This number includes explicit 58 | // zeroes, if stored, and may be exceed the total number of matrix elements 59 | // (rows * columns) if duplicate coordinates are stored. 60 | func (c *COO) NNZ() int { 61 | return len(c.data) 62 | } 63 | 64 | // DoNonZero calls the function fn for each of the stored data elements in the receiver. 65 | // The function fn takes a row/column index and the element value of the receiver at 66 | // (i, j). The order of visiting to each non-zero element is not guaranteed. 67 | func (c *COO) DoNonZero(fn func(i, j int, v float64)) { 68 | nnz := c.NNZ() 69 | for i := 0; i < nnz; i++ { 70 | fn(c.rows[i], c.cols[i], c.data[i]) 71 | } 72 | } 73 | 74 | // Dims returns the size of the matrix as the number of rows and columns 75 | func (c *COO) Dims() (int, int) { 76 | return c.r, c.c 77 | } 78 | 79 | // At returns the element of the matrix located at row i and column j. At will panic if specified values 80 | // for i or j fall outside the dimensions of the matrix. As the COO format allows duplicate elements, any 81 | // duplicate values will be summed together. 82 | func (c *COO) At(i, j int) float64 { 83 | if uint(i) < 0 || uint(i) >= uint(c.r) { 84 | panic(mat.ErrRowAccess) 85 | } 86 | if uint(j) < 0 || uint(j) >= uint(c.c) { 87 | panic(mat.ErrColAccess) 88 | } 89 | 90 | result := 0.0 91 | for k := 0; k < len(c.data); k++ { 92 | if c.rows[k] == i && c.cols[k] == j { 93 | // sum values for duplicate elements 94 | result += c.data[k] 95 | } 96 | } 97 | 98 | return result 99 | } 100 | 101 | // T transposes the matrix creating a new COO matrix, reusing the same underlying 102 | // storage, but switching column and row sizes and index slices i.e. rows become 103 | // columns and columns become rows. 104 | func (c *COO) T() mat.Matrix { 105 | return NewCOO(c.c, c.r, c.cols, c.rows, c.data) 106 | } 107 | 108 | // RawMatrix converts the matrix into a CSR matrix and returns a pointer 109 | // to the underlying blas sparse matrix. 110 | func (c *COO) RawMatrix() *blas.SparseMatrix { 111 | return c.ToCSR().RawMatrix() 112 | } 113 | 114 | // Set sets the element of the matrix located at row i and column j to equal the 115 | // specified value, v. Set will panic if specified values for i or j fall outside 116 | // the dimensions of the matrix. Duplicate values are allowed and will be added. 117 | func (c *COO) Set(i, j int, v float64) { 118 | if uint(i) < 0 || uint(i) >= uint(c.r) { 119 | panic(mat.ErrRowAccess) 120 | } 121 | if uint(j) < 0 || uint(j) >= uint(c.c) { 122 | panic(mat.ErrColAccess) 123 | } 124 | 125 | c.rows = append(c.rows, i) 126 | c.cols = append(c.cols, j) 127 | c.data = append(c.data, v) 128 | } 129 | 130 | // ToDense returns a mat.Dense dense format version of the matrix. The returned mat.Dense 131 | // matrix will not share underlying storage with the receiver. nor is the receiver modified by this call 132 | func (c *COO) ToDense() *mat.Dense { 133 | mat := mat.NewDense(c.r, c.c, nil) 134 | for i := 0; i < len(c.data); i++ { 135 | mat.Set(c.rows[i], c.cols[i], mat.At(c.rows[i], c.cols[i])+c.data[i]) 136 | } 137 | 138 | return mat 139 | } 140 | 141 | // ToDOK returns a DOK (Dictionary Of Keys) sparse format version of the matrix. The returned DOK 142 | // matrix will not share underlying storage with the receiver nor is the receiver modified by this call. 143 | func (c *COO) ToDOK() *DOK { 144 | dok := NewDOK(c.r, c.c) 145 | for i := 0; i < len(c.data); i++ { 146 | dok.Set(c.rows[i], c.cols[i], dok.At(c.rows[i], c.cols[i])+c.data[i]) 147 | } 148 | 149 | return dok 150 | } 151 | 152 | // ToCOO returns the receiver 153 | func (c *COO) ToCOO() *COO { 154 | return c 155 | } 156 | 157 | func cumsum(p []int, c []int, n int) int { 158 | nz := 0 159 | for i := 0; i < n; i++ { 160 | p[i] = nz 161 | nz += c[i] 162 | c[i] = p[i] 163 | } 164 | p[n] = nz 165 | return nz 166 | } 167 | 168 | func compress(row []int, col []int, data []float64, n int) (ia []int, ja []int, d []float64) { 169 | //w := make([]int, n+1) 170 | w := getInts(n+1, true) 171 | defer putInts(w) 172 | ia = make([]int, n+1) 173 | ja = make([]int, len(col)) 174 | d = make([]float64, len(data)) 175 | 176 | for _, v := range row { 177 | w[v]++ 178 | } 179 | cumsum(ia, w, n) 180 | 181 | for j, v := range col { 182 | p := w[row[j]] 183 | ja[p] = v 184 | d[p] = data[j] 185 | w[row[j]]++ 186 | } 187 | return 188 | } 189 | 190 | func dedupe(ia []int, ja []int, d []float64, m int, n int) ([]int, []float64) { 191 | //w := make([]int, n) 192 | w := getInts(n, true) 193 | defer putInts(w) 194 | nz := 0 195 | 196 | for i := 0; i < m; i++ { 197 | q := nz 198 | for j := ia[i]; j < ia[i+1]; j++ { 199 | if w[ja[j]] > q { 200 | d[w[ja[j]]] += d[j] 201 | } else { 202 | w[ja[j]] = nz 203 | ja[nz] = ja[j] 204 | d[nz] = d[j] 205 | nz++ 206 | } 207 | } 208 | ia[i] = q 209 | } 210 | ia[m] = nz 211 | 212 | return ja[:nz], d[:nz] 213 | } 214 | 215 | func compressInPlace(row []int, col []int, data []float64, n int) (ia []int, ja []int, d []float64) { 216 | //w := make([]int, n+1) 217 | w := getInts(n+1, true) 218 | defer putInts(w) 219 | for _, v := range row { 220 | w[v+1]++ 221 | } 222 | for i := 0; i < n; i++ { 223 | w[i+1] += w[i] 224 | } 225 | 226 | var i, j int 227 | var ipos, iNext, jNext int 228 | var dt, dNext float64 229 | for init := 0; init < len(data); { 230 | dt = data[init] 231 | i = row[init] 232 | j = col[init] 233 | row[init] = -1 234 | for { 235 | ipos = w[i] 236 | dNext = data[ipos] 237 | iNext = row[ipos] 238 | jNext = col[ipos] 239 | 240 | data[ipos] = dt 241 | col[ipos] = j 242 | row[ipos] = -1 243 | w[i]++ 244 | if iNext < 0 { 245 | break 246 | } 247 | dt = dNext 248 | i = iNext 249 | j = jNext 250 | } 251 | init++ 252 | for init < len(data) && row[init] < 0 { 253 | init++ 254 | } 255 | } 256 | 257 | ia = useInts(row, n+1, false) 258 | ia[0] = 0 259 | for i := 0; i < n; i++ { 260 | ia[i+1] = w[i] 261 | } 262 | ja = col 263 | d = data 264 | 265 | return 266 | } 267 | 268 | // ToCSR returns a CSR (Compressed Sparse Row)(AKA CRS (Compressed Row Storage)) sparse format 269 | // version of the matrix. The returned CSR matrix will not share underlying storage with the 270 | // receiver nor is the receiver modified by this call. 271 | func (c *COO) ToCSR() *CSR { 272 | ia, ja, data := compress(c.rows, c.cols, c.data, c.r) 273 | ja, data = dedupe(ia, ja, data, c.r, c.c) 274 | return NewCSR(c.r, c.c, ia, ja, data) 275 | } 276 | 277 | // ToCSRReuseMem returns a CSR (Compressed Sparse Row)(AKA CRS (Compressed Row Storage)) sparse format 278 | // version of the matrix. Unlike with ToCSR(), The returned CSR matrix WILL share underlying storage with the 279 | // receiver and the receiver will be modified by this call. 280 | func (c *COO) ToCSRReuseMem() *CSR { 281 | ia, ja, data := compressInPlace(c.rows, c.cols, c.data, c.r) 282 | return NewCSR(c.r, c.c, ia, ja, data) 283 | } 284 | 285 | // ToCSC returns a CSC (Compressed Sparse Column)(AKA CCS (Compressed Column Storage)) sparse format 286 | // version of the matrix. The returned CSC matrix will not share underlying storage with the 287 | // receiver nor is the receiver modified by this call. 288 | func (c *COO) ToCSC() *CSC { 289 | ja, ia, data := compress(c.cols, c.rows, c.data, c.c) 290 | ia, data = dedupe(ja, ia, data, c.c, c.r) 291 | return NewCSC(c.r, c.c, ja, ia, data) 292 | } 293 | 294 | // ToCSCReuseMem returns a CSC (Compressed Sparse Column)(AKA CCS (Compressed Column Storage)) sparse format 295 | // version of the matrix. Unlike with ToCSC(), The returned CSC matrix WILL share underlying storage with the 296 | // receiver and the receiver will be modified by this call. 297 | func (c *COO) ToCSCReuseMem() *CSC { 298 | ja, ia, data := compressInPlace(c.cols, c.rows, c.data, c.c) 299 | return NewCSC(c.r, c.c, ja, ia, data) 300 | } 301 | 302 | // ToType returns an alternative format version fo the matrix in the format specified. 303 | func (c *COO) ToType(matType MatrixType) mat.Matrix { 304 | return matType.Convert(c) 305 | } 306 | 307 | // MulVecTo performs matrix vector multiplication (dst+=A*x or dst+=A^T*x), where A is 308 | // the receiver, and stores the result in dst. MulVecTo panics if ac != len(x) or 309 | // ar != len(dst) 310 | func (c *COO) MulVecTo(dst []float64, trans bool, x []float64) { 311 | if trans { 312 | if c.c != len(dst) || c.r != len(x) { 313 | panic(mat.ErrShape) 314 | } 315 | for i, v := range c.data { 316 | dst[c.cols[i]] += v * x[c.rows[i]] 317 | } 318 | return 319 | } 320 | 321 | if c.c != len(x) || c.r != len(dst) { 322 | panic(mat.ErrShape) 323 | } 324 | for i, v := range c.data { 325 | dst[c.rows[i]] += v * x[c.cols[i]] 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /coordinate_test.go: -------------------------------------------------------------------------------- 1 | package sparse 2 | 3 | import ( 4 | "math/rand" 5 | "testing" 6 | 7 | "gonum.org/v1/gonum/mat" 8 | ) 9 | 10 | func CreateCOOWithDupes(m, n int, data []float64) mat.Matrix { 11 | coo := CreateCOO(m, n, data).(*COO) 12 | for k := 0; k < rand.Intn(m*n-1)+1; k++ { 13 | i := rand.Intn(m) 14 | j := rand.Intn(n) 15 | coo.Set(i, j, 5) 16 | coo.Set(i, j, -5) 17 | } 18 | return coo 19 | } 20 | 21 | func TestCOOConversion(t *testing.T) { 22 | r, c := 3, 4 23 | data := []float64{ 24 | 1, 0, 0, 7, 25 | 0, 2, 4, 0, 26 | 3, 0, 3, 6, 27 | } 28 | 29 | var tests = []struct { 30 | desc string 31 | create MatrixCreator 32 | convert func(a TypeConverter) Sparser 33 | }{ 34 | { 35 | "COO -> DOK", 36 | CreateCOO, 37 | func(a TypeConverter) Sparser { return a.ToDOK() }, 38 | }, 39 | { 40 | "COO -> CSR", 41 | CreateCOO, 42 | func(a TypeConverter) Sparser { return a.ToCSR() }, 43 | }, 44 | { 45 | "COO -> CSR (With Dupes)", 46 | CreateCOOWithDupes, 47 | func(a TypeConverter) Sparser { return a.ToCSR() }, 48 | }, 49 | { 50 | "COO -> CSC", 51 | CreateCOO, 52 | func(a TypeConverter) Sparser { return a.ToCSC() }, 53 | }, 54 | } 55 | 56 | for ti, test := range tests { 57 | t.Logf("**** Test Run %d. %s\n", ti+1, test.desc) 58 | 59 | d := mat.NewDense(r, c, data) 60 | 61 | a := test.create(r, c, data) 62 | 63 | if !mat.Equal(d, a) { 64 | t.Logf("A : %v\n", a) 65 | t.Logf("Expected:\n%v\n but created:\n%v\n", mat.Formatted(d), mat.Formatted(a)) 66 | t.Fail() 67 | } 68 | 69 | sa, ok := a.(Sparser) 70 | if !ok { 71 | t.Fatalf("Created matrix type does not implement Sparser") 72 | } 73 | 74 | b := test.convert(sa.(TypeConverter)) 75 | 76 | if !mat.Equal(a, b) { 77 | t.Logf("A : %v\n", a) 78 | t.Logf("B : %v\n", b) 79 | t.Logf("Expected:\n%v\n but received:\n%v\n", mat.Formatted(a), mat.Formatted(b)) 80 | t.Fail() 81 | } 82 | 83 | if !mat.Equal(d, a) { 84 | t.Logf("D : %v\n", d) 85 | t.Logf("A : %v\n", a) 86 | t.Logf("Original matrix changed - Expected:\n%v\n but received:\n%v\n", mat.Formatted(d), mat.Formatted(a)) 87 | t.Fail() 88 | } 89 | } 90 | } 91 | 92 | func TestCOODoNonZero(t *testing.T) { 93 | var tests = []struct { 94 | r, c int 95 | data []float64 96 | }{ 97 | { 98 | r: 3, c: 3, 99 | data: []float64{ 100 | 1, 0, 3, 101 | 0, 2, 0, 102 | 1, 0, 3, 103 | }, 104 | }, 105 | { 106 | r: 3, c: 4, 107 | data: []float64{ 108 | 1, 0, 5, 8, 109 | 0, 0, 0, 0, 110 | 6, 0, 3, 8, 111 | }, 112 | }, 113 | { 114 | r: 4, c: 3, 115 | data: []float64{ 116 | 1, 0, 8, 117 | 0, 0, 0, 118 | 3, 0, 3, 119 | 2, 0, 0, 120 | }, 121 | }, 122 | } 123 | 124 | for ti, test := range tests { 125 | t.Logf("**** Test Run %d.\n", ti+1) 126 | 127 | matrix := CreateCOO(test.r, test.c, test.data).(*COO) 128 | 129 | var nnz int 130 | matrix.DoNonZero(func(i, j int, v float64) { 131 | if testv := test.data[i*test.c+j]; testv == 0 || testv != v { 132 | t.Logf("Expected %f at (%d, %d) but received %f\n", v, i, j, testv) 133 | t.Fail() 134 | } 135 | nnz++ 136 | }) 137 | 138 | if nnz != matrix.NNZ() { 139 | t.Logf("Expected %d Non Zero elements but found %d", nnz, matrix.NNZ()) 140 | t.Fail() 141 | } 142 | } 143 | } 144 | 145 | func TestCOOTranspose(t *testing.T) { 146 | tests := []struct { 147 | m *COO 148 | r int 149 | c int 150 | data []float64 151 | er int 152 | ec int 153 | edata []float64 154 | }{ 155 | { 156 | m: NewCOO( 157 | 3, 4, 158 | []int{0, 1, 2, 2}, 159 | []int{0, 1, 2, 3}, 160 | []float64{1, 2, 3, 6}, 161 | ), 162 | er: 4, ec: 3, 163 | edata: []float64{ 164 | 1, 0, 0, 165 | 0, 2, 0, 166 | 0, 0, 3, 167 | 0, 0, 6}, 168 | }, 169 | { 170 | m: NewCOO( 171 | 3, 4, 172 | []int{0, 2, 1, 2}, 173 | []int{0, 2, 1, 3}, 174 | []float64{1, 3, 2, 6}, 175 | ), 176 | er: 4, ec: 3, 177 | edata: []float64{ 178 | 1, 0, 0, 179 | 0, 2, 0, 180 | 0, 0, 3, 181 | 0, 0, 6}, 182 | }, 183 | } 184 | 185 | for ti, test := range tests { 186 | orig := mat.DenseCopyOf(test.m) 187 | 188 | e := mat.NewDense(test.er, test.ec, test.edata) 189 | 190 | tr := test.m.T() 191 | 192 | if !mat.Equal(e, tr) { 193 | t.Errorf("Test %d: Expected\n%v\nBut received\n%v\n", ti, mat.Formatted(e), mat.Formatted(tr)) 194 | } 195 | 196 | nt := tr.T() 197 | 198 | if !mat.Equal(orig, nt) { 199 | t.Errorf("Test %d: Transpose back Expected\n%v\nBut received\n%v\n", ti, mat.Formatted(orig), mat.Formatted(nt)) 200 | } 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /diagonal.go: -------------------------------------------------------------------------------- 1 | package sparse 2 | 3 | import ( 4 | "gonum.org/v1/gonum/floats" 5 | "gonum.org/v1/gonum/mat" 6 | ) 7 | 8 | var ( 9 | _ Sparser = (*DIA)(nil) 10 | _ mat.ColViewer = (*DIA)(nil) 11 | _ mat.RowViewer = (*DIA)(nil) 12 | ) 13 | 14 | // DIA matrix type is a specialised matrix designed to store DIAgonal values of square symmetrical 15 | // matrices (all zero values except along the diagonal running top left to bottom right). The DIA matrix 16 | // type is specifically designed to take advantage of the sparsity pattern of square symmetrical matrices. 17 | type DIA struct { 18 | m, n int 19 | data []float64 20 | } 21 | 22 | // NewDIA creates a new DIAgonal format sparse matrix. 23 | // The matrix is initialised to the size of the specified m * m dimensions (rows * columns) 24 | // (creating a square) with the specified slice containing it's diagonal values. The diagonal slice 25 | // will be used as the backing slice to the matrix so changes to values of the slice will be reflected 26 | // in the matrix. 27 | func NewDIA(m int, n int, diagonal []float64) *DIA { 28 | if uint(m) < 0 || m < len(diagonal) { 29 | panic(mat.ErrRowAccess) 30 | } 31 | if uint(n) < 0 || n < len(diagonal) { 32 | panic(mat.ErrColAccess) 33 | } 34 | 35 | return &DIA{m: m, n: n, data: diagonal} 36 | } 37 | 38 | // Dims returns the size of the matrix as the number of rows and columns 39 | func (d *DIA) Dims() (int, int) { 40 | return d.m, d.n 41 | } 42 | 43 | // At returns the element of the matrix located at row i and column j. At will panic if specified values 44 | // for i or j fall outside the dimensions of the matrix. 45 | func (d *DIA) At(i, j int) float64 { 46 | if uint(i) < 0 || uint(i) >= uint(d.m) { 47 | panic(mat.ErrRowAccess) 48 | } 49 | if uint(j) < 0 || uint(j) >= uint(d.n) { 50 | panic(mat.ErrColAccess) 51 | } 52 | 53 | if i == j { 54 | return d.data[i] 55 | } 56 | return 0 57 | } 58 | 59 | // T returns the matrix transposed. In the case of a DIA (DIAgonal) sparse matrix this method 60 | // returns a new DIA matrix with the m and n values transposed. 61 | func (d *DIA) T() mat.Matrix { 62 | return &DIA{m: d.n, n: d.m, data: d.data} 63 | } 64 | 65 | // DoNonZero calls the function fn for each of the non-zero elements of the receiver. 66 | // The function fn takes a row/column index and the element value of the receiver at 67 | // (i, j). The order of visiting to each non-zero element is from top left to bottom right. 68 | func (d *DIA) DoNonZero(fn func(i, j int, v float64)) { 69 | for i, v := range d.data { 70 | fn(i, i, v) 71 | } 72 | } 73 | 74 | // NNZ returns the Number of Non Zero elements in the sparse matrix. 75 | func (d *DIA) NNZ() int { 76 | return len(d.data) 77 | } 78 | 79 | // Diagonal returns the diagonal values of the matrix from top left to bottom right. 80 | // The values are returned as a slice backed by the same array as backing the receiver 81 | // so changes to values in the returned slice will be reflected in the receiver. 82 | func (d *DIA) Diagonal() []float64 { 83 | return d.data 84 | } 85 | 86 | // RowView slices the matrix and returns a Vector containing a copy of elements 87 | // of row i. 88 | func (d *DIA) RowView(i int) mat.Vector { 89 | return mat.NewVecDense(d.n, d.ScatterRow(i, nil)) 90 | } 91 | 92 | // ColView slices the matrix and returns a Vector containing a copy of elements 93 | // of column j. 94 | func (d *DIA) ColView(j int) mat.Vector { 95 | return mat.NewVecDense(d.m, d.ScatterCol(j, nil)) 96 | } 97 | 98 | // ScatterRow returns a slice representing row i of the matrix in dense format. row 99 | // is used as the storage for the operation unless it is nil in which case, new 100 | // storage of the correct length will be allocated. This method will panic if i 101 | // is out of range or row is not the same length as the number of columns in the matrix i.e. 102 | // the correct size to receive the dense representation of the row. 103 | func (d *DIA) ScatterRow(i int, row []float64) []float64 { 104 | if i >= d.m || i < 0 { 105 | panic(mat.ErrRowAccess) 106 | } 107 | if row != nil && len(row) != d.n { 108 | panic(mat.ErrRowLength) 109 | } 110 | if row == nil { 111 | row = make([]float64, d.n) 112 | } 113 | if i < len(d.data) { 114 | row[i] = d.data[i] 115 | } 116 | return row 117 | } 118 | 119 | // ScatterCol returns a slice representing column j of the matrix in dense format. Col 120 | // is used as the storage for the operation unless it is nil in which case, new 121 | // storage of the correct length will be allocated. This method will panic if j 122 | // is out of range or col is not the same length as the number of rows in the matrix i.e. 123 | // the correct size to receive the dense representation of the column. 124 | func (d *DIA) ScatterCol(j int, col []float64) []float64 { 125 | if j >= d.n || j < 0 { 126 | panic(mat.ErrColAccess) 127 | } 128 | if col != nil && len(col) != d.m { 129 | panic(mat.ErrColLength) 130 | } 131 | if col == nil { 132 | col = make([]float64, d.m) 133 | } 134 | if j < len(d.data) { 135 | col[j] = d.data[j] 136 | } 137 | return col 138 | } 139 | 140 | // MulVecTo performs matrix vector multiplication (dst+=A*x or dst+=A^T*x), where A is 141 | // the receiver, and stores the result in dst. MulVecTo panics if ac != len(x) or 142 | // ar != len(dst) 143 | func (d *DIA) MulVecTo(dst []float64, trans bool, x []float64) { 144 | if !trans { 145 | if d.n != len(x) || d.m != len(dst) { 146 | panic(mat.ErrShape) 147 | } 148 | } else { 149 | if d.m != len(x) || d.n != len(dst) { 150 | panic(mat.ErrShape) 151 | } 152 | } 153 | 154 | for i, v := range d.data { 155 | dst[i] += v * x[i] 156 | } 157 | } 158 | 159 | // Trace returns the trace. 160 | func (d *DIA) Trace() float64 { 161 | return floats.Sum(d.data) 162 | } 163 | -------------------------------------------------------------------------------- /diagonal_test.go: -------------------------------------------------------------------------------- 1 | package sparse 2 | 3 | import ( 4 | "testing" 5 | 6 | "gonum.org/v1/gonum/floats/scalar" 7 | "gonum.org/v1/gonum/mat" 8 | ) 9 | 10 | func TestDIARowColView(t *testing.T) { 11 | var tests = []struct { 12 | r, c int 13 | data []float64 14 | }{ 15 | { 16 | r: 4, c: 4, 17 | data: []float64{ 18 | 1, 0, 0, 0, 19 | 0, 2, 0, 0, 20 | 0, 0, 3, 0, 21 | 0, 0, 0, 4, 22 | }, 23 | }, 24 | { 25 | r: 4, c: 4, 26 | data: []float64{ 27 | 1, 0, 0, 0, 28 | 0, 0, 0, 0, 29 | 0, 0, 3, 0, 30 | 0, 0, 0, 5, 31 | }, 32 | }, 33 | { 34 | r: 4, c: 5, 35 | data: []float64{ 36 | 1, 0, 0, 0, 0, 37 | 0, 2, 0, 0, 0, 38 | 0, 0, 3, 0, 0, 39 | 0, 0, 0, 4, 0, 40 | }, 41 | }, 42 | { 43 | r: 5, c: 4, 44 | data: []float64{ 45 | 1, 0, 0, 0, 46 | 0, 0, 0, 0, 47 | 0, 0, 3, 0, 48 | 0, 0, 0, 5, 49 | 0, 0, 0, 0, 50 | }, 51 | }, 52 | } 53 | 54 | for ti, test := range tests { 55 | t.Logf("**** Test Run %d.\n", ti+1) 56 | 57 | dense := mat.NewDense(test.r, test.c, test.data) 58 | dia := CreateDIA(test.r, test.c, test.data).(*DIA) 59 | 60 | for i := 0; i < test.r; i++ { 61 | row := dia.RowView(i) 62 | for k := 0; k < row.Len(); k++ { 63 | if row.At(k, 0) != test.data[i*test.c+k] { 64 | t.Logf("ROW: Vector = \n%v\nElement %d = %f was not element %d, %d from \n%v\n", mat.Formatted(row), k, row.At(k, 0), i, k, mat.Formatted(dense)) 65 | t.Fail() 66 | } 67 | } 68 | } 69 | 70 | for j := 0; j < test.c; j++ { 71 | col := dia.ColView(j) 72 | for k := 0; k < col.Len(); k++ { 73 | if col.At(k, 0) != test.data[k*test.c+j] { 74 | t.Logf("COL: Vector = \n%v\nElement %d = %f was not element %d, %d from \n%v\n", mat.Formatted(col), k, col.At(k, 0), k, j, mat.Formatted(dense)) 75 | t.Fail() 76 | } 77 | } 78 | } 79 | } 80 | } 81 | 82 | func TestDIADoNonZero(t *testing.T) { 83 | var tests = []struct { 84 | r, c int 85 | data []float64 86 | }{ 87 | { 88 | r: 3, c: 3, 89 | data: []float64{ 90 | 1, 0, 0, 91 | 0, 2, 0, 92 | 0, 0, 3, 93 | }, 94 | }, 95 | { 96 | r: 3, c: 4, 97 | data: []float64{ 98 | 1, 0, 0, 0, 99 | 0, 2, 0, 0, 100 | 0, 0, 3, 0, 101 | }, 102 | }, 103 | { 104 | r: 4, c: 3, 105 | data: []float64{ 106 | 1, 0, 0, 107 | 0, 2, 0, 108 | 0, 0, 3, 109 | 0, 0, 0, 110 | }, 111 | }, 112 | } 113 | 114 | for ti, test := range tests { 115 | t.Logf("**** Test Run %d.\n", ti+1) 116 | 117 | matrix := CreateDIA(test.r, test.c, test.data).(*DIA) 118 | 119 | var nnz int 120 | matrix.DoNonZero(func(i, j int, v float64) { 121 | if testv := test.data[i*test.c+j]; testv == 0 || testv != v { 122 | t.Logf("Expected %f at (%d, %d) but received %f\n", v, i, j, testv) 123 | t.Fail() 124 | } 125 | nnz++ 126 | }) 127 | 128 | if nnz != matrix.NNZ() { 129 | t.Logf("Expected %d Non Zero elements but found %d", nnz, matrix.NNZ()) 130 | t.Fail() 131 | } 132 | } 133 | } 134 | 135 | func TestDIATrace(t *testing.T) { 136 | var tests = []struct { 137 | s int 138 | }{ 139 | {s: 8}, 140 | {s: 32}, 141 | {s: 100}, 142 | {s: 123}, 143 | } 144 | for _, test := range tests { 145 | dia := RandomDIA(test.s, test.s) 146 | tr := mat.Trace(dia) 147 | var checkTr float64 148 | for i := 0; i < test.s; i++ { 149 | checkTr += dia.At(i, i) 150 | } 151 | if !scalar.EqualWithinAbs(tr, checkTr, 1e-13) { 152 | t.Logf("trace mismatch: %f vs %f", tr, checkTr) 153 | t.Fail() 154 | } 155 | 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /dictionaryofkeys.go: -------------------------------------------------------------------------------- 1 | package sparse 2 | 3 | import ( 4 | "github.com/james-bowman/sparse/blas" 5 | "gonum.org/v1/gonum/mat" 6 | ) 7 | 8 | var ( 9 | _ Sparser = (*DOK)(nil) 10 | _ TypeConverter = (*DOK)(nil) 11 | _ mat.Mutable = (*DOK)(nil) 12 | ) 13 | 14 | // key is used to specify the row and column of elements within the matrix. 15 | type key struct { 16 | i, j int 17 | } 18 | 19 | // DOK is a Dictionary Of Keys sparse matrix implementation and implements the Matrix interface from gonum/matrix. 20 | // This allows large sparse (mostly zero values) matrices to be stored efficiently in memory (only storing 21 | // non-zero values). DOK matrices are good for incrementally constructing sparse matrices but poor for arithmetic 22 | // operations or other operations that require iterating over elements of the matrix sequentially. As this type 23 | // implements the gonum mat.Matrix interface, it may be used with any of the Gonum mat functions that accept 24 | // Matrix types as parameters in place of other matrix types included in the Gonum mat package e.g. mat.Dense. 25 | type DOK struct { 26 | r int 27 | c int 28 | elements map[key]float64 29 | } 30 | 31 | // NewDOK creates a new Dictionary Of Keys format sparse matrix initialised to the size of the specified r * c 32 | // dimensions (rows * columns) 33 | func NewDOK(r, c int) *DOK { 34 | if r < 0 { 35 | panic(mat.ErrRowAccess) 36 | } 37 | if c < 0 { 38 | panic(mat.ErrColAccess) 39 | } 40 | 41 | return &DOK{r: r, c: c, elements: make(map[key]float64)} 42 | } 43 | 44 | // Dims returns the size of the matrix as the number of rows and columns 45 | func (d *DOK) Dims() (r, c int) { 46 | return d.r, d.c 47 | } 48 | 49 | // At returns the element of the matrix located at row i and column j. At will panic if specified values 50 | // for i or j fall outside the dimensions of the matrix. 51 | func (d *DOK) At(i, j int) float64 { 52 | if i < 0 || i >= d.r { 53 | panic(mat.ErrRowAccess) 54 | } 55 | if j < 0 || j >= d.c { 56 | panic(mat.ErrColAccess) 57 | } 58 | 59 | return d.elements[key{i, j}] 60 | } 61 | 62 | // T transposes the matrix. This is an implicit transpose, wrapping the matrix in a mat.Transpose type. 63 | func (d *DOK) T() mat.Matrix { 64 | return mat.Transpose{Matrix: d} 65 | } 66 | 67 | // Set sets the element of the matrix located at row i and column j to equal the specified value, v. Set 68 | // will panic if specified values for i or j fall outside the dimensions of the matrix. 69 | func (d *DOK) Set(i, j int, v float64) { 70 | if i < 0 || i >= d.r { 71 | panic(mat.ErrRowAccess) 72 | } 73 | if j < 0 || j >= d.c { 74 | panic(mat.ErrColAccess) 75 | } 76 | 77 | d.elements[key{i, j}] = v 78 | } 79 | 80 | // DoNonZero calls the function fn for each of the non-zero elements of the receiver. 81 | // The function fn takes a row/column index and the element value of the receiver at 82 | // (i, j). The order of visiting to each non-zero element in the receiver is random. 83 | func (d *DOK) DoNonZero(fn func(i, j int, v float64)) { 84 | for k, v := range d.elements { 85 | fn(k.i, k.j, v) 86 | } 87 | } 88 | 89 | // NNZ returns the Number of Non Zero elements in the sparse matrix. 90 | func (d *DOK) NNZ() int { 91 | return len(d.elements) 92 | } 93 | 94 | // RawMatrix converts the matrix to a CSR matrix and returns a pointer to 95 | // the underlying blas sparse matrix. 96 | func (d *DOK) RawMatrix() *blas.SparseMatrix { 97 | return d.ToCSR().RawMatrix() 98 | } 99 | 100 | // ToDense returns a mat.Dense dense format version of the matrix. The returned mat.Dense 101 | // matrix will not share underlying storage with the receiver nor is the receiver modified by this call. 102 | func (d *DOK) ToDense() *mat.Dense { 103 | mat := mat.NewDense(d.r, d.c, nil) 104 | 105 | for k, v := range d.elements { 106 | mat.Set(k.i, k.j, v) 107 | } 108 | 109 | return mat 110 | } 111 | 112 | // ToDOK returns the receiver 113 | func (d *DOK) ToDOK() *DOK { 114 | return d 115 | } 116 | 117 | // ToCOO returns a COOrdinate sparse format version of the matrix. The returned COO matrix will 118 | // not share underlying storage with the receiver nor is the receiver modified by this call. 119 | func (d *DOK) ToCOO() *COO { 120 | nnz := d.NNZ() 121 | rows := make([]int, nnz) 122 | cols := make([]int, nnz) 123 | data := make([]float64, nnz) 124 | 125 | i := 0 126 | for k, v := range d.elements { 127 | rows[i], cols[i], data[i] = k.i, k.j, v 128 | i++ 129 | } 130 | 131 | coo := NewCOO(d.r, d.c, rows, cols, data) 132 | 133 | return coo 134 | } 135 | 136 | // ToCSR returns a CSR (Compressed Sparse Row)(AKA CRS (Compressed Row Storage)) sparse format 137 | // version of the matrix. The returned CSR matrix will not share underlying storage with the 138 | // receiver nor is the receiver modified by this call. 139 | func (d *DOK) ToCSR() *CSR { 140 | return d.ToCOO().ToCSRReuseMem() 141 | } 142 | 143 | // ToCSC returns a CSC (Compressed Sparse Column)(AKA CCS (Compressed Column Storage)) sparse format 144 | // version of the matrix. The returned CSC matrix will not share underlying storage with the 145 | // receiver nor is the receiver modified by this call. 146 | func (d *DOK) ToCSC() *CSC { 147 | return d.ToCOO().ToCSCReuseMem() 148 | } 149 | 150 | // ToType returns an alternative format version fo the matrix in the format specified. 151 | func (d *DOK) ToType(matType MatrixType) mat.Matrix { 152 | return matType.Convert(d) 153 | } 154 | 155 | // MulVecTo performs matrix vector multiplication (dst+=A*x or dst+=A^T*x), where A is 156 | // the receiver, and stores the result in dst. MulVecTo panics if ac != len(x) or 157 | // ar != len(dst) 158 | func (d *DOK) MulVecTo(dst []float64, trans bool, x []float64) { 159 | d.ToCSR().MulVecTo(dst, trans, x) 160 | } 161 | -------------------------------------------------------------------------------- /dictionaryofkeys_test.go: -------------------------------------------------------------------------------- 1 | package sparse 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "gonum.org/v1/gonum/mat" 8 | ) 9 | 10 | func TestDOKConversion(t *testing.T) { 11 | var tests = []struct { 12 | m, n int 13 | data map[key]float64 14 | output []float64 15 | }{ 16 | { 17 | m: 11, n: 11, 18 | data: map[key]float64{ 19 | {0, 3}: 1, 20 | {1, 1}: 2, 21 | {2, 2}: 3, 22 | {5, 8}: 4, 23 | {10, 10}: 5, 24 | {1, 5}: 6, 25 | {3, 5}: 7, 26 | }, 27 | output: []float64{ 28 | 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 29 | 0, 2, 0, 0, 0, 6, 0, 0, 0, 0, 0, 30 | 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 31 | 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 32 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 33 | 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 34 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 35 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 36 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 37 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 38 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 39 | }, 40 | }, 41 | { 42 | m: 5, n: 4, 43 | data: map[key]float64{ 44 | {0, 3}: 1, 45 | {1, 1}: 2, 46 | {2, 2}: 3, 47 | {4, 2}: 4, 48 | {0, 0}: 5, 49 | {1, 3}: 6, 50 | {3, 3}: 7, 51 | }, 52 | output: []float64{ 53 | 5, 0, 0, 1, 54 | 0, 2, 0, 6, 55 | 0, 0, 3, 0, 56 | 0, 0, 0, 7, 57 | 0, 0, 4, 0, 58 | }, 59 | }, 60 | } 61 | 62 | for ti, test := range tests { 63 | t.Logf("**** Test Run %d.\n", ti+1) 64 | expected := mat.NewDense(test.m, test.n, test.output) 65 | 66 | dok := NewDOK(test.m, test.n) 67 | for k, v := range test.data { 68 | dok.Set(k.i, k.j, v) 69 | } 70 | 71 | dok2 := dok.ToDOK() 72 | if !(mat.Equal(expected, dok2)) { 73 | t.Logf("Expected:\n%v \nbut found DOK matrix:\n%v\n", mat.Formatted(expected), mat.Formatted(dok2)) 74 | t.Fail() 75 | } 76 | 77 | coo := dok.ToCOO() 78 | if !(mat.Equal(expected, coo)) { 79 | t.Logf("Expected:\n%v \nbut found COO matrix:\n%v\n", mat.Formatted(expected), mat.Formatted(coo)) 80 | t.Fail() 81 | } 82 | 83 | csr := dok.ToCSR() 84 | if !(mat.Equal(expected, csr)) { 85 | t.Logf("Expected:\n%v \nbut found CSR matrix:\n%v\n", mat.Formatted(expected), mat.Formatted(csr)) 86 | t.Fail() 87 | } 88 | 89 | csc := dok.ToCSC() 90 | if !(mat.Equal(expected, csc)) { 91 | t.Logf("Expected:\n%v \nbut found CSC matrix:\n%v\n", mat.Formatted(expected), mat.Formatted(csc)) 92 | t.Fail() 93 | } 94 | 95 | dense := dok.ToDense() 96 | if !(mat.Equal(expected, dense)) { 97 | t.Logf("Expected:\n%v \nbut found Dense matrix:\n%v\n", mat.Formatted(expected), mat.Formatted(dense)) 98 | t.Fail() 99 | } 100 | } 101 | } 102 | 103 | func TestDOKTranspose(t *testing.T) { 104 | var tests = []struct { 105 | r, c int 106 | data []float64 107 | er, ec int 108 | result []float64 109 | }{ 110 | { 111 | r: 3, c: 4, 112 | data: []float64{ 113 | 1, 0, 0, 0, 114 | 0, 2, 0, 0, 115 | 0, 0, 3, 6, 116 | }, 117 | er: 4, ec: 3, 118 | result: []float64{ 119 | 1, 0, 0, 120 | 0, 2, 0, 121 | 0, 0, 3, 122 | 0, 0, 6, 123 | }, 124 | }, 125 | } 126 | 127 | for ti, test := range tests { 128 | t.Logf("**** Test Run %d.\n", ti+1) 129 | 130 | expected := mat.NewDense(test.er, test.ec, test.result) 131 | 132 | dok := CreateDOK(test.r, test.c, test.data) 133 | 134 | if !mat.Equal(expected, dok.T()) { 135 | t.Logf("Expected:\n %v\n but received:\n %v\n", mat.Formatted(expected), mat.Formatted(dok.T())) 136 | t.Fail() 137 | } 138 | } 139 | } 140 | 141 | func TestDOKDoNonZero(t *testing.T) { 142 | var tests = []struct { 143 | r, c int 144 | data []float64 145 | }{ 146 | { 147 | r: 3, c: 4, 148 | data: []float64{ 149 | 1, 0, 6, 0, 150 | 0, 2, 0, 0, 151 | 0, 0, 3, 6, 152 | }, 153 | }, 154 | { 155 | r: 3, c: 4, 156 | data: []float64{ 157 | 1, 0, 7, 0, 158 | 0, 0, 0, 0, 159 | 6, 0, 3, 0, 160 | }, 161 | }, 162 | } 163 | 164 | for ti, test := range tests { 165 | t.Logf("**** Test Run %d.\n", ti+1) 166 | 167 | dok := CreateDOK(test.r, test.c, test.data).(*DOK) 168 | 169 | var nnz int 170 | dok.DoNonZero(func(i, j int, v float64) { 171 | if testv := test.data[i*test.c+j]; testv == 0 || testv != v { 172 | t.Logf("Expected %f at (%d, %d) but received %f\n", v, i, j, testv) 173 | t.Fail() 174 | } 175 | nnz++ 176 | }) 177 | 178 | if nnz != dok.NNZ() { 179 | t.Logf("Expected %d Non Zero elements but found %d", nnz, dok.NNZ()) 180 | t.Fail() 181 | } 182 | } 183 | } 184 | 185 | type MatrixCreator func(m, n int, data []float64) mat.Matrix 186 | 187 | func CreateDOK(m, n int, data []float64) mat.Matrix { 188 | dok := NewDOK(m, n) 189 | if data != nil { 190 | for i := 0; i < m; i++ { 191 | for j := 0; j < n; j++ { 192 | if data[i*n+j] != 0 { 193 | dok.Set(i, j, data[i*n+j]) 194 | } 195 | } 196 | } 197 | } 198 | 199 | return dok 200 | } 201 | 202 | func CreateCOO(m, n int, data []float64) mat.Matrix { 203 | return CreateDOK(m, n, data).(*DOK).ToCOO() 204 | } 205 | 206 | func CreateCSR(m, n int, data []float64) mat.Matrix { 207 | return CreateDOK(m, n, data).(*DOK).ToCSR() 208 | } 209 | 210 | func CreateCSC(m, n int, data []float64) mat.Matrix { 211 | return CreateDOK(m, n, data).(*DOK).ToCSC() 212 | } 213 | 214 | func CreateDIA(m, n int, data []float64) mat.Matrix { 215 | min := n 216 | if m <= n { 217 | min = m 218 | } 219 | 220 | c := make([]float64, min) 221 | for i := 0; i < min; i++ { 222 | c[i] = data[i*n+i] 223 | } 224 | return NewDIA(m, n, c) 225 | } 226 | 227 | func CreateDense(m, n int, data []float64) mat.Matrix { 228 | return mat.NewDense(m, n, data) 229 | } 230 | 231 | func TestFailDokInitialization(t *testing.T) { 232 | tcs := []struct { 233 | r, c int 234 | }{ 235 | {-1, 1}, 236 | {1, -1}, 237 | {-1, -1}, 238 | } 239 | 240 | for _, tc := range tcs { 241 | t.Run(fmt.Sprintf("%v", tc), func(t *testing.T) { 242 | defer func() { 243 | if r := recover(); r == nil { 244 | t.Errorf("Haven`t panic for wrong size: %v", tc) 245 | } 246 | }() 247 | _ = NewDOK(tc.r, tc.c) 248 | }) 249 | } 250 | } 251 | 252 | func TestFailDokIndexes(t *testing.T) { 253 | d := NewDOK(1, 1) 254 | 255 | tcs := []struct { 256 | r, c int 257 | }{ 258 | {-1, 1}, 259 | {1, -1}, 260 | {-1, -1}, 261 | {0, 2}, 262 | {2, 0}, 263 | {100, 100}, 264 | } 265 | 266 | for _, tc := range tcs { 267 | t.Run(fmt.Sprintf("At:%v", tc), func(t *testing.T) { 268 | defer func() { 269 | if r := recover(); r == nil { 270 | t.Errorf("Haven`t panic for wrong index: %v", tc) 271 | } 272 | }() 273 | _ = d.At(tc.r, tc.c) 274 | }) 275 | t.Run(fmt.Sprintf("Set:%v", tc), func(t *testing.T) { 276 | defer func() { 277 | if r := recover(); r == nil { 278 | t.Errorf("Haven`t panic for wrong index: %v", tc) 279 | } 280 | }() 281 | d.Set(tc.r, tc.c, -1.0) 282 | }) 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package sparse provides implementations of selected sparse matrix formats. Matrices and linear algebra are used extensively in scientific computing and machine learning applications. Large datasets are analysed comprising vectors of numerical features that represent some object. The nature of feature encoding schemes, especially those like "one hot", tends to lead to vectors with mostly zero values for many of the features. In text mining applications, where features are typically terms from a vocabulary, it is not uncommon for 99% of the elements within these vectors to contain zero values. 3 | 4 | Sparse matrix formats take advantage of this fact to optimise memory usage and processing performance by only storing and processing non-zero values. 5 | 6 | Sparse matrix formats can broadly be divided into 3 main categories: 7 | 8 | 1. Creational - Sparse matrix formats suited to construction and building of matrices. Matrix formats in this category include DOK (Dictionary Of Keys) and COO (COOrdinate aka triplet). 9 | 10 | 2. Operational - Sparse matrix formats suited to arithmetic operations e.g. multiplication. Matrix formats in this category include CSR (Compressed Sparse Row aka CRS - Compressed Row Storage) and CSC (Compressed Sparse Column aka CCS - Compressed Column Storage) 11 | 12 | 3. Specialised - Specialised matrix formats suiting specific sparsity patterns. Matrix formats in this category include DIA (DIAgonal) for efficiently storing and manipulating symmetric diagonal matrices. 13 | 14 | A common practice is to construct sparse matrices using a creational format e.g. DOK or COO and then convert them to an operational format e.g. CSR for arithmetic operations. 15 | 16 | All sparse matrix implementations in this package implement the Matrix interface defined within the gonum/mat package and so may be used interchangeably with matrix types defined within the package e.g. mat.Dense, mat.VecDense, etc. 17 | */ 18 | package sparse 19 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package sparse_test 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | 7 | "github.com/james-bowman/sparse" 8 | "gonum.org/v1/gonum/mat" 9 | ) 10 | 11 | func Example() { 12 | // Construct a new 3x2 DOK (Dictionary Of Keys) matrix 13 | dokMatrix := sparse.NewDOK(3, 2) 14 | 15 | // Populate it with some non-zero values 16 | dokMatrix.Set(0, 0, 5) 17 | dokMatrix.Set(2, 1, 7) 18 | 19 | // Demonstrate accessing values (could use mat.Formatted() to 20 | // pretty print but this demonstrates element access) 21 | m, n := dokMatrix.Dims() 22 | for i := 0; i < m; i++ { 23 | for j := 0; j < n; j++ { 24 | fmt.Printf("%.0f,", dokMatrix.At(i, j)) 25 | } 26 | fmt.Printf("\n") 27 | } 28 | 29 | // Convert DOK matrix to CSR (Compressed Sparse Row) matrix 30 | // just for fun (not required for upcoming multiplication operation) 31 | csrMatrix := dokMatrix.ToCSR() 32 | 33 | // Create a random 2x3 COO (COOrdinate) matrix with 34 | // density of 0.5 (half the elements will be non-zero) 35 | cooMatrix := sparse.Random(sparse.COOFormat, 2, 3, 0.5) 36 | 37 | // Convert CSR matrix to Gonum mat.Dense matrix just for fun 38 | // (not required for upcoming multiplication operation) 39 | // then transpose so it is the right shape/dimensions for 40 | // multiplication with the original CSR matrix 41 | denseMatrix := csrMatrix.ToDense().T() 42 | 43 | // Multiply the 2 matrices together and store the result in the 44 | // sparse receiver (multiplication with sparse product) 45 | var csrProduct sparse.CSR 46 | csrProduct.Mul(csrMatrix, cooMatrix) 47 | 48 | // As an alternative, use the sparse BLAS routines for efficient 49 | // sparse matrix multiplication with a Gonum mat.Dense product 50 | // (multiplication with dense product) 51 | denseProduct := sparse.MulMatMat(false, 1, csrMatrix, denseMatrix, nil) 52 | rows, cols := denseProduct.Dims() 53 | if rows != 2 && cols != 3 { 54 | fmt.Printf("Expected product 2x3 but received %dx%d\n", rows, cols) 55 | } 56 | 57 | // Output: 5,0, 58 | //0,0, 59 | //0,7, 60 | } 61 | 62 | func ExampleLU() { 63 | // ExampleLU created on example from: 64 | // https://www.gnu.org/software/gsl/doc/html/splinalg.html#examples 65 | 66 | size := 2000 // amount intermediate points 67 | n := size - 2 // without boundary condition 68 | A := sparse.NewDOK(n, n) // sparse matrix 69 | f := mat.NewDense(n, 1, nil) // right hand size vector 70 | d := mat.NewDense(n, 1, nil) // solution vector 71 | 72 | for i := 0; i < n; i++ { 73 | xi := float64(i+1) / float64(size-1) 74 | f.Set(i, 0, -math.Pow(math.Pi, 2.0)*math.Sin(math.Pi*xi)) 75 | 76 | dh := math.Pow(float64(size-1), 2.0) 77 | A.Set(i, i, -2*dh) 78 | if i+1 < n { 79 | A.Set(i, i+1, 1*dh) 80 | A.Set(i+1, i, 1*dh) 81 | } 82 | } 83 | 84 | // LU decomposition 85 | var lu mat.LU 86 | lu.Factorize(A) 87 | 88 | err := lu.SolveTo(d, false, f) 89 | if err != nil { 90 | fmt.Printf("err = %v", err) 91 | return 92 | } 93 | 94 | // compare with except result 95 | fmt.Printf("%-8s %-12s %-12s %-8s\n", 96 | "x,rad.", "Solved", "Expect", "Delta, %") 97 | for i := 0; i < n; i += 100 { 98 | h := 1.0 / float64(size-1) 99 | xi := float64(i+1) * h 100 | expect := math.Sin(math.Pi * xi) 101 | delta := (d.At(i, 0) - expect) / expect * 100 102 | fmt.Printf("%-8f %-12f %-12f %-8e\n", 103 | xi, d.At(i, 0), expect, delta) 104 | } 105 | 106 | // Output: 107 | // x,rad. Solved Expect Delta, % 108 | // 0.000500 0.001572 0.001572 2.058220e-05 109 | // 0.050525 0.158064 0.158064 2.058220e-05 110 | // 0.100550 0.310661 0.310661 2.058220e-05 111 | // 0.150575 0.455600 0.455600 2.058220e-05 112 | // 0.200600 0.589310 0.589310 2.058221e-05 113 | // 0.250625 0.708495 0.708495 2.058222e-05 114 | // 0.300650 0.810216 0.810216 2.058222e-05 115 | // 0.350675 0.891968 0.891968 2.058222e-05 116 | // 0.400700 0.951734 0.951734 2.058223e-05 117 | // 0.450725 0.988042 0.988042 2.058223e-05 118 | // 0.500750 0.999997 0.999997 2.058223e-05 119 | // 0.550775 0.987305 0.987304 2.058223e-05 120 | // 0.600800 0.950277 0.950276 2.058223e-05 121 | // 0.650825 0.889826 0.889826 2.058223e-05 122 | // 0.700850 0.807444 0.807444 2.058223e-05 123 | // 0.750875 0.705160 0.705159 2.058222e-05 124 | // 0.800900 0.585494 0.585494 2.058222e-05 125 | // 0.850925 0.451398 0.451398 2.058223e-05 126 | // 0.900950 0.306176 0.306176 2.058223e-05 127 | // 0.950975 0.153407 0.153407 2.058223e-05 128 | } 129 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/james-bowman/sparse 2 | 3 | go 1.14 4 | 5 | require ( 6 | golang.org/x/exp v0.0.0-20210220032938-85be41e4509f 7 | gonum.org/v1/gonum v0.8.2 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 2 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 3 | github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= 4 | github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= 5 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 6 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= 7 | github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= 8 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 9 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 10 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 11 | golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 12 | golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 13 | golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 14 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= 15 | golang.org/x/exp v0.0.0-20210220032938-85be41e4509f h1:GrkO5AtFUU9U/1f5ctbIBXtBGeSJbWwIYfIsTcFMaX4= 16 | golang.org/x/exp v0.0.0-20210220032938-85be41e4509f/go.mod h1:I6l2HNBLBZEcrOoCpyKLdY2lHoRZ8lI4x60KMCQDft4= 17 | golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= 18 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 19 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 20 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 21 | golang.org/x/mobile v0.0.0-20201217150744-e6ae53a27f4f/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4= 22 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 23 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 24 | golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 25 | golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 26 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 27 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 28 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 29 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 30 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 31 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 32 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 33 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 34 | golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 35 | golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 36 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 37 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 38 | golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 39 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 40 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 41 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 42 | gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= 43 | gonum.org/v1/gonum v0.8.2 h1:CCXrcPKiGGotvnN6jfUsKk4rRqm7q09/YbKb5xCEvtM= 44 | gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= 45 | gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0 h1:OE9mWmgKkjJyEmDAAtGMPjXu+YNeGvK9VTSHY6+Qihc= 46 | gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= 47 | gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= 48 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 49 | -------------------------------------------------------------------------------- /matrix.go: -------------------------------------------------------------------------------- 1 | package sparse 2 | 3 | import ( 4 | "math/rand" 5 | 6 | "github.com/james-bowman/sparse/blas" 7 | "gonum.org/v1/gonum/mat" 8 | ) 9 | 10 | // Sparser is the interface for Sparse matrices. Sparser contains the mat.Matrix interface so automatically 11 | // exposes all mat.Matrix methods. 12 | type Sparser interface { 13 | mat.Matrix 14 | mat.NonZeroDoer 15 | 16 | // NNZ returns the Number of Non Zero elements in the sparse matrix. 17 | NNZ() int 18 | } 19 | 20 | // TypeConverter interface for converting to other matrix formats 21 | type TypeConverter interface { 22 | // ToDense returns a mat.Dense dense format version of the matrix. 23 | ToDense() *mat.Dense 24 | 25 | // ToDOK returns a Dictionary Of Keys (DOK) sparse format version of the matrix. 26 | ToDOK() *DOK 27 | 28 | // ToCOO returns a COOrdinate sparse format version of the matrix. 29 | ToCOO() *COO 30 | 31 | // ToCSR returns a Compressed Sparse Row (CSR) sparse format version of the matrix. 32 | ToCSR() *CSR 33 | 34 | // ToCSC returns a Compressed Sparse Row (CSR) sparse format version of the matrix. 35 | ToCSC() *CSC 36 | 37 | // ToType returns an alternative format version fo the matrix in the format specified. 38 | ToType(matType MatrixType) mat.Matrix 39 | } 40 | 41 | // MatrixType represents a type of Matrix format. This is used to specify target format types for conversion, etc. 42 | type MatrixType interface { 43 | // Convert converts to the type of matrix format represented by the receiver from the specified TypeConverter. 44 | Convert(from TypeConverter) mat.Matrix 45 | } 46 | 47 | // DenseType represents the mat.Dense matrix type format 48 | type DenseType int 49 | 50 | // Convert converts the specified TypeConverter to mat.Dense format 51 | func (d DenseType) Convert(from TypeConverter) mat.Matrix { 52 | return from.ToDense() 53 | } 54 | 55 | // DOKType represents the DOK (Dictionary Of Keys) matrix type format 56 | type DOKType int 57 | 58 | // Convert converts the specified TypeConverter to DOK (Dictionary of Keys) format 59 | func (s DOKType) Convert(from TypeConverter) mat.Matrix { 60 | return from.ToDOK() 61 | } 62 | 63 | // COOType represents the COOrdinate matrix type format 64 | type COOType int 65 | 66 | // Convert converts the specified TypeConverter to COOrdinate format 67 | func (s COOType) Convert(from TypeConverter) mat.Matrix { 68 | return from.ToCOO() 69 | } 70 | 71 | // CSRType represents the CSR (Compressed Sparse Row) matrix type format 72 | type CSRType int 73 | 74 | // Convert converts the specified TypeConverter to CSR (Compressed Sparse Row) format 75 | func (s CSRType) Convert(from TypeConverter) mat.Matrix { 76 | return from.ToCSR() 77 | } 78 | 79 | // CSCType represents the CSC (Compressed Sparse Column) matrix type format 80 | type CSCType int 81 | 82 | // Convert converts the specified TypeConverter to CSC (Compressed Sparse Column) format 83 | func (s CSCType) Convert(from TypeConverter) mat.Matrix { 84 | return from.ToCSC() 85 | } 86 | 87 | const ( 88 | // DenseFormat is an enum value representing Dense matrix format 89 | DenseFormat DenseType = iota 90 | 91 | // DOKFormat is an enum value representing DOK matrix format 92 | DOKFormat DOKType = iota 93 | 94 | // COOFormat is an enum value representing COO matrix format 95 | COOFormat COOType = iota 96 | 97 | // CSRFormat is an enum value representing CSR matrix format 98 | CSRFormat CSRType = iota 99 | 100 | // CSCFormat is an enum value representing CSC matrix format 101 | CSCFormat CSCType = iota 102 | ) 103 | 104 | // Random constructs a new matrix of the specified type e.g. Dense, COO, CSR, etc. 105 | // It is constructed with random values randomly placed through the matrix according to the 106 | // matrix size, specified by dimensions r * c (rows * columns), and the specified density 107 | // of non zero values. Density is a value between 0 and 1 (0 >= density >= 1) where a density 108 | // of 1 will construct a matrix entirely composed of non zero values and a density of 0 will 109 | // have only zero values. 110 | func Random(t MatrixType, r int, c int, density float32) mat.Matrix { 111 | d := int(density * float32(r) * float32(c)) 112 | 113 | m := make([]int, d) 114 | n := make([]int, d) 115 | data := make([]float64, d) 116 | 117 | for i := 0; i < d; i++ { 118 | data[i] = rand.Float64() 119 | m[i] = rand.Intn(r) 120 | n[i] = rand.Intn(c) 121 | } 122 | 123 | return NewCOO(r, c, m, n, data).ToType(t) 124 | } 125 | 126 | // alias reports whether x and y share the same base array. 127 | func aliasFloats(x, y []float64) bool { 128 | return cap(x) > 0 && cap(y) > 0 && &x[0:cap(x)][cap(x)-1] == &y[0:cap(y)][cap(y)-1] 129 | } 130 | 131 | // alias reports whether x and y share the same base array. 132 | func aliasInts(x, y []int) bool { 133 | return cap(x) > 0 && cap(y) > 0 && &x[0:cap(x)][cap(x)-1] == &y[0:cap(y)][cap(y)-1] 134 | } 135 | 136 | // useFloats attempts to reuse the specified slice of floats ensuring it has 137 | // sufficient capacity for at least n elements. If slice does not have sufficient 138 | // capacity for n elements, new storage will be allocated. If clear is true, 139 | // all values in the slice will be zeroed. 140 | func useFloats(slice []float64, n int, clear bool) []float64 { 141 | if n <= cap(slice) { 142 | slice = slice[:n] 143 | if clear { 144 | for i := range slice { 145 | slice[i] = 0 146 | } 147 | } 148 | return slice 149 | } 150 | return make([]float64, n) 151 | } 152 | 153 | // useInts attempts to reuse the specified slice of ints ensuring it has 154 | // sufficient capacity for at least n elements. If slice does not have sufficient 155 | // capacity for n elements, new storage will be allocated. If clear is true, 156 | // all values in the slice will be zeroed. 157 | func useInts(slice []int, n int, clear bool) []int { 158 | if n <= cap(slice) { 159 | slice = slice[:n] 160 | if clear { 161 | for i := range slice { 162 | slice[i] = 0 163 | } 164 | } 165 | return slice 166 | } 167 | return make([]int, n) 168 | } 169 | 170 | // Normer is an interface for calculating the Norm of a matrix. 171 | // This allows matrices to implement format specific Norm 172 | // implementations optimised for each format processing only non-zero 173 | // elements for different sparsity patterns across sparse matrix formats. 174 | type Normer interface { 175 | Norm(L float64) float64 176 | } 177 | 178 | // Norm returns the norm of the matrix as a scalar value. This 179 | // implementation is able to take advantage of sparse matrix types 180 | // and only process non-zero values providing the supplied matrix 181 | // implements the Normer interface. If the supplied matrix does 182 | // not implement Normer then the function will invoke mat.Norm() 183 | // to process the matrix. 184 | func Norm(m mat.Matrix, L float64) float64 { 185 | if n, isNormer := m.(Normer); isNormer { 186 | return n.Norm(L) 187 | } 188 | 189 | return mat.Norm(m, L) 190 | } 191 | 192 | // BlasCompatibleSparser is an interface which represents Sparse matrices compatible with 193 | // sparse BLAS routines i.e. implementing the RawMatrix() method as a means of obtaining 194 | // a BLAS sparse matrix representation of the matrix. 195 | type BlasCompatibleSparser interface { 196 | Sparser 197 | RawMatrix() *blas.SparseMatrix 198 | } 199 | 200 | // MulMatVec (y = alpha * a * x + y) performs sparse matrix multiplication with a vector and 201 | // stores the result in a mat.VecDense vector. y is a *mat.VecDense, if c is nil, a new mat.VecDense 202 | // of the correct dimensions (Ac x 1) will be allocated and returned as the result of the function. 203 | // x is an implementation of mat.Vector and a is a sparse matri of type CSR, CSC or a format 204 | // that implements the BlasCompatibleSparser interface. Matrix A will be scaled by alpha. 205 | // If transA is true, the matrix A will be transposed as part of the operation. The function 206 | // will panic Ac != len(x) or if (y != nil and (Ac != len(y))) 207 | func MulMatVec(transA bool, alpha float64, a BlasCompatibleSparser, x mat.Vector, y *mat.VecDense) *mat.VecDense { 208 | // A is m x n (or n x m if transA), x is n, y is m 209 | ar, ac := a.Dims() 210 | if transA { 211 | ar, ac = ac, ar 212 | } 213 | if ac != x.Len() { 214 | panic(mat.ErrShape) 215 | } 216 | if y == nil { 217 | y = mat.NewVecDense(ar, nil) 218 | } else { 219 | if ar != y.Len() { 220 | panic(mat.ErrShape) 221 | } 222 | } 223 | 224 | yraw := y.RawVector() 225 | 226 | var araw *blas.SparseMatrix 227 | if as, ok := a.(*CSC); ok { 228 | // as CSC is the natural transpose of CSR, we will transpose here to CSR 229 | // then transpose back during the multiplication operation 230 | araw = as.T().(*CSR).RawMatrix() 231 | transA = !transA 232 | } else { 233 | araw = a.RawMatrix() 234 | } 235 | 236 | // xd, xIsDense := x.(*mat.VecDense) 237 | xd, xIsDense := x.(mat.RawVectorer) 238 | if !xIsDense { 239 | if xs, xIsSparse := x.(*Vector); xIsSparse { 240 | xd = xs.ToDense() 241 | } else { 242 | xd = mat.VecDenseCopyOf(x) 243 | } 244 | } 245 | xraw := xd.RawVector() 246 | blas.Dusmv(transA, alpha, araw, xraw.Data, xraw.Inc, yraw.Data, yraw.Inc) 247 | return y 248 | 249 | } 250 | 251 | // MulMatMat (c = alpha * a * b + c) performs sparse matrix multiplication with another matrix and 252 | // stores the result in a mat.Dense matrix. c is a *mat.Dense, if c is nil, a new mat.Dense 253 | // of the correct dimensions (Ar x Bc) will be allocated and returned as the result from the 254 | // function. b is an implementation of mat.Matrix and a is a sparse matrix of type CSR, CSC or 255 | // a format that implements the BlasCompatibleSparser interface. Matrix A 256 | // will be scaled by alpha. If transA is true, the matrix A will be transposed as part of the 257 | // operation. The function will panic if Ac != Br or if (C != nil and (ar != Cr or Bc != Cc)) 258 | func MulMatMat(transA bool, alpha float64, a BlasCompatibleSparser, b mat.Matrix, c *mat.Dense) *mat.Dense { 259 | // A is m x n (or n x m if transA), B is n x k, C is m x k 260 | ar, ac := a.Dims() 261 | if transA { 262 | ar, ac = ac, ar 263 | } 264 | br, bc := b.Dims() 265 | 266 | if ac != br { 267 | panic(mat.ErrShape) 268 | } 269 | if c == nil { 270 | c = mat.NewDense(ar, bc, nil) 271 | } else { 272 | cr, cc := c.Dims() 273 | if ar != cr || bc != cc { 274 | panic(mat.ErrShape) 275 | } 276 | } 277 | craw := c.RawMatrix() 278 | 279 | var araw *blas.SparseMatrix 280 | if as, ok := a.(*CSC); ok { 281 | // as CSC is the natural transpose of CSR, we will transpose here to CSR 282 | // then transpose back during the multiplication operation 283 | araw = as.T().(*CSR).RawMatrix() 284 | transA = !transA 285 | } else { 286 | araw = a.RawMatrix() 287 | } 288 | 289 | if bd, bIsDense := b.(mat.RawMatrixer); bIsDense { 290 | braw := bd.RawMatrix() 291 | blas.Dusmm(transA, bc, alpha, araw, braw.Data, braw.Stride, craw.Data, craw.Stride) 292 | return c 293 | } 294 | 295 | if bs, bIsCSC := b.(*CSC); bIsCSC { 296 | col := getFloats(br, true) 297 | for j := 0; j < bc; j++ { 298 | begin, end := bs.matrix.Indptr[j], bs.matrix.Indptr[j+1] 299 | ind := bs.matrix.Ind[begin:end] 300 | blas.Dussc(bs.matrix.Data[begin:end], col, 1, ind) 301 | blas.Dusmv(transA, alpha, araw, col, 1, craw.Data[j:], craw.Stride) 302 | for _, v := range ind { 303 | col[v] = 0 304 | } 305 | } 306 | putFloats(col) 307 | return c 308 | } 309 | 310 | if bs, bIsCSR := b.(*CSR); bIsCSR { 311 | if transA { 312 | for i := 0; i < ac; i++ { 313 | begin, end := bs.matrix.Indptr[i], bs.matrix.Indptr[i+1] 314 | for t := araw.Indptr[i]; t < araw.Indptr[i+1]; t++ { 315 | blas.Dusaxpy(alpha*araw.Data[t], bs.matrix.Data[begin:end], bs.matrix.Ind[begin:end], craw.Data[araw.Ind[t]*craw.Stride:(araw.Ind[t]+1)*craw.Stride], 1) 316 | } 317 | } 318 | } else { 319 | for i := 0; i < ar; i++ { 320 | for t := araw.Indptr[i]; t < araw.Indptr[i+1]; t++ { 321 | begin, end := bs.matrix.Indptr[araw.Ind[t]], bs.matrix.Indptr[araw.Ind[t]+1] 322 | blas.Dusaxpy(alpha*araw.Data[t], bs.matrix.Data[begin:end], bs.matrix.Ind[begin:end], craw.Data[i*craw.Stride:(i+1)*craw.Stride], 1) 323 | } 324 | } 325 | } 326 | return c 327 | } 328 | 329 | col := getFloats(br, false) 330 | for j := 0; j < bc; j++ { 331 | col = mat.Col(col, j, b) 332 | blas.Dusmv(transA, alpha, araw, col, 1, craw.Data[j:], craw.Stride) 333 | } 334 | putFloats(col) 335 | return c 336 | } 337 | -------------------------------------------------------------------------------- /matrix_test.go: -------------------------------------------------------------------------------- 1 | package sparse 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "gonum.org/v1/gonum/mat" 8 | ) 9 | 10 | // A: 11 | // 1, 0, 2, 0, 12 | // 0, 0, 0, 0, 13 | // 0, 3, 4, 5, 14 | var matrixPermutationsForA = []struct { 15 | name string 16 | matrix BlasCompatibleSparser 17 | }{ 18 | { 19 | name: "CSR", 20 | matrix: NewCSR( 21 | 3, 4, 22 | []int{0, 2, 2, 5}, 23 | []int{0, 2, 1, 2, 3}, 24 | []float64{1, 2, 3, 4, 5}), 25 | }, 26 | { 27 | name: "CSC", 28 | matrix: NewCSC( 29 | 3, 4, 30 | []int{0, 1, 2, 4, 5}, 31 | []int{0, 2, 0, 2, 2}, 32 | []float64{1, 3, 2, 4, 5}), 33 | }, 34 | { 35 | name: "COO", 36 | matrix: NewCOO( 37 | 3, 4, 38 | []int{0, 2, 0, 2, 2}, 39 | []int{0, 3, 2, 1, 2}, 40 | []float64{1, 5, 2, 3, 4}), 41 | }, 42 | } 43 | 44 | func TestMulMatVec(t *testing.T) { 45 | permsB := []struct { 46 | name string 47 | vector mat.Vector 48 | }{ 49 | { 50 | name: "Dense", 51 | vector: mat.NewVecDense(4, []float64{1, 2, 0, 3}), 52 | }, 53 | { 54 | name: "Sparse", 55 | vector: NewVector(4, []int{0, 1, 3}, []float64{1, 2, 3}), 56 | }, 57 | { 58 | name: "Transposed Dense", 59 | vector: mat.NewVecDense(4, []float64{1, 2, 0, 3}).TVec(), 60 | }, 61 | } 62 | 63 | tests := []struct { 64 | alpha float64 65 | y *mat.VecDense 66 | er, ec int 67 | eData []float64 68 | }{ 69 | { // y = 0 * A * x 70 | alpha: 0, 71 | y: mat.NewVecDense(3, []float64{0, 0, 0}), 72 | er: 3, ec: 1, 73 | eData: []float64{0, 0, 0}, 74 | }, 75 | { // y = 1 * A * x 76 | alpha: 1, 77 | y: mat.NewVecDense(3, []float64{0, 0, 0}), 78 | er: 3, ec: 1, 79 | eData: []float64{1, 0, 21}, 80 | }, 81 | { // y = 2 * A * x 82 | alpha: 2, 83 | y: mat.NewVecDense(3, []float64{0, 0, 0}), 84 | er: 3, ec: 1, 85 | eData: []float64{2, 0, 42}, 86 | }, 87 | { // y = 1 * A * x + y 88 | alpha: 1, 89 | y: mat.NewVecDense(3, []float64{1, 2, 0}), 90 | er: 3, ec: 1, 91 | eData: []float64{2, 2, 21}, 92 | }, 93 | { // y = 1 * A * x + y 94 | alpha: 1, 95 | y: nil, 96 | er: 3, ec: 1, 97 | eData: []float64{1, 0, 21}, 98 | }, 99 | } 100 | 101 | for ti, test := range tests { 102 | for _, transA := range []bool{false, true} { 103 | for _, b := range permsB { 104 | for _, a := range matrixPermutationsForA { 105 | amat := a.matrix 106 | var transInd string 107 | if transA { 108 | amat = amat.T().(BlasCompatibleSparser) 109 | transInd = "^T" 110 | } 111 | var ycopy *mat.VecDense 112 | if test.y != nil { 113 | ycopy = mat.VecDenseCopyOf(test.y) 114 | } 115 | y := MulMatVec(transA, test.alpha, amat, b.vector, ycopy) 116 | 117 | yr, yc := y.Dims() 118 | if yr != test.er || yc != test.ec { 119 | t.Errorf("Test %d (%s%s x %s): Incorrect dimensions: expected %d x %d but received %d x %d", ti+1, a.name, transInd, b.name, test.er, test.ec, yr, yc) 120 | } 121 | 122 | yraw := y.RawVector() 123 | 124 | for i, v := range test.eData { 125 | if v != yraw.Data[i] { 126 | e := mat.NewDense(test.er, test.ec, test.eData) 127 | t.Errorf("Test %d (%s%s x %s): Failed, expected\n%v\n but received \n%v", ti+1, a.name, transInd, b.name, mat.Formatted(e), mat.Formatted(y)) 128 | break 129 | } 130 | } 131 | } 132 | } 133 | } 134 | } 135 | } 136 | 137 | func TestMulMatMat(t *testing.T) { 138 | permsB := []struct { 139 | name string 140 | matrix mat.Matrix 141 | }{ 142 | { 143 | name: "Dense", 144 | matrix: mat.NewDense(4, 5, []float64{ 145 | 1, 2, 3, 4, 5, 146 | 6, 7, 8, 9, 1, 147 | 2, 3, 4, 5, 6, 148 | 7, 8, 9, 0, 1, 149 | }), 150 | }, 151 | { 152 | name: "COO", 153 | matrix: NewCOO(4, 5, 154 | []int{ 155 | 0, 0, 0, 0, 0, 156 | 1, 1, 1, 1, 1, 157 | 2, 2, 2, 2, 2, 158 | 3, 3, 3, 3, 159 | }, 160 | []int{ 161 | 0, 1, 2, 3, 4, 162 | 0, 1, 2, 3, 4, 163 | 0, 1, 2, 3, 4, 164 | 0, 1, 2, 4, 165 | }, 166 | []float64{ 167 | 1, 2, 3, 4, 5, 168 | 6, 7, 8, 9, 1, 169 | 2, 3, 4, 5, 6, 170 | 7, 8, 9, 1, 171 | }), 172 | }, 173 | { 174 | name: "CSR", 175 | matrix: NewCSR(4, 5, 176 | []int{0, 5, 10, 15, 19}, 177 | []int{ 178 | 0, 1, 2, 3, 4, 179 | 0, 1, 2, 3, 4, 180 | 0, 1, 2, 3, 4, 181 | 0, 1, 2, 4, 182 | }, 183 | []float64{ 184 | 1, 2, 3, 4, 5, 185 | 6, 7, 8, 9, 1, 186 | 2, 3, 4, 5, 6, 187 | 7, 8, 9, 1, 188 | }), 189 | }, 190 | { 191 | name: "CSC", 192 | matrix: NewCSC( 193 | 4, 5, 194 | []int{0, 4, 8, 12, 15, 19}, 195 | []int{ 196 | 0, 1, 2, 3, 197 | 0, 1, 2, 3, 198 | 0, 1, 2, 3, 199 | 0, 1, 2, 200 | 0, 1, 2, 3, 201 | }, 202 | []float64{ 203 | 1, 6, 2, 7, 204 | 2, 7, 3, 8, 205 | 3, 8, 4, 9, 206 | 4, 9, 5, 207 | 5, 1, 6, 1, 208 | }, 209 | ), 210 | }, 211 | } 212 | 213 | tests := []struct { 214 | alpha float64 215 | c *mat.Dense 216 | er, ec int 217 | eData []float64 218 | }{ 219 | { // C = 0 * A * B 220 | alpha: 0, 221 | c: mat.NewDense(3, 5, []float64{ 222 | 0, 0, 0, 0, 0, 223 | 0, 0, 0, 0, 0, 224 | 0, 0, 0, 0, 0, 225 | }), 226 | er: 3, ec: 5, 227 | eData: []float64{ 228 | 0, 0, 0, 0, 0, 229 | 0, 0, 0, 0, 0, 230 | 0, 0, 0, 0, 0, 231 | }, 232 | }, 233 | { // C = 1 * A * B 234 | alpha: 1, 235 | c: mat.NewDense(3, 5, []float64{ 236 | 0, 0, 0, 0, 0, 237 | 0, 0, 0, 0, 0, 238 | 0, 0, 0, 0, 0, 239 | }), 240 | er: 3, ec: 5, 241 | eData: []float64{ 242 | 5, 8, 11, 14, 17, 243 | 0, 0, 0, 0, 0, 244 | 61, 73, 85, 47, 32, 245 | }, 246 | }, 247 | { // C = 2 * A * B 248 | alpha: 2, 249 | c: mat.NewDense(3, 5, []float64{ 250 | 0, 0, 0, 0, 0, 251 | 0, 0, 0, 0, 0, 252 | 0, 0, 0, 0, 0, 253 | }), 254 | er: 3, ec: 5, 255 | eData: []float64{ 256 | 10, 16, 22, 28, 34, 257 | 0, 0, 0, 0, 0, 258 | 122, 146, 170, 94, 64, 259 | }, 260 | }, 261 | { // C = 1 * A * B + C 262 | alpha: 1, 263 | c: mat.NewDense(3, 5, []float64{ 264 | 1, 0, 5, 0, 8, 265 | 2, 0, 3, 0, 4, 266 | 0, 0, 0, 7, 0, 267 | }), 268 | er: 3, ec: 5, 269 | eData: []float64{ 270 | 6, 8, 16, 14, 25, 271 | 2, 0, 3, 0, 4, 272 | 61, 73, 85, 54, 32, 273 | }, 274 | }, 275 | { // C = 1 * A * B + C 276 | alpha: 1, 277 | c: nil, 278 | er: 3, ec: 5, 279 | eData: []float64{ 280 | 5, 8, 11, 14, 17, 281 | 0, 0, 0, 0, 0, 282 | 61, 73, 85, 47, 32, 283 | }, 284 | }, 285 | } 286 | 287 | for ti, test := range tests { 288 | for _, transA := range []bool{false, true} { 289 | for _, b := range permsB { 290 | for _, a := range matrixPermutationsForA { 291 | amat := a.matrix 292 | var transInd string 293 | if transA { 294 | amat = amat.T().(BlasCompatibleSparser) 295 | transInd = "^T" 296 | } 297 | var ccopy *mat.Dense 298 | if test.c != nil { 299 | ccopy = mat.DenseCopyOf(test.c) 300 | } 301 | c := MulMatMat(transA, test.alpha, amat, b.matrix, ccopy) 302 | 303 | cr, cc := c.Dims() 304 | if cr != test.er || cc != test.ec { 305 | t.Errorf("Test %d (%s%s x %s): Incorrect dimensions: expected %d x %d but received %d x %d", ti+1, a.name, transInd, b.name, test.er, test.ec, cr, cc) 306 | } 307 | 308 | craw := c.RawMatrix() 309 | for i, v := range test.eData { 310 | if v != craw.Data[i] { 311 | e := mat.NewDense(test.er, test.ec, test.eData) 312 | t.Errorf("Test %d (%s%s x %s): Failed, expected\n%v\n but received \n%v", ti+1, a.name, transInd, b.name, mat.Formatted(e), mat.Formatted(c)) 313 | break 314 | } 315 | } 316 | } 317 | } 318 | } 319 | } 320 | } 321 | 322 | func TestConvert(t *testing.T) { 323 | dok := NewDOK(4, 5) 324 | for i := 0; i < 4; i++ { 325 | for j := 0; j < 5; j++ { 326 | dok.Set(i, j, float64(i+j*4)) 327 | } 328 | } 329 | 330 | mats := []mat.Matrix{ 331 | dok, 332 | dok.ToCOO(), 333 | dok.ToCSR(), 334 | dok.ToCSC(), 335 | new(DOKType).Convert(dok), 336 | new(COOType).Convert(dok), 337 | new(CSRType).Convert(dok), 338 | new(CSCType).Convert(dok), 339 | } 340 | size := len(mats) 341 | 342 | for i := 0; i < size; i++ { 343 | for j := 0; j < size; j++ { 344 | from := mats[i] 345 | to := mats[j] 346 | t.Run(fmt.Sprintf("%T->%T", from, to), func(t *testing.T) { 347 | // compare 348 | if !(mat.Equal(from, to)) { 349 | t.Fatalf("Not same (%d,%d) : %T != %T", i, j, from, to) 350 | } 351 | }) 352 | } 353 | } 354 | 355 | } 356 | -------------------------------------------------------------------------------- /pool.go: -------------------------------------------------------------------------------- 1 | package sparse 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/james-bowman/sparse/blas" 7 | ) 8 | 9 | const ( 10 | pooledFloatSize = 200 11 | pooledIntSize = 200 12 | ) 13 | 14 | var ( 15 | pool = sync.Pool{ 16 | New: func() interface{} { 17 | return &CSR{ 18 | matrix: blas.SparseMatrix{}, 19 | } 20 | }, 21 | } 22 | vecPool = sync.Pool{ 23 | New: func() interface{} { 24 | return &Vector{} 25 | }, 26 | } 27 | floatPool = sync.Pool{ 28 | New: func() interface{} { 29 | return make([]float64, pooledFloatSize) 30 | }, 31 | } 32 | intPool = sync.Pool{ 33 | New: func() interface{} { 34 | return make([]int, pooledIntSize) 35 | }, 36 | } 37 | ) 38 | 39 | func getWorkspace(r, c, nnz int, clear bool) *CSR { 40 | w := pool.Get().(*CSR) 41 | w.matrix.Indptr = useInts(w.matrix.Indptr, r+1, false) 42 | w.matrix.Ind = useInts(w.matrix.Ind, nnz, false) 43 | w.matrix.Data = useFloats(w.matrix.Data, nnz, false) 44 | if clear { 45 | for i := range w.matrix.Indptr { 46 | w.matrix.Indptr[i] = 0 47 | } 48 | w.matrix.Ind = w.matrix.Ind[:0] 49 | w.matrix.Data = w.matrix.Data[:0] 50 | } 51 | w.matrix.I = r 52 | w.matrix.J = c 53 | return w 54 | } 55 | 56 | func putWorkspace(w *CSR) { 57 | pool.Put(w) 58 | } 59 | 60 | func getVecWorkspace(len, nnz int, clear bool) *Vector { 61 | w := vecPool.Get().(*Vector) 62 | w.ind = useInts(w.ind, nnz, false) 63 | w.data = useFloats(w.data, nnz, false) 64 | if clear { 65 | w.ind = w.ind[:0] 66 | w.data = w.data[:0] 67 | } 68 | w.len = len 69 | return w 70 | } 71 | 72 | func putVecWorkspace(w *Vector) { 73 | vecPool.Put(w) 74 | } 75 | 76 | // getFloats returns a []float64 of length l. If clear is true, 77 | // the slice visible is zeroed. 78 | func getFloats(l int, clear bool) []float64 { 79 | w := floatPool.Get().([]float64) 80 | return useFloats(w, l, clear) 81 | } 82 | 83 | // putFloats replaces a used []float64 into the appropriate size 84 | // workspace pool. putFloats must not be called with a slice 85 | // where references to the underlying data have been kept. 86 | func putFloats(w []float64) { 87 | if cap(w) > pooledFloatSize { 88 | floatPool.Put(w) 89 | } 90 | } 91 | 92 | // getInts returns a []ints of length l. If clear is true, 93 | // the slice visible is zeroed. 94 | func getInts(l int, clear bool) []int { 95 | w := intPool.Get().([]int) 96 | return useInts(w, l, clear) 97 | } 98 | 99 | // putInts replaces a used []int into the pool. 100 | func putInts(w []int) { 101 | if cap(w) > pooledIntSize { 102 | intPool.Put(w) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /sparse_test.go: -------------------------------------------------------------------------------- 1 | package sparse 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "log" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | func TestTodo(t *testing.T) { 14 | // Show all to do`s in comment code 15 | source, err := filepath.Glob(fmt.Sprintf("./%s", "*.go")) 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | 20 | var amount int 21 | 22 | for i := range source { 23 | t.Run(source[i], func(t *testing.T) { 24 | file, err := os.Open(source[i]) 25 | if err != nil { 26 | t.Fatal(err) 27 | } 28 | defer file.Close() 29 | 30 | pos := 0 31 | scanner := bufio.NewScanner(file) 32 | for scanner.Scan() { 33 | line := scanner.Text() 34 | pos++ 35 | index := strings.Index(line, "//") 36 | if index < 0 { 37 | continue 38 | } 39 | if !strings.Contains(line, "TODO") { 40 | continue 41 | } 42 | t.Logf("%13s:%-4d %s", source[i], pos, line[index:]) 43 | amount++ 44 | } 45 | 46 | if err := scanner.Err(); err != nil { 47 | log.Fatal(err) 48 | } 49 | }) 50 | } 51 | if amount > 0 { 52 | t.Logf("Amount TODO: %d", amount) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /vector.go: -------------------------------------------------------------------------------- 1 | package sparse 2 | 3 | import ( 4 | "math" 5 | "sort" 6 | 7 | "github.com/james-bowman/sparse/blas" 8 | "gonum.org/v1/gonum/floats" 9 | "gonum.org/v1/gonum/mat" 10 | ) 11 | 12 | var ( 13 | _ Sparser = (*Vector)(nil) 14 | _ mat.Matrix = (*Vector)(nil) 15 | _ mat.Vector = (*Vector)(nil) 16 | _ mat.Reseter = (*Vector)(nil) 17 | _ mat.Mutable = (*Vector)(nil) 18 | ) 19 | 20 | // Vector is a sparse vector format. It implements the mat.Vector 21 | // interface but is optimised for sparsely populated vectors where 22 | // most of the elements contain zero values by only storing and 23 | // processing the non-zero values. The format is similar to the 24 | // triplet format used by COO matrices (and CSR/CSC) but only uses 25 | // 2 arrays because the vector is 1 dimensional rather than 2. 26 | type Vector struct { 27 | len int 28 | ind []int 29 | data []float64 30 | } 31 | 32 | // NewVector returns a new sparse vector of length len with 33 | // elements specified by ind[] containing the values conatined 34 | // within data. Vector will reuse the same storage as the slices 35 | // passed in and so any changes to the vector will be reflected 36 | // in the slices and vice versa. 37 | func NewVector(len int, ind []int, data []float64) *Vector { 38 | return &Vector{ 39 | len: len, 40 | ind: ind, 41 | data: data, 42 | } 43 | } 44 | 45 | // Dims returns the dimensions of the vector. This will be 46 | // equivalent to Len(), 1 47 | func (v *Vector) Dims() (r, c int) { 48 | return v.len, 1 49 | } 50 | 51 | // At returns the element at r, c. At will panic if c != 0. 52 | func (v *Vector) At(r, c int) float64 { 53 | if c != 0 { 54 | panic(mat.ErrColAccess) 55 | } 56 | return v.AtVec(r) 57 | } 58 | 59 | // Set sets the element at row r, column c to the value val. Set will panic if c != 0. 60 | func (v *Vector) Set(r, c int, val float64) { 61 | if c != 0 { 62 | panic(mat.ErrColAccess) 63 | } 64 | v.SetVec(r, val) 65 | } 66 | 67 | // T returns the transpose of the receiver. 68 | func (v *Vector) T() mat.Matrix { 69 | return mat.TransposeVec{Vector: v} 70 | } 71 | 72 | // NNZ returns the number of non-zero elements in the vector. 73 | func (v *Vector) NNZ() int { 74 | return len(v.data) 75 | } 76 | 77 | // AtVec returns the i'th element of the Vector. 78 | func (v *Vector) AtVec(i int) float64 { 79 | if i < 0 || i >= v.len { 80 | panic(mat.ErrRowAccess) 81 | } 82 | for j := 0; j < len(v.ind); j++ { 83 | if v.ind[j] == i { 84 | return v.data[j] 85 | } 86 | } 87 | return 0.0 88 | } 89 | 90 | // SetVec sets the i'th element to the value val. It panics if i is out of bounds. 91 | func (v *Vector) SetVec(i int, val float64) { 92 | 93 | // Panic if the sought index is out of bounds 94 | if i < 0 || i >= v.len { 95 | panic(mat.ErrRowAccess) 96 | } 97 | 98 | // Identify where in the slice this index would exist 99 | j := sort.SearchInts(v.ind, i) 100 | 101 | // The value is zero so we are really removing it 102 | if val == 0.0 { 103 | if j < len(v.ind) && v.ind[j] == i { 104 | v.ind = append(v.ind[:j], v.ind[j+1:]...) 105 | v.data = append(v.data[:j], v.data[j+1:]...) 106 | } 107 | return 108 | } 109 | 110 | // Set the value 111 | if j == len(v.ind) { 112 | v.ind = append(v.ind, i) 113 | v.data = append(v.data, val) 114 | } else if j < len(v.ind) { 115 | if v.ind[j] == i { 116 | v.data[j] = val 117 | } else { 118 | v.ind = append(v.ind[:j], append([]int{i}, v.ind[j:]...)...) 119 | v.data = append(v.data[:j], append([]float64{val}, v.data[j:]...)...) 120 | } 121 | } 122 | } 123 | 124 | // Len returns the length of the vector 125 | func (v *Vector) Len() int { 126 | return v.len 127 | } 128 | 129 | // DoNonZero calls the function fn for each of the non-zero elements of the receiver. 130 | // The function fn takes a row/column index and the element value of the receiver at 131 | // (i, j). 132 | func (v *Vector) DoNonZero(fn func(i int, j int, v float64)) { 133 | for i := 0; i < len(v.ind); i++ { 134 | fn(v.ind[i], 0, v.data[i]) 135 | } 136 | } 137 | 138 | // RawVector returns the underlying sparse vector data and indices 139 | // respectively for raw manipulation or use in sparse BLAS routines. 140 | func (v *Vector) RawVector() ([]float64, []int) { 141 | return v.data, v.ind 142 | } 143 | 144 | // Gather gathers the entries from the supplied mat.VecDense structure 145 | // that have corresponding non-zero entries in the receiver into the 146 | // receiver. The method will panic if denseVector is not the same 147 | // length as the receiver. 148 | func (v *Vector) Gather(denseVector *mat.VecDense) { 149 | if v.len != denseVector.Len() { 150 | panic(mat.ErrShape) 151 | } 152 | vec := denseVector.RawVector() 153 | blas.Dusga(vec.Data, vec.Inc, v.data, v.ind) 154 | } 155 | 156 | // GatherAndZero gathers the entries from the supplied mat.VecDense 157 | // structure that have corresponding non-zero entries in the receiver 158 | // into the receiver and then zeros those entries in denseVector. 159 | // The method will panic if denseVector is not the same length 160 | // as the receiver. 161 | func (v *Vector) GatherAndZero(denseVector *mat.VecDense) { 162 | if v.len != denseVector.Len() { 163 | panic(mat.ErrShape) 164 | } 165 | vec := denseVector.RawVector() 166 | blas.Dusgz(vec.Data, vec.Inc, v.data, v.ind) 167 | } 168 | 169 | // Scatter scatters elements from the receiver into the supplied mat.VecDense 170 | // structure, denseVector and returns a pointer to it. The method will panic 171 | // if denseVector is not the same length as the receiver (unless it is nil) 172 | func (v *Vector) Scatter(denseVector *mat.VecDense) *mat.VecDense { 173 | if v.len != denseVector.Len() { 174 | panic(mat.ErrShape) 175 | } 176 | vec := denseVector.RawVector() 177 | blas.Dussc(v.data, vec.Data, vec.Inc, v.ind) 178 | return denseVector 179 | } 180 | 181 | // CloneVec clones the supplied mat.Vector, a into the receiver, overwriting 182 | // the previous values of the receiver. If the receiver is of a different 183 | // length from a, it will be resized to accommodate the values from a. 184 | func (v *Vector) CloneVec(a mat.Vector) { 185 | if v == a { 186 | return 187 | } 188 | 189 | v.len = a.Len() 190 | 191 | if s, isSparse := a.(*Vector); isSparse { 192 | v.reuseAs(s.Len(), s.NNZ(), false) 193 | copy(v.ind, s.ind) 194 | copy(v.data, s.data) 195 | return 196 | } 197 | 198 | v.reuseAs(v.len, v.len/10, false) 199 | 200 | for i := 0; i < v.len; i++ { 201 | val := a.AtVec(i) 202 | if val != 0 { 203 | v.ind = append(v.ind, i) 204 | v.data = append(v.data, val) 205 | } 206 | } 207 | } 208 | 209 | // ToDense converts the sparse vector to a dense vector 210 | // The returned dense matrix is a new copy of the receiver. 211 | func (v *Vector) ToDense() *mat.VecDense { 212 | return v.Scatter(mat.NewVecDense(v.len, nil)) 213 | } 214 | 215 | // AddVec adds the vectors a and b, placing the result in the receiver. 216 | // AddVec will panic if a and b are not the same length. If a and b 217 | // are both sparse Vector vectors then AddVec will only process the 218 | // non-zero elements. 219 | func (v *Vector) AddVec(a, b mat.Vector) { 220 | ar := a.Len() 221 | br := b.Len() 222 | 223 | if ar != br { 224 | panic(mat.ErrShape) 225 | } 226 | 227 | if t, temp, restore := v.spalloc(a, b); temp { 228 | defer restore() 229 | v = t 230 | } 231 | 232 | // Sparse specific optimised implementation 233 | sa, aIsSparse := a.(*Vector) 234 | sb, bIsSparse := b.(*Vector) 235 | if aIsSparse && bIsSparse { 236 | v.addVecSparse(1, sa, 1, sb) 237 | return 238 | } 239 | 240 | for i := 0; i < v.len; i++ { 241 | p := a.AtVec(i) + b.AtVec(i) 242 | if p != 0 { 243 | v.ind = append(v.ind, i) 244 | v.data = append(v.data, p) 245 | } 246 | } 247 | } 248 | 249 | // addVecSparse2 adds the vectors a and alpha*b. This method is 250 | // optimised for processing sparse Vector vectors and only processes 251 | // non-zero elements. 252 | func (v *Vector) addVecSparse(alpha float64, a *Vector, beta float64, b *Vector) { 253 | spa := NewSPA(a.len) 254 | spa.Scatter(a.data, a.ind, alpha, &v.ind) 255 | spa.Scatter(b.data, b.ind, beta, &v.ind) 256 | spa.Gather(&v.data, &v.ind) 257 | } 258 | 259 | // ScaleVec scales the vector a by alpha, placing the result in the 260 | // receiver. 261 | func (v *Vector) ScaleVec(alpha float64, a mat.Vector) { 262 | alen := a.Len() 263 | if !v.IsZero() && alen != v.len { 264 | panic(mat.ErrShape) 265 | } 266 | 267 | if alpha == 0 { 268 | v.len = alen 269 | v.ind = v.ind[:0] 270 | v.data = v.data[:0] 271 | return 272 | } 273 | 274 | if s, isSparse := a.(*Vector); isSparse { 275 | nnz := s.NNZ() 276 | v.reuseAs(alen, nnz, false) 277 | copy(v.ind, s.ind) 278 | for i, val := range s.data { 279 | v.data[i] = alpha * val 280 | } 281 | return 282 | } 283 | 284 | v.reuseAs(alen, alen/10, true) 285 | 286 | for i := 0; i < v.len; i++ { 287 | val := a.AtVec(i) 288 | if val != 0 { 289 | v.ind = append(v.ind, i) 290 | v.data = append(v.data, alpha*val) 291 | } 292 | } 293 | } 294 | 295 | // AddScaledVec adds the vectors a and alpha*b, placing the result 296 | // in the receiver. AddScaledVec will panic if a and b are not the 297 | // same length. 298 | func (v *Vector) AddScaledVec(a mat.Vector, alpha float64, b mat.Vector) { 299 | ar := a.Len() 300 | br := b.Len() 301 | 302 | if ar != br { 303 | panic(mat.ErrShape) 304 | } 305 | 306 | if t, temp, restore := v.spalloc(a, b); temp { 307 | defer restore() 308 | v = t 309 | } 310 | 311 | // Sparse specific optimised implementation 312 | sa, aIsSparse := a.(*Vector) 313 | sb, bIsSparse := b.(*Vector) 314 | if aIsSparse && bIsSparse { 315 | v.addVecSparse(1, sa, alpha, sb) 316 | return 317 | } 318 | 319 | for i := 0; i < v.len; i++ { 320 | val := a.AtVec(i) + alpha*b.AtVec(i) 321 | if val != 0 { 322 | v.ind = append(v.ind, i) 323 | v.data = append(v.data, val) 324 | } 325 | } 326 | } 327 | 328 | // Norm calculates the Norm of the vector only processing the 329 | // non-zero elements. 330 | // See Normer interface for more details. 331 | func (v *Vector) Norm(L float64) float64 { 332 | if L == 2 { 333 | return math.Sqrt(floats.Dot(v.data, v.data)) 334 | } 335 | return floats.Norm(v.data, L) 336 | } 337 | 338 | // Dot returns the sum of the element-wise product (dot product) of a and b. 339 | // Dot panics if the matrix sizes are unequal. For sparse vectors, Dot will 340 | // only process non-zero elements otherwise this method simply delegates to 341 | // mat.Dot() 342 | func Dot(a, b mat.Vector) float64 { 343 | if a.Len() != b.Len() { 344 | panic(mat.ErrShape) 345 | } 346 | 347 | as, aIsSparse := a.(*Vector) 348 | bs, bIsSparse := b.(*Vector) 349 | 350 | if aIsSparse { 351 | if bIsSparse { 352 | return dotSparseSparse(as, bs, nil) 353 | } 354 | if bdense, bIsDense := b.(mat.RawVectorer); bIsDense { 355 | raw := bdense.RawVector() 356 | return blas.Dusdot(as.data, as.ind, raw.Data, raw.Inc) 357 | } 358 | return dotSparse(as, b, nil) 359 | } 360 | if bIsSparse { 361 | if adense, aIsDense := a.(mat.RawVectorer); aIsDense { 362 | raw := adense.RawVector() 363 | return blas.Dusdot(bs.data, bs.ind, raw.Data, raw.Inc) 364 | } 365 | return dotSparse(bs, a, nil) 366 | } 367 | return mat.Dot(a, b) 368 | } 369 | 370 | // dotSparse returns the sum of the element-wise multiplication 371 | // of a and b where a is sparse and b is any implementation of 372 | // mat.Vector. 373 | func dotSparse(a *Vector, b mat.Vector, c *Vector) float64 { 374 | var result float64 375 | for i, ind := range a.ind { 376 | val := a.data[i] * b.AtVec(ind) 377 | result += val 378 | if c != nil { 379 | c.SetVec(ind, val) 380 | } 381 | } 382 | return result 383 | } 384 | 385 | // Reset zeros the dimensions of the vector so that it can be reused as the 386 | // receiver of a dimensionally restricted operation. 387 | // 388 | // See the Gonum mat.Reseter interface for more information. 389 | func (v *Vector) Reset() { 390 | v.len = 0 391 | v.ind = v.ind[:0] 392 | v.data = v.data[:0] 393 | } 394 | 395 | // IsZero returns whether the receiver is zero-sized. Zero-sized vectors can be the 396 | // receiver for size-restricted operations. Vectors can be zeroed using the Reset 397 | // method. 398 | func (v *Vector) IsZero() bool { 399 | return v.len == 0 400 | } 401 | 402 | // reuseAs resizes a zero-sized vector to be len long or checks a non-zero-sized vector 403 | // is already the correct size (len). If the vector is resized, the method will 404 | // ensure there is sufficient initial capacity allocated in the underlying storage 405 | // to store up to nnz non-zero elements although this will be extended 406 | // automatically later as needed (using Go's built-in append function). 407 | func (v *Vector) reuseAs(len, nnz int, zero bool) { 408 | if v.IsZero() { 409 | v.len = len 410 | } else if len != v.len { 411 | panic(mat.ErrShape) 412 | } 413 | 414 | v.ind = useInts(v.ind, nnz, false) 415 | v.data = useFloats(v.data, nnz, false) 416 | 417 | if zero { 418 | v.ind = v.ind[:0] 419 | v.data = v.data[:0] 420 | } 421 | } 422 | 423 | // checkOverlap checks whether the receiver overlaps or is an alias for the 424 | // matrix a. The method returns true (indicating overlap) if c == a or if 425 | // any of the receiver's internal data structures share underlying storage with a. 426 | func (v *Vector) checkOverlap(a mat.Matrix) bool { 427 | if v == a { 428 | return true 429 | } 430 | 431 | switch a := a.(type) { 432 | case *Vector: 433 | return aliasInts(v.ind, a.ind) || 434 | aliasFloats(v.data, a.data) 435 | case *COO: 436 | return aliasInts(v.ind, a.cols) || 437 | aliasInts(v.ind, a.rows) || 438 | aliasFloats(v.data, a.data) 439 | case *CSR, *CSC: 440 | m := a.(BlasCompatibleSparser).RawMatrix() 441 | return aliasInts(v.ind, m.Ind) || 442 | aliasFloats(v.data, m.Data) 443 | default: 444 | return false 445 | } 446 | } 447 | 448 | // temporaryWorkspace returns a new Vector w of length len with 449 | // initial capacity allocated for nnz non-zero elements and 450 | // returns a callback to defer which performs cleanup at the return of the call. 451 | // This should be used when a method receiver is the same pointer as an input argument. 452 | func (v *Vector) temporaryWorkspace(len, nnz int, zero bool) (w *Vector, restore func()) { 453 | w = getVecWorkspace(len, nnz, zero) 454 | 455 | return w, func() { 456 | v.CloneVec(w) 457 | putVecWorkspace(w) 458 | } 459 | } 460 | 461 | // spalloc ensures appropriate storage is allocated for the receiver sparse vector 462 | // ensuring it is of length len and checking for any overlap or aliasing 463 | // between operands a or b with c in which case a temporary isolated workspace is 464 | // allocated and the returned value isTemp is true with restore representing a 465 | // function to clean up and restore the workspace once finished. 466 | func (v *Vector) spalloc(a mat.Vector, b mat.Vector) (t *Vector, isTemp bool, restore func()) { 467 | var nnz int 468 | t = v 469 | lSp, lIsSp := a.(Sparser) 470 | rSp, rIsSp := b.(Sparser) 471 | if lIsSp && rIsSp { 472 | nnz = lSp.NNZ() + rSp.NNZ() 473 | } else { 474 | // assume 10% of elements will be non-zero 475 | nnz = a.Len() / 10 476 | } 477 | 478 | if v.checkOverlap(a) || v.checkOverlap(b) { 479 | if !v.IsZero() && a.Len() != v.len { 480 | panic(mat.ErrShape) 481 | } 482 | t, restore = v.temporaryWorkspace(a.Len(), nnz, true) 483 | isTemp = true 484 | } else { 485 | v.reuseAs(a.Len(), nnz, true) 486 | } 487 | 488 | return 489 | } 490 | 491 | // MulMatSparseVec (c = alpha * a * v + c) multiplies a dense matrix by a sparse 492 | // vector and stores the result in mat.VecDense. c is a *mat.VecDense, if c is nil, 493 | // a new mat.VecDense of the correct size will be allocated and returned as the 494 | // result from the function. a*v will be scaled by alpha. The function will 495 | // panic if ac != |v| or if (C != nil and |c| != ar). 496 | // Note this is not a Sparse BLAS routine -- that library does not cover this 497 | // case. This is a lookalike function in the Sparse BLAS style. As a and c are 498 | // dense there is limited benefit to including alpha and c; this is done for 499 | // consistency rather than performance. 500 | func MulMatSparseVec(alpha float64, a mat.Matrix, v *Vector, c *mat.VecDense) *mat.VecDense { 501 | rows, cols := a.Dims() 502 | if cols != v.Len() { 503 | panic(mat.ErrShape) 504 | } 505 | if c == nil { 506 | c = mat.NewVecDense(rows, nil) 507 | } else { 508 | if c.Len() != rows { 509 | panic(mat.ErrShape) 510 | } 511 | } 512 | res := mat.NewVecDense(rows, nil) 513 | // if a has RowView() we use that and sparse.Dot 514 | if rv, aIsRowViewer := a.(mat.RowViewer); aIsRowViewer { 515 | for row := 0; row < rows; row++ { 516 | thisRow := rv.RowView(row) 517 | res.SetVec(row, Dot(thisRow, v)) 518 | } 519 | } else { 520 | // otherwise can only rely on At() 521 | for row := 0; row < rows; row++ { 522 | thisVal := 0.0 523 | for i, col := range v.ind { 524 | thisVal += a.At(row, col) * v.data[i] 525 | } 526 | res.SetVec(row, thisVal) 527 | } 528 | } 529 | c.AddScaledVec(c, alpha, res) 530 | return c 531 | } 532 | 533 | type indexPair struct { 534 | index int 535 | value float64 536 | } 537 | 538 | // Sort the entries in a vector. 539 | func (v *Vector) Sort() { 540 | if v.IsSorted() { 541 | return 542 | } 543 | pairs := make([]indexPair, len(v.ind)) 544 | for i, idx := range v.ind { 545 | pairs[i].index = idx 546 | pairs[i].value = v.data[i] 547 | } 548 | sort.Slice(pairs, func(i, j int) bool { 549 | return pairs[i].index < pairs[j].index 550 | }) 551 | for i, p := range pairs { 552 | v.ind[i] = p.index 553 | v.data[i] = p.value 554 | } 555 | } 556 | 557 | // IsSorted checks if the vector is stored in sorted order 558 | func (v *Vector) IsSorted() bool { 559 | return sort.IntsAreSorted(v.ind) 560 | } 561 | 562 | // dotSparseSparse computes the dot product of two sparse vectors. 563 | // This will be called by Dot if both entered vectors are Sparse. 564 | func dotSparseSparse(a, b, c *Vector) float64 { 565 | a.Sort() 566 | b.Sort() 567 | return dotSparseSparseNoSort(a, b, c) 568 | } 569 | 570 | func dotSparseSparseNoSort(a, b, c *Vector) float64 { 571 | n := a.Len() 572 | return dotSparseSparseNoSortBefore(a, b, c, n) 573 | } 574 | 575 | func dotSparseSparseNoSortBefore(a, b, c *Vector, n int) float64 { 576 | v, _, _ := dotSparseSparseNoSortBeforeWithStart(a, b, c, n, 0, 0) 577 | return v 578 | } 579 | 580 | func dotSparseSparseNoSortBeforeWithStart(a, b, c *Vector, n, aStart, bStart int) (float64, int, int) { 581 | tot := 0.0 582 | aPos := aStart 583 | bPos := bStart 584 | aIndex := -1 585 | bIndex := -1 586 | for aPos < len(a.ind) && bPos < len(b.ind) && aIndex < n && bIndex < n { 587 | aIndex = a.ind[aPos] 588 | bIndex = b.ind[bPos] 589 | if aIndex == bIndex { 590 | val := a.data[aPos] * b.data[bPos] 591 | tot += val 592 | if c != nil { 593 | c.SetVec(aIndex, val) 594 | } 595 | aPos++ 596 | bPos++ 597 | } else { 598 | if aIndex < bIndex { 599 | aPos++ 600 | } else { 601 | bPos++ 602 | } 603 | } 604 | } 605 | return tot, aPos, bPos 606 | } 607 | 608 | // MulElemVec does element-by-element multiplication of a and b 609 | // and puts the result in the receiver. 610 | func (v *Vector) MulElemVec(a, b mat.Vector) { 611 | ar := a.Len() 612 | br := b.Len() 613 | if ar != br { 614 | panic(mat.ErrShape) 615 | } 616 | 617 | as, aIsSparse := a.(*Vector) 618 | bs, bIsSparse := b.(*Vector) 619 | 620 | if aIsSparse { 621 | aNNZ := as.NNZ() 622 | if bIsSparse { 623 | bNNZ := bs.NNZ() 624 | minNNZ := aNNZ 625 | if bNNZ < minNNZ { 626 | minNNZ = bNNZ 627 | } 628 | if v != nil { 629 | v.reuseAs(ar, minNNZ, true) 630 | } 631 | dotSparseSparse(as, bs, v) 632 | } else { 633 | if v != nil { 634 | v.reuseAs(ar, aNNZ, true) 635 | } 636 | dotSparse(as, b, v) 637 | } 638 | } else if bIsSparse { 639 | bNNZ := bs.NNZ() 640 | if v != nil { 641 | v.reuseAs(ar, bNNZ, true) 642 | } 643 | dotSparse(bs, a, v) 644 | } else { 645 | v.reuseAs(ar, ar, true) 646 | for i := 0; i < ar; i++ { 647 | v.SetVec(i, a.AtVec(i)*b.AtVec(i)) 648 | } 649 | } 650 | } 651 | --------------------------------------------------------------------------------