Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -9,6 +9,8 @@ import pickle
|
|
| 9 |
import hashlib
|
| 10 |
import time
|
| 11 |
import threading
|
|
|
|
|
|
|
| 12 |
|
| 13 |
# ===== 页面设置 =====
|
| 14 |
st.set_page_config(page_title="多肽虚拟筛选工具", layout="wide")
|
|
@@ -16,27 +18,58 @@ st.title("多肽虚拟筛选工具")
|
|
| 16 |
|
| 17 |
# ===== 防休眠机制 =====
|
| 18 |
def keep_alive():
|
| 19 |
-
"""每隔一段时间在后台执行一次轻量操作,防止应用被云平台冻结"""
|
| 20 |
while True:
|
| 21 |
-
time.sleep(300)
|
| 22 |
try:
|
| 23 |
_ = st.session_state.get("_", None)
|
| 24 |
except Exception:
|
| 25 |
pass
|
| 26 |
|
| 27 |
-
# 启动后台保活线程
|
| 28 |
if "alive_thread" not in st.session_state:
|
| 29 |
alive_thread = threading.Thread(target=keep_alive, daemon=True)
|
| 30 |
alive_thread.start()
|
| 31 |
st.session_state.alive_thread = True
|
| 32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
# ===== 侧边栏参数 =====
|
| 34 |
st.sidebar.header("筛选参数")
|
| 35 |
|
| 36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
-
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
cut_rule = st.sidebar.selectbox(
|
| 42 |
"酶切规则",
|
|
@@ -50,7 +83,7 @@ cut_rule = st.sidebar.selectbox(
|
|
| 50 |
"中性蛋白酶 (FLIV)",
|
| 51 |
"碱性蛋白酶 (KRFLIV)",
|
| 52 |
],
|
| 53 |
-
|
| 54 |
)
|
| 55 |
cut_map = {
|
| 56 |
"胰蛋白酶 (KR)": "KR",
|
|
@@ -64,9 +97,8 @@ cut_map = {
|
|
| 64 |
}
|
| 65 |
CUT = cut_map[cut_rule]
|
| 66 |
|
| 67 |
-
top_n = st.sidebar.number_input("输出Top几",
|
| 68 |
-
|
| 69 |
-
content_threshold = st.sidebar.number_input("蛋白含量阈值(%)", value=10, min_value=0, max_value=100)
|
| 70 |
|
| 71 |
# ===== 生成缓存文件名 =====
|
| 72 |
def get_cache_key():
|
|
@@ -188,123 +220,130 @@ def get_content(src_name):
|
|
| 188 |
# ===== 主界面 =====
|
| 189 |
st.subheader("开始筛选")
|
| 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 |
-
st.
|
| 309 |
-
|
| 310 |
-
|
|
|
|
|
|
| 9 |
import hashlib
|
| 10 |
import time
|
| 11 |
import threading
|
| 12 |
+
import re
|
| 13 |
+
from datetime import datetime
|
| 14 |
|
| 15 |
# ===== 页面设置 =====
|
| 16 |
st.set_page_config(page_title="多肽虚拟筛选工具", layout="wide")
|
|
|
|
| 18 |
|
| 19 |
# ===== 防休眠机制 =====
|
| 20 |
def keep_alive():
|
|
|
|
| 21 |
while True:
|
| 22 |
+
time.sleep(300)
|
| 23 |
try:
|
| 24 |
_ = st.session_state.get("_", None)
|
| 25 |
except Exception:
|
| 26 |
pass
|
| 27 |
|
|
|
|
| 28 |
if "alive_thread" not in st.session_state:
|
| 29 |
alive_thread = threading.Thread(target=keep_alive, daemon=True)
|
| 30 |
alive_thread.start()
|
| 31 |
st.session_state.alive_thread = True
|
| 32 |
|
| 33 |
+
# ===== 默认参数 =====
|
| 34 |
+
DEFAULTS = {
|
| 35 |
+
"drug": "HAEGTFTSDV",
|
| 36 |
+
"min_len": 10,
|
| 37 |
+
"max_len": 12,
|
| 38 |
+
"cut_rule": "胰蛋白酶 (KR)",
|
| 39 |
+
"top_n": 10,
|
| 40 |
+
"content_threshold": 10,
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
# 初始化 session_state
|
| 44 |
+
for key, value in DEFAULTS.items():
|
| 45 |
+
if key not in st.session_state:
|
| 46 |
+
st.session_state[key] = value
|
| 47 |
+
|
| 48 |
# ===== 侧边栏参数 =====
|
| 49 |
st.sidebar.header("筛选参数")
|
| 50 |
|
| 51 |
+
# 重置按钮(放在最上面)
|
| 52 |
+
def reset_params():
|
| 53 |
+
for key, value in DEFAULTS.items():
|
| 54 |
+
st.session_state[key] = value
|
| 55 |
+
|
| 56 |
+
st.sidebar.button("🔄 重置为默认参数", on_click=reset_params, use_container_width=True)
|
| 57 |
|
| 58 |
+
drug = st.sidebar.text_input(
|
| 59 |
+
"靶点序列(参照肽)",
|
| 60 |
+
key="drug",
|
| 61 |
+
help="请输入20种天然氨基酸字母序列(如:HAEGTFTSDV)"
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
# 靶点输入实时校验
|
| 65 |
+
if drug:
|
| 66 |
+
if re.fullmatch(r'[ACDEFGHIKLMNPQRSTVWYacdefghiklmnpqrstvwy]+', drug):
|
| 67 |
+
st.sidebar.success("✅ 序列格式正确")
|
| 68 |
+
else:
|
| 69 |
+
st.sidebar.error("❌ 序列包含非法字符,仅限20种天然氨基酸字母")
|
| 70 |
+
|
| 71 |
+
min_len = st.sidebar.number_input("最小长度", min_value=5, max_value=50, key="min_len")
|
| 72 |
+
max_len = st.sidebar.number_input("最大长度", min_value=5, max_value=50, key="max_len")
|
| 73 |
|
| 74 |
cut_rule = st.sidebar.selectbox(
|
| 75 |
"酶切规则",
|
|
|
|
| 83 |
"中性蛋白酶 (FLIV)",
|
| 84 |
"碱性蛋白酶 (KRFLIV)",
|
| 85 |
],
|
| 86 |
+
key="cut_rule",
|
| 87 |
)
|
| 88 |
cut_map = {
|
| 89 |
"胰蛋白酶 (KR)": "KR",
|
|
|
|
| 97 |
}
|
| 98 |
CUT = cut_map[cut_rule]
|
| 99 |
|
| 100 |
+
top_n = st.sidebar.number_input("输出Top几", min_value=5, max_value=100, key="top_n")
|
| 101 |
+
content_threshold = st.sidebar.number_input("蛋白含量阈值(%)", min_value=0, max_value=100, key="content_threshold")
|
|
|
|
| 102 |
|
| 103 |
# ===== 生成缓存文件名 =====
|
| 104 |
def get_cache_key():
|
|
|
|
| 220 |
# ===== 主界面 =====
|
| 221 |
st.subheader("开始筛选")
|
| 222 |
|
| 223 |
+
# 按钮在计算中禁用
|
| 224 |
+
if st.button("🚀 开始筛选", type="primary", disabled=st.session_state.get("running", False)):
|
| 225 |
+
# 输入校验
|
| 226 |
+
if not re.fullmatch(r'[ACDEFGHIKLMNPQRSTVWYacdefghiklmnpqrstvwy]+', drug):
|
| 227 |
+
st.error("靶点序列包含非法字符,请输入有效的氨基酸序列(仅限20种天然氨基酸字母)")
|
| 228 |
+
st.stop()
|
| 229 |
+
|
| 230 |
+
st.session_state.running = True
|
| 231 |
+
|
| 232 |
+
filter_params = f"靶点:{drug.upper()} | 酶:{cut_rule.split('(')[0].strip()} | 长度:{min_len}-{max_len} | 时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
| 233 |
+
|
| 234 |
+
try:
|
| 235 |
+
tok, mod = load_model()
|
| 236 |
+
|
| 237 |
+
food_cache_file = "food_cache.pkl"
|
| 238 |
+
if os.path.exists(food_cache_file):
|
| 239 |
+
with open(food_cache_file, "rb") as f:
|
| 240 |
+
FOOD = pickle.load(f)
|
| 241 |
+
st.success(f"已从缓存加载 {len(FOOD)} 条蛋白序列")
|
| 242 |
+
else:
|
| 243 |
+
with st.spinner("正在从UniProt下载食用蛋白序列..."):
|
| 244 |
+
FOOD = fetch_proteins()
|
| 245 |
+
with open(food_cache_file, "wb") as f:
|
| 246 |
+
pickle.dump(FOOD, f)
|
| 247 |
+
st.success(f"已获取并缓存 {len(FOOD)} 条蛋白序列")
|
| 248 |
+
|
| 249 |
+
allp = []
|
| 250 |
+
for name, seq in FOOD.items():
|
| 251 |
+
for p in cut(seq, CUT, min_len, max_len):
|
| 252 |
+
allp.append({"src": name, "p": p})
|
| 253 |
+
|
| 254 |
+
seen = set()
|
| 255 |
+
unip = []
|
| 256 |
+
for x in allp:
|
| 257 |
+
species_short = (
|
| 258 |
+
x["src"].split("(")[-1].split(")")[0].strip()
|
| 259 |
+
if "(" in x["src"]
|
| 260 |
+
else x["src"]
|
| 261 |
+
)
|
| 262 |
+
key = (species_short, x["p"])
|
| 263 |
+
if key not in seen:
|
| 264 |
+
seen.add(key)
|
| 265 |
+
unip.append(x)
|
| 266 |
+
st.info(f"酶切并去重后共 {len(unip)} 条候选肽")
|
| 267 |
+
|
| 268 |
+
cache_key = get_cache_key()
|
| 269 |
+
score_cache_file = f"score_cache_{cache_key}.pkl"
|
| 270 |
+
|
| 271 |
+
if os.path.exists(score_cache_file):
|
| 272 |
+
with open(score_cache_file, "rb") as f:
|
| 273 |
+
res = pickle.load(f)
|
| 274 |
+
st.success(f"已从缓存加载 {len(res)} 条打分结果")
|
| 275 |
+
else:
|
| 276 |
+
progress_bar = st.progress(0)
|
| 277 |
+
status_text = st.empty()
|
| 278 |
+
v_drug = vec(drug.upper(), tok, mod)
|
| 279 |
+
res = []
|
| 280 |
+
total = len(unip)
|
| 281 |
+
for idx, x in enumerate(unip):
|
| 282 |
+
v_p = vec(x["p"], tok, mod)
|
| 283 |
+
s = sim(v_p, v_drug)
|
| 284 |
+
res.append({"src": x["src"], "p": x["p"], "score": round(s, 4)})
|
| 285 |
+
if idx % 50 == 0:
|
| 286 |
+
progress_bar.progress(min(idx / total, 1.0))
|
| 287 |
+
status_text.text(f"打分进度: {idx}/{total}")
|
| 288 |
+
progress_bar.progress(1.0)
|
| 289 |
+
status_text.text(f"打分完成: {total}/{total}")
|
| 290 |
+
with open(score_cache_file, "wb") as f:
|
| 291 |
+
pickle.dump(res, f)
|
| 292 |
+
st.success(f"已完成打分并缓存")
|
| 293 |
+
|
| 294 |
+
for x in res:
|
| 295 |
+
x["content_pct"] = get_content(x["src"])
|
| 296 |
+
|
| 297 |
+
filtered_res = [x for x in res if x["content_pct"] >= content_threshold]
|
| 298 |
+
if filtered_res:
|
| 299 |
+
res = filtered_res
|
| 300 |
+
|
| 301 |
+
res.sort(key=lambda x: x["score"], reverse=True)
|
| 302 |
+
|
| 303 |
+
st.caption(f"📋 {filter_params}")
|
| 304 |
+
|
| 305 |
+
st.subheader("结构相似度排名")
|
| 306 |
+
df_top = pd.DataFrame(res[:top_n])
|
| 307 |
+
df_top.index = range(1, len(df_top) + 1)
|
| 308 |
+
show_cols = ["src", "p", "score"]
|
| 309 |
+
if "content_pct" in df_top.columns:
|
| 310 |
+
show_cols.append("content_pct")
|
| 311 |
+
max_display_rows = 100
|
| 312 |
+
st.caption(f"显示前 {min(top_n, max_display_rows)} 条结果(共 {top_n} 条)")
|
| 313 |
+
st.dataframe(df_top[show_cols].head(max_display_rows), width='stretch')
|
| 314 |
+
|
| 315 |
+
csv_top = df_top.to_csv(index=False).encode("utf-8-sig")
|
| 316 |
+
st.download_button("下载Top结果CSV", csv_top, "out_top.csv", "text/csv")
|
| 317 |
+
|
| 318 |
+
df_all = pd.DataFrame(res)
|
| 319 |
+
csv_all = df_all.to_csv(index=False).encode("utf-8-sig")
|
| 320 |
+
st.download_button("下载全部结果CSV", csv_all, "out_all.csv", "text/csv")
|
| 321 |
+
|
| 322 |
+
st.subheader("物种贡献统计(全部候选肽)")
|
| 323 |
+
species_counts = {}
|
| 324 |
+
for x in res:
|
| 325 |
+
s = (
|
| 326 |
+
x["src"].split("(")[-1].split(")")[0].strip()
|
| 327 |
+
if "(" in x["src"]
|
| 328 |
+
else "unknown"
|
| 329 |
+
)
|
| 330 |
+
species_counts[s] = species_counts.get(s, 0) + 1
|
| 331 |
+
species_df = pd.DataFrame(
|
| 332 |
+
list(species_counts.items()), columns=["物种", "候选肽数量"]
|
| 333 |
)
|
| 334 |
+
species_df = species_df.sort_values("候选肽数量", ascending=False)
|
| 335 |
+
species_df = species_df.reset_index(drop=True)
|
| 336 |
+
species_df.index = range(1, len(species_df) + 1)
|
| 337 |
+
total_count = species_df["候选肽数量"].sum()
|
| 338 |
+
species_df["占比(%)"] = (species_df["候选肽数量"] / total_count * 100).round(1)
|
| 339 |
+
|
| 340 |
+
col1, col2 = st.columns(2)
|
| 341 |
+
with col1:
|
| 342 |
+
st.dataframe(species_df[["物种", "候选肽数量", "占比(%)"]].head(20), width='stretch')
|
| 343 |
+
with col2:
|
| 344 |
+
st.bar_chart(species_df.set_index("物种")["候选肽数量"])
|
| 345 |
+
|
| 346 |
+
st.success("筛选完成!")
|
| 347 |
+
|
| 348 |
+
finally:
|
| 349 |
+
st.session_state.running = False
|