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