| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| const fs = require('fs'); |
| const path = require('path'); |
|
|
| const { ALPHA0, BETA0 } = require('../prover/scorer'); |
|
|
| const N_SIMULATIONS = 10000; |
| const P_DIVERGENCE = 0.01; |
| const THETA_DIVERGENCE = 0.5; |
| const THETA_VALID = 0; |
| const R_T = 5; |
|
|
| 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; |
|
|
| |
| let m_t; |
| if (scenario === 'A') { |
| |
| m_t = 1; |
| } else if (scenario === 'B') { |
| |
| m_t = Math.random() < 0.5 ? 2 : 3; |
| } else { |
| |
| 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; |
| 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) { |
| |
| 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); |
|
|
| |
| 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 }; |