Files
boc/quixzoom-capture-pipeline/value-score/calculator.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

503 lines
15 KiB
JavaScript

/**
* QUIXZOOM Data Value Score
*
* Varje observation får ett värde baserat på:
* - Hur mycket ny information den tillför
* - Hur sällsynt datan är
* - Hur mycket den förbättrar modellen
* - Teknisk kvalitet
*
* Ersättning baseras på Data Value Score, inte bara volym.
*/
class DataValueCalculator {
constructor(options = {}) {
// Vikter för olika värdekomponenter
this.weights = {
novelty: options.noveltyWeight || 0.25, // Ny information
rarity: options.rarityWeight || 0.20, // Sällsynthet
verification: options.verificationWeight || 0.20, // Verifiering
coverage: options.coverageWeight || 0.15, // Täckning
quality: options.qualityWeight || 0.15, // Kvalitet
diversity: options.diversityWeight || 0.05, // Diversitet
};
// Historik för att beräkna novelty
this.observationHistory = new Map(); // area -> observations
this.objectHistory = new Map(); // objectType -> count
// Basbelopp per poäng
this.baseReward = options.baseReward || 1.0; // USD per poäng
}
// ============================================================
// HUVUDBERÄKNING
// ============================================================
calculateValue(observation, context = {}) {
const scores = {
novelty: this.calculateNovelty(observation, context),
rarity: this.calculateRarity(observation, context),
verification: this.calculateVerificationValue(observation, context),
coverage: this.calculateCoverageValue(observation, context),
quality: this.calculateQualityValue(observation, context),
diversity: this.calculateDiversityValue(observation, context),
};
// Beräkna viktat medelvärde
let totalValue = 0;
for (const [key, score] of Object.entries(scores)) {
totalValue += score * this.weights[key];
}
// Normalisera till 0-100
const normalizedValue = Math.min(100, Math.max(0, totalValue * 100));
// Beräkna ersättning
const reward = normalizedValue * this.baseReward;
return {
totalValue: normalizedValue,
reward,
currency: 'USD',
breakdown: scores,
weights: this.weights,
factors: this.explainValue(observation, scores, context),
};
}
// ============================================================
// NOVELTY — Hur mycket ny information
// ============================================================
calculateNovelty(observation, context) {
const area = context.area;
const areaObservations = this.observationHistory.get(area) || [];
if (areaObservations.length === 0) {
// Första observationen i området — mycket värdefull
return 1.0;
}
// Jämför med tidigare observationer
let maxSimilarity = 0;
for (const prev of areaObservations.slice(-100)) { // Senaste 100
const similarity = this.calculateSimilarity(observation, prev);
maxSimilarity = Math.max(maxSimilarity, similarity);
}
// Ju mindre lik, desto mer novel
return 1 - maxSimilarity;
}
calculateSimilarity(a, b) {
// Jämför plats
const locationSim = this.locationSimilarity(a.location, b.location);
// Jämför objekt
const objectSim = this.objectSimilarity(a.objects, b.objects);
// Jämför tid
const timeSim = this.timeSimilarity(a.timestamp, b.timestamp);
// Viktat medelvärde
return locationSim * 0.5 + objectSim * 0.3 + timeSim * 0.2;
}
locationSimilarity(a, b) {
if (!a || !b) return 0;
const distance = this.calculateDistance(a, b);
// Exponential decay — 0 similarity at 100m
return Math.exp(-distance / 100);
}
objectSimilarity(a, b) {
if (!a || !b) return 0;
const aTypes = new Set(a.map(o => o.type));
const bTypes = new Set(b.map(o => o.type));
const intersection = new Set([...aTypes].filter(x => bTypes.has(x)));
const union = new Set([...aTypes, ...bTypes]);
return intersection.size / union.size;
}
timeSimilarity(a, b) {
if (!a || !b) return 0;
const diff = Math.abs(a - b) / 1000; // sekunder
// Exponential decay — 0 similarity after 1 hour
return Math.exp(-diff / 3600);
}
// ============================================================
// RARITY — Sällsynta objekttyper
// ============================================================
calculateRarity(observation, context) {
const objects = observation.objects || [];
if (objects.length === 0) return 0.5;
let totalRarity = 0;
for (const obj of objects) {
const count = this.objectHistory.get(obj.type) || 0;
const totalObjects = Array.from(this.objectHistory.values()).reduce((a, b) => a + b, 0);
if (totalObjects === 0) {
totalRarity += 1.0; // Första av denna typ
} else {
// Ju färre, desto mer sällsynt
const frequency = count / totalObjects;
totalRarity += 1 - frequency;
}
}
return totalRarity / objects.length;
}
// ============================================================
// VERIFICATION — Verifiera osäkra observationer
// ============================================================
calculateVerificationValue(observation, context) {
const objects = observation.objects || [];
if (objects.length === 0) return 0.5;
let totalValue = 0;
for (const obj of objects) {
if (obj.needsVerification) {
// Verifiering av osäker observation — högt värde
totalValue += 1.0;
} else if (obj.verificationStatus === 'verified') {
// Redan verifierad — lägre värde
totalValue += 0.3;
} else {
// Ingen verifieringsstatus — medelvärde
totalValue += 0.6;
}
}
return totalValue / objects.length;
}
// ============================================================
// COVERAGE — Förbättra spatial täckning
// ============================================================
calculateCoverageValue(observation, context) {
const area = context.area;
const areaObservations = this.observationHistory.get(area) || [];
if (areaObservations.length === 0) {
return 1.0; // Första observationen
}
// Beräkna täckning före och efter
const coverageBefore = this.calculateCoverage(areaObservations);
const newObservations = [...areaObservations, observation];
const coverageAfter = this.calculateCoverage(newObservations);
// Värdet är ökningen i täckning
return Math.min(1.0, (coverageAfter - coverageBefore) * 10);
}
calculateCoverage(observations) {
const uniqueLocations = new Set();
for (const obs of observations) {
if (obs.location) {
const key = `${obs.location.lat.toFixed(4)},${obs.location.lng.toFixed(4)}`;
uniqueLocations.add(key);
}
}
// Förenklad: anta att 1000 unika platser är 100%
return Math.min(1.0, uniqueLocations.size / 1000);
}
// ============================================================
// QUALITY — Teknisk kvalitet
// ============================================================
calculateQualityValue(observation, context) {
const quality = observation.quality || {};
let score = 0;
// Upplösning
score += (quality.resolution || 0.5) * 0.2;
// Skärpa
score += (quality.sharpness || 0.5) * 0.2;
// Exponering
score += (quality.exposure || 0.5) * 0.15;
// Stabilisering
score += (quality.stabilization ? 1.0 : 0.0) * 0.15;
// GPS-accuracy
score += Math.max(0, 1 - (quality.gpsAccuracy || 50) / 50) * 0.15;
// Komplett metadata
const metadataScore = this.calculateMetadataScore(observation);
score += metadataScore * 0.15;
return score;
}
calculateMetadataScore(observation) {
const required = ['timestamp', 'location', 'source'];
const optional = ['device', 'camera', 'weather', 'operator'];
let score = 0;
for (const field of required) {
if (observation[field]) score += 0.15;
}
for (const field of optional) {
if (observation[field]) score += 0.05;
}
return Math.min(1.0, score);
}
// ============================================================
// DIVERSITY — Ny vinkel, tid, etc.
// ============================================================
calculateDiversityValue(observation, context) {
const area = context.area;
const areaObservations = this.observationHistory.get(area) || [];
if (areaObservations.length === 0) {
return 1.0;
}
// Kontrollera om detta är en ny vinkel
const hasNewAngle = this.hasNewAngle(observation, areaObservations);
// Kontrollera om detta är en ny tid
const hasNewTime = this.hasNewTime(observation, areaObservations);
// Kontrollera om detta är nytt väder
const hasNewWeather = this.hasNewWeather(observation, areaObservations);
return (hasNewAngle ? 0.4 : 0) + (hasNewTime ? 0.35 : 0) + (hasNewWeather ? 0.25 : 0);
}
hasNewAngle(observation, previous) {
// Jämför kompassriktning
if (!observation.compass) return false;
for (const prev of previous.slice(-10)) {
if (prev.compass) {
const diff = Math.abs(observation.compass - prev.compass);
if (diff < 30) return false; // Liknande vinkel
}
}
return true;
}
hasNewTime(observation, previous) {
if (!observation.timestamp) return false;
const hour = new Date(observation.timestamp).getHours();
for (const prev of previous) {
if (prev.timestamp) {
const prevHour = new Date(prev.timestamp).getHours();
if (Math.abs(hour - prevHour) < 3) return false; // Liknande tid
}
}
return true;
}
hasNewWeather(observation, previous) {
if (!observation.weather) return false;
for (const prev of previous.slice(-5)) {
if (prev.weather === observation.weather) return false;
}
return true;
}
// ============================================================
// FÖRKLARING
// ============================================================
explainValue(observation, scores, context) {
const factors = [];
if (scores.novelty > 0.7) {
factors.push('Första observationen av detta område/objekt');
} else if (scores.novelty > 0.4) {
factors.push('Ny information jämfört med tidigare observationer');
}
if (scores.rarity > 0.7) {
factors.push('Sällsynt objekttyp');
}
if (scores.verification > 0.7) {
factors.push('Verifierar osäker tidigare observation');
}
if (scores.coverage > 0.5) {
factors.push('Förbättrar spatial täckning');
}
if (scores.quality > 0.8) {
factors.push('Mycket hög teknisk kvalitet');
}
if (scores.diversity > 0.5) {
factors.push('Ny vinkel, tid eller väderförhållande');
}
return factors;
}
// ============================================================
// HISTORIK
// ============================================================
addToHistory(observation, context) {
const area = context.area;
// Lägg till i area-historik
if (!this.observationHistory.has(area)) {
this.observationHistory.set(area, []);
}
this.observationHistory.get(area).push(observation);
// Uppdatera objekt-historik
for (const obj of observation.objects || []) {
const count = this.objectHistory.get(obj.type) || 0;
this.objectHistory.set(obj.type, count + 1);
}
}
// ============================================================
// HJÄLPMETODER
// ============================================================
calculateDistance(a, b) {
const R = 6371e3;
const φ1 = a.lat * Math.PI / 180;
const φ2 = b.lat * Math.PI / 180;
const Δφ = (b.lat - a.lat) * Math.PI / 180;
const Δλ = (b.lng - a.lng) * Math.PI / 180;
const x = Math.sin(Δφ/2) * Math.sin(Δφ/2) +
Math.cos(φ1) * Math.cos(φ2) *
Math.sin(Δλ/2) * Math.sin(Δλ/2);
const c = 2 * Math.atan2(Math.sqrt(x), Math.sqrt(1-x));
return R * c;
}
// ============================================================
// EXEMPEL
// ============================================================
static examples() {
const calculator = new DataValueCalculator({ baseReward: 0.1 });
console.log('=== DATA VALUE SCORE EXAMPLES ===\n');
// Exempel 1: Första observationen i ett område
const obs1 = {
location: { lat: 13.75, lng: 100.50 },
timestamp: Date.now(),
objects: [{ type: 'streetlight', condition: 'broken' }],
quality: { resolution: 1.0, sharpness: 0.9, stabilization: true },
compass: 90,
weather: 'sunny',
};
const result1 = calculator.calculateValue(obs1, { area: 'bangkok_sukhumvit' });
calculator.addToHistory(obs1, { area: 'bangkok_sukhumvit' });
console.log('Observation 1: Första i området');
console.log(` Total Value: ${result1.totalValue.toFixed(1)}/100`);
console.log(` Reward: $${result1.reward.toFixed(2)}`);
console.log(` Factors: ${result1.factors.join(', ')}`);
console.log(` Breakdown:`, result1.breakdown);
console.log();
// Exempel 2: Liknande observation (lågt värde)
const obs2 = {
location: { lat: 13.7501, lng: 100.5001 },
timestamp: Date.now() + 1000,
objects: [{ type: 'streetlight', condition: 'broken' }],
quality: { resolution: 1.0, sharpness: 0.9, stabilization: true },
compass: 95,
weather: 'sunny',
};
const result2 = calculator.calculateValue(obs2, { area: 'bangkok_sukhumvit' });
console.log('Observation 2: Liknande plats och objekt');
console.log(` Total Value: ${result2.totalValue.toFixed(1)}/100`);
console.log(` Reward: $${result2.reward.toFixed(2)}`);
console.log(` Factors: ${result2.factors.join(', ') || 'Inga särskilda faktorer'}`);
console.log();
// Exempel 3: Verifiering av osäker observation
const obs3 = {
location: { lat: 13.76, lng: 100.51 },
timestamp: Date.now() + 2000,
objects: [{ type: 'pothole', needsVerification: true }],
quality: { resolution: 0.8, sharpness: 0.7, stabilization: false },
compass: 180,
weather: 'cloudy',
};
const result3 = calculator.calculateValue(obs3, { area: 'bangkok_sukhumvit' });
calculator.addToHistory(obs3, { area: 'bangkok_sukhumvit' });
console.log('Observation 3: Verifiering av osäker observation');
console.log(` Total Value: ${result3.totalValue.toFixed(1)}/100`);
console.log(` Reward: $${result3.reward.toFixed(2)}`);
console.log(` Factors: ${result3.factors.join(', ')}`);
console.log();
// Exempel 4: Sällsynt objekt
const obs4 = {
location: { lat: 13.77, lng: 100.52 },
timestamp: Date.now() + 3000,
objects: [{ type: 'historic_marker', condition: 'good' }],
quality: { resolution: 1.0, sharpness: 0.95, stabilization: true },
compass: 270,
weather: 'sunny',
};
const result4 = calculator.calculateValue(obs4, { area: 'bangkok_sukhumvit' });
calculator.addToHistory(obs4, { area: 'bangkok_sukhumvit' });
console.log('Observation 4: Sällsynt objekt (historic_marker)');
console.log(` Total Value: ${result4.totalValue.toFixed(1)}/100`);
console.log(` Reward: $${result4.reward.toFixed(2)}`);
console.log(` Factors: ${result4.factors.join(', ')}`);
}
}
// Exportera
module.exports = DataValueCalculator;
// Kör exempel
if (require.main === module) {
DataValueCalculator.examples();
}