├── .github ├── dependabot.yml └── workflows │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── lib ├── matrix.rb └── matrix │ ├── eigenvalue_decomposition.rb │ ├── lup_decomposition.rb │ └── version.rb ├── matrix.gemspec └── test ├── lib └── helper.rb └── matrix ├── test_matrix.rb └── test_vector.rb /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | ruby-versions: 7 | uses: ruby/actions/.github/workflows/ruby_versions.yml@master 8 | with: 9 | engine: cruby 10 | min_version: 2.6 11 | 12 | test: 13 | needs: ruby-versions 14 | name: build (${{ matrix.ruby }} / ${{ matrix.os }}) 15 | strategy: 16 | matrix: 17 | ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }} 18 | os: [ ubuntu-latest, macos-latest ] 19 | runs-on: ${{ matrix.os }} 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Set up Ruby 23 | uses: ruby/setup-ruby@v1 24 | with: 25 | ruby-version: ${{ matrix.ruby }} 26 | bundler-cache: true 27 | - name: Run test 28 | run: bundle exec rake test 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | List of new feature changes (excluding most bug fixes and optimizations) 4 | 5 | ## v0.4.0 6 | 7 | * Add `Matrix#rotate_entries` [#19] 8 | 9 | ## v0.3.1 / Ruby 3.0 10 | 11 | * Frozen `Matrix` are Ractor-shareable. 12 | 13 | ## v0.3.0 14 | 15 | * Add `Matrix#adjoint` [#14] 16 | 17 | ## v0.2.0 / Ruby 2.7 18 | 19 | * Add Matrix#abs [ruby/ruby#2199] 20 | 21 | ## v0.1.0 / Ruby 2.6 22 | 23 | * Add `Matrix#antisymmetric?` / `#skew_symmetric?` 24 | * Add `Matrix#map!` / `#collect!` 25 | * Add `Matrix#[]=` 26 | * Add `Vector#map!` / `#collect!` 27 | * Add `Vector#[]=` 28 | 29 | ## Ruby 2.5 30 | 31 | * Add `Matrix.combine` and `Matrix#combine` 32 | * `Matrix#hadamard_product` and `Matrix#entrywise_product` 33 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem "rake" 6 | gem "test-unit" 7 | gem "test-unit-ruby-core" 8 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 1993-2013 Yukihiro Matsumoto. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions 5 | are met: 6 | 1. Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | 2. Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 14 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 15 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 16 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 17 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 18 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 19 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 20 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 21 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 22 | SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Matrix [![Version](https://badge.fury.io/rb/matrix.svg)](https://badge.fury.io/rb/matrix) [![Bundled Gem](https://img.shields.io/badge/stdgem-bundled-9c1260.svg)](https://stdgems.org/matrix/) [![Test](https://github.com/ruby/matrix/workflows/test/badge.svg)](https://github.com/ruby/matrix/actions?query=workflow%3Atest) 2 | 3 | An implementation of `Matrix` and `Vector` classes. 4 | 5 | The `Matrix` class represents a mathematical matrix. It provides methods for creating matrices, operating on them arithmetically and algebraically, and determining their mathematical properties (trace, rank, inverse, determinant, eigensystem, etc.). 6 | 7 | The `Vector` class represents a mathematical vector, which is useful in its own right, and also constitutes a row or column of a `Matrix`. 8 | 9 | ## Installation 10 | 11 | The `matrix` library comes pre-installed with Ruby. Unless you need recent features, you can simply `require 'matrix'` directly, no need to install it. 12 | 13 | If you need features that are more recent than the version of Ruby you want to support (check the [CHANGELOG](CHANGELOG.md)), you must use the gem. To do this, add this line to your application's Gemfile or gem's gemspec: 14 | 15 | ```ruby 16 | gem 'matrix' 17 | ``` 18 | 19 | And then execute: 20 | 21 | $ bundle 22 | 23 | To make sure that the gem takes precedence over the builtin library, call `bundle exec ...` (or call `gem 'matrix'` explicitly). 24 | 25 | ## Usage 26 | 27 | ```ruby 28 | require 'matrix' 29 | m = Matrix[[1, 2], [3, 4]] 30 | m.determinant # => -2 31 | ``` 32 | 33 | ## Development 34 | 35 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 36 | 37 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 38 | 39 | ## Contributing 40 | 41 | Bug reports and pull requests are welcome on GitHub at https://github.com/ruby/matrix. 42 | 43 | ## License 44 | 45 | The gem is available as open source under the terms of the [2-Clause BSD License](https://opensource.org/licenses/BSD-2-Clause). 46 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test/lib" 6 | t.ruby_opts << "-rhelper" 7 | t.test_files = FileList["test/**/test_*.rb"] 8 | end 9 | 10 | task :default => :test 11 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "matrix" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /lib/matrix.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # frozen_string_literal: false 3 | # 4 | # = matrix.rb 5 | # 6 | # An implementation of Matrix and Vector classes. 7 | # 8 | # See classes Matrix and Vector for documentation. 9 | # 10 | # Current Maintainer:: Marc-André Lafortune 11 | # Original Author:: Keiju ISHITSUKA 12 | # Original Documentation:: Gavin Sinclair (sourced from Ruby in a Nutshell (Matsumoto, O'Reilly)) 13 | ## 14 | 15 | require_relative "matrix/version" 16 | 17 | module ExceptionForMatrix # :nodoc: 18 | class ErrDimensionMismatch < StandardError 19 | def initialize(val = nil) 20 | if val 21 | super(val) 22 | else 23 | super("Dimension mismatch") 24 | end 25 | end 26 | end 27 | 28 | class ErrNotRegular < StandardError 29 | def initialize(val = nil) 30 | if val 31 | super(val) 32 | else 33 | super("Not Regular Matrix") 34 | end 35 | end 36 | end 37 | 38 | class ErrOperationNotDefined < StandardError 39 | def initialize(vals) 40 | if vals.is_a?(Array) 41 | super("Operation(#{vals[0]}) can't be defined: #{vals[1]} op #{vals[2]}") 42 | else 43 | super(vals) 44 | end 45 | end 46 | end 47 | 48 | class ErrOperationNotImplemented < StandardError 49 | def initialize(vals) 50 | super("Sorry, Operation(#{vals[0]}) not implemented: #{vals[1]} op #{vals[2]}") 51 | end 52 | end 53 | end 54 | 55 | # 56 | # The +Matrix+ class represents a mathematical matrix. It provides methods for creating 57 | # matrices, operating on them arithmetically and algebraically, 58 | # and determining their mathematical properties such as trace, rank, inverse, determinant, 59 | # or eigensystem. 60 | # 61 | class Matrix 62 | include Enumerable 63 | include ExceptionForMatrix 64 | autoload :EigenvalueDecomposition, "matrix/eigenvalue_decomposition" 65 | autoload :LUPDecomposition, "matrix/lup_decomposition" 66 | 67 | # instance creations 68 | private_class_method :new 69 | attr_reader :rows 70 | protected :rows 71 | 72 | # 73 | # Creates a matrix where each argument is a row. 74 | # Matrix[ [25, 93], [-1, 66] ] 75 | # # => 25 93 76 | # # -1 66 77 | # 78 | def Matrix.[](*rows) 79 | rows(rows, false) 80 | end 81 | 82 | # 83 | # Creates a matrix where +rows+ is an array of arrays, each of which is a row 84 | # of the matrix. If the optional argument +copy+ is false, use the given 85 | # arrays as the internal structure of the matrix without copying. 86 | # Matrix.rows([[25, 93], [-1, 66]]) 87 | # # => 25 93 88 | # # -1 66 89 | # 90 | def Matrix.rows(rows, copy = true) 91 | rows = convert_to_array(rows, copy) 92 | rows.map! do |row| 93 | convert_to_array(row, copy) 94 | end 95 | size = (rows[0] || []).size 96 | rows.each do |row| 97 | raise ErrDimensionMismatch, "row size differs (#{row.size} should be #{size})" unless row.size == size 98 | end 99 | new rows, size 100 | end 101 | 102 | # 103 | # Creates a matrix using +columns+ as an array of column vectors. 104 | # Matrix.columns([[25, 93], [-1, 66]]) 105 | # # => 25 -1 106 | # # 93 66 107 | # 108 | def Matrix.columns(columns) 109 | rows(columns, false).transpose 110 | end 111 | 112 | # 113 | # Creates a matrix of size +row_count+ x +column_count+. 114 | # It fills the values by calling the given block, 115 | # passing the current row and column. 116 | # Returns an enumerator if no block is given. 117 | # 118 | # m = Matrix.build(2, 4) {|row, col| col - row } 119 | # # => Matrix[[0, 1, 2, 3], [-1, 0, 1, 2]] 120 | # m = Matrix.build(3) { rand } 121 | # # => a 3x3 matrix with random elements 122 | # 123 | def Matrix.build(row_count, column_count = row_count) 124 | row_count = CoercionHelper.coerce_to_int(row_count) 125 | column_count = CoercionHelper.coerce_to_int(column_count) 126 | raise ArgumentError if row_count < 0 || column_count < 0 127 | return to_enum :build, row_count, column_count unless block_given? 128 | rows = Array.new(row_count) do |i| 129 | Array.new(column_count) do |j| 130 | yield i, j 131 | end 132 | end 133 | new rows, column_count 134 | end 135 | 136 | # 137 | # Creates a matrix where the diagonal elements are composed of +values+. 138 | # Matrix.diagonal(9, 5, -3) 139 | # # => 9 0 0 140 | # # 0 5 0 141 | # # 0 0 -3 142 | # 143 | def Matrix.diagonal(*values) 144 | size = values.size 145 | return Matrix.empty if size == 0 146 | rows = Array.new(size) {|j| 147 | row = Array.new(size, 0) 148 | row[j] = values[j] 149 | row 150 | } 151 | new rows 152 | end 153 | 154 | # 155 | # Creates an +n+ by +n+ diagonal matrix where each diagonal element is 156 | # +value+. 157 | # Matrix.scalar(2, 5) 158 | # # => 5 0 159 | # # 0 5 160 | # 161 | def Matrix.scalar(n, value) 162 | diagonal(*Array.new(n, value)) 163 | end 164 | 165 | # 166 | # Creates an +n+ by +n+ identity matrix. 167 | # Matrix.identity(2) 168 | # # => 1 0 169 | # # 0 1 170 | # 171 | def Matrix.identity(n) 172 | scalar(n, 1) 173 | end 174 | class << Matrix 175 | alias_method :unit, :identity 176 | alias_method :I, :identity 177 | end 178 | 179 | # 180 | # Creates a zero matrix. 181 | # Matrix.zero(2) 182 | # # => 0 0 183 | # # 0 0 184 | # 185 | def Matrix.zero(row_count, column_count = row_count) 186 | rows = Array.new(row_count){Array.new(column_count, 0)} 187 | new rows, column_count 188 | end 189 | 190 | # 191 | # Creates a single-row matrix where the values of that row are as given in 192 | # +row+. 193 | # Matrix.row_vector([4,5,6]) 194 | # # => 4 5 6 195 | # 196 | def Matrix.row_vector(row) 197 | row = convert_to_array(row) 198 | new [row] 199 | end 200 | 201 | # 202 | # Creates a single-column matrix where the values of that column are as given 203 | # in +column+. 204 | # Matrix.column_vector([4,5,6]) 205 | # # => 4 206 | # # 5 207 | # # 6 208 | # 209 | def Matrix.column_vector(column) 210 | column = convert_to_array(column) 211 | new [column].transpose, 1 212 | end 213 | 214 | # 215 | # Creates a empty matrix of +row_count+ x +column_count+. 216 | # At least one of +row_count+ or +column_count+ must be 0. 217 | # 218 | # m = Matrix.empty(2, 0) 219 | # m == Matrix[ [], [] ] 220 | # # => true 221 | # n = Matrix.empty(0, 3) 222 | # n == Matrix.columns([ [], [], [] ]) 223 | # # => true 224 | # m * n 225 | # # => Matrix[[0, 0, 0], [0, 0, 0]] 226 | # 227 | def Matrix.empty(row_count = 0, column_count = 0) 228 | raise ArgumentError, "One size must be 0" if column_count != 0 && row_count != 0 229 | raise ArgumentError, "Negative size" if column_count < 0 || row_count < 0 230 | 231 | new([[]]*row_count, column_count) 232 | end 233 | 234 | # 235 | # Create a matrix by stacking matrices vertically 236 | # 237 | # x = Matrix[[1, 2], [3, 4]] 238 | # y = Matrix[[5, 6], [7, 8]] 239 | # Matrix.vstack(x, y) # => Matrix[[1, 2], [3, 4], [5, 6], [7, 8]] 240 | # 241 | def Matrix.vstack(x, *matrices) 242 | x = CoercionHelper.coerce_to_matrix(x) 243 | result = x.send(:rows).map(&:dup) 244 | matrices.each do |m| 245 | m = CoercionHelper.coerce_to_matrix(m) 246 | if m.column_count != x.column_count 247 | raise ErrDimensionMismatch, "The given matrices must have #{x.column_count} columns, but one has #{m.column_count}" 248 | end 249 | result.concat(m.send(:rows)) 250 | end 251 | new result, x.column_count 252 | end 253 | 254 | 255 | # 256 | # Create a matrix by stacking matrices horizontally 257 | # 258 | # x = Matrix[[1, 2], [3, 4]] 259 | # y = Matrix[[5, 6], [7, 8]] 260 | # Matrix.hstack(x, y) # => Matrix[[1, 2, 5, 6], [3, 4, 7, 8]] 261 | # 262 | def Matrix.hstack(x, *matrices) 263 | x = CoercionHelper.coerce_to_matrix(x) 264 | result = x.send(:rows).map(&:dup) 265 | total_column_count = x.column_count 266 | matrices.each do |m| 267 | m = CoercionHelper.coerce_to_matrix(m) 268 | if m.row_count != x.row_count 269 | raise ErrDimensionMismatch, "The given matrices must have #{x.row_count} rows, but one has #{m.row_count}" 270 | end 271 | result.each_with_index do |row, i| 272 | row.concat m.send(:rows)[i] 273 | end 274 | total_column_count += m.column_count 275 | end 276 | new result, total_column_count 277 | end 278 | 279 | # :call-seq: 280 | # Matrix.combine(*matrices) { |*elements| ... } 281 | # 282 | # Create a matrix by combining matrices entrywise, using the given block 283 | # 284 | # x = Matrix[[6, 6], [4, 4]] 285 | # y = Matrix[[1, 2], [3, 4]] 286 | # Matrix.combine(x, y) {|a, b| a - b} # => Matrix[[5, 4], [1, 0]] 287 | # 288 | def Matrix.combine(*matrices) 289 | return to_enum(__method__, *matrices) unless block_given? 290 | 291 | return Matrix.empty if matrices.empty? 292 | matrices.map!(&CoercionHelper.method(:coerce_to_matrix)) 293 | x = matrices.first 294 | matrices.each do |m| 295 | raise ErrDimensionMismatch unless x.row_count == m.row_count && x.column_count == m.column_count 296 | end 297 | 298 | rows = Array.new(x.row_count) do |i| 299 | Array.new(x.column_count) do |j| 300 | yield matrices.map{|m| m[i,j]} 301 | end 302 | end 303 | new rows, x.column_count 304 | end 305 | 306 | # :call-seq: 307 | # combine(*other_matrices) { |*elements| ... } 308 | # 309 | # Creates new matrix by combining with other_matrices entrywise, 310 | # using the given block. 311 | # 312 | # x = Matrix[[6, 6], [4, 4]] 313 | # y = Matrix[[1, 2], [3, 4]] 314 | # x.combine(y) {|a, b| a - b} # => Matrix[[5, 4], [1, 0]] 315 | def combine(*matrices, &block) 316 | Matrix.combine(self, *matrices, &block) 317 | end 318 | 319 | # 320 | # Matrix.new is private; use ::rows, ::columns, ::[], etc... to create. 321 | # 322 | def initialize(rows, column_count = rows[0].size) 323 | # No checking is done at this point. rows must be an Array of Arrays. 324 | # column_count must be the size of the first row, if there is one, 325 | # otherwise it *must* be specified and can be any integer >= 0 326 | @rows = rows 327 | @column_count = column_count 328 | end 329 | 330 | private def new_matrix(rows, column_count = rows[0].size) # :nodoc: 331 | self.class.send(:new, rows, column_count) # bypass privacy of Matrix.new 332 | end 333 | 334 | # 335 | # Returns element (+i+,+j+) of the matrix. That is: row +i+, column +j+. 336 | # 337 | def [](i, j) 338 | @rows.fetch(i){return nil}[j] 339 | end 340 | alias element [] 341 | alias component [] 342 | 343 | # 344 | # :call-seq: 345 | # matrix[range, range] = matrix/element 346 | # matrix[range, integer] = vector/column_matrix/element 347 | # matrix[integer, range] = vector/row_matrix/element 348 | # matrix[integer, integer] = element 349 | # 350 | # Set element or elements of matrix. 351 | def []=(i, j, v) 352 | raise FrozenError, "can't modify frozen Matrix" if frozen? 353 | rows = check_range(i, :row) or row = check_int(i, :row) 354 | columns = check_range(j, :column) or column = check_int(j, :column) 355 | if rows && columns 356 | set_row_and_col_range(rows, columns, v) 357 | elsif rows 358 | set_row_range(rows, column, v) 359 | elsif columns 360 | set_col_range(row, columns, v) 361 | else 362 | set_value(row, column, v) 363 | end 364 | end 365 | alias set_element []= 366 | alias set_component []= 367 | private :set_element, :set_component 368 | 369 | # Returns range or nil 370 | private def check_range(val, direction) 371 | return unless val.is_a?(Range) 372 | count = direction == :row ? row_count : column_count 373 | CoercionHelper.check_range(val, count, direction) 374 | end 375 | 376 | private def check_int(val, direction) 377 | count = direction == :row ? row_count : column_count 378 | CoercionHelper.check_int(val, count, direction) 379 | end 380 | 381 | private def set_value(row, col, value) 382 | raise ErrDimensionMismatch, "Expected a value, got a #{value.class}" if value.respond_to?(:to_matrix) 383 | 384 | @rows[row][col] = value 385 | end 386 | 387 | private def set_row_and_col_range(row_range, col_range, value) 388 | if value.is_a?(Matrix) 389 | if row_range.size != value.row_count || col_range.size != value.column_count 390 | raise ErrDimensionMismatch, [ 391 | 'Expected a Matrix of dimensions', 392 | "#{row_range.size}x#{col_range.size}", 393 | 'got', 394 | "#{value.row_count}x#{value.column_count}", 395 | ].join(' ') 396 | end 397 | source = value.instance_variable_get :@rows 398 | row_range.each_with_index do |row, i| 399 | @rows[row][col_range] = source[i] 400 | end 401 | elsif value.is_a?(Vector) 402 | raise ErrDimensionMismatch, 'Expected a Matrix or a value, got a Vector' 403 | else 404 | value_to_set = Array.new(col_range.size, value) 405 | row_range.each do |i| 406 | @rows[i][col_range] = value_to_set 407 | end 408 | end 409 | end 410 | 411 | private def set_row_range(row_range, col, value) 412 | if value.is_a?(Vector) 413 | raise ErrDimensionMismatch unless row_range.size == value.size 414 | set_column_vector(row_range, col, value) 415 | elsif value.is_a?(Matrix) 416 | raise ErrDimensionMismatch unless value.column_count == 1 417 | value = value.column(0) 418 | raise ErrDimensionMismatch unless row_range.size == value.size 419 | set_column_vector(row_range, col, value) 420 | else 421 | @rows[row_range].each{|e| e[col] = value } 422 | end 423 | end 424 | 425 | private def set_column_vector(row_range, col, value) 426 | value.each_with_index do |e, index| 427 | r = row_range.begin + index 428 | @rows[r][col] = e 429 | end 430 | end 431 | 432 | private def set_col_range(row, col_range, value) 433 | value = if value.is_a?(Vector) 434 | value.to_a 435 | elsif value.is_a?(Matrix) 436 | raise ErrDimensionMismatch unless value.row_count == 1 437 | value.row(0).to_a 438 | else 439 | Array.new(col_range.size, value) 440 | end 441 | raise ErrDimensionMismatch unless col_range.size == value.size 442 | @rows[row][col_range] = value 443 | end 444 | 445 | # 446 | # Returns the number of rows. 447 | # 448 | def row_count 449 | @rows.size 450 | end 451 | 452 | alias_method :row_size, :row_count 453 | # 454 | # Returns the number of columns. 455 | # 456 | attr_reader :column_count 457 | alias_method :column_size, :column_count 458 | 459 | # 460 | # Returns row vector number +i+ of the matrix as a Vector (starting at 0 like 461 | # an array). When a block is given, the elements of that vector are iterated. 462 | # 463 | def row(i, &block) # :yield: e 464 | if block_given? 465 | @rows.fetch(i){return self}.each(&block) 466 | self 467 | else 468 | Vector.elements(@rows.fetch(i){return nil}) 469 | end 470 | end 471 | 472 | # 473 | # Returns column vector number +j+ of the matrix as a Vector (starting at 0 474 | # like an array). When a block is given, the elements of that vector are 475 | # iterated. 476 | # 477 | def column(j) # :yield: e 478 | if block_given? 479 | return self if j >= column_count || j < -column_count 480 | row_count.times do |i| 481 | yield @rows[i][j] 482 | end 483 | self 484 | else 485 | return nil if j >= column_count || j < -column_count 486 | col = Array.new(row_count) {|i| 487 | @rows[i][j] 488 | } 489 | Vector.elements(col, false) 490 | end 491 | end 492 | 493 | # 494 | # Returns a matrix that is the result of iteration of the given block over all 495 | # elements of the matrix. 496 | # Elements can be restricted by passing an argument: 497 | # * :all (default): yields all elements 498 | # * :diagonal: yields only elements on the diagonal 499 | # * :off_diagonal: yields all elements except on the diagonal 500 | # * :lower: yields only elements on or below the diagonal 501 | # * :strict_lower: yields only elements below the diagonal 502 | # * :strict_upper: yields only elements above the diagonal 503 | # * :upper: yields only elements on or above the diagonal 504 | # Matrix[ [1,2], [3,4] ].collect { |e| e**2 } 505 | # # => 1 4 506 | # # 9 16 507 | # 508 | def collect(which = :all, &block) # :yield: e 509 | return to_enum(:collect, which) unless block_given? 510 | dup.collect!(which, &block) 511 | end 512 | alias_method :map, :collect 513 | 514 | # 515 | # Invokes the given block for each element of matrix, replacing the element with the value 516 | # returned by the block. 517 | # Elements can be restricted by passing an argument: 518 | # * :all (default): yields all elements 519 | # * :diagonal: yields only elements on the diagonal 520 | # * :off_diagonal: yields all elements except on the diagonal 521 | # * :lower: yields only elements on or below the diagonal 522 | # * :strict_lower: yields only elements below the diagonal 523 | # * :strict_upper: yields only elements above the diagonal 524 | # * :upper: yields only elements on or above the diagonal 525 | # 526 | def collect!(which = :all) 527 | return to_enum(:collect!, which) unless block_given? 528 | raise FrozenError, "can't modify frozen Matrix" if frozen? 529 | each_with_index(which){ |e, row_index, col_index| @rows[row_index][col_index] = yield e } 530 | end 531 | 532 | alias map! collect! 533 | 534 | def freeze 535 | @rows.each(&:freeze).freeze 536 | 537 | super 538 | end 539 | 540 | # 541 | # Yields all elements of the matrix, starting with those of the first row, 542 | # or returns an Enumerator if no block given. 543 | # Elements can be restricted by passing an argument: 544 | # * :all (default): yields all elements 545 | # * :diagonal: yields only elements on the diagonal 546 | # * :off_diagonal: yields all elements except on the diagonal 547 | # * :lower: yields only elements on or below the diagonal 548 | # * :strict_lower: yields only elements below the diagonal 549 | # * :strict_upper: yields only elements above the diagonal 550 | # * :upper: yields only elements on or above the diagonal 551 | # 552 | # Matrix[ [1,2], [3,4] ].each { |e| puts e } 553 | # # => prints the numbers 1 to 4 554 | # Matrix[ [1,2], [3,4] ].each(:strict_lower).to_a # => [3] 555 | # 556 | def each(which = :all, &block) # :yield: e 557 | return to_enum :each, which unless block_given? 558 | last = column_count - 1 559 | case which 560 | when :all 561 | @rows.each do |row| 562 | row.each(&block) 563 | end 564 | when :diagonal 565 | @rows.each_with_index do |row, row_index| 566 | yield row.fetch(row_index){return self} 567 | end 568 | when :off_diagonal 569 | @rows.each_with_index do |row, row_index| 570 | column_count.times do |col_index| 571 | yield row[col_index] unless row_index == col_index 572 | end 573 | end 574 | when :lower 575 | @rows.each_with_index do |row, row_index| 576 | 0.upto([row_index, last].min) do |col_index| 577 | yield row[col_index] 578 | end 579 | end 580 | when :strict_lower 581 | @rows.each_with_index do |row, row_index| 582 | [row_index, column_count].min.times do |col_index| 583 | yield row[col_index] 584 | end 585 | end 586 | when :strict_upper 587 | @rows.each_with_index do |row, row_index| 588 | (row_index+1).upto(last) do |col_index| 589 | yield row[col_index] 590 | end 591 | end 592 | when :upper 593 | @rows.each_with_index do |row, row_index| 594 | row_index.upto(last) do |col_index| 595 | yield row[col_index] 596 | end 597 | end 598 | else 599 | raise ArgumentError, "expected #{which.inspect} to be one of :all, :diagonal, :off_diagonal, :lower, :strict_lower, :strict_upper or :upper" 600 | end 601 | self 602 | end 603 | 604 | # 605 | # Same as #each, but the row index and column index in addition to the element 606 | # 607 | # Matrix[ [1,2], [3,4] ].each_with_index do |e, row, col| 608 | # puts "#{e} at #{row}, #{col}" 609 | # end 610 | # # => Prints: 611 | # # 1 at 0, 0 612 | # # 2 at 0, 1 613 | # # 3 at 1, 0 614 | # # 4 at 1, 1 615 | # 616 | def each_with_index(which = :all) # :yield: e, row, column 617 | return to_enum :each_with_index, which unless block_given? 618 | last = column_count - 1 619 | case which 620 | when :all 621 | @rows.each_with_index do |row, row_index| 622 | row.each_with_index do |e, col_index| 623 | yield e, row_index, col_index 624 | end 625 | end 626 | when :diagonal 627 | @rows.each_with_index do |row, row_index| 628 | yield row.fetch(row_index){return self}, row_index, row_index 629 | end 630 | when :off_diagonal 631 | @rows.each_with_index do |row, row_index| 632 | column_count.times do |col_index| 633 | yield row[col_index], row_index, col_index unless row_index == col_index 634 | end 635 | end 636 | when :lower 637 | @rows.each_with_index do |row, row_index| 638 | 0.upto([row_index, last].min) do |col_index| 639 | yield row[col_index], row_index, col_index 640 | end 641 | end 642 | when :strict_lower 643 | @rows.each_with_index do |row, row_index| 644 | [row_index, column_count].min.times do |col_index| 645 | yield row[col_index], row_index, col_index 646 | end 647 | end 648 | when :strict_upper 649 | @rows.each_with_index do |row, row_index| 650 | (row_index+1).upto(last) do |col_index| 651 | yield row[col_index], row_index, col_index 652 | end 653 | end 654 | when :upper 655 | @rows.each_with_index do |row, row_index| 656 | row_index.upto(last) do |col_index| 657 | yield row[col_index], row_index, col_index 658 | end 659 | end 660 | else 661 | raise ArgumentError, "expected #{which.inspect} to be one of :all, :diagonal, :off_diagonal, :lower, :strict_lower, :strict_upper or :upper" 662 | end 663 | self 664 | end 665 | 666 | SELECTORS = {all: true, diagonal: true, off_diagonal: true, lower: true, strict_lower: true, strict_upper: true, upper: true}.freeze 667 | # 668 | # :call-seq: 669 | # index(value, selector = :all) -> [row, column] 670 | # index(selector = :all){ block } -> [row, column] 671 | # index(selector = :all) -> an_enumerator 672 | # 673 | # The index method is specialized to return the index as [row, column] 674 | # It also accepts an optional +selector+ argument, see #each for details. 675 | # 676 | # Matrix[ [1,2], [3,4] ].index(&:even?) # => [0, 1] 677 | # Matrix[ [1,1], [1,1] ].index(1, :strict_lower) # => [1, 0] 678 | # 679 | def index(*args) 680 | raise ArgumentError, "wrong number of arguments(#{args.size} for 0-2)" if args.size > 2 681 | which = (args.size == 2 || SELECTORS.include?(args.last)) ? args.pop : :all 682 | return to_enum :find_index, which, *args unless block_given? || args.size == 1 683 | if args.size == 1 684 | value = args.first 685 | each_with_index(which) do |e, row_index, col_index| 686 | return row_index, col_index if e == value 687 | end 688 | else 689 | each_with_index(which) do |e, row_index, col_index| 690 | return row_index, col_index if yield e 691 | end 692 | end 693 | nil 694 | end 695 | alias_method :find_index, :index 696 | 697 | # 698 | # Returns a section of the matrix. The parameters are either: 699 | # * start_row, nrows, start_col, ncols; OR 700 | # * row_range, col_range 701 | # 702 | # Matrix.diagonal(9, 5, -3).minor(0..1, 0..2) 703 | # # => 9 0 0 704 | # # 0 5 0 705 | # 706 | # Like Array#[], negative indices count backward from the end of the 707 | # row or column (-1 is the last element). Returns nil if the starting 708 | # row or column is greater than row_count or column_count respectively. 709 | # 710 | def minor(*param) 711 | case param.size 712 | when 2 713 | row_range, col_range = param 714 | from_row = row_range.first 715 | from_row += row_count if from_row < 0 716 | to_row = row_range.end 717 | to_row += row_count if to_row < 0 718 | to_row += 1 unless row_range.exclude_end? 719 | size_row = to_row - from_row 720 | 721 | from_col = col_range.first 722 | from_col += column_count if from_col < 0 723 | to_col = col_range.end 724 | to_col += column_count if to_col < 0 725 | to_col += 1 unless col_range.exclude_end? 726 | size_col = to_col - from_col 727 | when 4 728 | from_row, size_row, from_col, size_col = param 729 | return nil if size_row < 0 || size_col < 0 730 | from_row += row_count if from_row < 0 731 | from_col += column_count if from_col < 0 732 | else 733 | raise ArgumentError, param.inspect 734 | end 735 | 736 | return nil if from_row > row_count || from_col > column_count || from_row < 0 || from_col < 0 737 | rows = @rows[from_row, size_row].collect{|row| 738 | row[from_col, size_col] 739 | } 740 | new_matrix rows, [column_count - from_col, size_col].min 741 | end 742 | 743 | # 744 | # Returns the submatrix obtained by deleting the specified row and column. 745 | # 746 | # Matrix.diagonal(9, 5, -3, 4).first_minor(1, 2) 747 | # # => 9 0 0 748 | # # 0 0 0 749 | # # 0 0 4 750 | # 751 | def first_minor(row, column) 752 | raise RuntimeError, "first_minor of empty matrix is not defined" if empty? 753 | 754 | unless 0 <= row && row < row_count 755 | raise ArgumentError, "invalid row (#{row.inspect} for 0..#{row_count - 1})" 756 | end 757 | 758 | unless 0 <= column && column < column_count 759 | raise ArgumentError, "invalid column (#{column.inspect} for 0..#{column_count - 1})" 760 | end 761 | 762 | arrays = to_a 763 | arrays.delete_at(row) 764 | arrays.each do |array| 765 | array.delete_at(column) 766 | end 767 | 768 | new_matrix arrays, column_count - 1 769 | end 770 | 771 | # 772 | # Returns the (row, column) cofactor which is obtained by multiplying 773 | # the first minor by (-1)**(row + column). 774 | # 775 | # Matrix.diagonal(9, 5, -3, 4).cofactor(1, 1) 776 | # # => -108 777 | # 778 | def cofactor(row, column) 779 | raise RuntimeError, "cofactor of empty matrix is not defined" if empty? 780 | raise ErrDimensionMismatch unless square? 781 | 782 | det_of_minor = first_minor(row, column).determinant 783 | det_of_minor * (-1) ** (row + column) 784 | end 785 | 786 | # 787 | # Returns the adjugate of the matrix. 788 | # 789 | # Matrix[ [7,6],[3,9] ].adjugate 790 | # # => 9 -6 791 | # # -3 7 792 | # 793 | def adjugate 794 | raise ErrDimensionMismatch unless square? 795 | Matrix.build(row_count, column_count) do |row, column| 796 | cofactor(column, row) 797 | end 798 | end 799 | 800 | # 801 | # Returns the Laplace expansion along given row or column. 802 | # 803 | # Matrix[[7,6], [3,9]].laplace_expansion(column: 1) 804 | # # => 45 805 | # 806 | # Matrix[[Vector[1, 0], Vector[0, 1]], [2, 3]].laplace_expansion(row: 0) 807 | # # => Vector[3, -2] 808 | # 809 | # 810 | def laplace_expansion(row: nil, column: nil) 811 | num = row || column 812 | 813 | if !num || (row && column) 814 | raise ArgumentError, "exactly one the row or column arguments must be specified" 815 | end 816 | 817 | raise ErrDimensionMismatch unless square? 818 | raise RuntimeError, "laplace_expansion of empty matrix is not defined" if empty? 819 | 820 | unless 0 <= num && num < row_count 821 | raise ArgumentError, "invalid num (#{num.inspect} for 0..#{row_count - 1})" 822 | end 823 | 824 | send(row ? :row : :column, num).map.with_index { |e, k| 825 | e * cofactor(*(row ? [num, k] : [k,num])) 826 | }.inject(:+) 827 | end 828 | alias_method :cofactor_expansion, :laplace_expansion 829 | 830 | 831 | #-- 832 | # TESTING -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 833 | #++ 834 | 835 | # 836 | # Returns +true+ if this is a diagonal matrix. 837 | # Raises an error if matrix is not square. 838 | # 839 | def diagonal? 840 | raise ErrDimensionMismatch unless square? 841 | each(:off_diagonal).all?(&:zero?) 842 | end 843 | 844 | # 845 | # Returns +true+ if this is an empty matrix, i.e. if the number of rows 846 | # or the number of columns is 0. 847 | # 848 | def empty? 849 | column_count == 0 || row_count == 0 850 | end 851 | 852 | # 853 | # Returns +true+ if this is an hermitian matrix. 854 | # Raises an error if matrix is not square. 855 | # 856 | def hermitian? 857 | raise ErrDimensionMismatch unless square? 858 | each_with_index(:upper).all? do |e, row, col| 859 | e == rows[col][row].conj 860 | end 861 | end 862 | 863 | # 864 | # Returns +true+ if this is a lower triangular matrix. 865 | # 866 | def lower_triangular? 867 | each(:strict_upper).all?(&:zero?) 868 | end 869 | 870 | # 871 | # Returns +true+ if this is a normal matrix. 872 | # Raises an error if matrix is not square. 873 | # 874 | def normal? 875 | raise ErrDimensionMismatch unless square? 876 | rows.each_with_index do |row_i, i| 877 | rows.each_with_index do |row_j, j| 878 | s = 0 879 | rows.each_with_index do |row_k, k| 880 | s += row_i[k] * row_j[k].conj - row_k[i].conj * row_k[j] 881 | end 882 | return false unless s == 0 883 | end 884 | end 885 | true 886 | end 887 | 888 | # 889 | # Returns +true+ if this is an orthogonal matrix 890 | # Raises an error if matrix is not square. 891 | # 892 | def orthogonal? 893 | raise ErrDimensionMismatch unless square? 894 | 895 | rows.each_with_index do |row_i, i| 896 | rows.each_with_index do |row_j, j| 897 | s = 0 898 | row_count.times do |k| 899 | s += row_i[k] * row_j[k] 900 | end 901 | return false unless s == (i == j ? 1 : 0) 902 | end 903 | end 904 | true 905 | end 906 | 907 | # 908 | # Returns +true+ if this is a permutation matrix 909 | # Raises an error if matrix is not square. 910 | # 911 | def permutation? 912 | raise ErrDimensionMismatch unless square? 913 | cols = Array.new(column_count) 914 | rows.each_with_index do |row, i| 915 | found = false 916 | row.each_with_index do |e, j| 917 | if e == 1 918 | return false if found || cols[j] 919 | found = cols[j] = true 920 | elsif e != 0 921 | return false 922 | end 923 | end 924 | return false unless found 925 | end 926 | true 927 | end 928 | 929 | # 930 | # Returns +true+ if all entries of the matrix are real. 931 | # 932 | def real? 933 | all?(&:real?) 934 | end 935 | 936 | # 937 | # Returns +true+ if this is a regular (i.e. non-singular) matrix. 938 | # 939 | def regular? 940 | not singular? 941 | end 942 | 943 | # 944 | # Returns +true+ if this is a singular matrix. 945 | # 946 | def singular? 947 | determinant == 0 948 | end 949 | 950 | # 951 | # Returns +true+ if this is a square matrix. 952 | # 953 | def square? 954 | column_count == row_count 955 | end 956 | 957 | # 958 | # Returns +true+ if this is a symmetric matrix. 959 | # Raises an error if matrix is not square. 960 | # 961 | def symmetric? 962 | raise ErrDimensionMismatch unless square? 963 | each_with_index(:strict_upper) do |e, row, col| 964 | return false if e != rows[col][row] 965 | end 966 | true 967 | end 968 | 969 | # 970 | # Returns +true+ if this is an antisymmetric matrix. 971 | # Raises an error if matrix is not square. 972 | # 973 | def antisymmetric? 974 | raise ErrDimensionMismatch unless square? 975 | each_with_index(:upper) do |e, row, col| 976 | return false unless e == -rows[col][row] 977 | end 978 | true 979 | end 980 | alias_method :skew_symmetric?, :antisymmetric? 981 | 982 | # 983 | # Returns +true+ if this is a unitary matrix 984 | # Raises an error if matrix is not square. 985 | # 986 | def unitary? 987 | raise ErrDimensionMismatch unless square? 988 | rows.each_with_index do |row_i, i| 989 | rows.each_with_index do |row_j, j| 990 | s = 0 991 | row_count.times do |k| 992 | s += row_i[k].conj * row_j[k] 993 | end 994 | return false unless s == (i == j ? 1 : 0) 995 | end 996 | end 997 | true 998 | end 999 | 1000 | # 1001 | # Returns +true+ if this is an upper triangular matrix. 1002 | # 1003 | def upper_triangular? 1004 | each(:strict_lower).all?(&:zero?) 1005 | end 1006 | 1007 | # 1008 | # Returns +true+ if this is a matrix with only zero elements 1009 | # 1010 | def zero? 1011 | all?(&:zero?) 1012 | end 1013 | 1014 | #-- 1015 | # OBJECT METHODS -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 1016 | #++ 1017 | 1018 | # 1019 | # Returns whether the two matrices contain equal elements. 1020 | # 1021 | def ==(other) 1022 | return false unless Matrix === other && 1023 | column_count == other.column_count # necessary for empty matrices 1024 | rows == other.rows 1025 | end 1026 | 1027 | def eql?(other) 1028 | return false unless Matrix === other && 1029 | column_count == other.column_count # necessary for empty matrices 1030 | rows.eql? other.rows 1031 | end 1032 | 1033 | # 1034 | # Called for dup & clone. 1035 | # 1036 | private def initialize_copy(m) 1037 | super 1038 | @rows = @rows.map(&:dup) unless frozen? 1039 | end 1040 | 1041 | # 1042 | # Returns a hash-code for the matrix. 1043 | # 1044 | def hash 1045 | @rows.hash 1046 | end 1047 | 1048 | #-- 1049 | # ARITHMETIC -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 1050 | #++ 1051 | 1052 | # 1053 | # Matrix multiplication. 1054 | # Matrix[[2,4], [6,8]] * Matrix.identity(2) 1055 | # # => 2 4 1056 | # # 6 8 1057 | # 1058 | def *(m) # m is matrix or vector or number 1059 | case(m) 1060 | when Numeric 1061 | new_rows = @rows.collect {|row| 1062 | row.collect {|e| e * m } 1063 | } 1064 | return new_matrix new_rows, column_count 1065 | when Vector 1066 | m = self.class.column_vector(m) 1067 | r = self * m 1068 | return r.column(0) 1069 | when Matrix 1070 | raise ErrDimensionMismatch if column_count != m.row_count 1071 | m_rows = m.rows 1072 | new_rows = rows.map do |row_i| 1073 | Array.new(m.column_count) do |j| 1074 | vij = 0 1075 | column_count.times do |k| 1076 | vij += row_i[k] * m_rows[k][j] 1077 | end 1078 | vij 1079 | end 1080 | end 1081 | return new_matrix new_rows, m.column_count 1082 | else 1083 | return apply_through_coercion(m, __method__) 1084 | end 1085 | end 1086 | 1087 | # 1088 | # Matrix addition. 1089 | # Matrix.scalar(2,5) + Matrix[[1,0], [-4,7]] 1090 | # # => 6 0 1091 | # # -4 12 1092 | # 1093 | def +(m) 1094 | case m 1095 | when Numeric 1096 | raise ErrOperationNotDefined, ["+", self.class, m.class] 1097 | when Vector 1098 | m = self.class.column_vector(m) 1099 | when Matrix 1100 | else 1101 | return apply_through_coercion(m, __method__) 1102 | end 1103 | 1104 | raise ErrDimensionMismatch unless row_count == m.row_count && column_count == m.column_count 1105 | 1106 | rows = Array.new(row_count) {|i| 1107 | Array.new(column_count) {|j| 1108 | self[i, j] + m[i, j] 1109 | } 1110 | } 1111 | new_matrix rows, column_count 1112 | end 1113 | 1114 | # 1115 | # Matrix subtraction. 1116 | # Matrix[[1,5], [4,2]] - Matrix[[9,3], [-4,1]] 1117 | # # => -8 2 1118 | # # 8 1 1119 | # 1120 | def -(m) 1121 | case m 1122 | when Numeric 1123 | raise ErrOperationNotDefined, ["-", self.class, m.class] 1124 | when Vector 1125 | m = self.class.column_vector(m) 1126 | when Matrix 1127 | else 1128 | return apply_through_coercion(m, __method__) 1129 | end 1130 | 1131 | raise ErrDimensionMismatch unless row_count == m.row_count && column_count == m.column_count 1132 | 1133 | rows = Array.new(row_count) {|i| 1134 | Array.new(column_count) {|j| 1135 | self[i, j] - m[i, j] 1136 | } 1137 | } 1138 | new_matrix rows, column_count 1139 | end 1140 | 1141 | # 1142 | # Matrix division (multiplication by the inverse). 1143 | # Matrix[[7,6], [3,9]] / Matrix[[2,9], [3,1]] 1144 | # # => -7 1 1145 | # # -3 -6 1146 | # 1147 | def /(other) 1148 | case other 1149 | when Numeric 1150 | rows = @rows.collect {|row| 1151 | row.collect {|e| e / other } 1152 | } 1153 | return new_matrix rows, column_count 1154 | when Matrix 1155 | return self * other.inverse 1156 | else 1157 | return apply_through_coercion(other, __method__) 1158 | end 1159 | end 1160 | 1161 | # 1162 | # Hadamard product 1163 | # Matrix[[1,2], [3,4]].hadamard_product(Matrix[[1,2], [3,2]]) 1164 | # # => 1 4 1165 | # # 9 8 1166 | # 1167 | def hadamard_product(m) 1168 | combine(m){|a, b| a * b} 1169 | end 1170 | alias_method :entrywise_product, :hadamard_product 1171 | 1172 | # 1173 | # Returns the inverse of the matrix. 1174 | # Matrix[[-1, -1], [0, -1]].inverse 1175 | # # => -1 1 1176 | # # 0 -1 1177 | # 1178 | def inverse 1179 | raise ErrDimensionMismatch unless square? 1180 | self.class.I(row_count).send(:inverse_from, self) 1181 | end 1182 | alias_method :inv, :inverse 1183 | 1184 | private def inverse_from(src) # :nodoc: 1185 | last = row_count - 1 1186 | a = src.to_a 1187 | 1188 | 0.upto(last) do |k| 1189 | i = k 1190 | akk = a[k][k].abs 1191 | (k+1).upto(last) do |j| 1192 | v = a[j][k].abs 1193 | if v > akk 1194 | i = j 1195 | akk = v 1196 | end 1197 | end 1198 | raise ErrNotRegular if akk == 0 1199 | if i != k 1200 | a[i], a[k] = a[k], a[i] 1201 | @rows[i], @rows[k] = @rows[k], @rows[i] 1202 | end 1203 | akk = a[k][k] 1204 | 1205 | 0.upto(last) do |ii| 1206 | next if ii == k 1207 | q = a[ii][k].quo(akk) 1208 | a[ii][k] = 0 1209 | 1210 | (k + 1).upto(last) do |j| 1211 | a[ii][j] -= a[k][j] * q 1212 | end 1213 | 0.upto(last) do |j| 1214 | @rows[ii][j] -= @rows[k][j] * q 1215 | end 1216 | end 1217 | 1218 | (k+1).upto(last) do |j| 1219 | a[k][j] = a[k][j].quo(akk) 1220 | end 1221 | 0.upto(last) do |j| 1222 | @rows[k][j] = @rows[k][j].quo(akk) 1223 | end 1224 | end 1225 | self 1226 | end 1227 | 1228 | # 1229 | # Matrix exponentiation. 1230 | # Equivalent to multiplying the matrix by itself N times. 1231 | # Non-integer exponents will be handled by diagonalizing the matrix; 1232 | # this is not supported for Complex matrices. 1233 | # 1234 | # Matrix[[7,6], [3,9]] ** 2 1235 | # # => 67 96 1236 | # # 48 99 1237 | # 1238 | def **(exp) 1239 | case exp 1240 | when Integer 1241 | case 1242 | when exp == 0 1243 | raise ErrDimensionMismatch unless square? 1244 | self.class.identity(column_count) 1245 | when exp < 0 1246 | inverse.power_int(-exp) 1247 | else 1248 | power_int(exp) 1249 | end 1250 | when Numeric 1251 | v, d, v_inv = eigensystem 1252 | v * self.class.diagonal(*d.each(:diagonal).map{|e| e ** exp}) * v_inv 1253 | else 1254 | raise ErrOperationNotDefined, ["**", self.class, exp.class] 1255 | end 1256 | end 1257 | 1258 | protected def power_int(exp) 1259 | # assumes `exp` is an Integer > 0 1260 | # 1261 | # Previous algorithm: 1262 | # build M**2, M**4 = (M**2)**2, M**8, ... and multiplying those you need 1263 | # e.g. M**0b1011 = M**11 = M * M**2 * M**8 1264 | # ^ ^ 1265 | # (highlighted the 2 out of 5 multiplications involving `M * x`) 1266 | # 1267 | # Current algorithm has same number of multiplications but with lower exponents: 1268 | # M**11 = M * (M * M**4)**2 1269 | # ^ ^ ^ 1270 | # (highlighted the 3 out of 5 multiplications involving `M * x`) 1271 | # 1272 | # This should be faster for all (non nil-potent) matrices. 1273 | case 1274 | when exp == 1 1275 | self 1276 | when exp.odd? 1277 | self * power_int(exp - 1) 1278 | else 1279 | sqrt = power_int(exp / 2) 1280 | sqrt * sqrt 1281 | end 1282 | end 1283 | 1284 | def +@ 1285 | self 1286 | end 1287 | 1288 | # Unary matrix negation. 1289 | # 1290 | # -Matrix[[1,5], [4,2]] 1291 | # # => -1 -5 1292 | # # -4 -2 1293 | def -@ 1294 | collect {|e| -e } 1295 | end 1296 | 1297 | # 1298 | # Returns the absolute value elementwise 1299 | # 1300 | def abs 1301 | collect(&:abs) 1302 | end 1303 | 1304 | #-- 1305 | # MATRIX FUNCTIONS -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 1306 | #++ 1307 | 1308 | # 1309 | # Returns the determinant of the matrix. 1310 | # 1311 | # Beware that using Float values can yield erroneous results 1312 | # because of their lack of precision. 1313 | # Consider using exact types like Rational or BigDecimal instead. 1314 | # 1315 | # Matrix[[7,6], [3,9]].determinant 1316 | # # => 45 1317 | # 1318 | def determinant 1319 | raise ErrDimensionMismatch unless square? 1320 | m = @rows 1321 | case row_count 1322 | # Up to 4x4, give result using Laplacian expansion by minors. 1323 | # This will typically be faster, as well as giving good results 1324 | # in case of Floats 1325 | when 0 1326 | +1 1327 | when 1 1328 | + m[0][0] 1329 | when 2 1330 | + m[0][0] * m[1][1] - m[0][1] * m[1][0] 1331 | when 3 1332 | m0, m1, m2 = m 1333 | + m0[0] * m1[1] * m2[2] - m0[0] * m1[2] * m2[1] \ 1334 | - m0[1] * m1[0] * m2[2] + m0[1] * m1[2] * m2[0] \ 1335 | + m0[2] * m1[0] * m2[1] - m0[2] * m1[1] * m2[0] 1336 | when 4 1337 | m0, m1, m2, m3 = m 1338 | + m0[0] * m1[1] * m2[2] * m3[3] - m0[0] * m1[1] * m2[3] * m3[2] \ 1339 | - m0[0] * m1[2] * m2[1] * m3[3] + m0[0] * m1[2] * m2[3] * m3[1] \ 1340 | + m0[0] * m1[3] * m2[1] * m3[2] - m0[0] * m1[3] * m2[2] * m3[1] \ 1341 | - m0[1] * m1[0] * m2[2] * m3[3] + m0[1] * m1[0] * m2[3] * m3[2] \ 1342 | + m0[1] * m1[2] * m2[0] * m3[3] - m0[1] * m1[2] * m2[3] * m3[0] \ 1343 | - m0[1] * m1[3] * m2[0] * m3[2] + m0[1] * m1[3] * m2[2] * m3[0] \ 1344 | + m0[2] * m1[0] * m2[1] * m3[3] - m0[2] * m1[0] * m2[3] * m3[1] \ 1345 | - m0[2] * m1[1] * m2[0] * m3[3] + m0[2] * m1[1] * m2[3] * m3[0] \ 1346 | + m0[2] * m1[3] * m2[0] * m3[1] - m0[2] * m1[3] * m2[1] * m3[0] \ 1347 | - m0[3] * m1[0] * m2[1] * m3[2] + m0[3] * m1[0] * m2[2] * m3[1] \ 1348 | + m0[3] * m1[1] * m2[0] * m3[2] - m0[3] * m1[1] * m2[2] * m3[0] \ 1349 | - m0[3] * m1[2] * m2[0] * m3[1] + m0[3] * m1[2] * m2[1] * m3[0] 1350 | else 1351 | # For bigger matrices, use an efficient and general algorithm. 1352 | # Currently, we use the Gauss-Bareiss algorithm 1353 | determinant_bareiss 1354 | end 1355 | end 1356 | alias_method :det, :determinant 1357 | 1358 | # 1359 | # Private. Use Matrix#determinant 1360 | # 1361 | # Returns the determinant of the matrix, using 1362 | # Bareiss' multistep integer-preserving gaussian elimination. 1363 | # It has the same computational cost order O(n^3) as standard Gaussian elimination. 1364 | # Intermediate results are fraction free and of lower complexity. 1365 | # A matrix of Integers will have thus intermediate results that are also Integers, 1366 | # with smaller bignums (if any), while a matrix of Float will usually have 1367 | # intermediate results with better precision. 1368 | # 1369 | private def determinant_bareiss 1370 | size = row_count 1371 | last = size - 1 1372 | a = to_a 1373 | no_pivot = Proc.new{ return 0 } 1374 | sign = +1 1375 | pivot = 1 1376 | size.times do |k| 1377 | previous_pivot = pivot 1378 | if (pivot = a[k][k]) == 0 1379 | switch = (k+1 ... size).find(no_pivot) {|row| 1380 | a[row][k] != 0 1381 | } 1382 | a[switch], a[k] = a[k], a[switch] 1383 | pivot = a[k][k] 1384 | sign = -sign 1385 | end 1386 | (k+1).upto(last) do |i| 1387 | ai = a[i] 1388 | (k+1).upto(last) do |j| 1389 | ai[j] = (pivot * ai[j] - ai[k] * a[k][j]) / previous_pivot 1390 | end 1391 | end 1392 | end 1393 | sign * pivot 1394 | end 1395 | 1396 | # 1397 | # deprecated; use Matrix#determinant 1398 | # 1399 | def determinant_e 1400 | warn "Matrix#determinant_e is deprecated; use #determinant", uplevel: 1 1401 | determinant 1402 | end 1403 | alias_method :det_e, :determinant_e 1404 | 1405 | # 1406 | # Returns a new matrix resulting by stacking horizontally 1407 | # the receiver with the given matrices 1408 | # 1409 | # x = Matrix[[1, 2], [3, 4]] 1410 | # y = Matrix[[5, 6], [7, 8]] 1411 | # x.hstack(y) # => Matrix[[1, 2, 5, 6], [3, 4, 7, 8]] 1412 | # 1413 | def hstack(*matrices) 1414 | self.class.hstack(self, *matrices) 1415 | end 1416 | 1417 | # 1418 | # Returns the rank of the matrix. 1419 | # Beware that using Float values can yield erroneous results 1420 | # because of their lack of precision. 1421 | # Consider using exact types like Rational or BigDecimal instead. 1422 | # 1423 | # Matrix[[7,6], [3,9]].rank 1424 | # # => 2 1425 | # 1426 | def rank 1427 | # We currently use Bareiss' multistep integer-preserving gaussian elimination 1428 | # (see comments on determinant) 1429 | a = to_a 1430 | last_column = column_count - 1 1431 | last_row = row_count - 1 1432 | pivot_row = 0 1433 | previous_pivot = 1 1434 | 0.upto(last_column) do |k| 1435 | switch_row = (pivot_row .. last_row).find {|row| 1436 | a[row][k] != 0 1437 | } 1438 | if switch_row 1439 | a[switch_row], a[pivot_row] = a[pivot_row], a[switch_row] unless pivot_row == switch_row 1440 | pivot = a[pivot_row][k] 1441 | (pivot_row+1).upto(last_row) do |i| 1442 | ai = a[i] 1443 | (k+1).upto(last_column) do |j| 1444 | ai[j] = (pivot * ai[j] - ai[k] * a[pivot_row][j]) / previous_pivot 1445 | end 1446 | end 1447 | pivot_row += 1 1448 | previous_pivot = pivot 1449 | end 1450 | end 1451 | pivot_row 1452 | end 1453 | 1454 | # 1455 | # deprecated; use Matrix#rank 1456 | # 1457 | def rank_e 1458 | warn "Matrix#rank_e is deprecated; use #rank", uplevel: 1 1459 | rank 1460 | end 1461 | 1462 | # 1463 | # Returns a new matrix with rotated elements. 1464 | # The argument specifies the rotation (defaults to `:clockwise`): 1465 | # * :clockwise, 1, -3, etc.: "turn right" - first row becomes last column 1466 | # * :half_turn, 2, -2, etc.: first row becomes last row, elements in reverse order 1467 | # * :counter_clockwise, -1, 3: "turn left" - first row becomes first column 1468 | # (but with elements in reverse order) 1469 | # 1470 | # m = Matrix[ [1, 2], [3, 4] ] 1471 | # r = m.rotate_entries(:clockwise) 1472 | # # => Matrix[[3, 1], [4, 2]] 1473 | # 1474 | def rotate_entries(rotation = :clockwise) 1475 | rotation %= 4 if rotation.respond_to? :to_int 1476 | 1477 | case rotation 1478 | when 0 1479 | dup 1480 | when 1, :clockwise 1481 | new_matrix @rows.transpose.each(&:reverse!), row_count 1482 | when 2, :half_turn 1483 | new_matrix @rows.map(&:reverse).reverse!, column_count 1484 | when 3, :counter_clockwise 1485 | new_matrix @rows.transpose.reverse!, row_count 1486 | else 1487 | raise ArgumentError, "expected #{rotation.inspect} to be one of :clockwise, :counter_clockwise, :half_turn or an integer" 1488 | end 1489 | end 1490 | 1491 | # Returns a matrix with entries rounded to the given precision 1492 | # (see Float#round) 1493 | # 1494 | def round(ndigits=0) 1495 | map{|e| e.round(ndigits)} 1496 | end 1497 | 1498 | # 1499 | # Returns the trace (sum of diagonal elements) of the matrix. 1500 | # Matrix[[7,6], [3,9]].trace 1501 | # # => 16 1502 | # 1503 | def trace 1504 | raise ErrDimensionMismatch unless square? 1505 | (0...column_count).inject(0) do |tr, i| 1506 | tr + @rows[i][i] 1507 | end 1508 | end 1509 | alias_method :tr, :trace 1510 | 1511 | # 1512 | # Returns the transpose of the matrix. 1513 | # Matrix[[1,2], [3,4], [5,6]] 1514 | # # => 1 2 1515 | # # 3 4 1516 | # # 5 6 1517 | # Matrix[[1,2], [3,4], [5,6]].transpose 1518 | # # => 1 3 5 1519 | # # 2 4 6 1520 | # 1521 | def transpose 1522 | return self.class.empty(column_count, 0) if row_count.zero? 1523 | new_matrix @rows.transpose, row_count 1524 | end 1525 | alias_method :t, :transpose 1526 | 1527 | # 1528 | # Returns a new matrix resulting by stacking vertically 1529 | # the receiver with the given matrices 1530 | # 1531 | # x = Matrix[[1, 2], [3, 4]] 1532 | # y = Matrix[[5, 6], [7, 8]] 1533 | # x.vstack(y) # => Matrix[[1, 2], [3, 4], [5, 6], [7, 8]] 1534 | # 1535 | def vstack(*matrices) 1536 | self.class.vstack(self, *matrices) 1537 | end 1538 | 1539 | #-- 1540 | # DECOMPOSITIONS -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= 1541 | #++ 1542 | 1543 | # 1544 | # Returns the Eigensystem of the matrix; see +EigenvalueDecomposition+. 1545 | # m = Matrix[[1, 2], [3, 4]] 1546 | # v, d, v_inv = m.eigensystem 1547 | # d.diagonal? # => true 1548 | # v.inv == v_inv # => true 1549 | # (v * d * v_inv).round(5) == m # => true 1550 | # 1551 | # This is not supported for Complex matrices 1552 | def eigensystem 1553 | EigenvalueDecomposition.new(self) 1554 | end 1555 | alias_method :eigen, :eigensystem 1556 | 1557 | # 1558 | # Returns the LUP decomposition of the matrix; see +LUPDecomposition+. 1559 | # a = Matrix[[1, 2], [3, 4]] 1560 | # l, u, p = a.lup 1561 | # l.lower_triangular? # => true 1562 | # u.upper_triangular? # => true 1563 | # p.permutation? # => true 1564 | # l * u == p * a # => true 1565 | # a.lup.solve([2, 5]) # => Vector[(1/1), (1/2)] 1566 | # 1567 | def lup 1568 | LUPDecomposition.new(self) 1569 | end 1570 | alias_method :lup_decomposition, :lup 1571 | 1572 | #-- 1573 | # COMPLEX ARITHMETIC -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= 1574 | #++ 1575 | 1576 | # 1577 | # Returns the conjugate of the matrix. 1578 | # Matrix[[Complex(1,2), Complex(0,1), 0], [1, 2, 3]] 1579 | # # => 1+2i i 0 1580 | # # 1 2 3 1581 | # Matrix[[Complex(1,2), Complex(0,1), 0], [1, 2, 3]].conjugate 1582 | # # => 1-2i -i 0 1583 | # # 1 2 3 1584 | # 1585 | def conjugate 1586 | collect(&:conjugate) 1587 | end 1588 | alias_method :conj, :conjugate 1589 | 1590 | # 1591 | # Returns the adjoint of the matrix. 1592 | # 1593 | # Matrix[ [i,1],[2,-i] ].adjoint 1594 | # # => -i 2 1595 | # # 1 i 1596 | # 1597 | def adjoint 1598 | conjugate.transpose 1599 | end 1600 | 1601 | # 1602 | # Returns the imaginary part of the matrix. 1603 | # Matrix[[Complex(1,2), Complex(0,1), 0], [1, 2, 3]] 1604 | # # => 1+2i i 0 1605 | # # 1 2 3 1606 | # Matrix[[Complex(1,2), Complex(0,1), 0], [1, 2, 3]].imaginary 1607 | # # => 2i i 0 1608 | # # 0 0 0 1609 | # 1610 | def imaginary 1611 | collect(&:imaginary) 1612 | end 1613 | alias_method :imag, :imaginary 1614 | 1615 | # 1616 | # Returns the real part of the matrix. 1617 | # Matrix[[Complex(1,2), Complex(0,1), 0], [1, 2, 3]] 1618 | # # => 1+2i i 0 1619 | # # 1 2 3 1620 | # Matrix[[Complex(1,2), Complex(0,1), 0], [1, 2, 3]].real 1621 | # # => 1 0 0 1622 | # # 1 2 3 1623 | # 1624 | def real 1625 | collect(&:real) 1626 | end 1627 | 1628 | # 1629 | # Returns an array containing matrices corresponding to the real and imaginary 1630 | # parts of the matrix 1631 | # 1632 | # m.rect == [m.real, m.imag] # ==> true for all matrices m 1633 | # 1634 | def rect 1635 | [real, imag] 1636 | end 1637 | alias_method :rectangular, :rect 1638 | 1639 | #-- 1640 | # CONVERTING -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 1641 | #++ 1642 | 1643 | # 1644 | # The coerce method provides support for Ruby type coercion. 1645 | # This coercion mechanism is used by Ruby to handle mixed-type 1646 | # numeric operations: it is intended to find a compatible common 1647 | # type between the two operands of the operator. 1648 | # See also Numeric#coerce. 1649 | # 1650 | def coerce(other) 1651 | case other 1652 | when Numeric 1653 | return Scalar.new(other), self 1654 | else 1655 | raise TypeError, "#{self.class} can't be coerced into #{other.class}" 1656 | end 1657 | end 1658 | 1659 | # 1660 | # Returns an array of the row vectors of the matrix. See Vector. 1661 | # 1662 | def row_vectors 1663 | Array.new(row_count) {|i| 1664 | row(i) 1665 | } 1666 | end 1667 | 1668 | # 1669 | # Returns an array of the column vectors of the matrix. See Vector. 1670 | # 1671 | def column_vectors 1672 | Array.new(column_count) {|i| 1673 | column(i) 1674 | } 1675 | end 1676 | 1677 | # 1678 | # Explicit conversion to a Matrix. Returns self 1679 | # 1680 | def to_matrix 1681 | self 1682 | end 1683 | 1684 | # 1685 | # Returns an array of arrays that describe the rows of the matrix. 1686 | # 1687 | def to_a 1688 | @rows.collect(&:dup) 1689 | end 1690 | 1691 | # Deprecated. 1692 | # 1693 | # Use map(&:to_f) 1694 | def elements_to_f 1695 | warn "Matrix#elements_to_f is deprecated, use map(&:to_f)", uplevel: 1 1696 | map(&:to_f) 1697 | end 1698 | 1699 | # Deprecated. 1700 | # 1701 | # Use map(&:to_i) 1702 | def elements_to_i 1703 | warn "Matrix#elements_to_i is deprecated, use map(&:to_i)", uplevel: 1 1704 | map(&:to_i) 1705 | end 1706 | 1707 | # Deprecated. 1708 | # 1709 | # Use map(&:to_r) 1710 | def elements_to_r 1711 | warn "Matrix#elements_to_r is deprecated, use map(&:to_r)", uplevel: 1 1712 | map(&:to_r) 1713 | end 1714 | 1715 | #-- 1716 | # PRINTING -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 1717 | #++ 1718 | 1719 | # 1720 | # Overrides Object#to_s 1721 | # 1722 | def to_s 1723 | if empty? 1724 | "#{self.class}.empty(#{row_count}, #{column_count})" 1725 | else 1726 | "#{self.class}[" + @rows.collect{|row| 1727 | "[" + row.collect{|e| e.to_s}.join(", ") + "]" 1728 | }.join(", ")+"]" 1729 | end 1730 | end 1731 | 1732 | # 1733 | # Overrides Object#inspect 1734 | # 1735 | def inspect 1736 | if empty? 1737 | "#{self.class}.empty(#{row_count}, #{column_count})" 1738 | else 1739 | "#{self.class}#{@rows.inspect}" 1740 | end 1741 | end 1742 | 1743 | # Private helper modules 1744 | 1745 | module ConversionHelper # :nodoc: 1746 | # 1747 | # Converts the obj to an Array. If copy is set to true 1748 | # a copy of obj will be made if necessary. 1749 | # 1750 | private def convert_to_array(obj, copy = false) # :nodoc: 1751 | case obj 1752 | when Array 1753 | copy ? obj.dup : obj 1754 | when Vector 1755 | obj.to_a 1756 | else 1757 | begin 1758 | converted = obj.to_ary 1759 | rescue Exception => e 1760 | raise TypeError, "can't convert #{obj.class} into an Array (#{e.message})" 1761 | end 1762 | raise TypeError, "#{obj.class}#to_ary should return an Array" unless converted.is_a? Array 1763 | converted 1764 | end 1765 | end 1766 | end 1767 | 1768 | extend ConversionHelper 1769 | 1770 | module CoercionHelper # :nodoc: 1771 | # 1772 | # Applies the operator +oper+ with argument +obj+ 1773 | # through coercion of +obj+ 1774 | # 1775 | private def apply_through_coercion(obj, oper) 1776 | coercion = obj.coerce(self) 1777 | raise TypeError unless coercion.is_a?(Array) && coercion.length == 2 1778 | coercion[0].public_send(oper, coercion[1]) 1779 | rescue 1780 | raise TypeError, "#{obj.inspect} can't be coerced into #{self.class}" 1781 | end 1782 | 1783 | # 1784 | # Helper method to coerce a value into a specific class. 1785 | # Raises a TypeError if the coercion fails or the returned value 1786 | # is not of the right class. 1787 | # (from Rubinius) 1788 | # 1789 | def self.coerce_to(obj, cls, meth) # :nodoc: 1790 | return obj if obj.kind_of?(cls) 1791 | raise TypeError, "Expected a #{cls} but got a #{obj.class}" unless obj.respond_to? meth 1792 | begin 1793 | ret = obj.__send__(meth) 1794 | rescue Exception => e 1795 | raise TypeError, "Coercion error: #{obj.inspect}.#{meth} => #{cls} failed:\n" \ 1796 | "(#{e.message})" 1797 | end 1798 | raise TypeError, "Coercion error: obj.#{meth} did NOT return a #{cls} (was #{ret.class})" unless ret.kind_of? cls 1799 | ret 1800 | end 1801 | 1802 | def self.coerce_to_int(obj) 1803 | coerce_to(obj, Integer, :to_int) 1804 | end 1805 | 1806 | def self.coerce_to_matrix(obj) 1807 | coerce_to(obj, Matrix, :to_matrix) 1808 | end 1809 | 1810 | # Returns `nil` for non Ranges 1811 | # Checks range validity, return canonical range with 0 <= begin <= end < count 1812 | def self.check_range(val, count, kind) 1813 | canonical = (val.begin + (val.begin < 0 ? count : 0)).. 1814 | (val.end ? val.end + (val.end < 0 ? count : 0) - (val.exclude_end? ? 1 : 0) 1815 | : count - 1) 1816 | unless 0 <= canonical.begin && canonical.begin <= canonical.end && canonical.end < count 1817 | raise IndexError, "given range #{val} is outside of #{kind} dimensions: 0...#{count}" 1818 | end 1819 | canonical 1820 | end 1821 | 1822 | def self.check_int(val, count, kind) 1823 | val = CoercionHelper.coerce_to_int(val) 1824 | if val >= count || val < -count 1825 | raise IndexError, "given #{kind} #{val} is outside of #{-count}...#{count}" 1826 | end 1827 | val 1828 | end 1829 | end 1830 | 1831 | include CoercionHelper 1832 | 1833 | # Private CLASS 1834 | 1835 | class Scalar < Numeric # :nodoc: 1836 | include ExceptionForMatrix 1837 | include CoercionHelper 1838 | 1839 | def initialize(value) 1840 | @value = value 1841 | end 1842 | 1843 | # ARITHMETIC 1844 | def +(other) 1845 | case other 1846 | when Numeric 1847 | Scalar.new(@value + other) 1848 | when Vector, Matrix 1849 | raise ErrOperationNotDefined, ["+", @value.class, other.class] 1850 | else 1851 | apply_through_coercion(other, __method__) 1852 | end 1853 | end 1854 | 1855 | def -(other) 1856 | case other 1857 | when Numeric 1858 | Scalar.new(@value - other) 1859 | when Vector, Matrix 1860 | raise ErrOperationNotDefined, ["-", @value.class, other.class] 1861 | else 1862 | apply_through_coercion(other, __method__) 1863 | end 1864 | end 1865 | 1866 | def *(other) 1867 | case other 1868 | when Numeric 1869 | Scalar.new(@value * other) 1870 | when Vector, Matrix 1871 | other.collect{|e| @value * e} 1872 | else 1873 | apply_through_coercion(other, __method__) 1874 | end 1875 | end 1876 | 1877 | def /(other) 1878 | case other 1879 | when Numeric 1880 | Scalar.new(@value / other) 1881 | when Vector 1882 | raise ErrOperationNotDefined, ["/", @value.class, other.class] 1883 | when Matrix 1884 | self * other.inverse 1885 | else 1886 | apply_through_coercion(other, __method__) 1887 | end 1888 | end 1889 | 1890 | def **(other) 1891 | case other 1892 | when Numeric 1893 | Scalar.new(@value ** other) 1894 | when Vector 1895 | raise ErrOperationNotDefined, ["**", @value.class, other.class] 1896 | when Matrix 1897 | #other.powered_by(self) 1898 | raise ErrOperationNotImplemented, ["**", @value.class, other.class] 1899 | else 1900 | apply_through_coercion(other, __method__) 1901 | end 1902 | end 1903 | end 1904 | 1905 | end 1906 | 1907 | 1908 | # 1909 | # The +Vector+ class represents a mathematical vector, which is useful in its own right, and 1910 | # also constitutes a row or column of a Matrix. 1911 | # 1912 | # == Method Catalogue 1913 | # 1914 | # To create a Vector: 1915 | # * Vector.[](*array) 1916 | # * Vector.elements(array, copy = true) 1917 | # * Vector.basis(size: n, index: k) 1918 | # * Vector.zero(n) 1919 | # 1920 | # To access elements: 1921 | # * #[](i) 1922 | # 1923 | # To set elements: 1924 | # * #[]=(i, v) 1925 | # 1926 | # To enumerate the elements: 1927 | # * #each2(v) 1928 | # * #collect2(v) 1929 | # 1930 | # Properties of vectors: 1931 | # * #angle_with(v) 1932 | # * Vector.independent?(*vs) 1933 | # * #independent?(*vs) 1934 | # * #zero? 1935 | # 1936 | # Vector arithmetic: 1937 | # * #*(x) "is matrix or number" 1938 | # * #+(v) 1939 | # * #-(v) 1940 | # * #/(v) 1941 | # * #+@ 1942 | # * #-@ 1943 | # 1944 | # Vector functions: 1945 | # * #inner_product(v), #dot(v) 1946 | # * #cross_product(v), #cross(v) 1947 | # * #collect 1948 | # * #collect! 1949 | # * #magnitude 1950 | # * #map 1951 | # * #map! 1952 | # * #map2(v) 1953 | # * #norm 1954 | # * #normalize 1955 | # * #r 1956 | # * #round 1957 | # * #size 1958 | # 1959 | # Conversion to other data types: 1960 | # * #covector 1961 | # * #to_a 1962 | # * #coerce(other) 1963 | # 1964 | # String representations: 1965 | # * #to_s 1966 | # * #inspect 1967 | # 1968 | class Vector 1969 | include ExceptionForMatrix 1970 | include Enumerable 1971 | include Matrix::CoercionHelper 1972 | extend Matrix::ConversionHelper 1973 | #INSTANCE CREATION 1974 | 1975 | private_class_method :new 1976 | attr_reader :elements 1977 | protected :elements 1978 | 1979 | # 1980 | # Creates a Vector from a list of elements. 1981 | # Vector[7, 4, ...] 1982 | # 1983 | def Vector.[](*array) 1984 | new convert_to_array(array, false) 1985 | end 1986 | 1987 | # 1988 | # Creates a vector from an Array. The optional second argument specifies 1989 | # whether the array itself or a copy is used internally. 1990 | # 1991 | def Vector.elements(array, copy = true) 1992 | new convert_to_array(array, copy) 1993 | end 1994 | 1995 | # 1996 | # Returns a standard basis +n+-vector, where k is the index. 1997 | # 1998 | # Vector.basis(size:, index:) # => Vector[0, 1, 0] 1999 | # 2000 | def Vector.basis(size:, index:) 2001 | raise ArgumentError, "invalid size (#{size} for 1..)" if size < 1 2002 | raise ArgumentError, "invalid index (#{index} for 0...#{size})" unless 0 <= index && index < size 2003 | array = Array.new(size, 0) 2004 | array[index] = 1 2005 | new convert_to_array(array, false) 2006 | end 2007 | 2008 | # 2009 | # Return a zero vector. 2010 | # 2011 | # Vector.zero(3) # => Vector[0, 0, 0] 2012 | # 2013 | def Vector.zero(size) 2014 | raise ArgumentError, "invalid size (#{size} for 0..)" if size < 0 2015 | array = Array.new(size, 0) 2016 | new convert_to_array(array, false) 2017 | end 2018 | 2019 | # 2020 | # Vector.new is private; use Vector[] or Vector.elements to create. 2021 | # 2022 | def initialize(array) 2023 | # No checking is done at this point. 2024 | @elements = array 2025 | end 2026 | 2027 | # ACCESSING 2028 | 2029 | # 2030 | # :call-seq: 2031 | # vector[range] 2032 | # vector[integer] 2033 | # 2034 | # Returns element or elements of the vector. 2035 | # 2036 | def [](i) 2037 | @elements[i] 2038 | end 2039 | alias element [] 2040 | alias component [] 2041 | 2042 | # 2043 | # :call-seq: 2044 | # vector[range] = new_vector 2045 | # vector[range] = row_matrix 2046 | # vector[range] = new_element 2047 | # vector[integer] = new_element 2048 | # 2049 | # Set element or elements of vector. 2050 | # 2051 | def []=(i, v) 2052 | raise FrozenError, "can't modify frozen Vector" if frozen? 2053 | if i.is_a?(Range) 2054 | range = Matrix::CoercionHelper.check_range(i, size, :vector) 2055 | set_range(range, v) 2056 | else 2057 | index = Matrix::CoercionHelper.check_int(i, size, :index) 2058 | set_value(index, v) 2059 | end 2060 | end 2061 | alias set_element []= 2062 | alias set_component []= 2063 | private :set_element, :set_component 2064 | 2065 | private def set_value(index, value) 2066 | @elements[index] = value 2067 | end 2068 | 2069 | private def set_range(range, value) 2070 | if value.is_a?(Vector) 2071 | raise ArgumentError, "vector to be set has wrong size" unless range.size == value.size 2072 | @elements[range] = value.elements 2073 | elsif value.is_a?(Matrix) 2074 | raise ErrDimensionMismatch unless value.row_count == 1 2075 | @elements[range] = value.row(0).elements 2076 | else 2077 | @elements[range] = Array.new(range.size, value) 2078 | end 2079 | end 2080 | 2081 | # Returns a vector with entries rounded to the given precision 2082 | # (see Float#round) 2083 | # 2084 | def round(ndigits=0) 2085 | map{|e| e.round(ndigits)} 2086 | end 2087 | 2088 | # 2089 | # Returns the number of elements in the vector. 2090 | # 2091 | def size 2092 | @elements.size 2093 | end 2094 | 2095 | #-- 2096 | # ENUMERATIONS -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 2097 | #++ 2098 | 2099 | # 2100 | # Iterate over the elements of this vector 2101 | # 2102 | def each(&block) 2103 | return to_enum(:each) unless block_given? 2104 | @elements.each(&block) 2105 | self 2106 | end 2107 | 2108 | # 2109 | # Iterate over the elements of this vector and +v+ in conjunction. 2110 | # 2111 | def each2(v) # :yield: e1, e2 2112 | raise TypeError, "Integer is not like Vector" if v.kind_of?(Integer) 2113 | raise ErrDimensionMismatch if size != v.size 2114 | return to_enum(:each2, v) unless block_given? 2115 | size.times do |i| 2116 | yield @elements[i], v[i] 2117 | end 2118 | self 2119 | end 2120 | 2121 | # 2122 | # Collects (as in Enumerable#collect) over the elements of this vector and +v+ 2123 | # in conjunction. 2124 | # 2125 | def collect2(v) # :yield: e1, e2 2126 | raise TypeError, "Integer is not like Vector" if v.kind_of?(Integer) 2127 | raise ErrDimensionMismatch if size != v.size 2128 | return to_enum(:collect2, v) unless block_given? 2129 | Array.new(size) do |i| 2130 | yield @elements[i], v[i] 2131 | end 2132 | end 2133 | 2134 | #-- 2135 | # PROPERTIES -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 2136 | #++ 2137 | 2138 | # 2139 | # Returns whether all of vectors are linearly independent. 2140 | # 2141 | # Vector.independent?(Vector[1,0], Vector[0,1]) 2142 | # # => true 2143 | # 2144 | # Vector.independent?(Vector[1,2], Vector[2,4]) 2145 | # # => false 2146 | # 2147 | def Vector.independent?(*vs) 2148 | vs.each do |v| 2149 | raise TypeError, "expected Vector, got #{v.class}" unless v.is_a?(Vector) 2150 | raise ErrDimensionMismatch unless v.size == vs.first.size 2151 | end 2152 | return false if vs.count > vs.first.size 2153 | Matrix[*vs].rank.eql?(vs.count) 2154 | end 2155 | 2156 | # 2157 | # Returns whether all of vectors are linearly independent. 2158 | # 2159 | # Vector[1,0].independent?(Vector[0,1]) 2160 | # # => true 2161 | # 2162 | # Vector[1,2].independent?(Vector[2,4]) 2163 | # # => false 2164 | # 2165 | def independent?(*vs) 2166 | self.class.independent?(self, *vs) 2167 | end 2168 | 2169 | # 2170 | # Returns whether all elements are zero. 2171 | # 2172 | def zero? 2173 | all?(&:zero?) 2174 | end 2175 | 2176 | # 2177 | # Makes the matrix frozen and Ractor-shareable 2178 | # 2179 | def freeze 2180 | @elements.freeze 2181 | super 2182 | end 2183 | 2184 | # 2185 | # Called for dup & clone. 2186 | # 2187 | private def initialize_copy(v) 2188 | super 2189 | @elements = @elements.dup unless frozen? 2190 | end 2191 | 2192 | 2193 | #-- 2194 | # COMPARING -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 2195 | #++ 2196 | 2197 | # 2198 | # Returns whether the two vectors have the same elements in the same order. 2199 | # 2200 | def ==(other) 2201 | return false unless Vector === other 2202 | @elements == other.elements 2203 | end 2204 | 2205 | def eql?(other) 2206 | return false unless Vector === other 2207 | @elements.eql? other.elements 2208 | end 2209 | 2210 | # 2211 | # Returns a hash-code for the vector. 2212 | # 2213 | def hash 2214 | @elements.hash 2215 | end 2216 | 2217 | #-- 2218 | # ARITHMETIC -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 2219 | #++ 2220 | 2221 | # 2222 | # Multiplies the vector by +x+, where +x+ is a number or a matrix. 2223 | # 2224 | def *(x) 2225 | case x 2226 | when Numeric 2227 | els = @elements.collect{|e| e * x} 2228 | self.class.elements(els, false) 2229 | when Matrix 2230 | Matrix.column_vector(self) * x 2231 | when Vector 2232 | raise ErrOperationNotDefined, ["*", self.class, x.class] 2233 | else 2234 | apply_through_coercion(x, __method__) 2235 | end 2236 | end 2237 | 2238 | # 2239 | # Vector addition. 2240 | # 2241 | def +(v) 2242 | case v 2243 | when Vector 2244 | raise ErrDimensionMismatch if size != v.size 2245 | els = collect2(v) {|v1, v2| 2246 | v1 + v2 2247 | } 2248 | self.class.elements(els, false) 2249 | when Matrix 2250 | Matrix.column_vector(self) + v 2251 | else 2252 | apply_through_coercion(v, __method__) 2253 | end 2254 | end 2255 | 2256 | # 2257 | # Vector subtraction. 2258 | # 2259 | def -(v) 2260 | case v 2261 | when Vector 2262 | raise ErrDimensionMismatch if size != v.size 2263 | els = collect2(v) {|v1, v2| 2264 | v1 - v2 2265 | } 2266 | self.class.elements(els, false) 2267 | when Matrix 2268 | Matrix.column_vector(self) - v 2269 | else 2270 | apply_through_coercion(v, __method__) 2271 | end 2272 | end 2273 | 2274 | # 2275 | # Vector division. 2276 | # 2277 | def /(x) 2278 | case x 2279 | when Numeric 2280 | els = @elements.collect{|e| e / x} 2281 | self.class.elements(els, false) 2282 | when Matrix, Vector 2283 | raise ErrOperationNotDefined, ["/", self.class, x.class] 2284 | else 2285 | apply_through_coercion(x, __method__) 2286 | end 2287 | end 2288 | 2289 | def +@ 2290 | self 2291 | end 2292 | 2293 | def -@ 2294 | collect {|e| -e } 2295 | end 2296 | 2297 | #-- 2298 | # VECTOR FUNCTIONS -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 2299 | #++ 2300 | 2301 | # 2302 | # Returns the inner product of this vector with the other. 2303 | # Vector[4,7].inner_product Vector[10,1] # => 47 2304 | # 2305 | def inner_product(v) 2306 | raise ErrDimensionMismatch if size != v.size 2307 | 2308 | p = 0 2309 | each2(v) {|v1, v2| 2310 | p += v1 * v2.conj 2311 | } 2312 | p 2313 | end 2314 | alias_method :dot, :inner_product 2315 | 2316 | # 2317 | # Returns the cross product of this vector with the others. 2318 | # Vector[1, 0, 0].cross_product Vector[0, 1, 0] # => Vector[0, 0, 1] 2319 | # 2320 | # It is generalized to other dimensions to return a vector perpendicular 2321 | # to the arguments. 2322 | # Vector[1, 2].cross_product # => Vector[-2, 1] 2323 | # Vector[1, 0, 0, 0].cross_product( 2324 | # Vector[0, 1, 0, 0], 2325 | # Vector[0, 0, 1, 0] 2326 | # ) #=> Vector[0, 0, 0, 1] 2327 | # 2328 | def cross_product(*vs) 2329 | raise ErrOperationNotDefined, "cross product is not defined on vectors of dimension #{size}" unless size >= 2 2330 | raise ArgumentError, "wrong number of arguments (#{vs.size} for #{size - 2})" unless vs.size == size - 2 2331 | vs.each do |v| 2332 | raise TypeError, "expected Vector, got #{v.class}" unless v.is_a? Vector 2333 | raise ErrDimensionMismatch unless v.size == size 2334 | end 2335 | case size 2336 | when 2 2337 | Vector[-@elements[1], @elements[0]] 2338 | when 3 2339 | v = vs[0] 2340 | Vector[ v[2]*@elements[1] - v[1]*@elements[2], 2341 | v[0]*@elements[2] - v[2]*@elements[0], 2342 | v[1]*@elements[0] - v[0]*@elements[1] ] 2343 | else 2344 | rows = self, *vs, Array.new(size) {|i| Vector.basis(size: size, index: i) } 2345 | Matrix.rows(rows).laplace_expansion(row: size - 1) 2346 | end 2347 | end 2348 | alias_method :cross, :cross_product 2349 | 2350 | # 2351 | # Like Array#collect. 2352 | # 2353 | def collect(&block) # :yield: e 2354 | return to_enum(:collect) unless block_given? 2355 | els = @elements.collect(&block) 2356 | self.class.elements(els, false) 2357 | end 2358 | alias_method :map, :collect 2359 | 2360 | # 2361 | # Like Array#collect! 2362 | # 2363 | def collect!(&block) 2364 | return to_enum(:collect!) unless block_given? 2365 | raise FrozenError, "can't modify frozen Vector" if frozen? 2366 | @elements.collect!(&block) 2367 | self 2368 | end 2369 | alias map! collect! 2370 | 2371 | # 2372 | # Returns the modulus (Pythagorean distance) of the vector. 2373 | # Vector[5,8,2].r # => 9.643650761 2374 | # 2375 | def magnitude 2376 | Math.sqrt(@elements.inject(0) {|v, e| v + e.abs2}) 2377 | end 2378 | alias_method :r, :magnitude 2379 | alias_method :norm, :magnitude 2380 | 2381 | # 2382 | # Like Vector#collect2, but returns a Vector instead of an Array. 2383 | # 2384 | def map2(v, &block) # :yield: e1, e2 2385 | return to_enum(:map2, v) unless block_given? 2386 | els = collect2(v, &block) 2387 | self.class.elements(els, false) 2388 | end 2389 | 2390 | class ZeroVectorError < StandardError 2391 | end 2392 | # 2393 | # Returns a new vector with the same direction but with norm 1. 2394 | # v = Vector[5,8,2].normalize 2395 | # # => Vector[0.5184758473652127, 0.8295613557843402, 0.20739033894608505] 2396 | # v.norm # => 1.0 2397 | # 2398 | def normalize 2399 | n = magnitude 2400 | raise ZeroVectorError, "Zero vectors can not be normalized" if n == 0 2401 | self / n 2402 | end 2403 | 2404 | # 2405 | # Returns an angle with another vector. Result is within the [0..Math::PI]. 2406 | # Vector[1,0].angle_with(Vector[0,1]) 2407 | # # => Math::PI / 2 2408 | # 2409 | def angle_with(v) 2410 | raise TypeError, "Expected a Vector, got a #{v.class}" unless v.is_a?(Vector) 2411 | raise ErrDimensionMismatch if size != v.size 2412 | prod = magnitude * v.magnitude 2413 | raise ZeroVectorError, "Can't get angle of zero vector" if prod == 0 2414 | dot = inner_product(v) 2415 | if dot.abs >= prod 2416 | dot.positive? ? 0 : Math::PI 2417 | else 2418 | Math.acos(dot / prod) 2419 | end 2420 | end 2421 | 2422 | #-- 2423 | # CONVERTING 2424 | #++ 2425 | 2426 | # 2427 | # Creates a single-row matrix from this vector. 2428 | # 2429 | def covector 2430 | Matrix.row_vector(self) 2431 | end 2432 | 2433 | # 2434 | # Returns the elements of the vector in an array. 2435 | # 2436 | def to_a 2437 | @elements.dup 2438 | end 2439 | 2440 | # 2441 | # Return a single-column matrix from this vector 2442 | # 2443 | def to_matrix 2444 | Matrix.column_vector(self) 2445 | end 2446 | 2447 | def elements_to_f 2448 | warn "Vector#elements_to_f is deprecated", uplevel: 1 2449 | map(&:to_f) 2450 | end 2451 | 2452 | def elements_to_i 2453 | warn "Vector#elements_to_i is deprecated", uplevel: 1 2454 | map(&:to_i) 2455 | end 2456 | 2457 | def elements_to_r 2458 | warn "Vector#elements_to_r is deprecated", uplevel: 1 2459 | map(&:to_r) 2460 | end 2461 | 2462 | # 2463 | # The coerce method provides support for Ruby type coercion. 2464 | # This coercion mechanism is used by Ruby to handle mixed-type 2465 | # numeric operations: it is intended to find a compatible common 2466 | # type between the two operands of the operator. 2467 | # See also Numeric#coerce. 2468 | # 2469 | def coerce(other) 2470 | case other 2471 | when Numeric 2472 | return Matrix::Scalar.new(other), self 2473 | else 2474 | raise TypeError, "#{self.class} can't be coerced into #{other.class}" 2475 | end 2476 | end 2477 | 2478 | #-- 2479 | # PRINTING -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 2480 | #++ 2481 | 2482 | # 2483 | # Overrides Object#to_s 2484 | # 2485 | def to_s 2486 | "Vector[" + @elements.join(", ") + "]" 2487 | end 2488 | 2489 | # 2490 | # Overrides Object#inspect 2491 | # 2492 | def inspect 2493 | "Vector" + @elements.inspect 2494 | end 2495 | end 2496 | -------------------------------------------------------------------------------- /lib/matrix/eigenvalue_decomposition.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | class Matrix 3 | # Adapted from JAMA: http://math.nist.gov/javanumerics/jama/ 4 | # Like JAMA, this code does not support Complex matrices inputs. 5 | 6 | # Eigenvalues and eigenvectors of a real matrix. 7 | # 8 | # Computes the eigenvalues and eigenvectors of a matrix A. 9 | # 10 | # If A is diagonalizable, this provides matrices V and D 11 | # such that A = V*D*V.inv, where D is the diagonal matrix with entries 12 | # equal to the eigenvalues and V is formed by the eigenvectors. 13 | # 14 | # If A is symmetric, then V is orthogonal and thus A = V*D*V.t 15 | 16 | class EigenvalueDecomposition 17 | 18 | # Constructs the eigenvalue decomposition for a square matrix +A+ 19 | # 20 | def initialize(a) 21 | # @d, @e: Arrays for internal storage of eigenvalues. 22 | # @v: Array for internal storage of eigenvectors. 23 | # @h: Array for internal storage of nonsymmetric Hessenberg form. 24 | raise TypeError, "Expected Matrix but got #{a.class}" unless a.is_a?(Matrix) 25 | @size = a.row_count 26 | @d = Array.new(@size, 0) 27 | @e = Array.new(@size, 0) 28 | 29 | if (@symmetric = a.symmetric?) 30 | @v = a.to_a 31 | tridiagonalize 32 | diagonalize 33 | else 34 | @v = Array.new(@size) { Array.new(@size, 0) } 35 | @h = a.to_a 36 | @ort = Array.new(@size, 0) 37 | reduce_to_hessenberg 38 | hessenberg_to_real_schur 39 | end 40 | end 41 | 42 | # Returns the eigenvector matrix +V+ 43 | # 44 | def eigenvector_matrix 45 | Matrix.send(:new, build_eigenvectors.transpose) 46 | end 47 | alias_method :v, :eigenvector_matrix 48 | 49 | # Returns the inverse of the eigenvector matrix +V+ 50 | # 51 | def eigenvector_matrix_inv 52 | r = Matrix.send(:new, build_eigenvectors) 53 | r = r.transpose.inverse unless @symmetric 54 | r 55 | end 56 | alias_method :v_inv, :eigenvector_matrix_inv 57 | 58 | # Returns the eigenvalues in an array 59 | # 60 | def eigenvalues 61 | values = @d.dup 62 | @e.each_with_index{|imag, i| values[i] = Complex(values[i], imag) unless imag == 0} 63 | values 64 | end 65 | 66 | # Returns an array of the eigenvectors 67 | # 68 | def eigenvectors 69 | build_eigenvectors.map{|ev| Vector.send(:new, ev)} 70 | end 71 | 72 | # Returns the block diagonal eigenvalue matrix +D+ 73 | # 74 | def eigenvalue_matrix 75 | Matrix.diagonal(*eigenvalues) 76 | end 77 | alias_method :d, :eigenvalue_matrix 78 | 79 | # Returns [eigenvector_matrix, eigenvalue_matrix, eigenvector_matrix_inv] 80 | # 81 | def to_ary 82 | [v, d, v_inv] 83 | end 84 | alias_method :to_a, :to_ary 85 | 86 | 87 | private def build_eigenvectors 88 | # JAMA stores complex eigenvectors in a strange way 89 | # See http://web.archive.org/web/20111016032731/http://cio.nist.gov/esd/emaildir/lists/jama/msg01021.html 90 | @e.each_with_index.map do |imag, i| 91 | if imag == 0 92 | Array.new(@size){|j| @v[j][i]} 93 | elsif imag > 0 94 | Array.new(@size){|j| Complex(@v[j][i], @v[j][i+1])} 95 | else 96 | Array.new(@size){|j| Complex(@v[j][i-1], -@v[j][i])} 97 | end 98 | end 99 | end 100 | 101 | # Complex scalar division. 102 | 103 | private def cdiv(xr, xi, yr, yi) 104 | if (yr.abs > yi.abs) 105 | r = yi/yr 106 | d = yr + r*yi 107 | [(xr + r*xi)/d, (xi - r*xr)/d] 108 | else 109 | r = yr/yi 110 | d = yi + r*yr 111 | [(r*xr + xi)/d, (r*xi - xr)/d] 112 | end 113 | end 114 | 115 | 116 | # Symmetric Householder reduction to tridiagonal form. 117 | 118 | private def tridiagonalize 119 | 120 | # This is derived from the Algol procedures tred2 by 121 | # Bowdler, Martin, Reinsch, and Wilkinson, Handbook for 122 | # Auto. Comp., Vol.ii-Linear Algebra, and the corresponding 123 | # Fortran subroutine in EISPACK. 124 | 125 | @size.times do |j| 126 | @d[j] = @v[@size-1][j] 127 | end 128 | 129 | # Householder reduction to tridiagonal form. 130 | 131 | (@size-1).downto(0+1) do |i| 132 | 133 | # Scale to avoid under/overflow. 134 | 135 | scale = 0.0 136 | h = 0.0 137 | i.times do |k| 138 | scale = scale + @d[k].abs 139 | end 140 | if (scale == 0.0) 141 | @e[i] = @d[i-1] 142 | i.times do |j| 143 | @d[j] = @v[i-1][j] 144 | @v[i][j] = 0.0 145 | @v[j][i] = 0.0 146 | end 147 | else 148 | 149 | # Generate Householder vector. 150 | 151 | i.times do |k| 152 | @d[k] /= scale 153 | h += @d[k] * @d[k] 154 | end 155 | f = @d[i-1] 156 | g = Math.sqrt(h) 157 | if (f > 0) 158 | g = -g 159 | end 160 | @e[i] = scale * g 161 | h -= f * g 162 | @d[i-1] = f - g 163 | i.times do |j| 164 | @e[j] = 0.0 165 | end 166 | 167 | # Apply similarity transformation to remaining columns. 168 | 169 | i.times do |j| 170 | f = @d[j] 171 | @v[j][i] = f 172 | g = @e[j] + @v[j][j] * f 173 | (j+1).upto(i-1) do |k| 174 | g += @v[k][j] * @d[k] 175 | @e[k] += @v[k][j] * f 176 | end 177 | @e[j] = g 178 | end 179 | f = 0.0 180 | i.times do |j| 181 | @e[j] /= h 182 | f += @e[j] * @d[j] 183 | end 184 | hh = f / (h + h) 185 | i.times do |j| 186 | @e[j] -= hh * @d[j] 187 | end 188 | i.times do |j| 189 | f = @d[j] 190 | g = @e[j] 191 | j.upto(i-1) do |k| 192 | @v[k][j] -= (f * @e[k] + g * @d[k]) 193 | end 194 | @d[j] = @v[i-1][j] 195 | @v[i][j] = 0.0 196 | end 197 | end 198 | @d[i] = h 199 | end 200 | 201 | # Accumulate transformations. 202 | 203 | 0.upto(@size-1-1) do |i| 204 | @v[@size-1][i] = @v[i][i] 205 | @v[i][i] = 1.0 206 | h = @d[i+1] 207 | if (h != 0.0) 208 | 0.upto(i) do |k| 209 | @d[k] = @v[k][i+1] / h 210 | end 211 | 0.upto(i) do |j| 212 | g = 0.0 213 | 0.upto(i) do |k| 214 | g += @v[k][i+1] * @v[k][j] 215 | end 216 | 0.upto(i) do |k| 217 | @v[k][j] -= g * @d[k] 218 | end 219 | end 220 | end 221 | 0.upto(i) do |k| 222 | @v[k][i+1] = 0.0 223 | end 224 | end 225 | @size.times do |j| 226 | @d[j] = @v[@size-1][j] 227 | @v[@size-1][j] = 0.0 228 | end 229 | @v[@size-1][@size-1] = 1.0 230 | @e[0] = 0.0 231 | end 232 | 233 | 234 | # Symmetric tridiagonal QL algorithm. 235 | 236 | private def diagonalize 237 | # This is derived from the Algol procedures tql2, by 238 | # Bowdler, Martin, Reinsch, and Wilkinson, Handbook for 239 | # Auto. Comp., Vol.ii-Linear Algebra, and the corresponding 240 | # Fortran subroutine in EISPACK. 241 | 242 | 1.upto(@size-1) do |i| 243 | @e[i-1] = @e[i] 244 | end 245 | @e[@size-1] = 0.0 246 | 247 | f = 0.0 248 | tst1 = 0.0 249 | eps = Float::EPSILON 250 | @size.times do |l| 251 | 252 | # Find small subdiagonal element 253 | 254 | tst1 = [tst1, @d[l].abs + @e[l].abs].max 255 | m = l 256 | while (m < @size) do 257 | if (@e[m].abs <= eps*tst1) 258 | break 259 | end 260 | m+=1 261 | end 262 | 263 | # If m == l, @d[l] is an eigenvalue, 264 | # otherwise, iterate. 265 | 266 | if (m > l) 267 | iter = 0 268 | begin 269 | iter = iter + 1 # (Could check iteration count here.) 270 | 271 | # Compute implicit shift 272 | 273 | g = @d[l] 274 | p = (@d[l+1] - g) / (2.0 * @e[l]) 275 | r = Math.hypot(p, 1.0) 276 | if (p < 0) 277 | r = -r 278 | end 279 | @d[l] = @e[l] / (p + r) 280 | @d[l+1] = @e[l] * (p + r) 281 | dl1 = @d[l+1] 282 | h = g - @d[l] 283 | (l+2).upto(@size-1) do |i| 284 | @d[i] -= h 285 | end 286 | f += h 287 | 288 | # Implicit QL transformation. 289 | 290 | p = @d[m] 291 | c = 1.0 292 | c2 = c 293 | c3 = c 294 | el1 = @e[l+1] 295 | s = 0.0 296 | s2 = 0.0 297 | (m-1).downto(l) do |i| 298 | c3 = c2 299 | c2 = c 300 | s2 = s 301 | g = c * @e[i] 302 | h = c * p 303 | r = Math.hypot(p, @e[i]) 304 | @e[i+1] = s * r 305 | s = @e[i] / r 306 | c = p / r 307 | p = c * @d[i] - s * g 308 | @d[i+1] = h + s * (c * g + s * @d[i]) 309 | 310 | # Accumulate transformation. 311 | 312 | @size.times do |k| 313 | h = @v[k][i+1] 314 | @v[k][i+1] = s * @v[k][i] + c * h 315 | @v[k][i] = c * @v[k][i] - s * h 316 | end 317 | end 318 | p = -s * s2 * c3 * el1 * @e[l] / dl1 319 | @e[l] = s * p 320 | @d[l] = c * p 321 | 322 | # Check for convergence. 323 | 324 | end while (@e[l].abs > eps*tst1) 325 | end 326 | @d[l] = @d[l] + f 327 | @e[l] = 0.0 328 | end 329 | 330 | # Sort eigenvalues and corresponding vectors. 331 | 332 | 0.upto(@size-2) do |i| 333 | k = i 334 | p = @d[i] 335 | (i+1).upto(@size-1) do |j| 336 | if (@d[j] < p) 337 | k = j 338 | p = @d[j] 339 | end 340 | end 341 | if (k != i) 342 | @d[k] = @d[i] 343 | @d[i] = p 344 | @size.times do |j| 345 | p = @v[j][i] 346 | @v[j][i] = @v[j][k] 347 | @v[j][k] = p 348 | end 349 | end 350 | end 351 | end 352 | 353 | # Nonsymmetric reduction to Hessenberg form. 354 | 355 | private def reduce_to_hessenberg 356 | # This is derived from the Algol procedures orthes and ortran, 357 | # by Martin and Wilkinson, Handbook for Auto. Comp., 358 | # Vol.ii-Linear Algebra, and the corresponding 359 | # Fortran subroutines in EISPACK. 360 | 361 | low = 0 362 | high = @size-1 363 | 364 | (low+1).upto(high-1) do |m| 365 | 366 | # Scale column. 367 | 368 | scale = 0.0 369 | m.upto(high) do |i| 370 | scale = scale + @h[i][m-1].abs 371 | end 372 | if (scale != 0.0) 373 | 374 | # Compute Householder transformation. 375 | 376 | h = 0.0 377 | high.downto(m) do |i| 378 | @ort[i] = @h[i][m-1]/scale 379 | h += @ort[i] * @ort[i] 380 | end 381 | g = Math.sqrt(h) 382 | if (@ort[m] > 0) 383 | g = -g 384 | end 385 | h -= @ort[m] * g 386 | @ort[m] = @ort[m] - g 387 | 388 | # Apply Householder similarity transformation 389 | # @h = (I-u*u'/h)*@h*(I-u*u')/h) 390 | 391 | m.upto(@size-1) do |j| 392 | f = 0.0 393 | high.downto(m) do |i| 394 | f += @ort[i]*@h[i][j] 395 | end 396 | f = f/h 397 | m.upto(high) do |i| 398 | @h[i][j] -= f*@ort[i] 399 | end 400 | end 401 | 402 | 0.upto(high) do |i| 403 | f = 0.0 404 | high.downto(m) do |j| 405 | f += @ort[j]*@h[i][j] 406 | end 407 | f = f/h 408 | m.upto(high) do |j| 409 | @h[i][j] -= f*@ort[j] 410 | end 411 | end 412 | @ort[m] = scale*@ort[m] 413 | @h[m][m-1] = scale*g 414 | end 415 | end 416 | 417 | # Accumulate transformations (Algol's ortran). 418 | 419 | @size.times do |i| 420 | @size.times do |j| 421 | @v[i][j] = (i == j ? 1.0 : 0.0) 422 | end 423 | end 424 | 425 | (high-1).downto(low+1) do |m| 426 | if (@h[m][m-1] != 0.0) 427 | (m+1).upto(high) do |i| 428 | @ort[i] = @h[i][m-1] 429 | end 430 | m.upto(high) do |j| 431 | g = 0.0 432 | m.upto(high) do |i| 433 | g += @ort[i] * @v[i][j] 434 | end 435 | # Double division avoids possible underflow 436 | g = (g / @ort[m]) / @h[m][m-1] 437 | m.upto(high) do |i| 438 | @v[i][j] += g * @ort[i] 439 | end 440 | end 441 | end 442 | end 443 | end 444 | 445 | # Nonsymmetric reduction from Hessenberg to real Schur form. 446 | 447 | private def hessenberg_to_real_schur 448 | 449 | # This is derived from the Algol procedure hqr2, 450 | # by Martin and Wilkinson, Handbook for Auto. Comp., 451 | # Vol.ii-Linear Algebra, and the corresponding 452 | # Fortran subroutine in EISPACK. 453 | 454 | # Initialize 455 | 456 | nn = @size 457 | n = nn-1 458 | low = 0 459 | high = nn-1 460 | eps = Float::EPSILON 461 | exshift = 0.0 462 | p = q = r = s = z = 0 463 | 464 | # Store roots isolated by balanc and compute matrix norm 465 | 466 | norm = 0.0 467 | nn.times do |i| 468 | if (i < low || i > high) 469 | @d[i] = @h[i][i] 470 | @e[i] = 0.0 471 | end 472 | ([i-1, 0].max).upto(nn-1) do |j| 473 | norm = norm + @h[i][j].abs 474 | end 475 | end 476 | 477 | # Outer loop over eigenvalue index 478 | 479 | iter = 0 480 | while (n >= low) do 481 | 482 | # Look for single small sub-diagonal element 483 | 484 | l = n 485 | while (l > low) do 486 | s = @h[l-1][l-1].abs + @h[l][l].abs 487 | if (s == 0.0) 488 | s = norm 489 | end 490 | if (@h[l][l-1].abs < eps * s) 491 | break 492 | end 493 | l-=1 494 | end 495 | 496 | # Check for convergence 497 | # One root found 498 | 499 | if (l == n) 500 | @h[n][n] = @h[n][n] + exshift 501 | @d[n] = @h[n][n] 502 | @e[n] = 0.0 503 | n-=1 504 | iter = 0 505 | 506 | # Two roots found 507 | 508 | elsif (l == n-1) 509 | w = @h[n][n-1] * @h[n-1][n] 510 | p = (@h[n-1][n-1] - @h[n][n]) / 2.0 511 | q = p * p + w 512 | z = Math.sqrt(q.abs) 513 | @h[n][n] = @h[n][n] + exshift 514 | @h[n-1][n-1] = @h[n-1][n-1] + exshift 515 | x = @h[n][n] 516 | 517 | # Real pair 518 | 519 | if (q >= 0) 520 | if (p >= 0) 521 | z = p + z 522 | else 523 | z = p - z 524 | end 525 | @d[n-1] = x + z 526 | @d[n] = @d[n-1] 527 | if (z != 0.0) 528 | @d[n] = x - w / z 529 | end 530 | @e[n-1] = 0.0 531 | @e[n] = 0.0 532 | x = @h[n][n-1] 533 | s = x.abs + z.abs 534 | p = x / s 535 | q = z / s 536 | r = Math.sqrt(p * p+q * q) 537 | p /= r 538 | q /= r 539 | 540 | # Row modification 541 | 542 | (n-1).upto(nn-1) do |j| 543 | z = @h[n-1][j] 544 | @h[n-1][j] = q * z + p * @h[n][j] 545 | @h[n][j] = q * @h[n][j] - p * z 546 | end 547 | 548 | # Column modification 549 | 550 | 0.upto(n) do |i| 551 | z = @h[i][n-1] 552 | @h[i][n-1] = q * z + p * @h[i][n] 553 | @h[i][n] = q * @h[i][n] - p * z 554 | end 555 | 556 | # Accumulate transformations 557 | 558 | low.upto(high) do |i| 559 | z = @v[i][n-1] 560 | @v[i][n-1] = q * z + p * @v[i][n] 561 | @v[i][n] = q * @v[i][n] - p * z 562 | end 563 | 564 | # Complex pair 565 | 566 | else 567 | @d[n-1] = x + p 568 | @d[n] = x + p 569 | @e[n-1] = z 570 | @e[n] = -z 571 | end 572 | n -= 2 573 | iter = 0 574 | 575 | # No convergence yet 576 | 577 | else 578 | 579 | # Form shift 580 | 581 | x = @h[n][n] 582 | y = 0.0 583 | w = 0.0 584 | if (l < n) 585 | y = @h[n-1][n-1] 586 | w = @h[n][n-1] * @h[n-1][n] 587 | end 588 | 589 | # Wilkinson's original ad hoc shift 590 | 591 | if (iter == 10) 592 | exshift += x 593 | low.upto(n) do |i| 594 | @h[i][i] -= x 595 | end 596 | s = @h[n][n-1].abs + @h[n-1][n-2].abs 597 | x = y = 0.75 * s 598 | w = -0.4375 * s * s 599 | end 600 | 601 | # MATLAB's new ad hoc shift 602 | 603 | if (iter == 30) 604 | s = (y - x) / 2.0 605 | s *= s + w 606 | if (s > 0) 607 | s = Math.sqrt(s) 608 | if (y < x) 609 | s = -s 610 | end 611 | s = x - w / ((y - x) / 2.0 + s) 612 | low.upto(n) do |i| 613 | @h[i][i] -= s 614 | end 615 | exshift += s 616 | x = y = w = 0.964 617 | end 618 | end 619 | 620 | iter = iter + 1 # (Could check iteration count here.) 621 | 622 | # Look for two consecutive small sub-diagonal elements 623 | 624 | m = n-2 625 | while (m >= l) do 626 | z = @h[m][m] 627 | r = x - z 628 | s = y - z 629 | p = (r * s - w) / @h[m+1][m] + @h[m][m+1] 630 | q = @h[m+1][m+1] - z - r - s 631 | r = @h[m+2][m+1] 632 | s = p.abs + q.abs + r.abs 633 | p /= s 634 | q /= s 635 | r /= s 636 | if (m == l) 637 | break 638 | end 639 | if (@h[m][m-1].abs * (q.abs + r.abs) < 640 | eps * (p.abs * (@h[m-1][m-1].abs + z.abs + 641 | @h[m+1][m+1].abs))) 642 | break 643 | end 644 | m-=1 645 | end 646 | 647 | (m+2).upto(n) do |i| 648 | @h[i][i-2] = 0.0 649 | if (i > m+2) 650 | @h[i][i-3] = 0.0 651 | end 652 | end 653 | 654 | # Double QR step involving rows l:n and columns m:n 655 | 656 | m.upto(n-1) do |k| 657 | notlast = (k != n-1) 658 | if (k != m) 659 | p = @h[k][k-1] 660 | q = @h[k+1][k-1] 661 | r = (notlast ? @h[k+2][k-1] : 0.0) 662 | x = p.abs + q.abs + r.abs 663 | next if x == 0 664 | p /= x 665 | q /= x 666 | r /= x 667 | end 668 | s = Math.sqrt(p * p + q * q + r * r) 669 | if (p < 0) 670 | s = -s 671 | end 672 | if (s != 0) 673 | if (k != m) 674 | @h[k][k-1] = -s * x 675 | elsif (l != m) 676 | @h[k][k-1] = -@h[k][k-1] 677 | end 678 | p += s 679 | x = p / s 680 | y = q / s 681 | z = r / s 682 | q /= p 683 | r /= p 684 | 685 | # Row modification 686 | 687 | k.upto(nn-1) do |j| 688 | p = @h[k][j] + q * @h[k+1][j] 689 | if (notlast) 690 | p += r * @h[k+2][j] 691 | @h[k+2][j] = @h[k+2][j] - p * z 692 | end 693 | @h[k][j] = @h[k][j] - p * x 694 | @h[k+1][j] = @h[k+1][j] - p * y 695 | end 696 | 697 | # Column modification 698 | 699 | 0.upto([n, k+3].min) do |i| 700 | p = x * @h[i][k] + y * @h[i][k+1] 701 | if (notlast) 702 | p += z * @h[i][k+2] 703 | @h[i][k+2] = @h[i][k+2] - p * r 704 | end 705 | @h[i][k] = @h[i][k] - p 706 | @h[i][k+1] = @h[i][k+1] - p * q 707 | end 708 | 709 | # Accumulate transformations 710 | 711 | low.upto(high) do |i| 712 | p = x * @v[i][k] + y * @v[i][k+1] 713 | if (notlast) 714 | p += z * @v[i][k+2] 715 | @v[i][k+2] = @v[i][k+2] - p * r 716 | end 717 | @v[i][k] = @v[i][k] - p 718 | @v[i][k+1] = @v[i][k+1] - p * q 719 | end 720 | end # (s != 0) 721 | end # k loop 722 | end # check convergence 723 | end # while (n >= low) 724 | 725 | # Backsubstitute to find vectors of upper triangular form 726 | 727 | if (norm == 0.0) 728 | return 729 | end 730 | 731 | (nn-1).downto(0) do |k| 732 | p = @d[k] 733 | q = @e[k] 734 | 735 | # Real vector 736 | 737 | if (q == 0) 738 | l = k 739 | @h[k][k] = 1.0 740 | (k-1).downto(0) do |i| 741 | w = @h[i][i] - p 742 | r = 0.0 743 | l.upto(k) do |j| 744 | r += @h[i][j] * @h[j][k] 745 | end 746 | if (@e[i] < 0.0) 747 | z = w 748 | s = r 749 | else 750 | l = i 751 | if (@e[i] == 0.0) 752 | if (w != 0.0) 753 | @h[i][k] = -r / w 754 | else 755 | @h[i][k] = -r / (eps * norm) 756 | end 757 | 758 | # Solve real equations 759 | 760 | else 761 | x = @h[i][i+1] 762 | y = @h[i+1][i] 763 | q = (@d[i] - p) * (@d[i] - p) + @e[i] * @e[i] 764 | t = (x * s - z * r) / q 765 | @h[i][k] = t 766 | if (x.abs > z.abs) 767 | @h[i+1][k] = (-r - w * t) / x 768 | else 769 | @h[i+1][k] = (-s - y * t) / z 770 | end 771 | end 772 | 773 | # Overflow control 774 | 775 | t = @h[i][k].abs 776 | if ((eps * t) * t > 1) 777 | i.upto(k) do |j| 778 | @h[j][k] = @h[j][k] / t 779 | end 780 | end 781 | end 782 | end 783 | 784 | # Complex vector 785 | 786 | elsif (q < 0) 787 | l = n-1 788 | 789 | # Last vector component imaginary so matrix is triangular 790 | 791 | if (@h[n][n-1].abs > @h[n-1][n].abs) 792 | @h[n-1][n-1] = q / @h[n][n-1] 793 | @h[n-1][n] = -(@h[n][n] - p) / @h[n][n-1] 794 | else 795 | cdivr, cdivi = cdiv(0.0, -@h[n-1][n], @h[n-1][n-1]-p, q) 796 | @h[n-1][n-1] = cdivr 797 | @h[n-1][n] = cdivi 798 | end 799 | @h[n][n-1] = 0.0 800 | @h[n][n] = 1.0 801 | (n-2).downto(0) do |i| 802 | ra = 0.0 803 | sa = 0.0 804 | l.upto(n) do |j| 805 | ra = ra + @h[i][j] * @h[j][n-1] 806 | sa = sa + @h[i][j] * @h[j][n] 807 | end 808 | w = @h[i][i] - p 809 | 810 | if (@e[i] < 0.0) 811 | z = w 812 | r = ra 813 | s = sa 814 | else 815 | l = i 816 | if (@e[i] == 0) 817 | cdivr, cdivi = cdiv(-ra, -sa, w, q) 818 | @h[i][n-1] = cdivr 819 | @h[i][n] = cdivi 820 | else 821 | 822 | # Solve complex equations 823 | 824 | x = @h[i][i+1] 825 | y = @h[i+1][i] 826 | vr = (@d[i] - p) * (@d[i] - p) + @e[i] * @e[i] - q * q 827 | vi = (@d[i] - p) * 2.0 * q 828 | if (vr == 0.0 && vi == 0.0) 829 | vr = eps * norm * (w.abs + q.abs + 830 | x.abs + y.abs + z.abs) 831 | end 832 | cdivr, cdivi = cdiv(x*r-z*ra+q*sa, x*s-z*sa-q*ra, vr, vi) 833 | @h[i][n-1] = cdivr 834 | @h[i][n] = cdivi 835 | if (x.abs > (z.abs + q.abs)) 836 | @h[i+1][n-1] = (-ra - w * @h[i][n-1] + q * @h[i][n]) / x 837 | @h[i+1][n] = (-sa - w * @h[i][n] - q * @h[i][n-1]) / x 838 | else 839 | cdivr, cdivi = cdiv(-r-y*@h[i][n-1], -s-y*@h[i][n], z, q) 840 | @h[i+1][n-1] = cdivr 841 | @h[i+1][n] = cdivi 842 | end 843 | end 844 | 845 | # Overflow control 846 | 847 | t = [@h[i][n-1].abs, @h[i][n].abs].max 848 | if ((eps * t) * t > 1) 849 | i.upto(n) do |j| 850 | @h[j][n-1] = @h[j][n-1] / t 851 | @h[j][n] = @h[j][n] / t 852 | end 853 | end 854 | end 855 | end 856 | end 857 | end 858 | 859 | # Vectors of isolated roots 860 | 861 | nn.times do |i| 862 | if (i < low || i > high) 863 | i.upto(nn-1) do |j| 864 | @v[i][j] = @h[i][j] 865 | end 866 | end 867 | end 868 | 869 | # Back transformation to get eigenvectors of original matrix 870 | 871 | (nn-1).downto(low) do |j| 872 | low.upto(high) do |i| 873 | z = 0.0 874 | low.upto([j, high].min) do |k| 875 | z += @v[i][k] * @h[k][j] 876 | end 877 | @v[i][j] = z 878 | end 879 | end 880 | end 881 | 882 | end 883 | end 884 | -------------------------------------------------------------------------------- /lib/matrix/lup_decomposition.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | class Matrix 3 | # Adapted from JAMA: http://math.nist.gov/javanumerics/jama/ 4 | 5 | # 6 | # For an m-by-n matrix A with m >= n, the LU decomposition is an m-by-n 7 | # unit lower triangular matrix L, an n-by-n upper triangular matrix U, 8 | # and a m-by-m permutation matrix P so that L*U = P*A. 9 | # If m < n, then L is m-by-m and U is m-by-n. 10 | # 11 | # The LUP decomposition with pivoting always exists, even if the matrix is 12 | # singular, so the constructor will never fail. The primary use of the 13 | # LU decomposition is in the solution of square systems of simultaneous 14 | # linear equations. This will fail if singular? returns true. 15 | # 16 | 17 | class LUPDecomposition 18 | # Returns the lower triangular factor +L+ 19 | 20 | include Matrix::ConversionHelper 21 | 22 | def l 23 | Matrix.build(@row_count, [@column_count, @row_count].min) do |i, j| 24 | if (i > j) 25 | @lu[i][j] 26 | elsif (i == j) 27 | 1 28 | else 29 | 0 30 | end 31 | end 32 | end 33 | 34 | # Returns the upper triangular factor +U+ 35 | 36 | def u 37 | Matrix.build([@column_count, @row_count].min, @column_count) do |i, j| 38 | if (i <= j) 39 | @lu[i][j] 40 | else 41 | 0 42 | end 43 | end 44 | end 45 | 46 | # Returns the permutation matrix +P+ 47 | 48 | def p 49 | rows = Array.new(@row_count){Array.new(@row_count, 0)} 50 | @pivots.each_with_index{|p, i| rows[i][p] = 1} 51 | Matrix.send :new, rows, @row_count 52 | end 53 | 54 | # Returns +L+, +U+, +P+ in an array 55 | 56 | def to_ary 57 | [l, u, p] 58 | end 59 | alias_method :to_a, :to_ary 60 | 61 | # Returns the pivoting indices 62 | 63 | attr_reader :pivots 64 | 65 | # Returns +true+ if +U+, and hence +A+, is singular. 66 | 67 | def singular? 68 | @column_count.times do |j| 69 | if (@lu[j][j] == 0) 70 | return true 71 | end 72 | end 73 | false 74 | end 75 | 76 | # Returns the determinant of +A+, calculated efficiently 77 | # from the factorization. 78 | 79 | def det 80 | if (@row_count != @column_count) 81 | raise Matrix::ErrDimensionMismatch 82 | end 83 | d = @pivot_sign 84 | @column_count.times do |j| 85 | d *= @lu[j][j] 86 | end 87 | d 88 | end 89 | alias_method :determinant, :det 90 | 91 | # Returns +m+ so that A*m = b, 92 | # or equivalently so that L*U*m = P*b 93 | # +b+ can be a Matrix or a Vector 94 | 95 | def solve b 96 | if (singular?) 97 | raise Matrix::ErrNotRegular, "Matrix is singular." 98 | end 99 | if b.is_a? Matrix 100 | if (b.row_count != @row_count) 101 | raise Matrix::ErrDimensionMismatch 102 | end 103 | 104 | # Copy right hand side with pivoting 105 | nx = b.column_count 106 | m = @pivots.map{|row| b.row(row).to_a} 107 | 108 | # Solve L*Y = P*b 109 | @column_count.times do |k| 110 | (k+1).upto(@column_count-1) do |i| 111 | nx.times do |j| 112 | m[i][j] -= m[k][j]*@lu[i][k] 113 | end 114 | end 115 | end 116 | # Solve U*m = Y 117 | (@column_count-1).downto(0) do |k| 118 | nx.times do |j| 119 | m[k][j] = m[k][j].quo(@lu[k][k]) 120 | end 121 | k.times do |i| 122 | nx.times do |j| 123 | m[i][j] -= m[k][j]*@lu[i][k] 124 | end 125 | end 126 | end 127 | Matrix.send :new, m, nx 128 | else # same algorithm, specialized for simpler case of a vector 129 | b = convert_to_array(b) 130 | if (b.size != @row_count) 131 | raise Matrix::ErrDimensionMismatch 132 | end 133 | 134 | # Copy right hand side with pivoting 135 | m = b.values_at(*@pivots) 136 | 137 | # Solve L*Y = P*b 138 | @column_count.times do |k| 139 | (k+1).upto(@column_count-1) do |i| 140 | m[i] -= m[k]*@lu[i][k] 141 | end 142 | end 143 | # Solve U*m = Y 144 | (@column_count-1).downto(0) do |k| 145 | m[k] = m[k].quo(@lu[k][k]) 146 | k.times do |i| 147 | m[i] -= m[k]*@lu[i][k] 148 | end 149 | end 150 | Vector.elements(m, false) 151 | end 152 | end 153 | 154 | def initialize a 155 | raise TypeError, "Expected Matrix but got #{a.class}" unless a.is_a?(Matrix) 156 | # Use a "left-looking", dot-product, Crout/Doolittle algorithm. 157 | @lu = a.to_a 158 | @row_count = a.row_count 159 | @column_count = a.column_count 160 | @pivots = Array.new(@row_count) 161 | @row_count.times do |i| 162 | @pivots[i] = i 163 | end 164 | @pivot_sign = 1 165 | lu_col_j = Array.new(@row_count) 166 | 167 | # Outer loop. 168 | 169 | @column_count.times do |j| 170 | 171 | # Make a copy of the j-th column to localize references. 172 | 173 | @row_count.times do |i| 174 | lu_col_j[i] = @lu[i][j] 175 | end 176 | 177 | # Apply previous transformations. 178 | 179 | @row_count.times do |i| 180 | lu_row_i = @lu[i] 181 | 182 | # Most of the time is spent in the following dot product. 183 | 184 | kmax = [i, j].min 185 | s = 0 186 | kmax.times do |k| 187 | s += lu_row_i[k]*lu_col_j[k] 188 | end 189 | 190 | lu_row_i[j] = lu_col_j[i] -= s 191 | end 192 | 193 | # Find pivot and exchange if necessary. 194 | 195 | p = j 196 | (j+1).upto(@row_count-1) do |i| 197 | if (lu_col_j[i].abs > lu_col_j[p].abs) 198 | p = i 199 | end 200 | end 201 | if (p != j) 202 | @column_count.times do |k| 203 | t = @lu[p][k]; @lu[p][k] = @lu[j][k]; @lu[j][k] = t 204 | end 205 | k = @pivots[p]; @pivots[p] = @pivots[j]; @pivots[j] = k 206 | @pivot_sign = -@pivot_sign 207 | end 208 | 209 | # Compute multipliers. 210 | 211 | if (j < @row_count && @lu[j][j] != 0) 212 | (j+1).upto(@row_count-1) do |i| 213 | @lu[i][j] = @lu[i][j].quo(@lu[j][j]) 214 | end 215 | end 216 | end 217 | end 218 | end 219 | end 220 | -------------------------------------------------------------------------------- /lib/matrix/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Matrix 4 | VERSION = "0.4.2" 5 | end 6 | -------------------------------------------------------------------------------- /matrix.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | begin 4 | require_relative "lib/matrix/version" 5 | rescue LoadError 6 | # for Ruby core repository 7 | require_relative "version" 8 | end 9 | 10 | Gem::Specification.new do |spec| 11 | spec.name = "matrix" 12 | spec.version = Matrix::VERSION 13 | spec.authors = ["Marc-Andre Lafortune"] 14 | spec.email = ["ruby-core@marc-andre.ca"] 15 | 16 | spec.summary = %q{An implementation of Matrix and Vector classes.} 17 | spec.description = %q{An implementation of Matrix and Vector classes.} 18 | spec.homepage = "https://github.com/ruby/matrix" 19 | spec.licenses = ["Ruby", "BSD-2-Clause"] 20 | spec.required_ruby_version = ">= 2.5.0" 21 | 22 | spec.files = ["LICENSE.txt", "lib/matrix.rb", "lib/matrix/eigenvalue_decomposition.rb", "lib/matrix/lup_decomposition.rb", "lib/matrix/version.rb", "matrix.gemspec"] 23 | spec.bindir = "exe" 24 | spec.executables = [] 25 | spec.require_paths = ["lib"] 26 | end 27 | -------------------------------------------------------------------------------- /test/lib/helper.rb: -------------------------------------------------------------------------------- 1 | require "test/unit" 2 | require "core_assertions" 3 | 4 | Test::Unit::TestCase.include Test::Unit::CoreAssertions 5 | -------------------------------------------------------------------------------- /test/matrix/test_matrix.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | require 'test/unit' 3 | require 'matrix' 4 | 5 | class SubMatrix < Matrix 6 | end 7 | 8 | class TestMatrix < Test::Unit::TestCase 9 | def setup 10 | @m1 = Matrix[[1,2,3], [4,5,6]] 11 | @m2 = Matrix[[1,2,3], [4,5,6]] 12 | @m3 = @m1.clone 13 | @m4 = Matrix[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]] 14 | @n1 = Matrix[[2,3,4], [5,6,7]] 15 | @c1 = Matrix[[Complex(1,2), Complex(0,1), 0], [1, 2, 3]] 16 | @e1 = Matrix.empty(2,0) 17 | @e2 = Matrix.empty(0,3) 18 | @a3 = Matrix[[4, 1, -3], [0, 3, 7], [11, -4, 2]] 19 | @a5 = Matrix[[2, 0, 9, 3, 9], [8, 7, 0, 1, 9], [7, 5, 6, 6, 5], [0, 7, 8, 3, 0], [7, 8, 2, 3, 1]] 20 | @b3 = Matrix[[-7, 7, -10], [9, -3, -2], [-1, 3, 9]] 21 | @rot = Matrix[[0, -1, 0], [1, 0, 0], [0, 0, -1]] 22 | end 23 | 24 | def test_matrix 25 | assert_equal(1, @m1[0, 0]) 26 | assert_equal(2, @m1[0, 1]) 27 | assert_equal(3, @m1[0, 2]) 28 | assert_equal(4, @m1[1, 0]) 29 | assert_equal(5, @m1[1, 1]) 30 | assert_equal(6, @m1[1, 2]) 31 | end 32 | 33 | def test_identity 34 | assert_same @m1, @m1 35 | assert_not_same @m1, @m2 36 | assert_not_same @m1, @m3 37 | assert_not_same @m1, @m4 38 | assert_not_same @m1, @n1 39 | end 40 | 41 | def test_equality 42 | assert_equal @m1, @m1 43 | assert_equal @m1, @m2 44 | assert_equal @m1, @m3 45 | assert_equal @m1, @m4 46 | assert_not_equal @m1, @n1 47 | end 48 | 49 | def test_hash_equality 50 | assert @m1.eql?(@m1) 51 | assert @m1.eql?(@m2) 52 | assert @m1.eql?(@m3) 53 | assert !@m1.eql?(@m4) 54 | assert !@m1.eql?(@n1) 55 | 56 | hash = { @m1 => :value } 57 | assert hash.key?(@m1) 58 | assert hash.key?(@m2) 59 | assert hash.key?(@m3) 60 | assert !hash.key?(@m4) 61 | assert !hash.key?(@n1) 62 | end 63 | 64 | def test_hash 65 | assert_equal @m1.hash, @m1.hash 66 | assert_equal @m1.hash, @m2.hash 67 | assert_equal @m1.hash, @m3.hash 68 | end 69 | 70 | def test_uplus 71 | assert_equal(@m1, +@m1) 72 | end 73 | 74 | def test_negate 75 | assert_equal(Matrix[[-1, -2, -3], [-4, -5, -6]], -@m1) 76 | assert_equal(@m1, -(-@m1)) 77 | end 78 | 79 | def test_rank 80 | [ 81 | [[0]], 82 | [[0], [0]], 83 | [[0, 0], [0, 0]], 84 | [[0, 0], [0, 0], [0, 0]], 85 | [[0, 0, 0]], 86 | [[0, 0, 0], [0, 0, 0]], 87 | [[0, 0, 0], [0, 0, 0], [0, 0, 0]], 88 | [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], 89 | ].each do |rows| 90 | assert_equal 0, Matrix[*rows].rank 91 | end 92 | 93 | [ 94 | [[1], [0]], 95 | [[1, 0], [0, 0]], 96 | [[1, 0], [1, 0]], 97 | [[0, 0], [1, 0]], 98 | [[1, 0], [0, 0], [0, 0]], 99 | [[0, 0], [1, 0], [0, 0]], 100 | [[0, 0], [0, 0], [1, 0]], 101 | [[1, 0], [1, 0], [0, 0]], 102 | [[0, 0], [1, 0], [1, 0]], 103 | [[1, 0], [1, 0], [1, 0]], 104 | [[1, 0, 0]], 105 | [[1, 0, 0], [0, 0, 0]], 106 | [[0, 0, 0], [1, 0, 0]], 107 | [[1, 0, 0], [1, 0, 0]], 108 | [[1, 0, 0], [1, 0, 0]], 109 | [[1, 0, 0], [0, 0, 0], [0, 0, 0]], 110 | [[0, 0, 0], [1, 0, 0], [0, 0, 0]], 111 | [[0, 0, 0], [0, 0, 0], [1, 0, 0]], 112 | [[1, 0, 0], [1, 0, 0], [0, 0, 0]], 113 | [[0, 0, 0], [1, 0, 0], [1, 0, 0]], 114 | [[1, 0, 0], [0, 0, 0], [1, 0, 0]], 115 | [[1, 0, 0], [1, 0, 0], [1, 0, 0]], 116 | [[1, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], 117 | [[1, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], 118 | [[1, 0, 0], [1, 0, 0], [0, 0, 0], [0, 0, 0]], 119 | [[1, 0, 0], [0, 0, 0], [1, 0, 0], [0, 0, 0]], 120 | [[1, 0, 0], [0, 0, 0], [0, 0, 0], [1, 0, 0]], 121 | [[1, 0, 0], [1, 0, 0], [1, 0, 0], [0, 0, 0]], 122 | [[1, 0, 0], [0, 0, 0], [1, 0, 0], [1, 0, 0]], 123 | [[1, 0, 0], [1, 0, 0], [0, 0, 0], [1, 0, 0]], 124 | [[1, 0, 0], [1, 0, 0], [1, 0, 0], [1, 0, 0]], 125 | 126 | [[1]], 127 | [[1], [1]], 128 | [[1, 1]], 129 | [[1, 1], [1, 1]], 130 | [[1, 1], [1, 1], [1, 1]], 131 | [[1, 1, 1]], 132 | [[1, 1, 1], [1, 1, 1]], 133 | [[1, 1, 1], [1, 1, 1], [1, 1, 1]], 134 | [[1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1]], 135 | ].each do |rows| 136 | matrix = Matrix[*rows] 137 | assert_equal 1, matrix.rank 138 | assert_equal 1, matrix.transpose.rank 139 | end 140 | 141 | [ 142 | [[1, 0], [0, 1]], 143 | [[1, 0], [0, 1], [0, 0]], 144 | [[1, 0], [0, 1], [0, 1]], 145 | [[1, 0], [0, 1], [1, 1]], 146 | [[1, 0, 0], [0, 1, 0]], 147 | [[1, 0, 0], [0, 0, 1]], 148 | [[1, 0, 0], [0, 1, 0], [0, 0, 0]], 149 | [[1, 0, 0], [0, 0, 1], [0, 0, 0]], 150 | 151 | [[1, 0, 0], [0, 0, 0], [0, 1, 0]], 152 | [[1, 0, 0], [0, 0, 0], [0, 0, 1]], 153 | 154 | [[1, 0], [1, 1]], 155 | [[1, 2], [1, 1]], 156 | [[1, 2], [0, 1], [1, 1]], 157 | ].each do |rows| 158 | m = Matrix[*rows] 159 | assert_equal 2, m.rank 160 | assert_equal 2, m.transpose.rank 161 | end 162 | 163 | [ 164 | [[1, 0, 0], [0, 1, 0], [0, 0, 1]], 165 | [[1, 1, 0], [0, 1, 1], [1, 0, 1]], 166 | [[1, 1, 0], [0, 1, 1], [1, 0, 1]], 167 | [[1, 1, 0], [0, 1, 1], [1, 0, 1], [0, 0, 0]], 168 | [[1, 1, 0], [0, 1, 1], [1, 0, 1], [1, 1, 1]], 169 | [[1, 1, 1], [1, 1, 2], [1, 3, 1], [4, 1, 1]], 170 | ].each do |rows| 171 | m = Matrix[*rows] 172 | assert_equal 3, m.rank 173 | assert_equal 3, m.transpose.rank 174 | end 175 | end 176 | 177 | def test_inverse 178 | assert_equal(Matrix.empty(0, 0), Matrix.empty.inverse) 179 | assert_equal(Matrix[[-1, 1], [0, -1]], Matrix[[-1, -1], [0, -1]].inverse) 180 | assert_raise(ExceptionForMatrix::ErrDimensionMismatch) { @m1.inverse } 181 | end 182 | 183 | def test_determinant 184 | assert_equal(0, Matrix[[0,0],[0,0]].determinant) 185 | assert_equal(45, Matrix[[7,6], [3,9]].determinant) 186 | assert_equal(-18, Matrix[[2,0,1],[0,-2,2],[1,2,3]].determinant) 187 | assert_equal(-7, Matrix[[0,0,1],[0,7,6],[1,3,9]].determinant) 188 | assert_equal(42, Matrix[[7,0,1,0,12],[8,1,1,9,1],[4,0,0,-7,17],[-1,0,0,-4,8],[10,1,1,8,6]].determinant) 189 | end 190 | 191 | def test_new_matrix 192 | assert_raise(TypeError) { Matrix[Object.new] } 193 | o = Object.new 194 | def o.to_ary; [1,2,3]; end 195 | assert_equal(@m1, Matrix[o, [4,5,6]]) 196 | end 197 | 198 | def test_round 199 | a = Matrix[[1.0111, 2.32320, 3.04343], [4.81, 5.0, 6.997]] 200 | b = Matrix[[1.01, 2.32, 3.04], [4.81, 5.0, 7.0]] 201 | assert_equal(a.round(2), b) 202 | end 203 | 204 | def test_rows 205 | assert_equal(@m1, Matrix.rows([[1, 2, 3], [4, 5, 6]])) 206 | end 207 | 208 | def test_rows_copy 209 | rows1 = [[1], [1]] 210 | rows2 = [[1], [1]] 211 | 212 | m1 = Matrix.rows(rows1, copy = false) 213 | m2 = Matrix.rows(rows2, copy = true) 214 | 215 | rows1.uniq! 216 | rows2.uniq! 217 | 218 | assert_equal([[1]], m1.to_a) 219 | assert_equal([[1], [1]], m2.to_a) 220 | end 221 | 222 | def test_to_matrix 223 | assert @m1.equal? @m1.to_matrix 224 | end 225 | 226 | def test_columns 227 | assert_equal(@m1, Matrix.columns([[1, 4], [2, 5], [3, 6]])) 228 | end 229 | 230 | def test_diagonal 231 | assert_equal(Matrix.empty(0, 0), Matrix.diagonal( )) 232 | assert_equal(Matrix[[3,0,0],[0,2,0],[0,0,1]], Matrix.diagonal(3, 2, 1)) 233 | assert_equal(Matrix[[4,0,0,0],[0,3,0,0],[0,0,2,0],[0,0,0,1]], Matrix.diagonal(4, 3, 2, 1)) 234 | end 235 | 236 | def test_scalar 237 | assert_equal(Matrix.empty(0, 0), Matrix.scalar(0, 1)) 238 | assert_equal(Matrix[[2,0,0],[0,2,0],[0,0,2]], Matrix.scalar(3, 2)) 239 | assert_equal(Matrix[[2,0,0,0],[0,2,0,0],[0,0,2,0],[0,0,0,2]], Matrix.scalar(4, 2)) 240 | end 241 | 242 | def test_identity2 243 | assert_equal(Matrix[[1,0,0],[0,1,0],[0,0,1]], Matrix.identity(3)) 244 | assert_equal(Matrix[[1,0,0],[0,1,0],[0,0,1]], Matrix.unit(3)) 245 | assert_equal(Matrix[[1,0,0],[0,1,0],[0,0,1]], Matrix.I(3)) 246 | assert_equal(Matrix[[1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1]], Matrix.identity(4)) 247 | end 248 | 249 | def test_zero 250 | assert_equal(Matrix[[0,0,0],[0,0,0],[0,0,0]], Matrix.zero(3)) 251 | assert_equal(Matrix[[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0]], Matrix.zero(4)) 252 | assert_equal(Matrix[[0]], Matrix.zero(1)) 253 | end 254 | 255 | def test_row_vector 256 | assert_equal(Matrix[[1,2,3,4]], Matrix.row_vector([1,2,3,4])) 257 | end 258 | 259 | def test_column_vector 260 | assert_equal(Matrix[[1],[2],[3],[4]], Matrix.column_vector([1,2,3,4])) 261 | end 262 | 263 | def test_empty 264 | m = Matrix.empty(2, 0) 265 | assert_equal(Matrix[ [], [] ], m) 266 | n = Matrix.empty(0, 3) 267 | assert_equal(Matrix.columns([ [], [], [] ]), n) 268 | assert_equal(Matrix[[0, 0, 0], [0, 0, 0]], m * n) 269 | end 270 | 271 | def test_row 272 | assert_equal(Vector[1, 2, 3], @m1.row(0)) 273 | assert_equal(Vector[4, 5, 6], @m1.row(1)) 274 | a = []; @m1.row(0) {|x| a << x } 275 | assert_equal([1, 2, 3], a) 276 | end 277 | 278 | def test_column 279 | assert_equal(Vector[1, 4], @m1.column(0)) 280 | assert_equal(Vector[2, 5], @m1.column(1)) 281 | assert_equal(Vector[3, 6], @m1.column(2)) 282 | a = []; @m1.column(0) {|x| a << x } 283 | assert_equal([1, 4], a) 284 | end 285 | 286 | def test_collect 287 | m1 = Matrix.zero(2,2) 288 | m2 = Matrix.build(3,4){|row, col| 1} 289 | 290 | assert_equal(Matrix[[5, 5, 5, 5], [5, 5, 5, 5], [5, 5, 5, 5]], m2.collect{|e| e * 5}) 291 | assert_equal(Matrix[[7, 0],[0, 7]], m1.collect(:diagonal){|e| e + 7}) 292 | assert_equal(Matrix[[0, 5],[5, 0]], m1.collect(:off_diagonal){|e| e + 5}) 293 | assert_equal(Matrix[[8, 1, 1, 1], [8, 8, 1, 1], [8, 8, 8, 1]], m2.collect(:lower){|e| e + 7}) 294 | assert_equal(Matrix[[1, 1, 1, 1], [-11, 1, 1, 1], [-11, -11, 1, 1]], m2.collect(:strict_lower){|e| e - 12}) 295 | assert_equal(Matrix[[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]], m2.collect(:strict_upper){|e| e ** 2}) 296 | assert_equal(Matrix[[-1, -1, -1, -1], [1, -1, -1, -1], [1, 1, -1, -1]], m2.collect(:upper){|e| -e}) 297 | assert_raise(ArgumentError) {m1.collect(:test){|e| e + 7}} 298 | assert_not_equal(m2, m2.collect {|e| e * 2 }) 299 | end 300 | 301 | def test_minor 302 | assert_equal(Matrix[[1, 2], [4, 5]], @m1.minor(0..1, 0..1)) 303 | assert_equal(Matrix[[2], [5]], @m1.minor(0..1, 1..1)) 304 | assert_equal(Matrix[[4, 5]], @m1.minor(1..1, 0..1)) 305 | assert_equal(Matrix[[1, 2], [4, 5]], @m1.minor(0, 2, 0, 2)) 306 | assert_equal(Matrix[[4, 5]], @m1.minor(1, 1, 0, 2)) 307 | assert_equal(Matrix[[2], [5]], @m1.minor(0, 2, 1, 1)) 308 | assert_raise(ArgumentError) { @m1.minor(0) } 309 | end 310 | 311 | def test_first_minor 312 | assert_equal(Matrix.empty(0, 0), Matrix[[1]].first_minor(0, 0)) 313 | assert_equal(Matrix.empty(0, 2), Matrix[[1, 4, 2]].first_minor(0, 1)) 314 | assert_equal(Matrix[[1, 3]], @m1.first_minor(1, 1)) 315 | assert_equal(Matrix[[4, 6]], @m1.first_minor(0, 1)) 316 | assert_equal(Matrix[[1, 2]], @m1.first_minor(1, 2)) 317 | assert_raise(RuntimeError) { Matrix.empty(0, 0).first_minor(0, 0) } 318 | assert_raise(ArgumentError) { @m1.first_minor(4, 0) } 319 | assert_raise(ArgumentError) { @m1.first_minor(0, -1) } 320 | assert_raise(ArgumentError) { @m1.first_minor(-1, 4) } 321 | end 322 | 323 | def test_cofactor 324 | assert_equal(1, Matrix[[1]].cofactor(0, 0)) 325 | assert_equal(9, Matrix[[7,6],[3,9]].cofactor(0, 0)) 326 | assert_equal(0, Matrix[[0,0],[0,0]].cofactor(0, 0)) 327 | assert_equal(3, Matrix[[0,0,1],[0,7,6],[1,3,9]].cofactor(1, 0)) 328 | assert_equal(-21, Matrix[[7,0,1,0,12],[8,1,1,9,1],[4,0,0,-7,17],[-1,0,0,-4,8],[10,1,1,8,6]].cofactor(2, 3)) 329 | assert_raise(RuntimeError) { Matrix.empty(0, 0).cofactor(0, 0) } 330 | assert_raise(ArgumentError) { Matrix[[0,0],[0,0]].cofactor(-1, 4) } 331 | assert_raise(ExceptionForMatrix::ErrDimensionMismatch) { Matrix[[2,0,1],[0,-2,2]].cofactor(0, 0) } 332 | end 333 | 334 | def test_adjugate 335 | assert_equal(Matrix.empty, Matrix.empty.adjugate) 336 | assert_equal(Matrix[[1]], Matrix[[5]].adjugate) 337 | assert_equal(Matrix[[9,-6],[-3,7]], Matrix[[7,6],[3,9]].adjugate) 338 | assert_equal(Matrix[[45,3,-7],[6,-1,0],[-7,0,0]], Matrix[[0,0,1],[0,7,6],[1,3,9]].adjugate) 339 | assert_equal(Matrix.identity(5), (@a5.adjugate * @a5) / @a5.det) 340 | assert_equal(Matrix.I(3), Matrix.I(3).adjugate) 341 | assert_equal((@a3 * @b3).adjugate, @b3.adjugate * @a3.adjugate) 342 | assert_equal(4**(@a3.row_count-1) * @a3.adjugate, (4 * @a3).adjugate) 343 | assert_raise(ExceptionForMatrix::ErrDimensionMismatch) { @m1.adjugate } 344 | end 345 | 346 | def test_laplace_expansion 347 | assert_equal(1, Matrix[[1]].laplace_expansion(row: 0)) 348 | assert_equal(45, Matrix[[7,6], [3,9]].laplace_expansion(row: 1)) 349 | assert_equal(0, Matrix[[0,0],[0,0]].laplace_expansion(column: 0)) 350 | assert_equal(-7, Matrix[[0,0,1],[0,7,6],[1,3,9]].laplace_expansion(column: 2)) 351 | 352 | assert_equal(Vector[3, -2], Matrix[[Vector[1, 0], Vector[0, 1]], [2, 3]].laplace_expansion(row: 0)) 353 | 354 | assert_raise(ExceptionForMatrix::ErrDimensionMismatch) { @m1.laplace_expansion(row: 1) } 355 | assert_raise(ArgumentError) { Matrix[[7,6], [3,9]].laplace_expansion() } 356 | assert_raise(ArgumentError) { Matrix[[7,6], [3,9]].laplace_expansion(foo: 1) } 357 | assert_raise(ArgumentError) { Matrix[[7,6], [3,9]].laplace_expansion(row: 1, column: 1) } 358 | assert_raise(ArgumentError) { Matrix[[7,6], [3,9]].laplace_expansion(row: 2) } 359 | assert_raise(ArgumentError) { Matrix[[0,0,1],[0,7,6],[1,3,9]].laplace_expansion(column: -1) } 360 | 361 | assert_raise(RuntimeError) { Matrix.empty(0, 0).laplace_expansion(row: 0) } 362 | end 363 | 364 | def test_regular? 365 | assert(Matrix[[1, 0], [0, 1]].regular?) 366 | assert(Matrix[[1, 0, 0], [0, 1, 0], [0, 0, 1]].regular?) 367 | assert(!Matrix[[1, 0, 0], [0, 0, 1], [0, 0, 1]].regular?) 368 | end 369 | 370 | def test_singular? 371 | assert(!Matrix[[1, 0], [0, 1]].singular?) 372 | assert(!Matrix[[1, 0, 0], [0, 1, 0], [0, 0, 1]].singular?) 373 | assert(Matrix[[1, 0, 0], [0, 0, 1], [0, 0, 1]].singular?) 374 | end 375 | 376 | def test_square? 377 | assert(Matrix[[1, 0], [0, 1]].square?) 378 | assert(Matrix[[1, 0, 0], [0, 1, 0], [0, 0, 1]].square?) 379 | assert(Matrix[[1, 0, 0], [0, 0, 1], [0, 0, 1]].square?) 380 | assert(!Matrix[[1, 0, 0], [0, 1, 0]].square?) 381 | end 382 | 383 | def test_mul 384 | assert_equal(Matrix[[2,4],[6,8]], Matrix[[2,4],[6,8]] * Matrix.I(2)) 385 | assert_equal(Matrix[[4,8],[12,16]], Matrix[[2,4],[6,8]] * 2) 386 | assert_equal(Matrix[[4,8],[12,16]], 2 * Matrix[[2,4],[6,8]]) 387 | assert_equal(Matrix[[14,32],[32,77]], @m1 * @m1.transpose) 388 | assert_equal(Matrix[[17,22,27],[22,29,36],[27,36,45]], @m1.transpose * @m1) 389 | assert_equal(Vector[14,32], @m1 * Vector[1,2,3]) 390 | o = Object.new 391 | def o.coerce(m) 392 | [m, m.transpose] 393 | end 394 | assert_equal(Matrix[[14,32],[32,77]], @m1 * o) 395 | end 396 | 397 | def test_add 398 | assert_equal(Matrix[[6,0],[-4,12]], Matrix.scalar(2,5) + Matrix[[1,0],[-4,7]]) 399 | assert_equal(Matrix[[3,5,7],[9,11,13]], @m1 + @n1) 400 | assert_equal(Matrix[[3,5,7],[9,11,13]], @n1 + @m1) 401 | assert_equal(Matrix[[2],[4],[6]], Matrix[[1],[2],[3]] + Vector[1,2,3]) 402 | assert_raise(Matrix::ErrOperationNotDefined) { @m1 + 1 } 403 | o = Object.new 404 | def o.coerce(m) 405 | [m, m] 406 | end 407 | assert_equal(Matrix[[2,4,6],[8,10,12]], @m1 + o) 408 | end 409 | 410 | def test_sub 411 | assert_equal(Matrix[[4,0],[4,-2]], Matrix.scalar(2,5) - Matrix[[1,0],[-4,7]]) 412 | assert_equal(Matrix[[-1,-1,-1],[-1,-1,-1]], @m1 - @n1) 413 | assert_equal(Matrix[[1,1,1],[1,1,1]], @n1 - @m1) 414 | assert_equal(Matrix[[0],[0],[0]], Matrix[[1],[2],[3]] - Vector[1,2,3]) 415 | assert_raise(Matrix::ErrOperationNotDefined) { @m1 - 1 } 416 | o = Object.new 417 | def o.coerce(m) 418 | [m, m] 419 | end 420 | assert_equal(Matrix[[0,0,0],[0,0,0]], @m1 - o) 421 | end 422 | 423 | def test_div 424 | assert_equal(Matrix[[0,1,1],[2,2,3]], @m1 / 2) 425 | assert_equal(Matrix[[1,1],[1,1]], Matrix[[2,2],[2,2]] / Matrix.scalar(2,2)) 426 | o = Object.new 427 | def o.coerce(m) 428 | [m, Matrix.scalar(2,2)] 429 | end 430 | assert_equal(Matrix[[1,1],[1,1]], Matrix[[2,2],[2,2]] / o) 431 | end 432 | 433 | def test_hadamard_product 434 | assert_equal(Matrix[[1,4], [9,16]], Matrix[[1,2], [3,4]].hadamard_product(Matrix[[1,2], [3,4]])) 435 | assert_equal(Matrix[[2, 6, 12], [20, 30, 42]], @m1.hadamard_product(@n1)) 436 | o = Object.new 437 | def o.to_matrix 438 | Matrix[[1, 2, 3], [-1, 0, 1]] 439 | end 440 | assert_equal(Matrix[[1, 4, 9], [-4, 0, 6]], @m1.hadamard_product(o)) 441 | e = Matrix.empty(3, 0) 442 | assert_equal(e, e.hadamard_product(e)) 443 | e = Matrix.empty(0, 3) 444 | assert_equal(e, e.hadamard_product(e)) 445 | end 446 | 447 | def test_exp 448 | assert_equal(Matrix[[67,96],[48,99]], Matrix[[7,6],[3,9]] ** 2) 449 | assert_equal(Matrix.I(5), Matrix.I(5) ** -1) 450 | assert_raise(Matrix::ErrOperationNotDefined) { Matrix.I(5) ** Object.new } 451 | 452 | m = Matrix[[0,2],[1,0]] 453 | exp = 0b11101000 454 | assert_equal(Matrix.scalar(2, 1 << (exp/2)), m ** exp) 455 | exp = 0b11101001 456 | assert_equal(Matrix[[0, 2 << (exp/2)], [1 << (exp/2), 0]], m ** exp) 457 | end 458 | 459 | def test_det 460 | assert_equal(Matrix.instance_method(:determinant), Matrix.instance_method(:det)) 461 | end 462 | 463 | def test_rank2 464 | assert_equal(2, Matrix[[7,6],[3,9]].rank) 465 | assert_equal(0, Matrix[[0,0],[0,0]].rank) 466 | assert_equal(3, Matrix[[0,0,1],[0,7,6],[1,3,9]].rank) 467 | assert_equal(1, Matrix[[0,1],[0,1],[0,1]].rank) 468 | assert_equal(2, @m1.rank) 469 | end 470 | 471 | def test_trace 472 | assert_equal(1+5+9, Matrix[[1,2,3],[4,5,6],[7,8,9]].trace) 473 | end 474 | 475 | def test_transpose 476 | assert_equal(Matrix[[1,4],[2,5],[3,6]], @m1.transpose) 477 | end 478 | 479 | def test_conjugate 480 | assert_equal(Matrix[[Complex(1,-2), Complex(0,-1), 0], [1, 2, 3]], @c1.conjugate) 481 | end 482 | 483 | def test_eigensystem 484 | m = Matrix[[1, 2], [3, 4]] 485 | v, d, v_inv = m.eigensystem 486 | assert(d.diagonal?) 487 | assert_equal(v.inv, v_inv) 488 | assert_equal((v * d * v_inv).round(5), m) 489 | end 490 | 491 | def test_imaginary 492 | assert_equal(Matrix[[2, 1, 0], [0, 0, 0]], @c1.imaginary) 493 | end 494 | 495 | def test_lup 496 | m = Matrix[[1, 2], [3, 4]] 497 | l, u, p = m.lup 498 | assert(l.lower_triangular?) 499 | assert(u.upper_triangular?) 500 | assert(p.permutation?) 501 | assert(l * u == p * m) 502 | assert_equal(m.lup.solve([2, 5]), Vector[1, Rational(1,2)]) 503 | end 504 | 505 | def test_real 506 | assert_equal(Matrix[[1, 0, 0], [1, 2, 3]], @c1.real) 507 | end 508 | 509 | def test_rect 510 | assert_equal([Matrix[[1, 0, 0], [1, 2, 3]], Matrix[[2, 1, 0], [0, 0, 0]]], @c1.rect) 511 | end 512 | 513 | def test_row_vectors 514 | assert_equal([Vector[1,2,3], Vector[4,5,6]], @m1.row_vectors) 515 | end 516 | 517 | def test_column_vectors 518 | assert_equal([Vector[1,4], Vector[2,5], Vector[3,6]], @m1.column_vectors) 519 | end 520 | 521 | def test_to_s 522 | assert_equal("Matrix[[1, 2, 3], [4, 5, 6]]", @m1.to_s) 523 | assert_equal("Matrix.empty(0, 0)", Matrix[].to_s) 524 | assert_equal("Matrix.empty(1, 0)", Matrix[[]].to_s) 525 | end 526 | 527 | def test_inspect 528 | assert_equal("Matrix[[1, 2, 3], [4, 5, 6]]", @m1.inspect) 529 | assert_equal("Matrix.empty(0, 0)", Matrix[].inspect) 530 | assert_equal("Matrix.empty(1, 0)", Matrix[[]].inspect) 531 | end 532 | 533 | def test_scalar_add 534 | s1 = @m1.coerce(1).first 535 | assert_equal(Matrix[[1]], (s1 + 0) * Matrix[[1]]) 536 | assert_raise(Matrix::ErrOperationNotDefined) { s1 + Vector[0] } 537 | assert_raise(Matrix::ErrOperationNotDefined) { s1 + Matrix[[0]] } 538 | o = Object.new 539 | def o.coerce(x) 540 | [1, 1] 541 | end 542 | assert_equal(2, s1 + o) 543 | end 544 | 545 | def test_scalar_sub 546 | s1 = @m1.coerce(1).first 547 | assert_equal(Matrix[[1]], (s1 - 0) * Matrix[[1]]) 548 | assert_raise(Matrix::ErrOperationNotDefined) { s1 - Vector[0] } 549 | assert_raise(Matrix::ErrOperationNotDefined) { s1 - Matrix[[0]] } 550 | o = Object.new 551 | def o.coerce(x) 552 | [1, 1] 553 | end 554 | assert_equal(0, s1 - o) 555 | end 556 | 557 | def test_scalar_mul 558 | s1 = @m1.coerce(1).first 559 | assert_equal(Matrix[[1]], (s1 * 1) * Matrix[[1]]) 560 | assert_equal(Vector[2], s1 * Vector[2]) 561 | assert_equal(Matrix[[2]], s1 * Matrix[[2]]) 562 | o = Object.new 563 | def o.coerce(x) 564 | [1, 1] 565 | end 566 | assert_equal(1, s1 * o) 567 | end 568 | 569 | def test_scalar_div 570 | s1 = @m1.coerce(1).first 571 | assert_equal(Matrix[[1]], (s1 / 1) * Matrix[[1]]) 572 | assert_raise(Matrix::ErrOperationNotDefined) { s1 / Vector[0] } 573 | assert_equal(Matrix[[Rational(1,2)]], s1 / Matrix[[2]]) 574 | o = Object.new 575 | def o.coerce(x) 576 | [1, 1] 577 | end 578 | assert_equal(1, s1 / o) 579 | end 580 | 581 | def test_scalar_pow 582 | s1 = @m1.coerce(1).first 583 | assert_equal(Matrix[[1]], (s1 ** 1) * Matrix[[1]]) 584 | assert_raise(Matrix::ErrOperationNotDefined) { s1 ** Vector[0] } 585 | assert_raise(Matrix::ErrOperationNotImplemented) { s1 ** Matrix[[1]] } 586 | o = Object.new 587 | def o.coerce(x) 588 | [1, 1] 589 | end 590 | assert_equal(1, s1 ** o) 591 | end 592 | 593 | def test_abs 594 | s1 = @a3.abs 595 | assert_equal(s1, Matrix[[4, 1, 3], [0, 3, 7], [11, 4, 2]]) 596 | end 597 | 598 | def test_hstack 599 | assert_equal Matrix[[1,2,3,2,3,4,1,2,3], [4,5,6,5,6,7,4,5,6]], 600 | @m1.hstack(@n1, @m1) 601 | # Error checking: 602 | assert_raise(TypeError) { @m1.hstack(42) } 603 | assert_raise(TypeError) { Matrix.hstack(42, @m1) } 604 | assert_raise(Matrix::ErrDimensionMismatch) { @m1.hstack(Matrix.identity(3)) } 605 | assert_raise(Matrix::ErrDimensionMismatch) { @e1.hstack(@e2) } 606 | # Corner cases: 607 | assert_equal @m1, @m1.hstack 608 | assert_equal @e1, @e1.hstack(@e1) 609 | assert_equal Matrix.empty(0,6), @e2.hstack(@e2) 610 | assert_equal SubMatrix, SubMatrix.hstack(@e1).class 611 | # From Vectors: 612 | assert_equal Matrix[[1, 3],[2, 4]], Matrix.hstack(Vector[1,2], Vector[3, 4]) 613 | end 614 | 615 | def test_vstack 616 | assert_equal Matrix[[1,2,3], [4,5,6], [2,3,4], [5,6,7], [1,2,3], [4,5,6]], 617 | @m1.vstack(@n1, @m1) 618 | # Error checking: 619 | assert_raise(TypeError) { @m1.vstack(42) } 620 | assert_raise(TypeError) { Matrix.vstack(42, @m1) } 621 | assert_raise(Matrix::ErrDimensionMismatch) { @m1.vstack(Matrix.identity(2)) } 622 | assert_raise(Matrix::ErrDimensionMismatch) { @e1.vstack(@e2) } 623 | # Corner cases: 624 | assert_equal @m1, @m1.vstack 625 | assert_equal Matrix.empty(4,0), @e1.vstack(@e1) 626 | assert_equal @e2, @e2.vstack(@e2) 627 | assert_equal SubMatrix, SubMatrix.vstack(@e1).class 628 | # From Vectors: 629 | assert_equal Matrix[[1],[2],[3]], Matrix.vstack(Vector[1,2], Vector[3]) 630 | end 631 | 632 | def test_combine 633 | x = Matrix[[6, 6], [4, 4]] 634 | y = Matrix[[1, 2], [3, 4]] 635 | assert_equal Matrix[[5, 4], [1, 0]], Matrix.combine(x, y) {|a, b| a - b} 636 | assert_equal Matrix[[5, 4], [1, 0]], x.combine(y) {|a, b| a - b} 637 | # Without block 638 | assert_equal Matrix[[5, 4], [1, 0]], Matrix.combine(x, y).each {|a, b| a - b} 639 | # With vectors 640 | assert_equal Matrix[[111], [222]], Matrix.combine(Matrix[[1], [2]], Vector[10,20], Vector[100,200], &:sum) 641 | # Basic checks 642 | assert_raise(Matrix::ErrDimensionMismatch) { @m1.combine(x) { raise } } 643 | # Edge cases 644 | assert_equal Matrix.empty, Matrix.combine{ raise } 645 | assert_equal Matrix.empty(3,0), Matrix.combine(Matrix.empty(3,0), Matrix.empty(3,0)) { raise } 646 | assert_equal Matrix.empty(0,3), Matrix.combine(Matrix.empty(0,3), Matrix.empty(0,3)) { raise } 647 | end 648 | 649 | def test_set_element 650 | src = Matrix[ 651 | [1, 2, 3, 4], 652 | [5, 6, 7, 8], 653 | [9, 10, 11, 12], 654 | ] 655 | rows = { 656 | range: [1..2, 1...3, 1..-1, -2..2, 1.., 1..., -2.., -2...], 657 | int: [2, -1], 658 | invalid: [-4, 4, -4..2, 2..-4, 0...0, 2..0, -4..], 659 | } 660 | columns = { 661 | range: [2..3, 2...4, 2..-1, -2..3, 2.., 2..., -2..., -2..], 662 | int: [3, -1], 663 | invalid: [-5, 5, -5..2, 2..-5, 0...0, -5..], 664 | } 665 | values = { 666 | element: 42, 667 | matrix: Matrix[[20, 21], [22, 23]], 668 | vector: Vector[30, 31], 669 | row: Matrix[[60, 61]], 670 | column: Matrix[[50], [51]], 671 | mismatched_matrix: Matrix.identity(3), 672 | mismatched_vector: Vector[0, 1, 2, 3], 673 | } 674 | solutions = { 675 | [:int, :int] => { 676 | element: Matrix[[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 42]], 677 | }, 678 | [:range , :int] => { 679 | element: Matrix[[1, 2, 3, 4], [5, 6, 7, 42], [9, 10, 11, 42]], 680 | column: Matrix[[1, 2, 3, 4], [5, 6, 7, 50], [9, 10, 11, 51]], 681 | vector: Matrix[[1, 2, 3, 4], [5, 6, 7, 30], [9, 10, 11, 31]], 682 | }, 683 | [:int, :range] => { 684 | element: Matrix[[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 42, 42]], 685 | row: Matrix[[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 60, 61]], 686 | vector: Matrix[[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 30, 31]], 687 | }, 688 | [:range , :range] => { 689 | element: Matrix[[1, 2, 3, 4], [5, 6, 42, 42], [9, 10, 42, 42]], 690 | matrix: Matrix[[1, 2, 3, 4], [5, 6, 20, 21], [9, 10, 22, 23]], 691 | }, 692 | } 693 | solutions.default = Hash.new(IndexError) 694 | 695 | rows.each do |row_style, row_arguments| 696 | row_arguments.each do |row_argument| 697 | columns.each do |column_style, column_arguments| 698 | column_arguments.each do |column_argument| 699 | values.each do |value_type, value| 700 | expected = solutions[[row_style, column_style]][value_type] || Matrix::ErrDimensionMismatch 701 | 702 | result = src.clone 703 | begin 704 | result[row_argument, column_argument] = value 705 | assert_equal expected, result, 706 | "m[#{row_argument.inspect}][#{column_argument.inspect}] = #{value.inspect} failed" 707 | rescue Exception => e 708 | raise if e.class != expected 709 | end 710 | end 711 | end 712 | end 713 | end 714 | end 715 | end 716 | 717 | def test_map! 718 | m1 = Matrix.zero(2,2) 719 | m2 = Matrix.build(3,4){|row, col| 1} 720 | m3 = Matrix.zero(3,5).freeze 721 | m4 = Matrix.empty.freeze 722 | 723 | assert_equal Matrix[[5, 5, 5, 5], [5, 5, 5, 5], [5, 5, 5, 5]], m2.map!{|e| e * 5} 724 | assert_equal Matrix[[7, 0],[0, 7]], m1.map!(:diagonal){|e| e + 7} 725 | assert_equal Matrix[[7, 5],[5, 7]], m1.map!(:off_diagonal){|e| e + 5} 726 | assert_equal Matrix[[12, 5, 5, 5], [12, 12, 5, 5], [12, 12, 12, 5]], m2.map!(:lower){|e| e + 7} 727 | assert_equal Matrix[[12, 5, 5, 5], [0, 12, 5, 5], [0, 0, 12, 5]], m2.map!(:strict_lower){|e| e - 12} 728 | assert_equal Matrix[[12, 25, 25, 25], [0, 12, 25, 25], [0, 0, 12, 25]], m2.map!(:strict_upper){|e| e ** 2} 729 | assert_equal Matrix[[-12, -25, -25, -25], [0, -12, -25, -25], [0, 0, -12, -25]], m2.map!(:upper){|e| -e} 730 | assert_equal m1, m1.map!{|e| e ** 2 } 731 | assert_equal m2, m2.map!(:lower){ |e| e - 3 } 732 | assert_raise(ArgumentError) {m1.map!(:test){|e| e + 7}} 733 | assert_raise(FrozenError) { m3.map!{|e| e * 2} } 734 | assert_raise(FrozenError) { m4.map!{} } 735 | end 736 | 737 | def test_freeze 738 | m = Matrix[[1, 2, 3],[4, 5, 6]] 739 | f = m.freeze 740 | assert_equal true, f.frozen? 741 | assert m.equal?(f) 742 | assert m.equal?(f.freeze) 743 | assert_raise(FrozenError){ m[0, 1] = 56 } 744 | assert_equal m.dup, m 745 | end 746 | 747 | def test_clone 748 | a = Matrix[[4]] 749 | def a.foo 750 | 42 751 | end 752 | 753 | m = a.clone 754 | m[0, 0] = 2 755 | assert_equal a, m * 2 756 | assert_equal 42, m.foo 757 | 758 | a.freeze 759 | m = a.clone 760 | assert m.frozen? 761 | assert_equal 42, m.foo 762 | end 763 | 764 | def test_dup 765 | a = Matrix[[4]] 766 | def a.foo 767 | 42 768 | end 769 | a.freeze 770 | 771 | m = a.dup 772 | m[0, 0] = 2 773 | assert_equal a, m * 2 774 | assert !m.respond_to?(:foo) 775 | end 776 | 777 | def test_eigenvalues_and_eigenvectors_symmetric 778 | m = Matrix[ 779 | [8, 1], 780 | [1, 8] 781 | ] 782 | values = m.eigensystem.eigenvalues 783 | assert_in_epsilon(7.0, values[0]) 784 | assert_in_epsilon(9.0, values[1]) 785 | vectors = m.eigensystem.eigenvectors 786 | assert_in_epsilon(-vectors[0][0], vectors[0][1]) 787 | assert_in_epsilon(vectors[1][0], vectors[1][1]) 788 | end 789 | 790 | def test_eigenvalues_and_eigenvectors_nonsymmetric 791 | m = Matrix[ 792 | [8, 1], 793 | [4, 5] 794 | ] 795 | values = m.eigensystem.eigenvalues 796 | assert_in_epsilon(9.0, values[0]) 797 | assert_in_epsilon(4.0, values[1]) 798 | vectors = m.eigensystem.eigenvectors 799 | assert_in_epsilon(vectors[0][0], vectors[0][1]) 800 | assert_in_epsilon(-4 * vectors[1][0], vectors[1][1]) 801 | end 802 | 803 | def test_unitary? 804 | assert_equal true, @rot.unitary? 805 | assert_equal true, ((0+1i) * @rot).unitary? 806 | assert_equal false, @a3.unitary? 807 | assert_raise(Matrix::ErrDimensionMismatch) { @m1.unitary? } 808 | end 809 | 810 | def test_orthogonal 811 | assert_equal true, @rot.orthogonal? 812 | assert_equal false, ((0+1i) * @rot).orthogonal? 813 | assert_equal false, @a3.orthogonal? 814 | assert_raise(Matrix::ErrDimensionMismatch) { @m1.orthogonal? } 815 | end 816 | 817 | def test_adjoint 818 | assert_equal(Matrix[[(1-2i), 1], [(0-1i), 2], [0, 3]], @c1.adjoint) 819 | assert_equal(Matrix.empty(0,2), @e1.adjoint) 820 | end 821 | 822 | def test_ractor 823 | assert_ractor(<<~RUBY, require: 'matrix') 824 | class Ractor 825 | alias value take 826 | end unless Ractor.method_defined? :value # compat with Ruby 3.4 and olders 827 | 828 | obj1 = Matrix[[1, 2], [3, 4]].freeze 829 | 830 | obj2 = Ractor.new obj1 do |obj| 831 | obj 832 | end.value 833 | assert_same obj1, obj2 834 | RUBY 835 | end if defined?(Ractor) 836 | 837 | def test_rotate_with_symbol 838 | assert_equal(Matrix[[4, 1], [5, 2], [6, 3]], @m1.rotate_entries) 839 | assert_equal(@m1.rotate_entries, @m1.rotate_entries(:clockwise)) 840 | assert_equal(Matrix[[4, 1], [5, 2], [6, 3]], 841 | @m1.rotate_entries(:clockwise)) 842 | assert_equal(Matrix[[3, 6], [2, 5], [1, 4]], 843 | @m1.rotate_entries(:counter_clockwise)) 844 | assert_equal(Matrix[[6, 5, 4], [3, 2, 1]], 845 | @m1.rotate_entries(:half_turn)) 846 | assert_equal(Matrix[[6, 5, 4], [3, 2, 1]], 847 | @m1.rotate_entries(:half_turn)) 848 | assert_equal(Matrix.empty(0,2), 849 | @e1.rotate_entries(:clockwise)) 850 | assert_equal(Matrix.empty(0,2), 851 | @e1.rotate_entries(:counter_clockwise)) 852 | assert_equal(Matrix.empty(2,0), 853 | @e1.rotate_entries(:half_turn)) 854 | assert_equal(Matrix.empty(0,3), 855 | @e2.rotate_entries(:half_turn)) 856 | end 857 | 858 | def test_rotate_with_numeric 859 | assert_equal(Matrix[[4, 1], [5, 2], [6, 3]], 860 | @m1.rotate_entries(1)) 861 | assert_equal(@m2.rotate_entries(:half_turn), 862 | @m2.rotate_entries(2)) 863 | assert_equal(@m2.rotate_entries(:half_turn), 864 | @m1.rotate_entries(2)) 865 | assert_equal(@m1.rotate_entries(:counter_clockwise), 866 | @m1.rotate_entries(3)) 867 | assert_equal(@m1, 868 | @m1.rotate_entries(4)) 869 | assert_equal(@m1, 870 | @m1.rotate_entries(4)) 871 | assert_not_same(@m1, 872 | @m1.rotate_entries(4)) 873 | assert_equal(@m1.rotate_entries(:clockwise), 874 | @m1.rotate_entries(5)) 875 | assert_equal(Matrix.empty(0,2), 876 | @e1.rotate_entries(1)) 877 | assert_equal(@e2, 878 | @e2.rotate_entries(2)) 879 | assert_equal(@e2.rotate_entries(1), 880 | @e2.rotate_entries(3)) 881 | assert_equal(@e2.rotate_entries(:counter_clockwise), 882 | @e2.rotate_entries(-1)) 883 | assert_equal(@m1.rotate_entries(:counter_clockwise), 884 | @m1.rotate_entries(-1)) 885 | assert_equal(Matrix[[6, 5, 4], [3, 2, 1]], 886 | @m1.rotate_entries(-2)) 887 | assert_equal(@m1, 888 | @m1.rotate_entries(-4)) 889 | assert_equal(@m1.rotate_entries(-1), 890 | @m1.rotate_entries(-5)) 891 | end 892 | end 893 | -------------------------------------------------------------------------------- /test/matrix/test_vector.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | require 'test/unit' 3 | require 'matrix' 4 | 5 | class TestVector < Test::Unit::TestCase 6 | def setup 7 | @v1 = Vector[1,2,3] 8 | @v2 = Vector[1,2,3] 9 | @v3 = @v1.clone 10 | @v4 = Vector[1.0, 2.0, 3.0] 11 | @w1 = Vector[2,3,4] 12 | end 13 | 14 | def test_zero 15 | assert_equal Vector[0, 0, 0, 0], Vector.zero(4) 16 | assert_equal Vector[], Vector.zero(0) 17 | assert_raise(ArgumentError) { Vector.zero(-1) } 18 | assert Vector[0, 0, 0, 0].zero? 19 | end 20 | 21 | def test_basis 22 | assert_equal(Vector[1, 0, 0], Vector.basis(size: 3, index: 0)) 23 | assert_raise(ArgumentError) { Vector.basis(size: -1, index: 2) } 24 | assert_raise(ArgumentError) { Vector.basis(size: 4, index: -1) } 25 | assert_raise(ArgumentError) { Vector.basis(size: 3, index: 3) } 26 | assert_raise(ArgumentError) { Vector.basis(size: 3) } 27 | assert_raise(ArgumentError) { Vector.basis(index: 3) } 28 | end 29 | 30 | def test_get_element 31 | assert_equal(@v1[0..], [1, 2, 3]) 32 | assert_equal(@v1[0..1], [1, 2]) 33 | assert_equal(@v1[2], 3) 34 | assert_equal(@v1[4], nil) 35 | end 36 | 37 | def test_set_element 38 | 39 | assert_block do 40 | v = Vector[5, 6, 7, 8, 9] 41 | v[1..2] = Vector[1, 2] 42 | v == Vector[5, 1, 2, 8, 9] 43 | end 44 | 45 | assert_block do 46 | v = Vector[6, 7, 8] 47 | v[1..2] = Matrix[[1, 3]] 48 | v == Vector[6, 1, 3] 49 | end 50 | 51 | assert_block do 52 | v = Vector[1, 2, 3, 4, 5, 6] 53 | v[0..2] = 8 54 | v == Vector[8, 8, 8, 4, 5, 6] 55 | end 56 | 57 | assert_block do 58 | v = Vector[1, 3, 4, 5] 59 | v[2] = 5 60 | v == Vector[1, 3, 5, 5] 61 | end 62 | 63 | assert_block do 64 | v = Vector[2, 3, 5] 65 | v[-2] = 13 66 | v == Vector[2, 13, 5] 67 | end 68 | 69 | assert_block do 70 | v = Vector[4, 8, 9, 11, 30] 71 | v[1..-2] = Vector[1, 2, 3] 72 | v == Vector[4, 1, 2, 3, 30] 73 | end 74 | 75 | assert_raise(IndexError) {Vector[1, 3, 4, 5][5..6] = 17} 76 | assert_raise(IndexError) {Vector[1, 3, 4, 5][6] = 17} 77 | assert_raise(Matrix::ErrDimensionMismatch) {Vector[1, 3, 4, 5][0..2] = Matrix[[1], [2], [3]]} 78 | assert_raise(ArgumentError) {Vector[1, 2, 3, 4, 5, 6][0..2] = Vector[1, 2, 3, 4, 5, 6]} 79 | assert_raise(FrozenError) { Vector[7, 8, 9].freeze[0..1] = 5} 80 | end 81 | 82 | def test_map! 83 | v1 = Vector[1, 2, 3] 84 | v2 = Vector[1, 3, 5].freeze 85 | v3 = Vector[].freeze 86 | assert_equal Vector[1, 4, 9], v1.map!{|e| e ** 2} 87 | assert_equal v1, v1.map!{|e| e - 8} 88 | assert_raise(FrozenError) { v2.map!{|e| e + 2 }} 89 | assert_raise(FrozenError){ v3.map!{} } 90 | end 91 | 92 | def test_freeze 93 | v = Vector[1,2,3] 94 | f = v.freeze 95 | assert_equal true, f.frozen? 96 | assert v.equal?(f) 97 | assert v.equal?(f.freeze) 98 | assert_raise(FrozenError){ v[1] = 56 } 99 | assert_equal v.dup, v 100 | end 101 | 102 | def test_clone 103 | a = Vector[4] 104 | def a.foo 105 | 42 106 | end 107 | 108 | v = a.clone 109 | v[0] = 2 110 | assert_equal a, v * 2 111 | assert_equal 42, v.foo 112 | 113 | a.freeze 114 | v = a.clone 115 | assert v.frozen? 116 | assert_equal 42, v.foo 117 | end 118 | 119 | def test_dup 120 | a = Vector[4] 121 | def a.foo 122 | 42 123 | end 124 | a.freeze 125 | 126 | v = a.dup 127 | v[0] = 2 128 | assert_equal a, v * 2 129 | assert !v.respond_to?(:foo) 130 | end 131 | 132 | def test_identity 133 | assert_same @v1, @v1 134 | assert_not_same @v1, @v2 135 | assert_not_same @v1, @v3 136 | assert_not_same @v1, @v4 137 | assert_not_same @v1, @w1 138 | end 139 | 140 | def test_equality 141 | assert_equal @v1, @v1 142 | assert_equal @v1, @v2 143 | assert_equal @v1, @v3 144 | assert_equal @v1, @v4 145 | assert_not_equal @v1, @w1 146 | end 147 | 148 | def test_hash_equality 149 | assert @v1.eql?(@v1) 150 | assert @v1.eql?(@v2) 151 | assert @v1.eql?(@v3) 152 | assert !@v1.eql?(@v4) 153 | assert !@v1.eql?(@w1) 154 | 155 | hash = { @v1 => :value } 156 | assert hash.key?(@v1) 157 | assert hash.key?(@v2) 158 | assert hash.key?(@v3) 159 | assert !hash.key?(@v4) 160 | assert !hash.key?(@w1) 161 | end 162 | 163 | def test_hash 164 | assert_equal @v1.hash, @v1.hash 165 | assert_equal @v1.hash, @v2.hash 166 | assert_equal @v1.hash, @v3.hash 167 | end 168 | 169 | def test_aref 170 | assert_equal(1, @v1[0]) 171 | assert_equal(2, @v1[1]) 172 | assert_equal(3, @v1[2]) 173 | assert_equal(3, @v1[-1]) 174 | assert_equal(nil, @v1[3]) 175 | end 176 | 177 | def test_size 178 | assert_equal(3, @v1.size) 179 | end 180 | 181 | def test_each2 182 | a = [] 183 | @v1.each2(@v4) {|x, y| a << [x, y] } 184 | assert_equal([[1,1.0],[2,2.0],[3,3.0]], a) 185 | end 186 | 187 | def test_collect 188 | a = @v1.collect {|x| x + 1 } 189 | assert_equal(Vector[2,3,4], a) 190 | end 191 | 192 | def test_collect2 193 | a = @v1.collect2(@v4) {|x, y| x + y } 194 | assert_equal([2.0,4.0,6.0], a) 195 | end 196 | 197 | def test_map2 198 | a = @v1.map2(@v4) {|x, y| x + y } 199 | assert_equal(Vector[2.0,4.0,6.0], a) 200 | end 201 | 202 | def test_independent? 203 | assert(Vector.independent?(@v1, @w1)) 204 | assert( 205 | Vector.independent?( 206 | Vector.basis(size: 3, index: 0), 207 | Vector.basis(size: 3, index: 1), 208 | Vector.basis(size: 3, index: 2), 209 | ) 210 | ) 211 | 212 | refute(Vector.independent?(@v1, Vector[2,4,6])) 213 | refute(Vector.independent?(Vector[2,4], Vector[1,3], Vector[5,6])) 214 | 215 | assert_raise(TypeError) { Vector.independent?(@v1, 3) } 216 | assert_raise(Vector::ErrDimensionMismatch) { Vector.independent?(@v1, Vector[2,4]) } 217 | 218 | assert(@v1.independent?(Vector[1,2,4], Vector[1,3,4])) 219 | end 220 | 221 | def test_mul 222 | assert_equal(Vector[2,4,6], @v1 * 2) 223 | assert_equal(Matrix[[1, 4, 9], [2, 8, 18], [3, 12, 27]], @v1 * Matrix[[1,4,9]]) 224 | assert_raise(Matrix::ErrOperationNotDefined) { @v1 * Vector[1,4,9] } 225 | o = Object.new 226 | def o.coerce(x) 227 | [1, 1] 228 | end 229 | assert_equal(1, Vector[1, 2, 3] * o) 230 | end 231 | 232 | def test_add 233 | assert_equal(Vector[2,4,6], @v1 + @v1) 234 | assert_equal(Matrix[[2],[6],[12]], @v1 + Matrix[[1],[4],[9]]) 235 | o = Object.new 236 | def o.coerce(x) 237 | [1, 1] 238 | end 239 | assert_equal(2, Vector[1, 2, 3] + o) 240 | end 241 | 242 | def test_sub 243 | assert_equal(Vector[0,0,0], @v1 - @v1) 244 | assert_equal(Matrix[[0],[-2],[-6]], @v1 - Matrix[[1],[4],[9]]) 245 | o = Object.new 246 | def o.coerce(x) 247 | [1, 1] 248 | end 249 | assert_equal(0, Vector[1, 2, 3] - o) 250 | end 251 | 252 | def test_uplus 253 | assert_equal(@v1, +@v1) 254 | end 255 | 256 | def test_negate 257 | assert_equal(Vector[-1, -2, -3], -@v1) 258 | assert_equal(@v1, -(-@v1)) 259 | end 260 | 261 | def test_inner_product 262 | assert_equal(1+4+9, @v1.inner_product(@v1)) 263 | assert_equal(1+4+9, @v1.dot(@v1)) 264 | end 265 | 266 | def test_r 267 | assert_equal(5, Vector[3, 4].r) 268 | end 269 | 270 | def test_round 271 | assert_equal(Vector[1.234, 2.345, 3.40].round(2), Vector[1.23, 2.35, 3.4]) 272 | end 273 | 274 | def test_covector 275 | assert_equal(Matrix[[1,2,3]], @v1.covector) 276 | end 277 | 278 | def test_to_s 279 | assert_equal("Vector[1, 2, 3]", @v1.to_s) 280 | end 281 | 282 | def test_to_matrix 283 | assert_equal Matrix[[1], [2], [3]], @v1.to_matrix 284 | end 285 | 286 | def test_inspect 287 | assert_equal("Vector[1, 2, 3]", @v1.inspect) 288 | end 289 | 290 | def test_magnitude 291 | assert_in_epsilon(3.7416573867739413, @v1.norm) 292 | assert_in_epsilon(3.7416573867739413, @v4.norm) 293 | end 294 | 295 | def test_complex_magnitude 296 | bug6966 = '[ruby-dev:46100]' 297 | v = Vector[Complex(0,1), 0] 298 | assert_equal(1.0, v.norm, bug6966) 299 | end 300 | 301 | def test_rational_magnitude 302 | v = Vector[Rational(1,2), 0] 303 | assert_equal(0.5, v.norm) 304 | end 305 | 306 | def test_cross_product 307 | v = Vector[1, 0, 0].cross_product Vector[0, 1, 0] 308 | assert_equal(Vector[0, 0, 1], v) 309 | v2 = Vector[1, 2].cross_product 310 | assert_equal(Vector[-2, 1], v2) 311 | v3 = Vector[3, 5, 2, 1].cross(Vector[4, 3, 1, 8], Vector[2, 9, 4, 3]) 312 | assert_equal(Vector[16, -65, 139, -1], v3) 313 | assert_equal Vector[0, 0, 0, 1], 314 | Vector[1, 0, 0, 0].cross(Vector[0, 1, 0, 0], Vector[0, 0, 1, 0]) 315 | assert_equal Vector[0, 0, 0, 0, 1], 316 | Vector[1, 0, 0, 0, 0].cross(Vector[0, 1, 0, 0, 0], Vector[0, 0, 1, 0, 0], Vector[0, 0, 0, 1, 0]) 317 | assert_raise(Vector::ErrDimensionMismatch) { Vector[1, 2, 3].cross_product(Vector[1, 4]) } 318 | assert_raise(TypeError) { Vector[1, 2, 3].cross_product(42) } 319 | assert_raise(ArgumentError) { Vector[1, 2].cross_product(Vector[2, -1]) } 320 | assert_raise(Vector::ErrOperationNotDefined) { Vector[1].cross_product } 321 | end 322 | 323 | def test_angle_with 324 | assert_in_epsilon(Math::PI, Vector[1, 0].angle_with(Vector[-1, 0])) 325 | assert_in_epsilon(Math::PI/2, Vector[1, 0].angle_with(Vector[0, -1])) 326 | assert_in_epsilon(Math::PI/4, Vector[2, 2].angle_with(Vector[0, 1])) 327 | assert_in_delta(0.0, Vector[1, 1].angle_with(Vector[1, 1]), 0.00001) 328 | assert_equal(Vector[6, 6].angle_with(Vector[7, 7]), 0.0) 329 | assert_equal(Vector[6, 6].angle_with(Vector[-7, -7]), Math::PI) 330 | 331 | assert_raise(Vector::ZeroVectorError) { Vector[1, 1].angle_with(Vector[0, 0]) } 332 | assert_raise(Vector::ZeroVectorError) { Vector[0, 0].angle_with(Vector[1, 1]) } 333 | assert_raise(Matrix::ErrDimensionMismatch) { Vector[1, 2, 3].angle_with(Vector[0, 1]) } 334 | end 335 | end 336 | --------------------------------------------------------------------------------