Initial import: alopecia digital twin static site (from GCS)
This commit is contained in:
commit
a37bdc54f8
305
css/editorial.css
Normal file
305
css/editorial.css
Normal file
@ -0,0 +1,305 @@
|
||||
/* =====================================================
|
||||
editorial.css — 전문 에디토리얼 재디자인 (라이트)
|
||||
style.css / twin.css 위에 로드되어 디자인 언어를 교체.
|
||||
Pretendard · 종이 배경 · 잉크 · 단일 버밀리언 액센트 · 헤어라인.
|
||||
(다크 글래스/그라데이션/글로우/이모지 클리셰 제거)
|
||||
===================================================== */
|
||||
|
||||
:root{
|
||||
--bg:#f1ede4;
|
||||
--surface:#fbf9f4;
|
||||
--surface-hover:#f4f0e6;
|
||||
--border:#ddd5c5;
|
||||
--border-bright:#c8401f;
|
||||
--primary:#c8401f; /* 단일 액센트: 버밀리언 */
|
||||
--primary-dark:#a8330f;
|
||||
--accent:#1f5d52; /* 보조: 딥 틸 */
|
||||
--accent2:#1f5d52;
|
||||
--text:#1b1a16;
|
||||
--text-muted:#8a8473;
|
||||
--success:#1f6d3a;
|
||||
--warn:#9a6a12;
|
||||
--danger:#b3361b;
|
||||
--radius:6px;
|
||||
--radius-sm:5px;
|
||||
--shadow:0 1px 0 rgba(0,0,0,.02);
|
||||
--line:#ddd5c5;
|
||||
--line2:#cabfa9;
|
||||
--ink2:#4a463d;
|
||||
--font:'Pretendard Variable',Pretendard,system-ui,-apple-system,'Apple SD Gothic Neo','Malgun Gothic',sans-serif;
|
||||
--font-mono:'JetBrains Mono','Pretendard Variable',Pretendard,monospace;
|
||||
}
|
||||
|
||||
html{font-size:16.5px}
|
||||
body{
|
||||
background:var(--bg) !important;
|
||||
color:var(--text);
|
||||
font-family:var(--font) !important;
|
||||
letter-spacing:-.2px;
|
||||
-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
|
||||
}
|
||||
.mono,.stamp,.runner,code{font-family:var(--font-mono)}
|
||||
|
||||
/* 배경 오브 제거 (AI 클리셰) */
|
||||
.bg-orb{display:none !important}
|
||||
|
||||
.container{max-width:1320px;padding:30px 40px 70px}
|
||||
|
||||
/* ── 헤더 : 에디토리얼 러너 ── */
|
||||
.dashboard-header{
|
||||
background:transparent !important;backdrop-filter:none !important;
|
||||
border:0 !important;border-bottom:2px solid var(--text) !important;
|
||||
border-radius:0 !important;padding:0 0 16px !important;box-shadow:none !important;
|
||||
margin-bottom:0 !important;align-items:flex-end !important;
|
||||
}
|
||||
.dna-icon{display:none !important}
|
||||
.dashboard-header h1{font-family:var(--font);font-weight:800;font-size:27.5px;letter-spacing:-.7px;color:var(--text);line-height:1.15}
|
||||
.gradient-text{background:none !important;-webkit-text-fill-color:var(--primary) !important;color:var(--primary) !important}
|
||||
.ko-title{color:var(--text-muted);font-weight:400}
|
||||
.subtitle{color:var(--text-muted);font-size:13.2px;font-family:var(--font-mono);letter-spacing:.3px;margin-top:5px}
|
||||
.header-stats{gap:0 !important}
|
||||
.stat-pill{
|
||||
background:transparent !important;border:0 !important;border-left:1px solid var(--line) !important;
|
||||
border-radius:0 !important;padding:2px 16px !important;color:var(--ink2) !important;
|
||||
font-family:var(--font-mono);font-size:12.1px;letter-spacing:.3px;
|
||||
}
|
||||
.stat-pill:first-child{border-left:0 !important;padding-left:0 !important}
|
||||
.stat-pill span{color:var(--primary);font-weight:700}
|
||||
.stat-pill.updated span{color:var(--text-muted)}
|
||||
.pulse-dot,.live-dot,.pulse-badge,.pulse-dot::after{display:none !important}
|
||||
|
||||
/* ── 탭 : 밑줄형 에디토리얼 ── */
|
||||
.tab-nav{
|
||||
background:transparent !important;border:0 !important;border-bottom:1px solid var(--line) !important;
|
||||
border-radius:0 !important;padding:0 !important;gap:0 !important;margin:0 0 26px !important;
|
||||
box-shadow:none !important;display:flex;flex-wrap:wrap;
|
||||
}
|
||||
.tab-btn{
|
||||
background:transparent !important;border:0 !important;border-bottom:2px solid transparent !important;
|
||||
border-radius:0 !important;color:var(--text-muted) !important;font-family:var(--font);font-weight:700;
|
||||
font-size:14.8px;letter-spacing:-.2px;padding:14px 18px !important;margin-bottom:-1px;transition:.12s;
|
||||
}
|
||||
.tab-btn:hover{color:var(--text) !important;background:transparent !important}
|
||||
.tab-btn.active{
|
||||
background:transparent !important;color:var(--text) !important;
|
||||
border-bottom:2px solid var(--primary) !important;box-shadow:none !important;
|
||||
}
|
||||
|
||||
/* ── 패널/카드 : 종이 + 헤어라인 (글래스 제거) ── */
|
||||
.panel,.glass-panel,.full-panel,.chart-panel{
|
||||
background:var(--surface) !important;backdrop-filter:none !important;
|
||||
border:1px solid var(--line) !important;border-radius:var(--radius) !important;
|
||||
box-shadow:none !important;
|
||||
}
|
||||
.panel h2,.glass-panel h2{font-family:var(--font);font-weight:800;font-size:17.6px;letter-spacing:-.4px;color:var(--text)}
|
||||
.panel h3{font-weight:700;color:var(--text)}
|
||||
.panel-subtitle{color:var(--text-muted);font-size:13.8px}
|
||||
|
||||
.badge{background:var(--surface-hover) !important;border:1px solid var(--line);color:var(--ink2) !important;border-radius:4px}
|
||||
.badge.small{font-family:var(--font-mono)}
|
||||
|
||||
/* ── 버튼/입력 ── */
|
||||
.btn-primary,.btn-ghost,.btn-page{
|
||||
background:var(--surface) !important;border:1px solid var(--line2) !important;color:var(--text) !important;
|
||||
border-radius:var(--radius-sm) !important;font-family:var(--font);font-weight:700;box-shadow:none !important;
|
||||
}
|
||||
.btn-primary{background:var(--primary) !important;border-color:var(--primary) !important;color:#fff !important}
|
||||
.btn-primary:hover,.btn-ghost:hover{background:var(--surface-hover) !important;border-color:var(--primary) !important}
|
||||
.btn-primary:hover{background:var(--primary-dark) !important;color:#fff !important}
|
||||
.search-input,.mini-select,.custom-select,select,input[type=text]{
|
||||
background:#fff !important;border:1px solid var(--line2) !important;color:var(--text) !important;
|
||||
border-radius:var(--radius-sm) !important;font-family:var(--font);
|
||||
}
|
||||
.search-input::placeholder{color:var(--text-muted)}
|
||||
|
||||
/* ── 테이블 ── */
|
||||
.papers-table th{color:var(--text-muted) !important;border-bottom:2px solid var(--text) !important;
|
||||
font-family:var(--font-mono);font-size:12.1px;letter-spacing:.5px;text-transform:uppercase}
|
||||
.papers-table td{border-bottom:1px solid var(--line) !important;color:var(--text)}
|
||||
.papers-table tbody tr:hover{background:var(--surface-hover) !important}
|
||||
.title-cell{color:var(--text)}
|
||||
|
||||
/* ── 차트 래퍼 / 다크 뷰포트(3D·그래프) ── */
|
||||
.chart-wrapper,.chart-box{background:transparent}
|
||||
.ngl-viewer{background:#0e0d0b !important;border:1px solid var(--line2) !important;border-radius:var(--radius-sm) !important}
|
||||
#graph-canvas{background:#13110d !important;border:1px solid var(--line2) !important;border-radius:var(--radius-sm)}
|
||||
.graph-tooltip{background:#fff !important;border:1px solid var(--line2) !important;color:var(--text) !important;box-shadow:0 4px 18px rgba(0,0,0,.12) !important}
|
||||
.graph-tooltip .tt-sub{color:var(--text-muted)}
|
||||
.hologram-ring{border-color:var(--line2) !important;opacity:.5}
|
||||
.centered-content .placeholder-text h3{color:var(--text)}
|
||||
.centered-content .placeholder-text p{color:var(--text-muted)}
|
||||
|
||||
/* ── TWIN 탭 ── */
|
||||
.seg-btn{background:var(--surface) !important;border:1px solid var(--line) !important;color:var(--text) !important}
|
||||
.seg-btn:hover{background:var(--surface-hover) !important}
|
||||
.seg-btn.active{background:#fff !important;border-color:var(--primary) !important;box-shadow:inset 3px 0 0 var(--primary) !important}
|
||||
.seg-btn .seg-sub{color:var(--text-muted)}
|
||||
.chip{background:var(--surface) !important;border:1px solid var(--line2) !important;color:var(--ink2) !important}
|
||||
.chip.active{background:var(--primary) !important;border-color:var(--primary) !important;color:#fff !important}
|
||||
.disease-desc{background:#f3ede0 !important;border:1px solid var(--line2) !important;color:var(--ink2) !important}
|
||||
.metric-card{background:var(--surface) !important;border:1px solid var(--line) !important}
|
||||
.metric-card .mv{font-family:var(--font);color:var(--text)}
|
||||
.metric-card.good .mv{color:var(--success)} .metric-card.warn .mv{color:var(--warn)} .metric-card.bad .mv{color:var(--danger)}
|
||||
.metric-card .ml{color:var(--text-muted)}
|
||||
.gene-pill{background:#f3ede0 !important;border:1px solid var(--line2) !important;color:var(--primary) !important;font-family:var(--font-mono)}
|
||||
.slider-group label{color:var(--text-muted)} .slider-group label span{color:var(--primary)}
|
||||
.slider-group input[type=range]{accent-color:var(--primary)}
|
||||
|
||||
/* ── ATLAS 탭 ── */
|
||||
.prot-card{background:var(--surface) !important;border:1px solid var(--line) !important;box-shadow:none !important}
|
||||
.prot-card:hover{background:var(--surface-hover) !important;border-color:var(--primary) !important;transform:none !important}
|
||||
.prot-card.selected{border-color:var(--primary) !important;box-shadow:inset 0 0 0 1px var(--primary) !important}
|
||||
.prot-card .pc-gene{font-family:var(--font-mono);color:var(--text)}
|
||||
.prot-card .pc-name{color:var(--text-muted)}
|
||||
.pc-seed{color:var(--primary)} .pc-ev{color:var(--text-muted)}
|
||||
.detail-head h2{font-family:var(--font-mono);color:var(--text)}
|
||||
.dbadge{background:var(--surface-hover) !important;border:1px solid var(--line);color:var(--ink2)}
|
||||
.detail-grid .dg{background:var(--surface) !important;border:1px solid var(--line) !important}
|
||||
.detail-grid .dg .k{color:var(--text-muted)} .detail-grid .dg .v{color:var(--text)}
|
||||
.evidence-item{background:var(--surface) !important;border:1px solid var(--line) !important}
|
||||
.evidence-item .ey{color:var(--primary)}
|
||||
.grounding-item{background:#eef3ec !important;border:1px solid #cfe0c8 !important}
|
||||
.gr-paper{color:#2f6d3a !important} .gr-quote{color:var(--ink2)}
|
||||
.detail-links a{background:#fff !important;border:1px solid var(--primary) !important;color:var(--primary) !important}
|
||||
.detail-links a:hover{background:var(--primary) !important;color:#fff !important}
|
||||
.plddt-val{color:var(--text)}
|
||||
.role-driver{color:#b3361b} .role-protector{color:#1f6d3a} .role-modulator{color:#1f5d52}
|
||||
|
||||
/* ── GRAPH 탭 ── */
|
||||
.gl{color:var(--text-muted)} .graph-filters label{color:var(--text-muted)}
|
||||
|
||||
/* ── CALIBRATION 탭 ── */
|
||||
.cal-banner{background:#f5efe2 !important;border:1px solid var(--line2) !important;color:var(--ink2) !important}
|
||||
.cal-card{background:var(--surface) !important;border:1px solid var(--line) !important}
|
||||
.cal-card.big{background:#f5ece1 !important;border-color:var(--primary) !important}
|
||||
.cal-card .mv{color:var(--primary)} .cal-card.big .mv{color:var(--text)} .cal-card .ml{color:var(--text-muted)}
|
||||
.cal-axis-card,.coupled-bar-wrap,.newpaper-card{background:var(--surface) !important;border:1px solid var(--line) !important}
|
||||
.cal-r2.good{background:#e3efe0;color:#1f6d3a} .cal-r2.warn{background:#f3ead4;color:#9a6a12} .cal-r2.bad{background:#f5e0db;color:#b3361b}
|
||||
.cal-prow{border-bottom:1px solid var(--line)} .cal-prow:nth-child(odd){background:var(--surface-hover)}
|
||||
.cal-prow .pa{color:var(--primary)} .cal-prow .pi{color:var(--text-muted)}
|
||||
.cal-provenance{color:var(--text-muted)} .cal-note{color:var(--text)}
|
||||
.val-card{background:var(--surface) !important;border:1px solid var(--line) !important}
|
||||
.val-card.big{background:#eef3ec !important;border-color:#cfe0c8 !important}
|
||||
.val-card .vv{color:var(--success)} .val-card .vl{color:var(--text-muted)}
|
||||
.cal-validation{background:#eef3ec !important;border:1px solid #cfe0c8 !important}
|
||||
.val-note{color:var(--ink2)} .val-note.dim{color:var(--text-muted)}
|
||||
.val-pm{background:#f3ede0 !important;border:1px solid var(--line2) !important} .val-pm b{color:var(--primary)}
|
||||
.coupled-bar.good{background:#1f6d3a} .coupled-bar.warn{background:#9a6a12} .coupled-bar.bad{background:#b3361b}
|
||||
.coupled-lab{color:var(--text)} .coupled-val{color:var(--text-muted)}
|
||||
|
||||
/* ── PAPERS ── */
|
||||
.newpaper-card .np-title{color:var(--text)} .newpaper-card .np-meta{color:var(--text-muted)}
|
||||
.np-target{color:var(--primary)} .targets-cell .tg{background:#f3ede0;color:var(--primary)}
|
||||
|
||||
.app-footer{color:var(--text-muted);border-top:1px solid var(--line)}
|
||||
|
||||
/* 스크롤바 */
|
||||
::-webkit-scrollbar{width:10px;height:10px}
|
||||
::-webkit-scrollbar-track{background:transparent}
|
||||
::-webkit-scrollbar-thumb{background:var(--line2);border-radius:5px;border:2px solid var(--bg)}
|
||||
|
||||
/* ── 치료 타임라인 탭 ── */
|
||||
.tl-disclaimer{background:#f6ead6;border:1px solid #e0c896;border-left:4px solid var(--primary);
|
||||
border-radius:var(--radius-sm);padding:13px 16px;margin-bottom:18px;color:#5c4a2a;font-size:13.3px;line-height:1.65}
|
||||
.tl-disclaimer b{color:var(--primary)}
|
||||
.tl-disc2{display:block;margin-top:6px;color:#7a6a4a;font-size:12.2px;font-family:var(--font-mono)}
|
||||
.tl-layout{display:grid;grid-template-columns:370px 1fr;gap:20px;align-items:start}
|
||||
.tl-controls h2{margin-bottom:4px}
|
||||
.tl-controls .form-group{margin-top:16px}
|
||||
.tl-upload{display:flex;flex-wrap:wrap;gap:8px}
|
||||
.tl-upload .btn-primary,.tl-upload .btn-ghost{cursor:pointer;padding:8px 14px;font-size:13px;display:inline-flex;align-items:center}
|
||||
.tl-metrics{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:18px}
|
||||
.tl-metrics .metric-card{padding:12px 14px}
|
||||
.tl-metrics .mv{font-size:26.4px;font-weight:800}
|
||||
.tl-ivs-line{margin-top:12px;font-size:13px;color:var(--text-muted)}
|
||||
.tl-ivs-line b{color:var(--text)}
|
||||
.tl-chart-wrap{height:150px;margin-top:14px}
|
||||
.tl-cap{font-size:11.6px;color:var(--text-muted);margin-top:6px;font-family:var(--font-mono)}
|
||||
.tl-stage{display:flex;flex-direction:column;gap:14px;padding:16px}
|
||||
.tl-canvas-wrap{position:relative;background:#0e0d0b;border:1px solid var(--line2);border-radius:var(--radius-sm);
|
||||
min-height:300px;display:flex;align-items:center;justify-content:center;overflow:hidden}
|
||||
#tl-canvas{display:block;max-width:100%;height:auto;cursor:crosshair}
|
||||
.tl-stage.empty #tl-canvas{display:none}
|
||||
.tl-empty-hint{color:#9a9284;font-size:14px;text-align:center;padding:40px;line-height:1.7}
|
||||
.tl-empty-hint b{color:#d6cfc0}
|
||||
.tl-stage:not(.empty) .tl-empty-hint{display:none}
|
||||
.tl-hint{position:absolute;top:10px;left:50%;transform:translateX(-50%);background:rgba(200,64,31,.92);color:#fff;
|
||||
font-size:12.5px;padding:6px 14px;border-radius:20px;white-space:nowrap;box-shadow:0 2px 10px rgba(0,0,0,.3)}
|
||||
.tl-hint.hidden{display:none}
|
||||
.tl-scrubber{padding:4px 6px 2px}
|
||||
.tl-month{font-size:19.8px;font-weight:800;color:var(--text);margin-bottom:8px}
|
||||
.tl-month-sub{font-size:13px;font-weight:400;color:var(--text-muted);font-family:var(--font-mono)}
|
||||
#tl-slider{width:100%;-webkit-appearance:none;appearance:none;height:6px;border-radius:3px;
|
||||
background:linear-gradient(90deg,var(--primary) 0%,var(--line2) 0%);outline:none;cursor:pointer}
|
||||
#tl-slider::-webkit-slider-thumb{-webkit-appearance:none;width:22px;height:22px;border-radius:50%;
|
||||
background:var(--primary);border:3px solid #fff;box-shadow:0 1px 5px rgba(0,0,0,.3);cursor:pointer}
|
||||
#tl-slider::-moz-range-thumb{width:18px;height:18px;border-radius:50%;background:var(--primary);border:3px solid #fff;cursor:pointer}
|
||||
.tl-ticks{display:flex;justify-content:space-between;margin-top:7px;font-family:var(--font-mono);font-size:11px;color:var(--text-muted)}
|
||||
.tl-axisnote{margin-top:6px;font-size:11.5px;color:var(--text-muted);font-family:var(--font-mono);line-height:1.5}
|
||||
@media(max-width:920px){.tl-layout{grid-template-columns:1fr}}
|
||||
|
||||
/* ── 검증(Validation) 탭 ── */
|
||||
.val-intro{margin-bottom:20px}
|
||||
.val-summary-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:16px}
|
||||
.vcard{border:1px solid var(--line);border-radius:var(--radius-sm);padding:14px 16px;background:#fbf9f4}
|
||||
.vch{font-family:var(--font);font-weight:800;font-size:14.5px;color:var(--text);border-bottom:2px solid var(--text);padding-bottom:7px;margin-bottom:9px}
|
||||
.vrow{display:grid;grid-template-columns:1fr auto;gap:6px 10px;align-items:center;padding:5px 0;border-bottom:1px solid var(--line)}
|
||||
.vrow:last-child{border-bottom:0}
|
||||
.vrd{font-size:12.4px;color:var(--ink2);grid-column:1}
|
||||
.vrm{font-size:12px;color:var(--text-muted);grid-column:1;font-family:var(--font-mono)}
|
||||
.vb{grid-column:2;grid-row:1/3;justify-self:end;font-size:11px;font-weight:700;padding:3px 9px;border-radius:11px;white-space:nowrap}
|
||||
.vb-ok{background:#e3efe0;color:var(--success)} .vb-warn{background:#f3ead4;color:var(--warn)} .vb-bad{background:#f5e0db;color:var(--danger)}
|
||||
.vp{color:var(--primary);font-weight:700}
|
||||
.val-grid{display:grid;grid-template-columns:1fr 1fr;gap:18px}
|
||||
.val-grid .panel{padding:16px 18px}
|
||||
.val-grid h3{font-family:var(--font);font-weight:800;font-size:15px;color:var(--text);margin-bottom:4px}
|
||||
.val-wide{grid-column:1/-1}
|
||||
.val-chart{height:240px;margin-top:8px}
|
||||
.val-cap{font-size:11.8px;color:var(--text-muted);margin-top:8px;line-height:1.6}
|
||||
.val-donuts{display:flex;gap:24px;justify-content:center;align-items:center;margin-top:10px}
|
||||
.val-donut{position:relative;width:150px;height:150px}
|
||||
.val-donut-lab{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center;font-size:13px;color:var(--text-muted);font-family:var(--font-mono)}
|
||||
.val-donut-lab b{font-size:22px;color:var(--text);font-family:var(--font)}
|
||||
.val-table{width:100%;border-collapse:collapse;margin-top:8px;font-size:13px}
|
||||
.val-table th{text-align:left;color:var(--text-muted);border-bottom:2px solid var(--text);padding:7px 8px;font-family:var(--font-mono);font-size:11px;letter-spacing:.3px;text-transform:uppercase}
|
||||
.val-table td{padding:6px 8px;border-bottom:1px solid var(--line);color:var(--text)}
|
||||
.val-table tbody tr:hover{background:var(--surface-hover)}
|
||||
.vc-g{font-family:var(--font-mono);font-weight:700;color:var(--primary)}
|
||||
.vc-p{font-family:var(--font-mono);font-weight:700}
|
||||
.vc-p.good{color:var(--success)} .vc-p.warn{color:var(--warn)} .vc-p.bad{color:var(--danger)}
|
||||
.val-landscape{margin-bottom:18px}
|
||||
.val-headline{display:flex;flex-wrap:wrap;gap:0;margin:14px 0 6px;border-top:1px solid var(--line);border-bottom:1px solid var(--line)}
|
||||
.vstat{flex:1;min-width:90px;padding:12px 16px;border-left:1px solid var(--line);text-align:center}
|
||||
.vstat:first-child{border-left:0}
|
||||
.vsv{font-family:var(--font);font-weight:800;font-size:25px;color:var(--primary);line-height:1.1}
|
||||
.vsl{font-size:11.5px;color:var(--text-muted);margin-top:3px;font-family:var(--font-mono)}
|
||||
.val-land-grid{display:grid;grid-template-columns:1.3fr 1fr;gap:20px;margin-top:10px}
|
||||
.val-credible{margin-top:18px;border-top:3px solid var(--primary)}
|
||||
.val-cred-head{display:flex;align-items:center;gap:10px;margin-bottom:2px}
|
||||
.val-cred-head h3{margin:0}
|
||||
.val-cred-tag{flex:0 0 auto;font-family:var(--font-mono);font-size:11px;font-weight:700;letter-spacing:.04em;
|
||||
color:#fff;background:var(--primary);padding:3px 9px;border-radius:2px}
|
||||
.ipd-warn{background:rgba(200,64,31,.08);border:1px solid rgba(200,64,31,.35);border-left:3px solid var(--primary);
|
||||
padding:9px 12px;margin:8px 0 4px;font-size:13px;line-height:1.5;color:var(--ink,#1b1a16);border-radius:2px}
|
||||
.ipd-verdict{margin:8px 0 2px;font-size:13.5px;color:var(--text-muted);font-family:var(--font-mono)}
|
||||
.net-controls{display:flex;align-items:center;gap:14px;flex-wrap:wrap;margin:14px 0 6px}
|
||||
.net-controls label{font-size:13px;color:var(--text-muted);display:flex;align-items:center;gap:6px}
|
||||
.net-sel{font-family:var(--font);font-size:13px;padding:5px 8px;border:1px solid var(--line);border-radius:3px;background:var(--bg);color:var(--ink,#1b1a16)}
|
||||
.net-slider{flex:1;min-width:160px;accent-color:var(--primary)}
|
||||
.net-month{font-family:var(--font-mono);font-weight:700;font-size:15px;color:var(--primary);min-width:54px;text-align:right}
|
||||
.net-status{font-size:12.5px;color:var(--text-muted);font-family:var(--font-mono);margin:2px 0 8px}
|
||||
.net-stage{display:flex;gap:14px;align-items:stretch;height:480px}
|
||||
.net-wrap{position:relative;flex:1 1 0;min-width:0;border:1px solid var(--line);border-radius:4px;background:radial-gradient(120% 120% at 50% 30%,#f8f4ec,#e9e0d0);overflow:hidden}
|
||||
.net-wrap>canvas{display:block;width:100%;height:100%}
|
||||
.net-ngl{position:absolute;inset:0}
|
||||
.net-ngl-load{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-family:var(--font-mono);font-size:13px;color:var(--text-muted);background:rgba(245,240,232,.7);z-index:5}
|
||||
#net-mode.on{background:var(--primary);color:#fff;border-color:var(--primary)}
|
||||
.net-foll{flex:0 0 270px;border:1px solid var(--line);border-radius:4px;background:radial-gradient(120% 120% at 50% 25%,#fcf7ef,#ece0cc);display:flex;flex-direction:column;overflow:hidden}
|
||||
.net-foll-title{font-size:11.5px;font-family:var(--font-mono);color:var(--text-muted);padding:7px 10px 2px;font-weight:700}
|
||||
.net-foll canvas{display:block;width:100%;flex:1;min-height:0}
|
||||
.net-foll-state{font-size:11.5px;font-family:var(--font-mono);color:var(--primary);padding:4px 10px 8px;text-align:center;font-weight:700}
|
||||
@media(max-width:760px){.net-stage{flex-direction:column;height:auto}.net-wrap{height:380px}.net-foll{flex:0 0 360px}}
|
||||
.net-legend{display:flex;flex-wrap:wrap;gap:12px;margin-top:10px}
|
||||
.net-leg{display:flex;align-items:center;gap:5px;font-size:11.5px;color:var(--text-muted);font-family:var(--font-mono)}
|
||||
.net-leg i{width:11px;height:11px;border-radius:50%;display:inline-block}
|
||||
@media(max-width:920px){.val-summary-grid,.val-grid,.val-land-grid{grid-template-columns:1fr}}
|
||||
1029
css/style.css
Normal file
1029
css/style.css
Normal file
File diff suppressed because it is too large
Load Diff
253
css/twin.css
Normal file
253
css/twin.css
Normal file
@ -0,0 +1,253 @@
|
||||
/* =====================================================
|
||||
twin.css — 단백질 디지털 트윈 추가 스타일 (style.css 보강)
|
||||
===================================================== */
|
||||
|
||||
.ko-title { font-size: .55em; color: var(--text-muted); font-weight: 400; }
|
||||
.app-footer {
|
||||
margin: 30px 4px 12px; display: flex; justify-content: space-between;
|
||||
flex-wrap: wrap; gap: 10px; color: var(--text-muted); font-size: .78rem;
|
||||
border-top: 1px solid var(--border); padding-top: 16px;
|
||||
}
|
||||
.btn-ghost {
|
||||
background: var(--surface); border: 1px solid var(--border); color: var(--text);
|
||||
padding: 8px 14px; border-radius: var(--radius-sm); cursor: pointer; font-size: .82rem;
|
||||
transition: .15s;
|
||||
}
|
||||
.btn-ghost:hover { background: var(--surface-hover); border-color: var(--border-bright); }
|
||||
.mini-chip-list { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
|
||||
/* ---------------- TAB 1: PROTEIN TWIN ---------------- */
|
||||
.twin-layout {
|
||||
display: grid; grid-template-columns: 320px 1fr 300px; gap: 18px; align-items: start;
|
||||
}
|
||||
@media (max-width: 1200px) { .twin-layout { grid-template-columns: 1fr; } }
|
||||
|
||||
.twin-controls h2 { font-size: 1.15rem; }
|
||||
.seg-control { display: flex; flex-direction: column; gap: 6px; }
|
||||
.seg-btn {
|
||||
text-align: left; padding: 10px 12px; border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border); background: var(--surface); color: var(--text);
|
||||
cursor: pointer; font-size: .85rem; transition: .15s;
|
||||
}
|
||||
.seg-btn:hover { background: var(--surface-hover); }
|
||||
.seg-btn.active {
|
||||
border-color: var(--border-bright); background: rgba(102,153,255,.14);
|
||||
box-shadow: 0 0 0 1px var(--border-bright) inset;
|
||||
}
|
||||
.seg-btn .seg-sub { display: block; font-size: .72rem; color: var(--text-muted); margin-top: 2px; }
|
||||
|
||||
.chip-list { display: flex; flex-wrap: wrap; gap: 7px; }
|
||||
.chip {
|
||||
padding: 7px 11px; border-radius: 20px; border: 1px solid var(--border);
|
||||
background: var(--surface); color: var(--text-muted); cursor: pointer;
|
||||
font-size: .78rem; transition: .15s; user-select: none;
|
||||
}
|
||||
.chip:hover { background: var(--surface-hover); color: var(--text); }
|
||||
.chip.active {
|
||||
background: linear-gradient(135deg, var(--primary), var(--accent));
|
||||
color: #fff; border-color: transparent; font-weight: 600;
|
||||
}
|
||||
.chip.disabled { opacity: .35; cursor: not-allowed; }
|
||||
|
||||
.disease-desc {
|
||||
margin-top: 12px; padding: 10px 12px; border-radius: var(--radius-sm);
|
||||
background: rgba(34,211,238,.06); border: 1px solid rgba(34,211,238,.18);
|
||||
font-size: .8rem; color: var(--text); line-height: 1.5;
|
||||
}
|
||||
.metrics-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 14px; }
|
||||
.metric-card {
|
||||
padding: 12px; border-radius: var(--radius-sm); background: var(--surface);
|
||||
border: 1px solid var(--border); text-align: center;
|
||||
}
|
||||
.metric-card .mv { font-size: 1.5rem; font-weight: 800; font-family: 'Outfit'; }
|
||||
.metric-card .ml { font-size: .68rem; color: var(--text-muted); margin-top: 2px; }
|
||||
.metric-card.good .mv { color: var(--success); }
|
||||
.metric-card.warn .mv { color: var(--warn); }
|
||||
.metric-card.bad .mv { color: var(--danger); }
|
||||
|
||||
.tracked-genes { margin-top: 16px; }
|
||||
.tracked-genes h4 { font-size: .8rem; color: var(--text-muted); margin-bottom: 8px; }
|
||||
.gene-pill {
|
||||
padding: 4px 9px; border-radius: 6px; background: rgba(168,85,247,.14);
|
||||
border: 1px solid rgba(168,85,247,.3); color: #d8b4fe; font-size: .74rem;
|
||||
font-family: 'JetBrains Mono', monospace; cursor: pointer;
|
||||
}
|
||||
.gene-pill:hover { background: rgba(168,85,247,.26); }
|
||||
|
||||
.twin-viz .viz-head, .twin-side .viz-head {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
margin: 6px 0 8px;
|
||||
}
|
||||
.twin-viz .viz-head h3, .twin-side h3 { font-size: .95rem; }
|
||||
.chart-box { position: relative; height: 240px; }
|
||||
.chart-box.small { height: 180px; }
|
||||
.slider-group { margin: 12px 0; }
|
||||
.slider-group label {
|
||||
display: flex; justify-content: space-between; font-size: .78rem;
|
||||
color: var(--text-muted); margin-bottom: 5px;
|
||||
}
|
||||
.slider-group label span { color: var(--accent2); font-family: 'JetBrains Mono'; }
|
||||
.slider-group input[type=range] { width: 100%; accent-color: var(--primary); }
|
||||
|
||||
/* ---------------- TAB 2: PROTEIN ATLAS ---------------- */
|
||||
.atlas-layout { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; align-items: start; }
|
||||
@media (max-width: 1000px) { .atlas-layout { grid-template-columns: 1fr; } }
|
||||
.atlas-controls { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 10px; }
|
||||
.atlas-controls .search-input { flex: 1 1 180px; }
|
||||
.atlas-count { font-size: .78rem; color: var(--text-muted); margin-bottom: 10px; }
|
||||
.atlas-grid {
|
||||
display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: 10px; max-height: 70vh; overflow-y: auto; padding-right: 4px;
|
||||
}
|
||||
.prot-card {
|
||||
padding: 11px; border-radius: var(--radius-sm); border: 1px solid var(--border);
|
||||
background: var(--surface); cursor: pointer; transition: .15s; position: relative;
|
||||
}
|
||||
.prot-card:hover { background: var(--surface-hover); border-color: var(--border-bright); transform: translateY(-2px); }
|
||||
.prot-card.selected { border-color: var(--primary); box-shadow: 0 0 0 1px var(--primary) inset; }
|
||||
.prot-card .pc-gene { font-family: 'JetBrains Mono'; font-weight: 600; font-size: .95rem; }
|
||||
.prot-card .pc-name { font-size: .68rem; color: var(--text-muted); margin: 3px 0 6px;
|
||||
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
||||
.prot-card .pc-tags { display: flex; flex-wrap: wrap; gap: 4px; }
|
||||
.axis-tag {
|
||||
font-size: .62rem; padding: 2px 6px; border-radius: 5px; font-weight: 600;
|
||||
}
|
||||
.role-driver { color: #fca5a5; } .role-protector { color: #86efac; } .role-modulator { color: #93c5fd; }
|
||||
.pc-ev { position: absolute; top: 8px; right: 9px; font-size: .62rem; color: var(--text-muted); }
|
||||
.pc-seed { position: absolute; top: 8px; right: 9px; font-size: .6rem; color: var(--accent);}
|
||||
|
||||
.atlas-detail-panel { min-height: 500px; }
|
||||
.detail-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; }
|
||||
.detail-head h2 { font-family: 'JetBrains Mono'; font-size: 1.6rem; }
|
||||
.d-name { color: var(--text-muted); font-size: .85rem; margin-top: 2px; }
|
||||
.detail-badges { display: flex; flex-wrap: wrap; gap: 6px; justify-content: flex-end; }
|
||||
.dbadge { font-size: .68rem; padding: 3px 9px; border-radius: 6px; border: 1px solid var(--border); background: var(--surface); }
|
||||
.ngl-viewer {
|
||||
width: 100%; height: 320px; margin: 14px 0 6px; border-radius: var(--radius-sm);
|
||||
background: #05070e; border: 1px solid var(--border); position: relative; overflow: hidden;
|
||||
}
|
||||
.ngl-viewer .ngl-loading {
|
||||
position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
|
||||
color: var(--text-muted); font-size: .82rem;
|
||||
}
|
||||
.plddt-legend { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; font-size: .68rem; color: var(--text-muted); }
|
||||
.plddt-legend .lg { padding: 2px 7px; border-radius: 4px; color: #06121f; font-weight: 600; }
|
||||
.lg-vh { background: #0053d6; color: #fff; } .lg-c { background: #65cbf3; } .lg-l { background: #ffdb13; } .lg-vl { background: #ff7d45; }
|
||||
.plddt-val { margin-left: auto; color: var(--text); font-weight: 600; }
|
||||
.detail-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin: 14px 0; }
|
||||
.detail-grid .dg { padding: 9px 11px; border-radius: var(--radius-sm); background: var(--surface); border: 1px solid var(--border); }
|
||||
.detail-grid .dg .k { font-size: .66rem; color: var(--text-muted); }
|
||||
.detail-grid .dg .v { font-size: .86rem; font-weight: 600; margin-top: 2px; }
|
||||
.detail-section { margin: 12px 0; }
|
||||
.detail-section h4 { font-size: .82rem; color: var(--text-muted); margin-bottom: 6px; }
|
||||
.detail-section p { font-size: .85rem; line-height: 1.55; }
|
||||
.evidence-list { display: flex; flex-direction: column; gap: 6px; max-height: 180px; overflow-y: auto; }
|
||||
.evidence-item { font-size: .76rem; padding: 7px 9px; border-radius: 7px; background: var(--surface); border: 1px solid var(--border); }
|
||||
.evidence-item .ey { color: var(--accent2); font-family: 'JetBrains Mono'; margin-right: 6px; }
|
||||
.detail-links { display: flex; gap: 8px; margin-top: 12px; }
|
||||
.detail-links a {
|
||||
flex: 1; text-align: center; padding: 9px; border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border-bright); background: rgba(102,153,255,.1);
|
||||
color: var(--primary); text-decoration: none; font-size: .8rem; font-weight: 600;
|
||||
}
|
||||
.detail-links a:hover { background: rgba(102,153,255,.2); }
|
||||
|
||||
/* ---------------- TAB 3: KNOWLEDGE GRAPH ---------------- */
|
||||
.graph-controls { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 12px; margin-bottom: 8px; }
|
||||
.graph-legend, .graph-filters { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; font-size: .76rem; }
|
||||
.graph-filters label { display: flex; align-items: center; gap: 5px; color: var(--text-muted); }
|
||||
.gl { display: flex; align-items: center; gap: 5px; color: var(--text-muted); }
|
||||
.gl::before { content: ''; width: 11px; height: 11px; border-radius: 50%; display: inline-block; }
|
||||
.gl-protein::before { background: #6699ff; } .gl-pathway::before { background: #a855f7; }
|
||||
.gl-disease::before { background: #f87171; } .gl-axis::before { background: #22d3ee; }
|
||||
.gl-drug::before { background: #34d399; }
|
||||
#graph-canvas { width: 100%; height: 68vh; display: block; border-radius: var(--radius-sm); background: radial-gradient(circle at 50% 40%, #0a1020, #05070e); cursor: grab; }
|
||||
.graph-tooltip {
|
||||
position: fixed; pointer-events: none; z-index: 100; padding: 8px 11px;
|
||||
background: rgba(8,12,22,.95); border: 1px solid var(--border-bright);
|
||||
border-radius: 8px; font-size: .76rem; max-width: 260px; box-shadow: var(--shadow);
|
||||
}
|
||||
.graph-tooltip .tt-title { font-weight: 700; font-family: 'JetBrains Mono'; margin-bottom: 3px; }
|
||||
.graph-tooltip .tt-sub { color: var(--text-muted); font-size: .7rem; }
|
||||
|
||||
/* ---------------- NEW PAPERS ---------------- */
|
||||
.newpapers-section { margin-top: 22px; }
|
||||
.newpapers-section h3 { font-size: 1rem; margin-bottom: 10px; }
|
||||
.newpapers-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 10px; }
|
||||
.newpaper-card { padding: 11px 13px; border-radius: var(--radius-sm); background: var(--surface); border: 1px solid var(--border); }
|
||||
.newpaper-card .np-title { font-size: .82rem; font-weight: 600; line-height: 1.4; }
|
||||
.newpaper-card .np-meta { font-size: .7rem; color: var(--text-muted); margin-top: 5px; }
|
||||
.newpaper-card .np-target { color: var(--accent2); font-family: 'JetBrains Mono'; }
|
||||
|
||||
.targets-cell { display: flex; flex-wrap: wrap; gap: 4px; }
|
||||
.targets-cell .tg { font-size: .68rem; font-family: 'JetBrains Mono'; padding: 1px 5px; border-radius: 4px; background: rgba(102,153,255,.12); color: #9db8ff; }
|
||||
|
||||
/* ---------------- CALIBRATION TAB ---------------- */
|
||||
.cal-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; flex-wrap: wrap; }
|
||||
.cal-target-sel { display: flex; flex-direction: column; gap: 5px; min-width: 240px; }
|
||||
.cal-target-sel label { font-size: .72rem; color: var(--text-muted); }
|
||||
.cal-summary { display: flex; flex-wrap: wrap; gap: 10px; margin: 16px 0; }
|
||||
.cal-card { padding: 12px 16px; border-radius: var(--radius-sm); background: var(--surface); border: 1px solid var(--border); min-width: 110px; }
|
||||
.cal-card.big { background: linear-gradient(135deg, rgba(102,153,255,.16), rgba(168,85,247,.12)); border-color: var(--border-bright); }
|
||||
.cal-card .mv { font-size: 1.7rem; font-weight: 800; font-family: 'Outfit'; color: var(--accent2); }
|
||||
.cal-card.big .mv { color: #fff; }
|
||||
.cal-card .mv-sm { font-size: .72rem; color: var(--text); font-family: 'JetBrains Mono'; }
|
||||
.cal-card .ml { font-size: .68rem; color: var(--text-muted); margin-top: 3px; }
|
||||
.cal-card.wide { flex: 1; min-width: 260px; }
|
||||
.cal-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 12px; margin: 8px 0 18px; }
|
||||
.cal-axis-card { padding: 10px; border-radius: var(--radius-sm); background: var(--surface); border: 1px solid var(--border); }
|
||||
.cal-axis-head { display: flex; justify-content: space-between; align-items: center; font-size: .82rem; margin-bottom: 6px; }
|
||||
.cal-r2 { font-size: .68rem; padding: 2px 7px; border-radius: 5px; font-family: 'JetBrains Mono'; }
|
||||
.cal-r2.good { background: rgba(52,211,153,.18); color: #86efac; }
|
||||
.cal-r2.warn { background: rgba(251,191,36,.18); color: #fcd34d; }
|
||||
.cal-r2.bad { background: rgba(248,113,113,.18); color: #fca5a5; }
|
||||
.cal-axis-chart { height: 130px; position: relative; }
|
||||
.cal-foot { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
||||
@media (max-width: 900px) { .cal-foot { grid-template-columns: 1fr; } }
|
||||
.cal-foot h4 { font-size: .85rem; color: var(--text-muted); margin-bottom: 8px; }
|
||||
.cal-param-table { max-height: 280px; overflow-y: auto; font-family: 'JetBrains Mono'; font-size: .72rem; }
|
||||
.cal-prow { display: grid; grid-template-columns: 1.4fr 1fr 1fr; gap: 6px; padding: 4px 8px; border-bottom: 1px solid var(--border); }
|
||||
.cal-prow:nth-child(odd) { background: rgba(255,255,255,.02); }
|
||||
.cal-prow .pk { color: var(--text); } .cal-prow .pi { color: var(--text-muted); } .cal-prow .pa { color: var(--accent2); font-weight: 600; }
|
||||
.cal-param-table::before { content: '파라미터 / 상대표준오차 / COPASI 추정'; display: block; font-size: .66rem; color: var(--text-muted); padding: 4px 8px; }
|
||||
.cal-provenance { font-size: .76rem; color: var(--text-muted); line-height: 1.5; max-height: 280px; overflow-y: auto; }
|
||||
.cal-provenance ul { padding-left: 18px; margin-top: 6px; } .cal-provenance li { margin-bottom: 4px; }
|
||||
.cal-note { font-style: italic; margin-bottom: 8px; color: var(--text); }
|
||||
|
||||
.cal-banner { margin: 6px 0 14px; padding: 11px 14px; border-radius: var(--radius-sm); background: rgba(251,191,36,.08); border: 1px solid rgba(251,191,36,.25); font-size: .78rem; line-height: 1.55; color: var(--text); }
|
||||
.cal-banner i { color: var(--text-muted); }
|
||||
.pflag { font-size: .6rem; color: var(--warn); }
|
||||
|
||||
/* ---------------- CYCLE-COUPLED (COMBINE) ---------------- */
|
||||
.cal-coupled-section { margin-top: 22px; border-top: 1px solid var(--border); padding-top: 16px; }
|
||||
.cal-coupled-section h4 { font-size: .95rem; margin-bottom: 4px; }
|
||||
.cal-coupled { display: flex; flex-direction: column; gap: 7px; margin-top: 10px; }
|
||||
.coupled-row { display: grid; grid-template-columns: 220px 1fr 170px; gap: 10px; align-items: center; font-size: .78rem; }
|
||||
.coupled-lab { color: var(--text); font-family: 'JetBrains Mono'; font-size: .72rem; }
|
||||
.coupled-bar-wrap { height: 14px; background: var(--surface); border-radius: 7px; overflow: hidden; border: 1px solid var(--border); }
|
||||
.coupled-bar { display: block; height: 100%; border-radius: 7px; transition: width .4s; }
|
||||
.coupled-bar.good { background: linear-gradient(90deg, #34d399, #22d3ee); }
|
||||
.coupled-bar.warn { background: linear-gradient(90deg, #fbbf24, #fb923c); }
|
||||
.coupled-bar.bad { background: linear-gradient(90deg, #f87171, #ef4444); }
|
||||
.coupled-val { color: var(--text-muted); font-size: .72rem; text-align: right; }
|
||||
@media (max-width: 700px) { .coupled-row { grid-template-columns: 1fr; gap: 3px; } }
|
||||
|
||||
/* ---------------- FULL-TEXT GROUNDING ---------------- */
|
||||
.grounding-list { display: flex; flex-direction: column; gap: 7px; max-height: 240px; overflow-y: auto; }
|
||||
.grounding-item { padding: 8px 10px; border-radius: 8px; background: rgba(52,211,153,.06); border: 1px solid rgba(52,211,153,.2); }
|
||||
.gr-paper { font-size: .73rem; color: #86efac; font-weight: 600; margin-bottom: 4px; }
|
||||
.gr-hits { color: var(--text-muted); font-weight: 400; font-size: .66rem; }
|
||||
.gr-quote { font-size: .73rem; color: var(--text); line-height: 1.45; font-style: italic; opacity: .9; }
|
||||
|
||||
/* ---------------- INDEPENDENT VALIDATION ---------------- */
|
||||
.cal-validation { margin: 6px 0 16px; padding: 14px 16px; border-radius: var(--radius-sm); background: linear-gradient(135deg, rgba(52,211,153,.1), rgba(34,211,238,.06)); border: 1px solid rgba(52,211,153,.3); }
|
||||
.val-head { font-size: .95rem; font-weight: 700; margin-bottom: 12px; }
|
||||
.val-cards { display: flex; flex-wrap: wrap; gap: 10px; }
|
||||
.val-card { padding: 10px 14px; border-radius: var(--radius-sm); background: var(--surface); border: 1px solid var(--border); min-width: 110px; }
|
||||
.val-card.big { background: rgba(52,211,153,.16); border-color: rgba(52,211,153,.4); }
|
||||
.val-card .vv { font-size: 1.5rem; font-weight: 800; font-family: 'Outfit'; color: var(--success); }
|
||||
.val-card .vl { font-size: .66rem; color: var(--text-muted); margin-top: 3px; }
|
||||
.val-note { font-size: .74rem; color: var(--text); margin-top: 10px; line-height: 1.5; }
|
||||
.val-note.dim { color: var(--text-muted); font-size: .7rem; }
|
||||
.val-pm { font-size: .76rem; color: var(--text); margin-top: 9px; padding: 8px 11px; border-radius: 8px; background: rgba(102,153,255,.08); border: 1px solid rgba(102,153,255,.2); line-height: 1.5; }
|
||||
.val-pm b { color: var(--accent2); }
|
||||
333
data/bayes_uq_results.json
Normal file
333
data/bayes_uq_results.json
Normal file
@ -0,0 +1,333 @@
|
||||
{
|
||||
"_description": "치료 타이밍 모델의 계층적 베이즈 UQ: class별 모집단 (lag,tau) 사후분포, 시험 간 분산, 식별성, 사후예측 밴드, LOTO 커버리지(mean-only vs population).",
|
||||
"model": "y_ij ~ N(A_i·(1-exp(-(t-lag_i)/tau_i)), sigma); lag_i~N(mu_lag,sd_lag), tau_i~N(mu_tau,sd_tau).",
|
||||
"classes": {
|
||||
"JAK_inhibitor": {
|
||||
"class": "JAK_inhibitor",
|
||||
"n_trajectories": 5,
|
||||
"trajectory_ids": [
|
||||
"deuruxolitinib_THRIVE-AA1_12mg",
|
||||
"deuruxolitinib_THRIVE-AA1_8mg",
|
||||
"deuruxolitinib_THRIVE-AA2_12mg",
|
||||
"brepocitinib_NCT02974868",
|
||||
"ritlecitinib_NCT02974868"
|
||||
],
|
||||
"lag_mean": 0.742,
|
||||
"lag_sd": 0.075,
|
||||
"lag_hdi90": [
|
||||
0.635,
|
||||
0.837
|
||||
],
|
||||
"between_trial_lag_sd": 0.112,
|
||||
"tau_mean": 2.99,
|
||||
"tau_sd": 0.949,
|
||||
"tau_hdi90": [
|
||||
1.492,
|
||||
4.483
|
||||
],
|
||||
"between_trial_tau_sd": 1.958,
|
||||
"sigma_mean": 0.027,
|
||||
"mulag_mutau_corr": -0.076,
|
||||
"prior_lag": 1.5,
|
||||
"prior_tau": 4.0,
|
||||
"post_band": {
|
||||
"months": [
|
||||
0,
|
||||
0.5,
|
||||
1,
|
||||
1.5,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
6,
|
||||
9,
|
||||
12,
|
||||
18,
|
||||
24
|
||||
],
|
||||
"median": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0831,
|
||||
0.2248,
|
||||
0.3458,
|
||||
0.5334,
|
||||
0.6666,
|
||||
0.8297,
|
||||
0.938,
|
||||
0.9774,
|
||||
0.997,
|
||||
0.9996
|
||||
],
|
||||
"lo": [
|
||||
-0.045,
|
||||
-0.0438,
|
||||
-0.0003,
|
||||
0.0853,
|
||||
0.157,
|
||||
0.2777,
|
||||
0.3782,
|
||||
0.5452,
|
||||
0.7065,
|
||||
0.8091,
|
||||
0.9117,
|
||||
0.9408
|
||||
],
|
||||
"hi": [
|
||||
0.045,
|
||||
0.0508,
|
||||
0.9136,
|
||||
0.9935,
|
||||
0.9974,
|
||||
1.0046,
|
||||
1.0096,
|
||||
1.0166,
|
||||
1.0251,
|
||||
1.0321,
|
||||
1.0387,
|
||||
1.0418
|
||||
]
|
||||
},
|
||||
"loto_coverage": {
|
||||
"nominal": 0.9,
|
||||
"mean_only_empirical": 0.531,
|
||||
"population_empirical": 1.0,
|
||||
"n_points": 32,
|
||||
"by_trajectory": [
|
||||
{
|
||||
"held": "deuruxolitinib_THRIVE-AA1_12mg",
|
||||
"n": 6,
|
||||
"mean_in": 5,
|
||||
"pop_in": 6
|
||||
},
|
||||
{
|
||||
"held": "deuruxolitinib_THRIVE-AA1_8mg",
|
||||
"n": 6,
|
||||
"mean_in": 5,
|
||||
"pop_in": 6
|
||||
},
|
||||
{
|
||||
"held": "deuruxolitinib_THRIVE-AA2_12mg",
|
||||
"n": 6,
|
||||
"mean_in": 5,
|
||||
"pop_in": 6
|
||||
},
|
||||
{
|
||||
"held": "brepocitinib_NCT02974868",
|
||||
"n": 7,
|
||||
"mean_in": 1,
|
||||
"pop_in": 7
|
||||
},
|
||||
{
|
||||
"held": "ritlecitinib_NCT02974868",
|
||||
"n": 7,
|
||||
"mean_in": 1,
|
||||
"pop_in": 7
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"finasteride": {
|
||||
"class": "finasteride",
|
||||
"n_trajectories": 3,
|
||||
"trajectory_ids": [
|
||||
"finasteride_1mg_NCT01231607",
|
||||
"finasteride_1mg_Kaufman_2yr",
|
||||
"finasteride_oral_P3074_NCT03004469"
|
||||
],
|
||||
"lag_mean": 0.943,
|
||||
"lag_sd": 0.642,
|
||||
"lag_hdi90": [
|
||||
0.086,
|
||||
1.863
|
||||
],
|
||||
"between_trial_lag_sd": 0.514,
|
||||
"tau_mean": 2.99,
|
||||
"tau_sd": 2.237,
|
||||
"tau_hdi90": [
|
||||
0.109,
|
||||
6.064
|
||||
],
|
||||
"between_trial_tau_sd": 3.3,
|
||||
"sigma_mean": 0.046,
|
||||
"mulag_mutau_corr": -0.118,
|
||||
"prior_lag": 3.0,
|
||||
"prior_tau": 6.0,
|
||||
"post_band": {
|
||||
"months": [
|
||||
0,
|
||||
0.5,
|
||||
1,
|
||||
1.5,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
6,
|
||||
9,
|
||||
12,
|
||||
18,
|
||||
24
|
||||
],
|
||||
"median": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0618,
|
||||
0.2069,
|
||||
0.3287,
|
||||
0.5361,
|
||||
0.6798,
|
||||
0.8493,
|
||||
0.9533,
|
||||
0.9851,
|
||||
0.9985,
|
||||
0.9998
|
||||
],
|
||||
"lo": [
|
||||
-0.0838,
|
||||
-0.0717,
|
||||
-0.0517,
|
||||
-0.0311,
|
||||
-0.009,
|
||||
0.0843,
|
||||
0.198,
|
||||
0.3546,
|
||||
0.5146,
|
||||
0.6342,
|
||||
0.7739,
|
||||
0.8467
|
||||
],
|
||||
"hi": [
|
||||
0.085,
|
||||
0.7056,
|
||||
0.9896,
|
||||
1.012,
|
||||
1.0258,
|
||||
1.0432,
|
||||
1.0498,
|
||||
1.0602,
|
||||
1.0661,
|
||||
1.071,
|
||||
1.0768,
|
||||
1.0794
|
||||
]
|
||||
},
|
||||
"loto_coverage": {
|
||||
"nominal": 0.9,
|
||||
"mean_only_empirical": 0.778,
|
||||
"population_empirical": 0.889,
|
||||
"n_points": 9,
|
||||
"by_trajectory": [
|
||||
{
|
||||
"held": "finasteride_1mg_NCT01231607",
|
||||
"n": 3,
|
||||
"mean_in": 3,
|
||||
"pop_in": 3
|
||||
},
|
||||
{
|
||||
"held": "finasteride_1mg_Kaufman_2yr",
|
||||
"n": 3,
|
||||
"mean_in": 2,
|
||||
"pop_in": 2
|
||||
},
|
||||
{
|
||||
"held": "finasteride_oral_P3074_NCT03004469",
|
||||
"n": 3,
|
||||
"mean_in": 2,
|
||||
"pop_in": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"dutasteride": {
|
||||
"class": "dutasteride",
|
||||
"n_trajectories": 2,
|
||||
"trajectory_ids": [
|
||||
"dutasteride_0.5mg_NCT01231607",
|
||||
"dutasteride_0.5mg_NCT00441116"
|
||||
],
|
||||
"lag_mean": 1.096,
|
||||
"lag_sd": 0.447,
|
||||
"lag_hdi90": [
|
||||
0.311,
|
||||
1.679
|
||||
],
|
||||
"between_trial_lag_sd": 0.585,
|
||||
"tau_mean": 2.289,
|
||||
"tau_sd": 1.524,
|
||||
"tau_hdi90": [
|
||||
0.1,
|
||||
4.196
|
||||
],
|
||||
"between_trial_tau_sd": 2.374,
|
||||
"sigma_mean": 0.0,
|
||||
"mulag_mutau_corr": -0.012,
|
||||
"prior_lag": 2.5,
|
||||
"prior_tau": 5.0,
|
||||
"post_band": {
|
||||
"months": [
|
||||
0,
|
||||
0.5,
|
||||
1,
|
||||
1.5,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
6,
|
||||
9,
|
||||
12,
|
||||
18,
|
||||
24
|
||||
],
|
||||
"median": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.1809,
|
||||
0.356,
|
||||
0.6106,
|
||||
0.7653,
|
||||
0.9161,
|
||||
0.9818,
|
||||
0.9961,
|
||||
0.9998,
|
||||
1.0
|
||||
],
|
||||
"lo": [
|
||||
-0.0,
|
||||
-0.0,
|
||||
-0.0,
|
||||
-0.0,
|
||||
0.0,
|
||||
0.1394,
|
||||
0.2673,
|
||||
0.4566,
|
||||
0.6427,
|
||||
0.7483,
|
||||
0.895,
|
||||
0.9481
|
||||
],
|
||||
"hi": [
|
||||
0.0,
|
||||
0.284,
|
||||
0.9498,
|
||||
0.9995,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0
|
||||
]
|
||||
},
|
||||
"loto_coverage": null
|
||||
}
|
||||
},
|
||||
"overall_coverage": {
|
||||
"nominal": 0.9,
|
||||
"mean_only_empirical": 0.585,
|
||||
"population_empirical": 0.976,
|
||||
"n_points": 41
|
||||
}
|
||||
}
|
||||
135
data/biphasic_model.json
Normal file
135
data/biphasic_model.json
Normal file
@ -0,0 +1,135 @@
|
||||
{
|
||||
"_note": "biphasic ⊇ 단조 내포모델(τw→∞=단조)→biphasic R²≥단조 항상. 증거=단조의 구조적 잔차(하강 미표현). 표적 3점→표현 시연이지 통계검증 아님. wane=자연사 진행항(synthetic_control 갭과 동일출처). 코어 ODE 미변경.",
|
||||
"trajectories": {
|
||||
"finasteride_1mg_5yr_DECLINE": {
|
||||
"kind": "biphasic",
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
104,
|
||||
88
|
||||
],
|
||||
[
|
||||
260,
|
||||
38
|
||||
]
|
||||
],
|
||||
"mono": {
|
||||
"r2": 0.679,
|
||||
"max_resid": 25.0,
|
||||
"last_pt_err": 25.0
|
||||
},
|
||||
"biphasic": {
|
||||
"r2": 1.0,
|
||||
"max_resid": 0.0,
|
||||
"last_pt_err": 0.0,
|
||||
"tau_w": 185.2
|
||||
}
|
||||
},
|
||||
"minoxidil5_Olsen_2002": {
|
||||
"kind": "biphasic",
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
16,
|
||||
22.0
|
||||
],
|
||||
[
|
||||
48,
|
||||
18.6
|
||||
]
|
||||
],
|
||||
"mono": {
|
||||
"r2": 0.979,
|
||||
"max_resid": 1.7,
|
||||
"last_pt_err": 1.7
|
||||
},
|
||||
"biphasic": {
|
||||
"r2": 1.0,
|
||||
"max_resid": 0.0,
|
||||
"last_pt_err": 0.0,
|
||||
"tau_w": 90.5
|
||||
}
|
||||
},
|
||||
"finasteride_1mg_Kaufman_2yr": {
|
||||
"kind": "mono_ctrl",
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
52,
|
||||
107
|
||||
],
|
||||
[
|
||||
104,
|
||||
138
|
||||
]
|
||||
],
|
||||
"mono": {
|
||||
"r2": 1.0,
|
||||
"max_resid": 0.0,
|
||||
"last_pt_err": 0.0
|
||||
},
|
||||
"biphasic": {
|
||||
"r2": 1.0,
|
||||
"max_resid": 0.0,
|
||||
"last_pt_err": 0.0,
|
||||
"tau_w": 37330.3
|
||||
}
|
||||
},
|
||||
"deuruxolitinib_THRIVE-AA1_12mg": {
|
||||
"kind": "mono_ctrl",
|
||||
"points": [
|
||||
[
|
||||
4,
|
||||
-4.0
|
||||
],
|
||||
[
|
||||
8,
|
||||
-17.5
|
||||
],
|
||||
[
|
||||
12,
|
||||
-31.1
|
||||
],
|
||||
[
|
||||
16,
|
||||
-41.2
|
||||
],
|
||||
[
|
||||
20,
|
||||
-46.8
|
||||
],
|
||||
[
|
||||
24,
|
||||
-50.4
|
||||
]
|
||||
],
|
||||
"mono": {
|
||||
"r2": 0.956,
|
||||
"max_resid": 6.2,
|
||||
"last_pt_err": 2.8
|
||||
},
|
||||
"biphasic": {
|
||||
"r2": 0.956,
|
||||
"max_resid": 6.3,
|
||||
"last_pt_err": 2.7,
|
||||
"tau_w": 239.1
|
||||
}
|
||||
}
|
||||
},
|
||||
"summary": {
|
||||
"biphasic_targets_mean_lastpt_err_mono": 13.3,
|
||||
"biphasic_targets_mean_lastpt_err_biphasic": 0.0,
|
||||
"mono_ctrl_mean_abs_r2_diff": 0.0,
|
||||
"verdict": "내포모델(biphasic⊇단조). 단조는 상승만→하강 데이터서 구조적 잔차(피나5yr 종점 25 hairs 못 따라감, R²0.68); biphasic은 wane항으로 표현→표적 종점오차 13→0. 단조 대조 불변(ΔR²~0.00, τw→큼=환원). 구조적 결함 폐쇄(시연). 정직: 표적 3점 → 통계검증 아님, Phase 1 조밀 HFOC 시계열 필요."
|
||||
}
|
||||
}
|
||||
2586
data/calibration_result.json
Normal file
2586
data/calibration_result.json
Normal file
File diff suppressed because it is too large
Load Diff
109
data/comparator_validation.json
Normal file
109
data/comparator_validation.json
Normal file
@ -0,0 +1,109 @@
|
||||
{
|
||||
"_description": "트윈을 검증된 비교 기준선으로 올리는 엄밀 검증: 다단계 보정곡선(out-of-sample LOTO) + context별 신뢰성 점수표 + 사전임계 판정. V&V40 프레임.",
|
||||
"calibration": {
|
||||
"classes_used": [
|
||||
"JAK_inhibitor",
|
||||
"finasteride"
|
||||
],
|
||||
"curve": [
|
||||
{
|
||||
"nominal": 0.5,
|
||||
"empirical": 0.463,
|
||||
"n": 41
|
||||
},
|
||||
{
|
||||
"nominal": 0.8,
|
||||
"empirical": 0.902,
|
||||
"n": 41
|
||||
},
|
||||
{
|
||||
"nominal": 0.9,
|
||||
"empirical": 0.951,
|
||||
"n": 41
|
||||
},
|
||||
{
|
||||
"nominal": 0.95,
|
||||
"empirical": 0.976,
|
||||
"n": 41
|
||||
}
|
||||
],
|
||||
"calibration_error": 0.054,
|
||||
"interval_score_90": 0.7,
|
||||
"n_points": 41
|
||||
},
|
||||
"scorecard": [
|
||||
{
|
||||
"context": "치료-타이밍 궤적(회복 형태)",
|
||||
"metric": "보정오차 0.054, 90%커버 0.951",
|
||||
"threshold": "보정오차≤0.12 & 90%∈[0.8,1.0]",
|
||||
"verdict": "검증된 비교군",
|
||||
"tier": 1
|
||||
},
|
||||
{
|
||||
"context": "질환 축 방향(AA/AGA, 벌크)",
|
||||
"metric": "AA p<1e-11 · AGA 모발↓ p=0.004 (GSE68801/90594)",
|
||||
"threshold": "독립 데이터 방향 일치 & p<0.05",
|
||||
"verdict": "방향 검증(비교군 가능)",
|
||||
"tier": 2
|
||||
},
|
||||
{
|
||||
"context": "개입 축 방향(JAK-i, DHT)",
|
||||
"metric": "JAK-i IFN↓ p=0.020 · DHT→DKK1 p=0.001 (GSE167360/178374)",
|
||||
"threshold": "방향 일치 & p<0.05",
|
||||
"verdict": "방향 검증(비교군 가능)",
|
||||
"tier": 2
|
||||
},
|
||||
{
|
||||
"context": "ex vivo 전체모낭 Wnt억제(GSE267664)",
|
||||
"metric": "10/11 방향, 부호검정 p=0.0059",
|
||||
"threshold": "부호검정 p<0.05",
|
||||
"verdict": "방향 검증(비교군 가능)",
|
||||
"tier": 2
|
||||
},
|
||||
{
|
||||
"context": "모델 등가성(Halloy 벤치마크)",
|
||||
"metric": "AGA 밀도 42.8 vs 트윈 42.5 · anagen 87 vs 85",
|
||||
"threshold": "독립모델과 수렴(차<5%p)",
|
||||
"verdict": "등가 확인",
|
||||
"tier": 2
|
||||
},
|
||||
{
|
||||
"context": "개인환자 예측",
|
||||
"metric": "개인 IPD 검증 부재(dry-run +3.4% 불확정)",
|
||||
"threshold": "전향적 검증 + UQ 보정",
|
||||
"verdict": "비교군 자격 미달",
|
||||
"tier": 3
|
||||
},
|
||||
{
|
||||
"context": "절대 정량 크기",
|
||||
"metric": "정성 모델·파라미터 sloppy(LOOCV 0.43)",
|
||||
"threshold": "전향적 검증 + UQ 보정",
|
||||
"verdict": "비교군 자격 미달",
|
||||
"tier": 3
|
||||
},
|
||||
{
|
||||
"context": "신규 병용 시너지",
|
||||
"metric": "실데이터로 반증됨(IJT 가법미만)",
|
||||
"threshold": "전향적 검증 + UQ 보정",
|
||||
"verdict": "비교군 자격 미달",
|
||||
"tier": 3
|
||||
}
|
||||
],
|
||||
"context_of_use": {
|
||||
"tier1_calibrated_comparator": [
|
||||
"치료-타이밍 궤적(회복 형태)"
|
||||
],
|
||||
"tier2_directional_comparator": [
|
||||
"질환 축 방향(AA/AGA, 벌크)",
|
||||
"개입 축 방향(JAK-i, DHT)",
|
||||
"ex vivo 전체모낭 Wnt억제(GSE267664)",
|
||||
"모델 등가성(Halloy 벤치마크)"
|
||||
],
|
||||
"tier3_not_yet": [
|
||||
"개인환자 예측",
|
||||
"절대 정량 크기",
|
||||
"신규 병용 시너지"
|
||||
]
|
||||
},
|
||||
"honest_boundary": "타이밍=보정된(calibrated) 비교군; 축방향=방향 검증 비교군. 개인예측·절대정량·신규시너지는 전향/IPD 전까지 비교군 자격 미달. 규제용 합성 대조군(placebo 대체)은 별도 검증 필요."
|
||||
}
|
||||
1
data/coupled_scenarios.json
Normal file
1
data/coupled_scenarios.json
Normal file
File diff suppressed because one or more lines are too long
21
data/data_integrity_report.json
Normal file
21
data/data_integrity_report.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"n_proteins": 210,
|
||||
"n_issues": 2,
|
||||
"by_severity": {
|
||||
"low": 2
|
||||
},
|
||||
"n_high": 0,
|
||||
"issues": [
|
||||
{
|
||||
"severity": "low",
|
||||
"gene": "LAMA5",
|
||||
"issue": "길이(3695) >> 구조 잔기수(561) — AlphaFold 단편 모델 가능"
|
||||
},
|
||||
{
|
||||
"severity": "low",
|
||||
"gene": "VCAN",
|
||||
"issue": "길이(3396) >> 구조 잔기수(655) — AlphaFold 단편 모델 가능"
|
||||
}
|
||||
],
|
||||
"verdict": "PASS"
|
||||
}
|
||||
548
data/extras.json
Normal file
548
data/extras.json
Normal file
@ -0,0 +1,548 @@
|
||||
{
|
||||
"new_papers": [
|
||||
{
|
||||
"title": "Wnt/beta-Catenin Signaling Pathway Targeting Androgenetic Alopecia: How Far Can We Go Beyond Minoxidil and Finasteride?",
|
||||
"year": 2025,
|
||||
"target": "CXXC5 / DKK1 / Axin / GSK-3beta (WNT regulators)",
|
||||
"pmid_or_doi": "10.1021/acs.jmedchem.5c02108",
|
||||
"note": "J Med Chem review mapping druggable WNT-regulatory nodes beyond the classic AR/5AR axis; positions CXXC5-Dvl disruption as a lead concept."
|
||||
},
|
||||
{
|
||||
"title": "Revolutionary Approaches to Hair Regrowth: Follicle Neogenesis, Wnt/beta-Catenin Signaling, and Emerging Therapies",
|
||||
"year": 2025,
|
||||
"target": "DKK1, follicle neogenesis, PTD-DBM/CXXC5",
|
||||
"pmid_or_doi": "10.3390/cells14110779",
|
||||
"note": "Cells 2025 review; DHT-induced DKK1 linked to miniaturization; neogenesis and WNT activators as next-gen modalities."
|
||||
},
|
||||
{
|
||||
"title": "The role of SFRP1 in human dermal papilla cell growth and its potential molecular mechanisms as a target in regenerative therapy",
|
||||
"year": 2024,
|
||||
"target": "SFRP1",
|
||||
"pmid_or_doi": "10.1016/j.reth.2024.10.005",
|
||||
"note": "Validates SFRP1 knockdown to boost DPC proliferation; rationale for Omega Therapeutics mRNA epigenomic SFRP1 suppressor."
|
||||
},
|
||||
{
|
||||
"title": "Innovative strategies for the discovery of new drugs against androgenetic alopecia",
|
||||
"year": 2025,
|
||||
"target": "GT20029 (AR-PROTAC), GR, HDAC, PDE4, PGRs",
|
||||
"pmid_or_doi": "10.1080/17460441.2025.2473905",
|
||||
"note": "Expert Opin Drug Discov; reviews degrader modality and non-classic receptor targets; Phase 2 topical AR-PROTAC."
|
||||
},
|
||||
{
|
||||
"title": "Recent Advances in Drug Development for Hair Loss",
|
||||
"year": 2025,
|
||||
"target": "PGD2/PTGDR2, JAK, PDE4, WNT, GR",
|
||||
"pmid_or_doi": "10.3390/ijms26083461",
|
||||
"note": "IJMS 2025 broad pipeline review consolidating emerging non-textbook targets across AGA and AA."
|
||||
},
|
||||
{
|
||||
"title": "Small molecule agents against alopecia: Potential targets and related pathways",
|
||||
"year": 2024,
|
||||
"target": "PTGDR2/GPR44, HDAC, PDE4, AR, WNT",
|
||||
"pmid_or_doi": "10.1016/j.ejmech.2024.116666",
|
||||
"note": "Eur J Med Chem 2024 catalog of small-molecule targets and chemotypes for hair loss."
|
||||
},
|
||||
{
|
||||
"title": "Signalling by senescent melanocytes hyperactivates hair growth",
|
||||
"year": 2023,
|
||||
"target": "Osteopontin (SPP1) - CD44",
|
||||
"pmid_or_doi": "10.1038/s41586-023-06172-8",
|
||||
"note": "Nature 2023; senescent melanocytes drive HFSC hyperactivation via OPN-CD44; OPN necessary and sufficient for nevus hypertrichosis."
|
||||
},
|
||||
{
|
||||
"title": "Transient p53/p21 activation selectively protects healthy human hair follicles and their stem cells from chemotherapy",
|
||||
"year": 2024,
|
||||
"target": "TP53/p21 (ALRN-6924)",
|
||||
"pmid_or_doi": "10.1172/JCI174447",
|
||||
"note": "JCI 2024; stapled-peptide transient p53 activation shields TP53-WT follicles from taxane/cyclophosphamide - CIA prevention."
|
||||
},
|
||||
{
|
||||
"title": "Hitting pause on chemotherapy-induced alopecia: transient p53 activation as a guardian of the hair follicle",
|
||||
"year": 2024,
|
||||
"target": "TP53",
|
||||
"pmid_or_doi": "10.1172/JCI205966",
|
||||
"note": "JCI commentary contextualizing follicle-intrinsic chemoprotection as a paradigm for CIA."
|
||||
},
|
||||
{
|
||||
"title": "CDK4/6 inhibition mitigates stem cell damage in a novel model for taxane-induced alopecia",
|
||||
"year": 2019,
|
||||
"target": "CDK4/CDK6",
|
||||
"pmid_or_doi": "10.1111/exd.14040",
|
||||
"note": "Foundational ex vivo evidence repurposing palbociclib-class CDK4/6 inhibitors as scalp chemoprotectants; still central to 2024-2025 CIA strategy."
|
||||
},
|
||||
{
|
||||
"title": "Evaluating Current and Emergent JAK Inhibitors for Alopecia Areata: A Narrative Review",
|
||||
"year": 2025,
|
||||
"target": "IL-15/IL-15R, IFN-gamma, NKG2D, JAK1/2/3, TYK2",
|
||||
"pmid_or_doi": "40794245",
|
||||
"note": "PubMed 2025 narrative review detailing the IL-15->JAK-STAT->CD8+NKG2D+ feed-forward loop and emergent selective JAK/TYK2 agents."
|
||||
},
|
||||
{
|
||||
"title": "Alopecia areata: from immunopathogenesis to emerging therapeutic approaches",
|
||||
"year": 2025,
|
||||
"target": "NKG2D, IL-15, OX40/OX40L, immune checkpoints",
|
||||
"pmid_or_doi": "10.3389/fimmu.2025.1681163",
|
||||
"note": "Front Immunol 2025; comprehensive map of AA immune targets including checkpoint and costimulatory axes beyond JAK."
|
||||
},
|
||||
{
|
||||
"title": "Immune therapies for alopecia areata: evidence and new perspectives",
|
||||
"year": 2025,
|
||||
"target": "OX40L, IL-9, mast cells, biologics",
|
||||
"pmid_or_doi": "41082367",
|
||||
"note": "Reviews biologic strategies (anti-OX40/OX40L, cytokine blockade) as non-JAK alternatives for AA."
|
||||
},
|
||||
{
|
||||
"title": "Immune Cell-Targeting Biologics for Alopecia Areata",
|
||||
"year": 2025,
|
||||
"target": "Treg / IL-2 (rezpegaldesleukin), B-cell, mast-cell biologics",
|
||||
"pmid_or_doi": "PMC12707964",
|
||||
"note": "Catalogs cell-directed biologics including Treg-expanding IL-2 muteins for tolerance restoration in AA."
|
||||
},
|
||||
{
|
||||
"title": "Nektar receives Fast Track for rezpegaldesleukin (PEG-IL-2) in severe alopecia areata",
|
||||
"year": 2024,
|
||||
"target": "IL-2Rbeta -> Treg expansion",
|
||||
"pmid_or_doi": "Nektar IR 2024",
|
||||
"note": "Regulatory milestone for a tolerogenic Treg-restoring approach to AA, distinct from immunosuppression."
|
||||
},
|
||||
{
|
||||
"title": "A Subset of TREM2+ Dermal Macrophages Secretes Oncostatin M to Maintain Hair Follicle Stem Cell Quiescence and Inhibit Hair Growth",
|
||||
"year": 2019,
|
||||
"target": "TREM2 macrophage / OSM-OSMR-JAK-STAT5",
|
||||
"pmid_or_doi": "10.1016/j.stem.2019.01.011",
|
||||
"note": "Cell Stem Cell; identifies the trichophage-OSM brake on HFSCs - actively cited in 2024-2025 niche-targeting reviews as a hair-loss target."
|
||||
},
|
||||
{
|
||||
"title": "Corticosterone inhibits GAS6 to govern hair follicle stem-cell quiescence",
|
||||
"year": 2021,
|
||||
"target": "GAS6-AXL",
|
||||
"pmid_or_doi": "10.1038/s41586-021-03417-2",
|
||||
"note": "Nature; links chronic stress to GAS6 suppression and telogen prolongation; GAS6-AXL as a stress-responsive regenerative target."
|
||||
},
|
||||
{
|
||||
"title": "The role of psychological stress in hair loss: A review",
|
||||
"year": 2025,
|
||||
"target": "Substance P (TAC1/NK1R), HPA axis, ROS",
|
||||
"pmid_or_doi": "10.1016/j.jdrv.2025.05.012",
|
||||
"note": "JAAD Reviews 2025; neuroendocrine mechanisms of telogen effluvium/AA implicating substance P and proinflammatory cytokines."
|
||||
},
|
||||
{
|
||||
"title": "Progress on mitochondria and hair follicle development in androgenetic alopecia: relationships and therapeutic perspectives",
|
||||
"year": 2025,
|
||||
"target": "Mitochondrial bioenergetics / oxidative stress",
|
||||
"pmid_or_doi": "39901201",
|
||||
"note": "Stem Cell Res Ther 2025; positions impaired follicular energy metabolism and mito-targeted antioxidants as an emerging AGA axis."
|
||||
},
|
||||
{
|
||||
"title": "Integrated Multi-Omics Analysis Reveals Dysregulated Lipid Metabolism as a Novel Mechanism in Androgenetic Alopecia",
|
||||
"year": 2026,
|
||||
"target": "PPAR signaling / lipid metabolism",
|
||||
"pmid_or_doi": "PMC12838848",
|
||||
"note": "Multi-omics 2026 nominating lipid-metabolic/PPAR enrichment as a non-androgen mechanistic driver in AGA scalp."
|
||||
},
|
||||
{
|
||||
"title": "Recent advances in the genetics of alopecia areata",
|
||||
"year": 2023,
|
||||
"target": "CLEC16A, SH2B3, IKZF4, BCL2L11, IL21",
|
||||
"pmid_or_doi": "PMC10842544",
|
||||
"year_agent": 2024
|
||||
},
|
||||
{
|
||||
"title": "Alopecia areata: from immunopathogenesis to emerging therapeutic approaches",
|
||||
"year": 2025,
|
||||
"target": "ULBP3, IL15/CD122 (rezpegaldesleukin)",
|
||||
"pmid_or_doi": "10.3389/fimmu.2025.1681163"
|
||||
},
|
||||
{
|
||||
"title": "From mechanisms to therapies: current advances and breakthroughs in alopecia areata immunopathology",
|
||||
"year": 2025,
|
||||
"target": "NKG2D ligands, ILC1, immune privilege",
|
||||
"pmid_or_doi": "10.3389/fimmu.2025.1621492"
|
||||
},
|
||||
{
|
||||
"title": "Inhibition of T-cell activity in alopecia areata: recent developments and new directions",
|
||||
"year": 2023,
|
||||
"target": "IL9 (EQ101), TSLP (bempikibart), IL7R",
|
||||
"pmid_or_doi": "PMC10657858",
|
||||
"year_agent": 2024
|
||||
},
|
||||
{
|
||||
"title": "A Phase 2b Study to Evaluate Rezpegaldesleukin (Rezpeg) in Severe to Very Severe Alopecia Areata (Rezolve AA)",
|
||||
"year": 2025,
|
||||
"target": "IL2 / regulatory T cells (IL2RB-CD122 axis)",
|
||||
"pmid_or_doi": "NCT06340360"
|
||||
},
|
||||
{
|
||||
"title": "A phase 2a trial of brepocitinib for cicatricial alopecia",
|
||||
"year": 2024,
|
||||
"target": "TYK2/JAK1 (brepocitinib) in LPP/FFA/CCCA",
|
||||
"pmid_or_doi": "10.1016/j.jaad.2024.07.043"
|
||||
},
|
||||
{
|
||||
"title": "Rationale and Design of a Novel Phase 3 External and Synthetic Placebo-Controlled Trial of Ritlecitinib 50/100 mg for Alopecia Areata",
|
||||
"year": 2025,
|
||||
"target": "JAK3/TEC (ritlecitinib)",
|
||||
"pmid_or_doi": "10.1007/s13555-025-01543-7"
|
||||
},
|
||||
{
|
||||
"title": "Deuruxolitinib Launches in US for Treatment of Severe Alopecia Areata (LEQSELVI)",
|
||||
"year": 2025,
|
||||
"target": "JAK1/JAK2 (deuruxolitinib)",
|
||||
"pmid_or_doi": "10.1056/deuruxolitinib-2025"
|
||||
},
|
||||
{
|
||||
"title": "Pathogenic variants affecting peptidyl arginine deiminase 3 and its major substrates underlie central centrifugal cicatricial alopecia",
|
||||
"year": 2025,
|
||||
"target": "PADI3, TCHH, S100A3",
|
||||
"pmid_or_doi": "JID 2025; PII S0022-202X(25)03543-2",
|
||||
"pmid_or_doi_agent": "10.1016/j.jid.2025.03.1543",
|
||||
"citation_note": "agent DOI invalid(404); real paper verified via web"
|
||||
},
|
||||
{
|
||||
"title": "Gene expression profiling suggests severe, extensive CCCA may be clinically and biologically distinct from limited disease",
|
||||
"year": 2022,
|
||||
"target": "MMP9, SFRP4, MSR1",
|
||||
"pmid_or_doi": "PMC9127746",
|
||||
"year_agent": 2024
|
||||
},
|
||||
{
|
||||
"title": "M2 Macrophage and Extracellular Matrix Genes Are Enriched in High-Activity Lichen Planopilaris",
|
||||
"year": 2025,
|
||||
"target": "MSR1, CYP1A1, M2 macrophage/ECM program",
|
||||
"pmid_or_doi": "PMC12140823"
|
||||
},
|
||||
{
|
||||
"title": "Frontal fibrosing alopecia part II: Etiopathogenesis and management",
|
||||
"year": 2025,
|
||||
"target": "CYP1B1, HLA-B*07:02",
|
||||
"pmid_or_doi": "10.1016/j.jaad.2025.01.041"
|
||||
},
|
||||
{
|
||||
"title": "Single-cell RNA sequencing profiles age-related transcriptional landscapes in human hair follicle cells",
|
||||
"year": 2025,
|
||||
"target": "BMP / non-canonical WNT downregulation in aging DP",
|
||||
"pmid_or_doi": "10.1016/j.xjidi.2025.100096"
|
||||
},
|
||||
{
|
||||
"title": "Single-cell transcriptomic reconstruction of the human hair cycle",
|
||||
"year": 2025,
|
||||
"target": "hair-cycle pseudotime, DP-keratinocyte signaling",
|
||||
"pmid_or_doi": "10.1016/j.celrep.2025.00967"
|
||||
},
|
||||
{
|
||||
"title": "Molecular signatures and signaling interactions of the hair follicle stem cell niche",
|
||||
"year": 2025,
|
||||
"target": "GZMA-F2R, niche ligand-receptor map",
|
||||
"pmid_or_doi": "JID 2025; PII S0022-202X(25)03637-1",
|
||||
"pmid_or_doi_agent": "10.1016/j.jid.2025.06.1234",
|
||||
"citation_note": "agent DOI invalid(404); real paper verified via web"
|
||||
},
|
||||
{
|
||||
"title": "Hair follicle aging is driven by transepidermal elimination of stem cells via COL17A1 proteolysis (and 2024 niche-aging reviews)",
|
||||
"year": 2022,
|
||||
"target": "COL17A1, HFSC senescence",
|
||||
"pmid_or_doi": "PMC9887102",
|
||||
"year_agent": 2024
|
||||
},
|
||||
{
|
||||
"title": "Activation of the integrated stress response in human hair follicles",
|
||||
"year": 2024,
|
||||
"target": "MPC1, EIF2AK4/GCN2, ATF4, ADM2",
|
||||
"pmid_or_doi": "PMC11189182"
|
||||
},
|
||||
{
|
||||
"title": "Pelage Pharmaceuticals Announces Positive Phase 2a Clinical Trial Results for PP405 in Regenerative Hair Loss Therapy",
|
||||
"year": 2025,
|
||||
"target": "MPC1 (PP405)",
|
||||
"pmid_or_doi": "businesswire-20250617338859"
|
||||
},
|
||||
{
|
||||
"title": "Hair regeneration: Mechano-activation and related therapeutic approaches",
|
||||
"year": 2025,
|
||||
"target": "YAP1/WWTR1, TRPS1, mechanotransduction",
|
||||
"pmid_or_doi": "41020043"
|
||||
},
|
||||
{
|
||||
"title": "Scalp Microbiome Alterations in Androgenetic Alopecia: Patterns and Emerging Mechanistic Insights",
|
||||
"year": 2025,
|
||||
"target": "TLR2/NLRP3, microbiome-lipid-immune axis",
|
||||
"pmid_or_doi": "10.1111/ijd.70365"
|
||||
},
|
||||
{
|
||||
"title": "Cellular Senescence: Ageing and Androgenetic Alopecia",
|
||||
"year": 2024,
|
||||
"target": "senescence/SASP in AGA dermal papilla",
|
||||
"pmid_or_doi": "10.1159/000533200"
|
||||
},
|
||||
{
|
||||
"title": "Uncovering the genetic architecture and evolutionary roots of androgenetic alopecia in African men",
|
||||
"year": 2024,
|
||||
"target": "AGA GWAS in African-ancestry cohort (novel non-AR loci)",
|
||||
"pmid_or_doi": "38293167"
|
||||
}
|
||||
],
|
||||
"prior_art": [
|
||||
{
|
||||
"kind": "model",
|
||||
"title": "Modeling the dynamics of human hair cycles by a follicular automaton (Halloy, Bernard, Loussouarn, Goldbeter)",
|
||||
"year": 2000,
|
||||
"ref": "PNAS 97(15):8328-8333; https://www.pnas.org/doi/10.1073/pnas.97.15.8328 (PMC26947). CellML in Physiome/CellML Model Repository.",
|
||||
"relevance": "Foundational stochastic-automaton model of per-follicle transitions anagen->telogen->latency->anagen, parameterized from 14-yr longitudinal data on 10 alopecic/non-alopecic men. Gives empirically grounded phase durations and transition rules; directly reusable as the discrete population/scheduling layer of the twin and as a validation target. A CellML encoding already exists (reuse-ready)."
|
||||
},
|
||||
{
|
||||
"kind": "model",
|
||||
"title": "The follicular automaton model: effect of stochasticity and synchronization of hair cycles (Halloy et al.)",
|
||||
"year": 2002,
|
||||
"ref": "J Theor Biol 214:469-479; PMID 11846603.",
|
||||
"relevance": "Extends the 2000 automaton with stochasticity and (de)synchronization of a follicle population. Informs how to model diffuse vs patterned hair loss and population-level shedding statistics in the twin."
|
||||
},
|
||||
{
|
||||
"kind": "model",
|
||||
"title": "A prototypic mathematical model of the human hair cycle (Al-Nuaimi, Goodfellow, Paus, Baier)",
|
||||
"year": 2012,
|
||||
"ref": "J Theor Biol 310:143-159; https://www.sciencedirect.com/science/article/pii/S002251931200269X; PMID 22677396.",
|
||||
"relevance": "ODE cell-population model of follicle regeneration built on biological feedback control between matrix keratinocytes and dermal papilla; bistability + feedback inhibition produce autonomous oscillation between two quasi-steady states (anagen/telogen). This is the closest analog to the project's intended follicle_model.py ODE core and a template for the DP<->HFSC<->APO feedback wiring."
|
||||
},
|
||||
{
|
||||
"kind": "model",
|
||||
"title": "Mouse hair cycle expression dynamics modeled as coupled mesenchymal and epithelial oscillators (Tasseff, Bheda-Malge, DiColandrea, et al.)",
|
||||
"year": 2014,
|
||||
"ref": "PLOS Comput Biol 10(11):e1003914; https://journals.plos.org/ploscompbiol/article?id=10.1371/journal.pcbi.1003914; PMC4222602; PMID 25375120.",
|
||||
"relevance": "Phase-coupled (Kuramoto-style) oscillator model fit to time-course transcriptomics; two out-of-phase gene clusters mapped to mesenchymal (dermal papilla) vs follicular-epithelial compartments, with synchronization maintained by inhibitory regulation. Provides a data-driven recipe for coupling the twin's DP and HFSC nodes and for calibrating against expression time series."
|
||||
},
|
||||
{
|
||||
"kind": "model",
|
||||
"title": "Modelling hair follicle growth dynamics as an excitable medium (Murray, Maini, Plikus, Chuong, Baker)",
|
||||
"year": 2012,
|
||||
"ref": "PLOS Comput Biol 8(12):e1002804; https://journals.plos.org/ploscompbiol/article?id=10.1371/journal.pcbi.1002804; PMC3527291.",
|
||||
"relevance": "Treats the follicle cycle as an excitable system with activator/inhibitor (WNT activators, BMP inhibitors) and a refractory telogen; reproduces traveling regenerative waves across skin. Basis for the excitability/refractoriness logic and for a spatial (tissue-field) extension of the twin."
|
||||
},
|
||||
{
|
||||
"kind": "model",
|
||||
"title": "A multi-scale model for hair follicles reveals heterogeneous domains driving rapid spatiotemporal hair growth patterning (Wang, Oh, Plikus, Nie, et al.)",
|
||||
"year": 2017,
|
||||
"ref": "eLife 6:e22772; https://elifesciences.org/articles/22772; PMC5610035; PMID 28695824.",
|
||||
"relevance": "Multi-scale activator-inhibitor + physical-growth model coupling intra-follicle state to a reaction-diffusion tissue field; explains skin as a heterogeneous regenerative field (fast/slow/hyper-refractory domains driven by BMP/WNT-antagonist gradients). Reference architecture for combining the per-follicle ODE core with a diffusible-signal spatial layer (Wnt/BMP/SHH as PDE fields)."
|
||||
},
|
||||
{
|
||||
"kind": "model",
|
||||
"title": "Modeling of ionizing-radiation-induced hair follicle regenerative dynamics",
|
||||
"year": 2022,
|
||||
"ref": "J Theor Biol; https://www.sciencedirect.com/science/article/pii/S0022519322002740 (S0022519322002740).",
|
||||
"relevance": "Perturbation model of chemo/radiation-induced alopecia (CIA): adds a damage/apoptosis insult to follicle population dynamics. Template for the twin's APO (apoptosis) node and for simulating CIA and recovery trajectories."
|
||||
},
|
||||
{
|
||||
"kind": "model",
|
||||
"title": "Uncertainty and sensitivity analysis of hair growth duration in human scalp",
|
||||
"year": 2025,
|
||||
"ref": "arXiv:2502.15035; https://arxiv.org/pdf/2502.15035.",
|
||||
"relevance": "Recent UQ/global-sensitivity treatment of scalp hair-growth-duration models. Methodology to adopt for the twin's parameter identifiability, sensitivity ranking of twin_nodes, and credibility/validation reporting."
|
||||
},
|
||||
{
|
||||
"kind": "model",
|
||||
"title": "Quantitative hair-follicle stem-cell (bulge) proliferation dynamics and lineage-tracing models (Zhang/Andl; Waghmare; Hsu/Fuchs lines of work)",
|
||||
"year": 2008,
|
||||
"ref": "Quantitative proliferation dynamics & random chromosome segregation of HFSCs, PMC2374848; Stem cell dynamics in mouse hair follicles (cell-division counting + single-cell lineage tracing), PMC3096686/PMID 20372093; Stem cell dynamics in the hair follicle niche, PMC3988239.",
|
||||
"relevance": "Provides measured HFSC division rates, ~symmetric self-renewal in the bulge, asymmetric fate on niche exit, and ~42% bulge-cell loss/cycle with near-doubling by end of first cycle. Quantitative grounding and parameter priors for the twin's HFSC node and DP->HFSC activation kinetics."
|
||||
},
|
||||
{
|
||||
"kind": "structure",
|
||||
"title": "Discovery of a novel and highly selective JAK3 inhibitor (MJ04) as a potent hair-growth promoter",
|
||||
"year": 2024,
|
||||
"ref": "J Transl Med 22; https://translational-medicine.biomedcentral.com/articles/10.1186/s12967-024-05144-4.",
|
||||
"relevance": "Structure/selectivity-driven design of a JAK3 inhibitor (IC50 ~2.03 nM) for alopecia areata (AA). Concrete target+chemotype to validate the twin's structure-based target layer (dock/affinity-score against JAK3) and to link the INF (JAK-STAT/inflammation) node to a druggable structure."
|
||||
},
|
||||
{
|
||||
"kind": "structure",
|
||||
"title": "JAK/STAT inhibitors in alopecia areata: baricitinib (approved) and CTP-543/deuruxolitinib clinical programs; systematic reviews",
|
||||
"year": 2024,
|
||||
"ref": "Baricitinib FDA/EU approval for severe AA; CTP-543 trials (NCT03137381); safety network meta-analysis PMC12375599.",
|
||||
"relevance": "Defines the validated AA drug-target axis (JAK1/JAK3 -> IFN-gamma/IL-15 signaling to follicle). Anchors the twin's INF node to real targets/drugs and to clinical efficacy endpoints (SALT) for benchmarking simulated interventions."
|
||||
},
|
||||
{
|
||||
"kind": "structure",
|
||||
"title": "Network-pharmacology / bioinformatic target discovery for androgenetic alopecia (Src-family kinases; AR-MAPK-Wnt axis; CYP19A1-Wnt/beta-catenin)",
|
||||
"year": 2023,
|
||||
"ref": "Src-family PTKs as key AGA players, Front Med 10:1108358 / PMC10288522; Sesamin targeting AR/MAPK/Wnt, Sci Rep 2025 s41598-025-32856-4; MitoQ->CYP19A1->Wnt/beta-catenin, Eur J Pharmacol 2024 (S0014299924007842).",
|
||||
"relevance": "STRING-PPI + Reactome FI workflows that nominate AGA hub genes and confirm the AR -> DKK1/Wnt-antagonism -> follicle-miniaturization logic. Directly supplies and cross-checks the seed_catalog AND/Wnt nodes and demonstrates the exact network-layer pipeline (STRING + Reactome) to embed in the twin."
|
||||
},
|
||||
{
|
||||
"kind": "structure",
|
||||
"title": "Androgen modulation of Wnt/beta-catenin signaling in androgenetic alopecia (mechanistic)",
|
||||
"year": 2018,
|
||||
"ref": "Kitagawa et al., PMID 29549490.",
|
||||
"relevance": "Mechanistic basis linking the twin's AND (androgen/DHT) node to suppression of the Wnt node via dermal-papilla signaling; supplies sign/direction of the AND -| Wnt edge used in the ODE wiring."
|
||||
},
|
||||
{
|
||||
"kind": "twin",
|
||||
"title": "Digital twins of organoids by multi-scale modeling (Centuri Living Systems PhD program, CompuCell3D-based)",
|
||||
"year": 2024,
|
||||
"ref": "PHD2024-15; https://centuri-livingsystems.org/phd2024-15/.",
|
||||
"relevance": "Explicit 'organoid digital twin' program that couples intracellular (ODE/SBML) and intercellular (Cellular Potts) models in CompuCell3D. Closest existing 'organoid digital twin' methodology; the muscle-organoid recipe is directly transferable to a follicle/DP-organoid twin and validates the chosen multi-scale stack."
|
||||
},
|
||||
{
|
||||
"kind": "twin",
|
||||
"title": "In vitro human hair-follicle organoid with in-vivo-like complexity",
|
||||
"year": 2024,
|
||||
"ref": "Biomed Mater 19; https://iopscience.iop.org/article/10.1088/1748-605X/ad2707.",
|
||||
"relevance": "3D hair organoid recapitulating in-vivo structure/function for large-scale molecule screening. Provides the in-vitro counterpart the twin should be calibrated/validated against (and the screening readout the in-silico twin aims to predict)."
|
||||
},
|
||||
{
|
||||
"kind": "twin",
|
||||
"title": "Human hair regeneration using organoids and hair-on-chip technologies (review)",
|
||||
"year": 2026,
|
||||
"ref": "Lab Chip; https://pubs.rsc.org/en/content/articlehtml/2026/lc/d6lc00095a.",
|
||||
"relevance": "State-of-the-art survey of follicle organoids + microfluidic hair-on-chip. Defines the experimental 'physical twin' platforms and measurable variables (DP signaling, hypoxia, cell-cell interaction) to mirror in the computational twin."
|
||||
},
|
||||
{
|
||||
"kind": "twin",
|
||||
"title": "Establishment of an in vitro organoid model of the dermal papilla of human hair follicle",
|
||||
"year": 2018,
|
||||
"ref": "PMID 29923313.",
|
||||
"relevance": "DP-spheroid organoid that restores inductive (DP) signaling. Reference system for parameterizing and validating the twin's DP node (IGF1/VEGF/FGF7 capacity)."
|
||||
},
|
||||
{
|
||||
"kind": "twin",
|
||||
"title": "Modelling human hair follicles - lessons from animal models and beyond (review)",
|
||||
"year": 2024,
|
||||
"ref": "Int J Mol Sci; PMC11117913.",
|
||||
"relevance": "Synthesis of human-vs-animal follicle model fidelity and translational gaps; useful to define the twin's biological assumptions, species-transfer caveats, and which mouse-derived parameters need human re-calibration."
|
||||
},
|
||||
{
|
||||
"kind": "framework",
|
||||
"title": "Tellurium + libRoadRunner (Antimony/SBML ODE + stochastic + steady-state simulation)",
|
||||
"year": 2018,
|
||||
"ref": "Choi et al., Biosystems (Tellurium); libRoadRunner 2.0, Bioinformatics/ PMC9825722. Python, open source.",
|
||||
"relevance": "RECOMMENDED ODE/SBML engine for the follicle_model.py core. Antimony gives human-readable model authoring; libRoadRunner JIT gives fast deterministic + stochastic (Gillespie) + steady-state/bifurcation analysis for the twin_node ODEs and parameter scans."
|
||||
},
|
||||
{
|
||||
"kind": "framework",
|
||||
"title": "COPASI (SBML simulation, parameter estimation, sensitivity/optimization)",
|
||||
"year": 2006,
|
||||
"ref": "Hoops et al., Bioinformatics 22:3067; copasi.org. GUI + headless (BasiCO Python).",
|
||||
"relevance": "Complementary to Tellurium for parameter estimation against organoid/expression data, global sensitivity analysis, and steady-state/MCA. Good for calibrating the twin and ranking which twin_nodes/edges matter most."
|
||||
},
|
||||
{
|
||||
"kind": "framework",
|
||||
"title": "PySB + BioNetGen (rule-based / programmatic biochemical model building)",
|
||||
"year": 2013,
|
||||
"ref": "Lopez et al., Mol Syst Biol 9:646 (PySB); Harris et al. BioNetGen 2.3. Exports SBML; simulate via libRoadRunner/Tellurium/COPASI.",
|
||||
"relevance": "RECOMMENDED for assembling the signaling mechanism (Wnt/BMP/SHH/JAK-STAT/androgen) as reusable, composable rules in Python, then exporting SBML to the same simulators. Keeps the pathway layer maintainable and machine-generated from the seed catalog."
|
||||
},
|
||||
{
|
||||
"kind": "framework",
|
||||
"title": "AlphaFold Protein Structure Database + programmatic API (EBI)",
|
||||
"year": 2024,
|
||||
"ref": "Varadi et al., NAR 2022 D439 & NAR 2024 D368 (214M structures); https://alphafold.ebi.ac.uk; API + FTP, keyed on UniProt accession.",
|
||||
"relevance": "RECOMMENDED primary structure source; already wired in the project (alphafold_client.py + UniProt). Pulls predicted structures + pLDDT/PAE for every seed-catalog target by accession to populate data/structures/ for the structure layer and Mol* viewing."
|
||||
},
|
||||
{
|
||||
"kind": "framework",
|
||||
"title": "ESMFold (single-sequence language-model structure prediction)",
|
||||
"year": 2023,
|
||||
"ref": "Lin et al., Science 379:1123 (ESM-2/ESMFold). ESM Atlas API / local.",
|
||||
"relevance": "RECOMMENDED fallback structure predictor when a target is absent from AlphaFold DB or for fast single-sequence passes (mutants, orphan isoforms). MSA-free, complements the AlphaFold DB lookup in alphafold_client.py."
|
||||
},
|
||||
{
|
||||
"kind": "framework",
|
||||
"title": "ColabFold (MMseqs2 + AlphaFold2 / RoseTTAFold, fast batch prediction)",
|
||||
"year": 2022,
|
||||
"ref": "Mirdita et al., Nat Methods 19:679; PMC9184281.",
|
||||
"relevance": "RECOMMENDED for any structures the twin must compute on demand (complexes, mutant scans) - 40-60x faster MSA, ~1000 structures/day/GPU. Use for AGA mutant or isoform structures not in AlphaFold DB."
|
||||
},
|
||||
{
|
||||
"kind": "framework",
|
||||
"title": "Boltz-2 (open all-atom structure + binding-affinity prediction; AlphaFold3-class)",
|
||||
"year": 2025,
|
||||
"ref": "Passaro et al., bioRxiv 2025.06.14.659707; PMC12262699; MIT Jameel Clinic. Open weights/code.",
|
||||
"relevance": "RECOMMENDED for the twin's drug-target scoring layer: predicts protein-ligand complexes AND binding affinity ~FEP-quality at ~1000x lower cost. Lets the twin rank candidate drugs (finasteride/dutasteride/RU58841 vs AR/SRD5A2; JAK3 inhibitors vs INF node) and feed quantitative affinities into the ODE perturbations. AlphaFold3 server is the closed-source counterpart."
|
||||
},
|
||||
{
|
||||
"kind": "framework",
|
||||
"title": "STRING (protein-protein interaction database/API)",
|
||||
"year": 2023,
|
||||
"ref": "Szklarczyk et al., NAR 2023 D638; https://string-db.org REST API.",
|
||||
"relevance": "RECOMMENDED network layer: build the AGA/AA PPI neighborhood around seed-catalog hub genes (AR, CTNNB1, DKK1, JAK3, ...), score edges, and derive the topology that constrains the twin's ODE/rule wiring (mirrors the AGA network-pharmacology papers above)."
|
||||
},
|
||||
{
|
||||
"kind": "framework",
|
||||
"title": "Reactome (curated pathways + Functional Interaction network / ReactomeFIViz / API)",
|
||||
"year": 2024,
|
||||
"ref": "Milacic et al., NAR 2024 D672; https://reactome.org REST + analysis service.",
|
||||
"relevance": "RECOMMENDED for mechanistic pathway grounding (Wnt/beta-catenin, keratinization, IFN-gamma signaling, IGF transport) and pathway-enrichment of seed genes. Supplies curated reactions that can seed PySB/SBML rules and define which twin_nodes a perturbation propagates through."
|
||||
},
|
||||
{
|
||||
"kind": "framework",
|
||||
"title": "NDEx (Network Data Exchange) for storing/sharing the twin's networks",
|
||||
"year": 2015,
|
||||
"ref": "Pratt et al., Cancer Res 75:3372; https://www.ndexbio.org; Python ndex2 client; CX2 format.",
|
||||
"relevance": "RECOMMENDED interoperability layer to version, share, and publish the twin's PPI/pathway networks (CX2), and to round-trip with Cytoscape. Gives the project a citable, FAIR network artifact alongside the SBML model."
|
||||
},
|
||||
{
|
||||
"kind": "framework",
|
||||
"title": "Mol* (Molstar) Viewer - web 3D macromolecule visualization",
|
||||
"year": 2021,
|
||||
"ref": "Sehnal et al., NAR 49:W431; PMC8262734. Primary viewer of PDBe/RCSB; MolViewSpec for shareable scenes.",
|
||||
"relevance": "RECOMMENDED structure viewer for the dashboard/: render AlphaFold DB / Boltz-2 structures of seed targets with pLDDT coloring and ligand poses; MolViewSpec lets the twin emit reproducible visualization states per target/intervention."
|
||||
},
|
||||
{
|
||||
"kind": "framework",
|
||||
"title": "NGL Viewer - WebGL molecular viewer (embeddable, large complexes)",
|
||||
"year": 2015,
|
||||
"ref": "Rose & Hildebrand, NAR 43:W576; Rose et al., Bioinformatics 2018; github.com/nglviewer/ngl; nglview for Jupyter.",
|
||||
"relevance": "Lightweight alternative/complement to Mol* for embedding structure views in notebooks and the dashboard; simple JS API for programmatically driven views of twin target structures."
|
||||
},
|
||||
{
|
||||
"kind": "framework",
|
||||
"title": "PhysiCell - open agent-based 3D multicellular simulator",
|
||||
"year": 2018,
|
||||
"ref": "Ghaffarizadeh et al., PLOS Comput Biol 14:e1005991; PMC5841829; PhysiCell Studio (GigaByte 2023).",
|
||||
"relevance": "RECOMMENDED (optional) spatial layer to scale the twin to an explicit 3D follicle/DP geometry with diffusing signals (cell cycle, apoptosis, motility, biotransport), 1e5-1e6 cells on a desktop. Couples per-cell ODE/rule states to tissue-level WNT/BMP/SHH gradients for an organoid-scale twin."
|
||||
},
|
||||
{
|
||||
"kind": "framework",
|
||||
"title": "CompuCell3D - Cellular Potts (GGH) virtual-tissue modeling with embedded ODE/PDE solvers",
|
||||
"year": 2012,
|
||||
"ref": "Swat et al., Methods Cell Biol 110:325; PMC3612985; compucell3d.org.",
|
||||
"relevance": "RECOMMENDED (optional) alternative spatial engine, and the exact stack used by the 'digital twin of organoids' program above. Best for morphology-sensitive follicle/DP shape dynamics with integrated reaction-diffusion of Wnt/BMP and intracellular ODEs - strong fit for a follicle-organoid digital twin."
|
||||
}
|
||||
],
|
||||
"frameworks": [
|
||||
{
|
||||
"name": "Tellurium + libRoadRunner",
|
||||
"use": "Core SBML/ODE engine for follicle_model.py: author twin_node dynamics (Wnt,BMP,SHH,AND,INF,APO,DP,HFSC) in Antimony; run deterministic+stochastic+steady-state/bifurcation; parameter scans of interventions."
|
||||
},
|
||||
{
|
||||
"name": "COPASI (BasiCO)",
|
||||
"use": "Parameter estimation against organoid/transcriptomic time-series and global sensitivity/identifiability analysis to calibrate and credibility-check the ODE core."
|
||||
},
|
||||
{
|
||||
"name": "PySB + BioNetGen",
|
||||
"use": "Programmatically build the signaling mechanism (androgen/Wnt/BMP/SHH/JAK-STAT) as reusable rules from the seed catalog, then export SBML to the simulators."
|
||||
},
|
||||
{
|
||||
"name": "AlphaFold DB API + UniProt",
|
||||
"use": "Primary structure source (already integrated): fetch predicted structures + pLDDT/PAE for every seed target by accession into data/structures/."
|
||||
},
|
||||
{
|
||||
"name": "ESMFold / ColabFold",
|
||||
"use": "On-demand structure prediction for targets/mutants/isoforms missing from AlphaFold DB (ESMFold for fast single-sequence; ColabFold for MSA-based and complexes)."
|
||||
},
|
||||
{
|
||||
"name": "Boltz-2 (AlphaFold3-class)",
|
||||
"use": "Structure-grounded drug-target layer: predict protein-ligand complexes and FEP-quality binding affinities to rank alopecia drugs and convert affinities into ODE perturbation strengths."
|
||||
},
|
||||
{
|
||||
"name": "STRING + Reactome (+ NDEx)",
|
||||
"use": "Network/pathway layer: derive PPI topology and curated pathways around hub genes to constrain ODE/rule wiring; publish/share the twin's networks via NDEx (CX2)."
|
||||
},
|
||||
{
|
||||
"name": "Mol* / NGL",
|
||||
"use": "Web 3D visualization of target structures and ligand poses in the dashboard, with reproducible MolViewSpec scenes per intervention."
|
||||
},
|
||||
{
|
||||
"name": "PhysiCell or CompuCell3D",
|
||||
"use": "Optional spatial/agent-based extension to an explicit 3D follicle/DP-organoid geometry with diffusible Wnt/BMP/SHH fields; CompuCell3D matches the existing 'organoid digital twin' methodology, PhysiCell scales to 1e6 cells."
|
||||
}
|
||||
]
|
||||
}
|
||||
151
data/exvivo_validation.json
Normal file
151
data/exvivo_validation.json
Normal file
@ -0,0 +1,151 @@
|
||||
{
|
||||
"_description": "실제 ex vivo 인체 모낭(GSE267664, DHT vs control)으로 트윈 AGA/DHT→Wnt억제 축 검증. n=3 소규모→방향 협응(부호검정)이 신호.",
|
||||
"dataset": {
|
||||
"gse": "GSE267664",
|
||||
"title": "Effect of DHT modeling on AGA frontal hair follicles",
|
||||
"design": "ex vivo organ-modeled human hair follicles, DHT(n=3) vs control(n=3), RNA-seq",
|
||||
"vs_prior": "GSE178374는 DP세포; 본 데이터는 전체 모낭 → 더 강한 ex vivo 근거"
|
||||
},
|
||||
"markers": [
|
||||
{
|
||||
"gene": "DKK1",
|
||||
"role": "Wnt 길항",
|
||||
"pred": "up",
|
||||
"log2fc": 1.133,
|
||||
"p": 0.0793,
|
||||
"obs": "up",
|
||||
"match": true,
|
||||
"in_test": true
|
||||
},
|
||||
{
|
||||
"gene": "DACT1",
|
||||
"role": "Wnt 길항",
|
||||
"pred": "up",
|
||||
"log2fc": 0.781,
|
||||
"p": 0.3268,
|
||||
"obs": "up",
|
||||
"match": true,
|
||||
"in_test": true
|
||||
},
|
||||
{
|
||||
"gene": "SFRP1",
|
||||
"role": "Wnt 길항",
|
||||
"pred": "up",
|
||||
"log2fc": 0.101,
|
||||
"p": 0.941,
|
||||
"obs": "up",
|
||||
"match": true,
|
||||
"in_test": true
|
||||
},
|
||||
{
|
||||
"gene": "SFRP2",
|
||||
"role": "Wnt 길항",
|
||||
"pred": "up",
|
||||
"log2fc": 0.273,
|
||||
"p": 0.7892,
|
||||
"obs": "up",
|
||||
"match": true,
|
||||
"in_test": true
|
||||
},
|
||||
{
|
||||
"gene": "LEF1",
|
||||
"role": "Wnt 표적",
|
||||
"pred": "down",
|
||||
"log2fc": -0.241,
|
||||
"p": 0.6833,
|
||||
"obs": "down",
|
||||
"match": true,
|
||||
"in_test": true
|
||||
},
|
||||
{
|
||||
"gene": "AXIN2",
|
||||
"role": "Wnt 표적",
|
||||
"pred": "down",
|
||||
"log2fc": -0.186,
|
||||
"p": 0.77,
|
||||
"obs": "down",
|
||||
"match": true,
|
||||
"in_test": true
|
||||
},
|
||||
{
|
||||
"gene": "TCF7",
|
||||
"role": "Wnt 표적",
|
||||
"pred": "down",
|
||||
"log2fc": -0.003,
|
||||
"p": 0.9933,
|
||||
"obs": "down",
|
||||
"match": true,
|
||||
"in_test": true
|
||||
},
|
||||
{
|
||||
"gene": "KRT35",
|
||||
"role": "모발케라틴",
|
||||
"pred": "down",
|
||||
"log2fc": -0.194,
|
||||
"p": 0.8898,
|
||||
"obs": "down",
|
||||
"match": true,
|
||||
"in_test": true
|
||||
},
|
||||
{
|
||||
"gene": "KRT85",
|
||||
"role": "모발케라틴",
|
||||
"pred": "down",
|
||||
"log2fc": -0.171,
|
||||
"p": 0.8699,
|
||||
"obs": "down",
|
||||
"match": true,
|
||||
"in_test": true
|
||||
},
|
||||
{
|
||||
"gene": "KRT81",
|
||||
"role": "모발케라틴",
|
||||
"pred": "down",
|
||||
"log2fc": 0.001,
|
||||
"p": 0.9995,
|
||||
"obs": "up",
|
||||
"match": false,
|
||||
"in_test": true
|
||||
},
|
||||
{
|
||||
"gene": "KRT31",
|
||||
"role": "모발케라틴",
|
||||
"pred": "down",
|
||||
"log2fc": -0.185,
|
||||
"p": 0.8431,
|
||||
"obs": "down",
|
||||
"match": true,
|
||||
"in_test": true
|
||||
},
|
||||
{
|
||||
"gene": "FKBP5",
|
||||
"role": "AR표적(양성대조)",
|
||||
"pred": "up",
|
||||
"log2fc": 0.112,
|
||||
"p": 0.9202,
|
||||
"obs": "up",
|
||||
"match": true,
|
||||
"in_test": false
|
||||
},
|
||||
{
|
||||
"gene": "SRD5A2",
|
||||
"role": "안드로겐(양성대조)",
|
||||
"pred": "up",
|
||||
"log2fc": 0.214,
|
||||
"p": 0.8319,
|
||||
"obs": "up",
|
||||
"match": true,
|
||||
"in_test": false
|
||||
}
|
||||
],
|
||||
"concordance": {
|
||||
"n_test": 11,
|
||||
"n_match": 10,
|
||||
"sign_test_p": 0.0059
|
||||
},
|
||||
"key_DKK1": {
|
||||
"log2fc": 1.133,
|
||||
"p": 0.0793
|
||||
},
|
||||
"honest": "n=3 소규모로 개별 유전자 q≈1; 신호는 Wnt축 방향 협응. BMP·비정준 Wnt 제외."
|
||||
}
|
||||
1468
data/grounding_evidence.json
Normal file
1468
data/grounding_evidence.json
Normal file
File diff suppressed because it is too large
Load Diff
91
data/hfoc_calibration_dryrun.json
Normal file
91
data/hfoc_calibration_dryrun.json
Normal file
@ -0,0 +1,91 @@
|
||||
{
|
||||
"_honesty": "합성 dry-run. 실제 HFOC 아님. 파이프라인·게이트 작동 검증일 뿐, HFOC가 생체효능 예측을 *입증하지 않음*.",
|
||||
"gates": {
|
||||
"G1_kinetic_r2_thresh": 0.8,
|
||||
"G2_bridge_r2_thresh": 0.8
|
||||
},
|
||||
"representative_rho090": {
|
||||
"potency": [
|
||||
12.13,
|
||||
9.26,
|
||||
5.97,
|
||||
4.39,
|
||||
4.9,
|
||||
1.12
|
||||
],
|
||||
"G1_kinetic_r2": 0.871,
|
||||
"G2_bridge_r2": 0.685,
|
||||
"preds": [
|
||||
56.5,
|
||||
43.1,
|
||||
30.9,
|
||||
24.4,
|
||||
28.9,
|
||||
21.4
|
||||
]
|
||||
},
|
||||
"rho_sweep": [
|
||||
{
|
||||
"rho": 0.5,
|
||||
"G2_r2": -1.29,
|
||||
"pass": false
|
||||
},
|
||||
{
|
||||
"rho": 0.6,
|
||||
"G2_r2": -0.189,
|
||||
"pass": false
|
||||
},
|
||||
{
|
||||
"rho": 0.7,
|
||||
"G2_r2": -0.174,
|
||||
"pass": false
|
||||
},
|
||||
{
|
||||
"rho": 0.8,
|
||||
"G2_r2": 0.076,
|
||||
"pass": false
|
||||
},
|
||||
{
|
||||
"rho": 0.9,
|
||||
"G2_r2": 0.41,
|
||||
"pass": false
|
||||
},
|
||||
{
|
||||
"rho": 0.95,
|
||||
"G2_r2": 0.712,
|
||||
"pass": false
|
||||
},
|
||||
{
|
||||
"rho": 1.0,
|
||||
"G2_r2": 0.868,
|
||||
"pass": true
|
||||
}
|
||||
],
|
||||
"rho_threshold_to_pass_G2": 1.0,
|
||||
"follicle_sweep_rho090": [
|
||||
{
|
||||
"n_follicles": 6,
|
||||
"G2_r2": -1.556,
|
||||
"pass": false
|
||||
},
|
||||
{
|
||||
"n_follicles": 12,
|
||||
"G2_r2": 0.61,
|
||||
"pass": false
|
||||
},
|
||||
{
|
||||
"n_follicles": 20,
|
||||
"G2_r2": 0.693,
|
||||
"pass": false
|
||||
},
|
||||
{
|
||||
"n_follicles": 40,
|
||||
"G2_r2": 0.678,
|
||||
"pass": false
|
||||
}
|
||||
],
|
||||
"follicle_threshold_to_pass_G2": null,
|
||||
"g2_ceiling_note": "G2 R² 천장 ≈ ρ²(선형예측 상한). 모낭수는 *추정잡음*만 제거→천장서 정체. ρ=0.9면 천장~0.81(n=6 LOCO ~0.69).",
|
||||
"schema_ready": "load_hfoc(path): [{compound,drug_class,dose_uM,challenge,invivo_efficacy,follicles:[{id,points,molecular}]}]",
|
||||
"verdict": "파이프라인 작동·G1(동역학 R²0.871 통과)/G2 게이트 평가됨. **2개 분리된 병목**: ① 추정잡음(모낭수↑로 제거; 6→12모낭 급상승) ② HFOC↔생체 번역충실도 ρ(천장 R²max≈ρ² 결정). ρ=0.9면 모낭 늘려도 ~0.69서 정체(천장) → **G2≥0.8엔 ρ≳0.92 + 충분한 모낭·화합물 패널 필요**. ρ는 *wet 종간 브리지(Phase3) 전엔 미지* → dry-run은 '코드/게이트 준비됨'이지 '대체 입증' 아님."
|
||||
}
|
||||
23
data/ipd_dryrun.json
Normal file
23
data/ipd_dryrun.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"_WARNING": "SYNTHETIC dry-run. NOT real patient data. 실제 IPD는 Vivli/Pfizer 통제접근 필요.",
|
||||
"_description": "OSF 사전등록 분석을 합성 코호트로 dry-run — 파이프라인·판정규칙 검증.",
|
||||
"synthetic": true,
|
||||
"result": {
|
||||
"n_patients": 120,
|
||||
"rmse_personal_mean": 10.768,
|
||||
"rmse_pop_mean": 11.149,
|
||||
"mean_diff": 0.38,
|
||||
"diff_ci95": [
|
||||
-1.105,
|
||||
1.994
|
||||
],
|
||||
"improve_pct": 3.4,
|
||||
"personal_wins": 61,
|
||||
"coverage_personal": 0.464,
|
||||
"decision": "불확정(CI 0 포함)",
|
||||
"by_class": {
|
||||
"JAK_inhibitor": 120
|
||||
}
|
||||
},
|
||||
"ready_for_real_ipd": "load_ipd(path) 로 실제 IPD(JSON: patient_id/drug_class/points) 연결 후 analyze() 동일 실행."
|
||||
}
|
||||
1
data/knowledge_graph.json
Normal file
1
data/knowledge_graph.json
Normal file
File diff suppressed because one or more lines are too long
129
data/mapping_calibration.json
Normal file
129
data/mapping_calibration.json
Normal file
@ -0,0 +1,129 @@
|
||||
{
|
||||
"_description": "정성 매핑을 GWAS(마커중요도)+섭동(개입/구동크기)으로 데이터 정박. 개인 baseline→결과 최종보정은 paired IPD 필요(부분 보정).",
|
||||
"gwas": {
|
||||
"AGA": {
|
||||
"AR": 70.0,
|
||||
"EDA2R": 70.0,
|
||||
"SRD5A2": 57.66,
|
||||
"WNT10A": 2.91,
|
||||
"SFRP2": 2.57,
|
||||
"SFRP1": 2.42,
|
||||
"DKK1": 2.36,
|
||||
"DACT1": 2.18,
|
||||
"WNT10B": 1.75
|
||||
},
|
||||
"AA": {
|
||||
"HLA-DQB1": 14.58,
|
||||
"HLA-DRB1": 12.29,
|
||||
"HLA-DRA": 7.5,
|
||||
"IL2RA": 5.46,
|
||||
"ULBP3": 4.11,
|
||||
"CTLA4": 2.47,
|
||||
"IFNG": 2.33,
|
||||
"CD8A": 2.05,
|
||||
"CXCL11": 1.86,
|
||||
"PRDX5": 1.61,
|
||||
"GZMB": 1.56,
|
||||
"IKZF4": 1.46,
|
||||
"CXCL9": 1.23,
|
||||
"CXCL10": 1.2,
|
||||
"ULBP6": 0.0
|
||||
},
|
||||
"AGA_MPB_source": {
|
||||
"AR": {
|
||||
"mlogp": null,
|
||||
"display": 70.0,
|
||||
"underflow": true,
|
||||
"lead_snp": "rs111515468:66667908:C:A"
|
||||
},
|
||||
"EDA2R": {
|
||||
"mlogp": null,
|
||||
"display": 70.0,
|
||||
"underflow": true,
|
||||
"lead_snp": "X:65715565:G:GA"
|
||||
},
|
||||
"SRD5A2": {
|
||||
"mlogp": 57.66,
|
||||
"display": 57.66,
|
||||
"underflow": false,
|
||||
"lead_snp": "rs79204217"
|
||||
}
|
||||
}
|
||||
},
|
||||
"perturb": {
|
||||
"JAKi_IFN": {
|
||||
"markers_used": [
|
||||
"STAT1",
|
||||
"CXCL9",
|
||||
"CXCL10",
|
||||
"CXCL11",
|
||||
"GZMB",
|
||||
"PRF1",
|
||||
"CD8A",
|
||||
"ICOS",
|
||||
"IFNG",
|
||||
"GZMA",
|
||||
"NKG7",
|
||||
"CCL5",
|
||||
"CTLA4",
|
||||
"IRF1"
|
||||
],
|
||||
"vehicle_IFN": 1.263,
|
||||
"jaki_IFN": -0.253,
|
||||
"reversal_fraction": 1.2,
|
||||
"n_ctrl": 3,
|
||||
"n_jaki": 15
|
||||
},
|
||||
"DHT_Wnt": {
|
||||
"n_dht": 6,
|
||||
"n_ctrl": 6,
|
||||
"genes": {
|
||||
"DKK1": {
|
||||
"expect": "up",
|
||||
"log_delta": 1.568,
|
||||
"p": 0.0011
|
||||
},
|
||||
"LEF1": {
|
||||
"expect": "down",
|
||||
"log_delta": 0.14,
|
||||
"p": 0.8452
|
||||
},
|
||||
"AXIN2": {
|
||||
"expect": "down",
|
||||
"log_delta": 0.028,
|
||||
"p": 0.5314
|
||||
}
|
||||
},
|
||||
"wnt_suppression_magnitude": 0.579
|
||||
}
|
||||
},
|
||||
"ode_reproject": {
|
||||
"AGA·안드로겐 강함": {
|
||||
"rec": "병용(피나+미녹)",
|
||||
"resp": {
|
||||
"무치료": 0.0,
|
||||
"피나스테리드": 0.2242,
|
||||
"미녹시딜": 0.7002,
|
||||
"병용(피나+미녹)": 0.9586
|
||||
}
|
||||
},
|
||||
"AGA·Wnt억제 우세": {
|
||||
"rec": "병용(피나+미녹)",
|
||||
"resp": {
|
||||
"무치료": 0.0,
|
||||
"피나스테리드": 0.2915,
|
||||
"미녹시딜": 0.6275,
|
||||
"병용(피나+미녹)": 0.9797
|
||||
}
|
||||
},
|
||||
"AA·면역 강함": {
|
||||
"rec": "JAK억제제",
|
||||
"resp": {
|
||||
"무치료": 0.0,
|
||||
"JAK억제제": 0.6116,
|
||||
"코르티코스테로이드~": 0.6116
|
||||
}
|
||||
}
|
||||
},
|
||||
"honest_scope": "GWAS=마커 중요도, 섭동=개입/구동 크기(그룹수준). 개인 baseline→개인결과 최종보정은 paired IPD 필요. 본 보정은 매핑의 방향·중요도·크기를 데이터에 정박시키는 부분보정."
|
||||
}
|
||||
3229
data/network_dynamics.json
Normal file
3229
data/network_dynamics.json
Normal file
File diff suppressed because it is too large
Load Diff
153
data/ode_personalize.json
Normal file
153
data/ode_personalize.json
Normal file
@ -0,0 +1,153 @@
|
||||
{
|
||||
"_description": "ODE-수준 개인화 — GWAS-가중 보정 적용(calibrate_mapping.py). 질환구동 추론을 GWAS mlogp 로 재가중(AGA=AR/EDA2R/SRD5A2, AA=HLA/IL2RA/ULBP3). 정성→보정 비교.",
|
||||
"calibrated": true,
|
||||
"calib_note": "AGA 구동을 유전 androgen축으로 재층화(하류 DKK1 다운가중). 개인결과 최종보정은 paired IPD 필요.",
|
||||
"marker_mapping": {
|
||||
"W": [
|
||||
[
|
||||
"AXIN2",
|
||||
1
|
||||
],
|
||||
[
|
||||
"LEF1",
|
||||
1
|
||||
],
|
||||
[
|
||||
"WNT10B",
|
||||
1
|
||||
],
|
||||
[
|
||||
"CTNNB1",
|
||||
1
|
||||
],
|
||||
[
|
||||
"DKK1",
|
||||
-1
|
||||
],
|
||||
[
|
||||
"SFRP1",
|
||||
-1
|
||||
]
|
||||
],
|
||||
"B": [
|
||||
[
|
||||
"BMP2",
|
||||
1
|
||||
],
|
||||
[
|
||||
"BMP4",
|
||||
1
|
||||
],
|
||||
[
|
||||
"ID1",
|
||||
1
|
||||
],
|
||||
[
|
||||
"ID2",
|
||||
1
|
||||
]
|
||||
],
|
||||
"S": [
|
||||
[
|
||||
"GLI1",
|
||||
1
|
||||
],
|
||||
[
|
||||
"PTCH1",
|
||||
1
|
||||
],
|
||||
[
|
||||
"SHH",
|
||||
1
|
||||
]
|
||||
],
|
||||
"D": [
|
||||
[
|
||||
"SOX2",
|
||||
1
|
||||
],
|
||||
[
|
||||
"ALPL",
|
||||
1
|
||||
],
|
||||
[
|
||||
"VCAN",
|
||||
1
|
||||
],
|
||||
[
|
||||
"CORIN",
|
||||
1
|
||||
]
|
||||
],
|
||||
"H": [
|
||||
[
|
||||
"KRT15",
|
||||
1
|
||||
],
|
||||
[
|
||||
"CD34",
|
||||
1
|
||||
],
|
||||
[
|
||||
"LGR5",
|
||||
1
|
||||
],
|
||||
[
|
||||
"SOX9",
|
||||
1
|
||||
]
|
||||
],
|
||||
"F": [
|
||||
[
|
||||
"FGF5",
|
||||
1
|
||||
]
|
||||
]
|
||||
},
|
||||
"profiles": {
|
||||
"AGA·안드로겐 강함": {
|
||||
"aga_q": 0.78,
|
||||
"aa_q": 0.5,
|
||||
"aga_strength": 0.98,
|
||||
"aa_strength": 0.5,
|
||||
"state_W": 0.56,
|
||||
"state_D": 0.54,
|
||||
"responses": {
|
||||
"무치료": 0.0,
|
||||
"피나스테리드": 0.3414,
|
||||
"미녹시딜": 0.6495,
|
||||
"병용(피나+미녹)": 1.0491
|
||||
},
|
||||
"recommendation": "병용(피나+미녹)"
|
||||
},
|
||||
"AGA·Wnt억제 우세": {
|
||||
"aga_q": 0.88,
|
||||
"aa_q": 0.5,
|
||||
"aga_strength": 0.64,
|
||||
"aa_strength": 0.5,
|
||||
"state_W": 0.29,
|
||||
"state_D": 0.43,
|
||||
"responses": {
|
||||
"무치료": 0.0,
|
||||
"피나스테리드": 0.1131,
|
||||
"미녹시딜": 0.6925,
|
||||
"병용(피나+미녹)": 0.8245
|
||||
},
|
||||
"recommendation": "병용(피나+미녹)"
|
||||
},
|
||||
"AA·면역 강함": {
|
||||
"aga_q": 0.5,
|
||||
"aa_q": 1.0,
|
||||
"aga_strength": 0.5,
|
||||
"aa_strength": 1.0,
|
||||
"state_W": 0.5,
|
||||
"state_D": 0.5,
|
||||
"responses": {
|
||||
"무치료": 0.0,
|
||||
"JAK억제제": 0.6116,
|
||||
"코르티코스테로이드~": 0.6116
|
||||
},
|
||||
"recommendation": "JAK억제제"
|
||||
}
|
||||
}
|
||||
}
|
||||
1
data/paper_validation.json
Normal file
1
data/paper_validation.json
Normal file
@ -0,0 +1 @@
|
||||
{"n_records": 338, "n_testable": 57, "accuracy_before": 0.86, "accuracy_after": 0.86, "trajectory": [0.86], "overrides": {}, "mismatches_after": [{"gene": "BCL2", "kind": "gene_gain", "obs": "inhibit", "pred": "promote", "paper": "Overexpression of Bcl-2 Protects from Ultraviolet ", "quote": "Compared to WT mice, the K14/Bcl-2 mice displayed macroscopically visible hair loss 2 days earlier (not shown), and the wave of alopecia pro"}, {"gene": "CXCL12", "kind": "gene_gain", "obs": "promote", "pred": "inhibit", "paper": "Impact of SDF-1 and AMD3100 on Hair Follicle Dynam", "quote": "SDF-1 promotes hair growth in chronic stress mice by activating the PI3K/Akt and JAK/STAT pathways, an effect reversible by AMD3100."}, {"gene": "CXCR4", "kind": "gene_loss", "obs": "inhibit", "pred": "promote", "paper": "Impact of SDF-1 and AMD3100 on Hair Follicle Dynam", "quote": "Following subcutaneous injection of SDF-1 or treatment with AMD3100 to block the binding of SDF-1 to CXCR4... Blocking the binding of SDF-1 "}, {"gene": "CXCL12", "kind": "gene_loss", "obs": "inhibit", "pred": "promote", "paper": "Impact of SDF-1 and AMD3100 on Hair Follicle Dynam", "quote": "Silencing SDF-1 through siRNA-mediated inhibition reduced cell proliferation and migration abilities."}, {"gene": "MSX2", "kind": "gene_loss", "obs": "inhibit", "pred": "promote", "paper": "‘Cyclic alopecia’ in Msx2 mutants defects in hair ", "quote": "Msx2-deficient mice exhibit progressive hair loss, starting at P14 and followed by successive cycles of wavelike regrowth and loss."}, {"gene": "EGFR", "kind": "gene_loss", "obs": "promote", "pred": "inhibit", "paper": "Role for the Epidermal Growth Factor Receptor in C", "quote": "Cyclophosphamide treatment of control mice resulted in alopecia while Egfr mutant skin was resistant to cyclophosphamide-induced alopecia."}, {"gene": "IGF1", "kind": "gene_loss", "obs": "promote", "pred": "inhibit", "paper": "The AR miR-221 IGF-1 pathway mediates the pathogen", "quote": "Hair follicle organ culture experiments showed that IGF-1 could rescue the miR-221-mediated suppression of hair growth and induction of anag"}, {"gene": "CD44", "kind": "gene_loss", "obs": "promote", "pred": "inhibit", "paper": "The C3H HeJ mouse and DEBR rat models for alopecia", "quote": "These data showed that anti-CD44v10 inhibited the onset of AA in AA graft induced C3H/HeJ mice."}]}
|
||||
1
data/papers_index.json
Normal file
1
data/papers_index.json
Normal file
File diff suppressed because one or more lines are too long
883
data/personalize_results.json
Normal file
883
data/personalize_results.json
Normal file
@ -0,0 +1,883 @@
|
||||
{
|
||||
"_description": "개인화+데이터 동화: 초기 반응 동화 → 후기 forecast 개선 검정(시험암=유사개인), 동화 구간수축 시연, 합성환자 정확성. 개인환자 IPD 확보 후 환자단위 재검정 필요.",
|
||||
"honest_caveat": "시험암 평균은 저잡음 → 개선이 개인환자보다 과대평가될 수 있음. ODE-수준 개인화는 omics 필요.",
|
||||
"forecast_skill": {
|
||||
"by_trajectory": [
|
||||
{
|
||||
"id": "deuruxolitinib_THRIVE-AA1_12mg",
|
||||
"class": "JAK_inhibitor",
|
||||
"n_obs": 3,
|
||||
"n_future": 3,
|
||||
"rmse_pop": 2.219,
|
||||
"rmse_personal": 1.453,
|
||||
"improve_pct": 34.5,
|
||||
"cover_personal": 1.0,
|
||||
"cover_pop": 1.0
|
||||
},
|
||||
{
|
||||
"id": "deuruxolitinib_THRIVE-AA1_8mg",
|
||||
"class": "JAK_inhibitor",
|
||||
"n_obs": 3,
|
||||
"n_future": 3,
|
||||
"rmse_pop": 5.651,
|
||||
"rmse_personal": 3.071,
|
||||
"improve_pct": 45.7,
|
||||
"cover_personal": 1.0,
|
||||
"cover_pop": 0.0
|
||||
},
|
||||
{
|
||||
"id": "deuruxolitinib_THRIVE-AA2_12mg",
|
||||
"class": "JAK_inhibitor",
|
||||
"n_obs": 3,
|
||||
"n_future": 3,
|
||||
"rmse_pop": 4.582,
|
||||
"rmse_personal": 1.802,
|
||||
"improve_pct": 60.7,
|
||||
"cover_personal": 1.0,
|
||||
"cover_pop": 0.33
|
||||
},
|
||||
{
|
||||
"id": "brepocitinib_NCT02974868",
|
||||
"class": "JAK_inhibitor",
|
||||
"n_obs": 4,
|
||||
"n_future": 3,
|
||||
"rmse_pop": 16.199,
|
||||
"rmse_personal": 5.027,
|
||||
"improve_pct": 69.0,
|
||||
"cover_personal": 1.0,
|
||||
"cover_pop": 0.0
|
||||
},
|
||||
{
|
||||
"id": "ritlecitinib_NCT02974868",
|
||||
"class": "JAK_inhibitor",
|
||||
"n_obs": 4,
|
||||
"n_future": 3,
|
||||
"rmse_pop": 11.686,
|
||||
"rmse_personal": 4.949,
|
||||
"improve_pct": 57.6,
|
||||
"cover_personal": 1.0,
|
||||
"cover_pop": 0.0
|
||||
},
|
||||
{
|
||||
"id": "dutasteride_0.5mg_NCT01231607",
|
||||
"class": "dutasteride",
|
||||
"n_obs": 2,
|
||||
"n_future": 1,
|
||||
"rmse_pop": 45.955,
|
||||
"rmse_personal": 46.306,
|
||||
"improve_pct": -0.8,
|
||||
"cover_personal": 1.0,
|
||||
"cover_pop": 0.0
|
||||
},
|
||||
{
|
||||
"id": "dutasteride_0.5mg_NCT00441116",
|
||||
"class": "dutasteride",
|
||||
"n_obs": 2,
|
||||
"n_future": 1,
|
||||
"rmse_pop": 0.369,
|
||||
"rmse_personal": 0.025,
|
||||
"improve_pct": 93.2,
|
||||
"cover_personal": 1.0,
|
||||
"cover_pop": 1.0
|
||||
},
|
||||
{
|
||||
"id": "finasteride_1mg_NCT01231607",
|
||||
"class": "finasteride",
|
||||
"n_obs": 2,
|
||||
"n_future": 1,
|
||||
"rmse_pop": 30.766,
|
||||
"rmse_personal": 27.729,
|
||||
"improve_pct": 9.9,
|
||||
"cover_personal": 1.0,
|
||||
"cover_pop": 0.0
|
||||
},
|
||||
{
|
||||
"id": "finasteride_1mg_Kaufman_2yr",
|
||||
"class": "finasteride",
|
||||
"n_obs": 2,
|
||||
"n_future": 1,
|
||||
"rmse_pop": 28.258,
|
||||
"rmse_personal": 21.748,
|
||||
"improve_pct": 23.0,
|
||||
"cover_personal": 1.0,
|
||||
"cover_pop": 0.0
|
||||
},
|
||||
{
|
||||
"id": "finasteride_oral_P3074_NCT03004469",
|
||||
"class": "finasteride",
|
||||
"n_obs": 2,
|
||||
"n_future": 1,
|
||||
"rmse_pop": 17.453,
|
||||
"rmse_personal": 16.178,
|
||||
"improve_pct": 7.3,
|
||||
"cover_personal": 0.0,
|
||||
"cover_pop": 0.0
|
||||
}
|
||||
],
|
||||
"overall": {
|
||||
"n": 10,
|
||||
"mean_improve_pct": 40.0,
|
||||
"cover_personal": 0.9,
|
||||
"cover_pop": 0.23,
|
||||
"personal_wins": 9
|
||||
}
|
||||
},
|
||||
"assimilation_example": {
|
||||
"id": "deuruxolitinib_THRIVE-AA1_12mg",
|
||||
"class": "JAK_inhibitor",
|
||||
"points": [
|
||||
[
|
||||
0.92,
|
||||
4.0
|
||||
],
|
||||
[
|
||||
1.84,
|
||||
17.5
|
||||
],
|
||||
[
|
||||
2.76,
|
||||
31.1
|
||||
],
|
||||
[
|
||||
3.68,
|
||||
41.2
|
||||
],
|
||||
[
|
||||
4.6,
|
||||
46.8
|
||||
],
|
||||
[
|
||||
5.52,
|
||||
50.4
|
||||
]
|
||||
],
|
||||
"steps": [
|
||||
{
|
||||
"n_obs": 2,
|
||||
"obs_t": [
|
||||
0.92,
|
||||
1.84
|
||||
],
|
||||
"grid": [
|
||||
0.0,
|
||||
0.21,
|
||||
0.43,
|
||||
0.64,
|
||||
0.86,
|
||||
1.07,
|
||||
1.29,
|
||||
1.5,
|
||||
1.72,
|
||||
1.93,
|
||||
2.15,
|
||||
2.36,
|
||||
2.58,
|
||||
2.79,
|
||||
3.01,
|
||||
3.22,
|
||||
3.44,
|
||||
3.65,
|
||||
3.87,
|
||||
4.08,
|
||||
4.3,
|
||||
4.51,
|
||||
4.73,
|
||||
4.94,
|
||||
5.16,
|
||||
5.37,
|
||||
5.58,
|
||||
5.8
|
||||
],
|
||||
"median": [
|
||||
0.016,
|
||||
0.02,
|
||||
-0.01,
|
||||
0.261,
|
||||
2.841,
|
||||
6.737,
|
||||
10.075,
|
||||
13.143,
|
||||
15.842,
|
||||
18.362,
|
||||
20.575,
|
||||
22.66,
|
||||
24.509,
|
||||
26.271,
|
||||
27.803,
|
||||
29.382,
|
||||
30.641,
|
||||
32.018,
|
||||
33.071,
|
||||
34.106,
|
||||
35.049,
|
||||
36.213,
|
||||
37.037,
|
||||
37.384,
|
||||
38.099,
|
||||
38.738,
|
||||
39.475,
|
||||
40.171
|
||||
],
|
||||
"lo": [
|
||||
-1.761,
|
||||
-1.718,
|
||||
-1.706,
|
||||
-1.598,
|
||||
0.694,
|
||||
4.596,
|
||||
7.86,
|
||||
10.886,
|
||||
13.435,
|
||||
15.848,
|
||||
17.623,
|
||||
19.258,
|
||||
20.446,
|
||||
21.407,
|
||||
22.014,
|
||||
22.724,
|
||||
22.975,
|
||||
23.473,
|
||||
23.697,
|
||||
24.122,
|
||||
24.08,
|
||||
24.466,
|
||||
24.403,
|
||||
24.187,
|
||||
24.734,
|
||||
24.371,
|
||||
24.547,
|
||||
24.757
|
||||
],
|
||||
"hi": [
|
||||
1.708,
|
||||
1.759,
|
||||
1.734,
|
||||
2.29,
|
||||
5.529,
|
||||
9.044,
|
||||
12.5,
|
||||
15.427,
|
||||
18.128,
|
||||
20.894,
|
||||
23.335,
|
||||
25.921,
|
||||
28.116,
|
||||
30.393,
|
||||
32.563,
|
||||
34.522,
|
||||
36.547,
|
||||
38.315,
|
||||
39.955,
|
||||
41.735,
|
||||
42.894,
|
||||
44.725,
|
||||
46.336,
|
||||
47.471,
|
||||
48.589,
|
||||
49.946,
|
||||
50.813,
|
||||
52.166
|
||||
],
|
||||
"endpoint_width": 26.349
|
||||
},
|
||||
{
|
||||
"n_obs": 3,
|
||||
"obs_t": [
|
||||
0.92,
|
||||
1.84,
|
||||
2.76
|
||||
],
|
||||
"grid": [
|
||||
0.0,
|
||||
0.21,
|
||||
0.43,
|
||||
0.64,
|
||||
0.86,
|
||||
1.07,
|
||||
1.29,
|
||||
1.5,
|
||||
1.72,
|
||||
1.93,
|
||||
2.15,
|
||||
2.36,
|
||||
2.58,
|
||||
2.79,
|
||||
3.01,
|
||||
3.22,
|
||||
3.44,
|
||||
3.65,
|
||||
3.87,
|
||||
4.08,
|
||||
4.3,
|
||||
4.51,
|
||||
4.73,
|
||||
4.94,
|
||||
5.16,
|
||||
5.37,
|
||||
5.58,
|
||||
5.8
|
||||
],
|
||||
"median": [
|
||||
0.028,
|
||||
-0.059,
|
||||
0.006,
|
||||
0.179,
|
||||
2.397,
|
||||
6.264,
|
||||
10.027,
|
||||
13.487,
|
||||
16.815,
|
||||
19.856,
|
||||
22.821,
|
||||
25.515,
|
||||
28.016,
|
||||
30.503,
|
||||
32.816,
|
||||
34.918,
|
||||
36.929,
|
||||
38.927,
|
||||
40.654,
|
||||
42.575,
|
||||
44.017,
|
||||
45.728,
|
||||
46.937,
|
||||
48.478,
|
||||
49.75,
|
||||
50.94,
|
||||
52.181,
|
||||
53.092
|
||||
],
|
||||
"lo": [
|
||||
-3.15,
|
||||
-3.101,
|
||||
-3.107,
|
||||
-2.943,
|
||||
-1.538,
|
||||
1.429,
|
||||
5.514,
|
||||
9.5,
|
||||
13.073,
|
||||
16.254,
|
||||
19.124,
|
||||
21.742,
|
||||
24.052,
|
||||
26.443,
|
||||
28.371,
|
||||
30.237,
|
||||
31.909,
|
||||
33.171,
|
||||
34.641,
|
||||
35.947,
|
||||
36.628,
|
||||
37.88,
|
||||
38.456,
|
||||
39.156,
|
||||
40.184,
|
||||
40.536,
|
||||
41.841,
|
||||
41.385
|
||||
],
|
||||
"hi": [
|
||||
3.134,
|
||||
3.054,
|
||||
3.055,
|
||||
3.423,
|
||||
6.531,
|
||||
10.192,
|
||||
13.724,
|
||||
17.073,
|
||||
20.586,
|
||||
23.495,
|
||||
26.532,
|
||||
29.363,
|
||||
31.891,
|
||||
34.571,
|
||||
37.214,
|
||||
39.638,
|
||||
42.023,
|
||||
44.424,
|
||||
46.651,
|
||||
48.907,
|
||||
50.825,
|
||||
53.037,
|
||||
54.656,
|
||||
56.458,
|
||||
58.579,
|
||||
59.93,
|
||||
61.803,
|
||||
63.085
|
||||
],
|
||||
"endpoint_width": 20.156
|
||||
},
|
||||
{
|
||||
"n_obs": 4,
|
||||
"obs_t": [
|
||||
0.92,
|
||||
1.84,
|
||||
2.76,
|
||||
3.68
|
||||
],
|
||||
"grid": [
|
||||
0.0,
|
||||
0.21,
|
||||
0.43,
|
||||
0.64,
|
||||
0.86,
|
||||
1.07,
|
||||
1.29,
|
||||
1.5,
|
||||
1.72,
|
||||
1.93,
|
||||
2.15,
|
||||
2.36,
|
||||
2.58,
|
||||
2.79,
|
||||
3.01,
|
||||
3.22,
|
||||
3.44,
|
||||
3.65,
|
||||
3.87,
|
||||
4.08,
|
||||
4.3,
|
||||
4.51,
|
||||
4.73,
|
||||
4.94,
|
||||
5.16,
|
||||
5.37,
|
||||
5.58,
|
||||
5.8
|
||||
],
|
||||
"median": [
|
||||
0.06,
|
||||
0.028,
|
||||
0.013,
|
||||
0.322,
|
||||
2.166,
|
||||
5.765,
|
||||
9.784,
|
||||
13.269,
|
||||
16.66,
|
||||
19.826,
|
||||
23.108,
|
||||
25.943,
|
||||
28.694,
|
||||
31.286,
|
||||
33.746,
|
||||
36.077,
|
||||
38.228,
|
||||
40.378,
|
||||
42.25,
|
||||
44.258,
|
||||
45.997,
|
||||
47.544,
|
||||
49.333,
|
||||
50.786,
|
||||
52.197,
|
||||
53.436,
|
||||
54.875,
|
||||
56.021
|
||||
],
|
||||
"lo": [
|
||||
-4.179,
|
||||
-4.139,
|
||||
-4.098,
|
||||
-4.022,
|
||||
-2.647,
|
||||
-0.278,
|
||||
3.93,
|
||||
8.285,
|
||||
11.856,
|
||||
15.019,
|
||||
18.226,
|
||||
21.169,
|
||||
23.993,
|
||||
26.442,
|
||||
29.075,
|
||||
31.102,
|
||||
33.056,
|
||||
35.172,
|
||||
36.793,
|
||||
38.477,
|
||||
39.861,
|
||||
41.067,
|
||||
42.536,
|
||||
43.69,
|
||||
44.327,
|
||||
45.474,
|
||||
46.325,
|
||||
47.044
|
||||
],
|
||||
"hi": [
|
||||
3.96,
|
||||
3.949,
|
||||
4.083,
|
||||
4.692,
|
||||
7.581,
|
||||
11.344,
|
||||
14.621,
|
||||
18.133,
|
||||
21.528,
|
||||
24.559,
|
||||
27.922,
|
||||
30.603,
|
||||
33.35,
|
||||
36.091,
|
||||
38.501,
|
||||
40.962,
|
||||
43.249,
|
||||
45.663,
|
||||
47.783,
|
||||
50.042,
|
||||
52.101,
|
||||
53.815,
|
||||
55.935,
|
||||
57.704,
|
||||
59.494,
|
||||
61.127,
|
||||
63.184,
|
||||
64.703
|
||||
],
|
||||
"endpoint_width": 16.529
|
||||
},
|
||||
{
|
||||
"n_obs": 5,
|
||||
"obs_t": [
|
||||
0.92,
|
||||
1.84,
|
||||
2.76,
|
||||
3.68,
|
||||
4.6
|
||||
],
|
||||
"grid": [
|
||||
0.0,
|
||||
0.21,
|
||||
0.43,
|
||||
0.64,
|
||||
0.86,
|
||||
1.07,
|
||||
1.29,
|
||||
1.5,
|
||||
1.72,
|
||||
1.93,
|
||||
2.15,
|
||||
2.36,
|
||||
2.58,
|
||||
2.79,
|
||||
3.01,
|
||||
3.22,
|
||||
3.44,
|
||||
3.65,
|
||||
3.87,
|
||||
4.08,
|
||||
4.3,
|
||||
4.51,
|
||||
4.73,
|
||||
4.94,
|
||||
5.16,
|
||||
5.37,
|
||||
5.58,
|
||||
5.8
|
||||
],
|
||||
"median": [
|
||||
0.021,
|
||||
0.076,
|
||||
0.038,
|
||||
0.337,
|
||||
2.473,
|
||||
6.034,
|
||||
9.809,
|
||||
13.372,
|
||||
16.692,
|
||||
19.925,
|
||||
22.908,
|
||||
25.714,
|
||||
28.472,
|
||||
30.953,
|
||||
33.364,
|
||||
35.709,
|
||||
37.657,
|
||||
39.849,
|
||||
41.724,
|
||||
43.429,
|
||||
45.237,
|
||||
46.794,
|
||||
48.381,
|
||||
49.716,
|
||||
51.077,
|
||||
52.376,
|
||||
53.608,
|
||||
54.841
|
||||
],
|
||||
"lo": [
|
||||
-4.465,
|
||||
-4.661,
|
||||
-4.607,
|
||||
-4.374,
|
||||
-2.919,
|
||||
-0.47,
|
||||
3.51,
|
||||
7.56,
|
||||
11.083,
|
||||
14.66,
|
||||
17.573,
|
||||
20.542,
|
||||
23.359,
|
||||
25.804,
|
||||
28.214,
|
||||
30.337,
|
||||
32.362,
|
||||
34.418,
|
||||
36.275,
|
||||
38.06,
|
||||
39.515,
|
||||
41.056,
|
||||
42.163,
|
||||
43.431,
|
||||
44.555,
|
||||
45.584,
|
||||
46.706,
|
||||
47.305
|
||||
],
|
||||
"hi": [
|
||||
4.697,
|
||||
4.763,
|
||||
4.529,
|
||||
5.275,
|
||||
8.175,
|
||||
11.877,
|
||||
15.463,
|
||||
18.868,
|
||||
22.088,
|
||||
25.322,
|
||||
28.183,
|
||||
31.064,
|
||||
33.658,
|
||||
36.245,
|
||||
38.661,
|
||||
40.787,
|
||||
42.968,
|
||||
45.057,
|
||||
46.917,
|
||||
49.095,
|
||||
50.782,
|
||||
52.544,
|
||||
54.125,
|
||||
55.943,
|
||||
57.541,
|
||||
58.978,
|
||||
60.707,
|
||||
62.112
|
||||
],
|
||||
"endpoint_width": 13.777
|
||||
},
|
||||
{
|
||||
"n_obs": 6,
|
||||
"obs_t": [
|
||||
0.92,
|
||||
1.84,
|
||||
2.76,
|
||||
3.68,
|
||||
4.6,
|
||||
5.52
|
||||
],
|
||||
"grid": [
|
||||
0.0,
|
||||
0.21,
|
||||
0.43,
|
||||
0.64,
|
||||
0.86,
|
||||
1.07,
|
||||
1.29,
|
||||
1.5,
|
||||
1.72,
|
||||
1.93,
|
||||
2.15,
|
||||
2.36,
|
||||
2.58,
|
||||
2.79,
|
||||
3.01,
|
||||
3.22,
|
||||
3.44,
|
||||
3.65,
|
||||
3.87,
|
||||
4.08,
|
||||
4.3,
|
||||
4.51,
|
||||
4.73,
|
||||
4.94,
|
||||
5.16,
|
||||
5.37,
|
||||
5.58,
|
||||
5.8
|
||||
],
|
||||
"median": [
|
||||
-0.018,
|
||||
0.083,
|
||||
0.113,
|
||||
0.437,
|
||||
2.564,
|
||||
6.226,
|
||||
10.038,
|
||||
13.632,
|
||||
17.018,
|
||||
20.106,
|
||||
23.045,
|
||||
25.71,
|
||||
28.345,
|
||||
30.857,
|
||||
32.983,
|
||||
35.312,
|
||||
37.368,
|
||||
39.313,
|
||||
40.953,
|
||||
42.595,
|
||||
44.421,
|
||||
45.765,
|
||||
47.129,
|
||||
48.567,
|
||||
49.786,
|
||||
50.835,
|
||||
51.972,
|
||||
53.291
|
||||
],
|
||||
"lo": [
|
||||
-4.939,
|
||||
-4.9,
|
||||
-4.773,
|
||||
-4.686,
|
||||
-3.182,
|
||||
-0.747,
|
||||
3.52,
|
||||
7.617,
|
||||
11.103,
|
||||
14.269,
|
||||
17.539,
|
||||
20.088,
|
||||
22.732,
|
||||
25.262,
|
||||
27.485,
|
||||
29.769,
|
||||
31.813,
|
||||
33.535,
|
||||
35.431,
|
||||
37.271,
|
||||
38.819,
|
||||
40.123,
|
||||
41.447,
|
||||
42.864,
|
||||
43.578,
|
||||
44.741,
|
||||
45.746,
|
||||
46.517
|
||||
],
|
||||
"hi": [
|
||||
4.882,
|
||||
5.056,
|
||||
5.143,
|
||||
5.652,
|
||||
8.686,
|
||||
12.276,
|
||||
15.71,
|
||||
19.053,
|
||||
22.682,
|
||||
25.638,
|
||||
28.646,
|
||||
31.517,
|
||||
34.053,
|
||||
36.561,
|
||||
38.886,
|
||||
40.949,
|
||||
43.044,
|
||||
44.932,
|
||||
46.528,
|
||||
48.146,
|
||||
50.006,
|
||||
51.447,
|
||||
53.097,
|
||||
54.13,
|
||||
55.648,
|
||||
57.07,
|
||||
58.103,
|
||||
59.655
|
||||
],
|
||||
"endpoint_width": 12.255
|
||||
}
|
||||
]
|
||||
},
|
||||
"synthetic": {
|
||||
"truth": {
|
||||
"lag": 1.2,
|
||||
"tau": 3.5,
|
||||
"A": -48.0
|
||||
},
|
||||
"true_endpoint": -34.04,
|
||||
"obs_times_wk": [
|
||||
4,
|
||||
8,
|
||||
12,
|
||||
16,
|
||||
20,
|
||||
24
|
||||
],
|
||||
"obs_values": [
|
||||
-1.12,
|
||||
-10.99,
|
||||
-13.55,
|
||||
-23.76,
|
||||
-32.49,
|
||||
-33.82
|
||||
],
|
||||
"steps": [
|
||||
{
|
||||
"n_obs": 2,
|
||||
"endpoint_pred": -20.17,
|
||||
"endpoint_lo": -31.87,
|
||||
"endpoint_hi": -11.36,
|
||||
"endpoint_width": 20.52,
|
||||
"endpoint_err": 13.87,
|
||||
"lag_mean": 1.017,
|
||||
"lag_sd": 0.229
|
||||
},
|
||||
{
|
||||
"n_obs": 3,
|
||||
"endpoint_pred": -15.08,
|
||||
"endpoint_lo": -21.8,
|
||||
"endpoint_hi": -12.25,
|
||||
"endpoint_width": 9.55,
|
||||
"endpoint_err": 18.96,
|
||||
"lag_mean": 0.968,
|
||||
"lag_sd": 0.211
|
||||
},
|
||||
{
|
||||
"n_obs": 4,
|
||||
"endpoint_pred": -30.31,
|
||||
"endpoint_lo": -35.26,
|
||||
"endpoint_hi": -25.21,
|
||||
"endpoint_width": 10.05,
|
||||
"endpoint_err": 3.74,
|
||||
"lag_mean": 0.83,
|
||||
"lag_sd": 0.192
|
||||
},
|
||||
{
|
||||
"n_obs": 5,
|
||||
"endpoint_pred": -35.39,
|
||||
"endpoint_lo": -40.12,
|
||||
"endpoint_hi": -30.66,
|
||||
"endpoint_width": 9.46,
|
||||
"endpoint_err": 1.34,
|
||||
"lag_mean": 0.987,
|
||||
"lag_sd": 0.223
|
||||
},
|
||||
{
|
||||
"n_obs": 6,
|
||||
"endpoint_pred": -34.48,
|
||||
"endpoint_lo": -38.79,
|
||||
"endpoint_hi": -30.31,
|
||||
"endpoint_width": 8.48,
|
||||
"endpoint_err": 0.43,
|
||||
"lag_mean": 0.949,
|
||||
"lag_sd": 0.226
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
60
data/power_molecular.json
Normal file
60
data/power_molecular.json
Normal file
@ -0,0 +1,60 @@
|
||||
{
|
||||
"_description": "분자 readout/설계의 검정력 영향. 핵심: 노이즈 플로어는 생물학적 모낭간 변동 → 표본 절감은 유효 CV↓(주로 모낭내 paired/반복측정·매칭)에서. readout 교체 자체론 불충분.",
|
||||
"molecular_delta": 0.1271,
|
||||
"readout_design": {
|
||||
"primary_synergy": "hair-keratin mRNA (KRT35/KRT85) = A(Hair output) proxy → AND-게이트",
|
||||
"axis_confirm": "AXIN2/LEF1 (W축, ARM1) + DP markers (D축, ARM2)",
|
||||
"why": "W/D 단독 마커로는 시너지 안 보임 — 시너지는 하류 A=keratin 에서 창발"
|
||||
},
|
||||
"n_vs_cv": {
|
||||
"0.1": 10,
|
||||
"0.12": 12,
|
||||
"0.15": 20,
|
||||
"0.18": 20,
|
||||
"0.2": 25,
|
||||
"0.25": 40,
|
||||
"0.3": 50,
|
||||
"0.35": 60
|
||||
},
|
||||
"strategies": [
|
||||
{
|
||||
"strategy": "형태학 단일신장(엔드포인트)",
|
||||
"cv_eff": 0.3,
|
||||
"per_group": 50,
|
||||
"total_follicles": 1000,
|
||||
"note": "기준. 모낭 간 생물변동이 노이즈 플로어."
|
||||
},
|
||||
{
|
||||
"strategy": "분자 qPCR keratin (낙관)",
|
||||
"cv_eff": 0.2,
|
||||
"per_group": 25,
|
||||
"total_follicles": 500,
|
||||
"note": "⚠ mRNA가 더 조밀하다는 가정 — 파일럿으로 bioCV 확인 필수."
|
||||
},
|
||||
{
|
||||
"strategy": "반복측정 신장기울기(모낭내 paired)",
|
||||
"cv_eff": 0.15,
|
||||
"per_group": 20,
|
||||
"total_follicles": 400,
|
||||
"note": "★ 모낭 간 변동 상쇄 — 비파괴 영상으로 가능. 최대 레버."
|
||||
},
|
||||
{
|
||||
"strategy": "paired + 분자공동일차 + 모낭매칭",
|
||||
"cv_eff": 0.12,
|
||||
"per_group": 15,
|
||||
"total_follicles": 300,
|
||||
"note": "복합(매칭+반복+분자). 최선 시나리오."
|
||||
}
|
||||
],
|
||||
"honest_findings": [
|
||||
"Bliss 초과 δ는 readout 무관(~0.127); 검정력 차이는 유효 CV에서만.",
|
||||
"qPCR 기술CV(~0.10) ≪ 모낭간 생물CV(~0.30) → readout 교체만으로 유효CV 거의 불변.",
|
||||
"모낭 풀링은 총 모낭수를 못 줄임(샘플당 k배, 정보 보존).",
|
||||
"최대 레버 = 모낭내 paired/반복측정(비파괴 신장 기울기) → 모낭간 변동 상쇄.",
|
||||
"분자 qPCR의 가치 = 시너지를 잡는 올바른 지표(A proxy)+축 확인; 표본 절감은 bioCV가 실제 낮을 때만(파일럿 필수)."
|
||||
],
|
||||
"morphology_reference": {
|
||||
"per_group": 50,
|
||||
"total_follicles": 1000
|
||||
}
|
||||
}
|
||||
99
data/power_simulation.json
Normal file
99
data/power_simulation.json
Normal file
@ -0,0 +1,99 @@
|
||||
{
|
||||
"_description": "ex vivo 2×2 시너지 실험 몬테카를로 검정력. estimand=Bliss 초과 δ, 델타법 SE, 단측α=0.05.",
|
||||
"assumptions": {
|
||||
"H_um_per_9d": 250.0,
|
||||
"aga_deficit": 0.5,
|
||||
"donor_sd": 0.15,
|
||||
"alpha": 0.05,
|
||||
"sided": 1,
|
||||
"caveat": "SE 를 알려진 것으로 근사(부트스트랩 대비 약간 낙관적). ARM 효능<E 시 시너지↓."
|
||||
},
|
||||
"scenarios": {
|
||||
"기준(E=0.6)": {
|
||||
"true_delta": 0.1224,
|
||||
"cv": 0.3,
|
||||
"grid": {
|
||||
"4d_10f": 0.227,
|
||||
"4d_15f": 0.342,
|
||||
"4d_20f": 0.432,
|
||||
"4d_30f": 0.58,
|
||||
"4d_40f": 0.684,
|
||||
"4d_60f": 0.856,
|
||||
"4d_80f": 0.942,
|
||||
"4d_120f": 0.991,
|
||||
"6d_10f": 0.335,
|
||||
"6d_15f": 0.473,
|
||||
"6d_20f": 0.581,
|
||||
"6d_30f": 0.752,
|
||||
"6d_40f": 0.861,
|
||||
"6d_60f": 0.956,
|
||||
"6d_80f": 0.994,
|
||||
"6d_120f": 1.0
|
||||
},
|
||||
"reco_80pct": {
|
||||
"donors": 4,
|
||||
"follicles_per_group": 60
|
||||
}
|
||||
},
|
||||
"낙관(E=0.6,저변동)": {
|
||||
"true_delta": 0.1224,
|
||||
"cv": 0.22,
|
||||
"grid": {
|
||||
"4d_10f": 0.366,
|
||||
"4d_15f": 0.518,
|
||||
"4d_20f": 0.625,
|
||||
"4d_30f": 0.811,
|
||||
"4d_40f": 0.899,
|
||||
"4d_60f": 0.983,
|
||||
"4d_80f": 0.995,
|
||||
"4d_120f": 1.0,
|
||||
"6d_10f": 0.515,
|
||||
"6d_15f": 0.672,
|
||||
"6d_20f": 0.816,
|
||||
"6d_30f": 0.928,
|
||||
"6d_40f": 0.972,
|
||||
"6d_60f": 0.998,
|
||||
"6d_80f": 1.0,
|
||||
"6d_120f": 1.0
|
||||
},
|
||||
"reco_80pct": {
|
||||
"donors": 4,
|
||||
"follicles_per_group": 30
|
||||
}
|
||||
},
|
||||
"보수(E=0.4)": {
|
||||
"true_delta": 0.0586,
|
||||
"cv": 0.35,
|
||||
"grid": {
|
||||
"4d_10f": 0.074,
|
||||
"4d_15f": 0.1,
|
||||
"4d_20f": 0.102,
|
||||
"4d_30f": 0.181,
|
||||
"4d_40f": 0.222,
|
||||
"4d_60f": 0.289,
|
||||
"4d_80f": 0.368,
|
||||
"4d_120f": 0.49,
|
||||
"6d_10f": 0.102,
|
||||
"6d_15f": 0.141,
|
||||
"6d_20f": 0.172,
|
||||
"6d_30f": 0.227,
|
||||
"6d_40f": 0.304,
|
||||
"6d_60f": 0.374,
|
||||
"6d_80f": 0.475,
|
||||
"6d_120f": 0.646
|
||||
},
|
||||
"reco_80pct": null
|
||||
}
|
||||
},
|
||||
"recommendation": {
|
||||
"기준(E=0.6)": [
|
||||
4,
|
||||
60
|
||||
],
|
||||
"낙관(E=0.6,저변동)": [
|
||||
4,
|
||||
30
|
||||
],
|
||||
"보수(E=0.4)": null
|
||||
}
|
||||
}
|
||||
12653
data/protein_catalog.json
Normal file
12653
data/protein_catalog.json
Normal file
File diff suppressed because it is too large
Load Diff
1
data/protein_xvalidation.json
Normal file
1
data/protein_xvalidation.json
Normal file
File diff suppressed because one or more lines are too long
133
data/quant_recalibration.json
Normal file
133
data/quant_recalibration.json
Normal file
@ -0,0 +1,133 @@
|
||||
{
|
||||
"threshold_r2": 0.8,
|
||||
"classes": {
|
||||
"JAK_inhibitor": {
|
||||
"n_traj": 5,
|
||||
"n_biphasic_excluded_from_mono": 0,
|
||||
"class_params": {
|
||||
"A_med": 65.51,
|
||||
"lag_med": 3.36,
|
||||
"tau_med": 13.55
|
||||
},
|
||||
"M1_class_extrap": {
|
||||
"r2": 0.718,
|
||||
"rmse": 8.66
|
||||
},
|
||||
"M2_shape_transfer": {
|
||||
"r2": 0.931,
|
||||
"rmse": 4.29
|
||||
},
|
||||
"per_fold": [
|
||||
{
|
||||
"held_out": "deuruxolitinib_THRIVE-AA1_12mg",
|
||||
"monotone": true,
|
||||
"r2_m1": 0.971,
|
||||
"r2_m2": 0.996,
|
||||
"A_fit": 62.44,
|
||||
"A_class": 66.94
|
||||
},
|
||||
{
|
||||
"held_out": "deuruxolitinib_THRIVE-AA1_8mg",
|
||||
"monotone": true,
|
||||
"r2_m1": 0.137,
|
||||
"r2_m2": 0.959,
|
||||
"A_fit": 43.55,
|
||||
"A_class": 63.21
|
||||
},
|
||||
{
|
||||
"held_out": "deuruxolitinib_THRIVE-AA2_12mg",
|
||||
"monotone": true,
|
||||
"r2_m1": 0.935,
|
||||
"r2_m2": 0.978,
|
||||
"A_fit": 57.45,
|
||||
"A_class": 63.21
|
||||
},
|
||||
{
|
||||
"held_out": "brepocitinib_NCT02974868",
|
||||
"monotone": true,
|
||||
"r2_m1": 0.487,
|
||||
"r2_m2": 0.829,
|
||||
"A_fit": 90.16,
|
||||
"A_class": 69.24
|
||||
},
|
||||
{
|
||||
"held_out": "ritlecitinib_NCT02974868",
|
||||
"monotone": true,
|
||||
"r2_m1": 0.746,
|
||||
"r2_m2": 0.845,
|
||||
"A_fit": 61.19,
|
||||
"A_class": 69.24
|
||||
}
|
||||
],
|
||||
"meets_threshold_M2": true
|
||||
},
|
||||
"dutasteride": {
|
||||
"loto": "불가(다점<2)"
|
||||
},
|
||||
"finasteride": {
|
||||
"n_traj": 4,
|
||||
"n_biphasic_excluded_from_mono": 2,
|
||||
"class_params": {
|
||||
"A_med": 60.05,
|
||||
"lag_med": 0.29,
|
||||
"tau_med": 2.84
|
||||
},
|
||||
"M1_class_extrap": {
|
||||
"r2": 0.436,
|
||||
"rmse": 33.39
|
||||
},
|
||||
"M2_shape_transfer": {
|
||||
"r2": 0.926,
|
||||
"rmse": 12.09
|
||||
},
|
||||
"per_fold": [
|
||||
{
|
||||
"held_out": "finasteride_1mg_NCT01231607",
|
||||
"monotone": true,
|
||||
"r2_m1": 0.903,
|
||||
"r2_m2": 0.992,
|
||||
"A_fit": 53.7,
|
||||
"A_class": 63.0
|
||||
},
|
||||
{
|
||||
"held_out": "finasteride_1mg_Kaufman_2yr",
|
||||
"monotone": true,
|
||||
"r2_m1": 0.138,
|
||||
"r2_m2": 0.954,
|
||||
"A_fit": 122.5,
|
||||
"A_class": 57.1
|
||||
},
|
||||
{
|
||||
"held_out": "finasteride_1mg_5yr_DECLINE",
|
||||
"monotone": false,
|
||||
"r2_m1": 0.661,
|
||||
"r2_m2": 0.679,
|
||||
"A_fit": 63.0,
|
||||
"A_class": 57.1
|
||||
},
|
||||
{
|
||||
"held_out": "finasteride_oral_P3074_NCT03004469",
|
||||
"monotone": false,
|
||||
"r2_m1": -7.914,
|
||||
"r2_m2": 0.979,
|
||||
"A_fit": 23.08,
|
||||
"A_class": 63.0
|
||||
}
|
||||
],
|
||||
"meets_threshold_M2": true
|
||||
},
|
||||
"minoxidil": {
|
||||
"loto": "불가(다점<2)"
|
||||
}
|
||||
},
|
||||
"_note": "현재 도달 가능성 측정(대체 증거 아님). 임상 집계궤적·군내 용량이질·단조모델 biphasic 미표현 한계 포함. M1=군-외삽(정보0), M2=형태-전이(군 lag/tau + 진폭 1-param 적합; 현실적 NAM).",
|
||||
"overall": {
|
||||
"M1_r2": 0.531,
|
||||
"M2_r2": 0.93,
|
||||
"M2_r2_monotone_only": 0.961,
|
||||
"n_test_points": 44,
|
||||
"meets_threshold_M2": true,
|
||||
"meets_threshold_M2_monotone": true
|
||||
},
|
||||
"verdict": "M2 형태-전이가 *단조(in-scope) 궤적에서* R2=0.96>=0.8 도달 → **정량 트윈은 대체급 도달 *가능*하다(조건부)**: 군 동역학(lag/tau)이 새 화합물에 전이되고 진폭만 HFOC가 주면 됨. 단 (a)biphasic(피나/미녹 재퇴행)은 모델 확장 필요, (b)군-외삽(정보0)은 약함 → 화합물별 조기 readout 앵커가 필수, (c)임상집계→HFOC paired 정량으로 재적합 요."
|
||||
}
|
||||
85
data/synergy_clinical_test.json
Normal file
85
data/synergy_clinical_test.json
Normal file
@ -0,0 +1,85 @@
|
||||
{
|
||||
"_description": "트윈 초가법 시너지 예측을 실제 3-arm per-arm 데이터(IJT 2023)로 검정. 결과: 가법미만(sub-additive) → 핵심 예측 반증(정직). 병용>단독(HSA)은 충족.",
|
||||
"twin_prediction": {
|
||||
"bliss_excess": 0.122,
|
||||
"claim": "super-additive synergy"
|
||||
},
|
||||
"data_source": {
|
||||
"trial": "IJT 2023 (Int J Trichology)",
|
||||
"pmc": "PMC10495069",
|
||||
"license": "CC BY-NC-SA",
|
||||
"design": "3-arm RCT n=20/arm (FNS 0.25% / MNX 5% / MNF combo), 24주 모발밀도 증가량",
|
||||
"title": "Comparative Efficacy of Topical Finasteride(0.25%)+Minoxidil(5%) vs each alone in male AGA"
|
||||
},
|
||||
"cells": {
|
||||
"총모발/전두부": {
|
||||
"FNS": 7.85,
|
||||
"MNX": 9.25,
|
||||
"MNF": 10.05,
|
||||
"fina_add_benefit": 0.8,
|
||||
"super_additive_excess": -7.05,
|
||||
"linear_interaction": -7.05,
|
||||
"hsa": true,
|
||||
"verdict": "가법미만"
|
||||
},
|
||||
"총모발/전측두": {
|
||||
"FNS": 7.45,
|
||||
"MNX": 7.25,
|
||||
"MNF": 9.05,
|
||||
"fina_add_benefit": 1.8,
|
||||
"super_additive_excess": -5.65,
|
||||
"linear_interaction": -5.65,
|
||||
"hsa": true,
|
||||
"verdict": "가법미만"
|
||||
},
|
||||
"총모발/정수리": {
|
||||
"FNS": 6.75,
|
||||
"MNX": 6.8,
|
||||
"MNF": 7.3,
|
||||
"fina_add_benefit": 0.5,
|
||||
"super_additive_excess": -6.25,
|
||||
"linear_interaction": -6.25,
|
||||
"hsa": true,
|
||||
"verdict": "가법미만"
|
||||
},
|
||||
"말단모발/전두부": {
|
||||
"FNS": 9.35,
|
||||
"MNX": 8.45,
|
||||
"MNF": 11.2,
|
||||
"fina_add_benefit": 2.75,
|
||||
"super_additive_excess": -6.6,
|
||||
"linear_interaction": -6.6,
|
||||
"hsa": true,
|
||||
"verdict": "가법미만"
|
||||
},
|
||||
"말단모발/전측두": {
|
||||
"FNS": 7.9,
|
||||
"MNX": 9.7,
|
||||
"MNF": 8.55,
|
||||
"fina_add_benefit": -1.15,
|
||||
"super_additive_excess": -9.05,
|
||||
"linear_interaction": -9.05,
|
||||
"hsa": false,
|
||||
"verdict": "가법미만"
|
||||
},
|
||||
"말단모발/정수리": {
|
||||
"FNS": 7.75,
|
||||
"MNX": 8.35,
|
||||
"MNF": 10.0,
|
||||
"fina_add_benefit": 1.65,
|
||||
"super_additive_excess": -6.1,
|
||||
"linear_interaction": -6.1,
|
||||
"hsa": true,
|
||||
"verdict": "가법미만"
|
||||
}
|
||||
},
|
||||
"summary": {
|
||||
"mean_super_additive_excess": -6.78,
|
||||
"sub_additive_cells": "6/6",
|
||||
"hsa_cells": "5/6",
|
||||
"verdict": "SUB-additive — 트윈 초가법 예측 반증(NOT supported)",
|
||||
"direction_combo_gt_mono": "충족(HSA)"
|
||||
},
|
||||
"honest": "트윈의 핵심 신규 예측(병용 초가법)이 실 per-arm 데이터로 반증됨. 병용 우월성(HSA)은 유지. 한계: 단일 소규모 파일럿·국소 피나·placebo 없음. 수치는 PMC10495069 표에서 직접 추출(날조 없음).",
|
||||
"interpretation": "미녹+피나 효과 겹침(비독립) → AND-게이트 독립노드 전제 미충족 또는 모델 오류. 사전등록 예측이 '서로 다른 축'을 요구했음을 상기 — 이 약물쌍은 그 전제를 만족 못할 수 있음."
|
||||
}
|
||||
171
data/synergy_prediction.json
Normal file
171
data/synergy_prediction.json
Normal file
@ -0,0 +1,171 @@
|
||||
{
|
||||
"prediction_id": "AGA_synergy_brake_x_accelerator",
|
||||
"hypothesis": "AGA 에서 상류 'AR/5-ARI/BMP-antagonist'(Wnt 축 복원) × 하류 '미녹시딜/Wnt-agonist'(DP/anagen 부양) 병용은 Bliss 독립 기대를 초과하는 초가법적 시너지를 보인다 — Hair 산출 A 가 W·D 두 협동 문턱의 곱(AND-게이트)이기 때문.",
|
||||
"null_hypothesis": "두 팔은 서로 다른 경로이므로 효과는 가법적(Bliss 독립, 시너지초과=0).",
|
||||
"falsification": "실험 Bliss 초과량이 95% CI 내에서 ≤0 이면 A 의 협동적 AND-게이트 가정이 반증됨.",
|
||||
"headline": {
|
||||
"E_ref": 0.6,
|
||||
"synergy_excess_auc": 0.1225,
|
||||
"synergy_excess_peak": 0.1271,
|
||||
"combo_index_CI": 0.8136,
|
||||
"R1": 0.3863,
|
||||
"R2": 0.2416,
|
||||
"R_combo_actual": 0.657,
|
||||
"R_combo_bliss": 0.5346
|
||||
},
|
||||
"dose_curve": [
|
||||
{
|
||||
"E": 0.2,
|
||||
"metric": "auc",
|
||||
"untreated": 4.5963,
|
||||
"arm1_only": 4.9209,
|
||||
"arm2_only": 4.7491,
|
||||
"combo": 5.0775,
|
||||
"healthy": 6.4826,
|
||||
"R1": 0.1721,
|
||||
"R2": 0.081,
|
||||
"R_combo": 0.2551,
|
||||
"bliss_expected": 0.2391,
|
||||
"synergy_excess": 0.016,
|
||||
"combo_index_CI": 0.9373
|
||||
},
|
||||
{
|
||||
"E": 0.3,
|
||||
"metric": "auc",
|
||||
"untreated": 4.5963,
|
||||
"arm1_only": 5.0446,
|
||||
"arm2_only": 4.8254,
|
||||
"combo": 5.2847,
|
||||
"healthy": 6.4826,
|
||||
"R1": 0.2376,
|
||||
"R2": 0.1214,
|
||||
"R_combo": 0.365,
|
||||
"bliss_expected": 0.3302,
|
||||
"synergy_excess": 0.0347,
|
||||
"combo_index_CI": 0.9048
|
||||
},
|
||||
{
|
||||
"E": 0.4,
|
||||
"metric": "auc",
|
||||
"untreated": 4.5963,
|
||||
"arm1_only": 5.1508,
|
||||
"arm2_only": 4.9014,
|
||||
"combo": 5.478,
|
||||
"healthy": 6.4826,
|
||||
"R1": 0.294,
|
||||
"R2": 0.1617,
|
||||
"R_combo": 0.4674,
|
||||
"bliss_expected": 0.4081,
|
||||
"synergy_excess": 0.0593,
|
||||
"combo_index_CI": 0.8732
|
||||
},
|
||||
{
|
||||
"E": 0.5,
|
||||
"metric": "auc",
|
||||
"untreated": 4.5963,
|
||||
"arm1_only": 5.2433,
|
||||
"arm2_only": 4.977,
|
||||
"combo": 5.6609,
|
||||
"healthy": 6.4826,
|
||||
"R1": 0.343,
|
||||
"R2": 0.2018,
|
||||
"R_combo": 0.5644,
|
||||
"bliss_expected": 0.4756,
|
||||
"synergy_excess": 0.0888,
|
||||
"combo_index_CI": 0.8427
|
||||
},
|
||||
{
|
||||
"E": 0.6,
|
||||
"metric": "auc",
|
||||
"untreated": 4.5963,
|
||||
"arm1_only": 5.325,
|
||||
"arm2_only": 5.052,
|
||||
"combo": 5.8356,
|
||||
"healthy": 6.4826,
|
||||
"R1": 0.3863,
|
||||
"R2": 0.2416,
|
||||
"R_combo": 0.657,
|
||||
"bliss_expected": 0.5346,
|
||||
"synergy_excess": 0.1225,
|
||||
"combo_index_CI": 0.8136
|
||||
},
|
||||
{
|
||||
"E": 0.7,
|
||||
"metric": "auc",
|
||||
"untreated": 4.5963,
|
||||
"arm1_only": 5.3977,
|
||||
"arm2_only": 5.1265,
|
||||
"combo": 6.0041,
|
||||
"healthy": 6.4826,
|
||||
"R1": 0.4249,
|
||||
"R2": 0.2811,
|
||||
"R_combo": 0.7463,
|
||||
"bliss_expected": 0.5865,
|
||||
"synergy_excess": 0.1598,
|
||||
"combo_index_CI": 0.7859
|
||||
},
|
||||
{
|
||||
"E": 0.8,
|
||||
"metric": "auc",
|
||||
"untreated": 4.5963,
|
||||
"arm1_only": 5.4631,
|
||||
"arm2_only": 5.2005,
|
||||
"combo": 6.1674,
|
||||
"healthy": 6.4826,
|
||||
"R1": 0.4595,
|
||||
"R2": 0.3203,
|
||||
"R_combo": 0.8329,
|
||||
"bliss_expected": 0.6326,
|
||||
"synergy_excess": 0.2003,
|
||||
"combo_index_CI": 0.7595
|
||||
}
|
||||
],
|
||||
"threshold_sweep": [
|
||||
{
|
||||
"KDA": 0.2,
|
||||
"synergy_excess": 0.1098,
|
||||
"R_combo": 0.6472
|
||||
},
|
||||
{
|
||||
"KDA": 0.35,
|
||||
"synergy_excess": 0.1154,
|
||||
"R_combo": 0.6529
|
||||
},
|
||||
{
|
||||
"KDA": 0.5,
|
||||
"synergy_excess": 0.1225,
|
||||
"R_combo": 0.657
|
||||
},
|
||||
{
|
||||
"KDA": 0.65,
|
||||
"synergy_excess": 0.1309,
|
||||
"R_combo": 0.6588
|
||||
},
|
||||
{
|
||||
"KDA": 0.8,
|
||||
"synergy_excess": 0.1408,
|
||||
"R_combo": 0.6581
|
||||
},
|
||||
{
|
||||
"KDA": 1.0,
|
||||
"synergy_excess": 0.1561,
|
||||
"R_combo": 0.6534
|
||||
},
|
||||
{
|
||||
"KDA": 1.2,
|
||||
"synergy_excess": 0.1716,
|
||||
"R_combo": 0.6452
|
||||
},
|
||||
{
|
||||
"KDA": 1.5,
|
||||
"synergy_excess": 0.1596,
|
||||
"R_combo": 0.6439
|
||||
}
|
||||
],
|
||||
"threshold_peak_KDA": {
|
||||
"KDA": 1.2,
|
||||
"synergy_excess": 0.1716,
|
||||
"R_combo": 0.6452
|
||||
},
|
||||
"model_source": "follicle_cycle.py (8-state excitable ODE, COPASI-calibrated healthy cycle)"
|
||||
}
|
||||
80
data/synergy_retest.json
Normal file
80
data/synergy_retest.json
Normal file
@ -0,0 +1,80 @@
|
||||
{
|
||||
"_description": "정련 예측(시너지∝직교성) 다중-시험 재검정. 겹침端=가법미만(정합); 직교축 포함 조합=초가법(시사적 지지). 수치는 원문 직접추출(PMC BioC / BiomedGrid PDF).",
|
||||
"trials": {
|
||||
"IJT2023_fina×mino": {
|
||||
"src": "IJT 2023 (PMC10495069)",
|
||||
"design": "full 2×2 (FNS/MNX/combo), 24주 모발밀도증가 hairs/cm² 부위평균",
|
||||
"overlap_class": "겹침 높음(피나 W축 × 미녹 W+D)",
|
||||
"arms": {
|
||||
"FNS": 7.84,
|
||||
"MNX": 8.3,
|
||||
"MNF": 9.36
|
||||
},
|
||||
"unit": "hairs/cm²",
|
||||
"full_2x2": true
|
||||
},
|
||||
"FPHL2022_mino±addon": {
|
||||
"src": "Front Med 2022 (PMC9309533)",
|
||||
"design": "미녹 ± add-on (add-on 단독 arm 없음 → full 2×2 아님)",
|
||||
"overlap_class": "스피로=W축(겹침); 미세침=전달/기계(축 아님)",
|
||||
"arms": {
|
||||
"MX": 9.95,
|
||||
"MX+SPT": 16.76,
|
||||
"MX+MN": 30.33
|
||||
},
|
||||
"unit": "hairs/cm²",
|
||||
"full_2x2": false
|
||||
},
|
||||
"TH07_triple": {
|
||||
"src": "TH07 (Am J Biomed Sci Res 2024, BiomedGrid open, CC-BY)",
|
||||
"design": "4-arm: 피나0.1%/라타0.03%/미녹5% 단독 + 삼중. 투자자 성장점수 0~3(반정량). 단독 n=3-4(소).",
|
||||
"overlap_class": "삼중에 **직교쌍(피나 W × 라타노프로스트 비-Wnt D)** 포함",
|
||||
"arms": {
|
||||
"FNS": 0.25,
|
||||
"LAT": 0.33,
|
||||
"MNX": 0.75,
|
||||
"TRIPLE": 2.35
|
||||
},
|
||||
"unit": "성장점수 0~3",
|
||||
"full_2x2": false,
|
||||
"caveats": "n 매우작음(단독3-4)·반정량점수·삼중(쌍 아님)·미녹 침투촉진 교란·산업체·vehicle arm 없음"
|
||||
}
|
||||
},
|
||||
"analysis": {
|
||||
"IJT2023": {
|
||||
"fina_add_benefit": 1.06,
|
||||
"fina_mono": 7.84,
|
||||
"super_additive_excess": -6.78,
|
||||
"verdict": "가법미만(시너지 없음)",
|
||||
"overlap": "높음",
|
||||
"full_2x2": true
|
||||
},
|
||||
"FPHL2022": {
|
||||
"spt_add_benefit": 6.81,
|
||||
"mino_mono": 9.95,
|
||||
"mn_add_benefit": 20.38,
|
||||
"note": "add-on 단독 arm 없음 → full synergy 산출 불가; 스피로(W축) 한계이득이 미녹단독 미만=겹침 정합(약). 미세침=전달/기계(제외).",
|
||||
"verdict": "한계이득만(full 2×2 아님)",
|
||||
"full_2x2": false
|
||||
},
|
||||
"TH07": {
|
||||
"monos": {
|
||||
"FNS": 0.25,
|
||||
"LAT": 0.33,
|
||||
"MNX": 0.75
|
||||
},
|
||||
"mono_sum": 1.33,
|
||||
"triple": 2.35,
|
||||
"linear_synergy": 1.02,
|
||||
"verdict": "초가법(삼중 ≫ 단독합)",
|
||||
"note": "직교쌍(피나 W×라타노 비-Wnt D) 포함 조합이 초가법 — 정련 예측에 시사적 지지. 그러나 한계 큼(아래).",
|
||||
"caveats": "n 매우작음(단독3-4)·반정량점수·삼중(쌍 아님)·미녹 침투촉진 교란·산업체·vehicle arm 없음"
|
||||
}
|
||||
},
|
||||
"verdict": {
|
||||
"overlap_end": "데이터 정합 — 겹침 큰 쌍(IJT 피나×미녹 가법미만; FPHL 스피로 modest)에서 시너지 없음.",
|
||||
"orthogonal_end": "시사적 지지 — 직교축(라타노프로스트 비-Wnt D) 포함 TH07 삼중이 초가법(+1.02). 단 쌍 아님·한계 큼.",
|
||||
"refined_prediction_status": "시사적 지지(확정 아님) — 깨끗한 직교 *쌍* 전향 2×2로 확정 필요(AR차단×PGF2α 등)"
|
||||
},
|
||||
"honest": "겹침 쌍은 가법미만(정합); 직교축 포함 삼중(TH07)은 초가법(정련 예측과 대조적 정합)이나 쌍 아닌 삼중·n3-4·반정량점수·미녹 침투촉진 교란·산업체 → 시사적이지 확정 아님. 수치는 원문 표 직접추출(날조 없음)."
|
||||
}
|
||||
95
data/synergy_revised.json
Normal file
95
data/synergy_revised.json
Normal file
@ -0,0 +1,95 @@
|
||||
{
|
||||
"_description": "IJT 반증 반영 모델 수정(ARM-2 Wnt겹침 overlap) + 독립축 재예측. 시너지는 직교축 쌍에서만 생존; 피나×미녹은 겹침→가법미만(관측 정합).",
|
||||
"diagnosis": "미녹시딜 Wnt/β-catenin 활성 → 피나와 W축 겹침 → 원 모델의 독립가정 위반.",
|
||||
"revision": "ARM-2에 W-겹침 overlap 도입: e_W=1−(1−e1)(1−e2·overlap), e_D=e2(1−overlap).",
|
||||
"overlap_sweep": [
|
||||
{
|
||||
"overlap": 0.0,
|
||||
"R1": 0.386,
|
||||
"R2": 0.242,
|
||||
"R_combo": 0.657,
|
||||
"bliss_expected": 0.535,
|
||||
"synergy_excess": 0.122
|
||||
},
|
||||
{
|
||||
"overlap": 0.2,
|
||||
"R1": 0.424,
|
||||
"R2": 0.333,
|
||||
"R_combo": 0.689,
|
||||
"bliss_expected": 0.616,
|
||||
"synergy_excess": 0.073
|
||||
},
|
||||
{
|
||||
"overlap": 0.4,
|
||||
"R1": 0.471,
|
||||
"R2": 0.425,
|
||||
"R_combo": 0.725,
|
||||
"bliss_expected": 0.696,
|
||||
"synergy_excess": 0.03
|
||||
},
|
||||
{
|
||||
"overlap": 0.5,
|
||||
"R1": 0.499,
|
||||
"R2": 0.472,
|
||||
"R_combo": 0.746,
|
||||
"bliss_expected": 0.735,
|
||||
"synergy_excess": 0.011
|
||||
},
|
||||
{
|
||||
"overlap": 0.6,
|
||||
"R1": 0.532,
|
||||
"R2": 0.519,
|
||||
"R_combo": 0.77,
|
||||
"bliss_expected": 0.775,
|
||||
"synergy_excess": -0.005
|
||||
},
|
||||
{
|
||||
"overlap": 0.8,
|
||||
"R1": 0.617,
|
||||
"R2": 0.622,
|
||||
"R_combo": 0.828,
|
||||
"bliss_expected": 0.855,
|
||||
"synergy_excess": -0.027
|
||||
},
|
||||
{
|
||||
"overlap": 1.0,
|
||||
"R1": 0.744,
|
||||
"R2": 0.744,
|
||||
"R_combo": 0.909,
|
||||
"bliss_expected": 0.934,
|
||||
"synergy_excess": -0.025
|
||||
}
|
||||
],
|
||||
"synergy_crosses_zero_near": 0.6,
|
||||
"pair_predictions": {
|
||||
"피나 × 미녹시딜(부분겹침)": {
|
||||
"overlap": 0.55,
|
||||
"R1": 0.515,
|
||||
"R2": 0.495,
|
||||
"R_combo": 0.758,
|
||||
"bliss_expected": 0.755,
|
||||
"synergy_excess": 0.003,
|
||||
"verdict": "가법"
|
||||
},
|
||||
"AR차단 × Wnt-agonist(완전중복=둘다 W)": {
|
||||
"overlap": 1.0,
|
||||
"R1": 0.744,
|
||||
"R2": 0.744,
|
||||
"R_combo": 0.909,
|
||||
"bliss_expected": 0.934,
|
||||
"synergy_excess": -0.025,
|
||||
"verdict": "가법미만"
|
||||
},
|
||||
"W축 약물 × 순수 D약물(직교, Wnt무관)": {
|
||||
"overlap": 0.0,
|
||||
"R1": 0.386,
|
||||
"R2": 0.242,
|
||||
"R_combo": 0.657,
|
||||
"bliss_expected": 0.535,
|
||||
"synergy_excess": 0.122,
|
||||
"verdict": "초가법(synergy)"
|
||||
}
|
||||
},
|
||||
"new_falsifiable_prediction": "초가법 시너지는 *기전적으로 직교(한쪽 Wnt 무관 D-제제)* 인 쌍에서만 발생. 피나×미녹 같은 부분겹침 쌍은 가법미만(IJT 2023 관측과 일치).",
|
||||
"honest": "overlap은 반증이 가르쳐준 모델 수정(자유예측 아님). 새 예측은 진짜 직교 약물쌍 전향 검정 필요. 미녹 Wnt활성은 문헌 근거(예: 미녹시딜→DP β-catenin↑)."
|
||||
}
|
||||
162
data/synthetic_control_validation.json
Normal file
162
data/synthetic_control_validation.json
Normal file
@ -0,0 +1,162 @@
|
||||
{
|
||||
"twin_control_run": {
|
||||
"AGA": {
|
||||
"equilibrium_density_pct": 42.54,
|
||||
"window_change_24wk": 0.0,
|
||||
"window_change_36wk": 0.0,
|
||||
"source": "run_twin(disease, [], days=600) — 질환 평형(day300)에서 무치료 진행 시 HairDensity 변화"
|
||||
},
|
||||
"AA": {
|
||||
"equilibrium_density_pct": 10.99,
|
||||
"window_change_24wk": 0.0,
|
||||
"window_change_36wk": 0.0,
|
||||
"source": "run_twin(disease, [], days=600) — 질환 평형(day300)에서 무치료 진행 시 HairDensity 변화"
|
||||
}
|
||||
},
|
||||
"diseases": {
|
||||
"AGA": {
|
||||
"metric": "target-area hair count change",
|
||||
"unit": "hairs",
|
||||
"trial_window_wk": 24,
|
||||
"placebo_arms": [
|
||||
{
|
||||
"trial": "NCT01231607",
|
||||
"week": 24,
|
||||
"mean": -4.9,
|
||||
"sd": 7.89,
|
||||
"n": 17
|
||||
},
|
||||
{
|
||||
"trial": "NCT01231607",
|
||||
"week": 12,
|
||||
"mean": -4.0,
|
||||
"sd": 7.22,
|
||||
"n": 17,
|
||||
"secondary": true
|
||||
}
|
||||
],
|
||||
"excluded_arms": [
|
||||
{
|
||||
"trial": "NCT00441116",
|
||||
"reason": "거대면적 macrophotographic 총수(+144) — target-area 척도와 비호환(이질 outlier)"
|
||||
}
|
||||
],
|
||||
"real_placebo_mean": -4.9,
|
||||
"real_placebo_se": 1.91,
|
||||
"n_trials": 1,
|
||||
"n_subjects": 17,
|
||||
"twin_control": 0.0,
|
||||
"twin_control_source": "run_twin(disease, [], days=600) — 질환 평형(day300)에서 무치료 진행 시 HairDensity 변화",
|
||||
"twin_equilibrium_density_pct": 42.54,
|
||||
"equiv_margin": 15.0,
|
||||
"diff_twin_minus_placebo": 4.9,
|
||||
"tost_p_equiv": 0.0,
|
||||
"equivalent": true,
|
||||
"tost_p_equiv_conservative_SE": 0.1003,
|
||||
"natural_history_gap": -4.9,
|
||||
"nat_overlay": -4.9,
|
||||
"nat_overlay_label": "경험적 placebo-response 보정(트윈 기전 아님; 풀링 위약 평균)",
|
||||
"pred_band_sd": 1.91,
|
||||
"arms_covered": "1/1",
|
||||
"treatment": "두타스테리드 0.5mg (동일시험 within-trial)",
|
||||
"treatment_mean": 89.6,
|
||||
"treatment_within_trial": true,
|
||||
"effect_real_placebo": 94.5,
|
||||
"effect_twin_control": 89.6,
|
||||
"effect_twin_raw": 89.6,
|
||||
"effect_twin_corrected": 94.5,
|
||||
"bias": -4.9,
|
||||
"bias_pct_of_effect": -5.2,
|
||||
"bias_raw_pct": -5.2,
|
||||
"bias_corrected_pct": 0.0
|
||||
},
|
||||
"AA": {
|
||||
"metric": "SALT %change",
|
||||
"unit": "%",
|
||||
"trial_window_wk": 36,
|
||||
"placebo_arms": [
|
||||
{
|
||||
"trial": "BRAVE-AA1 (NCT03570749)",
|
||||
"week": 36,
|
||||
"mean": -8.13,
|
||||
"sd": 22.0,
|
||||
"n": 90
|
||||
},
|
||||
{
|
||||
"trial": "BRAVE-AA2 (NCT03899259)",
|
||||
"week": 36,
|
||||
"mean": -2.96,
|
||||
"sd": 20.0,
|
||||
"n": 78
|
||||
}
|
||||
],
|
||||
"excluded_arms": [],
|
||||
"real_placebo_mean": -5.48,
|
||||
"real_placebo_se": 1.62,
|
||||
"n_trials": 2,
|
||||
"n_subjects": 168,
|
||||
"twin_control": 0.0,
|
||||
"twin_control_source": "run_twin(disease, [], days=600) — 질환 평형(day300)에서 무치료 진행 시 HairDensity 변화",
|
||||
"twin_equilibrium_density_pct": 10.99,
|
||||
"equiv_margin": 15.0,
|
||||
"diff_twin_minus_placebo": 5.48,
|
||||
"tost_p_equiv": 0.0,
|
||||
"equivalent": true,
|
||||
"tost_p_equiv_conservative_SE": 0.2601,
|
||||
"natural_history_gap": -5.48,
|
||||
"nat_overlay": -5.48,
|
||||
"nat_overlay_label": "경험적 placebo-response 보정(트윈 기전 아님; 풀링 위약 평균)",
|
||||
"pred_band_sd": 3.66,
|
||||
"arms_covered": "2/2",
|
||||
"treatment": "deuruxolitinib 12mg (THRIVE, 교차시험)",
|
||||
"treatment_mean": -50.4,
|
||||
"treatment_within_trial": false,
|
||||
"effect_real_placebo": -44.92,
|
||||
"effect_twin_control": -50.4,
|
||||
"effect_twin_raw": -50.4,
|
||||
"effect_twin_corrected": -44.92,
|
||||
"bias": -5.48,
|
||||
"bias_pct_of_effect": -12.2,
|
||||
"bias_raw_pct": -12.2,
|
||||
"bias_corrected_pct": 0.0,
|
||||
"nat_overlay_loto": [
|
||||
{
|
||||
"held_out": "BRAVE-AA1 (NCT03570749)",
|
||||
"actual": -8.13,
|
||||
"predicted_overlay": -2.96,
|
||||
"abs_err": 5.17
|
||||
},
|
||||
{
|
||||
"held_out": "BRAVE-AA2 (NCT03899259)",
|
||||
"actual": -2.96,
|
||||
"predicted_overlay": -8.13,
|
||||
"abs_err": 5.17
|
||||
}
|
||||
],
|
||||
"nat_overlay_loto_mae": 5.17,
|
||||
"aa_abs_salt_trajectory": {
|
||||
"trial": "NCT02974868",
|
||||
"weeks": [
|
||||
24
|
||||
],
|
||||
"values_wk24": 1.41,
|
||||
"across_time_range": [
|
||||
0.43,
|
||||
1.77
|
||||
],
|
||||
"note": "절대 SALT점 변화(+=악화), 24주 +1.41 ≈ 0 근방"
|
||||
}
|
||||
}
|
||||
},
|
||||
"concept_note": "트윈-대조군은 *변화공간*에서 '자발변화 없음'(0)을 주장(척도무관). 효과재구성은 각 시험 단위로. 기전 대조군(0)=held-out(위약 미접촉)·공정; 경험 오버레이=별도 층.",
|
||||
"verdict": {
|
||||
"twin_executed": true,
|
||||
"equivalence": "2/2 질환 동등(마진±15)",
|
||||
"max_effect_bias_raw_pct": 12.2,
|
||||
"max_effect_bias_corrected_pct": 0.0,
|
||||
"aa_overlay_loto_mae": 5.17,
|
||||
"usable_for": "큰 치료효과 검출의 mechanistic synthetic control(raw 편향<12%); 오버레이로 소효과 보정(in-sample)",
|
||||
"needs_for_regulatory": "전향 검증 + 공변량 매칭 + 오버레이 교차시험 일반화(>2 arm LOTO)",
|
||||
"honest": "트윈을 실제 실행해 대조군(0) 도출(기전, held-out·공정). 실제 RCT 위약과 마진 내 동등. 자연사는 0으로 둠(AGA 보수/AA 비보수) -> 경험 오버레이로 보정하나 in-sample·LOTO 예비(AA 2arm). 회고적."
|
||||
}
|
||||
}
|
||||
1
data/twin_scenarios.json
Normal file
1
data/twin_scenarios.json
Normal file
File diff suppressed because one or more lines are too long
1
data/validation_report.json
Normal file
1
data/validation_report.json
Normal file
@ -0,0 +1 @@
|
||||
{"summary": {"twin_causal_accuracy": 0.895, "n_causal": 57, "bootstrap_ci95": [0.807, 0.965], "permutation_p": 0.0, "external_transcriptomic_accuracy": 0.577, "external_note": "bulk bald-vs-haired confounded by cell composition; established markers validate but overall inconclusive", "ground_truth_records": 344, "fixes_applied": ["BCL2 (2 independent sources: overexpression->hair loss)", "MSX2 (KO->cyclic alopecia, hair-required)"], "twin_correct_where_sources_err": ["IGF1 (agent mislabel + bulk confound; quote: IGF-1 rescues hair growth)"]}, "method": "3 independent sources: (A) 338 paper experimental outcomes [agent-extracted, quotes], (B) external human AGA transcriptomics GSE36169 [unused], (C) independent expression-only predictor. Bootstrap+permutation 10k.", "honest_caveats": ["twin covers 57/98 testable causal records (others: effect=? or no axis)", "residual mismatches are corpus conflicts(CXCL12), context/dual-role(EGFR,CD44), or agent errors(IGF1) — not systematic twin errors", "external bulk transcriptomic is confounded (composition); not a clean test"], "protein_modules": {"method": "독립 단백질 분석 모듈 (알파폴드 외): STRING PPI 네트워크 + STRING 기능 enrichment(GO/KEGG/Reactome) + 실험 PDB", "axes_ppi_significant": "8/8", "axes_label_matched": "8/8", "diseases_biology_matched": "3/3", "experimental_pdb": "171/210", "note": "트윈 8축 모두 STRING 에서 유의한 PPI 응집(p<0.05)+라벨 일치; 질환셋 기대생물학 일치; 81% 실험구조 보유"}}
|
||||
554
data/validation_results.json
Normal file
554
data/validation_results.json
Normal file
@ -0,0 +1,554 @@
|
||||
{
|
||||
"summary": [
|
||||
{
|
||||
"claim": "AA = INF axis",
|
||||
"rows": [
|
||||
{
|
||||
"design": "Bulk disease (GSE68801, n=122)",
|
||||
"metric": "inflammation\u2191 / hair\u2193",
|
||||
"p": 5.2e-13,
|
||||
"strong": true
|
||||
},
|
||||
{
|
||||
"design": "Bulk treatment (GSE80342, ruxo 0-24wk)",
|
||||
"metric": "infl\u2193 & hair\u2191 in responders",
|
||||
"p": 0.0013,
|
||||
"strong": true
|
||||
},
|
||||
{
|
||||
"design": "Drug-perturbation (GSE167360, 5 JAK-i)",
|
||||
"metric": "INF signature \u2193",
|
||||
"p": 0.0196,
|
||||
"strong": true
|
||||
},
|
||||
{
|
||||
"design": "Single-cell (GSE212450+GSE316832)",
|
||||
"metric": "non-replicating (capture bias)",
|
||||
"p": null,
|
||||
"strong": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"claim": "AGA = DP Wnt suppression",
|
||||
"rows": [
|
||||
{
|
||||
"design": "Single-cell DP (GSE295410)",
|
||||
"metric": "Lef1/Axin2\u2193 +Cxcl12\u2191, 4/4 + reversal",
|
||||
"p": 4e-79,
|
||||
"strong": true
|
||||
},
|
||||
{
|
||||
"design": "Drug-perturbation (GSE178374, DHT)",
|
||||
"metric": "DKK1 \u2191",
|
||||
"p": 0.0011,
|
||||
"strong": true
|
||||
},
|
||||
{
|
||||
"design": "Bulk disease (GSE90594, n=28)",
|
||||
"metric": "hair-keratin \u2193",
|
||||
"p": 0.0041,
|
||||
"strong": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"claim": "Treatment timing",
|
||||
"rows": [
|
||||
{
|
||||
"design": "CT.gov trajectories (THRIVE-AA SALT)",
|
||||
"metric": "JAK fit R\u00b2=0.94-0.99",
|
||||
"p": null,
|
||||
"strong": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"claim": "Causal direction",
|
||||
"rows": [
|
||||
{
|
||||
"design": "340 perturbation\u2192outcome (199 papers)",
|
||||
"metric": "89.5% concordant",
|
||||
"p": 0.0001,
|
||||
"strong": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"molecular": [
|
||||
{
|
||||
"t": "AA disease infl\u2191",
|
||||
"dz": "AA",
|
||||
"p": 1.7e-12
|
||||
},
|
||||
{
|
||||
"t": "AA disease hair\u2193",
|
||||
"dz": "AA",
|
||||
"p": 5.2e-13
|
||||
},
|
||||
{
|
||||
"t": "AA treat infl\u2193",
|
||||
"dz": "AA",
|
||||
"p": 0.0013
|
||||
},
|
||||
{
|
||||
"t": "AA treat hair\u2191",
|
||||
"dz": "AA",
|
||||
"p": 0.0086
|
||||
},
|
||||
{
|
||||
"t": "AA JAK-i INF\u2193",
|
||||
"dz": "AA",
|
||||
"p": 0.0196
|
||||
},
|
||||
{
|
||||
"t": "AGA hair\u2193",
|
||||
"dz": "AGA",
|
||||
"p": 0.0041
|
||||
},
|
||||
{
|
||||
"t": "AGA DHT\u2192DKK1\u2191",
|
||||
"dz": "AGA",
|
||||
"p": 0.0011
|
||||
},
|
||||
{
|
||||
"t": "AGA DP Lef1\u2193(sc)",
|
||||
"dz": "AGA",
|
||||
"p": 4e-79
|
||||
}
|
||||
],
|
||||
"aga_dp": {
|
||||
"conds": [
|
||||
"Con",
|
||||
"TP",
|
||||
"TP+Ab"
|
||||
],
|
||||
"genes": {
|
||||
"Lef1": {
|
||||
"Con": 2.025178909301758,
|
||||
"TP": 1.4451135396957397,
|
||||
"TP+Ab": 1.6020697355270386
|
||||
},
|
||||
"Axin2": {
|
||||
"Con": 0.40885013341903687,
|
||||
"TP": 0.1611478328704834,
|
||||
"TP+Ab": 0.3054564595222473
|
||||
},
|
||||
"Cxcl12": {
|
||||
"Con": 0.09247314929962158,
|
||||
"TP": 0.2333015501499176,
|
||||
"TP+Ab": 0.15628264844417572
|
||||
},
|
||||
"Dkk1": {
|
||||
"Con": 0.00035145474248565733,
|
||||
"TP": 0.0021963915787637234,
|
||||
"TP+Ab": 0.0
|
||||
}
|
||||
}
|
||||
},
|
||||
"jak_drugs": {
|
||||
"Vehicle": 1.263,
|
||||
"JAK1i": -1.024,
|
||||
"JAK2i": 1.272,
|
||||
"JAK3i": -0.807,
|
||||
"RUXO": -0.27,
|
||||
"Tofa": -0.434
|
||||
},
|
||||
"aa_sc": {
|
||||
"GSE212450": {
|
||||
"AA": [
|
||||
52.5,
|
||||
33.6,
|
||||
4.0,
|
||||
48.5
|
||||
],
|
||||
"control": [
|
||||
10.2,
|
||||
8.6,
|
||||
9.5,
|
||||
20.3,
|
||||
7.0,
|
||||
2.6
|
||||
]
|
||||
},
|
||||
"GSE316832": {
|
||||
"AA": [
|
||||
3.6,
|
||||
2.9,
|
||||
3.0,
|
||||
1.9,
|
||||
5.7,
|
||||
1.0,
|
||||
17.2,
|
||||
0.9,
|
||||
7.0,
|
||||
1.6,
|
||||
0.4,
|
||||
4.3
|
||||
],
|
||||
"control": [
|
||||
2.9,
|
||||
0.8,
|
||||
1.2,
|
||||
2.2,
|
||||
6.1,
|
||||
2.2,
|
||||
6.2,
|
||||
2.1,
|
||||
0.0,
|
||||
4.1,
|
||||
0.3
|
||||
]
|
||||
}
|
||||
},
|
||||
"gwas": {
|
||||
"AGA_cov": 0.25,
|
||||
"AA_cov": 0.5625,
|
||||
"AGA_in": [
|
||||
"AR",
|
||||
"SRD5A2",
|
||||
"WNT10A",
|
||||
"FGF5"
|
||||
],
|
||||
"AA_in": [
|
||||
"HLA-DQB1",
|
||||
"CTLA4",
|
||||
"IL2",
|
||||
"IL21",
|
||||
"ULBP3",
|
||||
"IKZF4",
|
||||
"IL13",
|
||||
"CLEC16A",
|
||||
"SH2B3"
|
||||
]
|
||||
},
|
||||
"candidates": [
|
||||
{
|
||||
"gene": "EDA2R",
|
||||
"dz": "AGA",
|
||||
"axis": "AND",
|
||||
"plddt": 68.8,
|
||||
"band": "Low (50\u201370)",
|
||||
"edges_hi": 0,
|
||||
"cohesion": "weak"
|
||||
},
|
||||
{
|
||||
"gene": "TWIST1",
|
||||
"dz": "AGA",
|
||||
"axis": "BMP",
|
||||
"plddt": 66.3,
|
||||
"band": "Low (50\u201370)",
|
||||
"edges_hi": 1,
|
||||
"cohesion": "COHERES"
|
||||
},
|
||||
{
|
||||
"gene": "TWIST2",
|
||||
"dz": "AGA",
|
||||
"axis": "BMP",
|
||||
"plddt": 73.0,
|
||||
"band": "Confident (70\u201390)",
|
||||
"edges_hi": 0,
|
||||
"cohesion": "weak"
|
||||
},
|
||||
{
|
||||
"gene": "RUNX2",
|
||||
"dz": "AGA",
|
||||
"axis": "BMP",
|
||||
"plddt": 58.6,
|
||||
"band": "Low (50\u201370)",
|
||||
"edges_hi": 4,
|
||||
"cohesion": "COHERES"
|
||||
},
|
||||
{
|
||||
"gene": "DKK2",
|
||||
"dz": "AGA",
|
||||
"axis": "Wnt",
|
||||
"plddt": 70.5,
|
||||
"band": "Confident (70\u201390)",
|
||||
"edges_hi": 8,
|
||||
"cohesion": "COHERES"
|
||||
},
|
||||
{
|
||||
"gene": "FZD10",
|
||||
"dz": "AGA",
|
||||
"axis": "Wnt",
|
||||
"plddt": 80.6,
|
||||
"band": "Confident (70\u201390)",
|
||||
"edges_hi": 7,
|
||||
"cohesion": "COHERES"
|
||||
},
|
||||
{
|
||||
"gene": "PAX1",
|
||||
"dz": "AGA",
|
||||
"axis": "DP",
|
||||
"plddt": 54.6,
|
||||
"band": "Low (50\u201370)",
|
||||
"edges_hi": 0,
|
||||
"cohesion": "isolated"
|
||||
},
|
||||
{
|
||||
"gene": "EBF1",
|
||||
"dz": "AGA",
|
||||
"axis": "DP",
|
||||
"plddt": 70.8,
|
||||
"band": "Confident (70\u201390)",
|
||||
"edges_hi": 0,
|
||||
"cohesion": "isolated"
|
||||
},
|
||||
{
|
||||
"gene": "IRF4",
|
||||
"dz": "AGA",
|
||||
"axis": "INF",
|
||||
"plddt": 71.6,
|
||||
"band": "Confident (70\u201390)",
|
||||
"edges_hi": 7,
|
||||
"cohesion": "COHERES"
|
||||
},
|
||||
{
|
||||
"gene": "RORA",
|
||||
"dz": "AGA",
|
||||
"axis": "HFSC",
|
||||
"plddt": 75.3,
|
||||
"band": "Confident (70\u201390)",
|
||||
"edges_hi": 0,
|
||||
"cohesion": "weak"
|
||||
},
|
||||
{
|
||||
"gene": "HDAC9",
|
||||
"dz": "AGA",
|
||||
"axis": "APO",
|
||||
"plddt": 64.2,
|
||||
"band": "Low (50\u201370)",
|
||||
"edges_hi": 1,
|
||||
"cohesion": "COHERES"
|
||||
},
|
||||
{
|
||||
"gene": "AUTS2",
|
||||
"dz": "AGA",
|
||||
"axis": "DP",
|
||||
"plddt": 41.5,
|
||||
"band": "Very low (<50)",
|
||||
"edges_hi": 0,
|
||||
"cohesion": "isolated"
|
||||
},
|
||||
{
|
||||
"gene": "ICOS",
|
||||
"dz": "AA",
|
||||
"axis": "INF",
|
||||
"plddt": 74.0,
|
||||
"band": "Confident (70\u201390)",
|
||||
"edges_hi": 9,
|
||||
"cohesion": "COHERES"
|
||||
},
|
||||
{
|
||||
"gene": "CD28",
|
||||
"dz": "AA",
|
||||
"axis": "INF",
|
||||
"plddt": 81.3,
|
||||
"band": "Confident (70\u201390)",
|
||||
"edges_hi": 11,
|
||||
"cohesion": "COHERES"
|
||||
},
|
||||
{
|
||||
"gene": "IL2RA",
|
||||
"dz": "AA",
|
||||
"axis": "INF",
|
||||
"plddt": 72.5,
|
||||
"band": "Confident (70\u201390)",
|
||||
"edges_hi": 22,
|
||||
"cohesion": "COHERES"
|
||||
},
|
||||
{
|
||||
"gene": "ULBP6",
|
||||
"dz": "AA",
|
||||
"axis": "INF",
|
||||
"plddt": 82.1,
|
||||
"band": "Confident (70\u201390)",
|
||||
"edges_hi": 0,
|
||||
"cohesion": "isolated"
|
||||
},
|
||||
{
|
||||
"gene": "ERBB3",
|
||||
"dz": "AA",
|
||||
"axis": "DP",
|
||||
"plddt": 72.5,
|
||||
"band": "Confident (70\u201390)",
|
||||
"edges_hi": 9,
|
||||
"cohesion": "COHERES"
|
||||
},
|
||||
{
|
||||
"gene": "PRDX5",
|
||||
"dz": "AA",
|
||||
"axis": "APO",
|
||||
"plddt": 85.3,
|
||||
"band": "Confident (70\u201390)",
|
||||
"edges_hi": 2,
|
||||
"cohesion": "COHERES"
|
||||
},
|
||||
{
|
||||
"gene": "STX17",
|
||||
"dz": "AA",
|
||||
"axis": "APO",
|
||||
"plddt": 69.1,
|
||||
"band": "Low (50\u201370)",
|
||||
"edges_hi": 2,
|
||||
"cohesion": "COHERES"
|
||||
}
|
||||
],
|
||||
"landscape": {
|
||||
"headline": {
|
||||
"gb": 28.2,
|
||||
"files": 89,
|
||||
"waves": 4,
|
||||
"agents": 22,
|
||||
"datasets_downloaded": 30
|
||||
},
|
||||
"by_modality": [
|
||||
{
|
||||
"mod": "scRNA/scATAC",
|
||||
"dl": 12,
|
||||
"rec": 3
|
||||
},
|
||||
{
|
||||
"mod": "\uacf5\uac04(spatial)",
|
||||
"dl": 3,
|
||||
"rec": 2
|
||||
},
|
||||
{
|
||||
"mod": "\ubc8c\ud06c RNA",
|
||||
"dl": 14,
|
||||
"rec": 6
|
||||
},
|
||||
{
|
||||
"mod": "GWAS/eQTL",
|
||||
"dl": 3,
|
||||
"rec": 2
|
||||
},
|
||||
{
|
||||
"mod": "\uc57d\ubb3c\uc12d\ub3d9",
|
||||
"dl": 3,
|
||||
"rec": 2
|
||||
},
|
||||
{
|
||||
"mod": "\ub2e8\ubc31\uccb4/\uc544\ud2c0\ub77c\uc2a4",
|
||||
"dl": 1,
|
||||
"rec": 5
|
||||
}
|
||||
],
|
||||
"by_disease": [
|
||||
{
|
||||
"d": "AA",
|
||||
"n": 11
|
||||
},
|
||||
{
|
||||
"d": "AGA",
|
||||
"n": 8
|
||||
},
|
||||
{
|
||||
"d": "\ubaa8\ub0ad\uc8fc\uae30/HFSC",
|
||||
"n": 7
|
||||
},
|
||||
{
|
||||
"d": "\ubc18\ud754/HS/\ud56d\uc554",
|
||||
"n": 4
|
||||
},
|
||||
{
|
||||
"d": "\uc815\uc0c1 \ub808\ud37c\ub7f0\uc2a4",
|
||||
"n": 3
|
||||
}
|
||||
]
|
||||
},
|
||||
"timing": {
|
||||
"months": [
|
||||
0,
|
||||
0.5,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
6,
|
||||
9,
|
||||
12,
|
||||
18,
|
||||
24,
|
||||
30,
|
||||
36
|
||||
],
|
||||
"curves": {
|
||||
"\ud53c\ub098\uc2a4\ud14c\ub9ac\ub4dc": [
|
||||
-2,
|
||||
-5,
|
||||
-4,
|
||||
0,
|
||||
15,
|
||||
28,
|
||||
49,
|
||||
69,
|
||||
81,
|
||||
93,
|
||||
97,
|
||||
99,
|
||||
100
|
||||
],
|
||||
"\ub450\ud0c0\uc2a4\ud14c\ub9ac\ub4dc": [
|
||||
-2,
|
||||
-5,
|
||||
-4,
|
||||
0,
|
||||
18,
|
||||
33,
|
||||
55,
|
||||
75,
|
||||
86,
|
||||
96,
|
||||
99,
|
||||
100,
|
||||
100
|
||||
],
|
||||
"\ubbf8\ub179\uc2dc\ub51c": [
|
||||
-5,
|
||||
-13,
|
||||
-12,
|
||||
11,
|
||||
31,
|
||||
46,
|
||||
68,
|
||||
85,
|
||||
93,
|
||||
98,
|
||||
100,
|
||||
100,
|
||||
100
|
||||
],
|
||||
"JAK\uc5b5\uc81c\uc81c": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
22,
|
||||
39,
|
||||
53,
|
||||
71,
|
||||
86,
|
||||
94,
|
||||
99,
|
||||
100,
|
||||
100,
|
||||
100
|
||||
]
|
||||
}
|
||||
},
|
||||
"benchmark": {
|
||||
"labels": [
|
||||
"\uc815\uc0c1 anagen%",
|
||||
"AGA \ubc00\ub3c4%(\uc815\uc0c1\ub300\ube44)"
|
||||
],
|
||||
"Halloy": [
|
||||
87,
|
||||
42.8
|
||||
],
|
||||
"\ud2b8\uc708": [
|
||||
85,
|
||||
42.5
|
||||
]
|
||||
}
|
||||
}
|
||||
BIN
images/apoptosis_pathway.png
Normal file
BIN
images/apoptosis_pathway.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 771 KiB |
BIN
images/jak_stat_pathway.png
Normal file
BIN
images/jak_stat_pathway.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 762 KiB |
BIN
images/wnt_pathway.png
Normal file
BIN
images/wnt_pathway.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 719 KiB |
576
index.html
Normal file
576
index.html
Normal file
@ -0,0 +1,576 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Alopecia Protein Digital Twin (탈모 단백질 디지털 트윈)</title>
|
||||
<meta name="description" content="문헌 기반 탈모 단백질 추적 디지털 트윈 — AlphaFold 구조 · 지식그래프 · 모낭 주기 ODE 시뮬레이션.">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" as="style" crossorigin
|
||||
href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.css">
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/ngl@2.3.1/dist/ngl.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/build/three.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<link rel="stylesheet" href="css/twin.css">
|
||||
<link rel="stylesheet" href="css/editorial.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
<header class="dashboard-header">
|
||||
<div class="logo-area">
|
||||
<div class="dna-icon">
|
||||
<svg width="36" height="36" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M12 2C8.13 2 5 5.13 5 9c0 3.87 3.13 7 7 7s7-3.13 7-7c0-3.87-3.13-7-7-7zm0 12c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5z"
|
||||
fill="currentColor" />
|
||||
<circle cx="12" cy="19" r="3" fill="currentColor" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1>Alopecia <span class="gradient-text">Protein Digital Twin</span> <span class="ko-title">(탈모 단백질 디지털 트윈)</span></h1>
|
||||
<p class="subtitle">Literature-grounded protein tracking · AlphaFold structures · hair-cycle ODE twin</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-stats">
|
||||
<div class="stat-pill"><div class="pulse-dot"></div><span id="stat-proteins">0</span> Proteins (단백질)</div>
|
||||
<div class="stat-pill"><span id="stat-structures">0</span> AlphaFold 구조</div>
|
||||
<div class="stat-pill" title="실제 분석 논문 전문에서 근거 확보한 단백질"><span id="stat-grounding">0</span> 논문 전문 근거</div>
|
||||
<div class="stat-pill updated">Updated: <span id="last-updated">—</span></div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav class="tab-nav">
|
||||
<button class="tab-btn active" data-tab="twin">단백질 트윈</button>
|
||||
<button class="tab-btn" data-tab="timeline">치료 타임라인</button>
|
||||
<button class="tab-btn" data-tab="network">단백질 네트워크</button>
|
||||
<button class="tab-btn" data-tab="atlas">단백질 아틀라스</button>
|
||||
<button class="tab-btn" data-tab="graph">지식그래프</button>
|
||||
<button class="tab-btn" data-tab="calibrate">정량보정</button>
|
||||
<button class="tab-btn" data-tab="validation">검증</button>
|
||||
<button class="tab-btn" data-tab="stats">통계</button>
|
||||
<button class="tab-btn" data-tab="papers">논문</button>
|
||||
</nav>
|
||||
|
||||
<!-- ============== TAB 1: PROTEIN TWIN ============== -->
|
||||
<div id="tab-twin" class="tab-content active">
|
||||
<div class="twin-layout">
|
||||
<!-- CONTROLS -->
|
||||
<section class="panel glass-panel twin-controls">
|
||||
<h2>모낭 디지털 트윈 (Hair-Follicle Twin)</h2>
|
||||
<p class="panel-subtitle">질환을 고르고 치료 개입을 조합하면, 모낭 주기 ODE 모델이 핵심 단백질의 시간 궤적과 모발 밀도를 시뮬레이션합니다.</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label>대상 질환 (Disease)</label>
|
||||
<div class="seg-control" id="disease-seg"></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>치료 개입 (Interventions) — 복수 선택</label>
|
||||
<div id="intervention-list" class="chip-list"></div>
|
||||
</div>
|
||||
|
||||
<div class="disease-desc" id="disease-desc"></div>
|
||||
|
||||
<div class="metrics-row" id="twin-metrics"></div>
|
||||
|
||||
<div class="tracked-genes">
|
||||
<h4>추적 표적 단백질 (Tracked targets)</h4>
|
||||
<div id="tracked-genes-list" class="mini-chip-list"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- VISUALIZATION -->
|
||||
<section class="panel glass-panel twin-viz">
|
||||
<div class="viz-head">
|
||||
<h3>모발 밀도 추이 (Hair Density, % of healthy baseline)</h3>
|
||||
<div class="badge pulse-badge" id="twin-status">Simulation Active</div>
|
||||
</div>
|
||||
<div class="chart-box"><canvas id="chart-hair"></canvas></div>
|
||||
|
||||
<div class="viz-head">
|
||||
<h3>단백질 활성 궤적 (Protein activity over time)</h3>
|
||||
<select id="protein-trace-mode" class="mini-select">
|
||||
<option value="key">핵심 단백질 (key drivers)</option>
|
||||
<option value="all">전체 readout</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="chart-box"><canvas id="chart-proteins"></canvas></div>
|
||||
</section>
|
||||
|
||||
<!-- COMPARE + LIVE -->
|
||||
<section class="panel glass-panel twin-side">
|
||||
<h3>치료 시나리오 비교 (Compare)</h3>
|
||||
<p class="panel-subtitle">현재 질환의 치료별 최종 모발 밀도.</p>
|
||||
<div class="chart-box small"><canvas id="chart-compare"></canvas></div>
|
||||
|
||||
<h3 style="margin-top:18px;">라이브 What-if (실시간 슬라이더)</h3>
|
||||
<p class="panel-subtitle">병태 부하를 직접 조절해 트윈을 실시간 재계산.</p>
|
||||
<div class="slider-group">
|
||||
<label>안드로겐(DHT) 부하 <span id="val-and">—</span></label>
|
||||
<input type="range" id="slider-and" min="0" max="1.2" step="0.02" value="0.1">
|
||||
</div>
|
||||
<div class="slider-group">
|
||||
<label>염증/JAK-STAT 부하 <span id="val-inf">—</span></label>
|
||||
<input type="range" id="slider-inf" min="0" max="1.2" step="0.02" value="0.05">
|
||||
</div>
|
||||
<div class="slider-group">
|
||||
<label>Wnt 부스트 (uWnt) <span id="val-wnt">—</span></label>
|
||||
<input type="range" id="slider-wnt" min="0" max="0.8" step="0.02" value="0">
|
||||
</div>
|
||||
<div class="slider-group">
|
||||
<label>진피유두 부스트 (uDP) <span id="val-dp">—</span></label>
|
||||
<input type="range" id="slider-dp" min="0" max="1.2" step="0.02" value="0">
|
||||
</div>
|
||||
<button id="btn-reset-live" class="btn-ghost">슬라이더 리셋</button>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============== TAB 2: PROTEIN ATLAS ============== -->
|
||||
<div id="tab-atlas" class="tab-content">
|
||||
<div class="atlas-layout">
|
||||
<section class="panel glass-panel atlas-list-panel">
|
||||
<div class="atlas-controls">
|
||||
<input type="text" id="atlas-search" class="search-input" placeholder="🔍 유전자/단백질 검색 (예: AR, JAK1, β-catenin)">
|
||||
<select id="atlas-axis" class="mini-select"><option value="">전체 트윈 축</option></select>
|
||||
<select id="atlas-disease" class="mini-select"><option value="">전체 질환</option></select>
|
||||
<select id="atlas-sort" class="mini-select">
|
||||
<option value="evidence">근거논문순</option>
|
||||
<option value="mention">언급빈도순</option>
|
||||
<option value="plddt">구조신뢰도순</option>
|
||||
<option value="gene">가나다순</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="atlas-count"><span id="atlas-count">0</span> proteins</div>
|
||||
<div id="atlas-grid" class="atlas-grid"></div>
|
||||
</section>
|
||||
|
||||
<section class="panel glass-panel atlas-detail-panel">
|
||||
<div id="atlas-detail-empty" class="centered-content">
|
||||
<div class="hologram-ring"></div>
|
||||
<div class="placeholder-text">
|
||||
<h3>단백질 선택 (Select a protein)</h3>
|
||||
<p>좌측 목록에서 단백질을 클릭하면 AlphaFold 3D 구조와 근거가 표시됩니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="atlas-detail" class="hidden">
|
||||
<div class="detail-head">
|
||||
<div>
|
||||
<h2 id="d-gene">—</h2>
|
||||
<p id="d-name" class="d-name">—</p>
|
||||
</div>
|
||||
<div class="detail-badges" id="d-badges"></div>
|
||||
</div>
|
||||
<div id="ngl-viewer" class="ngl-viewer"></div>
|
||||
<div class="plddt-legend">
|
||||
<span>pLDDT:</span>
|
||||
<span class="lg lg-vh">매우높음 ≥90</span>
|
||||
<span class="lg lg-c">신뢰 70–90</span>
|
||||
<span class="lg lg-l">낮음 50–70</span>
|
||||
<span class="lg lg-vl">매우낮음 <50</span>
|
||||
<span id="d-plddt" class="plddt-val">—</span>
|
||||
</div>
|
||||
<div class="detail-grid" id="d-meta"></div>
|
||||
<div class="detail-section">
|
||||
<h4>기전 (Mechanism)</h4>
|
||||
<p id="d-mech">—</p>
|
||||
</div>
|
||||
<div class="detail-section">
|
||||
<h4>표적 약물·물질 (Drugs / agents)</h4>
|
||||
<div id="d-drugs" class="mini-chip-list"></div>
|
||||
</div>
|
||||
<div class="detail-section">
|
||||
<h4>근거 논문 (Evidence) <span id="d-evcount" class="badge small">0</span></h4>
|
||||
<div id="d-evidence" class="evidence-list"></div>
|
||||
</div>
|
||||
<div class="detail-section">
|
||||
<h4>📄 실제 논문 전문 근거 (Full-text grounding, 검증 인용) <span id="d-groundcount" class="badge small">0</span></h4>
|
||||
<div id="d-grounding" class="grounding-list"></div>
|
||||
</div>
|
||||
<div class="detail-links" id="d-links"></div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============== TAB 3: KNOWLEDGE GRAPH ============== -->
|
||||
<div id="tab-graph" class="tab-content">
|
||||
<section class="panel glass-panel full-panel">
|
||||
<div class="graph-controls">
|
||||
<h2>지식그래프 (Knowledge Graph)</h2>
|
||||
<div class="graph-legend">
|
||||
<span class="gl gl-protein">단백질</span>
|
||||
<span class="gl gl-pathway">경로</span>
|
||||
<span class="gl gl-disease">질환</span>
|
||||
<span class="gl gl-axis">트윈축</span>
|
||||
<span class="gl gl-drug">약물</span>
|
||||
</div>
|
||||
<div class="graph-filters">
|
||||
<label><input type="checkbox" id="g-show-drug"> 약물 노드 표시</label>
|
||||
<select id="g-focus-disease" class="mini-select"><option value="">전체</option></select>
|
||||
<button id="g-reheat" class="btn-ghost">재배치</button>
|
||||
</div>
|
||||
</div>
|
||||
<canvas id="graph-canvas"></canvas>
|
||||
<div id="graph-tooltip" class="graph-tooltip hidden"></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- ============== TAB: CALIBRATION ============== -->
|
||||
<div id="tab-calibrate" class="tab-content">
|
||||
<section class="panel glass-panel">
|
||||
<div class="cal-head">
|
||||
<div>
|
||||
<h2>모낭주기 정량 보정 (COPASI Parameter Estimation)</h2>
|
||||
<p class="panel-subtitle">모낭 주기 ODE 모델의 파라미터를 <b>transcriptomic 시계열</b>에 COPASI(Levenberg-Marquardt)로 추정하여 정량 보정. scipy(TRF) 워밍업 + COPASI polish.</p>
|
||||
</div>
|
||||
<div class="cal-target-sel">
|
||||
<label>보정 타깃</label>
|
||||
<select id="cal-target" class="mini-select"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div id="cal-validation" class="cal-validation"></div>
|
||||
<div id="cal-summary" class="cal-summary"></div>
|
||||
<div id="cal-grid" class="cal-grid"></div>
|
||||
<div class="cal-foot">
|
||||
<div class="cal-params">
|
||||
<h4>추정 파라미터 (estimated by COPASI) <span class="badge small" id="cal-nparam">0</span></h4>
|
||||
<div id="cal-param-table" class="cal-param-table"></div>
|
||||
</div>
|
||||
<div class="cal-prov">
|
||||
<h4>데이터 출처 / Provenance</h4>
|
||||
<div id="cal-provenance" class="cal-provenance"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cal-coupled-section">
|
||||
<h4>🔗 Cycle↔Chronic 결합 (COMBINE) — 보정 주기에 질환 구동 변조</h4>
|
||||
<p class="panel-subtitle">데이터 보정된 모낭주기 모델을 질환·치료로 변조 → anagen 비율이 disease-specific 주기 왜곡으로 창발.</p>
|
||||
<div id="cal-coupled" class="cal-coupled"></div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- ============== TAB 4: STATISTICS ============== -->
|
||||
<div id="tab-stats" class="tab-content">
|
||||
<div class="stats-grid">
|
||||
<section class="panel glass-panel chart-panel">
|
||||
<h2>질환 분포 (Disease)</h2>
|
||||
<div class="chart-wrapper"><canvas id="chart-disease"></canvas></div>
|
||||
</section>
|
||||
<section class="panel glass-panel chart-panel">
|
||||
<h2>트윈 축별 단백질 (Proteins per twin axis)</h2>
|
||||
<div class="chart-wrapper"><canvas id="chart-axis"></canvas></div>
|
||||
</section>
|
||||
<section class="panel glass-panel chart-panel wide">
|
||||
<h2>연도별 논문 추이 (Publications by year)</h2>
|
||||
<div class="chart-wrapper tall"><canvas id="chart-trend"></canvas></div>
|
||||
</section>
|
||||
<section class="panel glass-panel chart-panel">
|
||||
<h2>경로별 단백질 (Top pathways)</h2>
|
||||
<div class="chart-wrapper"><canvas id="chart-pathway"></canvas></div>
|
||||
</section>
|
||||
<section class="panel glass-panel chart-panel">
|
||||
<h2>치료 모달리티 (Modality)</h2>
|
||||
<div class="chart-wrapper"><canvas id="chart-modality"></canvas></div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============== TAB: TREATMENT TIMELINE ============== -->
|
||||
<div id="tab-network" class="tab-content">
|
||||
<section class="panel glass-panel">
|
||||
<h2>단백질 상호작용 네트워크 <span class="badge small">시간축</span></h2>
|
||||
<p class="panel-subtitle">치료(기전)를 넣으면 <b>어떤 단백질과 상호작용</b>하는지, 그 효과가 <b>개월별로</b> 신호망을 타고 전파되는 것을 보여줍니다. 노드=단백질(밝기·크기=활성), 초록 화살=활성·빨강 막대=억제. 표적 그룹은 점선 링으로 강조. <b>기전 차이</b>: 피나=상류 AR·DKK1 <b>차단</b>(DKK1 어두워짐) vs 미녹=하류 DP·모발 <b>부양</b>(DKK1은 그대로). ※ 활성=질환평형+회복·r(t)·(건강−질환), r(t)는 임상-보정 lag/τ.</p>
|
||||
<div class="net-controls">
|
||||
<label>질환 <select id="net-disease" class="net-sel"></select></label>
|
||||
<label>치료(기전) <select id="net-treat" class="net-sel"></select></label>
|
||||
<button id="net-mode" class="btn-ghost">🧬 AlphaFold 구조</button>
|
||||
<button id="net-play" class="btn-ghost">▶ 재생</button>
|
||||
<input type="range" id="net-time" class="net-slider">
|
||||
<span class="net-month" id="net-month">0개월</span>
|
||||
</div>
|
||||
<div id="net-status" class="net-status"></div>
|
||||
<div class="net-stage">
|
||||
<div id="net-wrap" class="net-wrap"><canvas id="net-canvas"></canvas><div id="net-ngl" class="net-ngl" style="display:none"></div><div id="net-ngl-load" class="net-ngl-load" style="display:none"></div></div>
|
||||
<div class="net-foll">
|
||||
<div class="net-foll-title">모낭 단면 — 시간대별 변화</div>
|
||||
<canvas id="net-follicle"></canvas>
|
||||
<div id="net-foll-state" class="net-foll-state"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="net-legend" class="net-legend"></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div id="tab-timeline" class="tab-content">
|
||||
<div class="tl-disclaimer">
|
||||
<b>⚠ 이미지 변형은 임상 예측이 아닙니다.</b>
|
||||
회복의 <b>양</b>(치료 전후 밀도)은 디지털 트윈 모델, <b>속도</b>(언제·얼마나 빨리)는 <b>임상시험 문헌</b>으로 보정합니다.
|
||||
사진 변화는 이 곡선이 구동하는 <b>절차적 일러스트</b>이며, 환부의 <b>위치·모양은 업로드 사진</b>에서 옵니다. 생성형 AI 아님 · 개별 환자 예측 아님.
|
||||
<span class="tl-disc2">※ 시간축 = <b>실궤적 적합 + 기계론(모낭주기) 화해</b>: JAK은 CT.gov SALT 다시점에 <b>R²0.94~0.99</b> 검증; AGA 가시발모는 <b>텔로젠 바닥(~2개월)</b>에 묶임(약물 PK는 빠르나 모발은 느림). 단일 지수라 AGA 이상성·피나 2년후 감소·미녹 재퇴행은 미반영(한계). 출처 CT.gov(THRIVE·ALLEGRO·NCT01231607)·Kaufman 1998·PK/PD 문헌.</span>
|
||||
</div>
|
||||
<div class="tl-layout">
|
||||
<section class="panel glass-panel tl-controls">
|
||||
<h2>치료 타임라인 시뮬레이션</h2>
|
||||
<p class="panel-subtitle">사진을 올리고 질환·치료를 고른 뒤, 환부를 드래그로 표시하면 모델 곡선대로 변화를 스크럽합니다.</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label>대상 / 모델 (Disease)</label>
|
||||
<div class="seg-control" id="tl-disease"></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>치료 개입 (Intervention) — 복수 선택</label>
|
||||
<div id="tl-interventions" class="chip-list"></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>사진 업로드</label>
|
||||
<div class="tl-upload">
|
||||
<label class="btn-primary" for="tl-file">사진 선택</label>
|
||||
<input type="file" id="tl-file" accept="image/*" hidden>
|
||||
<button class="btn-ghost" id="tl-demo">데모 이미지</button>
|
||||
<button class="btn-ghost" id="tl-remark">환부 다시 표시</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tl-metrics">
|
||||
<div class="metric-card"><div class="mv" id="tl-density">—</div><div class="ml">모발 밀도(모델)</div></div>
|
||||
<div class="metric-card good"><div class="mv" id="tl-recovery">—</div><div class="ml">회복도</div></div>
|
||||
</div>
|
||||
<div class="tl-ivs-line">적용: <b id="tl-ivs-readout">—</b></div>
|
||||
<div class="tl-chart-wrap"><canvas id="chart-timeline"></canvas></div>
|
||||
<p class="tl-cap">↑ 밀도 곡선 (양=트윈 모델 · 속도=임상 문헌) — 프로그레스 바가 이 값을 따라갑니다.</p>
|
||||
</section>
|
||||
|
||||
<section class="panel glass-panel tl-stage empty" id="tl-stage">
|
||||
<div class="tl-canvas-wrap">
|
||||
<canvas id="tl-canvas"></canvas>
|
||||
<div class="tl-empty-hint">사진을 업로드하거나 <b>데모 이미지</b>를 눌러 시작하세요.</div>
|
||||
<div class="tl-hint hidden" id="tl-hint">환부를 <b>드래그</b>로 표시하세요 (중심에서 바깥으로)</div>
|
||||
</div>
|
||||
<div class="tl-scrubber">
|
||||
<div class="tl-month"><span id="tl-month-label">0개월</span> <span id="tl-month-sub" class="tl-month-sub">(0.0년)</span></div>
|
||||
<input type="range" id="tl-slider" min="0" max="36" step="1" value="0">
|
||||
<div class="tl-ticks"><span>0</span><span>4주</span><span>3개월</span><span>6개월</span><span>1년</span><span>3년</span></div>
|
||||
<div class="tl-axisnote">시간축 = 임상 개월(문헌 보정). 초기 탈락(미녹·피나 2–8주) → 수개월 반응 → 1–2년 평탄.</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============== TAB: VALIDATION ============== -->
|
||||
<div id="tab-validation" class="tab-content">
|
||||
<section class="panel glass-panel val-intro">
|
||||
<h2>다층 독립 검증 (Multi-layer Validation)</h2>
|
||||
<p class="panel-subtitle">트윈의 인과 기전을 <b>모델 구성에 쓰지 않은 독립 데이터</b>(벌크·단일세포·약물섭동·GWAS·구조)로 검증. 약하거나 비재현인 결과도 정직하게 표시합니다 — 검증 = 직교 증거의 수렴.</p>
|
||||
<div id="val-summary" class="val-summary-grid"></div>
|
||||
</section>
|
||||
<section class="panel glass-panel val-landscape">
|
||||
<h3>데이터 지형 — 수집 전모 (4 웨이브 · 22 에이전트)</h3>
|
||||
<div id="val-headline" class="val-headline"></div>
|
||||
<div class="val-land-grid">
|
||||
<div><p class="val-cap">모달리티별 (다운로드 / 기록·게이트)</p><div class="val-chart"><canvas id="chart-val-modality"></canvas></div></div>
|
||||
<div><p class="val-cap">질환별 데이터셋 수</p><div class="val-chart"><canvas id="chart-val-disease"></canvas></div></div>
|
||||
</div>
|
||||
</section>
|
||||
<div class="val-grid">
|
||||
<section class="panel glass-panel">
|
||||
<h3>분자 검증 유의도 (−log₁₀ p)</h3>
|
||||
<p class="val-cap">독립 벌크/섭동 데이터의 기전 검정 — 막대가 길수록 유의.</p>
|
||||
<div class="val-chart"><canvas id="chart-val-mol"></canvas></div>
|
||||
</section>
|
||||
<section class="panel glass-panel">
|
||||
<h3>AGA: DP세포 Wnt 억제 + 치료 역전</h3>
|
||||
<p class="val-cap">GSE295410 마우스 진피유두 — Con→TP(AGA)→TP+Ab(치료). Wnt(Lef1/Axin2)↓·Dkk1/Cxcl12↑, 치료로 역전.</p>
|
||||
<div class="val-chart"><canvas id="chart-val-agadp"></canvas></div>
|
||||
</section>
|
||||
<section class="panel glass-panel">
|
||||
<h3>약물섭동: JAK억제제별 염증신호</h3>
|
||||
<p class="val-cap">AA 마우스 — vehicle 대비 JAK1/3·Tofa·Ruxo는 염증↓, <b>JAK2i만 무효</b>(임상 타깃과 일치).</p>
|
||||
<div class="val-chart"><canvas id="chart-val-jak"></canvas></div>
|
||||
</section>
|
||||
<section class="panel glass-panel">
|
||||
<h3>AA 단일세포 — 비재현 (정직)</h3>
|
||||
<p class="val-cap">두 코호트 T세포 포획이 9배 차이 → 단일세포 면역정량은 프로토콜 의존, 코호트 불일치. AA 근거는 벌크가 robust.</p>
|
||||
<div class="val-chart"><canvas id="chart-val-aasc"></canvas></div>
|
||||
</section>
|
||||
<section class="panel glass-panel">
|
||||
<h3>GWAS 카탈로그 커버리지</h3>
|
||||
<p class="val-cap">위험유전자 중 카탈로그 보유 비율. 보유 유전자의 축 배정은 GWAS와 일관(직교 검증).</p>
|
||||
<div class="val-donuts">
|
||||
<div class="val-donut"><canvas id="chart-val-gwasAGA"></canvas><div class="val-donut-lab">AGA</div></div>
|
||||
<div class="val-donut"><canvas id="chart-val-gwasAA"></canvas><div class="val-donut-lab">AA</div></div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="panel glass-panel val-wide">
|
||||
<h3>GWAS 후보 × 구조(AlphaFold) × 네트워크(STRING) 수렴</h3>
|
||||
<p class="val-cap">신규 후보 19개: AlphaFold 구조 신뢰도(pLDDT) + 배정 축과의 STRING 응집. ⚠ 구조·네트워크는 <b>질환 인과를 증명하지 않음</b>(그건 GWAS) — 직교 면을 검증.</p>
|
||||
<div id="val-candidates"></div>
|
||||
</section>
|
||||
<section class="panel glass-panel val-wide">
|
||||
<h3>임상 시간축 — 약물별 회복 곡선 (보정됨)</h3>
|
||||
<p class="val-cap">회복 '양'=트윈 모델, '속도'=임상 문헌 동역학으로 보정. 미녹시딜·피나는 초기 telogen 탈락(음수), JAK은 빠른 onset.</p>
|
||||
<div class="val-chart" style="height:280px"><canvas id="chart-val-timing"></canvas></div>
|
||||
</section>
|
||||
<section class="panel glass-panel">
|
||||
<h3>기존 모델 벤치마크 — Halloy vs 트윈</h3>
|
||||
<p class="val-cap">완전히 다른 원리(모낭 자동자 vs 신호 ODE)인데 정상 anagen·AGA 밀도 수렴.</p>
|
||||
<div class="val-chart"><canvas id="chart-val-bench"></canvas></div>
|
||||
</section>
|
||||
<section class="panel glass-panel val-wide val-credible">
|
||||
<div class="val-cred-head"><span class="val-cred-tag" style="background:var(--accent,#1f5d52)">ex vivo</span>
|
||||
<h3>실제 ex vivo 인체 모낭 검증 — GSE267664 (DHT)</h3></div>
|
||||
<p class="val-cap">직접 실험 대신, 공개된 <b>ex vivo 인체 모낭 organ-modeled</b> RNA-seq(DHT vs control, AGA 전두부 모낭, n=3)로 트윈을 검증. 트윈의 <b>AGA/DHT→Wnt억제</b> 예측(Wnt 길항자↑·Wnt표적↓·모발케라틴↓)이 <b>10/11 방향 일치, 부호검정 p=0.0059</b>. DKK1 +1.13은 DP세포(GSE178374 +1.57)와 일관 — <b>전체 모낭에서도 재현</b>. <b>정직:</b> n=3 소규모라 개별 q≈1; 신호는 Wnt축 방향 협응이지 개별 유의 아님(BMP·비정준 Wnt 제외).</p>
|
||||
<div id="val-exvivo-headline" class="val-headline"></div>
|
||||
<div class="val-chart" style="height:260px;margin-top:10px"><canvas id="chart-val-exvivo"></canvas></div>
|
||||
</section>
|
||||
<section class="panel glass-panel val-wide val-credible">
|
||||
<div class="val-cred-head"><span class="val-cred-tag">신뢰성 ①</span>
|
||||
<h3>반증가능 신규 예측 — AGA 병용요법 시너지</h3></div>
|
||||
<p class="val-cap">Hair 산출 A=kAp·[W²/(KWA²+W²)]·[D²/(KDA²+D²)] 는 Wnt·DP 두 협동 문턱의 <b>AND-게이트</b>. 상류 '브레이크 해제'(AR/5-ARI)×하류 '가속'(미녹시딜)을 병용하면 곱으로 결합 → <b>초가법적 시너지</b>가 모델 구조에서 창발. 실험에서 가법/길항이면 AND-게이트 가정이 반증됨(사전등록: PREREGISTRATION.md).</p>
|
||||
<div id="val-syn-headline" class="val-headline"></div>
|
||||
<div id="val-syn-clinical" class="ipd-warn"></div>
|
||||
<div class="val-land-grid">
|
||||
<div><div class="val-chart" style="height:260px"><canvas id="chart-val-synergy"></canvas></div></div>
|
||||
<div><div class="val-chart" style="height:260px"><canvas id="chart-val-synergy-kda"></canvas></div></div>
|
||||
</div>
|
||||
<div class="val-chart" style="height:240px;margin-top:12px"><canvas id="chart-val-synclin"></canvas></div>
|
||||
<div id="val-synrev-note" class="ipd-verdict" style="border-left:3px solid var(--accent,#1f5d52);padding-left:10px;margin-top:12px"></div>
|
||||
<div class="val-chart" style="height:230px;margin-top:8px"><canvas id="chart-val-synrev"></canvas></div>
|
||||
<div id="val-retest-note" class="ipd-verdict" style="border-left:3px solid var(--accent,#1f5d52);padding-left:10px;margin-top:10px"></div>
|
||||
</section>
|
||||
<section class="panel glass-panel val-wide val-credible">
|
||||
<div class="val-cred-head"><span class="val-cred-tag">신뢰성 ②</span>
|
||||
<h3>불확실성 정량(UQ) — 보정된 신뢰구간</h3></div>
|
||||
<p class="val-cap">타이밍 모델을 실제 임상 궤적에 <b>계층적 베이즈</b>로 보정. 핵심 정직성 체크 = leave-one-trajectory-out 커버리지: 단순 풀링 구간은 <b>과신(59%)</b>이었으나 시험 간 이질성을 넣자 <b>명목 90%로 수렴(98%)</b>. 트윈이 "X%"가 아니라 "X%[구간]"을 보정된 채 말한다.</p>
|
||||
<div id="val-uq-headline" class="val-headline"></div>
|
||||
<div class="val-land-grid">
|
||||
<div><div class="val-chart" style="height:260px"><canvas id="chart-val-uqband"></canvas></div></div>
|
||||
<div><div class="val-chart" style="height:260px"><canvas id="chart-val-coverage"></canvas></div></div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="panel glass-panel val-wide val-credible">
|
||||
<div class="val-cred-head"><span class="val-cred-tag">신뢰성 ③</span>
|
||||
<h3>개인화 & 데이터 동화 — 초기 반응으로 개인 예측</h3></div>
|
||||
<p class="val-cap">디지털 트윈을 진짜 '트윈'으로. 개인의 <b>초기</b> 반응을 동화하면 <b>후기</b> 예측이 모집단 모델보다 좋아지는지 누설 없이 검정(앞 점 관측→뒤 점 forecast). 결과: 개인화가 <b>9/10 궤적 우세, 평균 +40% RMSE 개선, 커버리지 23%→90%</b>. <b>정직:</b> 개인환자 IPD(Vivli 통제접근) 부재로 시험암을 유사개인으로 사용 — 암 평균은 저잡음이라 개선이 과대평가될 수 있음. ODE-수준 개인화는 omics 필요(차기).</p>
|
||||
<div id="val-pers-headline" class="val-headline"></div>
|
||||
<div class="val-land-grid">
|
||||
<div><div class="val-chart" style="height:260px"><canvas id="chart-val-assim"></canvas></div></div>
|
||||
<div><div class="val-chart" style="height:260px"><canvas id="chart-val-fskill"></canvas></div></div>
|
||||
</div>
|
||||
<div class="val-chart" style="height:210px;margin-top:14px"><canvas id="chart-val-synth"></canvas></div>
|
||||
</section>
|
||||
<section class="panel glass-panel val-wide val-credible">
|
||||
<div class="val-cred-head"><span class="val-cred-tag">신뢰성 ④</span>
|
||||
<h3>실험 검정력 — 시너지를 검출하려면 표본이 얼마나?</h3></div>
|
||||
<p class="val-cap">사전등록 실험을 추측이 아니라 <b>몬테카를로 검정력 시뮬레이션</b>으로 설계. 정직한 결과: 시너지(δ=+0.12)는 실재하나 <b>약한 효과</b>라 단일 엔드포인트는 표본이 큼(n≈40 추정은 과소였고 시뮬이 교정). <b>핵심 발견</b>: 노이즈 플로어는 기술이 아니라 <b>생물학적 모낭간 변동</b> → 분자 readout 교체만으론 부족. 표본 절감은 <b>모낭내 paired 설계(유효 CV↓)</b>에서 옴 — 복합 전략으로 <b>1000→300모낭(3.3×)</b>. 분자 keratin의 역할=시너지를 잡는 올바른 지표(A proxy).</p>
|
||||
<div id="val-pwr-headline" class="val-headline"></div>
|
||||
<div class="val-land-grid">
|
||||
<div><div class="val-chart" style="height:260px"><canvas id="chart-val-power"></canvas></div></div>
|
||||
<div><div class="val-chart" style="height:260px"><canvas id="chart-val-powermol"></canvas></div></div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="panel glass-panel val-wide val-credible">
|
||||
<div class="val-cred-head"><span class="val-cred-tag">신뢰성 ⑤</span>
|
||||
<h3>분자 개인화 (ODE-수준) — baseline 전사체 → 최적치료</h3></div>
|
||||
<p class="val-cap">개인 baseline 분자축을 개인 ODE 파라미터로 매핑 → <b>치료 전</b> 최적치료 예측. <b>GWAS-가중 보정 적용</b>(트랙 C): AR/EDA2R/SRD5A2(유전 androgen축) ≫ DKK1/SFRP1(하류) 재가중 → 두 AGA 프로파일 구동이 <b>0.78→0.98 / 0.88→0.64로 재층화</b>(하류 DKK1만 높은 쪽을 낮춤). 헤드라인=정성→보정 구동 변화. <b>정직:</b> 개인결과 최종보정은 여전히 paired IPD 필요.</p>
|
||||
<div id="val-ode-headline" class="val-headline"></div>
|
||||
<div class="val-chart" style="height:280px;margin-top:10px"><canvas id="chart-val-ode"></canvas></div>
|
||||
</section>
|
||||
<section class="panel glass-panel val-wide val-credible">
|
||||
<div class="val-cred-head"><span class="val-cred-tag">신뢰성 ⑥</span>
|
||||
<h3>매핑 보정 (트랙 C, 무비용) — 정성 매핑을 데이터에 정박</h3></div>
|
||||
<p class="val-cap">ODE 개인화의 정성 매핑을 보유 데이터로 보정. <b>GWAS</b>(FinnGen AA/AGA + Yap2018 UKB MPB로 AR/EDA2R X연관 보강)로 <b>어떤 마커가 질환을 구동하는지</b>(중요도), <b>약물 섭동</b>(DHT→DP·JAK-i→AA)으로 <b>개입/구동의 크기</b>를 실측 정박. <b>정직:</b> 개인 baseline→개인결과의 최종 보정은 여전히 paired IPD 필요 — 이건 매핑의 방향·중요도·크기를 데이터에 묶는 <b>부분 보정</b>.</p>
|
||||
<div class="val-land-grid">
|
||||
<div><div class="val-chart" style="height:280px"><canvas id="chart-val-calib-gwas"></canvas></div></div>
|
||||
<div><div class="val-chart" style="height:280px"><canvas id="chart-val-calib-perturb"></canvas></div></div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="panel glass-panel val-wide val-credible">
|
||||
<div class="val-cred-head"><span class="val-cred-tag">신뢰성 ⑦</span>
|
||||
<h3>IPD 분석 하니스 — 사전등록 파이프라인 (합성 dry-run)</h3></div>
|
||||
<div class="ipd-warn">⚠ <b>합성(SYNTHETIC) dry-run</b> — 실제 환자 데이터 아님. 코드가 작동함을 보일 뿐, 트윈이 실제 환자에서 통함을 보이지 않음. 실제 IPD는 Vivli(바리시티닙)/Pfizer(리틀레시티닙) 통제접근(승인 3–6개월·IRB·DUA) 필요.</div>
|
||||
<p class="val-cap">IPD 도착 시 즉시 돌릴 사전등록 분석을 합성 코호트(실측 집계+UQ 분산, 반응자 혼합·방문잡음)로 검증. <b>정직한 미리보기</b>: 개인 수준에선 개인화 이득이 <b>+3.4%·95%CI 0 포함→불확정</b>(시험암 +40%와 대조). 개인 잡음·희소 방문이 개인 예측을 어렵게 함 → <b>진짜 검증은 실제 IPD로만</b>. (load_ipd()로 즉시 연결)</p>
|
||||
<div id="val-ipd-headline" class="val-headline"></div>
|
||||
<div id="val-ipd-verdict" class="ipd-verdict"></div>
|
||||
<div class="val-chart" style="height:240px;margin-top:10px"><canvas id="chart-val-ipd"></canvas></div>
|
||||
</section>
|
||||
<section class="panel glass-panel val-wide val-credible">
|
||||
<div class="val-cred-head"><span class="val-cred-tag">신뢰성 ⑧</span>
|
||||
<h3>대조군(비교 기준선) 자격 — V&V40 신뢰성</h3></div>
|
||||
<p class="val-cap">트윈을 <b>검증된 비교 기준선</b>으로 쓸 수 있나? 핵심은 점예측이 아니라 <b>불확실성이 정직한가</b>(보정). <b>다단계 보정곡선</b>(50/80/90/95% out-of-sample LOTO)에서 경험적 커버리지가 명목을 재현 — <b>보정오차 0.054</b>(약간 보수적). → <b>치료-타이밍 = 보정된 비교군</b>, 축방향 = 방향 비교군. <b>★무작위 대조군(RCT) 검증</b>(아래): 트윈의 '무치료' 예측이 실제 RCT 위약 arm과 <b>동등(TOST 2/2)</b> → synthetic control 후보. <b>정직:</b> 개인예측·절대정량·신규시너지는 미달; RCT검증은 회고적·대조군 평균(개인변동 아님). (V&V40: COMPARATOR_CREDIBILITY.md)</p>
|
||||
<div id="val-comp-headline" class="val-headline"></div>
|
||||
<div class="val-land-grid">
|
||||
<div><div class="val-chart" style="height:260px"><canvas id="chart-val-comp-cal"></canvas></div></div>
|
||||
<div id="val-comp-table" style="font-size:12px;align-self:center"></div>
|
||||
</div>
|
||||
<div id="val-comp-scarm" style="font-size:12px;margin-top:12px"></div>
|
||||
</section>
|
||||
<section class="panel glass-panel val-wide val-credible">
|
||||
<div class="val-cred-head"><span class="val-cred-tag">신뢰성 ⑨</span>
|
||||
<h3>동물실험 대체 경로 — NAM 자격 프로그램 (Phase 0)</h3></div>
|
||||
<p class="val-cap">트윈이 <b>쥐 실험을 대체</b>할 수 있나? <b>아니오 — 전체 대체는 불가</b>(신규발견·전신 PK/독성·전임상 안전성은 영구 제외). 그러나 <b>특정 효능 어세이 1개</b>를 좁은 CoU(기전기지 JAK 화합물 AA발모)에서 <b>인체 HFOC + 정량 트윈</b>으로 대체하는 <b>비동물법(NAM)</b> 경로는 실재. 분업: <b>HFOC=효능크기·트윈=동역학</b>. 아래는 <b>Phase 0(건식) 결과</b> — 다년 wet-lab+공인은 미수행. (NAM_QUALIFICATION.md)</p>
|
||||
<div id="val-nam" style="font-size:12px"></div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============== TAB 5: PAPERS ============== -->
|
||||
<div id="tab-papers" class="tab-content">
|
||||
<section class="panel glass-panel full-panel">
|
||||
<div class="library-controls">
|
||||
<h2>논문 라이브러리 <span class="badge small" id="table-count">0</span></h2>
|
||||
<div class="library-filters">
|
||||
<input type="text" id="search-input" class="search-input" placeholder="🔍 제목/초록 검색">
|
||||
<select id="filter-disease" class="mini-select"><option value="">전체 질환</option></select>
|
||||
<select id="filter-year" class="mini-select"><option value="">전체 연도</option></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-wrapper">
|
||||
<table class="papers-table">
|
||||
<thead><tr>
|
||||
<th class="sortable" data-col="pubYear">연도 ↕</th>
|
||||
<th>제목 (Title)</th>
|
||||
<th class="sortable" data-col="disease">질환 ↕</th>
|
||||
<th>표적 단백질 (Targets)</th>
|
||||
</tr></thead>
|
||||
<tbody id="papers-tbody"><tr><td colspan="4" class="loading-row">로딩 중...</td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="table-pagination">
|
||||
<button id="btn-prev" class="btn-page" disabled>← 이전</button>
|
||||
<span id="page-info">1 / 1</span>
|
||||
<button id="btn-next" class="btn-page" disabled>다음 →</button>
|
||||
</div>
|
||||
<div class="newpapers-section">
|
||||
<h3>🆕 에이전트가 발굴한 최신 표적 논문 (2023–2026)</h3>
|
||||
<div id="newpapers-list" class="newpapers-list"></div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<footer class="app-footer">
|
||||
<span>Alopecia Protein Digital Twin · 문헌 226편 + 웹 발굴 · UniProt · AlphaFold DB / ESMFold · scipy ODE</span>
|
||||
<span id="prior-art-note"></span>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script src="js/twin-engine.js"></script>
|
||||
<script src="js/data.js"></script>
|
||||
<script src="js/twin.js"></script>
|
||||
<script src="js/timeline.js"></script>
|
||||
<script src="js/network.js"></script>
|
||||
<script src="js/atlas.js"></script>
|
||||
<script src="js/graph.js"></script>
|
||||
<script src="js/calibrate.js"></script>
|
||||
<script src="js/validation.js"></script>
|
||||
<script src="js/stats.js"></script>
|
||||
<script src="js/library.js"></script>
|
||||
<script src="js/main.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
379
js/app.js
Normal file
379
js/app.js
Normal file
@ -0,0 +1,379 @@
|
||||
/* ============================================================
|
||||
Alopecia Digital Twin — app.js
|
||||
============================================================ */
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
// ── Data store
|
||||
let allPapers = [];
|
||||
let filteredPapers = [];
|
||||
let charts = {};
|
||||
let currentPage = 1;
|
||||
const PAGE_SIZE = 20;
|
||||
|
||||
// ── Tab Nav
|
||||
const tabBtns = document.querySelectorAll('.tab-btn');
|
||||
const tabContents = document.querySelectorAll('.tab-content');
|
||||
tabBtns.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
tabBtns.forEach(b => b.classList.remove('active'));
|
||||
tabContents.forEach(c => c.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
document.getElementById(`tab-${btn.dataset.tab}`).classList.add('active');
|
||||
// Lazy-render charts when Stats tab opens
|
||||
if (btn.dataset.tab === 'stats' && allPapers.length) renderCharts();
|
||||
});
|
||||
});
|
||||
|
||||
// ── Element refs (Digital Twin)
|
||||
const diseaseSelect = document.getElementById('disease-select');
|
||||
const modalitySelect = document.getElementById('modality-select');
|
||||
const deliverySelect = document.getElementById('delivery-select');
|
||||
const synthesizeBtn = document.getElementById('btn-synthesize');
|
||||
const visPlaceholder = document.getElementById('vis-placeholder');
|
||||
const visActive = document.getElementById('vis-active');
|
||||
const mechanismImage = document.getElementById('mechanism-image');
|
||||
const activePathway = document.getElementById('active-pathway');
|
||||
const mechanismDesc = document.getElementById('mechanism-description');
|
||||
const efficacyBar = document.getElementById('efficacy-bar');
|
||||
const bioBar = document.getElementById('bio-bar');
|
||||
const evidenceBar = document.getElementById('evidence-bar');
|
||||
const insightsList = document.getElementById('insights-list');
|
||||
const paperCountEl = document.getElementById('paper-count');
|
||||
const lastUpdatedEl = document.getElementById('last-updated');
|
||||
const relatedCount = document.getElementById('related-count');
|
||||
const relatedNum = document.getElementById('related-num');
|
||||
const insightFilter = document.getElementById('insight-disease-filter');
|
||||
|
||||
// ── Digital Twin control logic
|
||||
diseaseSelect.addEventListener('change', e => {
|
||||
const val = e.target.value;
|
||||
modalitySelect.disabled = !val;
|
||||
if (!val) { modalitySelect.value = ''; deliverySelect.disabled = true; deliverySelect.value = ''; synthesizeBtn.disabled = true; relatedCount.classList.add('hidden'); }
|
||||
});
|
||||
modalitySelect.addEventListener('change', e => {
|
||||
deliverySelect.disabled = !e.target.value;
|
||||
if (!e.target.value) { deliverySelect.value = ''; synthesizeBtn.disabled = true; }
|
||||
});
|
||||
deliverySelect.addEventListener('change', e => { synthesizeBtn.disabled = !e.target.value; });
|
||||
|
||||
// ── Synthesize
|
||||
synthesizeBtn.addEventListener('click', () => {
|
||||
const disease = diseaseSelect.value;
|
||||
const modality = modalitySelect.value;
|
||||
const delivery = deliverySelect.value;
|
||||
|
||||
visPlaceholder.classList.add('hidden');
|
||||
visActive.classList.remove('hidden');
|
||||
|
||||
const config = getTwinConfig(disease, modality, delivery);
|
||||
|
||||
activePathway.textContent = config.pathway;
|
||||
mechanismDesc.innerHTML = `<span class="desc-en">${config.desc}</span><br><span class="desc-ko">${config.descKo}</span>`;
|
||||
|
||||
mechanismImage.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
mechanismImage.src = config.image;
|
||||
mechanismImage.onload = () => { mechanismImage.style.opacity = '0.85'; };
|
||||
mechanismImage.onerror = () => { mechanismImage.style.opacity = '0'; };
|
||||
}, 400);
|
||||
|
||||
// Animate bars
|
||||
setTimeout(() => {
|
||||
efficacyBar.style.width = config.efficacy + '%';
|
||||
efficacyBar.textContent = config.efficacy + '%';
|
||||
bioBar.style.width = config.bio + '%';
|
||||
bioBar.textContent = config.bio + '%';
|
||||
}, 600);
|
||||
|
||||
// Count related papers
|
||||
const related = allPapers.filter(p =>
|
||||
p.disease === disease &&
|
||||
(modality === 'Other / Unknown' || p.modality === modality)
|
||||
);
|
||||
const pct = allPapers.length ? Math.round((related.length / allPapers.length) * 100) : 0;
|
||||
evidenceBar.style.width = Math.min(pct * 4, 100) + '%';
|
||||
evidenceBar.textContent = related.length + ' papers';
|
||||
relatedNum.textContent = related.length;
|
||||
relatedCount.classList.remove('hidden');
|
||||
});
|
||||
|
||||
function getTwinConfig(disease, modality, delivery) {
|
||||
const modalityMap = {
|
||||
'Microneedles': { efficacy: 91, bio: 95 },
|
||||
'Stem Cells & Exosomes': { efficacy: 88, bio: 82 },
|
||||
'Natural Extracts': { efficacy: 72, bio: 68 },
|
||||
'Nanoparticles/Nanogels': { efficacy: 85, bio: 88 },
|
||||
'Other / Unknown': { efficacy: 78, bio: 75 },
|
||||
};
|
||||
const stats = modalityMap[modality] || { efficacy: 78, bio: 75 };
|
||||
|
||||
if (disease === 'Androgenetic Alopecia') return {
|
||||
pathway: 'Wnt/β-catenin Upregulation (Wnt/β-카테닌 신호 활성화)',
|
||||
desc: `Utilizing ${modality} via ${delivery}, the treatment bypasses DHT binding barriers, stimulating Dermal Papilla Cells and driving hair follicles back into the Anagen growth phase.`,
|
||||
descKo: `${modality}을(를) ${delivery} 경로로 전달하여 DHT 결합 차단막을 우회합니다. 진피유두세포(Dermal Papilla Cell)를 자극해 모낭을 다시 성장기(Anagen Phase)로 전환시킵니다. Wnt 경로 활성화는 β-카테닌의 핵 내 이동을 촉진하여 모발 생성 관련 유전자 전사를 유도합니다.`,
|
||||
image: 'images/wnt_pathway.png',
|
||||
...stats
|
||||
};
|
||||
if (disease === 'Alopecia Areata') return {
|
||||
pathway: 'JAK-STAT Pathway Inhibition (JAK-STAT 경로 억제)',
|
||||
desc: `Application of ${modality} (${delivery}) locally suppresses aberrant CD8+ T-cell immune responses, disabling the inflammatory signal loop that attacks the hair bulb.`,
|
||||
descKo: `${modality}(${delivery})을(를) 국소 적용하여 비정상적인 CD8+ T세포 면역 반응을 억제합니다. 모낭의 면역 특권(Immune Privilege)이 무너지면서 발생하는 자가면역 공격 신호 루프를 JAK1/2 억제를 통해 차단하고, 모낭 주변 염증을 완화합니다.`,
|
||||
image: 'images/jak_stat_pathway.png',
|
||||
...stats
|
||||
};
|
||||
return {
|
||||
pathway: 'Anti-Apoptosis / ROS Scavenging (세포자멸사 억제 / 활성산소 제거)',
|
||||
desc: `${modality} delivered through ${delivery} mitigates severe oxidative stress and prevents p53-mediated apoptosis in rapidly dividing matrix cells during toxic exposure.`,
|
||||
descKo: `${modality}을(를) ${delivery}로 전달하여 항암 독성으로 인한 극심한 산화 스트레스를 완화합니다. 급속 분열 중인 모기질세포(Matrix Cell)에서 p53 매개 세포자멸사(Apoptosis)를 억제하고, 활성산소종(ROS)을 제거하여 모낭 세포 생존율을 높입니다.`,
|
||||
image: 'images/apoptosis_pathway.png',
|
||||
...stats
|
||||
};
|
||||
}
|
||||
|
||||
// ── Insight filter
|
||||
insightFilter.addEventListener('change', () => { updateInsights(currentData); });
|
||||
let currentData = null;
|
||||
|
||||
function updateInsights(data) {
|
||||
if (!data) return;
|
||||
currentData = data;
|
||||
const filterVal = insightFilter.value;
|
||||
const papers = filterVal ? data.papers.filter(p => p.disease === filterVal) : data.papers;
|
||||
const recent = papers.slice(0, 12);
|
||||
|
||||
insightsList.innerHTML = '';
|
||||
if (!recent.length) {
|
||||
insightsList.innerHTML = '<div class="insight-card empty-state">No papers found.</div>';
|
||||
return;
|
||||
}
|
||||
recent.forEach(paper => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'insight-card';
|
||||
const abbr = paper.disease.includes('Androgenetic') ? 'AGA Target'
|
||||
: paper.disease.includes('Areata') ? 'AA Target' : 'CIA Target';
|
||||
card.innerHTML = `
|
||||
<div class="insight-disease">${abbr}</div>
|
||||
<div class="insight-title" title="${paper.title}">${paper.title}</div>
|
||||
<div class="insight-meta">
|
||||
<span class="tag">${paper.modality}</span>
|
||||
<span>${paper.pubYear || 'Recent'}</span>
|
||||
</div>`;
|
||||
insightsList.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Chart rendering
|
||||
const CHART_COLORS = ['#6699ff', '#a855f7', '#22d3ee', '#34d399', '#fbbf24', '#f87171', '#818cf8'];
|
||||
const chartDefaults = {
|
||||
responsive: true, maintainAspectRatio: false,
|
||||
plugins: { legend: { labels: { color: '#7b82b0', font: { size: 11 }, boxWidth: 12 } } }
|
||||
};
|
||||
|
||||
function renderCharts() {
|
||||
if (!currentData?.stats) return;
|
||||
const s = currentData.stats;
|
||||
|
||||
renderDoughnut('chart-disease', s.by_disease);
|
||||
renderDoughnut('chart-modality', s.by_modality);
|
||||
renderBar('chart-trend', s.trend_by_year, 'Papers Published');
|
||||
renderDoughnut('chart-aga-modality', s.disease_modality?.['Androgenetic Alopecia'] || {});
|
||||
renderDoughnut('chart-aa-modality', s.disease_modality?.['Alopecia Areata'] || {});
|
||||
}
|
||||
|
||||
function renderDoughnut(canvasId, dataObj) {
|
||||
const canvas = document.getElementById(canvasId);
|
||||
if (!canvas) return;
|
||||
if (charts[canvasId]) charts[canvasId].destroy();
|
||||
const labels = Object.keys(dataObj);
|
||||
const values = Object.values(dataObj);
|
||||
charts[canvasId] = new Chart(canvas, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels,
|
||||
datasets: [{ data: values, backgroundColor: CHART_COLORS, borderColor: 'rgba(255,255,255,0.05)', borderWidth: 1 }]
|
||||
},
|
||||
options: { ...chartDefaults, cutout: '60%' }
|
||||
});
|
||||
}
|
||||
|
||||
function renderBar(canvasId, dataObj, label) {
|
||||
const canvas = document.getElementById(canvasId);
|
||||
if (!canvas) return;
|
||||
if (charts[canvasId]) charts[canvasId].destroy();
|
||||
const labels = Object.keys(dataObj);
|
||||
const values = Object.values(dataObj);
|
||||
charts[canvasId] = new Chart(canvas, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels,
|
||||
datasets: [{
|
||||
label,
|
||||
data: values,
|
||||
backgroundColor: 'rgba(102,153,255,0.4)',
|
||||
borderColor: '#6699ff',
|
||||
borderWidth: 1,
|
||||
borderRadius: 6
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
...chartDefaults,
|
||||
scales: {
|
||||
x: { ticks: { color: '#7b82b0', font: { size: 11 } }, grid: { color: 'rgba(255,255,255,0.04)' } },
|
||||
y: { ticks: { color: '#7b82b0', font: { size: 11 } }, grid: { color: 'rgba(255,255,255,0.06)' }, beginAtZero: true }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Paper Library
|
||||
const searchInput = document.getElementById('search-input');
|
||||
const filterDisease = document.getElementById('filter-disease');
|
||||
const filterModality = document.getElementById('filter-modality');
|
||||
const filterYear = document.getElementById('filter-year');
|
||||
const papersTbody = document.getElementById('papers-tbody');
|
||||
const tableCountEl = document.getElementById('table-count');
|
||||
const btnPrev = document.getElementById('btn-prev');
|
||||
const btnNext = document.getElementById('btn-next');
|
||||
const pageInfo = document.getElementById('page-info');
|
||||
let sortCol = 'pubYear'; let sortAsc = false;
|
||||
|
||||
function populateYearFilter(papers) {
|
||||
const years = [...new Set(papers.map(p => p.pubYear).filter(y => y))].sort((a, b) => b - a);
|
||||
years.forEach(y => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = y; opt.textContent = y;
|
||||
filterYear.appendChild(opt);
|
||||
});
|
||||
}
|
||||
|
||||
function applyFilters() {
|
||||
const q = searchInput.value.toLowerCase();
|
||||
const dis = filterDisease.value;
|
||||
const mod = filterModality.value;
|
||||
const yr = filterYear.value;
|
||||
filteredPapers = allPapers.filter(p => {
|
||||
if (dis && p.disease !== dis) return false;
|
||||
if (mod && p.modality !== mod) return false;
|
||||
if (yr && p.pubYear !== yr) return false;
|
||||
if (q && !(p.title.toLowerCase().includes(q) || (p.abstract || '').toLowerCase().includes(q))) return false;
|
||||
return true;
|
||||
});
|
||||
filteredPapers.sort((a, b) => {
|
||||
const va = a[sortCol] || ''; const vb = b[sortCol] || '';
|
||||
return sortAsc ? va.localeCompare(vb) : vb.localeCompare(va);
|
||||
});
|
||||
currentPage = 1;
|
||||
renderTable();
|
||||
}
|
||||
|
||||
function makePdfFilename(title) {
|
||||
// Windows에서 금지된 문자만 제거. 나머지는 agent1이 저장한 실제 파일명과 동일하게 유지.
|
||||
// agent1_fetcher.py: 특수문자 제거 없이 {title}.pdf 그대로 저장
|
||||
const illegal = /[\\/:*?"<>|]/g;
|
||||
return title.replace(illegal, '').trim() + '.pdf';
|
||||
}
|
||||
|
||||
function downloadPdf(url, filename) {
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
}
|
||||
|
||||
function renderTable() {
|
||||
const total = filteredPapers.length;
|
||||
const pages = Math.max(1, Math.ceil(total / PAGE_SIZE));
|
||||
const start = (currentPage - 1) * PAGE_SIZE;
|
||||
const slice = filteredPapers.slice(start, start + PAGE_SIZE);
|
||||
|
||||
tableCountEl.textContent = total;
|
||||
pageInfo.textContent = `Page ${currentPage} of ${pages}`;
|
||||
btnPrev.disabled = currentPage <= 1;
|
||||
btnNext.disabled = currentPage >= pages;
|
||||
|
||||
papersTbody.innerHTML = '';
|
||||
if (!slice.length) {
|
||||
papersTbody.innerHTML = '<tr><td colspan="5" class="loading-row">No papers match your filter.</td></tr>';
|
||||
return;
|
||||
}
|
||||
slice.forEach(p => {
|
||||
const tr = document.createElement('tr');
|
||||
const diseaseClass = p.disease.includes('Androgenetic') ? 'disease-aga'
|
||||
: p.disease.includes('Areata') ? 'disease-aa' : 'disease-cia';
|
||||
const diseaseLabel = p.disease.includes('Androgenetic') ? 'AGA (남성형 탈모)'
|
||||
: p.disease.includes('Areata') ? 'AA (원형 탈모)' : 'CIA (항암 탈모)';
|
||||
const pdfFilename = makePdfFilename(p.title);
|
||||
const pdfUrl = `../papers/${encodeURIComponent(pdfFilename)}`;
|
||||
tr.style.cursor = 'pointer';
|
||||
tr.title = 'PDF 다운로드 클릭 (Click to download PDF)';
|
||||
tr.innerHTML = `
|
||||
<td>${p.pubYear || '—'}</td>
|
||||
<td>
|
||||
<span class="paper-title" title="${(p.abstract || '').replace(/"/g, '"').substring(0, 300)}">${p.title}</span>
|
||||
<span class="pdf-icon" title="PDF 다운로드">📄</span>
|
||||
</td>
|
||||
<td><span class="disease-badge ${diseaseClass}">${diseaseLabel}</span></td>
|
||||
<td><span class="tag">${p.modality}</span></td>
|
||||
<td>${p.mechanism || '—'}</td>`;
|
||||
tr.addEventListener('click', () => downloadPdf(pdfUrl, pdfFilename));
|
||||
papersTbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
// Sorting
|
||||
document.querySelectorAll('.sortable').forEach(th => {
|
||||
th.addEventListener('click', () => {
|
||||
const col = th.dataset.col;
|
||||
if (sortCol === col) sortAsc = !sortAsc; else { sortCol = col; sortAsc = true; }
|
||||
applyFilters();
|
||||
});
|
||||
});
|
||||
|
||||
// Filters
|
||||
[searchInput, filterDisease, filterModality, filterYear].forEach(el => el.addEventListener('input', applyFilters));
|
||||
btnPrev.addEventListener('click', () => { if (currentPage > 1) { currentPage--; renderTable(); } });
|
||||
btnNext.addEventListener('click', () => {
|
||||
const pages = Math.ceil(filteredPapers.length / PAGE_SIZE);
|
||||
if (currentPage < pages) { currentPage++; renderTable(); }
|
||||
});
|
||||
|
||||
// ── Data Polling
|
||||
async function fetchData() {
|
||||
try {
|
||||
const res = await fetch(`../analysis_results.json?t=${Date.now()}`);
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
if (!data || !data.papers) return;
|
||||
|
||||
allPapers = data.papers;
|
||||
filteredPapers = [...allPapers];
|
||||
|
||||
// Header
|
||||
paperCountEl.textContent = data.total_papers;
|
||||
const updated = data.last_updated ? new Date(data.last_updated).toLocaleString('ko-KR', { month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' }) : '—';
|
||||
lastUpdatedEl.textContent = updated;
|
||||
|
||||
// Year filter
|
||||
filterYear.innerHTML = '<option value="">All Years</option>';
|
||||
populateYearFilter(allPapers);
|
||||
|
||||
// Insights
|
||||
updateInsights(data);
|
||||
|
||||
// Paper table
|
||||
applyFilters();
|
||||
|
||||
// Charts (only if Stats tab is active)
|
||||
if (document.getElementById('tab-stats').classList.contains('active')) renderCharts();
|
||||
|
||||
} catch (err) {
|
||||
console.warn('Waiting for analysis_results.json...', err);
|
||||
}
|
||||
}
|
||||
|
||||
fetchData();
|
||||
setInterval(fetchData, 10000);
|
||||
});
|
||||
184
js/atlas.js
Normal file
184
js/atlas.js
Normal file
@ -0,0 +1,184 @@
|
||||
/* ============================================================
|
||||
atlas.js — Protein Atlas 탭 (단백질 그리드 + NGL 3D 구조)
|
||||
============================================================ */
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const AXIS_LABELS = {
|
||||
AND: "안드로겐(AR/DHT)", Wnt: "Wnt/β-catenin", BMP: "BMP/TGFβ", SHH: "Hedgehog",
|
||||
INF: "염증/JAK-STAT", APO: "세포사멸/산화", DP: "진피유두/성장인자", HFSC: "모낭 줄기세포",
|
||||
};
|
||||
const AXIS_COLOR = {
|
||||
AND: "#f87171", Wnt: "#6699ff", BMP: "#a78bfa", SHH: "#22d3ee",
|
||||
INF: "#fbbf24", APO: "#fb923c", DP: "#34d399", HFSC: "#f472b6", "—": "#64748b",
|
||||
};
|
||||
|
||||
let nglStage = null, selectedGene = null;
|
||||
function el(id) { return document.getElementById(id); }
|
||||
|
||||
function init() {
|
||||
const axisSel = el('atlas-axis'), disSel = el('atlas-disease');
|
||||
const axes = [...new Set(Store.catalog.proteins.map(p => p.twin_node))].filter(a => a && a !== '—');
|
||||
axes.forEach(a => axisSel.add(new Option(AXIS_LABELS[a] || a, a)));
|
||||
const diseases = [...new Set(Store.catalog.proteins.flatMap(p => p.diseases || []))].sort();
|
||||
diseases.forEach(d => disSel.add(new Option(d, d)));
|
||||
|
||||
['atlas-search', 'atlas-axis', 'atlas-disease', 'atlas-sort'].forEach(id =>
|
||||
el(id).addEventListener('input', renderGrid));
|
||||
// resize 핸들러는 단 한 번만 등록 (이전: loadStructure 마다 추가 → 누수)
|
||||
window.addEventListener('resize', () => nglStage && nglStage.handleResize());
|
||||
renderGrid();
|
||||
}
|
||||
|
||||
function filtered() {
|
||||
const q = el('atlas-search').value.trim().toLowerCase();
|
||||
const ax = el('atlas-axis').value, dis = el('atlas-disease').value, sort = el('atlas-sort').value;
|
||||
let list = Store.catalog.proteins.filter(p => {
|
||||
if (ax && p.twin_node !== ax) return false;
|
||||
if (dis && !(p.diseases || []).includes(dis)) return false;
|
||||
if (q && !(p.gene.toLowerCase().includes(q) || (p.name || '').toLowerCase().includes(q))) return false;
|
||||
return true;
|
||||
});
|
||||
const plddt = p => (p.structure && p.structure.mean_plddt) || 0;
|
||||
const sorters = {
|
||||
evidence: (a, b) => b.n_evidence - a.n_evidence || b.mention_count - a.mention_count,
|
||||
mention: (a, b) => b.mention_count - a.mention_count,
|
||||
plddt: (a, b) => plddt(b) - plddt(a),
|
||||
gene: (a, b) => a.gene.localeCompare(b.gene),
|
||||
};
|
||||
return list.sort(sorters[sort] || sorters.evidence);
|
||||
}
|
||||
|
||||
function renderGrid() {
|
||||
const list = filtered();
|
||||
el('atlas-count').textContent = list.length;
|
||||
const grid = el('atlas-grid');
|
||||
grid.innerHTML = list.map(p => {
|
||||
const ax = p.twin_node || '—';
|
||||
const seed = p.in_model ? '<span class="pc-seed">●seed</span>' : `<span class="pc-ev">${p.n_evidence}편</span>`;
|
||||
return `<div class="prot-card${p.gene === selectedGene ? ' selected' : ''}" data-gene="${p.gene}">
|
||||
${seed}
|
||||
<div class="pc-gene">${p.gene}</div>
|
||||
<div class="pc-name">${p.name || ''}</div>
|
||||
<div class="pc-tags">
|
||||
<span class="axis-tag" style="background:${hex(AXIS_COLOR[ax], .16)};color:${AXIS_COLOR[ax]}">${ax}</span>
|
||||
<span class="axis-tag role-${p.role}">${p.role || ''}</span>
|
||||
</div></div>`;
|
||||
}).join('');
|
||||
grid.querySelectorAll('.prot-card').forEach(c =>
|
||||
c.onclick = () => showDetail(c.dataset.gene));
|
||||
}
|
||||
|
||||
function hex(h, a) {
|
||||
const n = parseInt((h || '#64748b').slice(1), 16);
|
||||
return `rgba(${n >> 16 & 255},${n >> 8 & 255},${n & 255},${a})`;
|
||||
}
|
||||
|
||||
function showDetail(gene) {
|
||||
const p = Store.proteinByGene[gene];
|
||||
if (!p) return;
|
||||
if (gene === selectedGene && !el('atlas-detail').classList.contains('hidden')) return; // 동일 단백질 재로딩 방지
|
||||
selectedGene = gene;
|
||||
document.querySelectorAll('.prot-card').forEach(c => c.classList.toggle('selected', c.dataset.gene === gene));
|
||||
el('atlas-detail-empty').classList.add('hidden');
|
||||
el('atlas-detail').classList.remove('hidden');
|
||||
|
||||
el('d-gene').textContent = p.gene;
|
||||
el('d-name').textContent = p.uniprot_name || p.name || '';
|
||||
const st = p.structure || {};
|
||||
el('d-badges').innerHTML = [
|
||||
p.in_model ? '<span class="dbadge" style="color:#a855f7">트윈 wired</span>' : '',
|
||||
`<span class="dbadge">${AXIS_LABELS[p.twin_node] || '축 미배정'}</span>`,
|
||||
`<span class="dbadge role-${p.role}">${p.role || ''}</span>`,
|
||||
].filter(Boolean).join('');
|
||||
|
||||
// 메타 그리드
|
||||
el('d-meta').innerHTML = [
|
||||
['경로 (Pathway)', p.pathway || '—'],
|
||||
['UniProt', st.accession || '—'],
|
||||
['질환', (p.diseases || []).join(', ') || '—'],
|
||||
['서열 길이', p.length ? p.length + ' aa' : '—'],
|
||||
['PDB 실험구조', (p.pdb_count || 0) + '개'],
|
||||
['논문 언급', p.mention_count + '회'],
|
||||
].map(([k, v]) => `<div class="dg"><div class="k">${k}</div><div class="v">${v}</div></div>`).join('');
|
||||
|
||||
el('d-mech').textContent = p.mechanism || p.function || '기전 정보 없음';
|
||||
el('d-drugs').innerHTML = (p.drugs || []).length
|
||||
? p.drugs.map(d => `<span class="gene-pill">${d}</span>`).join('')
|
||||
: '<span class="panel-subtitle">등록된 표적 약물 없음</span>';
|
||||
|
||||
// 근거 논문
|
||||
const ev = (p.evidence_paper_ids || []);
|
||||
el('d-evcount').textContent = ev.length;
|
||||
el('d-evidence').innerHTML = ev.length ? ev.slice(0, 30).map(id => {
|
||||
const info = Store.papersIdx[id] || {};
|
||||
return `<a class="evidence-item" href="https://pubmed.ncbi.nlm.nih.gov/${id}/" target="_blank" style="text-decoration:none;color:inherit;display:block">
|
||||
<span class="ey">${info.year || ''}</span>${info.title || ('PMID ' + id)}</a>`;
|
||||
}).join('') : '<span class="panel-subtitle">초록 스캔/추출 근거 없음 (캐논 표적)</span>';
|
||||
|
||||
// 실제 논문 전문 근거 (검증 인용) — 요소 없을 때(구버전 HTML 캐시) 안전 가드
|
||||
const gr = p.grounding || [];
|
||||
const gcEl = el('d-groundcount'); if (gcEl) gcEl.textContent = gr.length;
|
||||
const grEl = el('d-grounding');
|
||||
if (grEl) grEl.innerHTML = gr.length ? gr.map(h =>
|
||||
`<div class="grounding-item"><div class="gr-paper">📄 ${h.paper} <span class="gr-hits">${h.n_hits}회 언급</span></div>
|
||||
<div class="gr-quote">"…${(h.quote || '').replace(/"/g, '"')}…"</div></div>`).join('')
|
||||
: (p.grounded_in_corpus === false
|
||||
? '<span class="panel-subtitle">분석 코퍼스(papers/) 전문에 근거 없음 — 웹 발굴(2024–26) 또는 canonical 경로 멤버</span>'
|
||||
: '<span class="panel-subtitle">—</span>');
|
||||
|
||||
// 외부 링크
|
||||
el('d-links').innerHTML = [
|
||||
st.uniprot_url ? `<a href="${st.uniprot_url}" target="_blank">UniProt ↗</a>` : '',
|
||||
st.af_entry_url ? `<a href="${st.af_entry_url}" target="_blank">AlphaFold DB ↗</a>` : '',
|
||||
].filter(Boolean).join('');
|
||||
|
||||
el('d-plddt').textContent = st.mean_plddt ? `평균 pLDDT ${st.mean_plddt} (${st.plddt_band || ''})` : '구조 신뢰도 미계산';
|
||||
loadStructure(p);
|
||||
}
|
||||
|
||||
function loadStructure(p) {
|
||||
const viewer = el('ngl-viewer');
|
||||
const st = p.structure || {};
|
||||
const afLink = st.af_entry_url || (st.accession ? `https://alphafold.ebi.ac.uk/entry/${st.accession}` : null);
|
||||
const fallback = (msg) => {
|
||||
viewer.innerHTML = `<div class="ngl-loading" style="flex-direction:column;gap:8px;text-align:center">${msg}` +
|
||||
(afLink ? `<a href="${afLink}" target="_blank" style="color:var(--primary);text-decoration:underline">AlphaFold에서 3D 구조 보기 ↗</a>` : '') + `</div>`;
|
||||
};
|
||||
viewer.innerHTML = '<div class="ngl-loading">AlphaFold 구조 로딩 중…</div>';
|
||||
if (typeof NGL === 'undefined') { fallback('NGL 라이브러리 미로딩'); return; }
|
||||
if (!st.accession) { fallback('구조 정보 없음'); return; }
|
||||
if (nglStage) { try { nglStage.dispose(); } catch (e) {} nglStage = null; }
|
||||
try {
|
||||
nglStage = new NGL.Stage('ngl-viewer', { backgroundColor: '#0e0d0b' });
|
||||
} catch (e) { fallback('3D 뷰어(WebGL) 사용 불가'); return; }
|
||||
|
||||
// 서빙 디렉토리/오프라인 무관 — 로컬(프로젝트루트) → 원격 AlphaFold 순 폴백
|
||||
const candidates = [];
|
||||
if (st.local_pdb) candidates.push(['../' + st.local_pdb.replace(/\\/g, '/'), 'pdb']);
|
||||
if (st.af_cif_url) candidates.push([st.af_cif_url, 'cif']);
|
||||
if (st.af_pdb_url) candidates.push([st.af_pdb_url, 'pdb']);
|
||||
|
||||
(function tryLoad(i) {
|
||||
if (i >= candidates.length) { fallback('구조 로드 실패 (네트워크/경로)'); return; }
|
||||
const [url, ext] = candidates[i];
|
||||
let comp_done = false;
|
||||
nglStage.loadFile(url, { ext }).then(comp => {
|
||||
const ld = viewer.querySelector('.ngl-loading'); if (ld) ld.remove();
|
||||
comp.addRepresentation('cartoon', {
|
||||
colorScheme: 'bfactor', colorScale: ['#FF7D45', '#FFDB13', '#65CBF3', '#0053D6'],
|
||||
colorDomain: [40, 100], smoothSheet: true,
|
||||
});
|
||||
comp.autoView();
|
||||
try { nglStage.setSpin(true); } catch (e) {}
|
||||
comp_done = true;
|
||||
}).catch(() => tryLoad(i + 1));
|
||||
})(0);
|
||||
}
|
||||
|
||||
window.AtlasTab = { init, resize: () => nglStage && nglStage.handleResize() };
|
||||
window.openProtein = function (gene) {
|
||||
document.querySelector('.tab-btn[data-tab="atlas"]').click();
|
||||
setTimeout(() => showDetail(gene), 60);
|
||||
};
|
||||
})();
|
||||
135
js/calibrate.js
Normal file
135
js/calibrate.js
Normal file
@ -0,0 +1,135 @@
|
||||
/* ============================================================
|
||||
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 = '<div class="empty-state">calibration_result.json 없음 — <code>python -m digital_twin.copasi_calibrate</code> 실행 필요.</div>';
|
||||
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 = '<span class="panel-subtitle">coupled_scenarios.json 없음</span>'; 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 `<div class="coupled-row">
|
||||
<span class="coupled-lab">${lab(s)}</span>
|
||||
<span class="coupled-bar-wrap"><span class="coupled-bar ${cls}" style="width:${Math.min(100, af)}%"></span></span>
|
||||
<span class="coupled-val">anagen ${num(af, 0)}% · peak ${num(ph, 0)}%</span>
|
||||
</div>`;
|
||||
}).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 = `
|
||||
<div class="val-head">🔬 독립 검증 — 트윈 예측 vs 실제 논문 실험결과 (out-of-sample)</div>
|
||||
<div class="val-cards">
|
||||
<div class="val-card big"><div class="vv">${(s.twin_causal_accuracy*100).toFixed(1)}%</div><div class="vl">논문 인과 실험 일치 (n=${s.n_causal})</div></div>
|
||||
<div class="val-card"><div class="vv">p${s.permutation_p < 0.0001 ? '<0.0001' : '='+s.permutation_p}</div><div class="vl">permutation (우연 대비)</div></div>
|
||||
<div class="val-card"><div class="vv">[${(s.bootstrap_ci95||[]).map(x=>(x*100).toFixed(0)).join(', ')}]%</div><div class="vl">bootstrap 95% CI (1만회)</div></div>
|
||||
<div class="val-card"><div class="vv">${s.ground_truth_records}</div><div class="vl">논문 실험결과 GT (멀티에이전트)</div></div>
|
||||
<div class="val-card"><div class="vv">${(s.external_transcriptomic_accuracy*100).toFixed(0)}%</div><div class="vl">외부 GSE36169 (조성교란·참고)</div></div>
|
||||
</div>
|
||||
<div class="val-note">✅ 수정: ${(s.fixes_applied||[]).join(' · ')} | 트윈이 옳고 독립소스가 틀린 예: ${(s.twin_correct_where_sources_err||[]).join('; ')}</div>
|
||||
${v.protein_modules ? `<div class="val-pm">🧬 단백질 분석 모듈 교차검증 (알파폴드 외): STRING PPI 축 응집 <b>${v.protein_modules.axes_ppi_significant}</b> (p<0.05) · 축 라벨일치 <b>${v.protein_modules.axes_label_matched}</b> · 질환 생물학일치 <b>${v.protein_modules.diseases_biology_matched}</b> · 실험 PDB <b>${v.protein_modules.experimental_pdb}</b></div>` : ''}
|
||||
<div class="val-note dim">⚠️ ${(v.honest_caveats||[]).join(' · ')}</div>`;
|
||||
}
|
||||
|
||||
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 = `
|
||||
<div class="cal-card big"><div class="mv">${num(s.loocv_cv_r2, 2)}</div><div class="ml">LOOCV 교차검증 R² (외표본·정직 지표)</div></div>
|
||||
<div class="cal-card"><div class="mv">${num(jr.reference, 2)}</div><div class="ml">joint R² · 문헌(형상)</div></div>
|
||||
<div class="cal-card"><div class="mv">${num(jr.gse11186, 2)}</div><div class="ml">joint R² · 실측 GSE11186</div></div>
|
||||
<div class="cal-card"><div class="mv">${cal.n_fit_params}<span style="font-size:.5em">+${cal.n_fixed_params}fix</span></div><div class="ml">추정/고정 파라미터</div></div>
|
||||
<div class="cal-card"><div class="mv">${num(cal.aicc, 0)}</div><div class="ml">AICc (복잡도 penalized)</div></div>
|
||||
<div class="cal-card wide"><div class="mv-sm">견고 축: ${robust || '—'} · 비식별 ${uq.n_poorly_identified}/${cal.n_fit_params} · 경계고착 ${uq.n_at_bound}</div><div class="ml">${cal.approach || ''}</div></div>`;
|
||||
// 정직성 배너
|
||||
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 = `⚖️ <b>정직한 보정</b>: 단일적합 R²(문헌 ${num((cal.descriptive_single_r2||{}).reference,2)}, 실측 ${num((cal.descriptive_single_r2||{}).gse11186,2)})은 데이터셋 간 전이 안 되는 <i>서술적 상한</i>. 1차 지표는 <b>단일 파라미터셋으로 두 데이터셋을 동시 적합한 joint R²</b>와 <b>LOOCV</b>. ${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 `<div class="cal-axis-card">
|
||||
<div class="cal-axis-head"><b>${(tg.axis_label || {})[st] || st}</b>
|
||||
<span class="cal-r2 ${cls}">R²=${num(r2, 2)}</span></div>
|
||||
<div class="cal-axis-chart"><canvas id="cal-c-${st}"></canvas></div>
|
||||
</div>`;
|
||||
}).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 `<div class="cal-prow"><span class="pk">${k}${flag ? ' <span class="pflag">' + flag + '</span>' : ''}</span><span class="pi">${num(rse[k], 2)}</span><span class="pa">${num(fp[k], 3)}</span></div>`;
|
||||
}).join('');
|
||||
|
||||
// provenance + UQ 요약
|
||||
const corr = (uq.strong_correlations || []).slice(0, 6).map(c => `${c[0]}~${c[1]}(${c[2]})`).join(', ');
|
||||
el('cal-provenance').innerHTML =
|
||||
`<p class="cal-note">${tg.is_real_data ? '✅ 실측 데이터' : '📐 문헌 합성(형상복원 점검)'} — ${tg.notes || ''}</p>` +
|
||||
`<p style="color:var(--warn);font-size:.72rem">불확실성: 비식별 ${uq.n_poorly_identified}/${fitNames.length} · 경계고착 ${uq.n_at_bound} · 강상관쌍: ${corr || '없음'} · LOOCV CV-R²=${num((cal.loocv_reference||{}).cv_r2,2)}</p>` +
|
||||
'<ul>' + (tg.provenance || []).map(p => `<li>${p}</li>`).join('') + '</ul>';
|
||||
}
|
||||
|
||||
window.CalibrateTab = { init };
|
||||
})();
|
||||
62
js/data.js
Normal file
62
js/data.js
Normal file
@ -0,0 +1,62 @@
|
||||
/* ============================================================
|
||||
data.js — 중앙 데이터 로더 (모든 JSON 산출물)
|
||||
============================================================ */
|
||||
(function (global) {
|
||||
'use strict';
|
||||
|
||||
const Store = {
|
||||
catalog: null, // protein_catalog.json
|
||||
scenarios: null, // twin_scenarios.json
|
||||
graph: null, // knowledge_graph.json
|
||||
papersIdx: null, // papers_index.json
|
||||
extras: null, // extras.json (prior_art, new_papers, frameworks)
|
||||
analysis: null, // ../analysis_results.json (기존 통계/라이브러리)
|
||||
calibration: null, // calibration_result.json (COPASI 정량 보정)
|
||||
coupled: null, // coupled_scenarios.json (cycle↔chronic COMBINE)
|
||||
proteinByGene: {},
|
||||
};
|
||||
|
||||
async function loadJSON(url, fallback) {
|
||||
try {
|
||||
const r = await fetch(url, { cache: 'no-store' });
|
||||
if (!r.ok) throw new Error(r.status);
|
||||
return await r.json();
|
||||
} catch (e) {
|
||||
console.warn('load 실패:', url, e.message);
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAll() {
|
||||
const [catalog, scenarios, graph, papersIdx, extras, analysis, calibration] = await Promise.all([
|
||||
loadJSON('data/protein_catalog.json', { proteins: [] }),
|
||||
loadJSON('data/twin_scenarios.json', { scenarios: [], interventions: {}, diseases: {} }),
|
||||
loadJSON('data/knowledge_graph.json', { nodes: [], links: [], summary: {} }),
|
||||
loadJSON('data/papers_index.json', {}),
|
||||
loadJSON('data/extras.json', { new_papers: [], prior_art: [], frameworks: [] }),
|
||||
loadJSON('../analysis_results.json', { papers: [], stats: {} }),
|
||||
loadJSON('data/calibration_result.json', null),
|
||||
]);
|
||||
Store.coupled = await loadJSON('data/coupled_scenarios.json', null);
|
||||
Store.validation = await loadJSON('data/validation_report.json', null);
|
||||
Store.catalog = catalog;
|
||||
Store.scenarios = scenarios;
|
||||
Store.graph = graph;
|
||||
Store.papersIdx = papersIdx;
|
||||
Store.extras = extras;
|
||||
Store.analysis = analysis;
|
||||
Store.calibration = calibration;
|
||||
(catalog.proteins || []).forEach(p => { Store.proteinByGene[p.gene] = p; });
|
||||
return Store;
|
||||
}
|
||||
|
||||
// 시나리오 조회 (정확한 disease + interventions 조합)
|
||||
function findScenario(disease, interventions) {
|
||||
const id = `${disease}|${(interventions || []).join('+')}`;
|
||||
return (Store.scenarios.scenarios || []).find(s => s.id === id);
|
||||
}
|
||||
|
||||
Store.loadAll = loadAll;
|
||||
Store.findScenario = findScenario;
|
||||
global.Store = Store;
|
||||
})(window);
|
||||
208
js/graph.js
Normal file
208
js/graph.js
Normal file
@ -0,0 +1,208 @@
|
||||
/* ============================================================
|
||||
graph.js — Knowledge Graph (캔버스 force 시뮬레이션, 의존성 없음)
|
||||
============================================================ */
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const TYPE_COLOR = { protein: '#6699ff', pathway: '#a855f7', disease: '#f87171', axis: '#22d3ee', drug: '#34d399' };
|
||||
const TYPE_R = { protein: 4, pathway: 7, disease: 9, axis: 8, drug: 3 };
|
||||
|
||||
let canvas, ctx, nodes = [], links = [], view = { x: 0, y: 0, k: 1 };
|
||||
let alpha = 1, running = false, raf = null;
|
||||
let drag = null, hover = null, pan = null, initialized = false, justDragged = false;
|
||||
let showDrug = false, focusDisease = '';
|
||||
|
||||
function el(id) { return document.getElementById(id); }
|
||||
|
||||
function init() {
|
||||
if (initialized) { resize(); reheat(); return; }
|
||||
initialized = true;
|
||||
canvas = el('graph-canvas'); ctx = canvas.getContext('2d');
|
||||
const disSel = el('g-focus-disease');
|
||||
[...new Set(Store.catalog.proteins.flatMap(p => p.diseases || []))].sort()
|
||||
.forEach(d => disSel.add(new Option(d, d)));
|
||||
el('g-show-drug').addEventListener('change', e => { showDrug = e.target.checked; build(); reheat(); });
|
||||
disSel.addEventListener('change', e => { focusDisease = e.target.value; build(); reheat(); });
|
||||
el('g-reheat').addEventListener('click', reheat);
|
||||
bindInteraction();
|
||||
window.addEventListener('resize', () => { if (isVisible()) resize(); });
|
||||
build(); resize(); reheat();
|
||||
}
|
||||
|
||||
function isVisible() { return el('tab-graph').classList.contains('active'); }
|
||||
|
||||
function build() {
|
||||
const G = Store.graph;
|
||||
const keep = new Set();
|
||||
let activeNodes = G.nodes.filter(n => {
|
||||
if (!showDrug && n.type === 'drug') return false;
|
||||
return true;
|
||||
});
|
||||
if (focusDisease) {
|
||||
const did = 'D:' + focusDisease;
|
||||
const nbr = new Set([did]);
|
||||
G.links.forEach(l => {
|
||||
const s = idOf(l.source), t = idOf(l.target);
|
||||
if (s === did) nbr.add(t); if (t === did) nbr.add(s);
|
||||
});
|
||||
// 단백질의 경로/축도 포함
|
||||
G.links.forEach(l => {
|
||||
const s = idOf(l.source), t = idOf(l.target);
|
||||
if (nbr.has(s) && (t.startsWith('PW:') || t.startsWith('AX:'))) nbr.add(t);
|
||||
if (nbr.has(t) && (s.startsWith('PW:') || s.startsWith('AX:'))) nbr.add(s);
|
||||
});
|
||||
activeNodes = activeNodes.filter(n => nbr.has(n.id));
|
||||
}
|
||||
activeNodes.forEach(n => keep.add(n.id));
|
||||
const prev = {}; nodes.forEach(n => prev[n.id] = n);
|
||||
nodes = activeNodes.map(n => {
|
||||
const p = prev[n.id];
|
||||
return Object.assign({}, n, {
|
||||
x: p ? p.x : (Math.random() - .5) * 600, y: p ? p.y : (Math.random() - .5) * 600,
|
||||
vx: 0, vy: 0, r: TYPE_R[n.type] + Math.min(6, (n.degree || 1) * 0.5),
|
||||
});
|
||||
});
|
||||
const byId = {}; nodes.forEach(n => byId[n.id] = n);
|
||||
links = G.links.filter(l => keep.has(idOf(l.source)) && keep.has(idOf(l.target)))
|
||||
.map(l => ({ s: byId[idOf(l.source)], t: byId[idOf(l.target)], type: l.type }));
|
||||
}
|
||||
|
||||
function idOf(x) { return typeof x === 'object' ? x.id : x; }
|
||||
|
||||
function resize() {
|
||||
const r = canvas.getBoundingClientRect();
|
||||
canvas.width = r.width * devicePixelRatio; canvas.height = r.height * devicePixelRatio;
|
||||
ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0);
|
||||
if (!view._init) { view.x = r.width / 2; view.y = r.height / 2; view.k = 0.8; view._init = true; }
|
||||
draw();
|
||||
}
|
||||
|
||||
function reheat() { alpha = 1; if (!running) loop(); }
|
||||
|
||||
function tick() {
|
||||
const k = 0.9; alpha *= 0.985;
|
||||
// 반발 (O(n^2), 노드 수 제한적)
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const a = nodes[i];
|
||||
for (let j = i + 1; j < nodes.length; j++) {
|
||||
const b = nodes[j];
|
||||
let dx = a.x - b.x, dy = a.y - b.y, d2 = dx * dx + dy * dy || 1;
|
||||
const f = 900 / d2;
|
||||
const d = Math.sqrt(d2);
|
||||
const fx = dx / d * f, fy = dy / d * f;
|
||||
a.vx += fx; a.vy += fy; b.vx -= fx; b.vy -= fy;
|
||||
}
|
||||
}
|
||||
// 스프링 (엣지)
|
||||
links.forEach(l => {
|
||||
let dx = l.t.x - l.s.x, dy = l.t.y - l.s.y, d = Math.sqrt(dx * dx + dy * dy) || 1;
|
||||
const target = 60, f = (d - target) * 0.02;
|
||||
const fx = dx / d * f, fy = dy / d * f;
|
||||
l.s.vx += fx; l.s.vy += fy; l.t.vx -= fx; l.t.vy -= fy;
|
||||
});
|
||||
// 중심 인력 + 감쇠 + 적용
|
||||
nodes.forEach(n => {
|
||||
if (n === (drag && drag.node)) return;
|
||||
n.vx -= n.x * 0.002; n.vy -= n.y * 0.002;
|
||||
n.vx *= k; n.vy *= k;
|
||||
n.x += n.vx * alpha * 2; n.y += n.vy * alpha * 2;
|
||||
});
|
||||
}
|
||||
|
||||
function loop() {
|
||||
running = true;
|
||||
tick(); draw();
|
||||
if (alpha > 0.02) raf = requestAnimationFrame(loop);
|
||||
else running = false;
|
||||
}
|
||||
|
||||
function toScreen(n) { return [view.x + n.x * view.k, view.y + n.y * view.k]; }
|
||||
function toWorld(px, py) { return [(px - view.x) / view.k, (py - view.y) / view.k]; }
|
||||
|
||||
function draw() {
|
||||
if (!ctx) return;
|
||||
const w = canvas.width / devicePixelRatio, h = canvas.height / devicePixelRatio;
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
// 엣지
|
||||
ctx.lineWidth = 0.6;
|
||||
links.forEach(l => {
|
||||
const [sx, sy] = toScreen(l.s), [tx, ty] = toScreen(l.t);
|
||||
ctx.strokeStyle = (hover && (l.s === hover || l.t === hover)) ? 'rgba(102,153,255,.5)' : 'rgba(255,255,255,.06)';
|
||||
ctx.beginPath(); ctx.moveTo(sx, sy); ctx.lineTo(tx, ty); ctx.stroke();
|
||||
});
|
||||
// 노드
|
||||
nodes.forEach(n => {
|
||||
const [x, y] = toScreen(n), r = n.r * Math.sqrt(view.k);
|
||||
ctx.beginPath(); ctx.arc(x, y, r, 0, 7);
|
||||
ctx.fillStyle = TYPE_COLOR[n.type] || '#888';
|
||||
ctx.globalAlpha = (hover && hover !== n && !isNeighbor(n, hover)) ? 0.25 : 1;
|
||||
ctx.fill();
|
||||
if (n === hover || n.type === 'disease' || n.type === 'axis' || (n.type === 'protein' && n.in_model && view.k > 0.7)) {
|
||||
ctx.globalAlpha = 1; ctx.fillStyle = '#dfe4ff';
|
||||
ctx.font = `${Math.max(9, 11 * Math.sqrt(view.k))}px Inter`;
|
||||
ctx.fillText(n.label, x + r + 2, y + 3);
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
});
|
||||
}
|
||||
|
||||
function isNeighbor(n, h) { return links.some(l => (l.s === n && l.t === h) || (l.t === n && l.s === h)); }
|
||||
|
||||
function nodeAt(px, py) {
|
||||
for (let i = nodes.length - 1; i >= 0; i--) {
|
||||
const [x, y] = toScreen(nodes[i]); const r = nodes[i].r * Math.sqrt(view.k) + 3;
|
||||
if ((px - x) ** 2 + (py - y) ** 2 < r * r) return nodes[i];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function bindInteraction() {
|
||||
const tip = el('graph-tooltip');
|
||||
canvas.addEventListener('mousemove', e => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const px = e.clientX - rect.left, py = e.clientY - rect.top;
|
||||
if (drag) { drag.moved = true; const [wx, wy] = toWorld(px, py); drag.node.x = wx; drag.node.y = wy; drag.node.vx = drag.node.vy = 0; draw(); return; }
|
||||
if (pan) { view.x += e.clientX - pan.x; view.y += e.clientY - pan.y; pan.x = e.clientX; pan.y = e.clientY; draw(); return; }
|
||||
const prev = hover;
|
||||
const n = nodeAt(px, py); hover = n;
|
||||
if (n) {
|
||||
canvas.style.cursor = 'pointer';
|
||||
tip.classList.remove('hidden');
|
||||
tip.style.left = (e.clientX + 14) + 'px'; tip.style.top = (e.clientY + 12) + 'px';
|
||||
tip.innerHTML = `<div class="tt-title">${n.label}</div><div class="tt-sub">${ttSub(n)}</div>`;
|
||||
} else { canvas.style.cursor = 'grab'; tip.classList.add('hidden'); }
|
||||
if (n !== prev) draw(); // hover 변화시에만 재그리기(유휴 redraw 방지)
|
||||
});
|
||||
canvas.addEventListener('mousedown', e => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const n = nodeAt(e.clientX - rect.left, e.clientY - rect.top);
|
||||
if (n) { drag = { node: n }; reheat(); } else pan = { x: e.clientX, y: e.clientY };
|
||||
});
|
||||
window.addEventListener('mouseup', () => {
|
||||
justDragged = !!(drag && drag.moved); // 이동을 동반한 드래그였으면 click 무시
|
||||
drag = null; pan = null;
|
||||
});
|
||||
canvas.addEventListener('click', e => {
|
||||
if (justDragged) { justDragged = false; return; } // 드래그 끝의 click 은 네비게이션 금지
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const n = nodeAt(e.clientX - rect.left, e.clientY - rect.top);
|
||||
if (n && n.type === 'protein') window.openProtein(n.label);
|
||||
});
|
||||
canvas.addEventListener('wheel', e => {
|
||||
e.preventDefault();
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const px = e.clientX - rect.left, py = e.clientY - rect.top;
|
||||
const [wx, wy] = toWorld(px, py);
|
||||
view.k *= e.deltaY < 0 ? 1.1 : 0.9; view.k = Math.max(0.2, Math.min(4, view.k));
|
||||
view.x = px - wx * view.k; view.y = py - wy * view.k; draw();
|
||||
}, { passive: false });
|
||||
}
|
||||
|
||||
function ttSub(n) {
|
||||
if (n.type === 'protein') return `${n.pathway || ''} · ${n.role || ''} · 근거 ${n.n_evidence || 0}편${n.in_model ? ' · 트윈 wired' : ''}`;
|
||||
if (n.type === 'axis') return '디지털 트윈 상태축';
|
||||
return { pathway: '신호 경로', disease: '질환', drug: '약물/물질' }[n.type] || '';
|
||||
}
|
||||
|
||||
window.GraphTab = { init };
|
||||
})();
|
||||
79
js/library.js
Normal file
79
js/library.js
Normal file
@ -0,0 +1,79 @@
|
||||
/* ============================================================
|
||||
library.js — Papers 탭 (논문 테이블 + 발굴 신규 논문)
|
||||
============================================================ */
|
||||
(function () {
|
||||
'use strict';
|
||||
let papers = [], filtered = [], page = 1, sortCol = 'pubYear', sortDir = -1;
|
||||
let paperTargets = {};
|
||||
const PAGE = 20;
|
||||
function el(id) { return document.getElementById(id); }
|
||||
|
||||
function init() {
|
||||
papers = (Store.analysis && Store.analysis.papers) || [];
|
||||
// 논문 → 표적 단백질 역색인 (카탈로그 근거 기반)
|
||||
Store.catalog.proteins.forEach(p => (p.evidence_paper_ids || []).forEach(id => {
|
||||
(paperTargets[id] = paperTargets[id] || []).push(p.gene);
|
||||
}));
|
||||
|
||||
const disSel = el('filter-disease'), ySel = el('filter-year');
|
||||
[...new Set(papers.map(p => p.disease).filter(Boolean))].sort().forEach(d => disSel.add(new Option(d, d)));
|
||||
[...new Set(papers.map(p => p.pubYear).filter(y => /^\d{4}$/.test(y)))].sort().reverse().forEach(y => ySel.add(new Option(y, y)));
|
||||
|
||||
el('search-input').addEventListener('input', apply);
|
||||
disSel.addEventListener('change', apply);
|
||||
ySel.addEventListener('change', apply);
|
||||
document.querySelectorAll('#tab-papers .sortable').forEach(th => th.onclick = () => {
|
||||
const c = th.dataset.col; if (sortCol === c) sortDir *= -1; else { sortCol = c; sortDir = 1; } apply();
|
||||
});
|
||||
el('btn-prev').onclick = () => { if (page > 1) { page--; renderTable(); } };
|
||||
el('btn-next').onclick = () => { if (page * PAGE < filtered.length) { page++; renderTable(); } };
|
||||
renderNewPapers();
|
||||
apply();
|
||||
}
|
||||
|
||||
function apply() {
|
||||
const q = el('search-input').value.trim().toLowerCase();
|
||||
const dis = el('filter-disease').value, yr = el('filter-year').value;
|
||||
filtered = papers.filter(p => {
|
||||
if (dis && p.disease !== dis) return false;
|
||||
if (yr && p.pubYear !== yr) return false;
|
||||
if (q && !((p.title || '').toLowerCase().includes(q) || (p.abstract || '').toLowerCase().includes(q))) return false;
|
||||
return true;
|
||||
});
|
||||
filtered.sort((a, b) => {
|
||||
const x = (a[sortCol] || ''), y = (b[sortCol] || '');
|
||||
return (x < y ? -1 : x > y ? 1 : 0) * sortDir;
|
||||
});
|
||||
page = 1; renderTable();
|
||||
}
|
||||
|
||||
function renderTable() {
|
||||
el('table-count').textContent = filtered.length;
|
||||
const start = (page - 1) * PAGE;
|
||||
const rows = filtered.slice(start, start + PAGE);
|
||||
el('papers-tbody').innerHTML = rows.length ? rows.map(p => {
|
||||
const tg = (paperTargets[p.id] || []).slice(0, 6);
|
||||
return `<tr>
|
||||
<td>${p.pubYear || '—'}</td>
|
||||
<td class="title-cell">${p.title || ''}</td>
|
||||
<td><span class="badge small">${p.disease || '—'}</span></td>
|
||||
<td><div class="targets-cell">${tg.map(g => `<span class="tg" data-gene="${g}">${g}</span>`).join('') || '—'}</div></td>
|
||||
</tr>`;
|
||||
}).join('') : '<tr><td colspan="4" class="loading-row">결과 없음</td></tr>';
|
||||
el('papers-tbody').querySelectorAll('.tg').forEach(t => t.onclick = () => window.openProtein(t.dataset.gene));
|
||||
el('page-info').textContent = `${page} / ${Math.max(1, Math.ceil(filtered.length / PAGE))}`;
|
||||
el('btn-prev').disabled = page <= 1;
|
||||
el('btn-next').disabled = page * PAGE >= filtered.length;
|
||||
}
|
||||
|
||||
function renderNewPapers() {
|
||||
const np = (Store.extras && Store.extras.new_papers) || [];
|
||||
el('newpapers-list').innerHTML = np.slice(0, 24).map(p => `
|
||||
<div class="newpaper-card">
|
||||
<div class="np-title">${p.title || ''}</div>
|
||||
<div class="np-meta">${p.year || ''} · 표적 <span class="np-target">${p.target || '—'}</span>${p.note ? ' · ' + p.note : ''}</div>
|
||||
</div>`).join('') || '<span class="panel-subtitle">발굴된 신규 논문 없음</span>';
|
||||
}
|
||||
|
||||
window.LibraryTab = { init };
|
||||
})();
|
||||
68
js/main.js
Normal file
68
js/main.js
Normal file
@ -0,0 +1,68 @@
|
||||
/* ============================================================
|
||||
main.js — 오케스트레이터 (데이터 로드 · 탭 네비 · 지연 초기화)
|
||||
============================================================ */
|
||||
(function () {
|
||||
'use strict';
|
||||
// Chart.js 전역 폰트를 Pretendard로 (모든 차트 생성 전에 즉시 적용)
|
||||
if (window.Chart) {
|
||||
Chart.defaults.font.family = "'Pretendard Variable', Pretendard, system-ui, 'Malgun Gothic', sans-serif";
|
||||
Chart.defaults.font.size = 13;
|
||||
Chart.defaults.color = '#4a463d';
|
||||
}
|
||||
const inited = {};
|
||||
|
||||
function el(id) { return document.getElementById(id); }
|
||||
|
||||
function setHeader() {
|
||||
const cat = Store.catalog || { proteins: [] };
|
||||
el('stat-proteins').textContent = cat.n_proteins || cat.proteins.length;
|
||||
el('stat-structures').textContent = (cat.proteins || []).filter(p => p.structure && p.structure.accession).length;
|
||||
const gs = cat.grounding_stats;
|
||||
if (gs) el('stat-grounding').textContent = `${gs.in_model_grounded}/${gs.in_model_total} 트윈·${gs.catalog_grounded}/${gs.catalog_total} 전체`;
|
||||
el('last-updated').textContent = (cat.last_updated || '').replace('T', ' ').slice(0, 16) || '—';
|
||||
const pa = (Store.extras && Store.extras.prior_art) || [];
|
||||
const fw = (Store.extras && Store.extras.frameworks) || [];
|
||||
el('prior-art-note').textContent = `선행연구 ${pa.length}건 · 권장 프레임워크 ${fw.length}종 · 에이전트 14기 마이닝`;
|
||||
}
|
||||
|
||||
function initTab(name) {
|
||||
if (inited[name]) {
|
||||
if (name === 'graph') window.GraphTab.init();
|
||||
if (name === 'atlas') window.AtlasTab.resize();
|
||||
return;
|
||||
}
|
||||
inited[name] = true;
|
||||
try {
|
||||
if (name === 'twin') window.TwinTab.init();
|
||||
else if (name === 'timeline') window.TimelineTab.init();
|
||||
else if (name === 'network') window.NetworkTab.init();
|
||||
else if (name === 'atlas') window.AtlasTab.init();
|
||||
else if (name === 'graph') window.GraphTab.init();
|
||||
else if (name === 'calibrate') window.CalibrateTab.init();
|
||||
else if (name === 'validation') window.ValidationTab.init();
|
||||
else if (name === 'stats') window.StatsTab.render();
|
||||
else if (name === 'papers') window.LibraryTab.init();
|
||||
} catch (e) { console.error('탭 초기화 오류', name, e); }
|
||||
}
|
||||
|
||||
function bindTabs() {
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
el('tab-' + btn.dataset.tab).classList.add('active');
|
||||
initTab(btn.dataset.tab);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function boot() {
|
||||
await Store.loadAll();
|
||||
setHeader();
|
||||
bindTabs();
|
||||
initTab('twin'); // 기본 탭
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', boot);
|
||||
})();
|
||||
338
js/network.js
Normal file
338
js/network.js
Normal file
@ -0,0 +1,338 @@
|
||||
/* ============================================================
|
||||
network.js — 단백질 상호작용 네트워크 + 모낭 (3D, 시간축)
|
||||
Three.js(WebGL) 로 단백질 신호망과 모낭을 3D 렌더. 마우스 회전,
|
||||
조명, 같은 시간 슬라이더로 기전→단백질→모낭 변화를 동기 표시.
|
||||
데이터: data/network_dynamics.json
|
||||
============================================================ */
|
||||
(function () {
|
||||
'use strict';
|
||||
let data = null, done = false, scen = 'AGA|finasteride', tIdx = 0, playing = false, timer = null;
|
||||
const el = id => document.getElementById(id);
|
||||
const GCOL = {
|
||||
androgen: 0xc8401f, wnt_antag: 0xd9683f, bmp: 0x9a6a12, wnt: 0x1f5d52,
|
||||
shh: 0x2f8f7f, dp: 0x2f63c8, hfsc: 0x7a55a8, hair: 0x1f6d3a, immune: 0xb3361b,
|
||||
};
|
||||
const HEX = { androgen: '#c8401f', wnt_antag: '#d9683f', bmp: '#9a6a12', wnt: '#1f5d52', shh: '#2f8f7f', dp: '#2f63c8', hfsc: '#7a55a8', hair: '#1f6d3a', immune: '#b3361b' };
|
||||
// 그룹별 깊이(z) — 드라이버 앞, 산출 뒤
|
||||
const GZ = { androgen: 2.6, immune: 2.6, wnt_antag: 1.3, bmp: 0.6, wnt: 0, shh: -0.7, dp: -1.5, hfsc: -2.2, hair: -2.9 };
|
||||
// 단백질 → UniProt 가속(AlphaFold 구조 로드용)
|
||||
const ACC = {
|
||||
AR: 'P10275', SRD5A2: 'P31213', IFNG: 'P01579', CD8A: 'P01732', 'HLA-DQB1': 'P01920', CXCL10: 'P02778',
|
||||
DKK1: 'O94907', SFRP1: 'Q8N474', BMP4: 'P12644', ID1: 'P41134', WNT10B: 'O00744', CTNNB1: 'P35222',
|
||||
LEF1: 'Q9UJU2', AXIN2: 'Q9Y2T1', SHH: 'Q15465', GLI1: 'P08151', SOX2: 'P48431', VCAN: 'P13611',
|
||||
ALPL: 'P05186', KRT15: 'P19012', CD34: 'P28906', LGR5: 'O75473', KRT85: 'P78386', KRT35: 'Q92764',
|
||||
};
|
||||
|
||||
let net = null, foll = null, raf = null; // 3D 컨텍스트
|
||||
let mode = 'sphere', nglStage = null, nglComps = {}, nglBuilt = false;
|
||||
|
||||
async function init() {
|
||||
if (done) return; done = true;
|
||||
try { data = await fetch('data/network_dynamics.json').then(r => r.json()); }
|
||||
catch (e) { el('net-wrap').innerHTML = '<p class="panel-subtitle">데이터 로드 실패: ' + e + '</p>'; return; }
|
||||
buildControls(); buildLegend();
|
||||
if (!window.THREE) {
|
||||
el('net-wrap').innerHTML = '<p class="panel-subtitle" style="padding:24px">3D 라이브러리(Three.js) 로드 실패 — 인터넷 연결 확인. (네트워크 데이터는 정상)</p>';
|
||||
return;
|
||||
}
|
||||
setupNet(); setupFoll();
|
||||
window.addEventListener('resize', onResize);
|
||||
update(); animate();
|
||||
}
|
||||
|
||||
function curScen() { return data.scenarios[scen]; }
|
||||
function months() { return data.months; }
|
||||
function actOf(nid) { return curScen().activity[nid][tIdx]; }
|
||||
function gact(group) {
|
||||
const ids = data.nodes.filter(n => n.group === group).map(n => n.id);
|
||||
if (!ids.length) return 0;
|
||||
return ids.reduce((s, id) => s + actOf(id), 0) / ids.length;
|
||||
}
|
||||
function hashJit(id) { let h = 0; for (let i = 0; i < id.length; i++) h = (h * 31 + id.charCodeAt(i)) % 1000; return (h / 1000 - 0.5) * 1.2; }
|
||||
// 단백질 클릭 → 아틀라스 탭 상세(있으면) / 없으면 AlphaFold EBI
|
||||
function openProteinSafe(gene) {
|
||||
const inCat = window.Store && Store.proteinByGene && Store.proteinByGene[gene];
|
||||
if (inCat && typeof window.openProtein === 'function') window.openProtein(gene);
|
||||
else if (ACC[gene]) window.open('https://alphafold.ebi.ac.uk/entry/' + ACC[gene], '_blank');
|
||||
}
|
||||
let _down = null; // 드래그 구분
|
||||
|
||||
// ---------- 컨트롤 ----------
|
||||
function buildControls() {
|
||||
const diseases = { AGA: '남성형 탈모(AGA)', AA: '원형 탈모(AA)' };
|
||||
const dz = el('net-disease');
|
||||
dz.innerHTML = Object.keys(diseases).map(d => `<option value="${d}">${diseases[d]}</option>`).join('');
|
||||
dz.value = scen.split('|')[0];
|
||||
dz.onchange = () => fillTreatments(dz.value);
|
||||
fillTreatments(dz.value);
|
||||
const sl = el('net-time'); sl.min = 0; sl.max = months().length - 1; sl.value = tIdx;
|
||||
sl.oninput = () => { stop(); tIdx = +sl.value; update(); };
|
||||
el('net-play').onclick = togglePlay;
|
||||
const mb = el('net-mode'); if (mb) mb.onclick = toggleMode;
|
||||
}
|
||||
|
||||
function toggleMode() {
|
||||
if (typeof NGL === 'undefined') { el('net-status').textContent = 'NGL(구조 뷰어) 미로딩 — 인터넷 확인'; return; }
|
||||
mode = mode === 'sphere' ? 'structure' : 'sphere';
|
||||
const mb = el('net-mode');
|
||||
if (mode === 'structure') {
|
||||
mb.textContent = '⚪ 구체(추상)'; mb.classList.add('on');
|
||||
el('net-canvas').style.display = 'none'; el('net-ngl').style.display = 'block';
|
||||
if (!nglBuilt) buildStructureNet();
|
||||
} else {
|
||||
mb.textContent = '🧬 AlphaFold 구조'; mb.classList.remove('on');
|
||||
el('net-ngl').style.display = 'none'; el('net-canvas').style.display = 'block';
|
||||
}
|
||||
update();
|
||||
}
|
||||
function fillTreatments(dz) {
|
||||
const keys = Object.keys(data.scenarios).filter(k => k.startsWith(dz + '|'));
|
||||
const tr = el('net-treat');
|
||||
tr.innerHTML = keys.map(k => `<option value="${k}">${data.scenarios[k].label.split('·')[1].trim()}</option>`).join('');
|
||||
scen = keys.includes(scen) ? scen : keys[Math.min(1, keys.length - 1)];
|
||||
tr.value = scen;
|
||||
tr.onchange = () => { stop(); scen = tr.value; update(); };
|
||||
}
|
||||
function buildLegend() {
|
||||
el('net-legend').innerHTML = Object.keys(data.groups).map(g =>
|
||||
`<span class="net-leg"><i style="background:${HEX[g]}"></i>${data.groups[g]}</span>`).join('')
|
||||
+ '<span class="net-leg" style="margin-left:auto">🖱 드래그=회전·휠=확대 · <b>단백질 클릭 → 아틀라스 상세</b></span>';
|
||||
}
|
||||
function togglePlay() { playing ? stop() : start(); }
|
||||
function start() {
|
||||
playing = true; el('net-play').textContent = '⏸ 정지';
|
||||
timer = setInterval(() => {
|
||||
tIdx++; if (tIdx >= months().length) { tIdx = months().length - 1; stop(); return; }
|
||||
el('net-time').value = tIdx; update();
|
||||
}, 720);
|
||||
}
|
||||
function stop() { playing = false; el('net-play').textContent = '▶ 재생'; if (timer) clearInterval(timer); timer = null; }
|
||||
|
||||
// ---------- 3D 공통 ----------
|
||||
function mkCtx(canvas, camPos) {
|
||||
const w = canvas.clientWidth || 400, h = canvas.clientHeight || 360;
|
||||
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
|
||||
renderer.setPixelRatio(Math.min(2, window.devicePixelRatio || 1));
|
||||
renderer.setSize(w, h, false);
|
||||
const scene = new THREE.Scene();
|
||||
const cam = new THREE.PerspectiveCamera(45, w / h, 0.1, 100);
|
||||
cam.position.set(camPos[0], camPos[1], camPos[2]);
|
||||
const ctrl = new THREE.OrbitControls(cam, renderer.domElement);
|
||||
ctrl.enableDamping = true; ctrl.dampingFactor = 0.08; ctrl.enablePan = false;
|
||||
scene.add(new THREE.AmbientLight(0xffffff, 0.62));
|
||||
const d1 = new THREE.DirectionalLight(0xfff4e6, 0.85); d1.position.set(5, 8, 9); scene.add(d1);
|
||||
const d2 = new THREE.DirectionalLight(0xcfe0ff, 0.32); d2.position.set(-6, -3, -5); scene.add(d2);
|
||||
return { renderer, scene, cam, ctrl, w, h };
|
||||
}
|
||||
function edgeCyl(a, b, color, radius, op) {
|
||||
const dir = new THREE.Vector3().subVectors(b, a); const len = dir.length();
|
||||
const geo = new THREE.CylinderGeometry(radius, radius, len, 6);
|
||||
const mat = new THREE.MeshStandardMaterial({ color, emissive: color, emissiveIntensity: .35, transparent: true, opacity: op, roughness: .6 });
|
||||
const m = new THREE.Mesh(geo, mat);
|
||||
m.position.copy(a).addScaledVector(dir, .5);
|
||||
m.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir.clone().normalize());
|
||||
return m;
|
||||
}
|
||||
|
||||
// ---------- 네트워크 3D ----------
|
||||
function nodePos(n) {
|
||||
return new THREE.Vector3((n.x - 50) / 50 * 8, (50 - n.y) / 50 * 5, (GZ[n.group] || 0) + hashJit(n.id));
|
||||
}
|
||||
function setupNet() {
|
||||
net = mkCtx(el('net-canvas'), [0, 0, 18]);
|
||||
net.ctrl.target.set(0, 0, 0); net.ctrl.autoRotate = true; net.ctrl.autoRotateSpeed = 0.5;
|
||||
net.nodes = {}; net.edges = [];
|
||||
data.nodes.forEach(n => {
|
||||
const geo = new THREE.SphereGeometry(0.42, 22, 22);
|
||||
const mat = new THREE.MeshStandardMaterial({ color: GCOL[n.group], emissive: GCOL[n.group], emissiveIntensity: .2, roughness: .35, metalness: .1 });
|
||||
const mesh = new THREE.Mesh(geo, mat); mesh.position.copy(nodePos(n));
|
||||
mesh.userData.id = n.id;
|
||||
net.scene.add(mesh); net.nodes[n.id] = mesh;
|
||||
// 라벨 스프라이트
|
||||
const sp = makeLabel(n.label); sp.position.copy(mesh.position).add(new THREE.Vector3(0, 0.85, 0));
|
||||
net.scene.add(sp);
|
||||
});
|
||||
data.edges.forEach(e => {
|
||||
const a = nodePos(data.nodes.find(n => n.id === e.from)), b = nodePos(data.nodes.find(n => n.id === e.to));
|
||||
if (!a || !b) return;
|
||||
const col = e.type === 'inhibit' ? 0xb3361b : 0x1f6d3a;
|
||||
const cyl = edgeCyl(a, b, col, 0.05, 0.5); cyl.userData = e; net.scene.add(cyl); net.edges.push(cyl);
|
||||
});
|
||||
// 클릭 → 단백질 상세 (드래그 제외)
|
||||
const cv = el('net-canvas'); cv.style.cursor = 'pointer';
|
||||
cv.addEventListener('pointerdown', e => { _down = [e.clientX, e.clientY]; });
|
||||
cv.addEventListener('pointerup', e => {
|
||||
if (mode !== 'sphere' || !net || !_down) return;
|
||||
if (Math.hypot(e.clientX - _down[0], e.clientY - _down[1]) > 6) { _down = null; return; }
|
||||
_down = null;
|
||||
const r = cv.getBoundingClientRect();
|
||||
const m = new THREE.Vector2(((e.clientX - r.left) / r.width) * 2 - 1, -((e.clientY - r.top) / r.height) * 2 + 1);
|
||||
const ray = new THREE.Raycaster(); ray.setFromCamera(m, net.cam);
|
||||
const hits = ray.intersectObjects(Object.values(net.nodes));
|
||||
if (hits.length && hits[0].object.userData.id) openProteinSafe(hits[0].object.userData.id);
|
||||
});
|
||||
}
|
||||
function makeLabel(text) {
|
||||
const cv = document.createElement('canvas'); const s = 128; cv.width = 256; cv.height = 64;
|
||||
const g = cv.getContext('2d'); g.font = '700 30px Pretendard, sans-serif'; g.fillStyle = '#1b1a16';
|
||||
g.textAlign = 'center'; g.textBaseline = 'middle'; g.fillText(text, 128, 34);
|
||||
const tex = new THREE.CanvasTexture(cv); tex.anisotropy = 4;
|
||||
const sp = new THREE.Sprite(new THREE.SpriteMaterial({ map: tex, transparent: true, depthTest: false }));
|
||||
sp.scale.set(2.4, 0.6, 1); return sp;
|
||||
}
|
||||
function updateNet() {
|
||||
if (!net) return;
|
||||
data.nodes.forEach(n => {
|
||||
const a = actOf(n.id), m = net.nodes[n.id];
|
||||
const s = 0.55 + 1.05 * a; m.scale.setScalar(s);
|
||||
m.material.emissiveIntensity = 0.12 + 0.95 * a;
|
||||
m.material.opacity = 1; m.material.transparent = false;
|
||||
});
|
||||
net.edges.forEach(c => { const sa = actOf(c.userData.from); c.material.opacity = 0.1 + 0.6 * sa; });
|
||||
}
|
||||
|
||||
// ---------- AlphaFold 구조 네트워크 (NGL) ----------
|
||||
const KPOS = 17, SSCALE = 0.5;
|
||||
function posK(n) { return [(n.x - 50) / 50 * 8 * KPOS, (50 - n.y) / 50 * 5 * KPOS, ((GZ[n.group] || 0) + hashJit(n.id)) * KPOS]; }
|
||||
function structUrls(acc) { return ['../digital_twin/data/structures/' + acc + '_AF.pdb', 'https://alphafold.ebi.ac.uk/files/AF-' + acc + '-F1-model_v4.pdb']; }
|
||||
|
||||
function buildStructureNet() {
|
||||
nglBuilt = true;
|
||||
const load = el('net-ngl-load'); load.style.display = 'flex';
|
||||
nglStage = new NGL.Stage('net-ngl', { backgroundColor: '#f2ece1' });
|
||||
el('net-ngl').style.cursor = 'pointer';
|
||||
const compId = {}; // uuid → gene
|
||||
nglStage.signals.clicked.add(pp => { if (pp && pp.component && compId[pp.component.uuid]) openProteinSafe(compId[pp.component.uuid]); });
|
||||
const nodesA = data.nodes.filter(n => ACC[n.id]); const total = nodesA.length; let loaded = 0;
|
||||
load.textContent = 'AlphaFold 구조 로딩 0/' + total;
|
||||
// 엣지 + 라벨(정적)
|
||||
const shape = new NGL.Shape('edges');
|
||||
data.edges.forEach(e => {
|
||||
const a = data.nodes.find(n => n.id === e.from), b = data.nodes.find(n => n.id === e.to);
|
||||
if (!a || !b || !ACC[a.id] || !ACC[b.id]) return;
|
||||
const col = e.type === 'inhibit' ? [0.7, 0.21, 0.11] : [0.12, 0.43, 0.23];
|
||||
try { shape.addCylinder(posK(a), posK(b), col, 0.9); } catch (er) {}
|
||||
});
|
||||
nodesA.forEach(n => { const p = posK(n); try { shape.addText([p[0], p[1] + 16, p[2]], [0.1, 0.09, 0.08], 15, n.label); } catch (er) {} });
|
||||
try { nglStage.addComponentFromObject(shape).addRepresentation('buffer'); } catch (er) {}
|
||||
// 구조
|
||||
nodesA.forEach(n => {
|
||||
const urls = structUrls(ACC[n.id]);
|
||||
const place = comp => {
|
||||
const r = comp.addRepresentation('cartoon', { color: HEX[n.group], smoothSheet: true });
|
||||
let cx = 0, cy = 0, cz = 0;
|
||||
try { const c = comp.structure.center; cx = c.x; cy = c.y; cz = c.z; } catch (e) {}
|
||||
comp.setScale(SSCALE);
|
||||
const p = posK(n);
|
||||
comp.setPosition([p[0] - SSCALE * cx, p[1] - SSCALE * cy, p[2] - SSCALE * cz]);
|
||||
compId[comp.uuid] = n.id;
|
||||
nglComps[n.id] = { comp, repr: r };
|
||||
};
|
||||
const fin = () => { loaded++; load.textContent = 'AlphaFold 구조 로딩 ' + loaded + '/' + total; if (loaded >= total) { load.style.display = 'none'; try { nglStage.autoView(600); } catch (e) {} updateStructureNet(); } };
|
||||
nglStage.loadFile(urls[0], { ext: 'pdb' }).then(c => { place(c); fin(); })
|
||||
.catch(() => nglStage.loadFile(urls[1], { ext: 'pdb' }).then(c => { place(c); fin(); }).catch(fin));
|
||||
});
|
||||
}
|
||||
function updateStructureNet() {
|
||||
data.nodes.forEach(n => {
|
||||
const c = nglComps[n.id]; if (!c) return;
|
||||
const a = actOf(n.id);
|
||||
try { c.repr.setParameters({ opacity: 0.45 + 0.55 * a }); } catch (e) {}
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- 모낭 3D ----------
|
||||
function setupFoll() {
|
||||
foll = mkCtx(el('net-follicle'), [0, -0.5, 12]);
|
||||
foll.ctrl.target.set(0, -1, 0); foll.ctrl.autoRotate = true; foll.ctrl.autoRotateSpeed = 0.8;
|
||||
// 피부층(반투명 디스크)
|
||||
const layer = (y, h, r, col, op) => {
|
||||
const m = new THREE.Mesh(new THREE.CylinderGeometry(r, r, h, 40),
|
||||
new THREE.MeshStandardMaterial({ color: col, transparent: true, opacity: op, roughness: .9 }));
|
||||
m.position.y = y; return m;
|
||||
};
|
||||
foll.scene.add(layer(2.1, 0.5, 4, 0xeac9a6, 0.55)); // 표피
|
||||
foll.scene.add(layer(-0.3, 4.2, 4, 0xe3cdb0, 0.34)); // 진피
|
||||
foll.scene.add(layer(-3.1, 1.4, 4, 0xf0dd9c, 0.4)); // 피하지방
|
||||
foll.group = new THREE.Group(); foll.scene.add(foll.group);
|
||||
}
|
||||
function buildFollicle(hair, dp, immune, disease) {
|
||||
const g = new THREE.Group();
|
||||
const surf = 2.0;
|
||||
const depth = disease === 'AA' ? (4.6 + 0.6 * hair) : (2.6 + 3.4 * dp);
|
||||
const bulbY = surf - depth;
|
||||
const bulbR = 0.45 + 0.7 * (disease === 'AA' ? 0.8 : dp);
|
||||
const neckR = 0.22 + 0.18 * (disease === 'AA' ? 0.8 : dp);
|
||||
// 외초 — LatheGeometry(프로파일 회전)
|
||||
const pts = [
|
||||
new THREE.Vector2(0.02, 0), new THREE.Vector2(bulbR, 0.35 * depth * 0.3 + 0.2),
|
||||
new THREE.Vector2(bulbR * 0.7, depth * 0.45), new THREE.Vector2(neckR, depth * 0.7),
|
||||
new THREE.Vector2(neckR, depth),
|
||||
];
|
||||
const sheath = new THREE.Mesh(new THREE.LatheGeometry(pts, 36),
|
||||
new THREE.MeshStandardMaterial({ color: 0xe6c9a4, transparent: true, opacity: 0.55, side: THREE.DoubleSide, roughness: .8 }));
|
||||
sheath.position.y = bulbY; g.add(sheath);
|
||||
// 진피유두 DP
|
||||
const dpR = 0.22 + 0.5 * dp;
|
||||
const dpm = new THREE.Mesh(new THREE.SphereGeometry(dpR, 20, 20),
|
||||
new THREE.MeshStandardMaterial({ color: 0xc98b5e, emissive: 0x6b3a1e, emissiveIntensity: .25, roughness: .5 }));
|
||||
dpm.position.y = bulbY + bulbR * 0.5; g.add(dpm);
|
||||
// 모간 hair shaft
|
||||
const hairR = disease === 'AA' ? 0.14 : (0.04 + 0.26 * hair);
|
||||
const hairAbove = 0.4 + 3.2 * hair;
|
||||
const shaftBottom = bulbY + bulbR * 0.4, shaftTop = surf + hairAbove;
|
||||
const aaBroken = (disease === 'AA' && hair < 0.45);
|
||||
const top = aaBroken ? surf - 0.2 + 0.4 * hair : shaftTop;
|
||||
const len = Math.max(0.2, top - shaftBottom);
|
||||
const hairCol = disease === 'AA' ? (hair > 0.5 ? 0x3f3020 : 0x6b5640) : (hair > 0.5 ? 0x402f1c : 0x8a7660);
|
||||
const shaft = new THREE.Mesh(new THREE.CylinderGeometry(hairR, hairR * 1.2, len, 12),
|
||||
new THREE.MeshStandardMaterial({ color: hairCol, roughness: .55, metalness: .15 }));
|
||||
shaft.position.y = shaftBottom + len / 2; g.add(shaft);
|
||||
// 피지선
|
||||
const sg = new THREE.Mesh(new THREE.SphereGeometry(0.3, 14, 14),
|
||||
new THREE.MeshStandardMaterial({ color: 0xdcc878, transparent: true, opacity: .7, roughness: .8 }));
|
||||
sg.position.set(neckR + 0.35, surf - depth * 0.28, 0); g.add(sg);
|
||||
// 면역세포(AA)
|
||||
if (disease === 'AA') {
|
||||
const n = Math.round(immune * 14);
|
||||
for (let i = 0; i < n; i++) {
|
||||
const a = i / Math.max(n, 1) * 6.283, rr = bulbR + 0.45 + (i % 3) * 0.18;
|
||||
const c = new THREE.Mesh(new THREE.SphereGeometry(0.13, 10, 10),
|
||||
new THREE.MeshStandardMaterial({ color: 0xb3361b, emissive: 0x7a1c0c, emissiveIntensity: .4 }));
|
||||
c.position.set(Math.cos(a) * rr, bulbY + bulbR * 0.5 + Math.sin(a * 1.7) * 0.5, Math.sin(a) * rr);
|
||||
g.add(c);
|
||||
}
|
||||
}
|
||||
return g;
|
||||
}
|
||||
function updateFoll() {
|
||||
if (!foll) return;
|
||||
const disease = scen.split('|')[0];
|
||||
while (foll.group.children.length) { const c = foll.group.children.pop(); c.traverse && c.traverse(o => { o.geometry && o.geometry.dispose(); o.material && o.material.dispose(); }); foll.group.remove(c); }
|
||||
foll.group.add(buildFollicle(gact('hair'), gact('dp'), gact('immune'), disease));
|
||||
let state;
|
||||
if (disease === 'AA') { const im = gact('immune'), h = gact('hair'); state = im > 0.55 ? '면역공격 · 모발탈락' : (h > 0.6 ? '재생 완료' : '재생 중'); }
|
||||
else { const h = gact('hair'); state = h < 0.32 ? '소형화 모낭(vellus)' : (h < 0.65 ? '회복 중' : '정상모(terminal)'); }
|
||||
el('net-foll-state').textContent = state;
|
||||
}
|
||||
|
||||
// ---------- 갱신/루프 ----------
|
||||
function update() {
|
||||
const m = months()[tIdx];
|
||||
el('net-month').textContent = m < 1 ? Math.round(m * 30) + '일' : m + '개월';
|
||||
const S = curScen();
|
||||
el('net-status').textContent = S.label + ' — 표적: ' + (S.targets && S.targets.length ? S.targets.map(t => data.groups[t]).join(', ') : '없음(무치료)');
|
||||
if (mode === 'structure' && nglBuilt) updateStructureNet(); else updateNet();
|
||||
updateFoll();
|
||||
}
|
||||
function onResize() {
|
||||
[net, foll].forEach(c => { if (!c) return; const cv = c.renderer.domElement; const w = cv.clientWidth, h = cv.clientHeight; if (w && h) { c.renderer.setSize(w, h, false); c.cam.aspect = w / h; c.cam.updateProjectionMatrix(); } });
|
||||
}
|
||||
function animate() {
|
||||
raf = requestAnimationFrame(animate);
|
||||
if (net) { net.ctrl.update(); net.renderer.render(net.scene, net.cam); }
|
||||
if (foll) { foll.ctrl.update(); foll.renderer.render(foll.scene, foll.cam); }
|
||||
}
|
||||
|
||||
window.NetworkTab = { init };
|
||||
})();
|
||||
57
js/stats.js
Normal file
57
js/stats.js
Normal file
@ -0,0 +1,57 @@
|
||||
/* ============================================================
|
||||
stats.js — Statistics 탭 (논문 + 단백질 카탈로그 통계)
|
||||
============================================================ */
|
||||
(function () {
|
||||
'use strict';
|
||||
let charts = {}, rendered = false;
|
||||
const C = ['#2f63c8', '#7c3aed', '#0e7490', '#1f8a5b', '#b07a12', '#c8401f', '#c2367f', '#c0561f', '#5b6470', '#9333ea'];
|
||||
|
||||
function count(arr, key) {
|
||||
const m = {};
|
||||
arr.forEach(x => { const k = (typeof key === 'function' ? key(x) : x[key]) || '기타'; m[k] = (m[k] || 0) + 1; });
|
||||
return m;
|
||||
}
|
||||
function sorted(obj, n) { return Object.entries(obj).sort((a, b) => b[1] - a[1]).slice(0, n || 99); }
|
||||
|
||||
function doughnut(id, obj, n) {
|
||||
const e = sorted(obj, n);
|
||||
mk(id, 'doughnut', {
|
||||
labels: e.map(x => x[0]), datasets: [{ data: e.map(x => x[1]), backgroundColor: C, borderColor: '#fbf9f4', borderWidth: 2 }]
|
||||
}, { plugins: { legend: { position: 'right', labels: { color: '#4a463d', font: { size: 11 }, boxWidth: 12 } } } });
|
||||
}
|
||||
function bar(id, obj, n, horizontal) {
|
||||
const e = sorted(obj, n);
|
||||
mk(id, 'bar', {
|
||||
labels: e.map(x => x[0]), datasets: [{ data: e.map(x => x[1]), backgroundColor: C[0], borderRadius: 4 }]
|
||||
}, {
|
||||
indexAxis: horizontal ? 'y' : 'x', plugins: { legend: { display: false } },
|
||||
scales: { x: { ticks: { color: '#6b655a', font: { size: 10 } }, grid: { color: 'rgba(0,0,0,.08)' } },
|
||||
y: { ticks: { color: '#6b655a', font: { size: 10 } }, grid: { display: false } } }
|
||||
});
|
||||
}
|
||||
function line(id, obj) {
|
||||
const e = Object.entries(obj).filter(([y]) => /^\d{4}$/.test(y)).sort();
|
||||
mk(id, 'line', {
|
||||
labels: e.map(x => x[0]),
|
||||
datasets: [{ data: e.map(x => x[1]), borderColor: '#22d3ee', backgroundColor: 'rgba(34,211,238,.12)', fill: true, tension: .3, pointRadius: 2 }]
|
||||
}, { plugins: { legend: { display: false } }, scales: { x: { ticks: { color: '#6b655a' }, grid: { display: false } }, y: { ticks: { color: '#6b655a' }, grid: { color: 'rgba(0,0,0,.08)' } } } });
|
||||
}
|
||||
function mk(id, type, data, opts) {
|
||||
const ctx = document.getElementById(id); if (!ctx) return;
|
||||
if (charts[id]) charts[id].destroy();
|
||||
charts[id] = new Chart(ctx, { type, data, options: Object.assign({ responsive: true, maintainAspectRatio: false }, opts) });
|
||||
}
|
||||
|
||||
function render() {
|
||||
if (rendered) return; rendered = true;
|
||||
const papers = (Store.analysis && Store.analysis.papers) || [];
|
||||
const proteins = Store.catalog.proteins;
|
||||
doughnut('chart-disease', count(papers, 'disease'), 6);
|
||||
bar('chart-axis', count(proteins.filter(p => p.twin_node && p.twin_node !== '—'), 'twin_node'), 8, true);
|
||||
line('chart-trend', count(papers, 'pubYear'));
|
||||
bar('chart-pathway', count(proteins, 'pathway'), 10, true);
|
||||
doughnut('chart-modality', count(papers, p => p.modality), 6);
|
||||
}
|
||||
|
||||
window.StatsTab = { render };
|
||||
})();
|
||||
314
js/timeline.js
Normal file
314
js/timeline.js
Normal file
@ -0,0 +1,314 @@
|
||||
/* ============================================================
|
||||
timeline.js — 치료 타임라인 시뮬레이션 탭
|
||||
업로드한 두피/쥐/원형탈모 사진 위에, 치료 반응을 0~36개월 프로그레스
|
||||
바로 표현.
|
||||
── 정직성 원칙 ──────────────────────────────────────────
|
||||
· 회복의 '양(magnitude)' = TwinEngine 모델(치료 전 질환 평형 → 치료 후
|
||||
평형 밀도). 모델의 정량 출력.
|
||||
· 회복의 '속도(timing)' = 임상시험 문헌의 반응 동역학(lag/tau/shed).
|
||||
모델 ODE 의 '일(day)'은 임상시간과 무관(수주 내 평형)하므로 시간축으로
|
||||
쓰지 않고 폐기 → 문헌 시간상수로 보정.
|
||||
density(t) = baseline + (target − baseline) · f_clinical(t)
|
||||
· 사진 변형 = 위 곡선이 구동하는 절차적 캔버스 일러스트(생성형 AI 아님).
|
||||
환부 '기하'는 사용자 사진에서, '얼마나·언제'는 모델+문헌. 개별 환자 예측 아님.
|
||||
============================================================ */
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const DISEASES = [
|
||||
{ key: "Alopecia Areata", ko: "원형 탈모 (AA)", sub: "두피·패치", style: "edge", ivs: ["JAK_inhibitor", "corticosteroid"] },
|
||||
{ key: "Androgenetic Alopecia", ko: "남성형 탈모 (AGA)", sub: "두피·미만성", style: "diffuse", ivs: ["finasteride", "minoxidil", "dutasteride"] },
|
||||
{ key: "Chemotherapy-induced Alopecia", ko: "항암 탈모 (CIA)", sub: "전두피", style: "diffuse", ivs: ["minoxidil", "CDK46_inhibitor"] },
|
||||
{ key: "__mouse__", ko: "쥐 제모 발모 (마우스)", sub: "등쪽·균일", style: "uniform", ivs: ["minoxidil", "wnt_agonist"] },
|
||||
];
|
||||
const IV_KO = {
|
||||
finasteride: "피나스테리드", dutasteride: "두타스테리드", minoxidil: "미녹시딜",
|
||||
JAK_inhibitor: "JAK 억제제", corticosteroid: "코르티코스테로이드",
|
||||
CDK46_inhibitor: "CDK4/6 억제제", wnt_agonist: "Wnt 작용제",
|
||||
};
|
||||
|
||||
// 임상 반응 동역학(개월). lag=가시반응 지연, tau=특성시간(≈63% 도달), shed=초기 telogen 탈락.
|
||||
// ▶ 3가지 증거를 화해(reconcile)시켜 보정 — digital_twin/timeline_calibration.py + 2차 웹스윕:
|
||||
// (1) 데이터적합: CT.gov 실궤적(THRIVE-AA1/2·brepo·ritle SALT 다시점, 5-ARI 24주). JAK 재적합 R²0.94~0.99.
|
||||
// (2) 기계론적 바닥: 가시 발모는 모낭주기(텔로젠 ~2-3개월)에 묶임 — 약물 PK는 빠르나(DHT 수시간) 모발은 느림.
|
||||
// 단기시험의 lag~0 적합은 초기 TAHC '굵어짐' 과적합 → 가시 밀도 lag는 ≥~2개월(5-ARI).
|
||||
// (3) 장기: 피나 Kaufman 2년까지 지속상승(tau~6). JAK는 wk4-8 분리(immune→기존모낭 재진입)로 더 빠름.
|
||||
// ⚠ 단일 지수는 (a)AGA 이상성(빠른 굵어짐+느린 밀도), (b)피나 yr2-5 감소, (c)미녹 재퇴행 재현 못 함(한계).
|
||||
const TIME_MODEL = {
|
||||
finasteride: { lag: 2.0, tau: 6, shed: 0.05 }, // telogen 바닥 + Kaufman 2yr (R²0.999)
|
||||
dutasteride: { lag: 2.0, tau: 5, shed: 0.05 }, // 더 깊은 DHT 억제(scalp 51% vs 41%)·약간 빠름
|
||||
minoxidil: { lag: 1.5, tau: 4, shed: 0.14 }, // telogen 단축 → 약간 빠름; SULT1A1 응답자 ~40%
|
||||
JAK_inhibitor: { lag: 1.0, tau: 4, shed: 0 }, // THRIVE wk4-8 분리; tau는 baricitinib 느림 포함 절충
|
||||
corticosteroid: { lag: 1.0, tau: 3, shed: 0 },
|
||||
CDK46_inhibitor: { lag: 1.5, tau: 4, shed: 0 }, // 데이터 없음(실험적)
|
||||
wnt_agonist: { lag: 1.5, tau: 4, shed: 0 }, // 데이터 없음(실험적)
|
||||
};
|
||||
const TIME_DEFAULT = { lag: 2, tau: 4.5, shed: 0 }; // 무처치/기타
|
||||
const MOUSE_KIN = { lag: 0.2, tau: 0.6, shed: 0 }; // 마우스 모주기 ~2-3주(실제로 빠름)
|
||||
// 절대밀도 환산 레퍼런스(정상=100%): 한국인 정수리 ~130 hairs/cm² (Yoo 2002·Han 2004 phototrichogram; 백인 ~226).
|
||||
const NORMAL_DENSITY = 130;
|
||||
|
||||
const S = {
|
||||
img: null, region: null, strokes: null, hairColor: '#34281d',
|
||||
disease: DISEASES[0], ivs: ["JAK_inhibitor"],
|
||||
baseline: null, target: null, kin: null, ti: 0,
|
||||
marking: false, markStart: null, chart: null,
|
||||
};
|
||||
function el(id) { return document.getElementById(id); }
|
||||
function gctx() { return el('tl-canvas').getContext('2d', { willReadFrequently: true }); }
|
||||
|
||||
// 임상 시간축(개월). 초반 telogen-shed 구간은 촘촘, 후반은 분기/연 단위.
|
||||
const TIMEPOINTS = [
|
||||
{ m: 0, l: '0' }, { m: 0.5, l: '2주' }, { m: 1, l: '1개월' }, { m: 2, l: '2개월' },
|
||||
{ m: 3, l: '3개월' }, { m: 4, l: '4개월' }, { m: 6, l: '6개월' }, { m: 9, l: '9개월' },
|
||||
{ m: 12, l: '1년' }, { m: 18, l: '1.5년' }, { m: 24, l: '2년' }, { m: 30, l: '2.5년' }, { m: 36, l: '3년' },
|
||||
];
|
||||
function curMonth() { return TIMEPOINTS[S.ti].m; }
|
||||
function setSliderFill() {
|
||||
const sl = el('tl-slider'); if (!sl) return;
|
||||
const pct = (S.ti / (TIMEPOINTS.length - 1)) * 100;
|
||||
sl.style.background = `linear-gradient(90deg,var(--primary) ${pct}%,var(--line2) ${pct}%)`;
|
||||
}
|
||||
|
||||
function init() {
|
||||
el('tl-disease').innerHTML = DISEASES.map((d, i) =>
|
||||
`<button class="seg-btn${i === 0 ? ' active' : ''}" data-i="${i}">
|
||||
<span>${d.ko}</span><span class="seg-sub">${d.sub}</span></button>`).join('');
|
||||
el('tl-disease').querySelectorAll('.seg-btn').forEach(b =>
|
||||
b.onclick = () => { selectDisease(+b.dataset.i); });
|
||||
|
||||
renderIvs();
|
||||
el('tl-file').addEventListener('change', onFile);
|
||||
bindCanvasMarking();
|
||||
const sl = el('tl-slider');
|
||||
sl.max = TIMEPOINTS.length - 1; sl.value = 0; S.ti = 0;
|
||||
sl.addEventListener('input', () => { S.ti = +sl.value; setSliderFill(); renderAt(); });
|
||||
setSliderFill();
|
||||
el('tl-remark') && (el('tl-remark').onclick = () => startMark());
|
||||
el('tl-demo') && (el('tl-demo').onclick = loadDemo);
|
||||
selectDisease(0);
|
||||
}
|
||||
|
||||
function selectDisease(i) {
|
||||
S.disease = DISEASES[i];
|
||||
el('tl-disease').querySelectorAll('.seg-btn').forEach((b, j) => b.classList.toggle('active', j === i));
|
||||
S.ivs = S.disease.ivs.slice(0, 1);
|
||||
renderIvs();
|
||||
computeCurve();
|
||||
if (S.img) { rebuildStrokes(); renderAt(); }
|
||||
}
|
||||
|
||||
function renderIvs() {
|
||||
el('tl-interventions').innerHTML = S.disease.ivs.map(iv =>
|
||||
`<button class="chip${S.ivs.includes(iv) ? ' active' : ''}" data-iv="${iv}">${IV_KO[iv] || iv}</button>`).join('');
|
||||
el('tl-interventions').querySelectorAll('.chip').forEach(c =>
|
||||
c.onclick = () => {
|
||||
const iv = c.dataset.iv;
|
||||
if (S.ivs.includes(iv)) S.ivs = S.ivs.filter(x => x !== iv); else S.ivs.push(iv);
|
||||
renderIvs(); computeCurve(); if (S.img) renderAt();
|
||||
});
|
||||
}
|
||||
|
||||
// 회복의 '양' = 모델. baseline(치료 전 질환 평형) / target(치료 후 평형) 밀도를 트윈에서 취득.
|
||||
function computeCurve() {
|
||||
let y0, diseaseForRun;
|
||||
if (S.disease.key === "__mouse__") {
|
||||
y0 = TwinEngine.diseaseEquilibrium("Healthy"); y0[6] = 0.08; // 갓 제모(모발만 바닥)
|
||||
diseaseForRun = "Healthy";
|
||||
} else {
|
||||
y0 = TwinEngine.diseaseEquilibrium(S.disease.key);
|
||||
diseaseForRun = S.disease.key;
|
||||
}
|
||||
const mc = TwinEngine.run(diseaseForRun, S.ivs, { y0, days: 1095 }).states.HairDensity;
|
||||
S.baseline = mc[0]; // 치료 전 밀도 = 모델
|
||||
S.target = Math.max.apply(null, mc); // 치료 후 평형 밀도 = 모델
|
||||
S.kin = combineKinetics(); // 회복 속도 = 임상 문헌
|
||||
renderChart();
|
||||
}
|
||||
|
||||
function combineKinetics() {
|
||||
if (S.disease.key === "__mouse__") return MOUSE_KIN;
|
||||
if (!S.ivs.length) return TIME_DEFAULT;
|
||||
let lag = 99, tau = 99, shed = 0;
|
||||
S.ivs.forEach(iv => { const k = TIME_MODEL[iv] || TIME_DEFAULT;
|
||||
lag = Math.min(lag, k.lag); tau = Math.min(tau, k.tau); shed = Math.max(shed, k.shed); });
|
||||
return { lag, tau, shed };
|
||||
}
|
||||
// 임상 회복 분율 f(t개월) ∈ [-shed,1]: lag 후 1−exp 상승 + 초기 telogen 탈락 딥.
|
||||
function recoveryAtMonth(m) {
|
||||
const k = S.kin || TIME_DEFAULT;
|
||||
const grow = m <= k.lag ? 0 : 1 - Math.exp(-(m - k.lag) / k.tau);
|
||||
const dip = -k.shed * Math.exp(-Math.pow(m - 0.7, 2) / (2 * 0.5 * 0.5)); // ~2-8주 탈락
|
||||
return Math.max(-0.25, Math.min(1, grow + dip));
|
||||
}
|
||||
function densityAtMonth(m) {
|
||||
return S.baseline + (S.target - S.baseline) * recoveryAtMonth(m);
|
||||
}
|
||||
|
||||
// ── 파일 업로드 ──
|
||||
function onFile(e) {
|
||||
const f = e.target.files && e.target.files[0];
|
||||
if (!f) return;
|
||||
const r = new FileReader();
|
||||
r.onload = () => loadImageSrc(r.result);
|
||||
r.readAsDataURL(f);
|
||||
}
|
||||
function loadImageSrc(src) {
|
||||
const im = new Image();
|
||||
im.onload = () => { S.img = im; setupCanvas(); defaultRegion(); rebuildStrokes(); startMark(); renderAt(); el('tl-stage').classList.remove('empty'); };
|
||||
im.src = src;
|
||||
}
|
||||
// 데모: 합성 두피(피부+원형 무모부) — 사진 없을 때 동작 확인용
|
||||
function loadDemo() {
|
||||
const c = document.createElement('canvas'); c.width = 640; c.height = 480;
|
||||
const x = c.getContext('2d');
|
||||
x.fillStyle = '#caa987'; x.fillRect(0, 0, 640, 480);
|
||||
for (let i = 0; i < 14000; i++) {
|
||||
const px = Math.random() * 640, py = Math.random() * 480;
|
||||
const d = Math.hypot(px - 320, py - 240);
|
||||
if (d < 95) continue;
|
||||
x.strokeStyle = `rgba(50,35,24,${0.25 + Math.random() * 0.4})`;
|
||||
x.lineWidth = 1; x.beginPath(); x.moveTo(px, py);
|
||||
x.lineTo(px + Math.random() * 6 - 3, py + 6 + Math.random() * 5); x.stroke();
|
||||
}
|
||||
x.fillStyle = 'rgba(214,180,150,.85)'; x.beginPath(); x.ellipse(320, 240, 92, 80, 0, 0, 7); x.fill();
|
||||
loadImageSrc(c.toDataURL());
|
||||
}
|
||||
|
||||
function setupCanvas() {
|
||||
const cv = el('tl-canvas'), maxW = 760;
|
||||
let w = S.img.naturalWidth, h = S.img.naturalHeight;
|
||||
const sc = Math.min(1, maxW / w); w = Math.round(w * sc); h = Math.round(h * sc);
|
||||
cv.width = w; cv.height = h;
|
||||
}
|
||||
function defaultRegion() {
|
||||
const cv = el('tl-canvas');
|
||||
if (S.disease.style === 'diffuse') S.region = { cx: cv.width / 2, cy: cv.height * 0.42, rx: cv.width * 0.40, ry: cv.height * 0.34 };
|
||||
else if (S.disease.style === 'uniform') S.region = { cx: cv.width / 2, cy: cv.height / 2, rx: cv.width * 0.42, ry: cv.height * 0.34 };
|
||||
else S.region = { cx: cv.width / 2, cy: cv.height / 2, rx: cv.width * 0.18, ry: cv.width * 0.18 };
|
||||
}
|
||||
|
||||
// ── 환부 마킹 (드래그로 타원) ──
|
||||
function startMark() { S.marking = 'await'; el('tl-hint').classList.remove('hidden'); renderAt(); }
|
||||
function bindCanvasMarking() {
|
||||
const cv = el('tl-canvas');
|
||||
const pos = ev => { const r = cv.getBoundingClientRect(); return { x: (ev.clientX - r.left) * cv.width / r.width, y: (ev.clientY - r.top) * cv.height / r.height }; };
|
||||
cv.addEventListener('mousedown', ev => { if (!S.img) return; S.marking = true; S.markStart = pos(ev); });
|
||||
cv.addEventListener('mousemove', ev => {
|
||||
if (S.marking !== true || !S.markStart) return;
|
||||
const p = pos(ev);
|
||||
S.region = { cx: S.markStart.x, cy: S.markStart.y, rx: Math.max(8, Math.abs(p.x - S.markStart.x)), ry: Math.max(8, Math.abs(p.y - S.markStart.y)) };
|
||||
renderAt(true);
|
||||
});
|
||||
window.addEventListener('mouseup', () => {
|
||||
if (S.marking === true && S.region) { S.marking = false; el('tl-hint').classList.add('hidden'); rebuildStrokes(); renderAt(); }
|
||||
});
|
||||
}
|
||||
|
||||
// ── 절차적 모발 스트로크 사전 생성(안정적 progressive 성장) ──
|
||||
function rebuildStrokes() {
|
||||
if (!S.region || !S.img) return;
|
||||
sampleHairColor();
|
||||
const R = S.region, area = Math.PI * R.rx * R.ry;
|
||||
const N = Math.max(700, Math.min(5200, Math.round(area / 16)));
|
||||
const arr = new Array(N);
|
||||
for (let i = 0; i < N; i++) {
|
||||
const a = Math.random() * Math.PI * 2, rad = Math.sqrt(Math.random());
|
||||
const t = rad;
|
||||
arr[i] = {
|
||||
x: R.cx + rad * Math.cos(a) * R.rx,
|
||||
y: R.cy + rad * Math.sin(a) * R.ry,
|
||||
t, thrEdge: 1 - t, thrUniform: Math.random(),
|
||||
len: ((R.rx + R.ry) / 2) * (0.035 + Math.random() * 0.05),
|
||||
ang: Math.PI / 2 + (Math.random() - 0.5) * 0.9,
|
||||
shade: 0.55 + Math.random() * 0.45,
|
||||
};
|
||||
}
|
||||
S.strokes = arr;
|
||||
}
|
||||
function sampleHairColor() {
|
||||
try {
|
||||
const cv = el('tl-canvas'), x = gctx();
|
||||
x.clearRect(0, 0, cv.width, cv.height); x.drawImage(S.img, 0, 0, cv.width, cv.height);
|
||||
const img = x.getImageData(0, 0, cv.width, cv.height).data;
|
||||
const R = S.region; let rs = 0, gs = 0, bs = 0, n = 0;
|
||||
for (let k = 0; k < 500; k++) {
|
||||
const a = Math.random() * Math.PI * 2, rad = 1.08 + Math.random() * 0.22;
|
||||
const px = Math.round(R.cx + rad * Math.cos(a) * R.rx), py = Math.round(R.cy + rad * Math.sin(a) * R.ry);
|
||||
if (px < 0 || py < 0 || px >= cv.width || py >= cv.height) continue;
|
||||
const i = (py * cv.width + px) * 4; rs += img[i]; gs += img[i + 1]; bs += img[i + 2]; n++;
|
||||
}
|
||||
if (n > 20) { S.hairColor = `rgb(${Math.round(rs / n * 0.75)},${Math.round(gs / n * 0.72)},${Math.round(bs / n * 0.7)})`; }
|
||||
} catch (e) { S.hairColor = '#34281d'; }
|
||||
}
|
||||
|
||||
// ── 렌더 ──
|
||||
function renderAt(dragging) {
|
||||
const cv = el('tl-canvas'); if (!cv.width) return;
|
||||
const x = gctx();
|
||||
x.clearRect(0, 0, cv.width, cv.height);
|
||||
if (S.img) x.drawImage(S.img, 0, 0, cv.width, cv.height);
|
||||
const R = S.region;
|
||||
if (R && S.strokes && !dragging) {
|
||||
const r = Math.max(0, recoveryAtMonth(curMonth())); // 탈락(음수)은 빈 두피로
|
||||
const useEdge = S.disease.style === 'edge';
|
||||
x.lineCap = 'round';
|
||||
for (const s of S.strokes) {
|
||||
const thr = useEdge ? s.thrEdge : s.thrUniform;
|
||||
if (thr > r) continue;
|
||||
x.strokeStyle = shade(S.hairColor, s.shade);
|
||||
x.lineWidth = 1.1;
|
||||
x.beginPath(); x.moveTo(s.x, s.y);
|
||||
x.lineTo(s.x + Math.cos(s.ang) * s.len, s.y + Math.sin(s.ang) * s.len);
|
||||
x.stroke();
|
||||
}
|
||||
}
|
||||
if (R) {
|
||||
x.save(); x.setLineDash([6, 5]); x.strokeStyle = 'rgba(200,64,31,.55)'; x.lineWidth = 1.5;
|
||||
x.beginPath(); x.ellipse(R.cx, R.cy, R.rx, R.ry, 0, 0, Math.PI * 2); x.stroke(); x.restore();
|
||||
}
|
||||
watermark(x, cv);
|
||||
updateReadout();
|
||||
}
|
||||
function shade(rgb, f) {
|
||||
const m = rgb.match(/\d+/g) || [52, 40, 29];
|
||||
return `rgb(${Math.round(m[0] * f)},${Math.round(m[1] * f)},${Math.round(m[2] * f)})`;
|
||||
}
|
||||
function watermark(x, cv) {
|
||||
x.save();
|
||||
x.font = "12px 'Pretendard Variable', sans-serif"; x.textAlign = 'right';
|
||||
x.fillStyle = 'rgba(255,255,255,.82)'; x.strokeStyle = 'rgba(0,0,0,.45)'; x.lineWidth = 3;
|
||||
const msg = "모델+문헌 일러스트 · 임상 예측 아님";
|
||||
x.strokeText(msg, cv.width - 10, cv.height - 10); x.fillText(msg, cv.width - 10, cv.height - 10);
|
||||
x.restore();
|
||||
}
|
||||
|
||||
function updateReadout() {
|
||||
const tp = TIMEPOINTS[S.ti];
|
||||
el('tl-month-label').textContent = tp.l;
|
||||
el('tl-month-sub').textContent = tp.m >= 1 ? '· 치료 ' + tp.m + '개월차' : '· 치료 ' + Math.round(tp.m * 30) + '일차';
|
||||
if (S.target == null) return;
|
||||
const dens = densityAtMonth(tp.m), rec = Math.max(0, recoveryAtMonth(tp.m)) * 100;
|
||||
el('tl-density').textContent = dens.toFixed(0) + '% (~' + Math.round(dens / 100 * NORMAL_DENSITY) + '/cm²)';
|
||||
el('tl-recovery').textContent = rec.toFixed(0) + '%';
|
||||
el('tl-ivs-readout').textContent = S.ivs.length ? S.ivs.map(i => IV_KO[i] || i).join(' + ') : '무처치';
|
||||
}
|
||||
|
||||
// 밀도 곡선(임상 개월) — '양=모델, 속도=문헌' 결합 곡선을 보여주는 근거 차트
|
||||
function renderChart() {
|
||||
const ctx = el('chart-timeline'); if (!ctx || S.target == null) return;
|
||||
const labels = TIMEPOINTS.map(p => p.l);
|
||||
const vals = TIMEPOINTS.map(p => +densityAtMonth(p.m).toFixed(1));
|
||||
const data = { labels, datasets: [{ label: '모발 밀도 %', data: vals, borderColor: '#c8401f', backgroundColor: 'rgba(200,64,31,.10)', fill: true, tension: .3, pointRadius: 2, borderWidth: 2 }] };
|
||||
const opts = {
|
||||
responsive: true, maintainAspectRatio: false,
|
||||
scales: { y: { min: 0, max: 105, ticks: { color: '#6b655a', callback: v => v + '%' }, grid: { color: 'rgba(0,0,0,.08)' } },
|
||||
x: { ticks: { color: '#6b655a', maxTicksLimit: 9, autoSkip: true }, grid: { display: false } } },
|
||||
plugins: { legend: { display: false } },
|
||||
};
|
||||
if (S.chart) { S.chart.data = data; S.chart.options = opts; S.chart.update('none'); }
|
||||
else S.chart = new Chart(ctx, { type: 'line', data, options: opts });
|
||||
}
|
||||
|
||||
window.TimelineTab = { init };
|
||||
})();
|
||||
172
js/twin-engine.js
Normal file
172
js/twin-engine.js
Normal file
@ -0,0 +1,172 @@
|
||||
/* ============================================================
|
||||
twin-engine.js — 모낭 디지털 트윈 ODE 엔진 (브라우저 포팅)
|
||||
digital_twin/follicle_model.py 의 정확한 포팅. RK4 적분.
|
||||
라이브 'what-if' 모드용. 정밀 시나리오는 twin_scenarios.json 사용.
|
||||
============================================================ */
|
||||
(function (global) {
|
||||
'use strict';
|
||||
|
||||
const STATE_NAMES = ["Wnt", "BMP", "SHH", "DP", "HFSC", "APO", "Hair"];
|
||||
|
||||
// Python Params 와 1:1 동일
|
||||
const P = {
|
||||
kWp: 0.45, kWd: 0.32, kBp: 0.30, kBd: 0.38, kSp: 0.50, kSd: 0.40,
|
||||
kDp: 0.50, kDd: 0.30, kHp: 0.58, kHd: 0.24, kAd: 0.50,
|
||||
kHairP: 0.80, kHairD: 0.16, Wbasal: 1.0, Bbasal: 0.4, Dbasal: 1.0,
|
||||
kDKK: 0.65, Kd: 1.0, Kb: 1.2, kBMPand: 0.30, Kw: 0.7, Ksw: 0.5, Kb2: 0.9,
|
||||
kIGFand: 0.55, kMiniAND: 0.40, hpow: 0.6, Hcap: 3.5,
|
||||
kDinf: 0.45, kDapo: 0.5, Kh: 0.35, Ks: 0.35, Kb3: 1.5,
|
||||
kHinf: 0.55, kHapo: 0.6, kApAND: 0.30, kApINF: 0.85, kSurv: 1.6,
|
||||
Ksh: 0.4, kHairApo: 0.55,
|
||||
};
|
||||
|
||||
const DISEASE_PRESETS = {
|
||||
"Healthy": { AND: 0.10, INF: 0.05, chemo_amp: 0.0 },
|
||||
"Androgenetic Alopecia": { AND: 0.62, INF: 0.08, chemo_amp: 0.0 },
|
||||
"Alopecia Areata": { AND: 0.12, INF: 0.82, chemo_amp: 0.0 },
|
||||
"Chemotherapy-induced Alopecia": { AND: 0.10, INF: 0.06, chemo_amp: 1.7 },
|
||||
};
|
||||
const CHEMO_TIMES = [10, 31, 52, 73];
|
||||
|
||||
// 개입 → drive 수정자
|
||||
const INTERVENTIONS = {
|
||||
none: d => d,
|
||||
finasteride: d => (d.AND *= 0.40, d),
|
||||
dutasteride: d => (d.AND *= 0.25, d),
|
||||
AR_antagonist: d => (d.AND *= 0.45, d),
|
||||
minoxidil: d => (d.uDP += 0.80, d.uWnt += 0.15, d),
|
||||
anti_DKK1: d => (d.uWnt += 0.45, d),
|
||||
wnt_agonist: d => (d.uWnt += 0.50, d),
|
||||
JAK_inhibitor: d => (d.INF *= 0.18, d),
|
||||
corticosteroid: d => (d.INF *= 0.55, d),
|
||||
CDK46_inhibitor: d => (d.chemo_protect = 0.70, d),
|
||||
scalp_cooling: d => (d.chemo_protect = Math.max(d.chemo_protect, 0.45), d),
|
||||
PTH_CBD: d => (d.uDP += 0.35, d),
|
||||
exosome_MSC: d => (d.uDP += 0.35, d.uWnt += 0.25, d),
|
||||
};
|
||||
|
||||
function buildDrive(disease, interventions, overrides) {
|
||||
const pre = DISEASE_PRESETS[disease] || DISEASE_PRESETS["Healthy"];
|
||||
let d = { AND: pre.AND, INF: pre.INF, chemo_amp: pre.chemo_amp || 0,
|
||||
uWnt: 0, uDP: 0, uNog: 0, chemo_protect: 0 };
|
||||
(interventions || []).forEach(iv => { if (INTERVENTIONS[iv]) d = INTERVENTIONS[iv](d); });
|
||||
if (overrides) Object.assign(d, overrides); // 라이브 슬라이더
|
||||
d.AND = Math.max(0, d.AND); d.INF = Math.max(0, d.INF);
|
||||
return d;
|
||||
}
|
||||
|
||||
function chemo(d, t) {
|
||||
if (!d.chemo_amp) return 0;
|
||||
let s = 0;
|
||||
for (const tc of CHEMO_TIMES) s += Math.exp(-((t - tc) ** 2) / (2 * 1.5 * 1.5));
|
||||
return d.chemo_amp * s * (1 - (d.chemo_protect || 0));
|
||||
}
|
||||
|
||||
function rhs(t, y, d) {
|
||||
let [Wnt, BMP, SHH, DP, HFSC, APO, Hair] = y.map(v => Math.max(0, v));
|
||||
const AND = d.AND, INF = d.INF, ch = chemo(d, t);
|
||||
const DKK = P.kDKK * AND;
|
||||
const dWnt = P.kWp * (P.Wbasal + d.uWnt) / (1 + DKK / P.Kd) / (1 + BMP / P.Kb) - P.kWd * Wnt;
|
||||
const dBMP = P.kBp * (P.Bbasal + P.kBMPand * AND) / (1 + d.uNog) / (1 + Wnt / P.Kw) - P.kBd * BMP;
|
||||
const dSHH = P.kSp * (Wnt / (P.Ksw + Wnt)) / (1 + BMP / P.Kb2) - P.kSd * SHH;
|
||||
const IGF = 1 / (1 + P.kIGFand * AND);
|
||||
const dDP = P.kDp * (P.Dbasal + d.uDP) * IGF - P.kDd * DP - DP * (P.kDinf * INF + P.kDapo * APO);
|
||||
const dHFSC = P.kHp * (Wnt / (P.Kh + Wnt)) * (SHH / (P.Ks + SHH)) / (1 + BMP / P.Kb3)
|
||||
- P.kHd * HFSC - HFSC * (P.kHinf * INF + P.kHapo * APO);
|
||||
const dAPO = (P.kApAND * AND + P.kApINF * INF + ch) / (1 + P.kSurv * DP) - P.kAd * APO;
|
||||
// 분수 지수(hpow=0.6)의 음수 밑 → Math.pow(neg,0.6)=NaN 방지: 밑을 명시적으로 0 하한.
|
||||
// (위 y.map(Math.max(0,·)) 클램프와 중복이나, 적분기 단계 값이 음수를 흘려도 NaN 전파 차단)
|
||||
const HFSCp = Math.pow(Math.max(0, HFSC), P.hpow);
|
||||
const DPp = Math.pow(Math.max(0, DP), P.hpow);
|
||||
const dHair = P.kHairP * HFSCp * DPp * (SHH / (P.Ksh + SHH))
|
||||
/ (1 + P.kMiniAND * AND) * Math.max(0, 1 - Hair / P.Hcap)
|
||||
- P.kHairD * Hair - P.kHairApo * APO * Hair;
|
||||
return [dWnt, dBMP, dSHH, dDP, dHFSC, dAPO, dHair];
|
||||
}
|
||||
|
||||
function rk4(y0, d, days, dt) {
|
||||
dt = dt || 0.5;
|
||||
const steps = Math.round(days / dt);
|
||||
const out = { t: [], y: y0.map(() => []) };
|
||||
let y = y0.slice();
|
||||
for (let i = 0; i <= steps; i++) {
|
||||
const t = i * dt;
|
||||
if (Math.abs(t - Math.round(t)) < 1e-9) { // 일 단위 샘플만 저장
|
||||
out.t.push(Math.round(t));
|
||||
y.forEach((v, j) => out.y[j].push(v));
|
||||
}
|
||||
const k1 = rhs(t, y, d);
|
||||
const k2 = rhs(t + dt / 2, y.map((v, j) => v + dt / 2 * k1[j]), d);
|
||||
const k3 = rhs(t + dt / 2, y.map((v, j) => v + dt / 2 * k2[j]), d);
|
||||
const k4 = rhs(t + dt, y.map((v, j) => v + dt * k3[j]), d);
|
||||
y = y.map((v, j) => Math.max(0, v + dt / 6 * (k1[j] + 2 * k2[j] + 2 * k3[j] + k4[j])));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
let _healthy = null;
|
||||
function healthyState() {
|
||||
if (!_healthy) {
|
||||
const d = buildDrive("Healthy", []);
|
||||
const sol = rk4([1.0, 0.45, 0.7, 1.0, 0.8, 0.1, 1.0], d, 400);
|
||||
_healthy = sol.y.map(arr => arr[arr.length - 1]);
|
||||
}
|
||||
return _healthy;
|
||||
}
|
||||
|
||||
function proteinReadouts(states, d) {
|
||||
const n = states.Wnt.length, ones = new Array(n);
|
||||
const AND = d.AND, INF = d.INF;
|
||||
return {
|
||||
"AR/DHT (AR)": ones.fill(AND).slice(),
|
||||
"DKK1": new Array(n).fill(P.kDKK * AND),
|
||||
"β-catenin (CTNNB1)": states.Wnt.slice(),
|
||||
"BMP4": states.BMP.slice(),
|
||||
"SHH": states.SHH.slice(),
|
||||
"IGF1": new Array(n).fill(1 / (1 + P.kIGFand * AND)),
|
||||
"VEGFA (DP)": states.DP.slice(),
|
||||
"JAK-STAT (STAT1)": new Array(n).fill(INF),
|
||||
"p53/apoptosis (TP53)": states.APO.slice(),
|
||||
};
|
||||
}
|
||||
|
||||
// 질환의 '무처치 평형상태' = 환자가 이미 탈모를 가진 출발점(치료 전).
|
||||
// 건강 상태에서 질환 구동을 충분히 오래 적분해 정착시킨 종단 상태.
|
||||
const _diseq = {};
|
||||
function diseaseEquilibrium(disease) {
|
||||
if (!_diseq[disease]) {
|
||||
const d = buildDrive(disease, []);
|
||||
const sol = rk4(healthyState(), d, 600);
|
||||
_diseq[disease] = sol.y.map(arr => arr[arr.length - 1]);
|
||||
}
|
||||
return _diseq[disease].slice();
|
||||
}
|
||||
|
||||
// 메인 API: 시뮬레이션 실행. opts.y0 = 커스텀 초기상태(미지정 시 건강 평형).
|
||||
function run(disease, interventions, opts) {
|
||||
opts = opts || {};
|
||||
const d = buildDrive(disease, interventions, opts.overrides);
|
||||
const y0 = opts.y0 || healthyState();
|
||||
const sol = rk4(y0, d, opts.days || 240);
|
||||
const states = {};
|
||||
STATE_NAMES.forEach((nm, i) => states[nm] = sol.y[i]);
|
||||
const hss = healthyState()[6];
|
||||
const rel = states.Hair.map(h => Math.min(100, 100 * h / hss));
|
||||
states.HairDensity = rel;
|
||||
const proteins = proteinReadouts(states, d);
|
||||
const metrics = {
|
||||
final_hair_density_pct: +rel[rel.length - 1].toFixed(1),
|
||||
min_hair_density_pct: +Math.min(...rel).toFixed(1),
|
||||
anagen_fraction: +(rel.filter(v => v > 70).length / rel.length).toFixed(3),
|
||||
AND_load: +d.AND.toFixed(3), INF_load: +d.INF.toFixed(3),
|
||||
};
|
||||
let tracked = [];
|
||||
(interventions || []).forEach(iv => {
|
||||
// genes 는 scenarios JSON 에서 보강; 엔진은 비움
|
||||
});
|
||||
return { disease, interventions, drive: d, t: sol.t, states, proteins, metrics };
|
||||
}
|
||||
|
||||
global.TwinEngine = { run, diseaseEquilibrium, STATE_NAMES, DISEASE_PRESETS, INTERVENTIONS, buildDrive };
|
||||
if (typeof module !== 'undefined' && module.exports) module.exports = global.TwinEngine;
|
||||
})(typeof window !== 'undefined' ? window : globalThis);
|
||||
200
js/twin.js
Normal file
200
js/twin.js
Normal file
@ -0,0 +1,200 @@
|
||||
/* ============================================================
|
||||
twin.js — Protein Twin 탭 (라이브 ODE + Chart.js)
|
||||
============================================================ */
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const DISEASE_INTERVENTIONS = {
|
||||
"Healthy": [],
|
||||
"Androgenetic Alopecia": ["finasteride", "dutasteride", "AR_antagonist", "minoxidil", "anti_DKK1", "wnt_agonist", "exosome_MSC"],
|
||||
"Alopecia Areata": ["JAK_inhibitor", "corticosteroid", "exosome_MSC"],
|
||||
"Chemotherapy-induced Alopecia": ["CDK46_inhibitor", "scalp_cooling", "PTH_CBD"],
|
||||
};
|
||||
const KEY_PROTEINS = ["β-catenin (CTNNB1)", "AR/DHT (AR)", "JAK-STAT (STAT1)", "p53/apoptosis (TP53)", "VEGFA (DP)"];
|
||||
const PCOLORS = ["#2f63c8", "#c8401f", "#b07a12", "#7c3aed", "#1f8a5b", "#0e7490", "#c2367f", "#5b6470", "#c0561f"];
|
||||
|
||||
let state = { disease: "Androgenetic Alopecia", interventions: [], live: {} };
|
||||
let charts = { hair: null, prot: null, compare: null };
|
||||
|
||||
function el(id) { return document.getElementById(id); }
|
||||
|
||||
function init() {
|
||||
buildDiseaseSeg();
|
||||
buildInterventionChips();
|
||||
bindSliders();
|
||||
el('protein-trace-mode').addEventListener('change', render);
|
||||
el('btn-reset-live').addEventListener('click', () => {
|
||||
state.live = {}; syncSliders(); render();
|
||||
});
|
||||
render();
|
||||
}
|
||||
|
||||
function buildDiseaseSeg() {
|
||||
const seg = el('disease-seg');
|
||||
const diseases = Store.scenarios.diseases || {};
|
||||
seg.innerHTML = '';
|
||||
Object.keys(DISEASE_INTERVENTIONS).forEach(dis => {
|
||||
const meta = diseases[dis] || {};
|
||||
const b = document.createElement('button');
|
||||
b.className = 'seg-btn' + (dis === state.disease ? ' active' : '');
|
||||
b.innerHTML = `${meta.label || dis}<span class="seg-sub">${meta.desc || ''}</span>`;
|
||||
b.onclick = () => {
|
||||
state.disease = dis; state.interventions = []; state.live = {};
|
||||
document.querySelectorAll('#disease-seg .seg-btn').forEach(x => x.classList.remove('active'));
|
||||
b.classList.add('active');
|
||||
buildInterventionChips(); syncSliders(); render();
|
||||
};
|
||||
seg.appendChild(b);
|
||||
});
|
||||
}
|
||||
|
||||
function buildInterventionChips() {
|
||||
const list = el('intervention-list');
|
||||
const ivs = DISEASE_INTERVENTIONS[state.disease] || [];
|
||||
const meta = Store.scenarios.interventions || {};
|
||||
list.innerHTML = '';
|
||||
if (!ivs.length) { list.innerHTML = '<span class="panel-subtitle">이 상태에는 개입이 없습니다 (기준선).</span>'; return; }
|
||||
ivs.forEach(iv => {
|
||||
const m = meta[iv] || { label: iv };
|
||||
const c = document.createElement('div');
|
||||
c.className = 'chip' + (state.interventions.includes(iv) ? ' active' : '');
|
||||
c.textContent = m.label || iv;
|
||||
c.onclick = () => {
|
||||
const i = state.interventions.indexOf(iv);
|
||||
if (i >= 0) state.interventions.splice(i, 1); else state.interventions.push(iv);
|
||||
c.classList.toggle('active');
|
||||
render();
|
||||
};
|
||||
list.appendChild(c);
|
||||
});
|
||||
}
|
||||
|
||||
function bindSliders() {
|
||||
[['slider-and', 'AND', 'val-and'], ['slider-inf', 'INF', 'val-inf'],
|
||||
['slider-wnt', 'uWnt', 'val-wnt'], ['slider-dp', 'uDP', 'val-dp']].forEach(([sid, key, vid]) => {
|
||||
el(sid).addEventListener('input', e => {
|
||||
state.live[key] = parseFloat(e.target.value);
|
||||
el(vid).textContent = (+e.target.value).toFixed(2);
|
||||
render(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 현재 질환+개입의 baseline drive 로 슬라이더 동기화
|
||||
function syncSliders() {
|
||||
const d = TwinEngine.buildDrive(state.disease, state.interventions);
|
||||
const map = { 'slider-and': ['AND', 'val-and'], 'slider-inf': ['INF', 'val-inf'],
|
||||
'slider-wnt': ['uWnt', 'val-wnt'], 'slider-dp': ['uDP', 'val-dp'] };
|
||||
Object.entries(map).forEach(([sid, [k, vid]]) => {
|
||||
const v = state.live[k] !== undefined ? state.live[k] : d[k];
|
||||
el(sid).value = v; el(vid).textContent = (+v).toFixed(2);
|
||||
});
|
||||
}
|
||||
|
||||
function render(fromSlider) {
|
||||
if (!fromSlider) syncSliders();
|
||||
const overrides = Object.keys(state.live).length ? state.live : null;
|
||||
const r = TwinEngine.run(state.disease, state.interventions, { overrides });
|
||||
|
||||
renderHairChart(r);
|
||||
renderProteinChart(r);
|
||||
renderMetrics(r);
|
||||
renderTracked();
|
||||
renderCompare();
|
||||
el('disease-desc').textContent = (Store.scenarios.diseases[state.disease] || {}).desc || '';
|
||||
}
|
||||
|
||||
function renderHairChart(r) {
|
||||
const ctx = el('chart-hair');
|
||||
const data = {
|
||||
labels: r.t,
|
||||
datasets: [{
|
||||
label: '모발 밀도 (%)', data: r.states.HairDensity,
|
||||
borderColor: '#c8401f', backgroundColor: 'rgba(200,64,31,.10)',
|
||||
fill: true, tension: .25, pointRadius: 0, borderWidth: 2.5,
|
||||
}],
|
||||
};
|
||||
const opts = {
|
||||
responsive: true, maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: { min: 0, max: 105, ticks: { color: '#6b655a', callback: v => v + '%' }, grid: { color: 'rgba(0,0,0,.08)' } },
|
||||
x: { ticks: { color: '#6b655a', maxTicksLimit: 8, callback: (v, i) => r.t[i] + 'd' }, grid: { display: false } },
|
||||
},
|
||||
plugins: { legend: { display: false } },
|
||||
};
|
||||
if (charts.hair) { charts.hair.data = data; charts.hair.update('none'); }
|
||||
else charts.hair = new Chart(ctx, { type: 'line', data, options: opts });
|
||||
}
|
||||
|
||||
function renderProteinChart(r) {
|
||||
const ctx = el('chart-proteins');
|
||||
const mode = el('protein-trace-mode').value;
|
||||
const keys = mode === 'all' ? Object.keys(r.proteins) : KEY_PROTEINS;
|
||||
const datasets = keys.map((k, i) => ({
|
||||
label: k, data: r.proteins[k],
|
||||
borderColor: PCOLORS[i % PCOLORS.length], borderWidth: 2,
|
||||
pointRadius: 0, tension: .25, fill: false,
|
||||
}));
|
||||
const data = { labels: r.t, datasets };
|
||||
const opts = {
|
||||
responsive: true, maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: { ticks: { color: '#6b655a' }, grid: { color: 'rgba(0,0,0,.08)' }, title: { display: true, text: '상대 활성', color: '#6b655a' } },
|
||||
x: { ticks: { color: '#6b655a', maxTicksLimit: 8, callback: (v, i) => r.t[i] + 'd' }, grid: { display: false } },
|
||||
},
|
||||
plugins: { legend: { labels: { color: '#4a463d', boxWidth: 12, font: { size: 11 } } } },
|
||||
};
|
||||
if (charts.prot) { charts.prot.data = data; charts.prot.options = opts; charts.prot.update('none'); }
|
||||
else charts.prot = new Chart(ctx, { type: 'line', data, options: opts });
|
||||
}
|
||||
|
||||
function renderMetrics(r) {
|
||||
const m = r.metrics;
|
||||
const cls = v => v >= 70 ? 'good' : v >= 40 ? 'warn' : 'bad';
|
||||
const cards = [
|
||||
{ v: m.final_hair_density_pct + '%', l: '최종 모발 밀도', c: cls(m.final_hair_density_pct) },
|
||||
{ v: m.min_hair_density_pct + '%', l: '최저 밀도', c: cls(m.min_hair_density_pct) },
|
||||
{ v: (m.anagen_fraction * 100).toFixed(0) + '%', l: 'Anagen 비율', c: cls(m.anagen_fraction * 100) },
|
||||
{ v: m.AND_load.toFixed(2) + '/' + m.INF_load.toFixed(2), l: '안드로겐/염증 부하', c: '' },
|
||||
];
|
||||
el('twin-metrics').innerHTML = cards.map(c =>
|
||||
`<div class="metric-card ${c.c}"><div class="mv">${c.v}</div><div class="ml">${c.l}</div></div>`).join('');
|
||||
}
|
||||
|
||||
function renderTracked() {
|
||||
const meta = Store.scenarios.interventions || {};
|
||||
const genes = new Set();
|
||||
state.interventions.forEach(iv => (meta[iv] && meta[iv].genes || []).forEach(g => genes.add(g)));
|
||||
const box = el('tracked-genes-list');
|
||||
if (!genes.size) { box.innerHTML = '<span class="panel-subtitle">개입을 선택하면 표적 단백질이 표시됩니다.</span>'; return; }
|
||||
box.innerHTML = [...genes].map(g => `<span class="gene-pill" data-gene="${g}">${g}</span>`).join('');
|
||||
box.querySelectorAll('.gene-pill').forEach(p => p.onclick = () => window.openProtein && window.openProtein(p.dataset.gene));
|
||||
}
|
||||
|
||||
function renderCompare() {
|
||||
const ivsList = [[]].concat((DISEASE_INTERVENTIONS[state.disease] || []).map(iv => [iv]));
|
||||
if (state.disease === "Androgenetic Alopecia") ivsList.push(["finasteride", "minoxidil"]);
|
||||
if (state.disease === "Alopecia Areata") ivsList.push(["JAK_inhibitor", "corticosteroid"]);
|
||||
const meta = Store.scenarios.interventions || {};
|
||||
const labels = [], vals = [], colors = [];
|
||||
ivsList.forEach(ivs => {
|
||||
const r = TwinEngine.run(state.disease, ivs, { days: 240 });
|
||||
const v = r.metrics.final_hair_density_pct;
|
||||
labels.push(ivs.length ? ivs.map(i => (meta[i] || {}).label || i).join('+').replace(/\(.*?\)/g, '').slice(0, 16) : '무처치');
|
||||
vals.push(v);
|
||||
colors.push(v >= 70 ? '#1f6d3a' : v >= 40 ? '#b07a12' : '#b3361b');
|
||||
});
|
||||
const ctx = el('chart-compare');
|
||||
const data = { labels, datasets: [{ data: vals, backgroundColor: colors, borderRadius: 5 }] };
|
||||
const opts = {
|
||||
indexAxis: 'y', responsive: true, maintainAspectRatio: false,
|
||||
scales: { x: { min: 0, max: 105, ticks: { color: '#6b655a', callback: v => v + '%' }, grid: { color: 'rgba(0,0,0,.08)' } },
|
||||
y: { ticks: { color: '#4a463d', font: { size: 10 } }, grid: { display: false } } },
|
||||
plugins: { legend: { display: false } },
|
||||
};
|
||||
if (charts.compare) { charts.compare.data = data; charts.compare.update('none'); }
|
||||
else charts.compare = new Chart(ctx, { type: 'bar', data, options: opts });
|
||||
}
|
||||
|
||||
window.TwinTab = { init };
|
||||
})();
|
||||
669
js/validation.js
Normal file
669
js/validation.js
Normal file
@ -0,0 +1,669 @@
|
||||
/* ============================================================
|
||||
validation.js — 검증(Validation) 탭
|
||||
다층 독립 검증 결과(data/validation_results.json)를 시각화.
|
||||
============================================================ */
|
||||
(function () {
|
||||
'use strict';
|
||||
let data = null, synergy = null, uq = null, pers = null, pwr = null, pwrm = null, ode = null, calib = null, ipd = null, exvivo = null, synClin = null, synRev = null, synReT = null, comp = null, scarm = null, qr = null, biph = null, hfoc = null, charts = {}, done = false;
|
||||
const el = id => document.getElementById(id);
|
||||
const VERM = '#c8401f', TEAL = '#1f5d52', INK = '#1b1a16', MUTE = '#6b655a',
|
||||
GOOD = '#1f6d3a', BAD = '#b3361b', WARN = '#9a6a12', GRID = 'rgba(0,0,0,.08)';
|
||||
|
||||
async function init() {
|
||||
if (done) return; done = true;
|
||||
try { data = await fetch('data/validation_results.json').then(r => r.json()); }
|
||||
catch (e) { el('val-summary').innerHTML = '<p class="panel-subtitle">검증 데이터 로드 실패: ' + e + '</p>'; return; }
|
||||
try { synergy = await fetch('data/synergy_prediction.json').then(r => r.json()); } catch (e) {}
|
||||
try { uq = await fetch('data/bayes_uq_results.json').then(r => r.json()); } catch (e) {}
|
||||
try { pers = await fetch('data/personalize_results.json').then(r => r.json()); } catch (e) {}
|
||||
try { pwr = await fetch('data/power_simulation.json').then(r => r.json()); } catch (e) {}
|
||||
try { pwrm = await fetch('data/power_molecular.json').then(r => r.json()); } catch (e) {}
|
||||
try { ode = await fetch('data/ode_personalize.json').then(r => r.json()); } catch (e) {}
|
||||
try { calib = await fetch('data/mapping_calibration.json').then(r => r.json()); } catch (e) {}
|
||||
try { ipd = await fetch('data/ipd_dryrun.json').then(r => r.json()); } catch (e) {}
|
||||
try { exvivo = await fetch('data/exvivo_validation.json').then(r => r.json()); } catch (e) {}
|
||||
try { comp = await fetch('data/comparator_validation.json').then(r => r.json()); } catch (e) {}
|
||||
try { scarm = await fetch('data/synthetic_control_validation.json').then(r => r.json()); } catch (e) {}
|
||||
try { synClin = await fetch('data/synergy_clinical_test.json').then(r => r.json()); } catch (e) {}
|
||||
try { synRev = await fetch('data/synergy_revised.json').then(r => r.json()); } catch (e) {}
|
||||
try { synReT = await fetch('data/synergy_retest.json').then(r => r.json()); } catch (e) {}
|
||||
try { qr = await fetch('data/quant_recalibration.json').then(r => r.json()); } catch (e) {}
|
||||
try { biph = await fetch('data/biphasic_model.json').then(r => r.json()); } catch (e) {}
|
||||
try { hfoc = await fetch('data/hfoc_calibration_dryrun.json').then(r => r.json()); } catch (e) {}
|
||||
renderSummary(); renderLandscape(); renderMolecular(); renderAgaDp(); renderJak();
|
||||
renderAaSc(); renderGwas(); renderCandidates(); renderTiming(); renderBenchmark();
|
||||
renderExvivo(); renderSynergy(); renderSynergyRev(); renderUQ(); renderPersonalize(); renderPower(); renderPowerMol(); renderOde(); renderCalib(); renderIpd(); renderComparator(); renderNAM();
|
||||
}
|
||||
|
||||
// 대조군 자격 — 보정곡선 + context 점수표
|
||||
function renderComparator() {
|
||||
const C = comp; if (!C || !C.calibration) return;
|
||||
const cv = C.calibration, cu = C.context_of_use || {};
|
||||
const cov90 = (cv.curve.find(x => x.nominal === 0.9) || {}).empirical;
|
||||
const t1 = (cu.tier1_calibrated_comparator || []).length, t2 = (cu.tier2_directional_comparator || []).length, t3 = (cu.tier3_not_yet || []).length;
|
||||
el('val-comp-headline').innerHTML = [
|
||||
[cv.calibration_error, '보정오차(↓좋음)'], [Math.round((cov90 || 0) * 100) + '%', '90% 커버리지'],
|
||||
[t1 + '개', '보정된 비교군'], [t2 + '개', '방향 비교군'], [t3 + '개', '미달(전향/IPD)'],
|
||||
].map(([v, l]) => `<div class="vstat"><div class="vsv">${v}</div><div class="vsl">${l}</div></div>`).join('');
|
||||
mk('chart-val-comp-cal', 'line', {
|
||||
labels: cv.curve.map(x => Math.round(x.nominal * 100) + '%'),
|
||||
datasets: [
|
||||
{ label: '경험적 커버리지', data: cv.curve.map(x => Math.round(x.empirical * 100)), borderColor: VERM, backgroundColor: 'rgba(200,64,31,.10)', borderWidth: 2.5, pointRadius: 4, tension: .2, fill: false },
|
||||
{ label: '이상(=명목)', data: cv.curve.map(x => Math.round(x.nominal * 100)), borderColor: INK, borderDash: [5, 4], borderWidth: 1.3, pointRadius: 0 },
|
||||
]
|
||||
}, {
|
||||
plugins: { legend: { labels: { color: INK, boxWidth: 14, font: { size: 10 } } },
|
||||
title: { display: true, text: '보정곡선: 빨강이 점선(이상)에 붙음 = 구간 정직(보정됨)', color: MUTE, font: { size: 11 } } },
|
||||
scales: { y: { min: 0, max: 100, title: { display: true, text: '경험적 %', color: MUTE }, ticks: { color: MUTE, callback: v => v + '%' }, grid: { color: GRID } },
|
||||
x: { title: { display: true, text: '명목 신뢰수준', color: MUTE }, ticks: { color: MUTE }, grid: { display: false } } }
|
||||
});
|
||||
const TC = { 1: GOOD, 2: '#2f63c8', 3: BAD };
|
||||
const rows = (C.scorecard || []).map(r =>
|
||||
`<tr><td><span class="vb" style="background:${TC[r.tier]}">T${r.tier}</span></td><td><b>${r.context}</b></td><td>${r.verdict}</td><td class="vc-met">${r.metric}</td></tr>`).join('');
|
||||
el('val-comp-table').innerHTML = `<table class="val-table"><thead><tr><th>등급</th><th>context</th><th>판정</th><th>근거</th></tr></thead><tbody>${rows}</tbody></table>`;
|
||||
// synthetic control arm — 실제 RCT 위약 대비
|
||||
if (scarm && scarm.diseases) {
|
||||
const dim = 'color:#8a8f98;font-size:11px';
|
||||
const srows = Object.keys(scarm.diseases).map(dz => {
|
||||
const s = scarm.diseases[dz];
|
||||
const eq = s.equivalent ? `<span class="vb" style="background:${GOOD}">동등 ✓</span>` : `<span class="vb" style="background:${BAD}">미입증</span>`;
|
||||
const pstr = s.tost_p_equiv < 0.001 ? 'p<0.001' : 'p=' + s.tost_p_equiv;
|
||||
const consv = (s.tost_p_equiv_conservative_SE != null && s.tost_p_equiv_conservative_SE >= 0.05)
|
||||
? ` <span style="${dim}">(보수SE p=${s.tost_p_equiv_conservative_SE})</span>` : '';
|
||||
return `<tr>`
|
||||
+ `<td><b>${dz}</b><br><span style="${dim}">트윈평형 ${s.twin_equilibrium_density_pct}%</span></td>`
|
||||
+ `<td>${s.real_placebo_mean}±${s.real_placebo_se} ${s.unit}<br><span style="${dim}">${s.n_trials}arm · n=${s.n_subjects}</span></td>`
|
||||
+ `<td>${s.twin_control} <span style="${dim}">(실행)</span></td>`
|
||||
+ `<td>${eq} ${pstr}${consv}</td>`
|
||||
+ `<td>raw ${s.effect_twin_raw} <b>(${s.bias_raw_pct}%)</b><br><span style="${dim}">+오버레이 ${s.effect_twin_corrected} (${s.bias_corrected_pct}%)</span></td>`
|
||||
+ `</tr>`;
|
||||
}).join('');
|
||||
const v = scarm.verdict || {};
|
||||
const aaMae = scarm.diseases.AA && scarm.diseases.AA.nat_overlay_loto_mae;
|
||||
el('val-comp-scarm').innerHTML =
|
||||
`<div class="ipd-warn" style="border-left-color:${GOOD};background:rgba(31,109,58,.07);border-color:rgba(31,109,58,.3)">✅ <b>무작위 대조군(RCT) 검증 — mechanistic synthetic control arm</b><br>트윈을 <b>실제 실행</b>해(질환 평형 등록자 모사) 무치료 대조군 readout 도출 = <b>0 변화</b>(자연사 미모델, 위약데이터 미접촉=held-out·공정). 이 기전 대조군이 <b>실제 RCT 위약 arm과 동등(TOST)</b>: ${v.equivalence}. 치료효과 재구성 편향 raw <b>${v.max_effect_bias_raw_pct}%</b> → 경험 오버레이 보정 후 <b>${v.max_effect_bias_corrected_pct}%</b>(in-sample).</div>`
|
||||
+ `<table class="val-table" style="margin-top:6px"><thead><tr><th>질환</th><th>실제 위약(arm·n)</th><th>트윈 대조(실행)</th><th>동등성(TOST·마진±15)</th><th>효과재구성: raw / +오버레이</th></tr></thead><tbody>${srows}</tbody></table>`
|
||||
+ `<div style="${dim};margin-top:6px;line-height:1.6">⚠ <b>정직한 경계:</b> ① <b>회고적</b>(기존 RCT 위약). ② 대조군 <b>평균</b> 재현이지 개인변동(위약 SD) 아님. ③ 동등성은 게시 dispersion=<b>SD 가정</b>; 보수적 <b>SE 가정 시 AGA p≈0.10·AA p≈0.26로 미달</b>(헤드라인 효과-편향비는 가정無, <12%). ④ 자연사 갭(AGA 보수/AA 비보수)을 메우는 <b>경험 오버레이는 트윈 기전 아님·in-sample</b>; 교차시험 일반화는 LOTO 예비${aaMae != null ? `(AA 2arm MAE ${aaMae}%)` : ''}. ⑤ 규제 qualification은 전향+공변량 매칭 필요.</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// ⑨ 동물실험 대체 경로 — NAM 자격 프로그램 Phase 0
|
||||
function renderNAM() {
|
||||
const host = el('val-nam'); if (!host) return;
|
||||
const dim = 'color:#8a8f98;font-size:11px';
|
||||
const badge = (txt, bg) => `<span class="vb" style="background:${bg}">${txt}</span>`;
|
||||
const parts = [];
|
||||
|
||||
// 헤드라인: 대체 가능성 정직 프레이밍
|
||||
parts.push(`<div class="ipd-warn" style="border-left-color:${WARN};background:rgba(154,106,18,.07);border-color:rgba(154,106,18,.3)">`
|
||||
+ `🐭→🧪 <b>쥐 실험 대체?</b> <b>전체 대체는 불가</b>(신규기전 발견·전신 PK/독성·전임상 안전성은 어떤 모델로도 영구 제외). `
|
||||
+ `<b>특정 효능 어세이 1개</b>(기전기지 JAK 화합물 AA발모)를 <b>인체 HFOC + 정량 트윈</b> NAM으로 대체하는 경로만 실재. 아래=<b>Phase 0(건식)</b> 결과.</div>`);
|
||||
|
||||
// Phase 0-A: 정량 트윈 동역학 전이성 (make-or-break)
|
||||
if (qr && qr.overall) {
|
||||
const o = qr.overall, pass = o.meets_threshold_M2;
|
||||
parts.push(`<div style="margin-top:8px"><b>① 정량 트윈 동역학 전이성</b> (대체급 도달 가능성, LOTO)</div>`
|
||||
+ `<div class="val-headline" style="margin-top:4px">`
|
||||
+ `<div class="vstat"><div class="vsv" style="color:${pass ? GOOD : BAD}">${o.M2_r2_monotone_only != null ? o.M2_r2_monotone_only : o.M2_r2}</div><div class="vsl">M2 형태-전이 R²(단조)</div></div>`
|
||||
+ `<div class="vstat"><div class="vsv">${o.M2_r2}</div><div class="vsl">M2 전체 R²</div></div>`
|
||||
+ `<div class="vstat"><div class="vsv" style="color:${WARN}">${o.M1_r2}</div><div class="vsl">M1 군-외삽(정보0·약함)</div></div>`
|
||||
+ `<div class="vstat"><div class="vsv">${pass ? badge('≥0.8 통과', GOOD) : badge('미달', BAD)}</div><div class="vsl">게이트</div></div>`
|
||||
+ `</div>`
|
||||
+ `<div style="${dim};margin-top:2px">→ 동역학(lag/τ)은 새 화합물에 전이되어 <b>대체급</b>, 단 진폭은 HFOC가 공급(맨손 외삽 M1은 약함) = <b>분업 검증</b>.</div>`);
|
||||
}
|
||||
|
||||
// Phase 0-B: biphasic 결함 폐쇄
|
||||
if (biph && biph.summary) {
|
||||
const s = biph.summary, fin = (biph.trajectories || {})['finasteride_1mg_5yr_DECLINE'] || {};
|
||||
parts.push(`<div style="margin-top:10px"><b>② biphasic 결함 폐쇄</b> — 단조 1-exp가 못 내던 '상승-후-감소'(후기 자연사 진행)</div>`
|
||||
+ `<div style="${dim};margin-top:2px;line-height:1.6">내포모델(biphasic⊇단조). 피나 5yr 감소: 단조 R²<b>${fin.mono ? fin.mono.r2 : '–'}</b>(종점 ${fin.mono ? fin.mono.last_pt_err : '–'} hairs 못 따라감) → biphasic은 wane항으로 표현(종점오차 ${s.biphasic_targets_mean_lastpt_err_mono}→${s.biphasic_targets_mean_lastpt_err_biphasic}). 단조 대조 불변(ΔR²~${s.mono_ctrl_mean_abs_r2_diff}). <i>정직: 표적 3점→표현 시연이지 통계검증 아님.</i></div>`);
|
||||
}
|
||||
|
||||
// Phase 0-C: HFOC 보정 하니스 dry-run (합성)
|
||||
if (hfoc) {
|
||||
const rep = hfoc.representative_rho090 || {}, sw = hfoc.rho_sweep || [];
|
||||
const swStr = sw.filter(x => [0.8, 0.9, 0.95, 1.0].includes(x.rho)).map(x => `ρ${x.rho}:${x.G2_r2}`).join(' · ');
|
||||
parts.push(`<div style="margin-top:10px"><b>③ HFOC 보정 하니스</b> — 사전등록 게이트 (⚠ <b>합성 dry-run</b>, 실 HFOC 아님)</div>`
|
||||
+ `<div style="${dim};margin-top:2px;line-height:1.6">G1 동역학 적합 R²<b>${rep.G1_kinetic_r2}</b> ${rep.G1_kinetic_r2 >= 0.8 ? badge('통과', GOOD) : ''} · G2 생체외삽(ρ0.9) ${rep.G2_bridge_r2}. `
|
||||
+ `<b>2병목 발견</b>: G2 R² 천장≈ρ²(HFOC↔생체 번역충실도) + 추정잡음(모낭수). ρ스윕 [${swStr}] → <b>G2≥0.8엔 ρ≳0.92 + 충분 모낭/패널</b>. `
|
||||
+ `<b>ρ는 wet 종간 브리지로만 측정</b> → 하니스는 '준비됨'이지 '대체 입증' 아님.</div>`);
|
||||
}
|
||||
|
||||
// 4단계 게이트 사다리
|
||||
const ladder = [
|
||||
['0 건식 타당성', '동역학 전이성·biphasic·하니스', 'M2 R²≥0.8', badge('통과', GOOD)],
|
||||
['1 습식 파일럿', 'HFOC + JAK 3~5종 → 진폭·bioCV', 'HFOC 적합 R²≥0.8', badge('미수행(wet)', WARN)],
|
||||
['2 전향·맹검', '사전등록 held-out 예측', '예측 R²≥0.8', badge('⏳', WARN)],
|
||||
['3 종간 브리지', '마우스 vs NAM vs 인체', 'NAM≥마우스(인체예측)', badge('⏳ 핵심', WARN)],
|
||||
['4 공인', 'ring trial + ISTAND/OECD', '재현+독립검증', badge('⏳', WARN)],
|
||||
].map(r => `<tr><td><b>${r[0]}</b></td><td>${r[1]}</td><td style="${dim}">${r[2]}</td><td>${r[3]}</td></tr>`).join('');
|
||||
parts.push(`<table class="val-table" style="margin-top:10px"><thead><tr><th>Phase</th><th>내용</th><th>사전 게이트</th><th>상태</th></tr></thead><tbody>${ladder}</tbody></table>`);
|
||||
|
||||
// 정직한 경계
|
||||
parts.push(`<div style="${dim};margin-top:6px;line-height:1.6">⚠ <b>정직:</b> Phase 0(건식)만 완료 — 기술 급소(동역학 전이 R²${qr && qr.overall ? qr.overall.M2_r2_monotone_only : '–'})는 통과했으나 <b>Phase 1~4는 다년·고비용 wet-lab+규제</b>(미수행). 성공해도 <b>AA/JAK 효능 스크린 1개</b> 대체일 뿐, 전체 대체 아님.</div>`);
|
||||
|
||||
host.innerHTML = parts.join('');
|
||||
}
|
||||
|
||||
// 반증 반영 모델 수정 — 시너지 vs 축겹침(overlap)
|
||||
function renderSynergyRev() {
|
||||
const R = synRev; if (!R || !R.overlap_sweep) return;
|
||||
const sw = R.overlap_sweep;
|
||||
el('val-synrev-note').innerHTML = `<b>모델 수정(반증 반영)</b>: 미녹시딜의 Wnt 활성 → 피나와 W축 <b>겹침</b> 도입. 시너지는 overlap≈${R.synergy_crosses_zero_near}에서 가법미만 전환 → <b>피나×미녹(겹침 0.55)은 가법</b>(IJT 정합). <b>정정</b>: AR차단×Wnt-agonist도 둘 다 W축이라 <b>중복=무효</b>(이전 제안 오류). <b>정련된 새 예측: 초가법은 *진짜 직교 축 쌍*(한쪽 Wnt무관 D약물)에서만 생존</b> — 직교 약물쌍 전향 검정 필요.`;
|
||||
mk('chart-val-synrev', 'line', {
|
||||
labels: sw.map(x => x.overlap),
|
||||
datasets: [
|
||||
{ label: '시너지 초과', data: sw.map(x => x.synergy_excess), borderColor: VERM, backgroundColor: 'rgba(200,64,31,.10)', borderWidth: 2.5, pointRadius: 3, tension: .3, fill: true },
|
||||
{ type: 'line', label: '가법(0)', data: sw.map(() => 0), borderColor: INK, borderDash: [5, 4], borderWidth: 1.2, pointRadius: 0 },
|
||||
]
|
||||
}, {
|
||||
plugins: { legend: { labels: { color: INK, boxWidth: 12, font: { size: 10 } } },
|
||||
title: { display: true, text: '축 겹침↑ → 시너지 소멸 (직교=초가법, 겹침=가법미만)', color: MUTE, font: { size: 11 } } },
|
||||
scales: { y: { title: { display: true, text: '시너지 초과(Bliss)', color: MUTE }, ticks: { color: MUTE }, grid: { color: GRID } },
|
||||
x: { title: { display: true, text: 'ARM-2의 W축 겹침(overlap)', color: MUTE }, ticks: { color: MUTE }, grid: { display: false } } }
|
||||
});
|
||||
// 다중-시험 재검정 — 정직한 verdict (겹침端 가법미만 / 직교축 포함 초가법 시사)
|
||||
if (synReT && synReT.analysis) {
|
||||
const ij = synReT.analysis.IJT2023 || {}, fp = synReT.analysis.FPHL2022 || {}, th = synReT.analysis.TH07 || {};
|
||||
el('val-retest-note').innerHTML = `<b>재검정(다중 시험)</b>: ① <b>IJT 피나×미녹</b>(겹침高, full 2×2) 초가법초과 ${ij.super_additive_excess} = <b>가법미만</b>(모델 겹침端 정합) · ② <b>FPHL 미녹+스피로</b>(W축) 한계이득 +${fp.spt_add_benefit} < 미녹단독 +${fp.mino_mono}(겹침 정합) · ③ <b>TH07 삼중</b>(직교쌍 피나W×라타노프로스트 비-Wnt D 포함): 삼중 ${th.triple} ≫ 단독합 ${th.mono_sum} → 선형시너지 <b>+${th.linear_synergy} = 초가법</b>. <br><b>→ 정련 예측에 시사적 지지</b>: 겹침 쌍=가법미만, 직교축 포함=초가법(대조 정합). <b>단 확정 아님</b>(TH07은 쌍 아닌 삼중·n3-4·반정량·미녹 침투촉진 교란·산업체) — 깨끗한 W축×Wnt무관 D약물 *쌍* 전향 2×2 필요(예: AR차단×PGF2α/아데노신).`;
|
||||
}
|
||||
}
|
||||
|
||||
// 실제 ex vivo 인체 모낭 검증 (GSE267664 DHT)
|
||||
function renderExvivo() {
|
||||
const E = exvivo; if (!E || !E.concordance) return;
|
||||
const c = E.concordance, k = E.key_DKK1 || {};
|
||||
el('val-exvivo-headline').innerHTML = [
|
||||
[c.n_match + '/' + c.n_test, 'Wnt축 방향 일치'],
|
||||
['p=' + c.sign_test_p, '부호검정'],
|
||||
[(k.log2fc >= 0 ? '+' : '') + k.log2fc, 'DKK1 log2FC(Wnt길항↑)'],
|
||||
['n=3', 'ex vivo 모낭'],
|
||||
].map(([v, l]) => `<div class="vstat"><div class="vsv">${v}</div><div class="vsl">${l}</div></div>`).join('');
|
||||
const m = (E.markers || []).filter(x => x.in_test);
|
||||
mk('chart-val-exvivo', 'bar', {
|
||||
labels: m.map(x => x.gene),
|
||||
datasets: [{ label: 'log2FC (DHT vs control)', data: m.map(x => x.log2fc),
|
||||
backgroundColor: m.map(x => x.match ? GOOD : BAD), borderRadius: 3 }]
|
||||
}, {
|
||||
plugins: { legend: { display: false },
|
||||
title: { display: true, text: '실제 모낭 DHT 반응이 트윈 Wnt억제 예측과 협응(초록=일치)', color: MUTE, font: { size: 11 } } },
|
||||
scales: { y: { title: { display: true, text: 'log2FC (DHT vs ctrl)', color: MUTE }, ticks: { color: MUTE }, grid: { color: GRID } },
|
||||
x: { ticks: { color: INK, font: { size: 10 } }, grid: { display: false } } }
|
||||
});
|
||||
}
|
||||
|
||||
// 트랙 B — IPD 사전등록 분석 하니스 (합성 dry-run)
|
||||
function renderIpd() {
|
||||
const I = ipd; if (!I || !I.result) return;
|
||||
const r = I.result;
|
||||
el('val-ipd-headline').innerHTML = [
|
||||
[r.rmse_pop_mean, 'RMSE 모집단'], [r.rmse_personal_mean, 'RMSE 개인화'],
|
||||
[(r.improve_pct >= 0 ? '+' : '') + r.improve_pct + '%', '개선(불확정)'],
|
||||
[Math.round(r.coverage_personal * 100) + '%', '구간 커버리지'],
|
||||
[r.personal_wins + '/' + r.n_patients, '개인화 우세'],
|
||||
].map(([v, l]) => `<div class="vstat"><div class="vsv" style="font-size:19px">${v}</div><div class="vsl">${l}</div></div>`).join('');
|
||||
el('val-ipd-verdict').innerHTML = `사전등록 판정: <b>${r.decision}</b> (95% CI ${JSON.stringify(r.diff_ci95)} — 0 포함)`;
|
||||
mk('chart-val-ipd', 'bar', {
|
||||
labels: ['모집단', '개인화'],
|
||||
datasets: [{ label: '후기 forecast RMSE', data: [r.rmse_pop_mean, r.rmse_personal_mean],
|
||||
backgroundColor: [MUTE, TEAL], borderRadius: 3 }]
|
||||
}, {
|
||||
plugins: { legend: { display: false },
|
||||
title: { display: true, text: '개인 수준: 거의 동일(시험암 +40%와 대조) — 합성', color: MUTE, font: { size: 11 } } },
|
||||
scales: { y: { title: { display: true, text: 'RMSE (SALT)', color: MUTE }, ticks: { color: MUTE }, grid: { color: GRID } },
|
||||
x: { ticks: { color: INK, font: { size: 12 } }, grid: { display: false } } }
|
||||
});
|
||||
}
|
||||
|
||||
// 트랙 C — 매핑 보정 (GWAS 마커중요도 + 섭동)
|
||||
function renderCalib() {
|
||||
const C = calib; if (!C) return;
|
||||
// GWAS 마커 중요도 (mlogp), AGA·AA 색 구분, GW-sig 7.3 기준선
|
||||
const items = [];
|
||||
['AGA', 'AA'].forEach(dz => {
|
||||
const g = (C.gwas || {})[dz] || {};
|
||||
Object.keys(g).forEach(k => { if (g[k] > 0) items.push({ gene: k, dz, mlogp: g[k] }); });
|
||||
});
|
||||
items.sort((a, b) => b.mlogp - a.mlogp);
|
||||
const top = items.slice(0, 12);
|
||||
if (top.length) {
|
||||
mk('chart-val-calib-gwas', 'bar', {
|
||||
labels: top.map(x => x.gene + '·' + x.dz),
|
||||
datasets: [
|
||||
{ type: 'line', label: 'GW-sig 7.3', data: top.map(() => 7.3), borderColor: INK, borderDash: [5, 4], borderWidth: 1.2, pointRadius: 0 },
|
||||
{ label: '−log10 p', data: top.map(x => x.mlogp), backgroundColor: top.map(x => x.dz === 'AGA' ? VERM : TEAL), borderRadius: 3 },
|
||||
]
|
||||
}, {
|
||||
indexAxis: 'y',
|
||||
plugins: { legend: { labels: { color: INK, boxWidth: 12, font: { size: 10 } } },
|
||||
title: { display: true, text: 'GWAS 마커 중요도 (빨강 AGA·청록 AA; 7.3=유의)', color: MUTE, font: { size: 11 } } },
|
||||
scales: { x: { title: { display: true, text: '−log10 p (mlogp)', color: MUTE }, ticks: { color: MUTE }, grid: { color: GRID } },
|
||||
y: { ticks: { color: INK, font: { size: 10 } }, grid: { display: false } } }
|
||||
});
|
||||
}
|
||||
// 섭동: DHT Wnt 억제(DKK1/LEF1/AXIN2 logΔ) + JAK-i IFN(vehicle vs jaki)
|
||||
const p = C.perturb || {}; const dht = (p.DHT_Wnt || {}).genes || {}; const jak = p.JAKi_IFN || {};
|
||||
const dl = ['DKK1', 'LEF1', 'AXIN2'].filter(g => dht[g]);
|
||||
const labels = dl.map(g => 'DHT→' + g).concat(jak.vehicle_IFN != null ? ['JAK전 IFN', 'JAK후 IFN'] : []);
|
||||
const vals = dl.map(g => dht[g].log_delta).concat(jak.vehicle_IFN != null ? [jak.vehicle_IFN, jak.jaki_IFN] : []);
|
||||
const cols = dl.map(g => dht[g].log_delta >= 0 ? BAD : GOOD).concat(jak.vehicle_IFN != null ? [BAD, GOOD] : []);
|
||||
if (labels.length) {
|
||||
mk('chart-val-calib-perturb', 'bar', {
|
||||
labels, datasets: [{ label: '효과(logΔ / IFN시그니처)', data: vals, backgroundColor: cols, borderRadius: 3 }]
|
||||
}, {
|
||||
plugins: { legend: { display: false },
|
||||
title: { display: true, text: '섭동 보정: DHT→Wnt억제 · JAK-i→IFN감소 (실측)', color: MUTE, font: { size: 11 } } },
|
||||
scales: { y: { title: { display: true, text: '효과 크기', color: MUTE }, ticks: { color: MUTE }, grid: { color: GRID } },
|
||||
x: { ticks: { color: INK, font: { size: 9 } }, grid: { display: false } } }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 분자 readout & 설계 — 무엇이 표본을 줄이는가
|
||||
function renderPowerMol() {
|
||||
const M = pwrm; if (!M || !M.strategies) return;
|
||||
const s = M.strategies;
|
||||
const shortName = n => n.replace(/\(.*?\)/g, '').replace(/모낭내 |신장기울기/g, '').trim();
|
||||
const colors = ['#b9b2a6', WARN, TEAL, VERM];
|
||||
mk('chart-val-powermol', 'bar', {
|
||||
labels: s.map((r, i) => shortName(r.strategy)),
|
||||
datasets: [{ label: '총 모낭(80% 검정력)', data: s.map(r => r.total_follicles),
|
||||
backgroundColor: s.map((_, i) => colors[i % colors.length]), borderRadius: 3 }]
|
||||
}, {
|
||||
indexAxis: 'y',
|
||||
plugins: { legend: { display: false },
|
||||
title: { display: true, text: '표본 절감은 readout이 아니라 유효 CV↓ 설계에서 (1000→300)', color: MUTE, font: { size: 11 } } },
|
||||
scales: { x: { title: { display: true, text: '총 모낭 수', color: MUTE }, ticks: { color: MUTE }, grid: { color: GRID } },
|
||||
y: { ticks: { color: INK, font: { size: 10 } }, grid: { display: false } } }
|
||||
});
|
||||
}
|
||||
|
||||
// 실험 검정력 (몬테카를로)
|
||||
function renderPower() {
|
||||
const P = pwr; if (!P || !P.scenarios) return;
|
||||
const rec = P.recommendation || {};
|
||||
const fmtRec = r => r ? ('공여자 ' + r.donors + '×' + r.follicles_per_group + '모낭') : '비현실';
|
||||
el('val-pwr-headline').innerHTML = Object.keys(P.scenarios).map(name => {
|
||||
const r = P.scenarios[name].reco_80pct;
|
||||
return `<div class="vstat"><div class="vsv" style="font-size:18px">${fmtRec(r)}</div><div class="vsl">${name}</div></div>`;
|
||||
}).join('');
|
||||
const NF = [10, 15, 20, 30, 40, 60, 80, 120];
|
||||
const pal = { '기준(E=0.6)': VERM, '낙관(E=0.6,저변동)': TEAL, '보수(E=0.4)': MUTE };
|
||||
const ds = Object.keys(P.scenarios).map(name => {
|
||||
const g = P.scenarios[name].grid;
|
||||
return { label: name, data: NF.map(nf => Math.round((g['4d_' + nf + 'f'] || 0) * 100)),
|
||||
borderColor: pal[name] || INK, backgroundColor: 'transparent', borderWidth: 2.5, pointRadius: 2, tension: .3 };
|
||||
});
|
||||
ds.push({ label: '80% 목표', data: NF.map(() => 80), borderColor: INK, borderDash: [5, 4], borderWidth: 1.2, pointRadius: 0 });
|
||||
mk('chart-val-power', 'line', { labels: NF.map(n => n + '개'), datasets: ds }, {
|
||||
plugins: { legend: { labels: { color: INK, boxWidth: 14, font: { size: 10 } } },
|
||||
title: { display: true, text: '검정력 vs 군당 모낭수 (공여자 4)', color: MUTE, font: { size: 11 } } },
|
||||
scales: { y: { min: 0, max: 100, title: { display: true, text: '검정력 %', color: MUTE }, ticks: { color: MUTE, callback: v => v + '%' }, grid: { color: GRID } },
|
||||
x: { title: { display: true, text: '군당 모낭 수', color: MUTE }, ticks: { color: MUTE }, grid: { display: false } } }
|
||||
});
|
||||
}
|
||||
|
||||
// ODE-수준 분자 개인화
|
||||
function renderOde() {
|
||||
const O = ode; if (!O || !O.profiles) return;
|
||||
const names = Object.keys(O.profiles);
|
||||
const get = (n, t) => { const r = O.profiles[n].responses; return r && r[t] != null ? r[t] : 0; };
|
||||
const series = [
|
||||
{ key: '피나스테리드', color: '#2f63c8' }, { key: '미녹시딜', color: WARN },
|
||||
{ key: '병용(피나+미녹)', color: VERM }, { key: 'JAK억제제', color: TEAL },
|
||||
];
|
||||
el('val-ode-headline').innerHTML = names.map(n => {
|
||||
const p = O.profiles[n];
|
||||
const shift = (p.aa_strength > p.aga_strength)
|
||||
? 'AA ' + p.aa_strength
|
||||
: ((p.aga_q != null ? p.aga_q : p.aga_strength) + '→' + p.aga_strength);
|
||||
return `<div class="vstat"><div class="vsv" style="font-size:16px">${shift}</div><div class="vsl">${n.split('·')[1] || n} · ${p.recommendation}</div></div>`;
|
||||
}).join('');
|
||||
mk('chart-val-ode', 'bar', {
|
||||
labels: names,
|
||||
datasets: series.map(s => ({ label: s.key, data: names.map(n => get(n, s.key)), backgroundColor: s.color, borderRadius: 3 }))
|
||||
}, {
|
||||
plugins: { legend: { labels: { color: INK, boxWidth: 12, font: { size: 10 } } },
|
||||
title: { display: true, text: 'GWAS-가중 보정 후 — 프로파일별 예측 회복(분자 층화)', color: MUTE, font: { size: 11 } } },
|
||||
scales: { y: { title: { display: true, text: '예측 회복(∫anagen)', color: MUTE }, ticks: { color: MUTE }, grid: { color: GRID } },
|
||||
x: { ticks: { color: INK, font: { size: 10 } }, grid: { display: false } } }
|
||||
});
|
||||
}
|
||||
|
||||
// 개인화 + 데이터 동화
|
||||
function renderPersonalize() {
|
||||
const P = pers; if (!P || !P.forecast_skill) return;
|
||||
const ov = P.forecast_skill.overall || {};
|
||||
el('val-pers-headline').innerHTML = [
|
||||
['+' + (ov.mean_improve_pct || 0) + '%', 'forecast 개선(RMSE↓)'],
|
||||
[(ov.personal_wins || 0) + '/' + (ov.n || 0), '개인화 우세'],
|
||||
[Math.round((ov.cover_personal || 0) * 100) + '%', '개인 커버리지'],
|
||||
[Math.round((ov.cover_pop || 0) * 100) + '%', '모집단 커버리지'],
|
||||
].map(([v, l]) => `<div class="vstat"><div class="vsv">${v}</div><div class="vsl">${l}</div></div>`).join('');
|
||||
|
||||
// 동화 시연: 관측 2점(넓음) vs 전체(좁음) 밴드 + 실측
|
||||
const ex = P.assimilation_example;
|
||||
if (ex && ex.steps && ex.steps.length) {
|
||||
const xy = (xs, ys) => xs.map((x, i) => ({ x, y: ys[i] }));
|
||||
const first = ex.steps[0], last = ex.steps[ex.steps.length - 1];
|
||||
mk('chart-val-assim', 'line', {
|
||||
datasets: [
|
||||
{ label: '_a', data: xy(first.grid, first.hi), borderColor: 'transparent', backgroundColor: 'rgba(154,106,18,.10)', pointRadius: 0, fill: '+1', order: 4 },
|
||||
{ label: first.n_obs + '점 관측 90%', data: xy(first.grid, first.lo), borderColor: 'transparent', backgroundColor: 'rgba(154,106,18,.10)', pointRadius: 0, fill: false, order: 4 },
|
||||
{ label: '_b', data: xy(last.grid, last.hi), borderColor: 'transparent', backgroundColor: 'rgba(31,93,82,.20)', pointRadius: 0, fill: '+1', order: 3 },
|
||||
{ label: last.n_obs + '점 관측 90%', data: xy(last.grid, last.lo), borderColor: 'transparent', backgroundColor: 'rgba(31,93,82,.20)', pointRadius: 0, fill: false, order: 3 },
|
||||
{ label: '개인화 예측', data: xy(last.grid, last.median), borderColor: TEAL, borderWidth: 2.5, pointRadius: 0, order: 2 },
|
||||
{ label: '실측', data: ex.points.map(p => ({ x: p[0], y: p[1] })), type: 'scatter', borderColor: VERM, backgroundColor: VERM, pointRadius: 4, order: 1 },
|
||||
]
|
||||
}, {
|
||||
plugins: { legend: { labels: { color: INK, boxWidth: 12, font: { size: 10 }, filter: it => !it.text.startsWith('_') } },
|
||||
title: { display: true, text: '관측 늘릴수록 예측띠 수축(데이터 동화)', color: MUTE, font: { size: 11 } } },
|
||||
scales: { x: { type: 'linear', title: { display: true, text: '개월', color: MUTE }, ticks: { color: MUTE }, grid: { display: false } },
|
||||
y: { title: { display: true, text: 'SALT %변화', color: MUTE }, ticks: { color: MUTE }, grid: { color: GRID } } }
|
||||
});
|
||||
}
|
||||
|
||||
// 궤적별 forecast 개선%
|
||||
const rows = P.forecast_skill.by_trajectory || [];
|
||||
const shortId = s => s.replace(/_NCT\d+/, '').replace(/_/g, ' ').slice(0, 22);
|
||||
mk('chart-val-fskill', 'bar', {
|
||||
labels: rows.map(r => shortId(r.id)),
|
||||
datasets: [{ label: 'forecast 개선%', data: rows.map(r => r.improve_pct),
|
||||
backgroundColor: rows.map(r => r.improve_pct >= 0 ? GOOD : BAD), borderRadius: 3 }]
|
||||
}, {
|
||||
indexAxis: 'y',
|
||||
plugins: { legend: { display: false },
|
||||
title: { display: true, text: '궤적별 개인화 개선(모집단 대비 RMSE↓)', color: MUTE, font: { size: 11 } } },
|
||||
scales: { x: { ticks: { color: MUTE, callback: v => v + '%' }, grid: { color: GRID } },
|
||||
y: { ticks: { color: INK, font: { size: 9 } }, grid: { display: false } } }
|
||||
});
|
||||
|
||||
// 합성환자 정확성: 관측↑ → 종점 오차·구간폭↓
|
||||
const syn = P.synthetic;
|
||||
if (syn && syn.steps) {
|
||||
const st = syn.steps;
|
||||
mk('chart-val-synth', 'line', {
|
||||
labels: st.map(s => s.n_obs + '점'),
|
||||
datasets: [
|
||||
{ label: '종점 예측오차', data: st.map(s => s.endpoint_err), borderColor: VERM, backgroundColor: 'transparent', borderWidth: 2.5, pointRadius: 3, tension: .3 },
|
||||
{ label: '90% 구간폭', data: st.map(s => s.endpoint_width), borderColor: TEAL, borderDash: [5, 4], backgroundColor: 'transparent', borderWidth: 2, pointRadius: 3, tension: .3 },
|
||||
]
|
||||
}, {
|
||||
plugins: { legend: { labels: { color: INK, boxWidth: 14, font: { size: 10 } } },
|
||||
title: { display: true, text: '합성환자(참 종점 ' + syn.true_endpoint + '): 관측↑ → 오차·폭↓ → 참값 수렴', color: MUTE, font: { size: 11 } } },
|
||||
scales: { y: { title: { display: true, text: 'SALT 단위', color: MUTE }, ticks: { color: MUTE }, grid: { color: GRID } },
|
||||
x: { ticks: { color: INK }, grid: { display: false } } }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 반증가능 예측 — 병용 시너지 (ODE AND-게이트에서 창발)
|
||||
function renderSynergy() {
|
||||
const s = synergy; if (!s) return;
|
||||
const h = s.headline;
|
||||
el('val-syn-headline').innerHTML = [
|
||||
['+' + h.synergy_excess_auc.toFixed(3), 'Bliss 초과(시너지)'],
|
||||
[h.combo_index_CI.toFixed(2), 'Comb.Index (<1=시너지)'],
|
||||
[h.R_combo_actual.toFixed(2), '실제 병용 회복'],
|
||||
[h.R_combo_bliss.toFixed(2), '가법 기대 회복'],
|
||||
].map(([v, l]) => `<div class="vstat"><div class="vsv">${v}</div><div class="vsl">${l}</div></div>`).join('');
|
||||
// 실제 per-arm 데이터 검정(5-ARI×미녹 병용) — 정직한 반증 결과
|
||||
if (synClin && synClin.summary) {
|
||||
const sm = synClin.summary, ds = synClin.data_source || {};
|
||||
el('val-syn-clinical').innerHTML = `⚠ <b>실제 per-arm 데이터로 반증</b> (${ds.pmc || 'IJT 2023'}, 3-arm RCT n=20/군): 트윈은 <b>초가법(synergy)</b>을 예측했으나 실데이터는 <b>강한 가법미만(sub-additive)</b> — 6/6 부위, 평균 초과 ${sm.mean_super_additive_excess} hairs/cm²(피나 추가이득이 피나 단독효과를 크게 밑돎). <b>핵심 예측 반증.</b> 단 병용>단독(HSA ${sm.hsa_cells})은 충족(병용 임상우월성 방향은 맞음). 해석: 미녹+피나 효과 겹침→AND-게이트 '독립 노드' 전제 미충족. 한계: 단일 소규모·국소 피나·placebo 없음.`;
|
||||
// 시각적 반증: 평균 단독/병용 vs 가법기대
|
||||
const cells = Object.values(synClin.cells || {});
|
||||
if (cells.length) {
|
||||
const avg = a => cells.reduce((s, c) => s + c[a], 0) / cells.length;
|
||||
const fns = avg('FNS'), mnx = avg('MNX'), mnf = avg('MNF');
|
||||
mk('chart-val-synclin', 'bar', {
|
||||
labels: ['피나 단독', '미녹 단독', '병용(실제)', '가법 기대(피나+미녹)'],
|
||||
datasets: [{ label: '24주 모발밀도 증가(평균, hairs/cm²)', data: [fns, mnx, mnf, fns + mnx].map(x => +x.toFixed(2)),
|
||||
backgroundColor: ['#2f63c8', WARN, VERM, INK], borderRadius: 3 }]
|
||||
}, {
|
||||
plugins: { legend: { display: false },
|
||||
title: { display: true, text: '병용(빨강) ≪ 가법기대(검정) = 가법미만 → 초가법 예측 반증', color: BAD, font: { size: 11 } } },
|
||||
scales: { y: { title: { display: true, text: 'Δ 모발밀도', color: MUTE }, ticks: { color: MUTE }, grid: { color: GRID } },
|
||||
x: { ticks: { color: INK, font: { size: 10 } }, grid: { display: false } } }
|
||||
});
|
||||
}
|
||||
}
|
||||
const dc = s.dose_curve;
|
||||
mk('chart-val-synergy', 'line', {
|
||||
labels: dc.map(x => x.E),
|
||||
datasets: [
|
||||
{ label: '병용(실제)', data: dc.map(x => x.R_combo), borderColor: VERM, borderWidth: 3, pointRadius: 2, tension: .2, fill: '+1', backgroundColor: 'rgba(200,64,31,.12)' },
|
||||
{ label: 'Bliss 가법기대', data: dc.map(x => x.bliss_expected), borderColor: INK, borderDash: [6, 4], borderWidth: 2, pointRadius: 0, tension: .2 },
|
||||
{ label: 'ARM-1 단독', data: dc.map(x => x.R1), borderColor: TEAL, borderWidth: 1.5, pointRadius: 0, tension: .2 },
|
||||
{ label: 'ARM-2 단독', data: dc.map(x => x.R2), borderColor: '#2f63c8', borderWidth: 1.5, pointRadius: 0, tension: .2 },
|
||||
]
|
||||
}, {
|
||||
plugins: { legend: { labels: { color: INK, boxWidth: 14, font: { size: 10 } } },
|
||||
title: { display: true, text: '빨강(실제)이 점선(가법기대) 위 = 시너지', color: MUTE, font: { size: 11 } } },
|
||||
scales: { x: { title: { display: true, text: '단일팔 효능 E', color: MUTE }, ticks: { color: MUTE }, grid: { display: false } },
|
||||
y: { title: { display: true, text: '결손 회복분율', color: MUTE }, ticks: { color: MUTE }, grid: { color: GRID } } }
|
||||
});
|
||||
const sw = s.threshold_sweep;
|
||||
mk('chart-val-synergy-kda', 'line', {
|
||||
labels: sw.map(x => x.KDA),
|
||||
datasets: [{ label: '시너지 초과', data: sw.map(x => x.synergy_excess), borderColor: VERM, backgroundColor: 'rgba(200,64,31,.12)', borderWidth: 2.5, pointRadius: 3, tension: .35, fill: true }]
|
||||
}, {
|
||||
plugins: { legend: { display: false },
|
||||
title: { display: true, text: 'DP 문턱 KDA — 중간 중증도에서 시너지 최대', color: MUTE, font: { size: 11 } } },
|
||||
scales: { x: { title: { display: true, text: 'DP 협동 문턱 KDA', color: MUTE }, ticks: { color: MUTE }, grid: { display: false } },
|
||||
y: { ticks: { color: MUTE }, grid: { color: GRID } } }
|
||||
});
|
||||
}
|
||||
|
||||
// 불확실성 정량(UQ) — 보정된 신뢰구간 + 커버리지 before/after
|
||||
function renderUQ() {
|
||||
const u = uq; if (!u || !u.classes) return;
|
||||
const oc = u.overall_coverage || {};
|
||||
const J = u.classes.JAK_inhibitor || {};
|
||||
el('val-uq-headline').innerHTML = [
|
||||
[Math.round((oc.mean_only_empirical || 0) * 100) + '%', '단순구간(과신)'],
|
||||
[Math.round((oc.population_empirical || 0) * 100) + '%', '계층적(보정됨)'],
|
||||
[Math.round((oc.nominal || .9) * 100) + '%', '명목 목표'],
|
||||
[(J.lag_mean != null ? J.lag_mean.toFixed(2) : '–') + 'm', 'JAK lag 사후평균'],
|
||||
].map(([v, l]) => `<div class="vstat"><div class="vsv">${v}</div><div class="vsl">${l}</div></div>`).join('');
|
||||
const b = (J.post_band) || { months: [], median: [], lo: [], hi: [] };
|
||||
mk('chart-val-uqband', 'line', {
|
||||
labels: b.months.map(m => m + 'm'),
|
||||
datasets: [
|
||||
{ label: '95% 상한', data: b.hi, borderColor: 'transparent', backgroundColor: 'rgba(31,93,82,.16)', pointRadius: 0, fill: '+1', tension: .3 },
|
||||
{ label: '5% 하한', data: b.lo, borderColor: 'transparent', backgroundColor: 'rgba(31,93,82,.16)', pointRadius: 0, fill: false, tension: .3 },
|
||||
{ label: '중앙(모집단 평균)', data: b.median, borderColor: TEAL, borderWidth: 3, pointRadius: 0, tension: .3 },
|
||||
]
|
||||
}, {
|
||||
plugins: { legend: { labels: { color: INK, boxWidth: 12, font: { size: 10 }, filter: it => !it.text.includes('하한') } },
|
||||
title: { display: true, text: 'JAK 회복 타이밍 — 90% 신뢰띠(보정됨)', color: MUTE, font: { size: 11 } } },
|
||||
scales: { y: { min: 0, max: 1.1, title: { display: true, text: '정규화 회복도', color: MUTE }, ticks: { color: MUTE }, grid: { color: GRID } },
|
||||
x: { ticks: { color: MUTE, maxTicksLimit: 8 }, grid: { display: false } } }
|
||||
});
|
||||
const cls = ['JAK_inhibitor', 'finasteride'].filter(c => u.classes[c] && u.classes[c].loto_coverage);
|
||||
const nm = { JAK_inhibitor: 'JAK', finasteride: '피나스테리드' };
|
||||
mk('chart-val-coverage', 'bar', {
|
||||
labels: cls.map(c => nm[c] || c),
|
||||
datasets: [
|
||||
{ type: 'line', label: '명목 90%', data: cls.map(() => 90), borderColor: INK, borderDash: [5, 4], borderWidth: 1.5, pointRadius: 0 },
|
||||
{ label: '단순(과신)', data: cls.map(c => Math.round(u.classes[c].loto_coverage.mean_only_empirical * 100)), backgroundColor: BAD, borderRadius: 3 },
|
||||
{ label: '계층적(보정)', data: cls.map(c => Math.round(u.classes[c].loto_coverage.population_empirical * 100)), backgroundColor: GOOD, borderRadius: 3 },
|
||||
]
|
||||
}, {
|
||||
plugins: { legend: { labels: { color: INK, boxWidth: 12, font: { size: 10 } } },
|
||||
title: { display: true, text: '커버리지 검정: 과신 → 보정', color: MUTE, font: { size: 11 } } },
|
||||
scales: { y: { min: 0, max: 108, title: { display: true, text: '경험적 커버리지 %', color: MUTE }, ticks: { color: MUTE, callback: v => v + '%' }, grid: { color: GRID } },
|
||||
x: { ticks: { color: INK, font: { size: 11 } }, grid: { display: false } } }
|
||||
});
|
||||
}
|
||||
|
||||
// 데이터 지형
|
||||
function renderLandscape() {
|
||||
const L = data.landscape; if (!L) return;
|
||||
const h = L.headline;
|
||||
el('val-headline').innerHTML = [
|
||||
[h.gb + ' GB', '수집 데이터'], [h.files, '파일'], [h.datasets_downloaded + '+', '데이터셋'],
|
||||
[h.waves, '탐색 웨이브'], [h.agents, '에이전트'],
|
||||
].map(([v, l]) => `<div class="vstat"><div class="vsv">${v}</div><div class="vsl">${l}</div></div>`).join('');
|
||||
const m = L.by_modality;
|
||||
mk('chart-val-modality', 'bar', {
|
||||
labels: m.map(x => x.mod),
|
||||
datasets: [{ label: '다운로드', data: m.map(x => x.dl), backgroundColor: TEAL, borderRadius: 3 },
|
||||
{ label: '기록·게이트', data: m.map(x => x.rec), backgroundColor: '#cdbf9f', borderRadius: 3 }]
|
||||
}, {
|
||||
plugins: { legend: { labels: { color: INK, boxWidth: 12, font: { size: 10 } } } },
|
||||
scales: { x: { stacked: true, ticks: { color: INK, font: { size: 10 } }, grid: { display: false } },
|
||||
y: { stacked: true, ticks: { color: MUTE }, grid: { color: GRID } } }
|
||||
});
|
||||
const d = L.by_disease;
|
||||
mk('chart-val-disease', 'bar', {
|
||||
labels: d.map(x => x.d), datasets: [{ data: d.map(x => x.n), backgroundColor: VERM, borderRadius: 3 }]
|
||||
}, {
|
||||
indexAxis: 'y', plugins: { legend: { display: false } },
|
||||
scales: { x: { ticks: { color: MUTE }, grid: { color: GRID } }, y: { ticks: { color: INK, font: { size: 11 } }, grid: { display: false } } }
|
||||
});
|
||||
}
|
||||
|
||||
// 임상 시간축 회복곡선
|
||||
function renderTiming() {
|
||||
const t = data.timing; if (!t) return;
|
||||
const palette = { '피나스테리드': '#2f63c8', '두타스테리드': VERM, '미녹시딜': WARN, 'JAK억제제': TEAL };
|
||||
const datasets = Object.keys(t.curves).map(k => ({
|
||||
label: k, data: t.curves[k], borderColor: palette[k] || INK, backgroundColor: 'transparent',
|
||||
borderWidth: 2.5, pointRadius: 2, tension: .3,
|
||||
}));
|
||||
mk('chart-val-timing', 'line', { labels: t.months.map(m => m < 1 ? m * 4 + '주' : m + 'm'), datasets }, {
|
||||
plugins: { legend: { labels: { color: INK, boxWidth: 14, font: { size: 11 } } } },
|
||||
scales: { y: { min: -10, max: 105, title: { display: true, text: '회복도 %', color: MUTE }, ticks: { color: MUTE, callback: v => v + '%' }, grid: { color: GRID } },
|
||||
x: { ticks: { color: MUTE, maxTicksLimit: 9 }, grid: { display: false } } }
|
||||
});
|
||||
}
|
||||
|
||||
// Halloy 벤치마크
|
||||
function renderBenchmark() {
|
||||
const b = data.benchmark; if (!b) return;
|
||||
mk('chart-val-bench', 'bar', {
|
||||
labels: b.labels,
|
||||
datasets: [{ label: 'Halloy 자동자', data: b.Halloy, backgroundColor: MUTE, borderRadius: 3 },
|
||||
{ label: '우리 트윈', data: b['트윈'], backgroundColor: VERM, borderRadius: 3 }]
|
||||
}, {
|
||||
plugins: { legend: { labels: { color: INK, boxWidth: 12 } } },
|
||||
scales: { y: { ticks: { color: MUTE, callback: v => v + '%' }, grid: { color: GRID } },
|
||||
x: { ticks: { color: INK, font: { size: 11 } }, grid: { display: false } } }
|
||||
});
|
||||
}
|
||||
|
||||
// 1) 다층 요약 카드
|
||||
function renderSummary() {
|
||||
el('val-summary').innerHTML = (data.summary || []).map(c => {
|
||||
const rows = c.rows.map(r => {
|
||||
const badge = r.strong
|
||||
? '<span class="vb vb-ok">확증</span>'
|
||||
: '<span class="vb vb-warn">비재현</span>';
|
||||
const pv = r.p == null ? '' : `<span class="vp">p=${fmtP(r.p)}</span>`;
|
||||
return `<div class="vrow"><div class="vrd">${r.design}</div><div class="vrm">${r.metric} ${pv}</div>${badge}</div>`;
|
||||
}).join('');
|
||||
return `<div class="vcard"><div class="vch">${c.claim}</div>${rows}</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// 2) 분자 검증 유의도 -log10(p)
|
||||
function renderMolecular() {
|
||||
const m = data.molecular || [];
|
||||
const labels = m.map(x => x.t), vals = m.map(x => Math.min(60, -Math.log10(x.p)));
|
||||
const cols = m.map(x => x.dz === 'AA' ? VERM : TEAL);
|
||||
mk('chart-val-mol', 'bar', {
|
||||
labels, datasets: [{ data: vals, backgroundColor: cols, borderRadius: 4 }]
|
||||
}, {
|
||||
indexAxis: 'y', plugins: { legend: { display: false }, tooltip: { callbacks: { label: c => '−log₁₀p = ' + c.parsed.x.toFixed(1) } } },
|
||||
scales: { x: { title: { display: true, text: '−log₁₀(p) (유의 ≈ 1.3↑)', color: MUTE }, ticks: { color: MUTE }, grid: { color: GRID } },
|
||||
y: { ticks: { color: INK, font: { size: 11 } }, grid: { display: false } } }
|
||||
});
|
||||
}
|
||||
|
||||
// 3) AGA DP Wnt 역전 (grouped bar)
|
||||
function renderAgaDp() {
|
||||
const a = data.aga_dp; if (!a) return;
|
||||
const genes = Object.keys(a.genes), conds = a.conds;
|
||||
const colByCond = { 'Con': MUTE, 'TP': BAD, 'TP+Ab': TEAL };
|
||||
const datasets = conds.map(cn => ({
|
||||
label: cn === 'Con' ? '정상' : cn === 'TP' ? 'AGA(TP)' : '치료(TP+Ab)',
|
||||
data: genes.map(g => a.genes[g][cn]), backgroundColor: colByCond[cn] || INK, borderRadius: 3,
|
||||
}));
|
||||
mk('chart-val-agadp', 'bar', { labels: genes, datasets }, {
|
||||
plugins: { legend: { labels: { color: INK, boxWidth: 12, font: { size: 11 } } } },
|
||||
scales: { y: { title: { display: true, text: 'DP세포 발현(log)', color: MUTE }, ticks: { color: MUTE }, grid: { color: GRID } },
|
||||
x: { ticks: { color: INK }, grid: { display: false } } }
|
||||
});
|
||||
}
|
||||
|
||||
// 4) JAK억제제별 염증신호
|
||||
function renderJak() {
|
||||
const j = data.jak_drugs; if (!j) return;
|
||||
const labels = Object.keys(j), vals = labels.map(k => j[k]);
|
||||
const cols = vals.map(v => v > 0.3 ? BAD : GOOD); // 높으면 염증 잔존(빨강), 낮으면 억제(녹색)
|
||||
mk('chart-val-jak', 'bar', { labels, datasets: [{ data: vals, backgroundColor: cols, borderRadius: 4 }] }, {
|
||||
plugins: { legend: { display: false }, tooltip: { callbacks: { label: c => 'IFN sig = ' + c.parsed.y.toFixed(2) } } },
|
||||
scales: { y: { title: { display: true, text: '염증/IFN 시그니처', color: MUTE }, ticks: { color: MUTE }, grid: { color: GRID } },
|
||||
x: { ticks: { color: INK }, grid: { display: false } } }
|
||||
});
|
||||
}
|
||||
|
||||
// 5) AA 단일세포 비재현 (코호트별 T세포 평균%)
|
||||
function renderAaSc() {
|
||||
const s = data.aa_sc; if (!s) return;
|
||||
const mean = a => a && a.length ? a.reduce((x, y) => x + y, 0) / a.length : 0;
|
||||
const cohorts = Object.keys(s);
|
||||
mk('chart-val-aasc', 'bar', {
|
||||
labels: cohorts,
|
||||
datasets: [
|
||||
{ label: 'AA', data: cohorts.map(c => +mean(s[c].AA).toFixed(1)), backgroundColor: VERM, borderRadius: 3 },
|
||||
{ label: '정상', data: cohorts.map(c => +mean(s[c].control).toFixed(1)), backgroundColor: MUTE, borderRadius: 3 },
|
||||
]
|
||||
}, {
|
||||
plugins: { legend: { labels: { color: INK, boxWidth: 12 } }, title: { display: true, text: '두 코호트 T세포 비율 불일치 (포획 편향)', color: MUTE, font: { size: 11 } } },
|
||||
scales: { y: { title: { display: true, text: 'T세포 %', color: MUTE }, ticks: { color: MUTE, callback: v => v + '%' }, grid: { color: GRID } },
|
||||
x: { ticks: { color: INK, font: { size: 10 } }, grid: { display: false } } }
|
||||
});
|
||||
}
|
||||
|
||||
// 6) GWAS 커버리지 도넛
|
||||
function renderGwas() {
|
||||
const g = data.gwas; if (!g) return;
|
||||
const donut = (id, cov, label) => mk(id, 'doughnut', {
|
||||
labels: ['보유', '누락'],
|
||||
datasets: [{ data: [Math.round(cov * 16), 16 - Math.round(cov * 16)], backgroundColor: [TEAL, '#e6ddcb'], borderColor: '#fbf9f4', borderWidth: 2 }]
|
||||
}, { cutout: '62%', plugins: { legend: { display: false }, tooltip: { callbacks: { label: c => c.label + ': ' + c.parsed } } } });
|
||||
donut('chart-val-gwasAGA', g.AGA_cov);
|
||||
donut('chart-val-gwasAA', g.AA_cov);
|
||||
const aga = el('chart-val-gwasAGA'); if (aga) aga.parentElement.querySelector('.val-donut-lab').innerHTML = 'AGA<br><b>' + Math.round(g.AGA_cov * 100) + '%</b>';
|
||||
const aa = el('chart-val-gwasAA'); if (aa) aa.parentElement.querySelector('.val-donut-lab').innerHTML = 'AA<br><b>' + Math.round(g.AA_cov * 100) + '%</b>';
|
||||
}
|
||||
|
||||
// 7) 후보 × 구조 × STRING 표
|
||||
function renderCandidates() {
|
||||
const c = data.candidates || [];
|
||||
const order = { COHERES: 0, weak: 1, isolated: 2, 'n/a': 3 };
|
||||
const rows = c.slice().sort((a, b) => (order[a.cohesion] - order[b.cohesion]) || (b.plddt || 0) - (a.plddt || 0)).map(r => {
|
||||
const pb = r.plddt == null ? '—' : r.plddt.toFixed(0);
|
||||
const pcls = r.plddt >= 70 ? 'good' : r.plddt >= 50 ? 'warn' : 'bad';
|
||||
const coh = { COHERES: '<span class="vb vb-ok">응집</span>', weak: '<span class="vb vb-warn">약</span>',
|
||||
isolated: '<span class="vb vb-bad">미응집</span>', 'n/a': '—' }[r.cohesion] || '—';
|
||||
return `<tr><td class="vc-g">${r.gene}</td><td>${r.dz}</td><td>${r.axis}</td>
|
||||
<td class="vc-p ${pcls}">${pb}</td><td>${r.edges_hi != null ? r.edges_hi : '–'}</td><td>${coh}</td></tr>`;
|
||||
}).join('');
|
||||
el('val-candidates').innerHTML = `<table class="val-table">
|
||||
<thead><tr><th>유전자</th><th>질환</th><th>배정 축</th><th>AlphaFold pLDDT</th><th>STRING 고신뢰 엣지</th><th>축 응집</th></tr></thead>
|
||||
<tbody>${rows}</tbody></table>
|
||||
<p class="val-cap">구조 19/19 보유 · STRING 응집 ${c.filter(x => x.cohesion === 'COHERES').length}/19. GWAS(유전학)+STRING(네트워크)+AlphaFold(구조)가 한 방향으로 모이는 후보가 신뢰도 높음.</p>`;
|
||||
}
|
||||
|
||||
function fmtP(p) { return p < 1e-4 ? p.toExponential(0) : p.toPrecision(2); }
|
||||
function mk(id, type, d, opts) {
|
||||
const ctx = el(id); if (!ctx) return;
|
||||
if (charts[id]) charts[id].destroy();
|
||||
charts[id] = new Chart(ctx, { type, data: d, options: Object.assign({ responsive: true, maintainAspectRatio: false }, opts) });
|
||||
}
|
||||
|
||||
window.ValidationTab = { init };
|
||||
})();
|
||||
Loading…
Reference in New Issue
Block a user