/** * QUIXZOOM Urban Foundation Dataset Manager * * "Git" för hela AI-datasetet. * Håller reda på: * - alla städer (Reference Cities) * - alla objekt * - alla etiketter * - alla annoteringar * - alla modellversioner * - alla träningsdataset * - licensstatus * - datakvalitet * - täckningsgrad * - versionshistorik */ const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); class UrbanFoundationDatasetManager { constructor(config = {}) { this.config = { dataDir: config.dataDir || './dataset-store', ...config, }; // Huvudstrukturer this.cities = new Map(); // Reference Cities this.objects = new Map(); // Alla objekt this.annotations = new Map(); // Alla annoteringar this.datasets = new Map(); // Träningsdataset this.models = new Map(); // Modellversioner this.versions = new Map(); // Versionshistorik // Coverage tracking this.coverage = new Map(); this.init(); } init() { if (!fs.existsSync(this.config.dataDir)) { fs.mkdirSync(this.config.dataDir, { recursive: true }); } this.loadState(); } /** * ============================================================ * REFERENCE CITY * ============================================================ */ addReferenceCity(cityData) { const city = { id: cityData.id || `city_${crypto.randomBytes(4).toString('hex')}`, name: cityData.name, country: cityData.country, region: cityData.region, climate: cityData.climate, type: cityData.type, // tropical, mediterranean, nordic, etc. location: cityData.location, // { lat, lng } timezone: cityData.timezone, // Metadata addedAt: new Date().toISOString(), status: 'active', // Statistik stats: { totalObservations: 0, totalObjects: 0, totalAnnotations: 0, lastUpdated: null, }, // Coverage coverage: {}, }; this.cities.set(city.id, city); this.saveState(); console.log(`[DATASET] Added Reference City: ${city.name} (${city.country})`); return city; } getReferenceCity(cityId) { return this.cities.get(cityId); } listReferenceCities() { return Array.from(this.cities.values()); } /** * ============================================================ * COVERAGE SCORE * ============================================================ */ updateCoverage(cityId, category, conditions = {}) { const city = this.cities.get(cityId); if (!city) return null; const coverage = { category: category, total: conditions.total || 0, found: conditions.found || 0, percentage: conditions.total > 0 ? (conditions.found / conditions.total * 100).toFixed(1) : 0, // Breakdown byTimeOfDay: conditions.byTimeOfDay || {}, byWeather: conditions.byWeather || {}, bySeason: conditions.bySeason || {}, lastUpdated: new Date().toISOString(), }; if (!city.coverage[category]) { city.coverage[category] = {}; } city.coverage[category] = coverage; this.saveState(); return coverage; } getCoverageReport(cityId) { const city = this.cities.get(cityId); if (!city) return null; const report = { city: city.name, overall: 0, categories: {}, gaps: [], recommendations: [], }; let totalPercentage = 0; let categoryCount = 0; for (const [category, coverage] of Object.entries(city.coverage)) { report.categories[category] = coverage; totalPercentage += parseFloat(coverage.percentage); categoryCount++; // Identifiera gap if (parseFloat(coverage.percentage) < 50) { report.gaps.push({ category, coverage: coverage.percentage, severity: 'critical', }); } else if (parseFloat(coverage.percentage) < 80) { report.gaps.push({ category, coverage: coverage.percentage, severity: 'warning', }); } // Generera rekommendationer if (coverage.byTimeOfDay) { const nightCoverage = coverage.byTimeOfDay.night || 0; if (nightCoverage < 20) { report.recommendations.push({ category, action: 'collect_night', message: `${category}: Only ${nightCoverage}% night coverage. Collect more night observations.`, }); } } } report.overall = categoryCount > 0 ? (totalPercentage / categoryCount).toFixed(1) : 0; return report; } /** * ============================================================ * DATA VALUE SCORE * ============================================================ */ calculateDataValueScore(observation) { // Information Gain: Hur mycket ny information tillför observationen? const informationGain = this.calculateInformationGain(observation); // Coverage Gap: Hur mycket behövs denna kategori? const coverageGap = this.calculateCoverageGap(observation); // Image Quality: Teknisk kvalitet const imageQuality = observation.quality?.score || 0.5; // Novelty: Hur unik är observationen? const novelty = this.calculateNovelty(observation); // Verification Need: Behöver den verifieras? const verificationNeed = observation.verified ? 0.3 : 1.0; // Data Value Score = produkt av alla faktorer const score = informationGain * coverageGap * imageQuality * novelty * verificationNeed; return { total: Math.round(score * 100), breakdown: { informationGain: Math.round(informationGain * 100), coverageGap: Math.round(coverageGap * 100), imageQuality: Math.round(imageQuality * 100), novelty: Math.round(novelty * 100), verificationNeed: Math.round(verificationNeed * 100), }, }; } calculateInformationGain(observation) { // Om objektet redan har många observationer → lägre gain const objectId = observation.objectId; if (!objectId) return 1.0; const obj = this.objects.get(objectId); if (!obj) return 1.0; const observationCount = obj.observations?.length || 0; // Diminishing returns: 1 obs = 1.0, 10 obs = 0.5, 100 obs = 0.1 return Math.max(0.1, 1 / Math.sqrt(observationCount + 1)); } calculateCoverageGap(observation) { // Om kategorin är underrepresenterad → högre gap const cityId = observation.cityId; const category = observation.objectType; if (!cityId || !category) return 1.0; const city = this.cities.get(cityId); if (!city || !city.coverage[category]) return 1.0; const coverage = parseFloat(city.coverage[category].percentage); // 0% coverage = 1.0, 100% coverage = 0.1 return Math.max(0.1, 1 - (coverage / 100)); } calculateNovelty(observation) { // Hur lik är denna observation tidigare? // Simplified: baserat på tid sedan senaste observation av samma typ const cityId = observation.cityId; const category = observation.objectType; if (!cityId || !category) return 1.0; const recentObs = this.getRecentObservations(cityId, category, 24); // 24h if (recentObs.length === 0) return 1.0; // Ju fler nyligen, desto lägre novelty return Math.max(0.1, 1 / Math.sqrt(recentObs.length + 1)); } getRecentObservations(cityId, category, hours) { const cutoff = new Date(Date.now() - hours * 3600000); const recent = []; for (const obs of this.objects.values()) { if (obs.cityId === cityId && obs.type === category) { const obsTime = new Date(obs.timestamp); if (obsTime > cutoff) { recent.push(obs); } } } return recent; } /** * ============================================================ * DATASET VERSIONING * ============================================================ */ createDatasetVersion(name, config = {}) { const version = { id: `dataset_${crypto.randomBytes(4).toString('hex')}`, name: name, createdAt: new Date().toISOString(), // Konfiguration cities: config.cities || [], categories: config.categories || [], conditions: config.conditions || {}, // Innehåll observations: [], annotations: [], // Metadata stats: { totalObservations: 0, totalAnnotations: 0, verifiedAnnotations: 0, avgQuality: 0, }, // Spårbarhet parentVersion: config.parentVersion || null, modelVersion: config.modelVersion || null, // Status status: 'draft', }; this.datasets.set(version.id, version); this.saveState(); console.log(`[DATASET] Created dataset version: ${name} (${version.id})`); return version; } addToDataset(datasetId, observationIds, annotationIds) { const dataset = this.datasets.get(datasetId); if (!dataset) return null; dataset.observations.push(...observationIds); dataset.annotations.push(...annotationIds); dataset.stats.totalObservations = dataset.observations.length; dataset.stats.totalAnnotations = dataset.annotations.length; this.saveState(); return dataset; } commitDataset(datasetId, message) { const dataset = this.datasets.get(datasetId); if (!dataset) return null; dataset.status = 'committed'; dataset.committedAt = new Date().toISOString(); dataset.commitMessage = message; // Skapa snapshot const snapshot = { id: `snapshot_${crypto.randomBytes(4).toString('hex')}`, datasetId: datasetId, timestamp: dataset.committedAt, observations: [...dataset.observations], annotations: [...dataset.annotations], stats: { ...dataset.stats }, }; this.versions.set(snapshot.id, snapshot); this.saveState(); console.log(`[DATASET] Committed: ${message} (${snapshot.id})`); return snapshot; } /** * ============================================================ * MODEL TRACKING * ============================================================ */ registerModel(modelData) { const model = { id: `model_${crypto.randomBytes(4).toString('hex')}`, name: modelData.name, version: modelData.version, type: modelData.type, // yolo, grounding_dino, sam2, etc. // Träningsinfo datasetId: modelData.datasetId, trainingConfig: modelData.trainingConfig, // Prestanda metrics: modelData.metrics || {}, // Status status: 'trained', createdAt: new Date().toISOString(), }; this.models.set(model.id, model); this.saveState(); console.log(`[DATASET] Registered model: ${model.name} v${model.version}`); return model; } getModelLineage(modelId) { const model = this.models.get(modelId); if (!model) return null; const lineage = [model]; // Hitta föräldradataset let datasetId = model.datasetId; while (datasetId) { const dataset = this.datasets.get(datasetId); if (!dataset) break; lineage.push({ type: 'dataset', id: dataset.id, name: dataset.name, }); datasetId = dataset.parentVersion; } return lineage; } /** * ============================================================ * PERSISTENCE * ============================================================ */ saveState() { const state = { cities: Array.from(this.cities.entries()), objects: Array.from(this.objects.entries()), annotations: Array.from(this.annotations.entries()), datasets: Array.from(this.datasets.entries()), models: Array.from(this.models.entries()), versions: Array.from(this.versions.entries()), }; const statePath = path.join(this.config.dataDir, 'dataset-state.json'); fs.writeFileSync(statePath, JSON.stringify(state, null, 2)); } loadState() { const statePath = path.join(this.config.dataDir, 'dataset-state.json'); if (!fs.existsSync(statePath)) return; try { const state = JSON.parse(fs.readFileSync(statePath)); if (state.cities) this.cities = new Map(state.cities); if (state.objects) this.objects = new Map(state.objects); if (state.annotations) this.annotations = new Map(state.annotations); if (state.datasets) this.datasets = new Map(state.datasets); if (state.models) this.models = new Map(state.models); if (state.versions) this.versions = new Map(state.versions); } catch (error) { console.warn('[DATASET] Failed to load state:', error.message); } } /** * ============================================================ * RAPPORTER * ============================================================ */ generateReport() { const report = { generatedAt: new Date().toISOString(), summary: { totalCities: this.cities.size, totalObjects: this.objects.size, totalAnnotations: this.annotations.size, totalDatasets: this.datasets.size, totalModels: this.models.size, }, cities: [], datasets: [], models: [], }; for (const city of this.cities.values()) { const cityReport = this.getCoverageReport(city.id); report.cities.push(cityReport); } for (const dataset of this.datasets.values()) { report.datasets.push({ id: dataset.id, name: dataset.name, status: dataset.status, stats: dataset.stats, }); } for (const model of this.models.values()) { report.models.push({ id: model.id, name: model.name, version: model.version, metrics: model.metrics, }); } return report; } } module.exports = UrbanFoundationDatasetManager; // Demo if (require.main === module) { const manager = new UrbanFoundationDatasetManager(); // Lägg till Reference Cities const bangkok = manager.addReferenceCity({ name: 'Bangkok', country: 'TH', region: 'Southeast Asia', climate: 'tropical', type: 'tropical_megacity', location: { lat: 13.7563, lng: 100.5018 }, timezone: 'Asia/Bangkok', }); const torrevieja = manager.addReferenceCity({ name: 'Torrevieja', country: 'ES', region: 'Mediterranean', climate: 'mediterranean', type: 'mediterranean_coastal', location: { lat: 37.9838, lng: -0.6828 }, timezone: 'Europe/Madrid', }); // Uppdatera coverage manager.updateCoverage(bangkok.id, 'street_lamp', { total: 1000, found: 940, byTimeOfDay: { day: 500, evening: 300, night: 140 }, byWeather: { clear: 600, cloudy: 250, rain: 90 }, }); manager.updateCoverage(bangkok.id, 'traffic_sign', { total: 500, found: 455, byTimeOfDay: { day: 300, evening: 120, night: 35 }, }); // Generera coverage report const report = manager.getCoverageReport(bangkok.id); console.log('\n=== BANGKOK COVERAGE REPORT ==='); console.log(JSON.stringify(report, null, 2)); // Generera full rapport const fullReport = manager.generateReport(); console.log('\n=== FULL REPORT ==='); console.log(JSON.stringify(fullReport.summary, null, 2)); }