File size: 5,719 Bytes
93c7565 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 | // test/simulate-classification.js
// Simulates ROC curve for trigger score threshold calibration.
//
// Runs Monte Carlo simulations to estimate TPR and FPR across thresholds.
// Assumes Beta-Binomial model with α0=1, β0=10.
//
// Simulation parameters:
// - N = 10,000 poll cycles
// - P(H_t=1) = 0.01 (prior belief in tampering)
// - When H_t=1, θ = 0.5 (50% chance per gateway to detect mismatch)
// - When H_t=0, θ = 0 (no false mismatches)
// - r_t = 5 (fixed for simplicity, representing 5 responding gateways)
//
// Outputs ROC data to console and generates demo/roc-report.md
const fs = require('fs');
const path = require('path');
const { ALPHA0, BETA0 } = require('../prover/scorer');
const N_SIMULATIONS = 10000;
const P_DIVERGENCE = 0.01; // P(H_t=1)
const THETA_DIVERGENCE = 0.5; // Probability of detecting mismatch when H_t=1
const THETA_VALID = 0; // No mismatches when valid
const R_T = 5; // Responding gateways
function binomialSample(n, p) {
let successes = 0;
for (let i = 0; i < n; i++) {
if (Math.random() < p) successes++;
}
return successes;
}
function simulatePollCycle(scenario = null) {
const H_t = Math.random() < P_DIVERGENCE ? 1 : 0;
let theta = H_t === 1 ? THETA_DIVERGENCE : THETA_VALID;
// If scenario specified, force the mismatch count
let m_t;
if (scenario === 'A') {
// Class A: exactly 1 mismatch
m_t = 1;
} else if (scenario === 'B') {
// Class B: at least 2 mismatches
m_t = Math.random() < 0.5 ? 2 : 3; // 2 or 3 out of 5
} else {
// General simulation
m_t = binomialSample(R_T, theta);
}
const tau_t = (ALPHA0 + m_t) / (ALPHA0 + BETA0 + R_T);
return { H_t, tau_t };
}
function computeROC(thresholds, scenario = null) {
const roc = thresholds.map(tau => {
let TP = 0, FP = 0, TN = 0, FN = 0;
for (let i = 0; i < N_SIMULATIONS; i++) {
const { H_t, tau_t } = simulatePollCycle(scenario);
const decision = tau_t >= tau ? 1 : 0; // Trip if >= tau
if (H_t === 1 && decision === 1) TP++;
else if (H_t === 0 && decision === 1) FP++;
else if (H_t === 0 && decision === 0) TN++;
else if (H_t === 1 && decision === 0) FN++;
}
const TPR = TP / (TP + FN) || 0;
const FPR = FP / (FP + TN) || 0;
return { tau, TPR, FPR, TP, FP, TN, FN };
});
return roc;
}
function findOptimalThreshold(roc, costRatio = 10, pValid = 0.99, pInvalid = 0.01) {
// Slope dTPR/dFPR = (c1/c2) * (P(H=0)/P(H=1))
const targetSlope = (1 / costRatio) * (pValid / pInvalid);
let bestTau = 0;
let minDiff = Infinity;
for (let i = 1; i < roc.length; i++) {
const prev = roc[i-1];
const curr = roc[i];
const slope = (curr.TPR - prev.TPR) / (curr.FPR - prev.FPR) || 0;
const diff = Math.abs(slope - targetSlope);
if (diff < minDiff) {
minDiff = diff;
bestTau = curr.tau;
}
}
return bestTau;
}
function calibrateStratifiedThresholds() {
const thresholds = [];
for (let i = 0; i <= 100; i++) {
thresholds.push(i / 100);
}
console.log('Calibrating Class A threshold (single mismatch scenarios)...');
const rocA = computeROC(thresholds, 'A');
const tauA = findOptimalThreshold(rocA);
console.log('Calibrating Class B threshold (multi-gateway mismatch scenarios)...');
const rocB = computeROC(thresholds, 'B');
const tauB = findOptimalThreshold(rocB);
return { tauA, tauB };
}
function generateReport(roc, optimalTau) {
let report = `# ROC Calibration Report for Trigger Score Threshold\n\n`;
report += `Generated: ${new Date().toISOString()}\n\n`;
report += `## Simulation Parameters\n\n`;
report += `- N = ${N_SIMULATIONS} poll cycles\n`;
report += `- P(H_t=1) = ${P_DIVERGENCE}\n`;
report += `- θ (divergence) = ${THETA_DIVERGENCE}\n`;
report += `- θ (valid) = ${THETA_VALID}\n`;
report += `- r_t = ${R_T} responding gateways\n`;
report += `- Prior: Beta(${ALPHA0}, ${BETA0})\n\n`;
report += `## ROC Data\n\n`;
report += `| Threshold (τ) | TPR | FPR |\n`;
report += `|---------------|-----|-----|\n`;
roc.forEach(point => {
report += `| ${point.tau.toFixed(3)} | ${point.TPR.toFixed(3)} | ${point.FPR.toFixed(3)} |\n`;
});
report += `\n`;
report += `## Optimal Threshold\n\n`;
report += `Chosen τ* = ${optimalTau.toFixed(3)}\n\n`;
report += `Based on cost ratio ρ = c2/c1 = 10 (safety-first posture).\n\n`;
report += `This threshold minimizes expected loss under conservative assumptions.\n\n`;
const reportPath = path.join(__dirname, '..', 'demo', 'roc-report.md');
fs.mkdirSync(path.dirname(reportPath), { recursive: true });
fs.writeFileSync(reportPath, report);
console.log(`ROC report written to ${reportPath}`);
}
function main() {
console.log('Running ROC simulation...');
const thresholds = [];
for (let i = 0; i <= 100; i++) {
thresholds.push(i / 100);
}
const roc = computeROC(thresholds);
const optimalTau = findOptimalThreshold(roc);
console.log(`Global optimal threshold: ${optimalTau.toFixed(3)}`);
console.log('Calibrating stratified thresholds...');
const { tauA, tauB } = calibrateStratifiedThresholds();
console.log(`Class A optimal threshold: ${tauA.toFixed(3)}`);
console.log(`Class B optimal threshold: ${tauB.toFixed(3)}`);
generateReport(roc, optimalTau);
// Update config/scoring.json
const config = require('../config/scoring.json');
config.thresholdA = tauA;
config.thresholdB = tauB;
const fs = require('fs');
fs.writeFileSync('./config/scoring.json', JSON.stringify(config, null, 2));
console.log('Updated config/scoring.json with recalibrated thresholds');
}
if (require.main === module) {
main();
}
module.exports = { simulatePollCycle, computeROC, findOptimalThreshold }; |