@raven-js/cortex
Version:
Zero-dependency machine learning, AI, and data processing library for modern JavaScript
567 lines (502 loc) • 14.2 kB
JavaScript
/**
* @author Anonyfox <max@anonyfox.com>
* @license MIT
* @see {@link https://github.com/Anonyfox/ravenjs}
* @see {@link https://ravenjs.dev}
* @see {@link https://anonyfox.com}
*/
/**
* @file Matrix class for linear algebra operations using Float32Array storage.
*
* Supports matrix creation, arithmetic operations, activation functions, and serialization.
* Optimized for neural network workloads with in-place operations and direct array access.
*/
/**
* Matrix implementation for linear algebra operations using Float32Array storage.
*
* Supports matrix creation, arithmetic operations, and activation functions.
* Includes in-place operations for memory efficiency and direct array access patterns.
*
* @example
* // Create matrices
* const a = new Matrix(2, 3, [1, 2, 3, 4, 5, 6]);
* const b = new Matrix(3, 2, [1, 2, 3, 4, 5, 6]);
*
* // Matrix multiplication
* const c = a.multiply(b);
* console.log(c.toString()); // 2x2 result matrix
*/
export class Matrix {
/**
* Create a new matrix with specified dimensions.
* Optionally initialize with data array.
*
* @param {number} rows - Number of rows
* @param {number} cols - Number of columns
* @param {Array<number>|Float32Array} [data] - Initial data in row-major order
*/
constructor(rows, cols, data = null) {
if (!Number.isInteger(rows) || rows <= 0) {
throw new Error("Rows must be a positive integer");
}
if (!Number.isInteger(cols) || cols <= 0) {
throw new Error("Columns must be a positive integer");
}
this.rows = rows;
this.cols = cols;
this.size = rows * cols;
// Use Float32Array for V8 optimization and memory efficiency
this.data = new Float32Array(this.size);
// Initialize with provided data if given
if (data !== null) {
if (data.length !== this.size) {
throw new Error(
`Data length ${data.length} does not match matrix size ${this.size}`,
);
}
// Copy data to Float32Array with validation
for (let i = 0; i < this.size; i++) {
if (!Number.isFinite(data[i])) {
throw new Error(`Invalid data value at index ${i}: ${data[i]}`);
}
this.data[i] = data[i];
}
}
}
/**
* Get matrix element at specified row and column.
* Uses inline row-major indexing for V8 optimization.
*
* @param {number} row - Row index (0-based)
* @param {number} col - Column index (0-based)
* @returns {number} Matrix element value
*/
get(row, col) {
this._validateIndices(row, col);
return this.data[row * this.cols + col];
}
/**
* Set matrix element at specified row and column.
* Uses inline row-major indexing for V8 optimization.
*
* @param {number} row - Row index (0-based)
* @param {number} col - Column index (0-based)
* @param {number} value - Value to set
*/
set(row, col, value) {
this._validateIndices(row, col);
if (!Number.isFinite(value)) {
throw new Error(`Invalid value: ${value}`);
}
this.data[row * this.cols + col] = value;
}
/**
* Validate row and column indices for bounds checking.
* @private
* @param {number} row - Row index
* @param {number} col - Column index
*/
_validateIndices(row, col) {
if (!Number.isInteger(row) || row < 0 || row >= this.rows) {
throw new Error(`Row index ${row} out of bounds [0, ${this.rows})`);
}
if (!Number.isInteger(col) || col < 0 || col >= this.cols) {
throw new Error(`Column index ${col} out of bounds [0, ${this.cols})`);
}
}
/**
* Create a matrix filled with zeros.
*
* @param {number} rows - Number of rows
* @param {number} cols - Number of columns
* @returns {Matrix} New zero matrix
*/
static zeros(rows, cols) {
return new Matrix(rows, cols);
}
/**
* Create a matrix filled with ones.
*
* @param {number} rows - Number of rows
* @param {number} cols - Number of columns
* @returns {Matrix} New matrix filled with ones
*/
static ones(rows, cols) {
const matrix = new Matrix(rows, cols);
matrix.data.fill(1);
return matrix;
}
/**
* Create an identity matrix.
*
* @param {number} size - Size of square identity matrix
* @returns {Matrix} New identity matrix
*/
static identity(size) {
const matrix = new Matrix(size, size);
for (let i = 0; i < size; i++) {
matrix.set(i, i, 1);
}
return matrix;
}
/**
* Create a matrix filled with uniform random values.
*
* @param {number} rows - Number of rows
* @param {number} cols - Number of columns
* @param {number} [min=-1] - Minimum value
* @param {number} [max=1] - Maximum value
* @returns {Matrix} New matrix with uniform random values
*/
static random(rows, cols, min = -1, max = 1) {
const matrix = new Matrix(rows, cols);
const range = max - min;
for (let i = 0; i < matrix.size; i++) {
matrix.data[i] = Math.random() * range + min;
}
return matrix;
}
/**
* Matrix multiplication with another matrix.
*
* @param {Matrix} other - Matrix to multiply with
* @param {Matrix} [result] - Optional output matrix to write result
* @returns {Matrix} Result matrix (new or provided result matrix)
*/
multiply(other, result = null) {
if (!(other instanceof Matrix)) {
throw new Error("Expected Matrix instance");
}
if (this.cols !== other.rows) {
throw new Error(
`Cannot multiply ${this.rows}×${this.cols} with ${other.rows}×${other.cols}: inner dimensions must match`,
);
}
// Create result matrix if not provided
if (result === null) {
result = new Matrix(this.rows, other.cols);
} else {
if (result.rows !== this.rows || result.cols !== other.cols) {
throw new Error(
`Result matrix dimensions ${result.rows}×${result.cols} do not match expected ${this.rows}×${other.cols}`,
);
}
}
// Standard matrix multiplication
for (let i = 0; i < this.rows; i++) {
for (let j = 0; j < other.cols; j++) {
let sum = 0;
for (let k = 0; k < this.cols; k++) {
sum += this.data[i * this.cols + k] * other.data[k * other.cols + j];
}
result.data[i * result.cols + j] = sum;
}
}
return result;
}
/**
* Element-wise addition with another matrix.
* Optionally write result to provided output matrix.
*
* @param {Matrix} other - Matrix to add
* @param {Matrix} [result] - Optional output matrix
* @returns {Matrix} Result matrix
*/
add(other, result = null) {
if (!(other instanceof Matrix)) {
throw new Error("Expected Matrix instance");
}
if (this.rows !== other.rows || this.cols !== other.cols) {
throw new Error(
`Cannot add matrices of different dimensions: ${this.rows}×${this.cols} vs ${other.rows}×${other.cols}`,
);
}
if (result === null) {
result = new Matrix(this.rows, this.cols);
} else {
if (result.rows !== this.rows || result.cols !== this.cols) {
throw new Error(`Result matrix dimensions do not match`);
}
}
// Vectorized addition
for (let i = 0; i < this.size; i++) {
result.data[i] = this.data[i] + other.data[i];
}
return result;
}
/**
* Element-wise subtraction with another matrix.
*
* @param {Matrix} other - Matrix to subtract
* @param {Matrix} [result] - Optional output matrix
* @returns {Matrix} Result matrix
*/
subtract(other, result = null) {
if (!(other instanceof Matrix)) {
throw new Error("Expected Matrix instance");
}
if (this.rows !== other.rows || this.cols !== other.cols) {
throw new Error(`Cannot subtract matrices of different dimensions`);
}
if (result === null) {
result = new Matrix(this.rows, this.cols);
}
for (let i = 0; i < this.size; i++) {
result.data[i] = this.data[i] - other.data[i];
}
return result;
}
/**
* Element-wise addition with another matrix, modifying this matrix in place.
*
* @param {Matrix} other - Matrix to add
* @returns {Matrix} This matrix (for chaining)
*/
addInPlace(other) {
if (!(other instanceof Matrix)) {
throw new Error("Expected Matrix instance");
}
if (this.rows !== other.rows || this.cols !== other.cols) {
throw new Error("Matrix dimensions must match for in-place addition");
}
const thisData = this.data;
const otherData = other.data;
const size = this.size;
for (let i = 0; i < size; i++) {
thisData[i] += otherData[i];
}
return this;
}
/**
* Scalar multiplication modifying this matrix in place.
*
* @param {number} scalar - Scalar value to multiply by
* @returns {Matrix} This matrix (for chaining)
*/
scaleInPlace(scalar) {
if (!Number.isFinite(scalar)) {
throw new Error(`Invalid scalar value: ${scalar}`);
}
const data = this.data;
const size = this.size;
for (let i = 0; i < size; i++) {
data[i] *= scalar;
}
return this;
}
/**
* Transpose the matrix (flip rows and columns).
*
* @param {Matrix} [result] - Optional output matrix
* @returns {Matrix} Transposed matrix
*/
transpose(result = null) {
if (result === null) {
result = new Matrix(this.cols, this.rows);
} else {
if (result.rows !== this.cols || result.cols !== this.rows) {
throw new Error(
`Result matrix dimensions must be ${this.cols}×${this.rows} for transpose`,
);
}
}
for (let i = 0; i < this.rows; i++) {
for (let j = 0; j < this.cols; j++) {
result.data[j * result.cols + i] = this.data[i * this.cols + j];
}
}
return result;
}
/**
* Apply ReLU activation function element-wise.
* ReLU(x) = max(0, x)
*
* @param {Matrix} [result] - Optional output matrix
* @returns {Matrix} Result matrix with ReLU applied
*/
relu(result = null) {
if (result === null) {
result = new Matrix(this.rows, this.cols);
}
for (let i = 0; i < this.size; i++) {
result.data[i] = Math.max(0, this.data[i]);
}
return result;
}
/**
* Apply ReLU activation function element-wise, modifying this matrix in place.
*
* @returns {Matrix} This matrix (for chaining)
*/
reluInPlace() {
const data = this.data;
const size = this.size;
for (let i = 0; i < size; i++) {
const value = data[i];
data[i] = value > 0 ? value : 0;
}
return this;
}
/**
* Create a copy of this matrix.
*
* @returns {Matrix} New matrix with same values
*/
clone() {
const copy = new Matrix(this.rows, this.cols);
copy.data.set(this.data);
return copy;
}
/**
* Fill matrix with specified value.
*
* @param {number} value - Value to fill with
* @returns {Matrix} This matrix (for chaining)
*/
fill(value) {
if (!Number.isFinite(value)) {
throw new Error(`Invalid fill value: ${value}`);
}
this.data.fill(value);
return this;
}
/**
* Get a specific row as a new matrix.
*
* @param {number} rowIndex - Row to extract
* @returns {Matrix} Row vector (1×cols matrix)
*/
getRow(rowIndex) {
if (rowIndex < 0 || rowIndex >= this.rows) {
throw new Error(`Row index ${rowIndex} out of bounds`);
}
const row = new Matrix(1, this.cols);
const startIdx = rowIndex * this.cols;
for (let j = 0; j < this.cols; j++) {
row.data[j] = this.data[startIdx + j];
}
return row;
}
/**
* Get a specific column as a new matrix.
*
* @param {number} colIndex - Column to extract
* @returns {Matrix} Column vector (rows×1 matrix)
*/
getColumn(colIndex) {
if (colIndex < 0 || colIndex >= this.cols) {
throw new Error(`Column index ${colIndex} out of bounds`);
}
const col = new Matrix(this.rows, 1);
for (let i = 0; i < this.rows; i++) {
col.data[i] = this.data[i * this.cols + colIndex];
}
return col;
}
/**
* Calculate Frobenius norm of the matrix.
* ||A||_F = sqrt(sum of all elements squared)
*
* @returns {number} Frobenius norm
*/
norm() {
let sum = 0;
for (let i = 0; i < this.size; i++) {
sum += this.data[i] * this.data[i];
}
return Math.sqrt(sum);
}
/**
* Check if matrix equals another matrix within tolerance.
*
* @param {Matrix} other - Matrix to compare with
* @param {number} [tolerance=1e-6] - Tolerance for floating point comparison
* @returns {boolean} True if matrices are equal within tolerance
*/
equals(other, tolerance = 1e-6) {
if (!(other instanceof Matrix)) {
return false;
}
if (this.rows !== other.rows || this.cols !== other.cols) {
return false;
}
for (let i = 0; i < this.size; i++) {
if (Math.abs(this.data[i] - other.data[i]) > tolerance) {
return false;
}
}
return true;
}
/**
* Convert matrix to plain JavaScript array (row-major order).
*
* @returns {Array<Array<number>>} 2D array representation
*/
toArray() {
const result = [];
for (let i = 0; i < this.rows; i++) {
const row = [];
for (let j = 0; j < this.cols; j++) {
row.push(this.data[i * this.cols + j]);
}
result.push(row);
}
return result;
}
/**
* Convert matrix to flat array.
*
* @returns {Array<number>} Flat array in row-major order
*/
toFlatArray() {
return Array.from(this.data);
}
/**
* Serialize matrix to JSON.
*
* @returns {Object} JSON representation
*/
toJSON() {
return {
rows: this.rows,
cols: this.cols,
data: Array.from(this.data),
};
}
/**
* Create matrix from JSON representation.
*
* @param {Object} json - JSON object with rows, cols, and data
* @returns {Matrix} New matrix instance
*/
static fromJSON(json) {
if (!json || typeof json !== "object") {
throw new Error("Invalid JSON: expected object");
}
const jsonAny = /** @type {any} */ (json);
if (!Number.isInteger(jsonAny.rows) || !Number.isInteger(jsonAny.cols)) {
throw new Error("Invalid JSON: rows and cols must be integers");
}
if (!Array.isArray(jsonAny.data)) {
throw new Error("Invalid JSON: data must be an array");
}
return new Matrix(jsonAny.rows, jsonAny.cols, jsonAny.data);
}
/**
* String representation of matrix for debugging.
*
* @param {number} [precision=3] - Number of decimal places
* @returns {string} Formatted matrix string
*/
toString(precision = 3) {
const rows = [];
for (let i = 0; i < this.rows; i++) {
const row = [];
for (let j = 0; j < this.cols; j++) {
row.push(this.data[i * this.cols + j].toFixed(precision));
}
rows.push(`[${row.join(", ")}]`);
}
return `Matrix ${this.rows}×${this.cols}:\n${rows.join("\n")}`;
}
}