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.
- package/.cursorrules +74 -0
- package/CLAUDE.md +61 -0
- package/LICENSE +190 -0
- package/README.md +107 -0
- package/dist/index.js +194 -0
- package/package.json +84 -0
- package/src/antispoofing/detector.ts +509 -0
- package/src/antispoofing/index.ts +7 -0
- package/src/gamification/badges.ts +429 -0
- package/src/gamification/fleet-leaderboard.ts +293 -0
- package/src/gamification/index.ts +44 -0
- package/src/gamification/streaks.ts +243 -0
- package/src/gamification/trust-tiers.ts +393 -0
- package/src/gamification/zone-mastery.ts +256 -0
- package/src/index.ts +341 -0
- package/src/memory/index.ts +9 -0
- package/src/memory/place-cells.ts +279 -0
- package/src/memory/spatial-memory.ts +375 -0
- package/src/navigation/index.ts +1 -0
- package/src/navigation/pathfinding.ts +403 -0
- package/src/perception/camera.ts +249 -0
- package/src/perception/index.ts +2 -0
- package/src/types/index.ts +416 -0
- package/src/utils/crypto.ts +94 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/math.ts +204 -0
- package/src/verification/index.ts +9 -0
- package/src/verification/spatial-proof.ts +442 -0
- package/tests/antispoofing/detector.test.ts +196 -0
- package/tests/gamification/badges.test.ts +163 -0
- package/tests/gamification/fleet-leaderboard.test.ts +181 -0
- package/tests/gamification/streaks.test.ts +158 -0
- package/tests/gamification/trust-tiers.test.ts +165 -0
- package/tests/gamification/zone-mastery.test.ts +143 -0
- package/tests/memory/place-cells.test.ts +128 -0
- package/tests/stress/load.test.ts +499 -0
- package/tests/stress/security.test.ts +378 -0
- package/tests/stress/simulation.test.ts +361 -0
- package/tests/utils/crypto.test.ts +115 -0
- package/tests/utils/math.test.ts +195 -0
- package/tests/verification/spatial-proof.test.ts +299 -0
- 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,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
|
+
}
|