/**
* Main orchestrator for parameter-based corpuscle selection from Ragno corpus
* Now includes ZPT ontology integration for RDF navigation storage
*/
import ParameterValidator from '../parameters/ParameterValidator.js';
import ParameterNormalizer from '../parameters/ParameterNormalizer.js';
import FilterBuilder from '../parameters/FilterBuilder.js';
import SelectionCriteria from '../parameters/SelectionCriteria.js';
import { logger } from '../../Utils.js';
// Import ZPT ontology integration
import { NamespaceUtils, getSPARQLPrefixes } from '../ontology/ZPTNamespaces.js';
import { ZPTDataFactory } from '../ontology/ZPTDataFactory.js';
import { EnhancedZPTQueries } from '../enhanced/EnhancedZPTQueries.js';
export default class CorpuscleSelector {
constructor(ragnoCorpus, options = {}) {
this.corpus = ragnoCorpus;
this.sparqlStore = options.sparqlStore;
this.embeddingHandler = options.embeddingHandler;
// Initialize parameter processing components
this.validator = new ParameterValidator();
this.normalizer = new ParameterNormalizer();
this.filterBuilder = new FilterBuilder(options);
this.criteriaBuilder = new SelectionCriteria(options);
// Selection configuration
this.config = {
maxResults: options.maxResults || 1000,
timeoutMs: options.timeoutMs || 30000,
enableCaching: options.enableCaching !== false,
debugMode: options.debugMode || false,
enableZPTStorage: options.enableZPTStorage !== false, // Enable ZPT RDF storage by default
navigationGraph: options.navigationGraph || 'http://purl.org/stuff/navigation',
...options
};
// Initialize ZPT ontology integration
if (this.config.enableZPTStorage) {
this.zptDataFactory = new ZPTDataFactory({
navigationGraph: this.config.navigationGraph
});
}
// Initialize enhanced query engine
this.enhancedQueries = new EnhancedZPTQueries({
contentGraph: 'http://hyperdata.it/content',
navigationGraph: this.config.navigationGraph,
sparqlStore: this.sparqlStore
});
// Performance tracking
this.metrics = {
totalSelections: 0,
avgSelectionTime: 0,
cacheHits: 0,
cacheMisses: 0
};
// Result cache
this.cache = new Map();
this.cacheExpiry = options.cacheExpiry || 3600000; // 1 hour
}
/**
* Main selection method - selects corpuscles based on ZPT parameters
* @param {Object} params - Raw ZPT navigation parameters
* @returns {Promise<Object>} Selection results with corpuscles and metadata
*/
async select(params) {
const startTime = Date.now();
this.metrics.totalSelections++;
try {
logger.info('Starting corpuscle selection', { params });
// Phase 1: Validate parameters
const validationResult = this.validator.validate(params);
if (!validationResult.valid) {
throw new Error(`Parameter validation failed: ${validationResult.message}`);
}
// Phase 2: Normalize parameters
const normalizedParams = this.normalizer.normalize(params);
logger.debug('Parameters normalized', { normalizedParams });
// Phase 3: Check cache
const cacheKey = this.normalizer.createParameterHash(normalizedParams);
if (this.config.enableCaching) {
const cachedResult = this.getCachedResult(cacheKey);
if (cachedResult) {
this.metrics.cacheHits++;
logger.debug('Cache hit', { cacheKey });
return this.enrichCachedResult(cachedResult, normalizedParams);
}
this.metrics.cacheMisses++;
}
// Phase 4: Build selection criteria
const selectionCriteria = this.criteriaBuilder.buildCriteria(normalizedParams);
logger.debug('Selection criteria built', {
criteria: this.criteriaBuilder.getSummary(selectionCriteria)
});
// Phase 5: Execute selection based on tilt type
let corpuscles;
switch (normalizedParams.tilt.representation) {
case 'embedding':
corpuscles = await this.selectByEmbedding(normalizedParams, selectionCriteria);
break;
case 'keywords':
corpuscles = await this.selectByKeywords(normalizedParams, selectionCriteria);
break;
case 'graph':
corpuscles = await this.selectByGraph(normalizedParams, selectionCriteria);
break;
case 'temporal':
corpuscles = await this.selectByTemporal(normalizedParams, selectionCriteria);
break;
default:
throw new Error(`Unsupported tilt representation: ${normalizedParams.tilt.representation}`);
}
// Phase 6: Apply post-processing
const processedCorpuscles = await this.postProcessCorpuscles(
corpuscles,
normalizedParams,
selectionCriteria
);
// Phase 7: Build result object
const result = this.buildSelectionResult(
processedCorpuscles,
normalizedParams,
selectionCriteria,
Date.now() - startTime
);
// Phase 8: Cache result
if (this.config.enableCaching) {
this.cacheResult(cacheKey, result);
}
// Update metrics
this.updateMetrics(Date.now() - startTime);
// Store navigation metadata in ZPT RDF if enabled
if (this.config.enableZPTStorage) {
await this.storeNavigationData(normalizedParams, result);
}
logger.info('Corpuscle selection completed', {
resultCount: result.corpuscles.length,
selectionTime: result.metadata.selectionTime,
cacheKey
});
return result;
} catch (error) {
logger.error('Corpuscle selection failed', { error, params });
throw new Error(`Selection failed: ${error.message}`);
}
}
/**
* Enhanced selection method using live SPARQL filtering
* @param {Object} params - ZPT navigation parameters
* @returns {Promise<Object>} Enhanced selection results
*/
async selectEnhanced(params) {
const startTime = Date.now();
this.metrics.totalSelections++;
try {
logger.info('🚀 Starting enhanced ZPT corpuscle selection', { params });
// Use the enhanced query engine
const result = await this.enhancedQueries.executeNavigation(params);
// Transform results to corpuscle format
const corpuscles = this.transformToCorpuscles(result.results, params);
// Build enhanced result object
const enhancedResult = {
corpuscles: corpuscles,
metadata: {
selectionTime: Date.now() - startTime,
totalFound: result.results.length,
zoomLevel: params.zoom,
panFilters: params.pan,
tiltProjection: params.tilt,
queryTimestamp: result.timestamp,
enhanced: true
},
navigation: {
queryParams: result.queryParams,
success: result.success
}
};
// Update metrics
this.updateMetrics(Date.now() - startTime);
logger.info('✅ Enhanced corpuscle selection completed', {
resultCount: enhancedResult.corpuscles.length,
selectionTime: enhancedResult.metadata.selectionTime
});
return enhancedResult;
} catch (error) {
logger.error('❌ Enhanced corpuscle selection failed', { error, params });
throw new Error(`Enhanced selection failed: ${error.message}`);
}
}
/**
* Transform SPARQL results to corpuscle format
*/
transformToCorpuscles(sparqlResults, params) {
return sparqlResults.map((binding, index) => {
const corpuscle = {
uri: binding.item?.value || `http://purl.org/stuff/instance/corpuscle-${index}`,
content: binding.content?.value || binding.label?.value || '',
type: this.getCorpuscleType(params.zoom),
metadata: {
zoom: params.zoom,
tilt: params.tilt,
index: index
}
};
// Add zoom-specific fields
switch (params.zoom?.toLowerCase()) {
case 'entity':
corpuscle.label = binding.label?.value;
corpuscle.isEntryPoint = binding.isEntryPoint?.value === 'true';
corpuscle.frequency = parseInt(binding.frequency?.value || '0');
break;
case 'unit':
corpuscle.created = binding.created?.value;
corpuscle.hasEmbedding = !!binding.hasEmbedding?.value;
break;
case 'text':
corpuscle.created = binding.created?.value;
corpuscle.sourceDocument = binding.sourceDocument?.value;
break;
case 'community':
corpuscle.memberCount = parseInt(binding.memberCount?.value || '0');
break;
case 'corpus':
corpuscle.elementCount = parseInt(binding.elementCount?.value || '0');
corpuscle.description = binding.description?.value;
break;
}
// Add tilt-specific fields
if (params.tilt === 'embedding' && binding.embedding) {
corpuscle.embedding = {
model: binding.embeddingModel?.value,
dimension: parseInt(binding.dimension?.value || '0'),
vector: binding.embedding?.value
};
}
return corpuscle;
});
}
/**
* Get corpuscle type based on zoom level
*/
getCorpuscleType(zoomLevel) {
const typeMapping = {
'entity': 'ragno:Entity',
'unit': 'ragno:Unit',
'text': 'ragno:TextElement',
'community': 'ragno:Community',
'corpus': 'ragno:Corpus'
};
return typeMapping[zoomLevel?.toLowerCase()] || 'ragno:Element';
}
/**
* Select corpuscles using embedding similarity
*/
async selectByEmbedding(normalizedParams, selectionCriteria) {
if (!this.embeddingHandler) {
throw new Error('EmbeddingHandler required for embedding-based selection');
}
// Build SPARQL query for embedding search
const queryConfig = this.filterBuilder.buildQuery(normalizedParams);
// Execute base query to get candidates
const candidates = await this.executeQuery(queryConfig);
// If we have a topic, generate query embedding for similarity
if (normalizedParams.pan.topic) {
const queryEmbedding = await this.embeddingHandler.generateEmbedding(
normalizedParams.pan.topic.value
);
// Calculate similarities and rank
return this.rankBySimilarity(candidates, queryEmbedding, selectionCriteria);
}
// Otherwise, return candidates filtered by selection criteria
return this.filterCorpuscles(candidates, selectionCriteria);
}
/**
* Select corpuscles using keyword matching
*/
async selectByKeywords(normalizedParams, selectionCriteria) {
const queryConfig = this.filterBuilder.buildQuery(normalizedParams);
const candidates = await this.executeQuery(queryConfig);
// Apply keyword-based scoring
return this.scoreByKeywords(candidates, normalizedParams, selectionCriteria);
}
/**
* Select corpuscles using graph structure
*/
async selectByGraph(normalizedParams, selectionCriteria) {
const queryConfig = this.filterBuilder.buildQuery(normalizedParams);
const candidates = await this.executeQuery(queryConfig);
// Apply graph-based scoring (connectivity, centrality)
return this.scoreByGraph(candidates, normalizedParams, selectionCriteria);
}
/**
* Select corpuscles using temporal ordering
*/
async selectByTemporal(normalizedParams, selectionCriteria) {
const queryConfig = this.filterBuilder.buildQuery(normalizedParams);
queryConfig.query = queryConfig.query.replace(
'ORDER BY ?uri',
'ORDER BY DESC(?created) DESC(?modified)'
);
const candidates = await this.executeQuery(queryConfig);
return this.filterCorpuscles(candidates, selectionCriteria);
}
/**
* Execute SPARQL query against the corpus
*/
async executeQuery(queryConfig) {
if (!this.sparqlStore) {
throw new Error('SPARQLStore required for corpus queries');
}
try {
logger.debug('Executing SPARQL query', {
query: queryConfig.query.substring(0, 200) + '...'
});
const result = await this.sparqlStore._executeSparqlQuery(
queryConfig.query,
this.sparqlStore.endpoint.query
);
return this.parseQueryResults(result, queryConfig);
} catch (error) {
logger.error('SPARQL query execution failed', { error, queryConfig });
throw new Error(`Query execution failed: ${error.message}`);
}
}
/**
* Parse SPARQL query results into corpuscle objects
*/
parseQueryResults(sparqlResult, queryConfig) {
if (!sparqlResult.results || !sparqlResult.results.bindings) {
return [];
}
return sparqlResult.results.bindings.map(binding => {
const corpuscle = {
uri: binding.uri?.value,
type: this.determineCorpuscleType(binding, queryConfig.zoomLevel),
content: this.extractContent(binding),
metadata: this.extractMetadata(binding),
score: 0, // Will be calculated later
binding // Keep original binding for debugging
};
return corpuscle;
});
}
/**
* Determine corpuscle type from SPARQL binding
*/
determineCorpuscleType(binding, zoomLevel) {
if (binding.type?.value) {
const rdfType = binding.type.value;
if (rdfType.includes('Entity')) return 'entity';
if (rdfType.includes('SemanticUnit') || rdfType.includes('Unit')) return 'unit';
if (rdfType.includes('TextElement') || rdfType.includes('Text')) return 'text';
if (rdfType.includes('Community')) return 'community';
if (rdfType.includes('Corpus')) return 'corpus';
if (rdfType === 'interaction') return 'interaction';
}
// Check if it's a semem:Interaction by URI pattern
if (binding.uri?.value && binding.uri.value.includes('/interaction/')) {
return 'interaction';
}
return zoomLevel; // Fallback to zoom level
}
/**
* Extract content from SPARQL binding
*/
extractContent(binding) {
const content = {};
if (binding.label?.value) content.label = binding.label.value;
if (binding.prefLabel?.value) content.prefLabel = binding.prefLabel.value;
if (binding.text?.value) content.text = binding.text.value;
if (binding.content?.value) content.content = binding.content.value;
if (binding.description?.value) content.description = binding.description.value;
return content;
}
/**
* Extract metadata from SPARQL binding
*/
extractMetadata(binding) {
const metadata = {};
if (binding.created?.value) metadata.created = binding.created.value;
if (binding.modified?.value) metadata.modified = binding.modified.value;
if (binding.source?.value) metadata.source = binding.source.value;
if (binding.position?.value) metadata.position = binding.position.value;
if (binding.embedding?.value) {
try {
metadata.embedding = JSON.parse(binding.embedding.value);
} catch (e) {
logger.warn('Failed to parse embedding', { embedding: binding.embedding.value });
}
}
return metadata;
}
/**
* Rank corpuscles by embedding similarity
*/
async rankBySimilarity(corpuscles, queryEmbedding, selectionCriteria) {
const scoredCorpuscles = corpuscles.map(corpuscle => {
let similarity = 0;
if (corpuscle.metadata.embedding) {
similarity = this.calculateCosineSimilarity(
queryEmbedding,
corpuscle.metadata.embedding
);
}
return {
...corpuscle,
score: similarity,
similarity
};
});
// Sort by similarity and apply selection criteria
scoredCorpuscles.sort((a, b) => b.similarity - a.similarity);
return this.filterCorpuscles(scoredCorpuscles, selectionCriteria);
}
/**
* Score corpuscles by keyword relevance
*/
scoreByKeywords(corpuscles, normalizedParams, selectionCriteria) {
const topicValue = normalizedParams.pan.topic?.value;
if (!topicValue) {
return this.filterCorpuscles(corpuscles, selectionCriteria);
}
const keywords = topicValue.toLowerCase().split(/\s+/);
const scoredCorpuscles = corpuscles.map(corpuscle => {
const text = [
corpuscle.content.label,
corpuscle.content.prefLabel,
corpuscle.content.text,
corpuscle.content.content,
corpuscle.content.description
].filter(Boolean).join(' ').toLowerCase();
let score = 0;
keywords.forEach(keyword => {
const matches = (text.match(new RegExp(keyword, 'g')) || []).length;
score += matches;
});
return {
...corpuscle,
score: score / keywords.length,
keywordScore: score
};
});
scoredCorpuscles.sort((a, b) => b.score - a.score);
return this.filterCorpuscles(scoredCorpuscles, selectionCriteria);
}
/**
* Score corpuscles by graph connectivity
*/
scoreByGraph(corpuscles, normalizedParams, selectionCriteria) {
// For now, use a simple connectivity heuristic
// In a full implementation, this would use graph metrics
const scoredCorpuscles = corpuscles.map(corpuscle => {
let connectivityScore = 0;
// Count relationships/connections (simplified)
if (corpuscle.binding.entity) connectivityScore += 1;
if (corpuscle.binding.unit) connectivityScore += 1;
if (corpuscle.binding.members) connectivityScore += 2;
return {
...corpuscle,
score: connectivityScore,
connectivityScore
};
});
scoredCorpuscles.sort((a, b) => b.score - a.score);
return this.filterCorpuscles(scoredCorpuscles, selectionCriteria);
}
/**
* Apply selection criteria to filter corpuscles
*/
filterCorpuscles(corpuscles, selectionCriteria) {
let filtered = [...corpuscles];
// Apply constraints
if (selectionCriteria.constraints) {
const resultLimit = selectionCriteria.constraints.find(c => c.type === 'result_count')?.limit;
if (resultLimit) {
filtered = filtered.slice(0, resultLimit);
}
}
return filtered;
}
/**
* Store navigation data as ZPT RDF metadata
*/
async storeNavigationData(normalizedParams, selectionResult) {
if (!this.config.enableZPTStorage || !this.zptDataFactory) {
return;
}
try {
// Convert parameters to ZPT URIs
const zptParams = this.convertParametersToZPTURIs(normalizedParams);
// Create navigation session
const sessionConfig = {
agentURI: 'http://example.org/agents/corpuscle_selector',
startTime: new Date(),
purpose: `Corpuscle selection using ${zptParams.tiltURI || normalizedParams.tilt.representation} analysis`
};
const session = this.zptDataFactory.createNavigationSession(sessionConfig);
// Create navigation view with selected corpuscles
const viewConfig = {
query: normalizedParams.pan?.topic?.value || 'corpus selection',
zoom: zptParams.zoomURI || this.getDefaultZoomURI(normalizedParams.zoom.level),
tilt: zptParams.tiltURI || this.getDefaultTiltURI(normalizedParams.tilt.representation),
pan: { domains: zptParams.panURIs || [] },
sessionURI: session.uri.value,
selectedCorpuscles: selectionResult.corpuscles.map(c => c.uri).filter(Boolean)
};
const view = this.zptDataFactory.createNavigationView(viewConfig);
// Store in SPARQL if available
if (this.sparqlStore) {
await this.storeZPTDataInSPARQL(session, view);
}
// Add ZPT metadata to selection result
selectionResult.zptMetadata = {
sessionURI: session.uri.value,
viewURI: view.uri.value,
zptParameters: zptParams,
stored: !!this.sparqlStore
};
logger.info('ZPT navigation data stored', {
sessionURI: session.uri.value,
viewURI: view.uri.value,
selectedCorpuscles: viewConfig.selectedCorpuscles.length
});
} catch (error) {
logger.warn('Failed to store ZPT navigation data', { error });
// Don't throw - selection should succeed even if ZPT storage fails
}
}
/**
* Convert normalized parameters to ZPT URIs
*/
convertParametersToZPTURIs(normalizedParams) {
const zptParams = {};
// Convert zoom level
if (normalizedParams.zoom?.level) {
const zoomURI = NamespaceUtils.resolveStringToURI('zoom', normalizedParams.zoom.level);
if (zoomURI) {
zptParams.zoomURI = zoomURI.value;
}
}
// Convert tilt representation
if (normalizedParams.tilt?.representation) {
const tiltURI = NamespaceUtils.resolveStringToURI('tilt', normalizedParams.tilt.representation);
if (tiltURI) {
zptParams.tiltURI = tiltURI.value;
}
}
// Convert pan domains (if available)
if (normalizedParams.pan?.domains) {
zptParams.panURIs = normalizedParams.pan.domains
.map(domain => NamespaceUtils.resolveStringToURI('pan', domain))
.filter(uri => uri !== null)
.map(uri => uri.value);
}
return zptParams;
}
/**
* Get default zoom URI for fallback cases
*/
getDefaultZoomURI(zoomLevel) {
const zoomURI = NamespaceUtils.resolveStringToURI('zoom', zoomLevel);
return zoomURI ? zoomURI.value : 'http://purl.org/stuff/zpt/EntityLevel';
}
/**
* Get default tilt URI for fallback cases
*/
getDefaultTiltURI(tiltRepresentation) {
const tiltURI = NamespaceUtils.resolveStringToURI('tilt', tiltRepresentation);
return tiltURI ? tiltURI.value : 'http://purl.org/stuff/zpt/KeywordProjection';
}
/**
* Store ZPT session and view data in SPARQL
*/
async storeZPTDataInSPARQL(session, view) {
if (!this.sparqlStore) {
return;
}
try {
// Generate SPARQL INSERT for session
const sessionTriples = this.generateTriplesFromQuads(session.quads);
const sessionInsert = getSPARQLPrefixes(['zpt', 'prov']) + `
INSERT DATA {
GRAPH <${this.config.navigationGraph}> {
${sessionTriples}
}
}`;
// Generate SPARQL INSERT for view
const viewTriples = this.generateTriplesFromQuads(view.quads);
const viewInsert = getSPARQLPrefixes(['zpt']) + `
INSERT DATA {
GRAPH <${this.config.navigationGraph}> {
${viewTriples}
}
}`;
// Execute SPARQL updates
await this.executeSPARQLUpdate(sessionInsert, 'Store ZPT navigation session');
await this.executeSPARQLUpdate(viewInsert, 'Store ZPT navigation view');
} catch (error) {
logger.error('Failed to store ZPT data in SPARQL', { error });
throw error;
}
}
/**
* Execute SPARQL UPDATE operation
*/
async executeSPARQLUpdate(sparql, description) {
if (!this.sparqlStore) {
throw new Error('SPARQL store not available');
}
try {
logger.debug(`Executing SPARQL update: ${description}`);
// Use the SPARQL store's update method
if (typeof this.sparqlStore.update === 'function') {
await this.sparqlStore.update(sparql);
} else {
// Fallback to direct endpoint call
const response = await fetch(this.sparqlStore.endpoint.update, {
method: 'POST',
headers: {
'Content-Type': 'application/sparql-update',
'Authorization': this.sparqlStore.auth
},
body: sparql
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
}
logger.debug(`SPARQL update successful: ${description}`);
} catch (error) {
logger.error(`SPARQL update failed: ${description}`, { error });
throw error;
}
}
/**
* Generate SPARQL triples from RDF quads
*/
generateTriplesFromQuads(quads) {
return quads.map(quad => {
const obj = this.formatRDFObject(quad.object);
return ` <${quad.subject.value}> <${quad.predicate.value}> ${obj} .`;
}).join('\n');
}
/**
* Format RDF object for SPARQL
*/
formatRDFObject(object) {
if (object.termType === 'Literal') {
let formatted = `"${object.value.replace(/"/g, '\\"')}"`;
if (object.datatype) {
formatted += `^^<${object.datatype.value}>`;
} else if (object.language) {
formatted += `@${object.language}`;
}
return formatted;
} else {
return `<${object.value}>`;
}
}
/**
* Post-process selected corpuscles
*/
async postProcessCorpuscles(corpuscles, normalizedParams, selectionCriteria) {
// Apply diversity filtering if needed
if (selectionCriteria.scoring.components.some(c => c.name === 'diversity')) {
corpuscles = this.applyDiversityFilter(corpuscles, normalizedParams);
}
// Sort by final score
corpuscles.sort((a, b) => b.score - a.score);
return corpuscles;
}
/**
* Apply diversity filtering to reduce redundancy
*/
applyDiversityFilter(corpuscles, normalizedParams) {
const diversityThreshold = 0.8;
const filtered = [];
for (const corpuscle of corpuscles) {
let isDiverse = true;
for (const existing of filtered) {
if (this.calculateContentSimilarity(corpuscle, existing) > diversityThreshold) {
isDiverse = false;
break;
}
}
if (isDiverse) {
filtered.push(corpuscle);
}
}
return filtered;
}
/**
* Calculate cosine similarity between embeddings
*/
calculateCosineSimilarity(embedding1, embedding2) {
if (!embedding1 || !embedding2 || embedding1.length !== embedding2.length) {
return 0;
}
let dotProduct = 0;
let norm1 = 0;
let norm2 = 0;
for (let i = 0; i < embedding1.length; i++) {
dotProduct += embedding1[i] * embedding2[i];
norm1 += embedding1[i] * embedding1[i];
norm2 += embedding2[i] * embedding2[i];
}
return dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2));
}
/**
* Calculate content similarity between corpuscles
*/
calculateContentSimilarity(corpuscle1, corpuscle2) {
const text1 = Object.values(corpuscle1.content).join(' ').toLowerCase();
const text2 = Object.values(corpuscle2.content).join(' ').toLowerCase();
// Simple Jaccard similarity
const words1 = new Set(text1.split(/\s+/));
const words2 = new Set(text2.split(/\s+/));
const intersection = new Set([...words1].filter(w => words2.has(w)));
const union = new Set([...words1, ...words2]);
return intersection.size / union.size;
}
/**
* Build final selection result object
*/
buildSelectionResult(corpuscles, normalizedParams, selectionCriteria, selectionTime) {
return {
corpuscles,
metadata: {
selectionTime,
parameters: normalizedParams,
criteria: this.criteriaBuilder.getSummary(selectionCriteria),
resultCount: corpuscles.length,
zoomLevel: normalizedParams.zoom.level,
tiltRepresentation: normalizedParams.tilt.representation,
hasFilters: normalizedParams._metadata.hasFilters,
complexity: normalizedParams._metadata.complexity,
timestamp: new Date().toISOString()
},
navigation: {
zoom: normalizedParams.zoom.level,
pan: normalizedParams.pan,
tilt: normalizedParams.tilt.representation
}
};
}
/**
* Cache management methods
*/
getCachedResult(cacheKey) {
if (!this.cache.has(cacheKey)) return null;
const cached = this.cache.get(cacheKey);
if (Date.now() - cached.timestamp > this.cacheExpiry) {
this.cache.delete(cacheKey);
return null;
}
return cached.result;
}
cacheResult(cacheKey, result) {
this.cache.set(cacheKey, {
result: JSON.parse(JSON.stringify(result)), // Deep copy
timestamp: Date.now()
});
// Cleanup old cache entries
if (this.cache.size > 100) {
const oldestKey = this.cache.keys().next().value;
this.cache.delete(oldestKey);
}
}
enrichCachedResult(cachedResult, normalizedParams) {
return {
...cachedResult,
metadata: {
...cachedResult.metadata,
fromCache: true,
parameters: normalizedParams,
timestamp: new Date().toISOString()
}
};
}
/**
* Update performance metrics
*/
updateMetrics(selectionTime) {
this.metrics.avgSelectionTime =
(this.metrics.avgSelectionTime * (this.metrics.totalSelections - 1) + selectionTime) /
this.metrics.totalSelections;
}
/**
* Get selector statistics
*/
getMetrics() {
return {
...this.metrics,
cacheSize: this.cache.size,
cacheHitRate: this.metrics.cacheHits / (this.metrics.cacheHits + this.metrics.cacheMisses)
};
}
/**
* Clear cache and reset metrics
*/
reset() {
this.cache.clear();
this.metrics = {
totalSelections: 0,
avgSelectionTime: 0,
cacheHits: 0,
cacheMisses: 0
};
}
/**
* Dispose of resources
*/
dispose() {
this.cache.clear();
logger.info('CorpuscleSelector disposed');
}
}