Compare commits

..

3 Commits

Author SHA1 Message Date
RYDE-WORK
c225fc67a7 Dedicated docker-compose.yml for gpu deployment 2026-01-26 11:11:52 +08:00
RYDE-WORK
3cce4c9373 Dockerize 2026-01-26 11:08:57 +08:00
RYDE-WORK
68119df128 Update app.py 2026-01-26 10:33:50 +08:00
7 changed files with 381 additions and 72 deletions

75
.dockerignore Normal file
View File

@ -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/

63
Dockerfile Normal file
View File

@ -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"]

View File

@ -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 #
#################################################################################

View File

@ -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",
@ -27,13 +32,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 +151,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 +190,7 @@ def main():
# ========== 侧边栏 ==========
with st.sidebar:
st.header("⚙️ 参数设置")
# st.header("⚙️ 参数设置")
# API 状态
if api_online:
@ -193,7 +199,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 +212,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 +234,7 @@ def main():
index=0,
)
st.divider()
# st.divider()
# 高级选项
with st.expander("🔧 高级选项"):
@ -294,8 +300,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 +339,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__":

54
docker-compose-gpu.yml Normal file
View File

@ -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

49
docker-compose.yml Normal file
View File

@ -0,0 +1,49 @@
services:
# FastAPI 后端服务
api:
build:
context: .
dockerfile: Dockerfile
target: api
container_name: lnp-api
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
# 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

20
requirements.txt Normal file
View File

@ -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