gridstamp 1.0.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.
Files changed (42) hide show
  1. package/.cursorrules +74 -0
  2. package/CLAUDE.md +61 -0
  3. package/LICENSE +190 -0
  4. package/README.md +107 -0
  5. package/dist/index.js +194 -0
  6. package/package.json +84 -0
  7. package/src/antispoofing/detector.ts +509 -0
  8. package/src/antispoofing/index.ts +7 -0
  9. package/src/gamification/badges.ts +429 -0
  10. package/src/gamification/fleet-leaderboard.ts +293 -0
  11. package/src/gamification/index.ts +44 -0
  12. package/src/gamification/streaks.ts +243 -0
  13. package/src/gamification/trust-tiers.ts +393 -0
  14. package/src/gamification/zone-mastery.ts +256 -0
  15. package/src/index.ts +341 -0
  16. package/src/memory/index.ts +9 -0
  17. package/src/memory/place-cells.ts +279 -0
  18. package/src/memory/spatial-memory.ts +375 -0
  19. package/src/navigation/index.ts +1 -0
  20. package/src/navigation/pathfinding.ts +403 -0
  21. package/src/perception/camera.ts +249 -0
  22. package/src/perception/index.ts +2 -0
  23. package/src/types/index.ts +416 -0
  24. package/src/utils/crypto.ts +94 -0
  25. package/src/utils/index.ts +2 -0
  26. package/src/utils/math.ts +204 -0
  27. package/src/verification/index.ts +9 -0
  28. package/src/verification/spatial-proof.ts +442 -0
  29. package/tests/antispoofing/detector.test.ts +196 -0
  30. package/tests/gamification/badges.test.ts +163 -0
  31. package/tests/gamification/fleet-leaderboard.test.ts +181 -0
  32. package/tests/gamification/streaks.test.ts +158 -0
  33. package/tests/gamification/trust-tiers.test.ts +165 -0
  34. package/tests/gamification/zone-mastery.test.ts +143 -0
  35. package/tests/memory/place-cells.test.ts +128 -0
  36. package/tests/stress/load.test.ts +499 -0
  37. package/tests/stress/security.test.ts +378 -0
  38. package/tests/stress/simulation.test.ts +361 -0
  39. package/tests/utils/crypto.test.ts +115 -0
  40. package/tests/utils/math.test.ts +195 -0
  41. package/tests/verification/spatial-proof.test.ts +299 -0
  42. package/tsconfig.json +26 -0
package/src/index.ts ADDED
@@ -0,0 +1,341 @@
1
+ /**
2
+ * GridStamp — Spatial Proof-of-Presence for Autonomous Robots
3
+ *
4
+ * Nobody else unifies spatial memory + payment verification + anti-spoofing.
5
+ * - Niantic has maps but no payments
6
+ * - NVIDIA has rendering but no memory persistence
7
+ * - OpenMind has robot payments but no spatial proof
8
+ * - FOAM/Auki has proof-of-location but no payments
9
+ *
10
+ * GridStamp sits at the intersection.
11
+ *
12
+ * API:
13
+ * agent.see() — Capture + process current view
14
+ * agent.remember() — Store spatial context to memory
15
+ * agent.navigate() — Plan path to target
16
+ * agent.verifySpatial() — Prove robot is at claimed location
17
+ * agent.settle() — Payment with spatial proof requirement
18
+ */
19
+
20
+ // Re-export all public types
21
+ export type {
22
+ Vec3,
23
+ Quaternion,
24
+ Pose,
25
+ Mat4,
26
+ AABB,
27
+ CameraIntrinsics,
28
+ StereoConfig,
29
+ CameraFrame,
30
+ DepthMap,
31
+ CameraConfig,
32
+ GaussianSplat,
33
+ SplatScene,
34
+ RenderedView,
35
+ ShortTermEntry,
36
+ EpisodicMemory,
37
+ LongTermMemory,
38
+ ConsolidationEvent,
39
+ PlaceCell,
40
+ GridCell,
41
+ SpatialCode,
42
+ Waypoint,
43
+ Path,
44
+ SpatialMetrics,
45
+ VerificationThresholds,
46
+ SpatialProof,
47
+ SpatialSettlement,
48
+ ThreatDetection,
49
+ FrameIntegrity,
50
+ GridStampConfig,
51
+ GridStampAgent,
52
+ } from './types/index.js';
53
+
54
+ export {
55
+ CameraType,
56
+ MemoryTier,
57
+ PathAlgorithm,
58
+ ReferenceFrame,
59
+ SettlementStatus,
60
+ ThreatType,
61
+ ThreatSeverity,
62
+ } from './types/index.js';
63
+
64
+ // Re-export modules
65
+ export * from './perception/index.js';
66
+ export * from './memory/index.js';
67
+ export * from './navigation/index.js';
68
+ export * from './verification/index.js';
69
+ export * from './antispoofing/index.js';
70
+ export * from './gamification/index.js';
71
+
72
+ // Re-export key utilities
73
+ export {
74
+ hmacSign,
75
+ hmacVerify,
76
+ signFrame,
77
+ verifyFrame,
78
+ generateNonce,
79
+ sha256,
80
+ deriveKey,
81
+ } from './utils/crypto.js';
82
+
83
+ export {
84
+ vec3Distance,
85
+ vec3Add,
86
+ vec3Sub,
87
+ vec3Scale,
88
+ vec3Normalize,
89
+ egoToAllo,
90
+ alloToEgo,
91
+ stereoDepth,
92
+ gaussian3D,
93
+ meanAbsoluteError,
94
+ poseToMat4,
95
+ quatRotateVec3,
96
+ quatSlerp,
97
+ } from './utils/math.js';
98
+
99
+ // ============================================================
100
+ // AGENT FACTORY
101
+ // ============================================================
102
+
103
+ import type {
104
+ GridStampConfig,
105
+ GridStampAgent as IGridStampAgent,
106
+ CameraFrame,
107
+ EpisodicMemory,
108
+ Path,
109
+ SpatialProof,
110
+ SpatialSettlement,
111
+ SpatialCode,
112
+ Pose,
113
+ Vec3,
114
+ PathAlgorithm,
115
+ VerificationThresholds,
116
+ } from './types/index.js';
117
+ import type { CameraDriver } from './perception/index.js';
118
+ import { FrameCapture } from './perception/index.js';
119
+ import {
120
+ ShortTermMemory,
121
+ MidTermMemory,
122
+ LongTermMemory as LongTermMemoryStore,
123
+ MemoryConsolidator,
124
+ } from './memory/index.js';
125
+ import { PlaceCellPopulation, GridCellSystem, computeSpatialCode } from './memory/index.js';
126
+ import { OccupancyGrid, planPath } from './navigation/index.js';
127
+ import {
128
+ generateSpatialProof,
129
+ createSettlement,
130
+ } from './verification/index.js';
131
+ import { FrameIntegrityChecker, CanarySystem } from './antispoofing/index.js';
132
+ import { deriveKey } from './utils/crypto.js';
133
+
134
+ /**
135
+ * Create a GridStamp agent
136
+ *
137
+ * This is the main entry point. Pass a config + camera driver,
138
+ * get back an agent with see/remember/navigate/verify/settle methods.
139
+ */
140
+ export function createAgent(
141
+ config: GridStampConfig,
142
+ primaryDriver: CameraDriver,
143
+ ): IGridStampAgent {
144
+ // Validate config
145
+ if (!config.robotId) throw new Error('robotId is required');
146
+ if (!config.hmacSecret || config.hmacSecret.length < 32) {
147
+ throw new Error('hmacSecret must be at least 32 characters');
148
+ }
149
+
150
+ // Derive separate keys for each subsystem (key separation)
151
+ const frameKey = deriveKey(config.hmacSecret, 'frame-signing');
152
+ const memoryKey = deriveKey(config.hmacSecret, 'memory-signing');
153
+ const proofKey = config.hmacSecret; // proof uses master key
154
+
155
+ // Initialize subsystems
156
+ const frameCapture = new FrameCapture(
157
+ primaryDriver,
158
+ config.cameras[0]!,
159
+ frameKey,
160
+ );
161
+
162
+ const shortTerm = new ShortTermMemory(
163
+ 900,
164
+ config.memoryConfig?.shortTermTTL ?? 30_000,
165
+ );
166
+ const midTerm = new MidTermMemory(
167
+ config.memoryConfig?.midTermMaxEntries ?? 1000,
168
+ );
169
+ const longTermStore = new LongTermMemoryStore(memoryKey);
170
+ const consolidator = new MemoryConsolidator(shortTerm, midTerm, longTermStore);
171
+
172
+ const placeCells = new PlaceCellPopulation();
173
+ const gridCells = new GridCellSystem();
174
+
175
+ const integrityChecker = new FrameIntegrityChecker(frameKey);
176
+ const canaries = new CanarySystem(memoryKey);
177
+
178
+ let lastFrame: CameraFrame | undefined;
179
+ let initialized = false;
180
+
181
+ const agent: IGridStampAgent = {
182
+ async see(): Promise<CameraFrame> {
183
+ if (!initialized) {
184
+ await frameCapture.initialize();
185
+ initialized = true;
186
+ }
187
+
188
+ const frame = await frameCapture.capture();
189
+
190
+ // Run integrity check on every frame (fail-closed)
191
+ const integrity = integrityChecker.check(frame);
192
+ if (!integrityChecker.isSafe(integrity)) {
193
+ const criticalThreats = integrity.threats
194
+ .filter(t => t.severity === 'critical')
195
+ .map(t => t.details)
196
+ .join('; ');
197
+ throw new Error(`Frame rejected by anti-spoofing: ${criticalThreats}`);
198
+ }
199
+
200
+ // Store in short-term memory (empty splats for now — 3DGS integration point)
201
+ shortTerm.add(frame, []);
202
+
203
+ // Auto-generate place cell at frame position if we have pose
204
+ if (frame.pose) {
205
+ const cell = (await import('./memory/place-cells.js')).createPlaceCell(
206
+ frame.pose.position,
207
+ );
208
+ placeCells.add(cell);
209
+ }
210
+
211
+ lastFrame = frame;
212
+ return frame;
213
+ },
214
+
215
+ async remember(tags?: string[]): Promise<EpisodicMemory> {
216
+ const consolidated = consolidator.consolidateToMidTerm(tags ?? []);
217
+ if (!consolidated) {
218
+ // Even if not enough for full consolidation, store what we have
219
+ const entries = shortTerm.getAll();
220
+ if (entries.length === 0) {
221
+ throw new Error('No frames in short-term memory to remember');
222
+ }
223
+ // Force store
224
+ const allSplats = entries.flatMap(e => e.splats);
225
+ const scene = {
226
+ id: (await import('./utils/crypto.js')).generateNonce(16),
227
+ splats: allSplats,
228
+ count: allSplats.length,
229
+ boundingBox: {
230
+ min: { x: 0, y: 0, z: 0 },
231
+ max: { x: 0, y: 0, z: 0 },
232
+ },
233
+ createdAt: Date.now(),
234
+ };
235
+ const location = lastFrame?.pose?.position ?? { x: 0, y: 0, z: 0 };
236
+ return midTerm.store(scene, location, tags ?? []);
237
+ }
238
+
239
+ // Get the most recent mid-term memory
240
+ const memories = midTerm.findNear(
241
+ lastFrame?.pose?.position ?? { x: 0, y: 0, z: 0 },
242
+ 100,
243
+ );
244
+ return memories[0]!;
245
+ },
246
+
247
+ async navigate(target: Vec3, options?: { algorithm?: PathAlgorithm }): Promise<Path> {
248
+ const start = lastFrame?.pose?.position ?? { x: 0, y: 0, z: 0 };
249
+ const bounds = {
250
+ min: {
251
+ x: Math.min(start.x, target.x) - 10,
252
+ y: Math.min(start.y, target.y) - 10,
253
+ z: Math.min(start.z, target.z) - 2,
254
+ },
255
+ max: {
256
+ x: Math.max(start.x, target.x) + 10,
257
+ y: Math.max(start.y, target.y) + 10,
258
+ z: Math.max(start.z, target.z) + 2,
259
+ },
260
+ };
261
+ const grid = new OccupancyGrid(bounds);
262
+ const algorithm = options?.algorithm ?? config.navigationConfig?.defaultAlgorithm ?? 'a-star' as PathAlgorithm;
263
+ const path = planPath(start, target, grid, bounds, algorithm);
264
+ if (!path) throw new Error('No path found to target');
265
+ return path;
266
+ },
267
+
268
+ async verifySpatial(claimedPose?: Pose): Promise<SpatialProof> {
269
+ if (!lastFrame) throw new Error('No frame captured. Call see() first.');
270
+
271
+ const pose = claimedPose ?? lastFrame.pose;
272
+ if (!pose) throw new Error('No pose available. Provide claimedPose or ensure camera provides pose.');
273
+
274
+ // In production, this would render from long-term memory 3DGS
275
+ // For now, use the last frame as "expected" (self-verification)
276
+ const expectedRender = {
277
+ rgb: lastFrame.rgb,
278
+ depth: lastFrame.depth ?? new Float32Array(0),
279
+ width: lastFrame.width,
280
+ height: lastFrame.height,
281
+ pose,
282
+ renderTimeMs: 0,
283
+ };
284
+
285
+ return generateSpatialProof(
286
+ config.robotId,
287
+ pose,
288
+ lastFrame,
289
+ expectedRender,
290
+ 'pending-merkle-root', // would come from long-term memory
291
+ [],
292
+ proofKey,
293
+ config.verificationThresholds as VerificationThresholds | undefined,
294
+ );
295
+ },
296
+
297
+ async settle(params: {
298
+ amount: number;
299
+ currency: string;
300
+ payeeId: string;
301
+ spatialProof: boolean;
302
+ }): Promise<SpatialSettlement> {
303
+ if (!params.spatialProof) {
304
+ throw new Error('GridStamp requires spatialProof=true. Use MnemoPay directly for non-spatial payments.');
305
+ }
306
+
307
+ const proof = await agent.verifySpatial();
308
+ return createSettlement(proof, params.amount, params.currency, params.payeeId, proofKey);
309
+ },
310
+
311
+ getSpatialCode(): SpatialCode {
312
+ const position = lastFrame?.pose?.position ?? { x: 0, y: 0, z: 0 };
313
+ return computeSpatialCode(position, placeCells, gridCells);
314
+ },
315
+
316
+ getMemoryStats() {
317
+ const shortEntries = shortTerm.getAll();
318
+ return {
319
+ shortTerm: {
320
+ count: shortTerm.count,
321
+ oldestMs: shortEntries.length > 0 ? Date.now() - shortEntries[0]!.timestamp : 0,
322
+ },
323
+ midTerm: {
324
+ count: midTerm.count,
325
+ totalSplats: midTerm.getTotalSplatCount(),
326
+ },
327
+ longTerm: {
328
+ count: longTermStore.totalEntries,
329
+ rooms: longTermStore.roomCount,
330
+ },
331
+ };
332
+ },
333
+
334
+ async shutdown(): Promise<void> {
335
+ await frameCapture.shutdown();
336
+ integrityChecker.reset();
337
+ },
338
+ };
339
+
340
+ return agent;
341
+ }
@@ -0,0 +1,9 @@
1
+ export { ShortTermMemory, MidTermMemory, LongTermMemory, MemoryConsolidator } from './spatial-memory.js';
2
+ export {
3
+ createPlaceCell,
4
+ PlaceCellPopulation,
5
+ createGridCell,
6
+ GridCellModule,
7
+ GridCellSystem,
8
+ computeSpatialCode,
9
+ } from './place-cells.js';
@@ -0,0 +1,279 @@
1
+ /**
2
+ * Place Cell & Grid Cell Model (Bio-inspired spatial coding)
3
+ *
4
+ * Based on Nobel Prize 2014 work:
5
+ * - Place cells (O'Keefe, 1971): Neurons in hippocampus that fire at specific locations
6
+ * - Grid cells (Moser & Moser, 2005): Neurons in entorhinal cortex with hexagonal firing patterns
7
+ *
8
+ * Together they form a biological GPS for spatial coding.
9
+ * We use them to create robust position estimates from noisy sensor data.
10
+ */
11
+ import type { Vec3, PlaceCell, GridCell, SpatialCode } from '../types/index.js';
12
+ import { vec3Distance, gaussian3D } from '../utils/math.js';
13
+ import { generateNonce } from '../utils/crypto.js';
14
+
15
+ // ============================================================
16
+ // PLACE CELLS
17
+ // ============================================================
18
+
19
+ /**
20
+ * Create a place cell centered at a specific location
21
+ * Activation follows a 3D Gaussian: peaks at center, falls off with distance
22
+ */
23
+ export function createPlaceCell(
24
+ center: Vec3,
25
+ radius: number = 2.0, // meters — typical place field radius
26
+ peakRate: number = 1.0,
27
+ ): PlaceCell {
28
+ const id = `pc_${generateNonce(8)}`;
29
+ const sigma = radius / 2.0; // sigma = half the field radius
30
+
31
+ return {
32
+ id,
33
+ center,
34
+ radius,
35
+ peakRate,
36
+ activation(position: Vec3): number {
37
+ return gaussian3D(position, center, sigma, peakRate);
38
+ },
39
+ };
40
+ }
41
+
42
+ /**
43
+ * Place cell population — manages a set of place cells covering an environment
44
+ */
45
+ export class PlaceCellPopulation {
46
+ private cells: PlaceCell[] = [];
47
+ private readonly minSpacing: number; // minimum distance between cell centers
48
+
49
+ constructor(minSpacing: number = 1.5) {
50
+ this.minSpacing = minSpacing;
51
+ }
52
+
53
+ /** Add a place cell (reject if too close to existing cell) */
54
+ add(cell: PlaceCell): boolean {
55
+ for (const existing of this.cells) {
56
+ if (vec3Distance(existing.center, cell.center) < this.minSpacing) {
57
+ return false; // too close to existing cell
58
+ }
59
+ }
60
+ this.cells.push(cell);
61
+ return true;
62
+ }
63
+
64
+ /**
65
+ * Auto-generate place cells to cover a region
66
+ * Uses a quasi-random distribution for even coverage
67
+ */
68
+ coverRegion(
69
+ min: Vec3,
70
+ max: Vec3,
71
+ spacing: number = 2.0,
72
+ radius: number = 2.0,
73
+ ): number {
74
+ let count = 0;
75
+ for (let x = min.x; x <= max.x; x += spacing) {
76
+ for (let y = min.y; y <= max.y; y += spacing) {
77
+ for (let z = min.z; z <= max.z; z += spacing) {
78
+ const cell = createPlaceCell({ x, y, z }, radius);
79
+ if (this.add(cell)) count++;
80
+ }
81
+ }
82
+ }
83
+ return count;
84
+ }
85
+
86
+ /**
87
+ * Get activation of all place cells for a position
88
+ * Returns map of cell ID → activation level [0,1]
89
+ */
90
+ getActivations(position: Vec3): ReadonlyMap<string, number> {
91
+ const activations = new Map<string, number>();
92
+ for (const cell of this.cells) {
93
+ const a = cell.activation(position);
94
+ if (a > 0.01) { // threshold noise
95
+ activations.set(cell.id, a);
96
+ }
97
+ }
98
+ return activations;
99
+ }
100
+
101
+ /**
102
+ * Estimate position from place cell activations (population vector decoding)
103
+ * Weighted average of cell centers by activation level
104
+ */
105
+ decodePosition(activations: ReadonlyMap<string, number>): Vec3 {
106
+ let totalWeight = 0;
107
+ let wx = 0, wy = 0, wz = 0;
108
+
109
+ for (const [id, activation] of activations) {
110
+ const cell = this.cells.find(c => c.id === id);
111
+ if (!cell) continue;
112
+ wx += cell.center.x * activation;
113
+ wy += cell.center.y * activation;
114
+ wz += cell.center.z * activation;
115
+ totalWeight += activation;
116
+ }
117
+
118
+ if (totalWeight < 1e-10) {
119
+ return { x: 0, y: 0, z: 0 };
120
+ }
121
+ return {
122
+ x: wx / totalWeight,
123
+ y: wy / totalWeight,
124
+ z: wz / totalWeight,
125
+ };
126
+ }
127
+
128
+ get count(): number {
129
+ return this.cells.length;
130
+ }
131
+ }
132
+
133
+ // ============================================================
134
+ // GRID CELLS
135
+ // ============================================================
136
+
137
+ /**
138
+ * Create a grid cell with hexagonal firing pattern
139
+ *
140
+ * Grid cells fire at regular intervals forming a hexagonal tiling.
141
+ * The activation is the sum of 3 cosine waves at 60° intervals.
142
+ * Parameters: spacing (period), orientation (rotation), phase (offset)
143
+ */
144
+ export function createGridCell(
145
+ spacing: number = 1.0, // meters between grid vertices
146
+ orientation: number = 0, // radians
147
+ phase: Vec3 = { x: 0, y: 0, z: 0 },
148
+ ): GridCell {
149
+ const id = `gc_${generateNonce(8)}`;
150
+ const k = (2 * Math.PI) / spacing; // wave number
151
+
152
+ // Three direction vectors at 60° intervals (hexagonal)
153
+ const dirs = [0, Math.PI / 3, 2 * Math.PI / 3].map(angle => {
154
+ const a = angle + orientation;
155
+ return { x: Math.cos(a), y: Math.sin(a) };
156
+ });
157
+
158
+ return {
159
+ id,
160
+ spacing,
161
+ orientation,
162
+ phase,
163
+ activation(position: Vec3): number {
164
+ // Sum of 3 cosine waves creates hexagonal pattern
165
+ let sum = 0;
166
+ for (const dir of dirs) {
167
+ const proj = (position.x - phase.x) * dir.x + (position.y - phase.y) * dir.y;
168
+ sum += Math.cos(k * proj);
169
+ }
170
+ // Normalize from [-3, 3] to [0, 1]
171
+ return (sum + 3) / 6;
172
+ },
173
+ };
174
+ }
175
+
176
+ /**
177
+ * Grid cell module — a group of grid cells with the same spacing but different phases
178
+ * Multiple modules at different scales form a hierarchical coordinate system
179
+ */
180
+ export class GridCellModule {
181
+ private cells: GridCell[] = [];
182
+ readonly spacing: number;
183
+ readonly orientation: number;
184
+
185
+ constructor(spacing: number, orientation: number, cellCount: number = 16) {
186
+ this.spacing = spacing;
187
+ this.orientation = orientation;
188
+
189
+ // Generate cells with different random phases
190
+ for (let i = 0; i < cellCount; i++) {
191
+ const phase: Vec3 = {
192
+ x: (Math.random() - 0.5) * spacing,
193
+ y: (Math.random() - 0.5) * spacing,
194
+ z: 0,
195
+ };
196
+ this.cells.push(createGridCell(spacing, orientation, phase));
197
+ }
198
+ }
199
+
200
+ getActivations(position: Vec3): ReadonlyMap<string, number> {
201
+ const activations = new Map<string, number>();
202
+ for (const cell of this.cells) {
203
+ activations.set(cell.id, cell.activation(position));
204
+ }
205
+ return activations;
206
+ }
207
+
208
+ get count(): number {
209
+ return this.cells.length;
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Multi-scale grid cell system
215
+ * Uses modules at different spacings (like a multi-resolution ruler)
216
+ * Typical ratios: 1:1.4:2:2.8 (approximate √2 scaling)
217
+ */
218
+ export class GridCellSystem {
219
+ private modules: GridCellModule[] = [];
220
+
221
+ constructor(
222
+ baseSpacing: number = 0.5,
223
+ scaleRatio: number = Math.SQRT2,
224
+ numModules: number = 4,
225
+ cellsPerModule: number = 16,
226
+ ) {
227
+ for (let i = 0; i < numModules; i++) {
228
+ const spacing = baseSpacing * Math.pow(scaleRatio, i);
229
+ const orientation = (i * Math.PI) / (6 * numModules); // slight rotation per module
230
+ this.modules.push(new GridCellModule(spacing, orientation, cellsPerModule));
231
+ }
232
+ }
233
+
234
+ getActivations(position: Vec3): ReadonlyMap<string, number> {
235
+ const all = new Map<string, number>();
236
+ for (const module of this.modules) {
237
+ for (const [id, activation] of module.getActivations(position)) {
238
+ all.set(id, activation);
239
+ }
240
+ }
241
+ return all;
242
+ }
243
+
244
+ get totalCells(): number {
245
+ return this.modules.reduce((sum, m) => sum + m.count, 0);
246
+ }
247
+ }
248
+
249
+ // ============================================================
250
+ // SPATIAL CODING — combined place + grid cell system
251
+ // ============================================================
252
+
253
+ /**
254
+ * Compute full spatial code for a position
255
+ * Combines place cell and grid cell activations into a population vector
256
+ */
257
+ export function computeSpatialCode(
258
+ position: Vec3,
259
+ placeCells: PlaceCellPopulation,
260
+ gridCells: GridCellSystem,
261
+ ): SpatialCode {
262
+ const placeActivations = placeCells.getActivations(position);
263
+ const gridActivations = gridCells.getActivations(position);
264
+
265
+ // Confidence based on number of active place cells
266
+ const activePlaceCells = [...placeActivations.values()].filter(a => a > 0.1).length;
267
+ const confidence = Math.min(1, activePlaceCells / 3); // 3+ active cells = full confidence
268
+
269
+ // Position estimate from place cell decoding
270
+ const estimatedPosition = placeCells.decodePosition(placeActivations);
271
+
272
+ return {
273
+ placeCellActivations: placeActivations,
274
+ gridCellActivations: gridActivations,
275
+ estimatedPosition,
276
+ confidence,
277
+ timestamp: Date.now(),
278
+ };
279
+ }