/* ============================================================
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)}
` +
'' + (tg.provenance || []).map(p => `- ${p}
`).join('') + '
';
}
window.CalibrateTab = { init };
})();