From 68119df128d01f870a239799b3aab06232f7d655 Mon Sep 17 00:00:00 2001 From: RYDE-WORK Date: Mon, 26 Jan 2026 10:33:50 +0800 Subject: [PATCH 1/3] Update app.py --- app/app.py | 143 +++++++++++++++++++++++++++-------------------------- 1 file changed, 72 insertions(+), 71 deletions(-) diff --git a/app/app.py b/app/app.py index 6b3a093..4ecb698 100644 --- a/app/app.py +++ b/app/app.py @@ -27,13 +27,13 @@ AVAILABLE_ORGANS = [ ] ORGAN_LABELS = { - "liver": "🫀 肝脏 (Liver)", - "spleen": "🟣 脾脏 (Spleen)", - "lung": "🫁 肺 (Lung)", - "heart": "❤️ 心脏 (Heart)", - "kidney": "🫘 肾脏 (Kidney)", - "muscle": "💪 肌肉 (Muscle)", - "lymph_nodes": "🔵 淋巴结 (Lymph Nodes)", + "liver": "肝脏 (Liver)", + "spleen": "脾脏 (Spleen)", + "lung": "肺 (Lung)", + "heart": "心脏 (Heart)", + "kidney": "肾脏 (Kidney)", + "muscle": "肌肉 (Muscle)", + "lymph_nodes": "淋巴结 (Lymph Nodes)", } # ============ 页面配置 ============ @@ -146,19 +146,20 @@ def format_results_dataframe(results: dict) -> pd.DataFrame: for f in formulations: row = { "排名": f["rank"], - f"Biodist_{target_organ}": f"{f['target_biodist']:.4f}", - "阳离子脂质/mRNA": f["cationic_lipid_to_mrna_ratio"], - "阳离子脂质(mol)": f["cationic_lipid_mol_ratio"], - "磷脂(mol)": f["phospholipid_mol_ratio"], - "胆固醇(mol)": f["cholesterol_mol_ratio"], - "PEG脂质(mol)": f["peg_lipid_mol_ratio"], + # f"{target_organ}分布": f"{f['target_biodist']*100:.2f}%", + f"{target_organ}分布": f"{f['target_biodist']*100:.8f}%", + "阳离子脂质/mRNA比例": f["cationic_lipid_to_mrna_ratio"], + "阳离子脂质(mol)比例": f["cationic_lipid_mol_ratio"], + "磷脂(mol)比例": f["phospholipid_mol_ratio"], + "胆固醇(mol)比例": f["cholesterol_mol_ratio"], + "PEG脂质(mol)比例": f["peg_lipid_mol_ratio"], "辅助脂质": f["helper_lipid"], "给药途径": f["route"], } # 添加其他器官的 biodist for organ, value in f["all_biodist"].items(): if organ != target_organ: - row[f"Biodist_{organ}"] = f"{value:.4f}" + row[f"{organ}分布"] = f"{value*100:.2f}%" rows.append(row) return pd.DataFrame(rows) @@ -184,7 +185,7 @@ def main(): # ========== 侧边栏 ========== with st.sidebar: - st.header("⚙️ 参数设置") + # st.header("⚙️ 参数设置") # API 状态 if api_online: @@ -193,7 +194,7 @@ def main(): st.error("🔴 API 服务离线") st.info("请先启动 API 服务:\n```\nuvicorn app.api:app --port 8000\n```") - st.divider() + # st.divider() # SMILES 输入 st.subheader("🔬 分子结构") @@ -206,18 +207,18 @@ def main(): ) # 示例 SMILES - with st.expander("📋 示例 SMILES"): - example_smiles = { - "DLin-MC3-DMA": "CC(C)=CCCC(C)=CCCC(C)=CCN(C)CCCCCCCCOC(=O)CCCCCCC/C=C\\CCCCCCCC", - "简单胺": "CC(C)NCCNC(C)C", - "长链胺": "CCCCCCCCCCCCNCCNCCCCCCCCCCCC", - } - for name, smi in example_smiles.items(): - if st.button(f"使用 {name}", key=f"example_{name}"): - st.session_state["smiles_input"] = smi - st.rerun() + # with st.expander("📋 示例 SMILES"): + # example_smiles = { + # "DLin-MC3-DMA": "CC(C)=CCCC(C)=CCCC(C)=CCN(C)CCCCCCCCOC(=O)CCCCCCC/C=C\\CCCCCCCC", + # "简单胺": "CC(C)NCCNC(C)C", + # "长链胺": "CCCCCCCCCCCCNCCNCCCCCCCCCCCC", + # } + # for name, smi in example_smiles.items(): + # if st.button(f"使用 {name}", key=f"example_{name}"): + # st.session_state["smiles_input"] = smi + # st.rerun() - st.divider() + # st.divider() # 目标器官选择 st.subheader("🎯 目标器官") @@ -228,7 +229,7 @@ def main(): index=0, ) - st.divider() + # st.divider() # 高级选项 with st.expander("🔧 高级选项"): @@ -294,8 +295,8 @@ def main(): with col2: best_score = results["formulations"][0]["target_biodist"] st.metric( - "最优 Biodistribution", - f"{best_score:.4f}", + "最优分布", + f"{best_score*100:.2f}%", ) with col3: @@ -333,61 +334,61 @@ def main(): ) # 详细信息 - with st.expander("🔍 查看最优配方详情"): - best = results["formulations"][0] + # with st.expander("🔍 查看最优配方详情"): + # best = results["formulations"][0] - col1, col2 = st.columns(2) + # col1, col2 = st.columns(2) - with col1: - st.markdown("**配方参数**") - st.json({ - "阳离子脂质/mRNA 比例": best["cationic_lipid_to_mrna_ratio"], - "阳离子脂质 (mol%)": best["cationic_lipid_mol_ratio"], - "磷脂 (mol%)": best["phospholipid_mol_ratio"], - "胆固醇 (mol%)": best["cholesterol_mol_ratio"], - "PEG 脂质 (mol%)": best["peg_lipid_mol_ratio"], - "辅助脂质": best["helper_lipid"], - "给药途径": best["route"], - }) + # with col1: + # st.markdown("**配方参数**") + # st.json({ + # "阳离子脂质/mRNA 比例": best["cationic_lipid_to_mrna_ratio"], + # "阳离子脂质 (mol%)": best["cationic_lipid_mol_ratio"], + # "磷脂 (mol%)": best["phospholipid_mol_ratio"], + # "胆固醇 (mol%)": best["cholesterol_mol_ratio"], + # "PEG 脂质 (mol%)": best["peg_lipid_mol_ratio"], + # "辅助脂质": best["helper_lipid"], + # "给药途径": best["route"], + # }) - with col2: - st.markdown("**各器官 Biodistribution 预测**") - biodist_df = pd.DataFrame([ - {"器官": ORGAN_LABELS.get(k, k), "Biodistribution": f"{v:.4f}"} - for k, v in best["all_biodist"].items() - ]) - st.dataframe(biodist_df, hide_index=True, use_container_width=True) + # with col2: + # st.markdown("**各器官 Biodistribution 预测**") + # biodist_df = pd.DataFrame([ + # {"器官": ORGAN_LABELS.get(k, k), "Biodistribution": f"{v:.4f}"} + # for k, v in best["all_biodist"].items() + # ]) + # st.dataframe(biodist_df, hide_index=True, use_container_width=True) else: # 欢迎信息 st.info("👈 请在左侧输入 SMILES 并选择目标器官,然后点击「开始配方优选」") # 使用说明 - with st.expander("📖 使用说明"): - st.markdown(""" - ### 如何使用 + # with st.expander("📖 使用说明"): + # st.markdown(""" + # ### 如何使用 - 1. **输入 SMILES**: 在左侧输入框中输入阳离子脂质的 SMILES 字符串 - 2. **选择目标器官**: 选择您希望优化的器官靶向 - 3. **点击优选**: 系统将自动搜索最优配方组合 - 4. **查看结果**: 右侧将显示 Top-20 优选配方 - 5. **导出数据**: 点击导出按钮将结果保存为 CSV 文件 + # 1. **输入 SMILES**: 在左侧输入框中输入阳离子脂质的 SMILES 字符串 + # 2. **选择目标器官**: 选择您希望优化的器官靶向 + # 3. **点击优选**: 系统将自动搜索最优配方组合 + # 4. **查看结果**: 右侧将显示 Top-20 优选配方 + # 5. **导出数据**: 点击导出按钮将结果保存为 CSV 文件 - ### 优化参数 + # ### 优化参数 - 系统会优化以下配方参数: - - **阳离子脂质/mRNA 比例**: 0.05 - 0.30 - - **阳离子脂质 mol 比例**: 0.05 - 0.80 - - **磷脂 mol 比例**: 0.00 - 0.80 - - **胆固醇 mol 比例**: 0.00 - 0.80 - - **PEG 脂质 mol 比例**: 0.00 - 0.05 - - **辅助脂质**: DOPE / DSPC / DOTAP - - **给药途径**: 静脉注射 / 肌肉注射 + # 系统会优化以下配方参数: + # - **阳离子脂质/mRNA 比例**: 0.05 - 0.30 + # - **阳离子脂质 mol 比例**: 0.05 - 0.80 + # - **磷脂 mol 比例**: 0.00 - 0.80 + # - **胆固醇 mol 比例**: 0.00 - 0.80 + # - **PEG 脂质 mol 比例**: 0.00 - 0.05 + # - **辅助脂质**: DOPE / DSPC / DOTAP + # - **给药途径**: 静脉注射 / 肌肉注射 - ### 约束条件 + # ### 约束条件 - mol 比例之和 = 1 (阳离子脂质 + 磷脂 + 胆固醇 + PEG 脂质) - """) + # mol 比例之和 = 1 (阳离子脂质 + 磷脂 + 胆固醇 + PEG 脂质) + # """) if __name__ == "__main__": From 3cce4c93735bcfb3bae709d2d18f2c527978f726 Mon Sep 17 00:00:00 2001 From: RYDE-WORK Date: Mon, 26 Jan 2026 11:08:57 +0800 Subject: [PATCH 2/3] Dockerize --- .dockerignore | 75 ++++++++++++++++++++++++++++++++++++++++++++++ Dockerfile | 63 ++++++++++++++++++++++++++++++++++++++ Makefile | 42 ++++++++++++++++++++++++++ app/app.py | 7 ++++- docker-compose.yml | 60 +++++++++++++++++++++++++++++++++++++ requirements.txt | 20 +++++++++++++ 6 files changed, 266 insertions(+), 1 deletion(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 requirements.txt diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e9992af --- /dev/null +++ b/.dockerignore @@ -0,0 +1,75 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +*.egg-info/ +.eggs/ +dist/ +build/ +*.egg + +# Virtual environments +.venv/ +venv/ +ENV/ +env/ +.pixi/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +.cursor/ + +# Git +.git/ +.gitignore + +# Data (不需要打包到镜像) +data/ +!data/.gitkeep + +# Notebooks +notebooks/ +*.ipynb + +# Documentation +docs/ + +# Reports +reports/ + +# References +references/ + +# Scripts (训练脚本不需要) +scripts/ + +# Lock files +pixi.lock + +# Tests +tests/ +.pytest_cache/ + +# Logs +*.log +logs/ + +# Temporary files +*.tmp +*.temp +.DS_Store + +# Models (will be mounted as volume or copied explicitly) +# Note: models/final/ is copied in Dockerfile +models/finetune_cv/ +models/pretrain_cv/ +models/mpnn/ +models/*.pt +models/*.json +!models/final/ + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..260ed56 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,63 @@ +# LNP-ML Docker Image +# 多阶段构建,支持 API 和 Streamlit 两种服务 + +FROM python:3.8-slim AS base + +# 设置环境变量 +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PIP_NO_CACHE_DIR=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 + +WORKDIR /app + +# 安装系统依赖 +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + libxrender1 \ + libxext6 \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# 复制依赖文件 +COPY requirements.txt . + +# 安装 Python 依赖 +RUN pip install --upgrade pip && \ + pip install -r requirements.txt + +# 复制项目代码 +COPY pyproject.toml . +COPY README.md . +COPY LICENSE . +COPY lnp_ml/ ./lnp_ml/ +COPY app/ ./app/ + +# 安装项目包 +RUN pip install -e . + +# 复制模型文件 +COPY models/final/ ./models/final/ + +# ============ API 服务 ============ +FROM base AS api + +EXPOSE 8000 + +ENV MODEL_PATH=/app/models/final/model.pt + +CMD ["uvicorn", "app.api:app", "--host", "0.0.0.0", "--port", "8000"] + +# ============ Streamlit 服务 ============ +FROM base AS streamlit + +EXPOSE 8501 + +# Streamlit 配置 +ENV STREAMLIT_SERVER_PORT=8501 \ + STREAMLIT_SERVER_ADDRESS=0.0.0.0 \ + STREAMLIT_SERVER_HEADLESS=true \ + STREAMLIT_BROWSER_GATHER_USAGE_STATS=false + +CMD ["streamlit", "run", "app/app.py"] + diff --git a/Makefile b/Makefile index 8d7182b..53aade8 100644 --- a/Makefile +++ b/Makefile @@ -200,6 +200,48 @@ serve: @echo "然后访问: http://localhost:8501" +################################################################################# +# DOCKER COMMANDS # +################################################################################# + +## Build Docker images +.PHONY: docker-build +docker-build: + docker compose build + +## Start all services with Docker Compose +.PHONY: docker-up +docker-up: + docker compose up -d + +## Stop all Docker services +.PHONY: docker-down +docker-down: + docker compose down + +## View Docker logs +.PHONY: docker-logs +docker-logs: + docker compose logs -f + +## Build and start all services +.PHONY: docker-serve +docker-serve: docker-build docker-up + @echo "" + @echo "🚀 服务已启动!" + @echo " - API: http://localhost:8000" + @echo " - Web 应用: http://localhost:8501" + @echo "" + @echo "查看日志: make docker-logs" + @echo "停止服务: make docker-down" + +## Clean Docker resources (images, volumes, etc.) +.PHONY: docker-clean +docker-clean: + docker compose down -v --rmi local + docker system prune -f + + ################################################################################# # Self Documenting Commands # ################################################################################# diff --git a/app/app.py b/app/app.py index 4ecb698..ffccca8 100644 --- a/app/app.py +++ b/app/app.py @@ -3,9 +3,13 @@ Streamlit 配方优化交互界面 启动应用: streamlit run app/app.py + +Docker 环境变量: + API_URL: API 服务地址 (默认: http://localhost:8000) """ import io +import os from datetime import datetime import httpx @@ -14,7 +18,8 @@ import streamlit as st # ============ 配置 ============ -API_URL = "http://localhost:8000" +# 从环境变量读取 API 地址,支持 Docker 环境 +API_URL = os.environ.get("API_URL", "http://localhost:8000") AVAILABLE_ORGANS = [ "liver", diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..83f0ec5 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,60 @@ +services: + # FastAPI 后端服务 + api: + build: + context: . + dockerfile: Dockerfile + target: api + container_name: lnp-api + # ports: + # - "8000:8000" + environment: + - MODEL_PATH=/app/models/final/model.pt + - DEVICE=cpu + volumes: + # 挂载模型目录以便更新模型 + - ./models/final:/app/models/final:ro + - ./models/mpnn:/app/models/mpnn:ro + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s + # 如果有 NVIDIA GPU,取消下面的注释 + # deploy: + # resources: + # reservations: + # devices: + # - driver: nvidia + # count: 1 + # capabilities: [gpu] + + # Streamlit 前端服务 + streamlit: + build: + context: . + dockerfile: Dockerfile + target: streamlit + container_name: lnp-streamlit + ports: + - "8501:8501" + environment: + # 连接到 API 服务(使用 Docker 内部网络) + - API_URL=http://api:8000 + depends_on: + api: + condition: service_started + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8501/_stcore/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + +networks: + default: + name: lnp-network + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..98176eb --- /dev/null +++ b/requirements.txt @@ -0,0 +1,20 @@ +# 严格遵循 pixi.toml 的依赖版本 +# 注意: lnp_ml 本地包在 Dockerfile 中单独安装 + +# conda dependencies (in pixi.toml [dependencies]) +loguru +tqdm +typer + +# pypi dependencies (in pixi.toml [pypi-dependencies]) +chemprop==1.7.0 +setuptools +pandas>=2.0.3,<3 +openpyxl>=3.1.5,<4 +python-dotenv>=1.0.1,<2 +pyarrow>=17.0.0,<18 +fastparquet>=2024.2.0,<2025 +fastapi>=0.124.4,<0.125 +streamlit>=1.40.1,<2 +httpx>=0.28.1,<0.29 +uvicorn>=0.33.0,<0.34 From c225fc67a7fb25e40cee573c624ad0fca11c3fb9 Mon Sep 17 00:00:00 2001 From: RYDE-WORK Date: Mon, 26 Jan 2026 11:11:52 +0800 Subject: [PATCH 3/3] Dedicated docker-compose.yml for gpu deployment --- docker-compose-gpu.yml | 54 ++++++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 11 --------- 2 files changed, 54 insertions(+), 11 deletions(-) create mode 100644 docker-compose-gpu.yml diff --git a/docker-compose-gpu.yml b/docker-compose-gpu.yml new file mode 100644 index 0000000..01cc47b --- /dev/null +++ b/docker-compose-gpu.yml @@ -0,0 +1,54 @@ +services: + # FastAPI 后端服务 + api: + build: + context: . + dockerfile: Dockerfile + target: api + container_name: lnp-api + environment: + - MODEL_PATH=/app/models/final/model.pt + volumes: + # 挂载模型目录以便更新模型 + - ./models/final:/app/models/final:ro + - ./models/mpnn:/app/models/mpnn:ro + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: 1 + capabilities: [gpu] + + # Streamlit 前端服务 + streamlit: + build: + context: . + dockerfile: Dockerfile + target: streamlit + container_name: lnp-streamlit + ports: + - "8501:8501" + environment: + - API_URL=http://api:8000 + depends_on: + api: + condition: service_started + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8501/_stcore/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + +networks: + default: + name: lnp-network diff --git a/docker-compose.yml b/docker-compose.yml index 83f0ec5..9185138 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,8 +6,6 @@ services: dockerfile: Dockerfile target: api container_name: lnp-api - # ports: - # - "8000:8000" environment: - MODEL_PATH=/app/models/final/model.pt - DEVICE=cpu @@ -22,14 +20,6 @@ services: timeout: 10s retries: 3 start_period: 60s - # 如果有 NVIDIA GPU,取消下面的注释 - # deploy: - # resources: - # reservations: - # devices: - # - driver: nvidia - # count: 1 - # capabilities: [gpu] # Streamlit 前端服务 streamlit: @@ -41,7 +31,6 @@ services: ports: - "8501:8501" environment: - # 连接到 API 服务(使用 Docker 内部网络) - API_URL=http://api:8000 depends_on: api: