Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401]

   1  <?php
   2  
   3  /**
   4   *
   5   * Class for the management of Matrices
   6   *
   7   * @copyright  Copyright (c) 2018 Mark Baker (https://github.com/MarkBaker/PHPMatrix)
   8   * @license    https://opensource.org/licenses/MIT    MIT
   9   */
  10  
  11  namespace Matrix;
  12  
  13  use Generator;
  14  use Matrix\Decomposition\LU;
  15  use Matrix\Decomposition\QR;
  16  
  17  /**
  18   * Matrix object.
  19   *
  20   * @package Matrix
  21   *
  22   * @property-read int $rows The number of rows in the matrix
  23   * @property-read int $columns The number of columns in the matrix
  24   * @method Matrix antidiagonal()
  25   * @method Matrix adjoint()
  26   * @method Matrix cofactors()
  27   * @method float determinant()
  28   * @method Matrix diagonal()
  29   * @method Matrix identity()
  30   * @method Matrix inverse()
  31   * @method Matrix minors()
  32   * @method float trace()
  33   * @method Matrix transpose()
  34   * @method Matrix add(...$matrices)
  35   * @method Matrix subtract(...$matrices)
  36   * @method Matrix multiply(...$matrices)
  37   * @method Matrix divideby(...$matrices)
  38   * @method Matrix divideinto(...$matrices)
  39   * @method Matrix directsum(...$matrices)
  40   */
  41  class Matrix
  42  {
  43      protected $rows;
  44      protected $columns;
  45      protected $grid = [];
  46  
  47      /*
  48       * Create a new Matrix object from an array of values
  49       *
  50       * @param array $grid
  51       */
  52      final public function __construct(array $grid)
  53      {
  54          $this->buildFromArray(array_values($grid));
  55      }
  56  
  57      /*
  58       * Create a new Matrix object from an array of values
  59       *
  60       * @param array $grid
  61       */
  62      protected function buildFromArray(array $grid): void
  63      {
  64          $this->rows = count($grid);
  65          $columns = array_reduce(
  66              $grid,
  67              function ($carry, $value) {
  68                  return max($carry, is_array($value) ? count($value) : 1);
  69              }
  70          );
  71          $this->columns = $columns;
  72  
  73          array_walk(
  74              $grid,
  75              function (&$value) use ($columns) {
  76                  if (!is_array($value)) {
  77                      $value = [$value];
  78                  }
  79                  $value = array_pad(array_values($value), $columns, null);
  80              }
  81          );
  82  
  83          $this->grid = $grid;
  84      }
  85  
  86      /**
  87       * Validate that a row number is a positive integer
  88       *
  89       * @param int $row
  90       * @return int
  91       * @throws Exception
  92       */
  93      public static function validateRow(int $row): int
  94      {
  95          if ((!is_numeric($row)) || (intval($row) < 1)) {
  96              throw new Exception('Invalid Row');
  97          }
  98  
  99          return (int)$row;
 100      }
 101  
 102      /**
 103       * Validate that a column number is a positive integer
 104       *
 105       * @param int $column
 106       * @return int
 107       * @throws Exception
 108       */
 109      public static function validateColumn(int $column): int
 110      {
 111          if ((!is_numeric($column)) || (intval($column) < 1)) {
 112              throw new Exception('Invalid Column');
 113          }
 114  
 115          return (int)$column;
 116      }
 117  
 118      /**
 119       * Validate that a row number falls within the set of rows for this matrix
 120       *
 121       * @param int $row
 122       * @return int
 123       * @throws Exception
 124       */
 125      protected function validateRowInRange(int $row): int
 126      {
 127          $row = static::validateRow($row);
 128          if ($row > $this->rows) {
 129              throw new Exception('Requested Row exceeds matrix size');
 130          }
 131  
 132          return $row;
 133      }
 134  
 135      /**
 136       * Validate that a column number falls within the set of columns for this matrix
 137       *
 138       * @param int $column
 139       * @return int
 140       * @throws Exception
 141       */
 142      protected function validateColumnInRange(int $column): int
 143      {
 144          $column = static::validateColumn($column);
 145          if ($column > $this->columns) {
 146              throw new Exception('Requested Column exceeds matrix size');
 147          }
 148  
 149          return $column;
 150      }
 151  
 152      /**
 153       * Return a new matrix as a subset of rows from this matrix, starting at row number $row, and $rowCount rows
 154       * A $rowCount value of 0 will return all rows of the matrix from $row
 155       * A negative $rowCount value will return rows until that many rows from the end of the matrix
 156       *
 157       * Note that row numbers start from 1, not from 0
 158       *
 159       * @param int $row
 160       * @param int $rowCount
 161       * @return static
 162       * @throws Exception
 163       */
 164      public function getRows(int $row, int $rowCount = 1): Matrix
 165      {
 166          $row = $this->validateRowInRange($row);
 167          if ($rowCount === 0) {
 168              $rowCount = $this->rows - $row + 1;
 169          }
 170  
 171          return new static(array_slice($this->grid, $row - 1, (int)$rowCount));
 172      }
 173  
 174      /**
 175       * Return a new matrix as a subset of columns from this matrix, starting at column number $column, and $columnCount columns
 176       * A $columnCount value of 0 will return all columns of the matrix from $column
 177       * A negative $columnCount value will return columns until that many columns from the end of the matrix
 178       *
 179       * Note that column numbers start from 1, not from 0
 180       *
 181       * @param int $column
 182       * @param int $columnCount
 183       * @return Matrix
 184       * @throws Exception
 185       */
 186      public function getColumns(int $column, int $columnCount = 1): Matrix
 187      {
 188          $column = $this->validateColumnInRange($column);
 189          if ($columnCount < 1) {
 190              $columnCount = $this->columns + $columnCount - $column + 1;
 191          }
 192  
 193          $grid = [];
 194          for ($i = $column - 1; $i < $column + $columnCount - 1; ++$i) {
 195              $grid[] = array_column($this->grid, $i);
 196          }
 197  
 198          return (new static($grid))->transpose();
 199      }
 200  
 201      /**
 202       * Return a new matrix as a subset of rows from this matrix, dropping rows starting at row number $row,
 203       *     and $rowCount rows
 204       * A negative $rowCount value will drop rows until that many rows from the end of the matrix
 205       * A $rowCount value of 0 will remove all rows of the matrix from $row
 206       *
 207       * Note that row numbers start from 1, not from 0
 208       *
 209       * @param int $row
 210       * @param int $rowCount
 211       * @return static
 212       * @throws Exception
 213       */
 214      public function dropRows(int $row, int $rowCount = 1): Matrix
 215      {
 216          $this->validateRowInRange($row);
 217          if ($rowCount === 0) {
 218              $rowCount = $this->rows - $row + 1;
 219          }
 220  
 221          $grid = $this->grid;
 222          array_splice($grid, $row - 1, (int)$rowCount);
 223  
 224          return new static($grid);
 225      }
 226  
 227      /**
 228       * Return a new matrix as a subset of columns from this matrix, dropping columns starting at column number $column,
 229       *     and $columnCount columns
 230       * A negative $columnCount value will drop columns until that many columns from the end of the matrix
 231       * A $columnCount value of 0 will remove all columns of the matrix from $column
 232       *
 233       * Note that column numbers start from 1, not from 0
 234       *
 235       * @param int $column
 236       * @param int $columnCount
 237       * @return static
 238       * @throws Exception
 239       */
 240      public function dropColumns(int $column, int $columnCount = 1): Matrix
 241      {
 242          $this->validateColumnInRange($column);
 243          if ($columnCount < 1) {
 244              $columnCount = $this->columns + $columnCount - $column + 1;
 245          }
 246  
 247          $grid = $this->grid;
 248          array_walk(
 249              $grid,
 250              function (&$row) use ($column, $columnCount) {
 251                  array_splice($row, $column - 1, (int)$columnCount);
 252              }
 253          );
 254  
 255          return new static($grid);
 256      }
 257  
 258      /**
 259       * Return a value from this matrix, from the "cell" identified by the row and column numbers
 260       * Note that row and column numbers start from 1, not from 0
 261       *
 262       * @param int $row
 263       * @param int $column
 264       * @return mixed
 265       * @throws Exception
 266       */
 267      public function getValue(int $row, int $column)
 268      {
 269          $row = $this->validateRowInRange($row);
 270          $column = $this->validateColumnInRange($column);
 271  
 272          return $this->grid[$row - 1][$column - 1];
 273      }
 274  
 275      /**
 276       * Returns a Generator that will yield each row of the matrix in turn as a vector matrix
 277       *     or the value of each cell if the matrix is a column vector
 278       *
 279       * @return Generator|Matrix[]|mixed[]
 280       */
 281      public function rows(): Generator
 282      {
 283          foreach ($this->grid as $i => $row) {
 284              yield $i + 1 => ($this->columns == 1)
 285                  ? $row[0]
 286                  : new static([$row]);
 287          }
 288      }
 289  
 290      /**
 291       * Returns a Generator that will yield each column of the matrix in turn as a vector matrix
 292       *     or the value of each cell if the matrix is a row vector
 293       *
 294       * @return Generator|Matrix[]|mixed[]
 295       */
 296      public function columns(): Generator
 297      {
 298          for ($i = 0; $i < $this->columns; ++$i) {
 299              yield $i + 1 => ($this->rows == 1)
 300                  ? $this->grid[0][$i]
 301                  : new static(array_column($this->grid, $i));
 302          }
 303      }
 304  
 305      /**
 306       * Identify if the row and column dimensions of this matrix are equal,
 307       *     i.e. if it is a "square" matrix
 308       *
 309       * @return bool
 310       */
 311      public function isSquare(): bool
 312      {
 313          return $this->rows === $this->columns;
 314      }
 315  
 316      /**
 317       * Identify if this matrix is a vector
 318       *     i.e. if it comprises only a single row or a single column
 319       *
 320       * @return bool
 321       */
 322      public function isVector(): bool
 323      {
 324          return $this->rows === 1 || $this->columns === 1;
 325      }
 326  
 327      /**
 328       * Return the matrix as a 2-dimensional array
 329       *
 330       * @return array
 331       */
 332      public function toArray(): array
 333      {
 334          return $this->grid;
 335      }
 336  
 337      /**
 338       * Solve A*X = B.
 339       *
 340       * @param Matrix $B Right hand side
 341       *
 342       * @throws Exception
 343       *
 344       * @return Matrix ... Solution if A is square, least squares solution otherwise
 345       */
 346      public function solve(Matrix $B): Matrix
 347      {
 348          if ($this->columns === $this->rows) {
 349              return (new LU($this))->solve($B);
 350          }
 351  
 352          return (new QR($this))->solve($B);
 353      }
 354  
 355      protected static $getters = [
 356          'rows',
 357          'columns',
 358      ];
 359  
 360      /**
 361       * Access specific properties as read-only (no setters)
 362       *
 363       * @param string $propertyName
 364       * @return mixed
 365       * @throws Exception
 366       */
 367      public function __get(string $propertyName)
 368      {
 369          $propertyName = strtolower($propertyName);
 370  
 371          // Test for function calls
 372          if (in_array($propertyName, self::$getters)) {
 373              return $this->$propertyName;
 374          }
 375  
 376          throw new Exception('Property does not exist');
 377      }
 378  
 379      protected static $functions = [
 380          'adjoint',
 381          'antidiagonal',
 382          'cofactors',
 383          'determinant',
 384          'diagonal',
 385          'identity',
 386          'inverse',
 387          'minors',
 388          'trace',
 389          'transpose',
 390      ];
 391  
 392      protected static $operations = [
 393          'add',
 394          'subtract',
 395          'multiply',
 396          'divideby',
 397          'divideinto',
 398          'directsum',
 399      ];
 400  
 401      /**
 402       * Returns the result of the function call or operation
 403       *
 404       * @param string $functionName
 405       * @param mixed[] $arguments
 406       * @return Matrix|float
 407       * @throws Exception
 408       */
 409      public function __call(string $functionName, $arguments)
 410      {
 411          $functionName = strtolower(str_replace('_', '', $functionName));
 412  
 413          // Test for function calls
 414          if (in_array($functionName, self::$functions, true)) {
 415              return Functions::$functionName($this, ...$arguments);
 416          }
 417          // Test for operation calls
 418          if (in_array($functionName, self::$operations, true)) {
 419              return Operations::$functionName($this, ...$arguments);
 420          }
 421          throw new Exception('Function or Operation does not exist');
 422      }
 423  }