Skip to content

Commit

Permalink
Add Matrices section with basic Matrix operations (multiplication, tr…
Browse files Browse the repository at this point in the history
…ansposition, etc.)
  • Loading branch information
trekhleb committed Dec 19, 2020
1 parent e220450 commit 2c81deb
Show file tree
Hide file tree
Showing 5 changed files with 852 additions and 21 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ a set of rules that precisely define a sequence of operations.
* `B` [Radian & Degree](src/algorithms/math/radian) - radians to degree and backwards conversion
* `B` [Fast Powering](src/algorithms/math/fast-powering)
* `B` [Horner's method](src/algorithms/math/horner-method) - polynomial evaluation
* `B` [Matrices](src/algorithms/math/matrix) - matrices and basic matrix operations (multiplication, transposition, etc.)
* `A` [Integer Partition](src/algorithms/math/integer-partition)
* `A` [Square Root](src/algorithms/math/square-root) - Newton's method
* `A` [Liu Hui π Algorithm](src/algorithms/math/liu-hui) - approximate π calculations based on N-gons
Expand Down
45 changes: 24 additions & 21 deletions src/algorithms/cryptography/hill-cipher/hillCipher.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import * as mtrx from '../../math/matrix/Matrix';

// The code of an 'A' character (equals to 65).
const alphabetCodeShift = 'A'.codePointAt(0);
const englishAlphabetSize = 26;
Expand All @@ -15,33 +17,36 @@ const generateKeyMatrix = (keyString) => {
'Invalid key string length. The square root of the key string must be an integer',
);
}
const keyMatrix = [];
let keyStringIndex = 0;
for (let i = 0; i < matrixSize; i += 1) {
const keyMatrixRow = [];
for (let j = 0; j < matrixSize; j += 1) {
return mtrx.generate(
[matrixSize, matrixSize],
// Callback to get a value of each matrix cell.
// The order the matrix is being filled in is from left to right, from top to bottom.
() => {
// A → 0, B → 1, ..., a → 32, b → 33, ...
const charCodeShifted = (keyString.codePointAt(keyStringIndex)) % alphabetCodeShift;
keyMatrixRow.push(charCodeShifted);
keyStringIndex += 1;
}
keyMatrix.push(keyMatrixRow);
}
return keyMatrix;
return charCodeShifted;
},
);
};

/**
* Generates a message vector from a given message.
*
* @param {string} message - the message to encrypt.
* @return {number[]} messageVector
* @return {number[][]} messageVector
*/
const generateMessageVector = (message) => {
const messageVector = [];
for (let i = 0; i < message.length; i += 1) {
messageVector.push(message.codePointAt(i) % alphabetCodeShift);
}
return messageVector;
return mtrx.generate(
[message.length, 1],
// Callback to get a value of each matrix cell.
// The order the matrix is being filled in is from left to right, from top to bottom.
(cellIndices) => {
const rowIndex = cellIndices[0];
return message.codePointAt(rowIndex) % alphabetCodeShift;
},
);
};

/**
Expand All @@ -59,19 +64,17 @@ export function hillCipherEncrypt(message, keyString) {
}

const keyMatrix = generateKeyMatrix(keyString);
const messageVector = generateMessageVector(message);

// keyString.length must equal to square of message.length
if (keyMatrix.length !== message.length) {
throw new Error('Invalid key string length. The key length must be a square of message length');
}

const messageVector = generateMessageVector(message);
const cipherVector = mtrx.dot(keyMatrix, messageVector);
let cipherString = '';
for (let row = 0; row < keyMatrix.length; row += 1) {
let item = 0;
for (let column = 0; column < keyMatrix.length; column += 1) {
item += keyMatrix[row][column] * messageVector[column];
}
for (let row = 0; row < cipherVector.length; row += 1) {
const item = cipherVector[row];
cipherString += String.fromCharCode((item % englishAlphabetSize) + alphabetCodeShift);
}

Expand Down
309 changes: 309 additions & 0 deletions src/algorithms/math/matrix/Matrix.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,309 @@
/**
* @typedef {number} Cell
* @typedef {Cell[][]|Cell[][][]} Matrix
* @typedef {number[]} Shape
* @typedef {number[]} CellIndices
*/

/**
* Gets the matrix's shape.
*
* @param {Matrix} m
* @returns {Shape}
*/
export const shape = (m) => {
const shapes = [];
let dimension = m;
while (dimension && Array.isArray(dimension)) {
shapes.push(dimension.length);
dimension = (dimension.length && [...dimension][0]) || null;
}
return shapes;
};

/**
* Checks if matrix has a correct type.
*
* @param {Matrix} m
* @throws {Error}
*/
const validateType = (m) => {
if (
!m
|| !Array.isArray(m)
|| !Array.isArray(m[0])
) {
throw new Error('Invalid matrix format');
}
};

/**
* Checks if matrix is two dimensional.
*
* @param {Matrix} m
* @throws {Error}
*/
const validate2D = (m) => {
validateType(m);
const aShape = shape(m);
if (aShape.length !== 2) {
throw new Error('Matrix is not of 2D shape');
}
};

/**
* Validates that matrices are of the same shape.
*
* @param {Matrix} a
* @param {Matrix} b
* @trows {Error}
*/
const validateSameShape = (a, b) => {
validateType(a);
validateType(b);

const aShape = shape(a);
const bShape = shape(b);

if (aShape.length !== bShape.length) {
throw new Error('Matrices have different dimensions');
}

while (aShape.length && bShape.length) {
if (aShape.pop() !== bShape.pop()) {
throw new Error('Matrices have different shapes');
}
}
};

/**
* Generates the matrix of specific shape with specific values.
*
* @param {Shape} mShape - the shape of the matrix to generate
* @param {function({CellIndex}): Cell} fill - cell values of a generated matrix.
* @returns {Matrix}
*/
export const generate = (mShape, fill) => {
/**
* Generates the matrix recursively.
*
* @param {Shape} recShape - the shape of the matrix to generate
* @param {CellIndices} recIndices
* @returns {Matrix}
*/
const generateRecursively = (recShape, recIndices) => {
if (recShape.length === 1) {
return Array(recShape[0])
.fill(null)
.map((cellValue, cellIndex) => fill([...recIndices, cellIndex]));
}
const m = [];
for (let i = 0; i < recShape[0]; i += 1) {
m.push(generateRecursively(recShape.slice(1), [...recIndices, i]));
}
return m;
};

return generateRecursively(mShape, []);
};

/**
* Generates the matrix of zeros of specified shape.
*
* @param {Shape} mShape - shape of the matrix
* @returns {Matrix}
*/
export const zeros = (mShape) => {
return generate(mShape, () => 0);
};

/**
* @param {Matrix} a
* @param {Matrix} b
* @return Matrix
* @throws {Error}
*/
export const dot = (a, b) => {
// Validate inputs.
validate2D(a);
validate2D(b);

// Check dimensions.
const aShape = shape(a);
const bShape = shape(b);
if (aShape[1] !== bShape[0]) {
throw new Error('Matrices have incompatible shape for multiplication');
}

// Perform matrix multiplication.
const outputShape = [aShape[0], bShape[1]];
const c = zeros(outputShape);

for (let bCol = 0; bCol < b[0].length; bCol += 1) {
for (let aRow = 0; aRow < a.length; aRow += 1) {
let cellSum = 0;
for (let aCol = 0; aCol < a[aRow].length; aCol += 1) {
cellSum += a[aRow][aCol] * b[aCol][bCol];
}
c[aRow][bCol] = cellSum;
}
}

return c;
};

/**
* Transposes the matrix.
*
* @param {Matrix} m
* @returns Matrix
* @throws {Error}
*/
export const t = (m) => {
validate2D(m);
const mShape = shape(m);
const transposed = zeros([mShape[1], mShape[0]]);
for (let row = 0; row < m.length; row += 1) {
for (let col = 0; col < m[0].length; col += 1) {
transposed[col][row] = m[row][col];
}
}
return transposed;
};

/**
* Traverses the matrix.
*
* @param {Matrix} m
* @param {function(indices: CellIndices, c: Cell)} visit
*/
const walk = (m, visit) => {
/**
* Traverses the matrix recursively.
*
* @param {Matrix} recM
* @param {CellIndices} cellIndices
* @return {Matrix}
*/
const recWalk = (recM, cellIndices) => {
const recMShape = shape(recM);

if (recMShape.length === 1) {
for (let i = 0; i < recM.length; i += 1) {
visit([...cellIndices, i], recM[i]);
}
}
for (let i = 0; i < recM.length; i += 1) {
recWalk(recM[i], [...cellIndices, i]);
}
};

recWalk(m, []);
};

/**
* Gets the matrix cell value at specific index.
*
* @param {Matrix} m - Matrix that contains the cell that needs to be updated
* @param {CellIndices} cellIndices - Array of cell indices
* @return {Cell}
*/
const getCellAtIndex = (m, cellIndices) => {
// We start from the row at specific index.
let cell = m[cellIndices[0]];
// Going deeper into the next dimensions but not to the last one to preserve
// the pointer to the last dimension array.
for (let dimIdx = 1; dimIdx < cellIndices.length - 1; dimIdx += 1) {
cell = cell[cellIndices[dimIdx]];
}
// At this moment the cell variable points to the array at the last needed dimension.
return cell[cellIndices[cellIndices.length - 1]];
};

/**
* Update the matrix cell at specific index.
*
* @param {Matrix} m - Matrix that contains the cell that needs to be updated
* @param {CellIndices} cellIndices - Array of cell indices
* @param {Cell} cellValue - New cell value
*/
const updateCellAtIndex = (m, cellIndices, cellValue) => {
// We start from the row at specific index.
let cell = m[cellIndices[0]];
// Going deeper into the next dimensions but not to the last one to preserve
// the pointer to the last dimension array.
for (let dimIdx = 1; dimIdx < cellIndices.length - 1; dimIdx += 1) {
cell = cell[cellIndices[dimIdx]];
}
// At this moment the cell variable points to the array at the last needed dimension.
cell[cellIndices[cellIndices.length - 1]] = cellValue;
};

/**
* Adds two matrices element-wise.
*
* @param {Matrix} a
* @param {Matrix} b
* @return {Matrix}
*/
export const add = (a, b) => {
validateSameShape(a, b);
const result = zeros(shape(a));

walk(a, (cellIndices, cellValue) => {
updateCellAtIndex(result, cellIndices, cellValue);
});

walk(b, (cellIndices, cellValue) => {
const currentCellValue = getCellAtIndex(result, cellIndices);
updateCellAtIndex(result, cellIndices, currentCellValue + cellValue);
});

return result;
};

/**
* Multiplies two matrices element-wise.
*
* @param {Matrix} a
* @param {Matrix} b
* @return {Matrix}
*/
export const mul = (a, b) => {
validateSameShape(a, b);
const result = zeros(shape(a));

walk(a, (cellIndices, cellValue) => {
updateCellAtIndex(result, cellIndices, cellValue);
});

walk(b, (cellIndices, cellValue) => {
const currentCellValue = getCellAtIndex(result, cellIndices);
updateCellAtIndex(result, cellIndices, currentCellValue * cellValue);
});

return result;
};

/**
* Subtract two matrices element-wise.
*
* @param {Matrix} a
* @param {Matrix} b
* @return {Matrix}
*/
export const sub = (a, b) => {
validateSameShape(a, b);
const result = zeros(shape(a));

walk(a, (cellIndices, cellValue) => {
updateCellAtIndex(result, cellIndices, cellValue);
});

walk(b, (cellIndices, cellValue) => {
const currentCellValue = getCellAtIndex(result, cellIndices);
updateCellAtIndex(result, cellIndices, currentCellValue - cellValue);
});

return result;
};
Loading

0 comments on commit 2c81deb

Please sign in to comment.