Matrix.php 11 KB

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