bae705aa97
- Add NFC ePassport roadmap (ICAO 9303, eIDAS) - Add TensorFlow.js edge face detection (BlazeFace) - Add structured audit logger (GDPR-compliant) - Risk scoring support Part of KYC Apple Native UX v1.1.0
345 lines
12 KiB
JavaScript
345 lines
12 KiB
JavaScript
/**
|
|
* 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;
|