You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

401 lines
11 KiB

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