/** * Bangkok Field Test Protocol * * UIOS v1.1 Real World Validation * Goal: 1,000+ verified observations from 3 Zoomers */ const fs = require('fs'); const path = require('path'); class BangkokFieldTest { constructor() { this.testId = `bangkok_test_${Date.now()}`; this.startDate = new Date().toISOString(); // Test configuration this.config = { zoomers: 3, objectsPerZoomer: 50, observationsPerObject: 3, totalTargetObservations: 1000, // Object types to test objectTypes: [ 'street_lamp', 'traffic_sign', 'manhole', 'electrical_cabinet', 'tree', 'bench', ], // GPS requirements gpsAccuracy: { target: 5, // meters minimum: 10, // meters }, // Quality requirements quality: { minResolution: [1920, 1080], minOverall: 0.7, }, // Time coverage timeOfDay: ['day', 'evening', 'night'], // Test areas (Bangkok districts) areas: [ { name: 'Sukhumvit', lat: 13.7373, lng: 100.5607, radius: 2000 }, { name: 'Silom', lat: 13.7246, lng: 100.5292, radius: 1500 }, { name: 'Siam', lat: 13.7466, lng: 100.5393, radius: 1000 }, ], }; // Results this.results = { observations: [], objects: new Map(), zoomers: new Map(), metrics: { totalObservations: 0, totalObjects: 0, avgObservationsPerObject: 0, gpsAccuracyDistribution: [], qualityDistribution: [], timeOfDayDistribution: { day: 0, evening: 0, night: 0 }, }, }; } /** * ============================================================ * TEST PROTOCOL * ============================================================ */ async runTest() { console.log('╔════════════════════════════════════════════════════════════╗'); console.log('║ BANGKOK FIELD TEST PROTOCOL v1.1 ║'); console.log('╚════════════════════════════════════════════════════════════╝\n'); console.log(`Test ID: ${this.testId}`); console.log(`Start: ${this.startDate}`); console.log(`Target: ${this.config.totalTargetObservations} observations\n`); // Phase 1: Setup await this.phase1_setup(); // Phase 2: Data Collection await this.phase2_collection(); // Phase 3: Validation await this.phase3_validation(); // Phase 4: Analysis await this.phase4_analysis(); // Phase 5: Report return this.generateReport(); } async phase1_setup() { console.log('=== PHASE 1: SETUP ===\n'); // Create Zoomer profiles for (let i = 1; i <= this.config.zoomers; i++) { const zoomer = { id: `zoomer_${i}`, name: `Bangkok Zoomer ${i}`, level: 'active', device: 'iPhone 14 Pro', assignedArea: this.config.areas[i - 1], }; this.results.zoomers.set(zoomer.id, zoomer); console.log(`Created Zoomer: ${zoomer.name} → ${zoomer.assignedArea.name}`); } // Generate test objects (known ground truth) for (const area of this.config.areas) { for (let i = 0; i < this.config.objectsPerZoomer; i++) { const object = this.generateTestObject(area, i); this.results.objects.set(object.id, object); } } console.log(`\nCreated ${this.results.objects.size} test objects`); console.log(`Object types: ${this.config.objectTypes.join(', ')}\n`); } generateTestObject(area, index) { const type = this.config.objectTypes[index % this.config.objectTypes.length]; // Random location within area const lat = area.lat + (Math.random() - 0.5) * (area.radius / 111000); const lng = area.lng + (Math.random() - 0.5) * (area.radius / (111000 * Math.cos(area.lat * Math.PI / 180))); return { id: `obj_${area.name.toLowerCase()}_${index}`, type, location: { lat, lng }, area: area.name, groundTruth: true, attributes: this.generateAttributes(type), }; } generateAttributes(type) { const attributes = { street_lamp: { height: 8 + Math.random() * 4, material: 'steel', light_status: Math.random() > 0.1 ? 'on' : 'off' }, traffic_sign: { sign_type: ['stop', 'speed', 'direction', 'warning'][Math.floor(Math.random() * 4)], height: 2.5 + Math.random() * 1.5 }, manhole: { diameter: 0.6 + Math.random() * 0.4, material: 'cast_iron' }, electrical_cabinet: { type: 'distribution', height: 1.5 + Math.random() * 0.5 }, tree: { species: ['oak', 'palm', 'maple'][Math.floor(Math.random() * 3)], height: 5 + Math.random() * 15 }, bench: { material: ['wood', 'metal', 'concrete'][Math.floor(Math.random() * 3)], condition: 'good' }, }; return attributes[type] || {}; } async phase2_collection() { console.log('=== PHASE 2: DATA COLLECTION ===\n'); // Simulate observations from each Zoomer for (const zoomer of this.results.zoomers.values()) { console.log(`Collecting from ${zoomer.name}...`); const areaObjects = Array.from(this.results.objects.values()) .filter(obj => obj.area === zoomer.assignedArea.name); for (const object of areaObjects) { // Each Zoomer captures each object multiple times for (let obs = 0; obs < this.config.observationsPerObject; obs++) { const observation = this.simulateObservation(zoomer, object, obs); this.results.observations.push(observation); // Update distributions this.results.metrics.timeOfDayDistribution[observation.context.timeOfDay]++; this.results.metrics.gpsAccuracyDistribution.push(observation.location.accuracy); this.results.metrics.qualityDistribution.push(observation.quality.overall); } } console.log(` ${areaObjects.length * this.config.observationsPerObject} observations`); } this.results.metrics.totalObservations = this.results.observations.length; console.log(`\nTotal observations: ${this.results.metrics.totalObservations}\n`); } simulateObservation(zoomer, object, obsIndex) { // Simulate GPS accuracy (varies by device and conditions) const baseAccuracy = 3 + Math.random() * 7; // 3-10 meters const accuracy = obsIndex === 0 ? baseAccuracy : baseAccuracy * (0.8 + Math.random() * 0.4); // Simulate location (slightly offset from true location) const lat = object.location.lat + (Math.random() - 0.5) * (accuracy / 111000); const lng = object.location.lng + (Math.random() - 0.5) * (accuracy / (111000 * Math.cos(object.location.lat * Math.PI / 180))); // Simulate quality const quality = { blur: Math.random() * 0.3, exposure: 0.7 + Math.random() * 0.3, noise: Math.random() * 0.2, overall: 0, }; quality.overall = (1 - quality.blur) * quality.exposure * (1 - quality.noise); return { id: `obs_${zoomer.id}_${object.id}_${obsIndex}`, objectId: object.id, zoomerId: zoomer.id, timestamp: new Date().toISOString(), location: { lat, lng, accuracy, }, objectType: object.type, context: { timeOfDay: this.config.timeOfDay[Math.floor(Math.random() * this.config.timeOfDay.length)], weather: ['sunny', 'cloudy', 'rainy'][Math.floor(Math.random() * 3)], }, quality, media: { type: 'image', resolution: [1920, 1080], format: 'jpg', }, }; } async phase3_validation() { console.log('=== PHASE 3: VALIDATION ===\n'); // Check GPS accuracy const gpsPassRate = this.results.observations.filter( obs => obs.location.accuracy <= this.config.gpsAccuracy.target ).length / this.results.observations.length; console.log(`GPS accuracy ≤ ${this.config.gpsAccuracy.target}m: ${(gpsPassRate * 100).toFixed(1)}%`); console.log(`Target: 90%`); console.log(`Status: ${gpsPassRate >= 0.9 ? 'PASS' : 'FAIL'}\n`); // Check quality const qualityPassRate = this.results.observations.filter( obs => obs.quality.overall >= this.config.quality.minOverall ).length / this.results.observations.length; console.log(`Quality ≥ ${this.config.quality.minOverall}: ${(qualityPassRate * 100).toFixed(1)}%`); console.log(`Target: 95%`); console.log(`Status: ${qualityPassRate >= 0.95 ? 'PASS' : 'FAIL'}\n`); // Check time-of-day coverage const timeDistribution = this.results.metrics.timeOfDayDistribution; const total = Object.values(timeDistribution).reduce((a, b) => a + b, 0); console.log('Time-of-day coverage:'); for (const [time, count] of Object.entries(timeDistribution)) { console.log(` ${time}: ${count} (${((count / total) * 100).toFixed(1)}%)`); } console.log(); // Check object coverage const objectTypeCounts = {}; for (const obs of this.results.observations) { objectTypeCounts[obs.objectType] = (objectTypeCounts[obs.objectType] || 0) + 1; } console.log('Object type coverage:'); for (const [type, count] of Object.entries(objectTypeCounts)) { console.log(` ${type}: ${count} observations`); } console.log(); } async phase4_analysis() { console.log('=== PHASE 4: ANALYSIS ===\n'); // Calculate metrics const uniqueObjects = new Set(this.results.observations.map(obs => obs.objectId)); this.results.metrics.totalObjects = uniqueObjects.size; this.results.metrics.avgObservationsPerObject = this.results.metrics.totalObservations / uniqueObjects.size; // GPS statistics const gpsAccuracies = this.results.metrics.gpsAccuracyDistribution; const avgGps = gpsAccuracies.reduce((a, b) => a + b, 0) / gpsAccuracies.length; const p50Gps = gpsAccuracies.sort((a, b) => a - b)[Math.floor(gpsAccuracies.length * 0.5)]; const p95Gps = gpsAccuracies.sort((a, b) => a - b)[Math.floor(gpsAccuracies.length * 0.95)]; // Quality statistics const qualities = this.results.metrics.qualityDistribution; const avgQuality = qualities.reduce((a, b) => a + b, 0) / qualities.length; console.log('=== METRICS ==='); console.log(`Total observations: ${this.results.metrics.totalObservations}`); console.log(`Total objects: ${this.results.metrics.totalObjects}`); console.log(`Avg observations/object: ${this.results.metrics.avgObservationsPerObject.toFixed(1)}`); console.log(); console.log('GPS Accuracy:'); console.log(` Average: ${avgGps.toFixed(1)}m`); console.log(` P50: ${p50Gps.toFixed(1)}m`); console.log(` P95: ${p95Gps.toFixed(1)}m`); console.log(); console.log('Quality:'); console.log(` Average: ${(avgQuality * 100).toFixed(1)}%`); console.log(); } generateReport() { const report = { testId: this.testId, version: '1.1.0', startDate: this.startDate, endDate: new Date().toISOString(), config: this.config, results: { totalObservations: this.results.metrics.totalObservations, totalObjects: this.results.metrics.totalObjects, avgObservationsPerObject: this.results.metrics.avgObservationsPerObject, timeOfDayDistribution: this.results.metrics.timeOfDayDistribution, }, status: this.results.metrics.totalObservations >= this.config.totalTargetObservations ? 'PASS' : 'FAIL', }; // Save report const reportPath = path.join(__dirname, `bangkok-field-test-${this.testId}.json`); fs.writeFileSync(reportPath, JSON.stringify(report, null, 2)); console.log('=== TEST COMPLETE ==='); console.log(`Status: ${report.status}`); console.log(`Report saved: ${reportPath}`); return report; } } // Run test if (require.main === module) { const test = new BangkokFieldTest(); test.runTest().catch(console.error); } module.exports = BangkokFieldTest;