UNPKG

semem

Version:

Semantic Memory for Intelligent Agents

415 lines (360 loc) 14.8 kB
/** * VSOMTopology.js - Map Topology Management for VSOM * * This module manages different map topologies for the VSOM algorithm including * rectangular and hexagonal grids. It provides neighborhood calculations, * coordinate transformations, and boundary condition handling. * * Key Features: * - Rectangular and hexagonal topologies * - Neighborhood distance calculations * - Boundary condition handling (toroidal, bounded) * - Coordinate transformation utilities * - Visualization coordinate generation */ import { logger } from '../../../Utils.js' export default class VSOMTopology { constructor(options = {}) { this.options = { topology: options.topology || 'rectangular', // 'rectangular', 'hexagonal' boundaryCondition: options.boundaryCondition || 'bounded', // 'bounded', 'toroidal' mapSize: options.mapSize || [10, 10], ...options } this.width = this.options.mapSize[0] this.height = this.options.mapSize[1] this.totalNodes = this.width * this.height // Precompute neighborhood lookup tables for efficiency this.neighborhoodCache = new Map() this.distanceCache = new Map() // Topology-specific parameters this.hexOffsetEven = this.options.topology === 'hexagonal' logger.debug(`VSOMTopology initialized: ${this.options.topology} ${this.width}x${this.height}`) } /** * Calculate distance between two nodes on the map * @param {Array} coords1 - [x, y] coordinates of first node * @param {Array} coords2 - [x, y] coordinates of second node * @returns {number} Distance between nodes */ calculateDistance(coords1, coords2) { const cacheKey = `${coords1[0]},${coords1[1]}-${coords2[0]},${coords2[1]}` if (this.distanceCache.has(cacheKey)) { return this.distanceCache.get(cacheKey) } let distance switch (this.options.topology) { case 'hexagonal': distance = this.calculateHexagonalDistance(coords1, coords2) break case 'rectangular': default: distance = this.calculateRectangularDistance(coords1, coords2) break } this.distanceCache.set(cacheKey, distance) return distance } /** * Calculate distance in rectangular topology * @param {Array} coords1 - [x, y] coordinates of first node * @param {Array} coords2 - [x, y] coordinates of second node * @returns {number} Euclidean distance */ calculateRectangularDistance(coords1, coords2) { let dx = coords2[0] - coords1[0] let dy = coords2[1] - coords1[1] // Handle toroidal boundary conditions if (this.options.boundaryCondition === 'toroidal') { dx = this.toroidalDistance(dx, this.width) dy = this.toroidalDistance(dy, this.height) } return Math.sqrt(dx * dx + dy * dy) } /** * Calculate distance in hexagonal topology * @param {Array} coords1 - [x, y] coordinates of first node * @param {Array} coords2 - [x, y] coordinates of second node * @returns {number} Hexagonal distance */ calculateHexagonalDistance(coords1, coords2) { // Convert to cube coordinates for hexagonal distance calculation const cube1 = this.offsetToCube(coords1[0], coords1[1]) const cube2 = this.offsetToCube(coords2[0], coords2[1]) // Handle toroidal boundary conditions in cube space if (this.options.boundaryCondition === 'toroidal') { // Hexagonal toroidal wrapping is complex - for now use bounded logger.warn('Toroidal boundary conditions not fully implemented for hexagonal topology') } // Hexagonal distance in cube coordinates return (Math.abs(cube1.x - cube2.x) + Math.abs(cube1.y - cube2.y) + Math.abs(cube1.z - cube2.z)) / 2 } /** * Calculate toroidal distance (shortest path around torus) * @param {number} delta - Raw coordinate difference * @param {number} size - Size of the dimension * @returns {number} Shortest toroidal distance */ toroidalDistance(delta, size) { const absDelta = Math.abs(delta) return Math.min(absDelta, size - absDelta) } /** * Convert offset coordinates to cube coordinates (for hexagonal topology) * @param {number} col - Column (x coordinate) * @param {number} row - Row (y coordinate) * @returns {Object} Cube coordinates {x, y, z} */ offsetToCube(col, row) { const x = col - (row - (row & 1)) / 2 const z = row const y = -x - z return { x, y, z } } /** * Convert cube coordinates to offset coordinates * @param {Object} cube - Cube coordinates {x, y, z} * @returns {Array} Offset coordinates [col, row] */ cubeToOffset(cube) { const col = cube.x + (cube.z - (cube.z & 1)) / 2 const row = cube.z return [col, row] } /** * Get all neighbors of a node within a given radius * @param {Array} coords - [x, y] coordinates of the center node * @param {number} radius - Neighborhood radius * @returns {Array} Array of neighbor coordinates [[x, y], ...] */ getNeighbors(coords, radius) { const cacheKey = `${coords[0]},${coords[1]}-${radius}` if (this.neighborhoodCache.has(cacheKey)) { return this.neighborhoodCache.get(cacheKey) } const neighbors = [] const [centerX, centerY] = coords // Search in a square/hexagonal region around the center const searchRadius = Math.ceil(radius) for (let x = centerX - searchRadius; x <= centerX + searchRadius; x++) { for (let y = centerY - searchRadius; y <= centerY + searchRadius; y++) { // Check if coordinates are valid if (this.isValidCoordinate(x, y)) { const nodeCoords = [x, y] const distance = this.calculateDistance(coords, nodeCoords) if (distance <= radius) { neighbors.push({ coords: nodeCoords, distance: distance }) } } } } this.neighborhoodCache.set(cacheKey, neighbors) return neighbors } /** * Check if coordinates are valid for the current map * @param {number} x - X coordinate * @param {number} y - Y coordinate * @returns {boolean} True if coordinates are valid */ isValidCoordinate(x, y) { if (this.options.boundaryCondition === 'toroidal') { return true // All coordinates are valid with wrapping } return x >= 0 && x < this.width && y >= 0 && y < this.height } /** * Normalize coordinates according to boundary conditions * @param {number} x - X coordinate * @param {number} y - Y coordinate * @returns {Array} Normalized coordinates [x, y] */ normalizeCoordinates(x, y) { if (this.options.boundaryCondition === 'toroidal') { const normalizedX = ((x % this.width) + this.width) % this.width const normalizedY = ((y % this.height) + this.height) % this.height return [normalizedX, normalizedY] } // Bounded: clamp to valid range const clampedX = Math.max(0, Math.min(this.width - 1, x)) const clampedY = Math.max(0, Math.min(this.height - 1, y)) return [clampedX, clampedY] } /** * Convert linear index to 2D coordinates * @param {number} index - Linear index * @returns {Array} [x, y] coordinates */ indexToCoordinates(index) { const x = index % this.width const y = Math.floor(index / this.width) return [x, y] } /** * Convert 2D coordinates to linear index * @param {number} x - X coordinate * @param {number} y - Y coordinate * @returns {number} Linear index */ coordinatesToIndex(x, y) { const [normalizedX, normalizedY] = this.normalizeCoordinates(x, y) return normalizedY * this.width + normalizedX } /** * Get visualization coordinates for the map nodes * Converts map coordinates to visual coordinates suitable for plotting * @param {string} outputFormat - 'cartesian', 'normalized', 'screen' * @returns {Array} Array of coordinate pairs for visualization */ getVisualizationCoordinates(outputFormat = 'cartesian') { const coordinates = [] for (let y = 0; y < this.height; y++) { for (let x = 0; x < this.width; x++) { let visualCoords switch (this.options.topology) { case 'hexagonal': visualCoords = this.getHexagonalVisualCoords(x, y) break case 'rectangular': default: visualCoords = [x, y] break } // Transform according to output format switch (outputFormat) { case 'normalized': visualCoords = [ visualCoords[0] / this.width, visualCoords[1] / this.height ] break case 'screen': // Assume screen coordinates with origin at top-left visualCoords = [ visualCoords[0], this.height - visualCoords[1] - 1 ] break case 'cartesian': default: // Keep as-is break } coordinates.push({ mapCoords: [x, y], visualCoords: visualCoords, index: this.coordinatesToIndex(x, y) }) } } return coordinates } /** * Get visual coordinates for hexagonal topology * @param {number} x - Map X coordinate * @param {number} y - Map Y coordinate * @returns {Array} [visualX, visualY] coordinates */ getHexagonalVisualCoords(x, y) { // Hexagonal grid layout with offset const hexWidth = Math.sqrt(3) const hexHeight = 2.0 const visualX = x * hexWidth + (y % 2) * (hexWidth / 2) const visualY = y * hexHeight * 0.75 return [visualX, visualY] } /** * Create neighborhood function for training * @param {string} functionType - 'gaussian', 'mexican_hat', 'bubble', 'linear' * @returns {Function} Neighborhood function(distance, radius) */ createNeighborhoodFunction(functionType = 'gaussian') { switch (functionType) { case 'gaussian': return (distance, radius) => { if (radius <= 0) return distance === 0 ? 1 : 0 const sigma = radius / 3.0 // 3-sigma rule return Math.exp(-(distance * distance) / (2 * sigma * sigma)) } case 'mexican_hat': return (distance, radius) => { if (radius <= 0) return distance === 0 ? 1 : 0 const sigma = radius / 3.0 const factor = 2 / (Math.sqrt(3 * sigma) * Math.pow(Math.PI, 0.25)) const x = distance / sigma return factor * (1 - x * x) * Math.exp(-x * x / 2) } case 'bubble': return (distance, radius) => { return distance <= radius ? 1 : 0 } case 'linear': return (distance, radius) => { if (radius <= 0) return distance === 0 ? 1 : 0 return Math.max(0, 1 - distance / radius) } default: logger.warn(`Unknown neighborhood function: ${functionType}, using gaussian`) return this.createNeighborhoodFunction('gaussian') } } /** * Get topology information * @returns {Object} Topology information */ getTopologyInfo() { return { topology: this.options.topology, boundaryCondition: this.options.boundaryCondition, mapSize: [this.width, this.height], totalNodes: this.totalNodes, cacheSize: this.neighborhoodCache.size, distanceCacheSize: this.distanceCache.size } } /** * Clear topology caches */ clearCaches() { this.neighborhoodCache.clear() this.distanceCache.clear() logger.debug('VSOMTopology caches cleared') } /** * Estimate memory usage of topology management * @returns {number} Estimated memory usage in bytes */ estimateMemoryUsage() { const neighborhoodCacheSize = this.neighborhoodCache.size * 100 // Rough estimate const distanceCacheSize = this.distanceCache.size * 16 // Cache key + value return neighborhoodCacheSize + distanceCacheSize } /** * Validate topology configuration * @returns {Object} Validation result {valid: boolean, errors: Array} */ validateConfiguration() { const errors = [] if (this.width <= 0 || this.height <= 0) { errors.push('Map dimensions must be positive') } if (!['rectangular', 'hexagonal'].includes(this.options.topology)) { errors.push(`Invalid topology: ${this.options.topology}`) } if (!['bounded', 'toroidal'].includes(this.options.boundaryCondition)) { errors.push(`Invalid boundary condition: ${this.options.boundaryCondition}`) } if (this.options.topology === 'hexagonal' && this.options.boundaryCondition === 'toroidal') { logger.warn('Toroidal hexagonal topology may have limitations') } return { valid: errors.length === 0, errors: errors } } }