Spaces:
Running
Running
| const operations = [ | |
| { | |
| id: "add", | |
| label: "Add two matrices (element-wise)", | |
| description: "A + B. Shapes of A and B should be equal or broadcast-compatible.", | |
| inputs: 2, | |
| code: "output = A + B", | |
| match: ["add", "sum", "plus", "element-wise add"], | |
| run: (A, B) => elementWise(A, B, (a, b) => a + b, "add"), | |
| }, | |
| { | |
| id: "subtract", | |
| label: "Subtract two matrices (element-wise)", | |
| description: "A - B. Shapes of A and B should be equal or broadcast-compatible.", | |
| inputs: 2, | |
| code: "output = A - B", | |
| match: ["subtract", "minus", "difference"], | |
| run: (A, B) => elementWise(A, B, (a, b) => a - b, "subtract"), | |
| }, | |
| { | |
| id: "multiply", | |
| label: "Multiply two matrices (element-wise)", | |
| description: "A * B. Shapes of A and B should be equal or broadcast-compatible.", | |
| inputs: 2, | |
| code: "output = A * B", | |
| match: ["multiply", "times", "element-wise multiply"], | |
| run: (A, B) => elementWise(A, B, (a, b) => a * b, "multiply"), | |
| }, | |
| { | |
| id: "matmul", | |
| label: "Matrix multiplication (np.matmul)", | |
| description: "np.matmul(A, B). Works for 2D and stacked arrays (up to 3D here).", | |
| inputs: 2, | |
| code: "output = np.matmul(A, B)", | |
| match: ["matmul", "matrix multiplication", "dot product", "@"], | |
| run: (A, B) => matmul(A, B), | |
| }, | |
| { | |
| id: "transpose", | |
| label: "Transpose matrix (np.transpose)", | |
| description: "Swap axes. For 2D, rows and columns are flipped.", | |
| inputs: 1, | |
| code: "output = np.transpose(A)", | |
| match: ["transpose", "swap axes", "flip rows columns"], | |
| run: (A) => transpose(A), | |
| }, | |
| { | |
| id: "reshape", | |
| label: "Reshape matrix (np.reshape)", | |
| description: "Reshape A into the target shape with same total number of elements.", | |
| inputs: 1, | |
| code: "output = np.reshape(A, new_shape)", | |
| match: ["reshape", "change shape"], | |
| requiresTarget: true, | |
| run: (A, _unused, targetShape) => reshape(A, targetShape), | |
| }, | |
| { | |
| id: "concat", | |
| label: "Concatenate two arrays (np.concatenate)", | |
| description: "Join A and B along axis 0. Non-concat dimensions must match.", | |
| inputs: 2, | |
| code: "output = np.concatenate([A, B], axis=0)", | |
| match: ["concatenate", "concat", "join arrays"], | |
| run: (A, B) => concatAxis0(A, B), | |
| }, | |
| { | |
| id: "stack", | |
| label: "Stack two arrays (np.stack)", | |
| description: "Stack A and B along a new axis 0. A and B shapes must be identical.", | |
| inputs: 2, | |
| code: "output = np.stack([A, B], axis=0)", | |
| match: ["stack", "new axis"], | |
| run: (A, B) => stackAxis0(A, B), | |
| }, | |
| { | |
| id: "sum", | |
| label: "Sum matrix values (np.sum)", | |
| description: "Sum values on an optional axis with optional keepdims.", | |
| inputs: 1, | |
| code: "output = np.sum(A)", | |
| match: ["sum all", "total", "np.sum", "sum"], | |
| supportsReductionOptions: true, | |
| run: (A, _unused, _unused2, options) => | |
| reduceArray(A, { | |
| mode: "sum", | |
| axis: options.axis, | |
| keepdims: options.keepdims, | |
| }), | |
| }, | |
| { | |
| id: "mean", | |
| label: "Mean matrix values (np.mean)", | |
| description: "Compute mean on an optional axis with optional keepdims.", | |
| inputs: 1, | |
| code: "output = np.mean(A)", | |
| match: ["mean", "average", "np.mean"], | |
| supportsReductionOptions: true, | |
| run: (A, _unused, _unused2, options) => | |
| reduceArray(A, { | |
| mode: "mean", | |
| axis: options.axis, | |
| keepdims: options.keepdims, | |
| }), | |
| }, | |
| { | |
| id: "ones", | |
| label: "Create ones matrix (np.ones)", | |
| description: "Generate an array filled with ones for the given shape.", | |
| inputs: 1, | |
| code: "output = np.ones(shape)", | |
| match: ["ones", "all ones", "np.ones"], | |
| generatorOnly: true, | |
| run: (A) => clone(A), | |
| }, | |
| { | |
| id: "zeros", | |
| label: "Create zeros matrix (np.zeros)", | |
| description: "Generate an array filled with zeros for the given shape.", | |
| inputs: 1, | |
| code: "output = np.zeros(shape)", | |
| match: ["zeros", "all zeros", "np.zeros"], | |
| generatorOnly: true, | |
| run: (A) => clone(A), | |
| }, | |
| ]; | |
| const operationSelect = document.getElementById("operationSelect"); | |
| const nlInput = document.getElementById("nlInput"); | |
| const matchBtn = document.getElementById("matchBtn"); | |
| const runBtn = document.getElementById("runBtn"); | |
| const operationInfo = document.getElementById("operationInfo"); | |
| const shapeInputs = document.getElementById("shapeInputs"); | |
| const operationOptions = document.getElementById("operationOptions"); | |
| const codeOutput = document.getElementById("codeOutput"); | |
| const inputViz = document.getElementById("inputViz"); | |
| const outputViz = document.getElementById("outputViz"); | |
| const detailViz = document.getElementById("detailViz"); | |
| let lastRunMeta = null; | |
| function init() { | |
| operations.forEach((op) => { | |
| const option = document.createElement("option"); | |
| option.value = op.id; | |
| option.textContent = op.label; | |
| operationSelect.appendChild(option); | |
| }); | |
| operationSelect.value = "add"; | |
| renderShapeInputs(); | |
| bindEvents(); | |
| } | |
| function bindEvents() { | |
| operationSelect.addEventListener("change", renderShapeInputs); | |
| matchBtn.addEventListener("click", () => { | |
| const query = nlInput.value.trim().toLowerCase(); | |
| if (!query) return; | |
| let best = operations[0]; | |
| let bestScore = 0; | |
| for (const op of operations) { | |
| let score = 0; | |
| for (const key of op.match) { | |
| if (query.includes(key)) score += key.length; | |
| } | |
| if (score > bestScore) { | |
| best = op; | |
| bestScore = score; | |
| } | |
| } | |
| operationSelect.value = best.id; | |
| renderShapeInputs(); | |
| }); | |
| runBtn.addEventListener("click", () => { | |
| try { | |
| lastRunMeta = null; | |
| const op = currentOperation(); | |
| const parsed = parseAllShapes(op); | |
| const inputArrays = buildInputArrays(op, parsed); | |
| const options = parsed.options || {}; | |
| const output = op.run( | |
| inputArrays[0], | |
| inputArrays[1], | |
| parsed.targetShape || null, | |
| options | |
| ); | |
| renderCode(op, parsed); | |
| renderInputs(inputArrays, op); | |
| renderOutput(output); | |
| renderComputationDetails(op, inputArrays, output, parsed, lastRunMeta); | |
| } catch (err) { | |
| codeOutput.textContent = "Error: " + err.message; | |
| inputViz.innerHTML = ""; | |
| outputViz.innerHTML = `<p class=\"error\">${escapeHtml(err.message)}</p>`; | |
| detailViz.innerHTML = ""; | |
| } | |
| }); | |
| } | |
| function currentOperation() { | |
| return operations.find((op) => op.id === operationSelect.value); | |
| } | |
| function renderShapeInputs() { | |
| const op = currentOperation(); | |
| operationInfo.innerHTML = `<strong>${op.label}</strong><br/>${escapeHtml(op.description)}`; | |
| const cards = []; | |
| for (let i = 0; i < op.inputs; i += 1) { | |
| cards.push(shapeCard(`inputShape${i + 1}`, `Input ${i + 1} shape`, "2,3")); | |
| } | |
| if (op.requiresTarget) { | |
| cards.push(shapeCard("targetShape", "Target shape", "3,2")); | |
| } | |
| shapeInputs.innerHTML = cards.join(""); | |
| operationOptions.innerHTML = op.supportsReductionOptions | |
| ? reductionOptionsCard() | |
| : ""; | |
| const s1 = document.getElementById("inputShape1"); | |
| const s2 = document.getElementById("inputShape2"); | |
| if (op.id === "matmul") { | |
| s1.value = "2,3"; | |
| if (s2) s2.value = "3,2"; | |
| } else if (op.id === "concat" || op.id === "stack") { | |
| s1.value = "2,2"; | |
| if (s2) s2.value = "2,2"; | |
| } else if (op.id === "transpose") { | |
| s1.value = "2,3"; | |
| } else if (op.id === "reshape") { | |
| s1.value = "2,3"; | |
| document.getElementById("targetShape").value = "3,2"; | |
| } else if (op.id === "ones" || op.id === "zeros") { | |
| s1.value = "3,3"; | |
| } else if (op.id === "sum" || op.id === "mean") { | |
| s1.value = "2,3"; | |
| } | |
| if (op.supportsReductionOptions) { | |
| const axisInput = document.getElementById("reduceAxis"); | |
| const keepdimsInput = document.getElementById("reduceKeepdims"); | |
| const setAxisHint = () => { | |
| try { | |
| const dims = parseShape(document.getElementById("inputShape1").value); | |
| axisInput.placeholder = `axis (optional): 0 to ${dims.length - 1}`; | |
| } catch (_err) { | |
| axisInput.placeholder = "axis (optional): 0"; | |
| } | |
| }; | |
| document.getElementById("inputShape1").addEventListener("input", setAxisHint); | |
| setAxisHint(); | |
| keepdimsInput.checked = false; | |
| } | |
| detailViz.innerHTML = ""; | |
| } | |
| function shapeCard(inputId, label, placeholder) { | |
| return ` | |
| <div class="shape-card"> | |
| <h3>${label}</h3> | |
| <input id="${inputId}" type="text" placeholder="${placeholder}" /> | |
| <small>Use comma-separated dimensions, max 3D, e.g. 2,3 or 2,2,3</small> | |
| </div> | |
| `; | |
| } | |
| function reductionOptionsCard() { | |
| return ` | |
| <div class="shape-card options-card"> | |
| <h3>Reduction options</h3> | |
| <div class="inline-fields"> | |
| <label class="mini-label" for="reduceAxis">axis</label> | |
| <input id="reduceAxis" type="text" placeholder="axis (optional): 0" /> | |
| </div> | |
| <div class="inline-fields"> | |
| <label class="mini-label" for="reduceKeepdims">keepdims</label> | |
| <input id="reduceKeepdims" type="checkbox" /> | |
| </div> | |
| <small>Leave axis empty to reduce all dimensions. keepdims keeps reduced axes as size 1.</small> | |
| </div> | |
| `; | |
| } | |
| function parseAllShapes(op) { | |
| const shapes = []; | |
| for (let i = 0; i < op.inputs; i += 1) { | |
| const input = document.getElementById(`inputShape${i + 1}`); | |
| shapes.push(parseShape(input.value)); | |
| } | |
| const parsed = { shapes }; | |
| if (op.requiresTarget) { | |
| parsed.targetShape = parseShape(document.getElementById("targetShape").value); | |
| } | |
| if (op.supportsReductionOptions) { | |
| const axisRaw = document.getElementById("reduceAxis").value.trim(); | |
| const keepdims = document.getElementById("reduceKeepdims").checked; | |
| let axis = null; | |
| if (axisRaw !== "") { | |
| axis = Number(axisRaw); | |
| if (!Number.isInteger(axis)) { | |
| throw new Error("axis must be an integer or left empty."); | |
| } | |
| if (axis < 0 || axis >= shapes[0].length) { | |
| throw new Error(`axis out of range for input rank ${shapes[0].length}.`); | |
| } | |
| } | |
| parsed.options = { axis, keepdims }; | |
| } | |
| return parsed; | |
| } | |
| function parseShape(raw) { | |
| if (!raw || !raw.trim()) { | |
| throw new Error("Shape cannot be empty."); | |
| } | |
| let normalized = raw.trim(); | |
| if (normalized.startsWith("(") && normalized.endsWith(")")) { | |
| normalized = normalized.slice(1, -1); | |
| } | |
| normalized = normalized.trim(); | |
| const dims = normalized | |
| .split(",") | |
| .map((x) => x.trim()) | |
| .filter((x) => x.length > 0) | |
| .map((x) => Number(x)); | |
| if (dims.length < 1 || dims.length > 3) { | |
| throw new Error("Each shape must have 1 to 3 dimensions."); | |
| } | |
| dims.forEach((d) => { | |
| if (!Number.isFinite(d) || !Number.isInteger(d) || d < 1 || d > 6) { | |
| throw new Error("Dimensions must be integers between 1 and 6."); | |
| } | |
| }); | |
| return dims; | |
| } | |
| function buildInputArrays(op, parsed) { | |
| if (op.id === "ones") { | |
| return [fillArray(parsed.shapes[0], 1)]; | |
| } | |
| if (op.id === "zeros") { | |
| return [fillArray(parsed.shapes[0], 0)]; | |
| } | |
| const [shapeA, shapeB] = parsed.shapes; | |
| const A = randomArray(shapeA); | |
| if (op.inputs === 1) return [A]; | |
| let B; | |
| if (["add", "subtract", "multiply"].includes(op.id)) { | |
| B = randomArray(shapeB || shapeA); | |
| } else if (op.id === "matmul") { | |
| B = randomArray(shapeB); | |
| } else if (op.id === "concat" || op.id === "stack") { | |
| B = randomArray(shapeB); | |
| } else { | |
| B = randomArray(shapeB || shapeA); | |
| } | |
| return [A, B]; | |
| } | |
| function randomArray(shape) { | |
| return createByShape(shape, () => Math.floor(Math.random() * 9) + 1); | |
| } | |
| function fillArray(shape, val) { | |
| return createByShape(shape, () => val); | |
| } | |
| function createByShape(shape, valueFn, level = 0) { | |
| const len = shape[level]; | |
| const arr = new Array(len); | |
| for (let i = 0; i < len; i += 1) { | |
| arr[i] = | |
| level === shape.length - 1 | |
| ? valueFn() | |
| : createByShape(shape, valueFn, level + 1); | |
| } | |
| return arr; | |
| } | |
| function shapeOf(arr) { | |
| if (!Array.isArray(arr)) return []; | |
| return [arr.length, ...shapeOf(arr[0])]; | |
| } | |
| function formatShape(shape) { | |
| return `(${shape.join(", ")})`; | |
| } | |
| function broadcastShapes(shapeA, shapeB) { | |
| const maxRank = Math.max(shapeA.length, shapeB.length); | |
| const out = new Array(maxRank); | |
| for (let i = 0; i < maxRank; i += 1) { | |
| const a = shapeA[shapeA.length - 1 - i] ?? 1; | |
| const b = shapeB[shapeB.length - 1 - i] ?? 1; | |
| if (a !== b && a !== 1 && b !== 1) { | |
| throw new Error( | |
| `Broadcast mismatch at dimension ${maxRank - i - 1}: ${a} vs ${b}.` | |
| ); | |
| } | |
| out[maxRank - 1 - i] = Math.max(a, b); | |
| } | |
| return out; | |
| } | |
| function getAtIndices(arr, indices) { | |
| let cur = arr; | |
| for (let i = 0; i < indices.length; i += 1) { | |
| cur = cur[indices[i]]; | |
| } | |
| return cur; | |
| } | |
| function createByShapeIndexed(shape, valueFn, idx = []) { | |
| if (shape.length === 0) return valueFn(idx); | |
| const dim = shape[idx.length]; | |
| const out = new Array(dim); | |
| for (let i = 0; i < dim; i += 1) { | |
| const nextIdx = idx.concat(i); | |
| if (nextIdx.length === shape.length) { | |
| out[i] = valueFn(nextIdx); | |
| } else { | |
| out[i] = createByShapeIndexed(shape, valueFn, nextIdx); | |
| } | |
| } | |
| return out; | |
| } | |
| function projectBroadcastIndices(outputIndices, sourceShape) { | |
| const offset = outputIndices.length - sourceShape.length; | |
| const mapped = []; | |
| for (let i = 0; i < sourceShape.length; i += 1) { | |
| const srcDim = sourceShape[i]; | |
| const outIndex = outputIndices[offset + i]; | |
| mapped.push(srcDim === 1 ? 0 : outIndex); | |
| } | |
| return mapped; | |
| } | |
| function elementWise(A, B, fn, opName) { | |
| const shapeA = shapeOf(A); | |
| const shapeB = shapeOf(B); | |
| const outputShape = broadcastShapes(shapeA, shapeB); | |
| const output = createByShapeIndexed(outputShape, (outIdx) => { | |
| const idxA = projectBroadcastIndices(outIdx, shapeA); | |
| const idxB = projectBroadcastIndices(outIdx, shapeB); | |
| return fn(getAtIndices(A, idxA), getAtIndices(B, idxB)); | |
| }); | |
| const usedBroadcasting = | |
| shapeA.length !== shapeB.length || | |
| shapeA.some((dim, i) => dim !== shapeB[i]) || | |
| shapeA.join(",") !== outputShape.join(",") || | |
| shapeB.join(",") !== outputShape.join(","); | |
| lastRunMeta = { | |
| kind: "elementwise", | |
| opName, | |
| shapeA, | |
| shapeB, | |
| outputShape, | |
| usedBroadcasting, | |
| }; | |
| return output; | |
| } | |
| function elementWiseDeep(A, B, fn) { | |
| if (!Array.isArray(A) && !Array.isArray(B)) return fn(A, B); | |
| return A.map((v, i) => elementWiseDeep(v, B[i], fn)); | |
| } | |
| function transpose(A) { | |
| const shape = shapeOf(A); | |
| if (shape.length === 1) return clone(A); | |
| if (shape.length === 2) { | |
| const [rows, cols] = shape; | |
| const out = []; | |
| for (let c = 0; c < cols; c += 1) { | |
| const row = []; | |
| for (let r = 0; r < rows; r += 1) { | |
| row.push(A[r][c]); | |
| } | |
| out.push(row); | |
| } | |
| return out; | |
| } | |
| if (shape.length === 3) { | |
| const [d0, d1, d2] = shape; | |
| const out = []; | |
| for (let i = 0; i < d2; i += 1) { | |
| const level2 = []; | |
| for (let j = 0; j < d1; j += 1) { | |
| const row = []; | |
| for (let k = 0; k < d0; k += 1) { | |
| row.push(A[k][j][i]); | |
| } | |
| level2.push(row); | |
| } | |
| out.push(level2); | |
| } | |
| return out; | |
| } | |
| throw new Error("Transpose supports up to 3D in this interface."); | |
| } | |
| function reshape(A, targetShape) { | |
| const flat = flatten(A); | |
| const totalA = flat.length; | |
| const totalTarget = targetShape.reduce((x, y) => x * y, 1); | |
| if (totalA !== totalTarget) { | |
| throw new Error( | |
| `reshape needs same number of elements. Got ${totalA} and ${totalTarget}.` | |
| ); | |
| } | |
| return unflatten(flat, targetShape); | |
| } | |
| function flatten(arr) { | |
| if (!Array.isArray(arr)) return [arr]; | |
| return arr.flatMap((x) => flatten(x)); | |
| } | |
| function unflatten(flat, shape) { | |
| let idx = 0; | |
| function build(level = 0) { | |
| const len = shape[level]; | |
| const out = []; | |
| for (let i = 0; i < len; i += 1) { | |
| if (level === shape.length - 1) { | |
| out.push(flat[idx]); | |
| idx += 1; | |
| } else { | |
| out.push(build(level + 1)); | |
| } | |
| } | |
| return out; | |
| } | |
| return build(); | |
| } | |
| function matmul(A, B) { | |
| const sA = shapeOf(A); | |
| const sB = shapeOf(B); | |
| const leftVec = sA.length === 1; | |
| const rightVec = sB.length === 1; | |
| const leftBatch = sA.length === 3 ? sA[0] : 1; | |
| const rightBatch = sB.length === 3 ? sB[0] : 1; | |
| const outBatch = Math.max(leftBatch, rightBatch); | |
| if (leftBatch !== rightBatch && leftBatch !== 1 && rightBatch !== 1) { | |
| throw new Error( | |
| `matmul batch broadcast mismatch: ${leftBatch} vs ${rightBatch}.` | |
| ); | |
| } | |
| const leftRows = leftVec ? 1 : sA[sA.length - 2]; | |
| const leftInner = sA[sA.length - 1]; | |
| const rightInner = rightVec ? sB[0] : sB[sB.length - 2]; | |
| const rightCols = rightVec ? 1 : sB[sB.length - 1]; | |
| if (leftInner !== rightInner) { | |
| throw new Error( | |
| `matmul shape mismatch on core dims: ${formatShape(sA)} @ ${formatShape( | |
| sB | |
| )} (inner ${leftInner} vs ${rightInner}).` | |
| ); | |
| } | |
| const getLeft = (batch, row, k) => { | |
| if (leftVec) return A[k]; | |
| if (sA.length === 2) return A[row][k]; | |
| const batchIdx = leftBatch === 1 ? 0 : batch; | |
| return A[batchIdx][row][k]; | |
| }; | |
| const getRight = (batch, k, col) => { | |
| if (rightVec) return B[k]; | |
| if (sB.length === 2) return B[k][col]; | |
| const batchIdx = rightBatch === 1 ? 0 : batch; | |
| return B[batchIdx][k][col]; | |
| }; | |
| const matrixForBatch = (batch) => { | |
| const out = []; | |
| for (let r = 0; r < leftRows; r += 1) { | |
| const row = []; | |
| for (let c = 0; c < rightCols; c += 1) { | |
| let sum = 0; | |
| for (let k = 0; k < leftInner; k += 1) { | |
| sum += getLeft(batch, r, k) * getRight(batch, k, c); | |
| } | |
| row.push(sum); | |
| } | |
| out.push(row); | |
| } | |
| return out; | |
| }; | |
| const hasBatchAxis = sA.length > 2 || sB.length > 2; | |
| const matrices = hasBatchAxis | |
| ? Array.from({ length: outBatch }, (_, batch) => matrixForBatch(batch)) | |
| : [matrixForBatch(0)]; | |
| let output; | |
| if (leftVec && rightVec) { | |
| output = hasBatchAxis ? matrices.map((m) => m[0][0]) : matrices[0][0][0]; | |
| } else if (leftVec) { | |
| output = hasBatchAxis ? matrices.map((m) => m[0].slice()) : matrices[0][0].slice(); | |
| } else if (rightVec) { | |
| output = hasBatchAxis | |
| ? matrices.map((m) => m.map((row) => row[0])) | |
| : matrices[0].map((row) => row[0]); | |
| } else { | |
| output = hasBatchAxis ? matrices : matrices[0]; | |
| } | |
| const outputShape = shapeOf(output); | |
| lastRunMeta = { | |
| kind: "matmul", | |
| shapeA: sA, | |
| shapeB: sB, | |
| outputShape, | |
| leftBatch, | |
| rightBatch, | |
| outBatch, | |
| usedBroadcasting: | |
| leftBatch !== rightBatch || sA.length !== sB.length || leftVec || rightVec, | |
| leftVectorPromoted: leftVec, | |
| rightVectorPromoted: rightVec, | |
| }; | |
| return output; | |
| } | |
| function matmul2D(A, B) { | |
| const rowsA = A.length; | |
| const colsA = A[0].length; | |
| const rowsB = B.length; | |
| const colsB = B[0].length; | |
| if (colsA !== rowsB) { | |
| throw new Error( | |
| `matmul shape mismatch: (${rowsA},${colsA}) x (${rowsB},${colsB})` | |
| ); | |
| } | |
| const out = []; | |
| for (let r = 0; r < rowsA; r += 1) { | |
| const row = []; | |
| for (let c = 0; c < colsB; c += 1) { | |
| let sum = 0; | |
| for (let k = 0; k < colsA; k += 1) { | |
| sum += A[r][k] * B[k][c]; | |
| } | |
| row.push(sum); | |
| } | |
| out.push(row); | |
| } | |
| return out; | |
| } | |
| function concatAxis0(A, B) { | |
| const sA = shapeOf(A); | |
| const sB = shapeOf(B); | |
| if (sA.length !== sB.length) { | |
| throw new Error("concatenate requires same rank."); | |
| } | |
| for (let i = 1; i < sA.length; i += 1) { | |
| if (sA[i] !== sB[i]) { | |
| throw new Error( | |
| "concatenate axis=0 requires other dimensions to be identical." | |
| ); | |
| } | |
| } | |
| return [...clone(A), ...clone(B)]; | |
| } | |
| function stackAxis0(A, B) { | |
| const sA = JSON.stringify(shapeOf(A)); | |
| const sB = JSON.stringify(shapeOf(B)); | |
| if (sA !== sB) { | |
| throw new Error("stack requires A and B to have the same shape."); | |
| } | |
| return [clone(A), clone(B)]; | |
| } | |
| function reduceArray(A, { mode, axis, keepdims }) { | |
| const rank = shapeOf(A).length; | |
| if (axis === null) { | |
| const flat = flatten(A); | |
| let scalar; | |
| if (mode === "sum") { | |
| scalar = flat.reduce((acc, x) => acc + x, 0); | |
| } else { | |
| scalar = Number( | |
| (flat.reduce((acc, x) => acc + x, 0) / flat.length).toFixed(4) | |
| ); | |
| } | |
| if (!keepdims) return scalar; | |
| let wrapped = scalar; | |
| for (let i = 0; i < rank; i += 1) wrapped = [wrapped]; | |
| return wrapped; | |
| } | |
| const reduced = reduceAlongAxis(A, axis, mode); | |
| if (!keepdims) return reduced; | |
| return insertAxisDimension(reduced, axis); | |
| } | |
| function reduceAlongAxis(arr, axis, mode) { | |
| if (axis === 0) { | |
| if (arr.length === 0) throw new Error("Cannot reduce empty array."); | |
| let accum = clone(arr[0]); | |
| for (let i = 1; i < arr.length; i += 1) { | |
| accum = elementWiseDeep(accum, arr[i], (a, b) => a + b); | |
| } | |
| if (mode === "sum") return accum; | |
| return elementWiseDeep(accum, accum, (a) => Number((a / arr.length).toFixed(4))); | |
| } | |
| return arr.map((sub) => reduceAlongAxis(sub, axis - 1, mode)); | |
| } | |
| function insertAxisDimension(value, axis) { | |
| if (axis === 0) return [value]; | |
| if (!Array.isArray(value)) return [value]; | |
| return value.map((v) => insertAxisDimension(v, axis - 1)); | |
| } | |
| function clone(value) { | |
| return JSON.parse(JSON.stringify(value)); | |
| } | |
| function renderCode(op, parsed) { | |
| const lines = ["import numpy as np", ""]; | |
| if (op.id === "ones" || op.id === "zeros") { | |
| lines.push(`shape = (${parsed.shapes[0].join(", ")})`); | |
| } else { | |
| lines.push(`A = np.random.randint(1, 10, size=(${parsed.shapes[0].join(", ")}))`); | |
| if (op.inputs === 2) { | |
| lines.push( | |
| `B = np.random.randint(1, 10, size=(${parsed.shapes[1].join(", ")}))` | |
| ); | |
| } | |
| if (op.requiresTarget) { | |
| lines.push(`new_shape = (${parsed.targetShape.join(", ")})`); | |
| } | |
| } | |
| if (op.supportsReductionOptions) { | |
| const axisPart = parsed.options.axis === null ? "None" : parsed.options.axis; | |
| lines.push( | |
| `output = np.${op.id}(A, axis=${axisPart}, keepdims=${parsed.options.keepdims})` | |
| ); | |
| } else { | |
| lines.push(op.code); | |
| } | |
| lines.push("print(output)"); | |
| codeOutput.textContent = lines.join("\n"); | |
| } | |
| function renderInputs(arrays, op) { | |
| inputViz.innerHTML = ""; | |
| if (op.id === "ones" || op.id === "zeros") { | |
| inputViz.appendChild( | |
| buildMatrixCard("Generated Array", arrays[0], "from requested shape") | |
| ); | |
| return; | |
| } | |
| arrays.forEach((arr, i) => { | |
| inputViz.appendChild(buildMatrixCard(`Input ${i + 1}`, arr)); | |
| }); | |
| } | |
| function renderOutput(output) { | |
| outputViz.innerHTML = ""; | |
| outputViz.appendChild(buildMatrixCard("Output", output)); | |
| } | |
| function renderComputationDetails(op, inputArrays, output, parsed, meta) { | |
| detailViz.innerHTML = ""; | |
| if (meta && (meta.kind === "elementwise" || meta.kind === "matmul")) { | |
| detailViz.appendChild(buildBroadcastUsageCard(meta)); | |
| } | |
| if (op.id === "matmul") { | |
| detailViz.appendChild( | |
| buildMatmulDetail(inputArrays[0], inputArrays[1], output, meta) | |
| ); | |
| } else if (op.id === "reshape") { | |
| detailViz.appendChild(buildReshapeDetail(inputArrays[0], parsed.targetShape)); | |
| } else if (op.supportsReductionOptions) { | |
| detailViz.appendChild(buildReductionDetail(op, inputArrays[0], parsed.options, output)); | |
| } else { | |
| const note = document.createElement("p"); | |
| note.className = "slice-label"; | |
| note.textContent = "No extra computation walkthrough for this operation yet."; | |
| detailViz.appendChild(note); | |
| } | |
| } | |
| function buildBroadcastUsageCard(meta) { | |
| const card = document.createElement("article"); | |
| card.className = "matrix-card"; | |
| const title = document.createElement("p"); | |
| title.className = "matrix-title"; | |
| title.textContent = meta.usedBroadcasting | |
| ? "Broadcasting detected" | |
| : "No broadcasting needed"; | |
| card.appendChild(title); | |
| const line1 = document.createElement("p"); | |
| line1.className = "formula-line"; | |
| line1.textContent = `Input shapes: A${formatShape(meta.shapeA)}, B${formatShape( | |
| meta.shapeB | |
| )}`; | |
| card.appendChild(line1); | |
| const line2 = document.createElement("p"); | |
| line2.className = "formula-line"; | |
| line2.textContent = `Output shape: ${formatShape(meta.outputShape)}`; | |
| card.appendChild(line2); | |
| const explain = document.createElement("p"); | |
| explain.className = "slice-label"; | |
| if (!meta.usedBroadcasting) { | |
| explain.textContent = "Inputs already align directly; operation runs without dimension expansion."; | |
| } else if (meta.kind === "elementwise") { | |
| explain.textContent = | |
| "Element-wise broadcasting aligns dimensions from the right. Any dimension with size 1 is repeated to match the other input."; | |
| } else { | |
| explain.textContent = | |
| "For np.matmul, only batch dimensions are broadcast; core matrix dimensions still follow (..., m, k) @ (..., k, n)."; | |
| } | |
| card.appendChild(explain); | |
| return card; | |
| } | |
| function buildMatmulDetail(A, B, output, meta) { | |
| const wrapper = document.createElement("article"); | |
| wrapper.className = "matrix-card"; | |
| const title = document.createElement("p"); | |
| title.className = "matrix-title"; | |
| title.textContent = "np.matmul interactive computation breakdown"; | |
| wrapper.appendChild(title); | |
| const sA = shapeOf(A); | |
| const sB = shapeOf(B); | |
| if (sA.length === 2 && sB.length === 2) { | |
| wrapper.appendChild(buildMatmulInteractive2D(A, B, output)); | |
| return wrapper; | |
| } | |
| if (sA.length === 3 && sB.length === 3 && sA[0] === sB[0]) { | |
| const help = document.createElement("p"); | |
| help.className = "slice-label"; | |
| help.textContent = | |
| "Select a batch and output cell to highlight A row, B column, and formula."; | |
| wrapper.appendChild(help); | |
| const controls = document.createElement("div"); | |
| controls.className = "inline-fields"; | |
| controls.innerHTML = ` | |
| <label class="mini-label" for="batchSelect">batch</label> | |
| <select id="batchSelect"></select> | |
| `; | |
| wrapper.appendChild(controls); | |
| const batchSelect = controls.querySelector("#batchSelect"); | |
| for (let i = 0; i < sA[0]; i += 1) { | |
| const opt = document.createElement("option"); | |
| opt.value = String(i); | |
| opt.textContent = `batch ${i}`; | |
| batchSelect.appendChild(opt); | |
| } | |
| const host = document.createElement("div"); | |
| wrapper.appendChild(host); | |
| const renderBatch = () => { | |
| const idx = Number(batchSelect.value); | |
| host.innerHTML = ""; | |
| const label = document.createElement("p"); | |
| label.className = "slice-label"; | |
| label.textContent = `output[${idx}] = A[${idx}] @ B[${idx}]`; | |
| host.appendChild(label); | |
| host.appendChild(buildMatmulInteractive2D(A[idx], B[idx], output[idx])); | |
| }; | |
| batchSelect.addEventListener("change", renderBatch); | |
| renderBatch(); | |
| return wrapper; | |
| } | |
| const mixedNote = document.createElement("p"); | |
| mixedNote.className = "slice-label"; | |
| mixedNote.textContent = | |
| "Mixed-rank/broadcasted matmul: each output batch uses A_batch @ B_batch after NumPy batch broadcasting."; | |
| wrapper.appendChild(mixedNote); | |
| if (meta && meta.outBatch > 1) { | |
| const mapLine = document.createElement("p"); | |
| mapLine.className = "formula-line"; | |
| mapLine.textContent = `Batch mapping: output batch i uses A[${meta.leftBatch === 1 ? "0" : "i"}] and B[${ | |
| meta.rightBatch === 1 ? "0" : "i" | |
| }].`; | |
| wrapper.appendChild(mapLine); | |
| } | |
| wrapper.appendChild(buildMatrixCard("Input A", A)); | |
| wrapper.appendChild(buildMatrixCard("Input B", B)); | |
| wrapper.appendChild(buildMatrixCard("Output", output)); | |
| return wrapper; | |
| } | |
| function buildMatmulInteractive2D(A, B, out) { | |
| const box = document.createElement("div"); | |
| const helper = document.createElement("p"); | |
| helper.className = "slice-label"; | |
| helper.textContent = | |
| "Click a value in the output matrix. The corresponding row/column will be highlighted."; | |
| box.appendChild(helper); | |
| const state = { row: 0, col: 0 }; | |
| const pickerHost = document.createElement("div"); | |
| const matrixHost = document.createElement("div"); | |
| matrixHost.className = "viz-grid"; | |
| const formulaLine = document.createElement("p"); | |
| formulaLine.className = "formula-line"; | |
| function renderPicker() { | |
| pickerHost.innerHTML = ""; | |
| const table = document.createElement("table"); | |
| table.className = "matrix-table"; | |
| out.forEach((rowValues, r) => { | |
| const tr = document.createElement("tr"); | |
| rowValues.forEach((value, c) => { | |
| const td = document.createElement("td"); | |
| const btn = document.createElement("button"); | |
| btn.type = "button"; | |
| btn.className = "matrix-pick-btn"; | |
| if (r === state.row && c === state.col) btn.classList.add("active"); | |
| btn.textContent = String(value); | |
| btn.addEventListener("click", () => { | |
| state.row = r; | |
| state.col = c; | |
| renderAll(); | |
| }); | |
| td.appendChild(btn); | |
| tr.appendChild(td); | |
| }); | |
| table.appendChild(tr); | |
| }); | |
| pickerHost.appendChild(table); | |
| } | |
| function build2DCard(title, matrix, subtitle, highlight) { | |
| const card = document.createElement("article"); | |
| card.className = "matrix-card"; | |
| const shape = shapeOf(matrix); | |
| const titleEl = document.createElement("p"); | |
| titleEl.className = "matrix-title"; | |
| titleEl.innerHTML = `${escapeHtml(title)} <span class="shape-badge">shape: (${shape.join( | |
| ", " | |
| )})</span>`; | |
| card.appendChild(titleEl); | |
| const sub = document.createElement("p"); | |
| sub.className = "slice-label"; | |
| sub.textContent = subtitle; | |
| card.appendChild(sub); | |
| card.appendChild(render2DTableWithHighlights(matrix, highlight)); | |
| return card; | |
| } | |
| function renderAll() { | |
| renderPicker(); | |
| matrixHost.innerHTML = ""; | |
| matrixHost.appendChild( | |
| build2DCard( | |
| "Input A", | |
| A, | |
| `highlighted row: ${state.row}`, | |
| { highlightRow: state.row } | |
| ) | |
| ); | |
| matrixHost.appendChild( | |
| build2DCard( | |
| "Input B", | |
| B, | |
| `highlighted column: ${state.col}`, | |
| { highlightCol: state.col } | |
| ) | |
| ); | |
| matrixHost.appendChild( | |
| build2DCard( | |
| "Output", | |
| out, | |
| `selected cell: [${state.row}, ${state.col}]`, | |
| { highlightCell: { row: state.row, col: state.col } } | |
| ) | |
| ); | |
| const terms = []; | |
| const values = []; | |
| for (let k = 0; k < A[0].length; k += 1) { | |
| terms.push(`${A[state.row][k]}*${B[k][state.col]}`); | |
| values.push(A[state.row][k] * B[k][state.col]); | |
| } | |
| formulaLine.textContent = `output[${state.row}, ${state.col}] = ${terms.join( | |
| " + " | |
| )} = ${values.join(" + ")} = ${out[state.row][state.col]}`; | |
| } | |
| box.appendChild(pickerHost); | |
| box.appendChild(formulaLine); | |
| box.appendChild(matrixHost); | |
| renderAll(); | |
| return box; | |
| } | |
| function buildReshapeDetail(A, targetShape) { | |
| const wrapper = document.createElement("article"); | |
| wrapper.className = "matrix-card"; | |
| const title = document.createElement("p"); | |
| title.className = "matrix-title"; | |
| title.textContent = "np.reshape step-by-step (dynamic)"; | |
| wrapper.appendChild(title); | |
| const flat = flatten(A); | |
| const total = flat.length; | |
| const helper = document.createElement("p"); | |
| helper.className = "slice-label"; | |
| helper.textContent = | |
| "Use Play to animate reshaping; slider also works for manual inspection."; | |
| wrapper.appendChild(helper); | |
| const controls = document.createElement("div"); | |
| controls.className = "slider-row"; | |
| controls.innerHTML = ` | |
| <label for="reshapeStep">step</label> | |
| <input class="reshape-step" id="reshapeStep" type="range" min="0" max="${total}" value="0" /> | |
| <span id="reshapeStepLabel">0 / ${total}</span> | |
| <button type="button" id="reshapePlayBtn">Play</button> | |
| <button type="button" id="reshapeResetBtn">Reset</button> | |
| `; | |
| wrapper.appendChild(controls); | |
| const flatLine = document.createElement("p"); | |
| flatLine.className = "formula-line"; | |
| flatLine.textContent = `flat(A) = [${flat.join(", ")}]`; | |
| wrapper.appendChild(flatLine); | |
| const previewHost = document.createElement("div"); | |
| wrapper.appendChild(previewHost); | |
| const slider = controls.querySelector("#reshapeStep"); | |
| const stepLabel = controls.querySelector("#reshapeStepLabel"); | |
| const playBtn = controls.querySelector("#reshapePlayBtn"); | |
| const resetBtn = controls.querySelector("#reshapeResetBtn"); | |
| let timer = null; | |
| let playing = false; | |
| const renderStep = () => { | |
| const step = Number(slider.value); | |
| stepLabel.textContent = `${step} / ${total}`; | |
| const partialFlat = new Array(total).fill("·"); | |
| for (let i = 0; i < step; i += 1) partialFlat[i] = flat[i]; | |
| const partial = unflatten(partialFlat, targetShape); | |
| previewHost.innerHTML = ""; | |
| previewHost.appendChild( | |
| buildMatrixCard( | |
| "Current reshaped output", | |
| partial, | |
| `first ${step} element(s) assigned in row-major order` | |
| ) | |
| ); | |
| }; | |
| const stopAnimation = () => { | |
| if (timer) { | |
| clearInterval(timer); | |
| timer = null; | |
| } | |
| playing = false; | |
| playBtn.textContent = "Play"; | |
| }; | |
| const startAnimation = () => { | |
| stopAnimation(); | |
| playing = true; | |
| playBtn.textContent = "Pause"; | |
| timer = setInterval(() => { | |
| if (!document.body.contains(wrapper)) { | |
| stopAnimation(); | |
| return; | |
| } | |
| const current = Number(slider.value); | |
| if (current >= total) { | |
| stopAnimation(); | |
| return; | |
| } | |
| slider.value = String(current + 1); | |
| renderStep(); | |
| }, 500); | |
| }; | |
| playBtn.addEventListener("click", () => { | |
| if (playing) { | |
| stopAnimation(); | |
| } else { | |
| startAnimation(); | |
| } | |
| }); | |
| resetBtn.addEventListener("click", () => { | |
| stopAnimation(); | |
| slider.value = "0"; | |
| renderStep(); | |
| }); | |
| slider.addEventListener("input", () => { | |
| if (playing) stopAnimation(); | |
| renderStep(); | |
| }); | |
| renderStep(); | |
| startAnimation(); | |
| return wrapper; | |
| } | |
| function buildReductionDetail(op, A, options, output) { | |
| const wrapper = document.createElement("article"); | |
| wrapper.className = "matrix-card"; | |
| const title = document.createElement("p"); | |
| title.className = "matrix-title"; | |
| title.textContent = `np.${op.id} options summary`; | |
| wrapper.appendChild(title); | |
| const summary = document.createElement("p"); | |
| summary.className = "formula-line"; | |
| summary.textContent = `axis=${options.axis === null ? "None" : options.axis}, keepdims=${options.keepdims}`; | |
| wrapper.appendChild(summary); | |
| const explain = document.createElement("p"); | |
| explain.className = "slice-label"; | |
| explain.textContent = | |
| options.axis === null | |
| ? "Reducing over all dimensions." | |
| : `Reducing along axis ${options.axis}; values on that axis are aggregated.`; | |
| wrapper.appendChild(explain); | |
| wrapper.appendChild(buildMatrixCard("Input A", A)); | |
| wrapper.appendChild(buildMatrixCard("Reduced output", output)); | |
| return wrapper; | |
| } | |
| function buildMatrixCard(title, arr, subtitle = "") { | |
| const card = document.createElement("article"); | |
| card.className = "matrix-card"; | |
| const shape = shapeOf(arr); | |
| const titleEl = document.createElement("p"); | |
| titleEl.className = "matrix-title"; | |
| titleEl.innerHTML = `${escapeHtml(title)} <span class="shape-badge">shape: (${shape.join( | |
| ", " | |
| ) || "scalar"})</span>`; | |
| card.appendChild(titleEl); | |
| if (subtitle) { | |
| const sub = document.createElement("p"); | |
| sub.className = "slice-label"; | |
| sub.textContent = subtitle; | |
| card.appendChild(sub); | |
| } | |
| if (!Array.isArray(arr)) { | |
| const scalar = document.createElement("p"); | |
| scalar.textContent = String(arr); | |
| scalar.style.fontFamily = "Courier New, monospace"; | |
| scalar.style.fontWeight = "700"; | |
| card.appendChild(scalar); | |
| return card; | |
| } | |
| const rank = shape.length; | |
| if (rank === 1) { | |
| card.appendChild(render2DTable([arr])); | |
| } else if (rank === 2) { | |
| card.appendChild(render2DTable(arr)); | |
| } else if (rank === 3) { | |
| arr.forEach((slice, idx) => { | |
| const lbl = document.createElement("p"); | |
| lbl.className = "slice-label"; | |
| lbl.textContent = `slice ${idx} (axis 0)`; | |
| card.appendChild(lbl); | |
| card.appendChild(render2DTable(slice)); | |
| }); | |
| } | |
| return card; | |
| } | |
| function render2DTable(matrix2d) { | |
| const table = document.createElement("table"); | |
| table.className = "matrix-table"; | |
| matrix2d.forEach((row) => { | |
| const tr = document.createElement("tr"); | |
| row.forEach((value) => { | |
| const td = document.createElement("td"); | |
| td.textContent = String(value); | |
| tr.appendChild(td); | |
| }); | |
| table.appendChild(tr); | |
| }); | |
| return table; | |
| } | |
| function render2DTableWithHighlights(matrix2d, highlight = {}) { | |
| const table = document.createElement("table"); | |
| table.className = "matrix-table"; | |
| matrix2d.forEach((row, r) => { | |
| const tr = document.createElement("tr"); | |
| row.forEach((value, c) => { | |
| const td = document.createElement("td"); | |
| td.textContent = String(value); | |
| if (highlight.highlightRow === r) td.classList.add("hl-row"); | |
| if (highlight.highlightCol === c) td.classList.add("hl-col"); | |
| if ( | |
| highlight.highlightCell && | |
| highlight.highlightCell.row === r && | |
| highlight.highlightCell.col === c | |
| ) { | |
| td.classList.add("hl-cell"); | |
| } | |
| tr.appendChild(td); | |
| }); | |
| table.appendChild(tr); | |
| }); | |
| return table; | |
| } | |
| function escapeHtml(text) { | |
| return String(text) | |
| .replaceAll("&", "&") | |
| .replaceAll("<", "<") | |
| .replaceAll(">", ">") | |
| .replaceAll('"', """) | |
| .replaceAll("'", "'"); | |
| } | |
| init(); | |