-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest.py
More file actions
338 lines (289 loc) · 12.6 KB
/
test.py
File metadata and controls
338 lines (289 loc) · 12.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
import streamlit as st
import plotly.graph_objects as go
from datetime import datetime
# --- 嘗試匯入 FPDF (防呆機制) ---
try:
from fpdf import FPDF
HAS_FPDF = True
except ImportError:
HAS_FPDF = False
# --- 設定頁面配置 ---
st.set_page_config(
page_title="RadShield AI: Prostate",
page_icon="🛡️",
layout="wide",
initial_sidebar_state="expanded"
)
# --- PDF 生成引擎 (保持英文以確保相容性) ---
if HAS_FPDF:
class PDFReport(FPDF):
def header(self):
self.set_font('Arial', 'B', 15)
self.cell(0, 10, 'RadShield AI - Clinical Decision Report', 0, 1, 'C')
self.ln(5)
def footer(self):
self.set_y(-15)
self.set_font('Arial', 'I', 8)
self.cell(0, 10, f'Page {self.page_no()}', 0, 0, 'C')
def create_pdf(patient_data, risk_result):
pdf = PDFReport()
pdf.add_page()
pdf.set_font("Arial", size=12)
# 1. Patient Info
pdf.set_font("Arial", 'B', 12)
pdf.cell(0, 10, "1. Patient Information", 0, 1)
pdf.set_font("Arial", size=10)
pdf.cell(0, 6, f"Name: {patient_data['name_en']} (ID: {patient_data['id']})", 0, 1)
pdf.cell(0, 6, f"Age: {patient_data['age']} | Date: {datetime.now().strftime('%Y-%m-%d')}", 0, 1)
pdf.ln(5)
# 2. Risk Assessment
pdf.set_font("Arial", 'B', 12)
pdf.cell(0, 10, "2. Risk Stratification", 0, 1)
pdf.set_font("Arial", size=10)
# Acute
if risk_result['acute_level'] == 'High':
pdf.set_text_color(220, 53, 69)
else:
pdf.set_text_color(0, 0, 0)
pdf.cell(0, 6, f"Acute Toxicity Risk: {risk_result['acute_level']} (Score: {risk_result['acute_score']}/100)", 0, 1)
# Chronic
if risk_result['chronic_level'] == 'High':
pdf.set_text_color(220, 53, 69)
else:
pdf.set_text_color(0, 0, 0)
pdf.cell(0, 6, f"Late Toxicity Risk: {risk_result['chronic_level']} (Score: {risk_result['chronic_score']}/100)", 0, 1)
pdf.set_text_color(0, 0, 0) # Reset color
pdf.ln(5)
# 3. Drivers
pdf.set_font("Arial", 'B', 12)
pdf.cell(0, 10, "3. Key Risk Drivers (Concept 4)", 0, 1)
pdf.set_font("Arial", size=10)
if risk_result['drivers_en']:
for driver in risk_result['drivers_en']:
pdf.cell(0, 6, f"- {driver}", 0, 1)
else:
pdf.cell(0, 6, "- No significant risk drivers identified.", 0, 1)
pdf.ln(5)
# 4. Recommendations
pdf.set_font("Arial", 'B', 12)
pdf.cell(0, 10, "4. Optimization Plan (Concept 3)", 0, 1)
pdf.set_font("Arial", size=10)
if risk_result['recommendations_en']:
for rec in risk_result['recommendations_en']:
pdf.multi_cell(0, 6, f"- {rec}")
else:
pdf.cell(0, 6, "- Standard of Care (SoC) recommended.", 0, 1)
pdf.ln(10)
pdf.set_font("Arial", 'I', 8)
pdf.cell(0, 6, "Disclaimer: This report is generated by a prototype AI system.", 0, 1)
return pdf.output(dest='S').encode('latin-1')
# --- 風險計算核心邏輯 ---
def calculate_risk(patient_data):
acute_score = 10
chronic_score = 5
drivers = []
drivers_en = []
recommendations = []
recommendations_en = []
# 1. 臨床因子
if patient_data['ibd']:
acute_score += 25
chronic_score += 30
drivers.append('發炎性腸道疾病 (IBD)')
drivers_en.append('Inflammatory Bowel Disease (IBD)')
if patient_data['diabetes']:
chronic_score += 10
if patient_data['anticoagulants']:
acute_score += 15
chronic_score += 25
drivers.append('使用抗凝血劑 (Bleeding Risk)')
drivers_en.append('Anticoagulant Usage (High Bleeding Risk)')
# 2. 基因組因子 (Concept 4)
if patient_data['atm_mutation']:
acute_score += 20
chronic_score += 25
drivers.append('ATM 基因突變 (Radiosensitivity)')
drivers_en.append('ATM Gene Mutation (Radiosensitivity)')
if patient_data['nbn_mutation']:
acute_score += 15
chronic_score += 20
drivers.append('NBN 基因突變')
drivers_en.append('NBN Gene Mutation')
if patient_data['lig4_mutation']:
acute_score += 30
chronic_score += 40
drivers.append('LIG4 基因表現異常 (DNA Repair Defect)')
drivers_en.append('LIG4 Expression Abnormality (DNA Repair Defect)')
# 3. 劑量學因子
v70 = patient_data['rectum_v70']
v50 = patient_data['rectum_v50']
if v70 > 20:
chronic_score += (v70 - 20) * 2
drivers.append(f'Rectum V70 過高 ({v70}%)')
drivers_en.append(f'High Rectum V70 ({v70}%)')
if v50 > 50:
acute_score += (v50 - 50) * 1.5
acute_score = min(acute_score, 100)
chronic_score = min(chronic_score, 100)
def get_level(score):
if score < 30: return 'Low', 'green'
if score < 60: return 'Intermediate', 'orange'
return 'High', 'red'
acute_level, acute_color = get_level(acute_score)
chronic_level, chronic_color = get_level(chronic_score)
# 產生建議
if chronic_level == 'High' or v70 > 20:
recommendations.append("💡 建議使用直腸隔離凝膠 (SpaceOAR) 以增加物理距離 (Concept 1)")
recommendations.append("💡 需重新優化治療計畫:限制 Rectum V70 < 15%")
recommendations_en.append("Apply Rectal Spacer (SpaceOAR) to increase physical separation (Concept 1).")
recommendations_en.append("Re-optimize treatment plan: Tighten Rectum V70 constraint to < 15%.")
if patient_data['atm_mutation'] or patient_data['lig4_mutation']:
recommendations.append("🧬 基因組顯示高輻射敏感性:考慮減少單次劑量或轉用質子治療 (Proton)")
recommendations_en.append("Genomic Radiosensitivity: Consider hypofractionation reduction or Proton Therapy.")
if patient_data['anticoagulants'] and acute_score > 40:
recommendations.append("💊 出血風險高:建議會診心臟科評估暫停抗凝血劑")
recommendations_en.append("High Bleeding Risk: Consult cardiology regarding temporary suspension of anticoagulants.")
return {
"acute_score": acute_score,
"chronic_score": chronic_score,
"acute_level": acute_level,
"chronic_level": chronic_level,
"drivers": drivers,
"drivers_en": drivers_en,
"recommendations": recommendations,
"recommendations_en": recommendations_en
}
# --- 側邊欄導航與輸入 ---
with st.sidebar:
st.title("🛡️ RadShield AI")
st.caption("Prostate Cancer Precision RT")
st.header("Patient Intake")
with st.expander("1. Clinical Profile", expanded=True):
p_name = st.text_input("Patient Name", "陳大明 (Chen, Da-Ming)")
p_name_en = "Chen, Da-Ming" if "Chen" in p_name else "Patient-001"
p_id = st.text_input("Patient ID", "P-2024-089")
p_age = st.number_input("Age", 40, 90, 68)
st.markdown("---")
st.subheader("Risk Factors")
c_diabetes = st.checkbox("糖尿病 (Diabetes)")
c_ibd = st.checkbox("發炎性腸病 (IBD)")
c_anticoag = st.checkbox("使用抗凝血劑 (Blood Thinners)")
c_smoking = st.checkbox("吸菸史 (Smoking History)")
with st.expander("2. Radiogenomics Panel", expanded=True):
st.info("Concept 4: Pre-therapy Gene Markers")
g_atm = st.checkbox("ATM Mutation", help="與 DNA 雙股斷裂修復相關")
g_nbn = st.checkbox("NBN Mutation", help="細胞週期檢查點")
g_lig4 = st.checkbox("LIG4 Mutation", help="NHEJ 修復機制異常")
with st.expander("3. Dosimetry (DVH)", expanded=True):
st.warning("Concept 3: DVH Constraints")
d_v70 = st.slider("Rectum V70 (%)", 0, 40, 18, help="Target < 15-20%")
d_v50 = st.slider("Rectum V50 (%)", 0, 80, 35)
# --- 整合資料 ---
patient_data = {
'name': p_name,
'name_en': p_name_en,
'id': p_id,
'ibd': c_ibd,
'diabetes': c_diabetes,
'anticoagulants': c_anticoag,
'atm_mutation': g_atm,
'nbn_mutation': g_nbn,
'lig4_mutation': g_lig4,
'rectum_v70': d_v70,
'rectum_v50': d_v50,
'age': p_age
}
# --- 執行風險運算 ---
risk_result = calculate_risk(patient_data)
# --- 主畫面 UI ---
st.title(f"Patient: {p_name}")
st.markdown(f"**ID:** {p_id} | **Date:** {datetime.now().strftime('%Y-%m-%d')}")
tab1, tab2, tab3 = st.tabs(["📊 Risk Dashboard", "🧬 Analysis Detail", "📄 Decision Report"])
# Tab 1: Dashboard
with tab1:
col1, col2 = st.columns(2)
with col1:
st.subheader("急性毒性風險 (Acute)")
st.caption("治療期間 ~ 3個月內")
delta_color = "normal" if risk_result['acute_level'] == 'Low' else "inverse"
st.metric("Risk Score / 100", risk_result['acute_score'], risk_result['acute_level'], delta_color=delta_color)
st.progress(risk_result['acute_score'] / 100)
with col2:
st.subheader("慢性毒性風險 (Late)")
st.caption("治療後 6個月以上")
delta_color_chronic = "normal" if risk_result['chronic_level'] == 'Low' else "inverse"
st.metric("Risk Score / 100", risk_result['chronic_score'], risk_result['chronic_level'], delta_color=delta_color_chronic)
st.progress(risk_result['chronic_score'] / 100)
st.markdown("---")
st.subheader("💡 Clinical Decision Support (CDS)")
if risk_result['recommendations']:
for rec in risk_result['recommendations']:
if "SpaceOAR" in rec or "V70" in rec:
st.error(rec)
else:
st.warning(rec)
else:
st.success("✅ 目前評估為低風險,建議維持標準治療計畫。")
# Tab 2: Analysis
with tab2:
col_chart, col_text = st.columns([2, 1])
with col_chart:
categories = ['Dosimetry', 'Genomics', 'Comorbidities', 'Meds', 'Age']
values = [
min(d_v70 * 3, 100),
90 if (g_atm or g_lig4) else 20,
80 if (c_ibd or c_diabetes) else 20,
80 if c_anticoag else 10,
70 if p_age > 75 else 40
]
fig = go.Figure()
fig.add_trace(go.Scatterpolar(r=values, theta=categories, fill='toself', name='Risk Profile', line_color='#3b82f6'))
fig.update_layout(polar=dict(radialaxis=dict(visible=True, range=[0, 100])), showlegend=False, title="Risk Radar")
st.plotly_chart(fig, use_container_width=True)
with col_text:
st.subheader("主要風險因子")
if risk_result['drivers']:
for driver in risk_result['drivers']:
st.write(f"🔴 **{driver}**")
else:
st.write("無顯著風險因子")
# Tab 3: Report (Modified)
with tab3:
st.header("Clinical Report Preview")
# 1. 準備 Markdown 格式的報告內容 (中文版,用於網頁顯示)
report_markdown = f"""
# RadShield AI - Patient Risk Report
**Patient:** {patient_data['name']} ({patient_data['id']})
**Date:** {datetime.now().strftime('%Y-%m-%d')}
## Risk Assessment
- **Acute Toxicity Risk:** {risk_result['acute_level']} (Score: {risk_result['acute_score']})
- **Late Toxicity Risk:** {risk_result['chronic_level']} (Score: {risk_result['chronic_score']})
## Identified Risk Drivers
{chr(10).join(['- ' + d for d in risk_result['drivers']] if risk_result['drivers'] else ['- No significant drivers'])}
## Optimization Recommendations
{chr(10).join(['- ' + r for r in risk_result['recommendations']] if risk_result['recommendations'] else ['- Maintain Standard of Care'])}
---
*Generated by RadShield AI Prototype*
"""
# 2. 在網頁上渲染 Markdown
st.markdown(report_markdown)
st.markdown("---")
# 3. PDF 下載按鈕
st.subheader("Download")
if HAS_FPDF:
if st.button("Generate PDF Report"):
pdf_bytes = create_pdf(patient_data, risk_result)
st.download_button(
label="📥 Download PDF to EMR",
data=pdf_bytes,
file_name=f"RadShield_Report_{p_id}.pdf",
mime="application/pdf"
)
else:
st.warning("⚠️ 未安裝 'fpdf' 套件,僅提供純文字下載。")
st.download_button(
label="📥 Download Text Report",
data=report_markdown, # 使用上面的 markdown 字串作為 txt 內容
file_name=f"RadShield_Report_{p_id}.txt",
mime="text/plain"
)