/* ============================================================ calibrate.js — Calibration 탭 (COPASI joint 보정 + 정직한 검증) ============================================================ */ (function () { 'use strict'; let charts = {}, inited = false; const AXIS_COLOR = { W: '#2f63c8', B: '#7c3aed', S: '#0e7490', D: '#1f8a5b', H: '#c2367f', F: '#c0561f', A: '#b07a12' }; function el(id) { return document.getElementById(id); } function num(v, d) { return (v === null || v === undefined || isNaN(v)) ? '—' : (+v).toFixed(d); } function init() { if (inited) return; inited = true; const cal = Store.calibration; const sel = el('cal-target'); if (!cal || !cal.targets) { el('cal-summary').innerHTML = '
calibration_result.json 없음 — python -m digital_twin.copasi_calibrate 실행 필요.
'; return; } Object.keys(cal.targets).forEach(k => sel.add(new Option((cal.targets[k] || {}).source_label || k, k))); sel.addEventListener('change', () => render(sel.value)); renderValidation(); renderHeader(); renderCoupled(); render(sel.value || Object.keys(cal.targets)[0]); } function renderCoupled() { const box = el('cal-coupled'); if (!box) return; const cp = Store.coupled; if (!cp || !cp.scenarios) { box.innerHTML = 'coupled_scenarios.json 없음'; return; } const lab = s => (s.interventions || []).length ? s.disease + ' + ' + s.interventions.join('+') : s.disease; box.innerHTML = cp.scenarios.map(s => { const af = s.anagen_fraction_pct_of_healthy, ph = s.peak_hair_pct_of_healthy; const cls = af >= 70 ? 'good' : af >= 30 ? 'warn' : 'bad'; return `
${lab(s)} anagen ${num(af, 0)}% · peak ${num(ph, 0)}%
`; }).join(''); } function renderValidation() { const box = el('cal-validation'); if (!box) return; const v = Store.validation; if (!v || !v.summary) { box.innerHTML = ''; return; } const s = v.summary; box.innerHTML = `
🔬 독립 검증 — 트윈 예측 vs 실제 논문 실험결과 (out-of-sample)
${(s.twin_causal_accuracy*100).toFixed(1)}%
논문 인과 실험 일치 (n=${s.n_causal})
p${s.permutation_p < 0.0001 ? '<0.0001' : '='+s.permutation_p}
permutation (우연 대비)
[${(s.bootstrap_ci95||[]).map(x=>(x*100).toFixed(0)).join(', ')}]%
bootstrap 95% CI (1만회)
${s.ground_truth_records}
논문 실험결과 GT (멀티에이전트)
${(s.external_transcriptomic_accuracy*100).toFixed(0)}%
외부 GSE36169 (조성교란·참고)
✅ 수정: ${(s.fixes_applied||[]).join(' · ')}  |  트윈이 옳고 독립소스가 틀린 예: ${(s.twin_correct_where_sources_err||[]).join('; ')}
${v.protein_modules ? `
🧬 단백질 분석 모듈 교차검증 (알파폴드 외): STRING PPI 축 응집 ${v.protein_modules.axes_ppi_significant} (p<0.05) · 축 라벨일치 ${v.protein_modules.axes_label_matched} · 질환 생물학일치 ${v.protein_modules.diseases_biology_matched} · 실험 PDB ${v.protein_modules.experimental_pdb}
` : ''}
⚠️ ${(v.honest_caveats||[]).join(' · ')}
`; } function renderHeader() { const cal = Store.calibration, s = cal.summary || {}, uq = cal.uncertainty || {}; const jr = s.joint_r2 || {}; const robust = (s.robust_axes || []).map(a => (cal.targets.reference.axis_label || {})[a] || a).join(', '); el('cal-summary').innerHTML = `
${num(s.loocv_cv_r2, 2)}
LOOCV 교차검증 R² (외표본·정직 지표)
${num(jr.reference, 2)}
joint R² · 문헌(형상)
${num(jr.gse11186, 2)}
joint R² · 실측 GSE11186
${cal.n_fit_params}+${cal.n_fixed_params}fix
추정/고정 파라미터
${num(cal.aicc, 0)}
AICc (복잡도 penalized)
견고 축: ${robust || '—'} · 비식별 ${uq.n_poorly_identified}/${cal.n_fit_params} · 경계고착 ${uq.n_at_bound}
${cal.approach || ''}
`; // 정직성 배너 let banner = el('cal-banner'); if (!banner) { banner = document.createElement('div'); banner.id = 'cal-banner'; banner.className = 'cal-banner'; el('cal-summary').insertAdjacentElement('afterend', banner); } banner.innerHTML = `⚖️ 정직한 보정: 단일적합 R²(문헌 ${num((cal.descriptive_single_r2||{}).reference,2)}, 실측 ${num((cal.descriptive_single_r2||{}).gse11186,2)})은 데이터셋 간 전이 안 되는 서술적 상한. 1차 지표는 단일 파라미터셋으로 두 데이터셋을 동시 적합한 joint R²LOOCV. ${s.honesty_note || ''}`; } function render(key) { const cal = Store.calibration; const tg = (cal.targets || {})[key]; if (!tg) return; const grid = el('cal-grid'); Object.values(charts).forEach(c => c.destroy()); charts = {}; const obs = tg.observed || []; grid.innerHTML = obs.filter(st => tg.model_dense && tg.model_dense[st] && tg.data && tg.data[st]).map(st => { const fq = (tg.fit_quality || {})[st] || {}; const r2 = fq.r2; const cls = r2 >= 0.7 ? 'good' : r2 >= 0.3 ? 'warn' : 'bad'; return `
${(tg.axis_label || {})[st] || st} R²=${num(r2, 2)}
`; }).join(''); obs.forEach(st => { if (!el(`cal-c-${st}`)) return; const col = AXIS_COLOR[st] || '#6699ff'; const modelPts = (tg.model_dense_t || []).map((t, i) => ({ x: t, y: tg.model_dense[st][i] })); const dataPts = (tg.timepoints_days || []).map((t, i) => ({ x: t, y: tg.data[st][i] })); charts[st] = new Chart(el(`cal-c-${st}`), { data: { datasets: [ { type: 'line', label: '모델(joint 보정)', data: modelPts, borderColor: col, borderWidth: 2, pointRadius: 0, tension: .3 }, { type: 'scatter', label: '데이터', data: dataPts, backgroundColor: '#1b1a16', borderColor: '#1b1a16', pointRadius: 3.5 }, ] }, options: { responsive: true, maintainAspectRatio: false, scales: { x: { type: 'linear', min: 0, ticks: { color: '#6b655a', font: { size: 10 }, callback: v => v + 'd' }, grid: { color: 'rgba(0,0,0,.08)' } }, y: { min: -0.05, max: 1.1, ticks: { color: '#6b655a', font: { size: 10 } }, grid: { color: 'rgba(0,0,0,.08)' } } }, plugins: { legend: { display: false } } }, }); }); // 추정 파라미터 + 식별가능성 const fp = cal.fitted_params || {}, uq = cal.uncertainty || {}, rse = uq.rel_stderr || {}; const fitNames = cal.fit_param_names || []; el('cal-nparam').textContent = fitNames.length; el('cal-param-table').innerHTML = fitNames.map(k => { const poor = (uq.poorly_identified || []).includes(k); const bound = (uq.params_at_bound || []).includes(k); const flag = bound ? '⚠경계' : poor ? '·비식별' : ''; return `
${k}${flag ? ' ' + flag + '' : ''}${num(rse[k], 2)}${num(fp[k], 3)}
`; }).join(''); // provenance + UQ 요약 const corr = (uq.strong_correlations || []).slice(0, 6).map(c => `${c[0]}~${c[1]}(${c[2]})`).join(', '); el('cal-provenance').innerHTML = `

${tg.is_real_data ? '✅ 실측 데이터' : '📐 문헌 합성(형상복원 점검)'} — ${tg.notes || ''}

` + `

불확실성: 비식별 ${uq.n_poorly_identified}/${fitNames.length} · 경계고착 ${uq.n_at_bound} · 강상관쌍: ${corr || '없음'} · LOOCV CV-R²=${num((cal.loocv_reference||{}).cv_r2,2)}

` + ''; } window.CalibrateTab = { init }; })();