Files
boc/quixzoom-capture-pipeline/test-data/bangkok-field-test.js
T
Bernt bae705aa97 ARCHITECTURE: NFC roadmap, edge AI, audit logging
- 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
2026-06-29 16:24:48 +00:00

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;