dimgrid 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tomislav Herman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,203 @@
1
+ # dimgrid
2
+
3
+ Build a typed N-dimensional grid of objects by adding named dimensions with discrete values.
4
+
5
+ Start from a single empty point and expand it into a full cartesian product by adding dimensions one at a time. Each `.dim()` call multiplies every existing point by the number of values in the new dimension, attaching the dimension key to each resulting point.
6
+
7
+ ## Install
8
+
9
+ ```
10
+ npm install dimgrid
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```typescript
16
+ import { dimgrid } from 'dimgrid'
17
+
18
+ const points = dimgrid()
19
+ .dim('color', ['red', 'green', 'blue'])
20
+ .dim('size', ['S', 'M', 'L'])
21
+ .toArray()
22
+
23
+ // 9 points — every combination of color × size
24
+ // [
25
+ // { color: 'red', size: 'S' },
26
+ // { color: 'red', size: 'M' },
27
+ // { color: 'red', size: 'L' },
28
+ // { color: 'green', size: 'S' },
29
+ // ...
30
+ // ]
31
+ ```
32
+
33
+ TypeScript infers the full type of each point from the chain, so `points` is typed as `{ color: 'red' | 'green' | 'blue'; size: 'S' | 'M' | 'L' }[]`.
34
+
35
+ ## Dynamic dimension values
36
+
37
+ Pass a function instead of an array to derive values from the point being expanded. The function receives the current point and returns the values for the new dimension. Return an empty array to drop a point entirely.
38
+
39
+ ```typescript
40
+ const points = dimgrid()
41
+ .dim('sign', [-1, 1])
42
+ .dim('magnitude', ({ sign }) => sign > 0 ? [1, 2, 3] : [1])
43
+ .toArray()
44
+
45
+ // [
46
+ // { sign: -1, magnitude: 1 },
47
+ // { sign: 1, magnitude: 1 },
48
+ // { sign: 1, magnitude: 2 },
49
+ // { sign: 1, magnitude: 3 },
50
+ // ]
51
+ ```
52
+
53
+ ## API
54
+
55
+ ### `dimgrid()`
56
+
57
+ Creates a new grid with a single empty point. All grids start here.
58
+
59
+ ### `grid.dim(key, values)`
60
+
61
+ Expands every existing point across the given values. Returns a new `DimGrid` — the original is not mutated.
62
+
63
+ | Parameter | Type | Description |
64
+ |-----------|------|-------------|
65
+ | `key` | `string` | Dimension name, becomes a property on each point |
66
+ | `values` | `readonly V[]` | One child point per value |
67
+ | `values` | `(point: T) => readonly V[]` | Values derived from the parent point; return `[]` to drop it |
68
+
69
+ ### `grid.toArray()`
70
+
71
+ Returns all points as a plain `T[]`.
72
+
73
+ ### `grid.size`
74
+
75
+ Number of points currently in the grid.
76
+
77
+ ### `grid[Symbol.iterator]`
78
+
79
+ The grid is directly iterable — `for...of` and spread both work.
80
+
81
+ ```typescript
82
+ for (const point of grid) { ... }
83
+ const points = [...grid]
84
+ ```
85
+
86
+ ---
87
+
88
+ ## Examples
89
+
90
+ ### Vitest — `test.each` with all dimension permutations
91
+
92
+ `test.each` accepts an array of objects and feeds each one as named arguments to the test function — a natural fit for dimgrid points. The dimension chain replaces manual case lists that grow stale as requirements change.
93
+
94
+ The example below tests a `clamp(value, min, max)` utility across all combinations of inputs and bounds. The function form of `.dim()` computes the expected result directly from each point's other dimensions, so no separate lookup table is needed and the expected value is always in sync with the inputs.
95
+
96
+ ```typescript
97
+ import { describe, expect, test } from 'vitest'
98
+ import { dimgrid } from 'dimgrid'
99
+ import { clamp } from './clamp'
100
+
101
+ const cases = dimgrid()
102
+ .dim('value', [-20, 0, 10, 50]) // below range, at min, inside, above max
103
+ .dim('min', [0, 5])
104
+ .dim('max', [10, 30])
105
+ .dim('expected', ({ value, min, max }) => [
106
+ value < min ? min : value > max ? max : value,
107
+ ])
108
+ .toArray()
109
+
110
+ describe('clamp', () => {
111
+ test.each(cases)(
112
+ 'clamp($value, $min, $max) → $expected',
113
+ ({ value, min, max, expected }) => {
114
+ expect(clamp(value, min, max)).toBe(expected)
115
+ },
116
+ )
117
+ })
118
+ ```
119
+
120
+ This produces **4 × 2 × 2 = 16 test cases** automatically, with names like:
121
+
122
+ ```
123
+ clamp(-20, 0, 10) → 0
124
+ clamp(-20, 0, 30) → 0
125
+ clamp(-20, 5, 10) → 5
126
+ clamp(10, 0, 10) → 10
127
+ clamp(50, 0, 10) → 10
128
+ ...
129
+ ```
130
+
131
+ Adding a new boundary value to any dimension (say, `max: [10, 20, 30]`) inserts a full slice of tests with no further changes, keeping coverage complete across all combinations.
132
+
133
+ ### Storybook — visual matrix of all component states
134
+
135
+ Design systems need stories for every meaningful prop combination. Writing them by hand is tedious and incomplete; dimgrid generates the full matrix and the function form prunes states that are visually invalid or redundant before they reach the story.
136
+
137
+ The example below covers a Button with four dimensions. A button cannot be both disabled and loading at the same time, so `loading` uses the function form to restrict itself to `[false]` whenever `disabled` is `true`.
138
+
139
+ ```tsx
140
+ // Button.stories.tsx
141
+ import type { Meta, StoryObj } from '@storybook/react'
142
+ import { dimgrid } from 'dimgrid'
143
+ import { Button } from './Button'
144
+
145
+ const meta: Meta<typeof Button> = { component: Button }
146
+ export default meta
147
+
148
+ const cases = dimgrid()
149
+ .dim('variant', ['primary', 'secondary', 'ghost', 'danger'])
150
+ .dim('size', ['sm', 'md', 'lg'])
151
+ .dim('disabled', [false, true])
152
+ .dim('loading', ({ disabled }) => disabled ? [false] : [false, true])
153
+ .toArray()
154
+ // 4 × 3 × 2 × 2 = 48 raw combinations, pruned to 36 by the loading constraint
155
+
156
+ export const AllVariants: StoryObj<typeof Button> = {
157
+ render: () => (
158
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
159
+ {cases.map((props, i) => <Button key={i} {...props} />)}
160
+ </div>
161
+ ),
162
+ }
163
+ ```
164
+
165
+ A single `AllVariants` story renders all 36 states in one snapshot. Visual regression tools like Chromatic catch regressions across the entire matrix on every commit. Adding a new `variant` value to the first dimension automatically propagates across all `size × disabled × loading` combinations with no other changes.
166
+
167
+ ### ML hyperparameter grid search
168
+
169
+ Grid search — systematically training a model for every combination of hyperparameters and picking the best result — is where the name "grid" comes from. dimgrid generates the search space; the function form prunes configurations that are known to be numerically unstable before any training job is launched.
170
+
171
+ SGD diverges at high learning rates, so the `optimizer` dimension restricts itself to `['adam']` whenever `learningRate` exceeds `1e-3`:
172
+
173
+ ```typescript
174
+ // grid-search.ts
175
+ import { dimgrid } from 'dimgrid'
176
+
177
+ const configs = dimgrid()
178
+ .dim('learningRate', [1e-4, 1e-3, 1e-2])
179
+ .dim('batchSize', [32, 64, 128])
180
+ .dim('dropout', [0.0, 0.3, 0.5])
181
+ .dim('optimizer', ({ learningRate }) =>
182
+ learningRate >= 1e-2 ? ['adam'] : ['adam', 'sgd']
183
+ )
184
+ .toArray()
185
+ // 3 × 3 × 3 × 2 = 54 raw combinations, pruned to 45
186
+
187
+ const results = await Promise.all(
188
+ configs.map(params =>
189
+ trainModel(params).then(({ valAccuracy, valLoss }) => ({
190
+ ...params,
191
+ valAccuracy,
192
+ valLoss,
193
+ }))
194
+ )
195
+ )
196
+
197
+ const best = results.sort((a, b) => b.valAccuracy - a.valAccuracy)[0]
198
+ console.log('best config:', best)
199
+ ```
200
+
201
+ `trainModel` is whatever launches a training run in your stack — a TensorFlow.js fit call, a Python subprocess, a remote job submitted to a GPU cluster. The dimgrid part is the same regardless.
202
+
203
+ The pruning matters at scale: a full 4-dimensional sweep without constraints wastes GPU hours on configurations that are guaranteed to fail. Adding a fifth dimension (say, `weightDecay`) multiplies the search space, but the function form keeps the invalid slice removed automatically.
@@ -0,0 +1,12 @@
1
+ declare class DimGrid<T extends object = {}> {
2
+ private readonly _points;
3
+ private constructor();
4
+ static create(): DimGrid<{}>;
5
+ dim<K extends string, const V>(key: K, values: readonly V[] | ((point: T) => readonly V[])): DimGrid<T & Record<K, V>>;
6
+ toArray(): T[];
7
+ get size(): number;
8
+ [Symbol.iterator](): Iterator<T>;
9
+ }
10
+ declare function dimgrid(): DimGrid<{}>;
11
+
12
+ export { DimGrid, dimgrid };
@@ -0,0 +1,12 @@
1
+ declare class DimGrid<T extends object = {}> {
2
+ private readonly _points;
3
+ private constructor();
4
+ static create(): DimGrid<{}>;
5
+ dim<K extends string, const V>(key: K, values: readonly V[] | ((point: T) => readonly V[])): DimGrid<T & Record<K, V>>;
6
+ toArray(): T[];
7
+ get size(): number;
8
+ [Symbol.iterator](): Iterator<T>;
9
+ }
10
+ declare function dimgrid(): DimGrid<{}>;
11
+
12
+ export { DimGrid, dimgrid };
package/dist/index.js ADDED
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ DimGrid: () => DimGrid,
24
+ dimgrid: () => dimgrid
25
+ });
26
+ module.exports = __toCommonJS(index_exports);
27
+ var DimGrid = class _DimGrid {
28
+ constructor(points) {
29
+ this._points = points;
30
+ }
31
+ static create() {
32
+ return new _DimGrid([{}]);
33
+ }
34
+ dim(key, values) {
35
+ const next = [];
36
+ const resolve = typeof values === "function" ? values : () => values;
37
+ for (const point of this._points) {
38
+ for (const value of resolve(point)) {
39
+ next.push({ ...point, [key]: value });
40
+ }
41
+ }
42
+ return new _DimGrid(next);
43
+ }
44
+ toArray() {
45
+ return [...this._points];
46
+ }
47
+ get size() {
48
+ return this._points.length;
49
+ }
50
+ *[Symbol.iterator]() {
51
+ yield* this._points;
52
+ }
53
+ };
54
+ function dimgrid() {
55
+ return DimGrid.create();
56
+ }
57
+ // Annotate the CommonJS export names for ESM import in node:
58
+ 0 && (module.exports = {
59
+ DimGrid,
60
+ dimgrid
61
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,35 @@
1
+ // src/index.ts
2
+ var DimGrid = class _DimGrid {
3
+ constructor(points) {
4
+ this._points = points;
5
+ }
6
+ static create() {
7
+ return new _DimGrid([{}]);
8
+ }
9
+ dim(key, values) {
10
+ const next = [];
11
+ const resolve = typeof values === "function" ? values : () => values;
12
+ for (const point of this._points) {
13
+ for (const value of resolve(point)) {
14
+ next.push({ ...point, [key]: value });
15
+ }
16
+ }
17
+ return new _DimGrid(next);
18
+ }
19
+ toArray() {
20
+ return [...this._points];
21
+ }
22
+ get size() {
23
+ return this._points.length;
24
+ }
25
+ *[Symbol.iterator]() {
26
+ yield* this._points;
27
+ }
28
+ };
29
+ function dimgrid() {
30
+ return DimGrid.create();
31
+ }
32
+ export {
33
+ DimGrid,
34
+ dimgrid
35
+ };
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "dimgrid",
3
+ "version": "0.1.0",
4
+ "description": "Build a typed N-dimensional grid of objects by adding named dimensions with discrete values",
5
+ "keywords": [
6
+ "grid",
7
+ "cartesian",
8
+ "dimensions",
9
+ "combinatorics",
10
+ "typescript",
11
+ "vitest",
12
+ "storybook",
13
+ "hyperparameters"
14
+ ],
15
+ "author": "Tomislav Herman <tomislav.herman@gmail.com> (https://github.com/tomislavherman)",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/tomislavherman/dimgrid.git"
19
+ },
20
+ "license": "MIT",
21
+ "main": "./dist/index.js",
22
+ "module": "./dist/index.mjs",
23
+ "types": "./dist/index.d.ts",
24
+ "exports": {
25
+ ".": {
26
+ "import": {
27
+ "types": "./dist/index.d.mts",
28
+ "default": "./dist/index.mjs"
29
+ },
30
+ "require": {
31
+ "types": "./dist/index.d.ts",
32
+ "default": "./dist/index.js"
33
+ }
34
+ }
35
+ },
36
+ "files": [
37
+ "dist"
38
+ ],
39
+ "scripts": {
40
+ "build": "tsup",
41
+ "dev": "tsup --watch",
42
+ "test": "vitest run",
43
+ "typecheck": "tsc --noEmit",
44
+ "prepublishOnly": "npm run typecheck && npm run test && npm run build"
45
+ },
46
+ "devDependencies": {
47
+ "tsup": "^8.5.0",
48
+ "typescript": "^5.8.3",
49
+ "vitest": "^3.2.3"
50
+ },
51
+ "tsup": {
52
+ "entry": [
53
+ "src/index.ts"
54
+ ],
55
+ "format": [
56
+ "esm",
57
+ "cjs"
58
+ ],
59
+ "dts": true,
60
+ "clean": true
61
+ },
62
+ "allowScripts": {
63
+ "esbuild@0.27.7": true
64
+ }
65
+ }