@danielsimonjr/mathts-tensor 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Tensor — rank-N, Float64Array-backed, row-major dense tensor. The
3
+ * compute primitive behind @danielsimonjr/mathts-tensor. Construction +
4
+ * elementwise here; einsum / matMul / transpose / reshape in the same
5
+ * file, added by the next task.
6
+ *
7
+ * Naive O(n) algorithms — a correctness baseline. Mirrors the storage
8
+ * pattern of DenseMatrix (Float64Array, row-major) but generalised to
9
+ * arbitrary rank, filling the `Matrix<T>` base class's documented
10
+ * "generic template for future rank-N support".
11
+ */
12
+ type NestedArray = number | NestedArray[];
13
+ interface EinsumSpec {
14
+ readonly contractions: ReadonlyArray<{
15
+ readonly pair: readonly [readonly [number, number], readonly [number, number]];
16
+ }>;
17
+ readonly free: ReadonlyArray<{
18
+ readonly operand: number;
19
+ readonly axis: number;
20
+ }>;
21
+ }
22
+ declare class Tensor {
23
+ readonly shape: ReadonlyArray<number>;
24
+ readonly data: Float64Array;
25
+ constructor(shape: ReadonlyArray<number>, data: Float64Array);
26
+ static sizeOf(shape: ReadonlyArray<number>): number;
27
+ static rowMajorStrides(shape: ReadonlyArray<number>): number[];
28
+ static fromNested(data: NestedArray, shape: ReadonlyArray<number>): Tensor;
29
+ static identity(n: number): Tensor;
30
+ toNested(): NestedArray;
31
+ private sameShape;
32
+ private elementwise;
33
+ add(other: Tensor): Tensor;
34
+ sub(other: Tensor): Tensor;
35
+ mul(other: Tensor): Tensor;
36
+ scale(k: number): Tensor;
37
+ normInf(): number;
38
+ private static forEachIndex;
39
+ private static flatIndex;
40
+ reshape(shape: ReadonlyArray<number>): Tensor;
41
+ transpose(perm?: ReadonlyArray<number>): Tensor;
42
+ matMul(other: Tensor): Tensor;
43
+ static einsum(spec: EinsumSpec, ...operands: Tensor[]): Tensor;
44
+ }
45
+
46
+ export { type EinsumSpec, type NestedArray, Tensor };
package/dist/index.js ADDED
@@ -0,0 +1,217 @@
1
+ // src/Tensor.ts
2
+ var Tensor = class _Tensor {
3
+ shape;
4
+ data;
5
+ constructor(shape, data) {
6
+ if (data.length !== _Tensor.sizeOf(shape)) {
7
+ throw new Error(`Tensor: data length ${data.length} does not match shape [${shape}] (size ${_Tensor.sizeOf(shape)})`);
8
+ }
9
+ this.shape = shape;
10
+ this.data = data;
11
+ }
12
+ static sizeOf(shape) {
13
+ return shape.reduce((a, b) => a * b, 1);
14
+ }
15
+ static rowMajorStrides(shape) {
16
+ const strides = new Array(shape.length);
17
+ let acc = 1;
18
+ for (let k = shape.length - 1; k >= 0; k--) {
19
+ strides[k] = acc;
20
+ acc *= shape[k];
21
+ }
22
+ return strides;
23
+ }
24
+ static fromNested(data, shape) {
25
+ const size = _Tensor.sizeOf(shape);
26
+ const out = new Float64Array(size);
27
+ let cursor = 0;
28
+ const walk = (node, depth) => {
29
+ if (depth === shape.length) {
30
+ if (typeof node !== "number") {
31
+ throw new Error(`Tensor.fromNested: expected a number at depth ${depth}`);
32
+ }
33
+ out[cursor++] = node;
34
+ return;
35
+ }
36
+ if (!Array.isArray(node) || node.length !== shape[depth]) {
37
+ throw new Error(`Tensor.fromNested: shape mismatch at depth ${depth}`);
38
+ }
39
+ for (const child of node) walk(child, depth + 1);
40
+ };
41
+ walk(data, 0);
42
+ if (cursor !== size) throw new Error(`Tensor.fromNested: filled ${cursor} of ${size}`);
43
+ return new _Tensor([...shape], out);
44
+ }
45
+ static identity(n) {
46
+ const out = new Float64Array(n * n);
47
+ for (let i = 0; i < n; i++) out[i * n + i] = 1;
48
+ return new _Tensor([n, n], out);
49
+ }
50
+ toNested() {
51
+ if (this.shape.length === 0) return this.data[0];
52
+ const strides = _Tensor.rowMajorStrides(this.shape);
53
+ const build = (depth, offset) => {
54
+ if (depth === this.shape.length - 1) {
55
+ const row = [];
56
+ for (let i = 0; i < this.shape[depth]; i++) row.push(this.data[offset + i]);
57
+ return row;
58
+ }
59
+ const out = [];
60
+ for (let i = 0; i < this.shape[depth]; i++) {
61
+ out.push(build(depth + 1, offset + i * strides[depth]));
62
+ }
63
+ return out;
64
+ };
65
+ return build(0, 0);
66
+ }
67
+ sameShape(other) {
68
+ return this.shape.length === other.shape.length && this.shape.every((v, i) => v === other.shape[i]);
69
+ }
70
+ elementwise(other, op, f) {
71
+ if (!this.sameShape(other)) {
72
+ throw new Error(`Tensor.${op}: shape mismatch [${this.shape}] vs [${other.shape}]`);
73
+ }
74
+ const out = new Float64Array(this.data.length);
75
+ for (let i = 0; i < this.data.length; i++) out[i] = f(this.data[i], other.data[i]);
76
+ return new _Tensor(this.shape, out);
77
+ }
78
+ add(other) {
79
+ return this.elementwise(other, "add", (x, y) => x + y);
80
+ }
81
+ sub(other) {
82
+ return this.elementwise(other, "sub", (x, y) => x - y);
83
+ }
84
+ mul(other) {
85
+ return this.elementwise(other, "mul", (x, y) => x * y);
86
+ }
87
+ scale(k) {
88
+ const out = new Float64Array(this.data.length);
89
+ for (let i = 0; i < this.data.length; i++) out[i] = this.data[i] * k;
90
+ return new _Tensor(this.shape, out);
91
+ }
92
+ normInf() {
93
+ let max = 0;
94
+ for (let i = 0; i < this.data.length; i++) {
95
+ const a = Math.abs(this.data[i]);
96
+ if (a > max) max = a;
97
+ }
98
+ return max;
99
+ }
100
+ static forEachIndex(shape, visit) {
101
+ if (shape.length === 0) {
102
+ visit([]);
103
+ return;
104
+ }
105
+ const idx = new Array(shape.length).fill(0);
106
+ const total = _Tensor.sizeOf(shape);
107
+ for (let n = 0; n < total; n++) {
108
+ visit(idx);
109
+ for (let k = shape.length - 1; k >= 0; k--) {
110
+ if (++idx[k] < shape[k]) break;
111
+ idx[k] = 0;
112
+ }
113
+ }
114
+ }
115
+ static flatIndex(idx, strides) {
116
+ let f = 0;
117
+ for (let k = 0; k < idx.length; k++) f += idx[k] * strides[k];
118
+ return f;
119
+ }
120
+ reshape(shape) {
121
+ if (_Tensor.sizeOf(shape) !== this.data.length) {
122
+ throw new Error(`Tensor.reshape: size mismatch [${this.shape}] -> [${shape}]`);
123
+ }
124
+ return new _Tensor([...shape], this.data.slice());
125
+ }
126
+ transpose(perm) {
127
+ const rank = this.shape.length;
128
+ const p = perm ?? Array.from({ length: rank }, (_, i) => rank - 1 - i);
129
+ if (p.length !== rank) throw new Error(`Tensor.transpose: perm length ${p.length} != rank ${rank}`);
130
+ const outShape = p.map((axis) => this.shape[axis]);
131
+ const inStrides = _Tensor.rowMajorStrides(this.shape);
132
+ const outStrides = _Tensor.rowMajorStrides(outShape);
133
+ const out = new Float64Array(this.data.length);
134
+ _Tensor.forEachIndex(outShape, (outIdx) => {
135
+ const inIdx = new Array(rank);
136
+ for (let k = 0; k < rank; k++) inIdx[p[k]] = outIdx[k];
137
+ out[_Tensor.flatIndex(outIdx, outStrides)] = this.data[_Tensor.flatIndex(inIdx, inStrides)];
138
+ });
139
+ return new _Tensor(outShape, out);
140
+ }
141
+ matMul(other) {
142
+ if (this.shape.length !== 2 || other.shape.length !== 2) {
143
+ throw new Error(`Tensor.matMul: both operands must be rank-2`);
144
+ }
145
+ const [m, k] = this.shape;
146
+ const [k2, n] = other.shape;
147
+ if (k !== k2) throw new Error(`Tensor.matMul: inner dimension mismatch ${k} != ${k2}`);
148
+ const out = new Float64Array(m * n);
149
+ for (let i = 0; i < m; i++) {
150
+ for (let j = 0; j < n; j++) {
151
+ let sum = 0;
152
+ for (let q = 0; q < k; q++) sum += this.data[i * k + q] * other.data[q * n + j];
153
+ out[i * n + j] = sum;
154
+ }
155
+ }
156
+ return new _Tensor([m, n], out);
157
+ }
158
+ static einsum(spec, ...operands) {
159
+ const inStrides = operands.map((o) => _Tensor.rowMajorStrides(o.shape));
160
+ const freeSizes = spec.free.map((fa) => operands[fa.operand].shape[fa.axis]);
161
+ const contractSizes = spec.contractions.map((c) => {
162
+ const [[oa, axa], [ob, axb]] = c.pair;
163
+ const sa = operands[oa]?.shape[axa];
164
+ const sb = operands[ob]?.shape[axb];
165
+ if (sa === void 0 || sb === void 0 || sa !== sb) {
166
+ throw new Error(`Tensor.einsum: contraction pair size mismatch`);
167
+ }
168
+ return sa;
169
+ });
170
+ const outStrides = _Tensor.rowMajorStrides(freeSizes);
171
+ const out = new Float64Array(_Tensor.sizeOf(freeSizes));
172
+ for (let o = 0; o < operands.length; o++) {
173
+ if (operands[o].shape.length === 0) continue;
174
+ const covered = /* @__PURE__ */ new Set();
175
+ spec.free.forEach((fa) => {
176
+ if (fa.operand === o) covered.add(fa.axis);
177
+ });
178
+ spec.contractions.forEach((c) => {
179
+ const [[oa, axa], [ob, axb]] = c.pair;
180
+ if (oa === o) covered.add(axa);
181
+ if (ob === o) covered.add(axb);
182
+ });
183
+ if (covered.size !== operands[o].shape.length) {
184
+ throw new Error(
185
+ `Tensor.einsum: operand ${o} has axes not referenced by the spec`
186
+ );
187
+ }
188
+ }
189
+ const operandFlatIndex = (opIndex, freeVals, contractVals) => {
190
+ const idx = new Array(operands[opIndex].shape.length).fill(0);
191
+ spec.free.forEach((fa, v) => {
192
+ if (fa.operand === opIndex) idx[fa.axis] = freeVals[v];
193
+ });
194
+ spec.contractions.forEach((c, v) => {
195
+ const [[oa, axa], [ob, axb]] = c.pair;
196
+ if (oa === opIndex) idx[axa] = contractVals[v];
197
+ if (ob === opIndex) idx[axb] = contractVals[v];
198
+ });
199
+ return _Tensor.flatIndex(idx, inStrides[opIndex]);
200
+ };
201
+ _Tensor.forEachIndex(freeSizes, (freeVals) => {
202
+ let acc = 0;
203
+ _Tensor.forEachIndex(contractSizes, (contractVals) => {
204
+ let product = 1;
205
+ for (let o = 0; o < operands.length; o++) {
206
+ product *= operands[o].data[operandFlatIndex(o, freeVals, contractVals)];
207
+ }
208
+ acc += product;
209
+ });
210
+ out[_Tensor.flatIndex(freeVals, outStrides)] = acc;
211
+ });
212
+ return new _Tensor(freeSizes, out);
213
+ }
214
+ };
215
+ export {
216
+ Tensor
217
+ };
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@danielsimonjr/mathts-tensor",
3
+ "version": "0.1.0",
4
+ "description": "Rank-N dense Tensor type for MathTS with Float64Array-backed storage",
5
+ "author": "Daniel Simon Jr.",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "./dist/index.js",
9
+ "module": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "import": "./dist/index.js",
14
+ "types": "./dist/index.d.ts"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "README.md"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsup src/index.ts --format esm --dts --clean",
23
+ "dev": "tsup src/index.ts --format esm --dts --watch",
24
+ "test": "vitest run",
25
+ "test:watch": "vitest",
26
+ "test:coverage": "vitest run --coverage",
27
+ "typecheck": "tsc --noEmit",
28
+ "lint": "eslint src --ext .ts",
29
+ "lint:fix": "eslint src --ext .ts --fix",
30
+ "clean": "rm -rf dist",
31
+ "build:prod": "tsup src/index.ts --format esm --dts --clean --minify --treeshake"
32
+ },
33
+ "dependencies": {
34
+ "@danielsimonjr/mathts-core": "*"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^25.5.2",
38
+ "tsup": "^8.0.0",
39
+ "typescript": "^5.3.0",
40
+ "vitest": "^4.1.5"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "https://github.com/danielsimonjr/mathts",
48
+ "directory": "tensor"
49
+ },
50
+ "keywords": [
51
+ "math",
52
+ "tensor",
53
+ "linear-algebra",
54
+ "typescript"
55
+ ]
56
+ }