Add files using upload-large-folder tool
Browse files- Biomni/.env.example +22 -0
- Biomni/.gitignore +234 -0
- Biomni/.pre-commit-config.yaml +41 -0
- Biomni/DETAILS.md +364 -0
- Biomni/LICENSE +201 -0
- Biomni/MANIFEST.in +27 -0
- Biomni/Q13398_ChIP-seq_Meta-clusters.bb +0 -0
- Biomni/biomni/__init__.py +3 -0
- Biomni/biomni/agent/__init__.py +1 -0
- Biomni/biomni/agent/__pycache__/__init__.cpython-311.pyc +0 -0
- Biomni/biomni/agent/a1.py +0 -0
- Biomni/biomni/agent/env_collection.py +313 -0
- Biomni/biomni/agent/function_generator.py +119 -0
- Biomni/biomni/agent/qa_llm.py +50 -0
- Biomni/biomni/agent/react.py +465 -0
- Biomni/biomni/config.py +99 -0
- Biomni/biomni/env_desc.py +221 -0
- Biomni/biomni/env_desc_cm.py +219 -0
- Biomni/biomni/eval/__init__.py +3 -0
- Biomni/biomni/llm.py +275 -0
- Biomni/biomni/task/__init__.py +0 -0
- Biomni/biomni/task/base_task.py +18 -0
- Biomni/biomni/task/hle.py +146 -0
- Biomni/biomni/task/lab_bench.py +115 -0
- Biomni/biomni/tool/__init__.py +1 -0
- Biomni/biomni/tool/biochemistry.py +1026 -0
- Biomni/biomni/tool/bioimaging.py +1394 -0
- Biomni/biomni/tool/biophysics.py +513 -0
- Biomni/biomni/tool/cell_biology.py +742 -0
- Biomni/biomni/tool/database.py +0 -0
- Biomni/biomni/tool/example_mcp_tools/pubmed_mcp.py +38 -0
- Biomni/biomni/tool/genetics.py +1672 -0
- Biomni/biomni/tool/literature.py +403 -0
- Biomni/biomni/tool/pathology.py +1034 -0
- Biomni/biomni/tool/protocols/thermofisher/Cell Freezing Protocols _ Thermo Fisher Scientific - US.txt +105 -0
- Biomni/biomni/tool/synthetic_biology.py +1351 -0
- Biomni/biomni/tool/systems_biology.py +926 -0
- Biomni/biomni/tool/tool_registry.py +89 -0
- Biomni/biomni/utils.py +2469 -0
- Biomni/biomni/version.py +1 -0
- Biomni/license_info.md +52 -0
- Biomni/pyproject.toml +105 -0
- cloningscenarios_all_models_cloning_20260524_163903.log +7 -0
- ollama.log +13 -0
- rerun_base_deepseek_cloningscenarios_50shards.py +153 -0
- rerun_base_deepseek_cloningscenarios_50shards.sh +80 -0
- run_base_openai_labbench_three_tasks_50shards.sh +127 -0
- run_biomni_nomcp_cloningscenarios.sh +108 -0
- run_cloningscenarios_all_models.sh +232 -0
- run_cloningscenarios_reactcode_and_base_50shards.sh +132 -0
Biomni/.env.example
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Biomni Environment Configuration
|
| 2 |
+
# Copy this file to .env and fill in your actual API keys
|
| 3 |
+
|
| 4 |
+
# Required: Anthropic API Key for Claude models
|
| 5 |
+
ANTHROPIC_API_KEY=your_anthropic_api_key_here
|
| 6 |
+
|
| 7 |
+
# Optional: OpenAI API Key (if using OpenAI models)
|
| 8 |
+
OPENAI_API_KEY=your_openai_api_key_here
|
| 9 |
+
|
| 10 |
+
# Optional: AWS Bedrock Configuration (if using AWS Bedrock models)
|
| 11 |
+
AWS_BEARER_TOKEN_BEDROCK=your_bedrock_api_key_here
|
| 12 |
+
AWS_REGION=us-east-1
|
| 13 |
+
|
| 14 |
+
# Optional: Custom model serving configuration
|
| 15 |
+
# CUSTOM_MODEL_BASE_URL=http://localhost:8000/v1
|
| 16 |
+
# CUSTOM_MODEL_API_KEY=your_custom_api_key_here
|
| 17 |
+
|
| 18 |
+
# Optional: Biomni data path (defaults to ./data)
|
| 19 |
+
# BIOMNI_DATA_PATH=/path/to/your/data
|
| 20 |
+
|
| 21 |
+
# Optional: Timeout settings
|
| 22 |
+
# BIOMNI_TIMEOUT_SECONDS=600
|
Biomni/.gitignore
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.gitignore
|
| 2 |
+
.buildplan
|
| 3 |
+
.env
|
| 4 |
+
debug*
|
| 5 |
+
.*
|
| 6 |
+
# Byte-compiled / optimized / DLL files
|
| 7 |
+
biomni_release/biomni_env/biomni_tools/
|
| 8 |
+
open_source_process.ipynb
|
| 9 |
+
biomni_env/biomni_tools/*
|
| 10 |
+
.gradio/
|
| 11 |
+
*xlsx
|
| 12 |
+
scripts/*.png
|
| 13 |
+
notebooks/figures/*
|
| 14 |
+
notebooks/tmp_dir/*
|
| 15 |
+
notebooks/tmp_directory/*
|
| 16 |
+
notebooks/save_folder/*
|
| 17 |
+
notebooks/cache
|
| 18 |
+
notebooks/\[draft*
|
| 19 |
+
notebooks/\[scratch*
|
| 20 |
+
notebooks/labbench/*
|
| 21 |
+
scripts/save_folder/*
|
| 22 |
+
scripts/\[draft*
|
| 23 |
+
scripts/tmp_dir/*
|
| 24 |
+
scripts/tmp_directory/*
|
| 25 |
+
scripts/figures/*
|
| 26 |
+
*/scripts/outputs/*
|
| 27 |
+
*/scripts/wandb/*
|
| 28 |
+
*/results/*
|
| 29 |
+
bioagentos/agent/popper_agent.py
|
| 30 |
+
bioagentos/scripts/biorxiv_results
|
| 31 |
+
bioagentos/tool/data_tool/old_data_tool/*
|
| 32 |
+
examples/*.png
|
| 33 |
+
examples/*.csv
|
| 34 |
+
examples/*.fa
|
| 35 |
+
scratch/
|
| 36 |
+
packages/
|
| 37 |
+
data
|
| 38 |
+
dataset
|
| 39 |
+
logs
|
| 40 |
+
scripts/gradio
|
| 41 |
+
data_lake
|
| 42 |
+
__pycache__/
|
| 43 |
+
*.py[cod]
|
| 44 |
+
*$py.class
|
| 45 |
+
res/
|
| 46 |
+
examples/tmp*
|
| 47 |
+
bioagentos/task/lab_bench.py
|
| 48 |
+
*database/
|
| 49 |
+
.history/
|
| 50 |
+
|
| 51 |
+
temp.*
|
| 52 |
+
|
| 53 |
+
scripts/analysis/all_tools.txt
|
| 54 |
+
|
| 55 |
+
BIOMNI_GRADIO/biomni/tool/files/*
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
# C extensions
|
| 59 |
+
*.so
|
| 60 |
+
|
| 61 |
+
# Distribution / packaging
|
| 62 |
+
.Python
|
| 63 |
+
build/
|
| 64 |
+
develop-eggs/
|
| 65 |
+
dist/
|
| 66 |
+
downloads/
|
| 67 |
+
eggs/
|
| 68 |
+
.eggs/
|
| 69 |
+
lib/
|
| 70 |
+
lib64/
|
| 71 |
+
parts/
|
| 72 |
+
sdist/
|
| 73 |
+
var/
|
| 74 |
+
wheels/
|
| 75 |
+
share/python-wheels/
|
| 76 |
+
*.egg-info/
|
| 77 |
+
.installed.cfg
|
| 78 |
+
*.egg
|
| 79 |
+
MANIFEST
|
| 80 |
+
|
| 81 |
+
# PyInstaller
|
| 82 |
+
# Usually these files are written by a python script from a template
|
| 83 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
| 84 |
+
*.manifest
|
| 85 |
+
*.spec
|
| 86 |
+
|
| 87 |
+
# Installer logs
|
| 88 |
+
pip-log.txt
|
| 89 |
+
pip-delete-this-directory.txt
|
| 90 |
+
|
| 91 |
+
# Unit test / coverage reports
|
| 92 |
+
htmlcov/
|
| 93 |
+
.tox/
|
| 94 |
+
.nox/
|
| 95 |
+
.coverage
|
| 96 |
+
.coverage.*
|
| 97 |
+
.cache
|
| 98 |
+
nosetests.xml
|
| 99 |
+
coverage.xml
|
| 100 |
+
*.cover
|
| 101 |
+
*.py,cover
|
| 102 |
+
.hypothesis/
|
| 103 |
+
.pytest_cache/
|
| 104 |
+
cover/
|
| 105 |
+
test.ipynb
|
| 106 |
+
|
| 107 |
+
# Translations
|
| 108 |
+
*.mo
|
| 109 |
+
*.pot
|
| 110 |
+
|
| 111 |
+
# Django stuff:
|
| 112 |
+
*.log
|
| 113 |
+
local_settings.py
|
| 114 |
+
db.sqlite3
|
| 115 |
+
db.sqlite3-journal
|
| 116 |
+
|
| 117 |
+
# Flask stuff:
|
| 118 |
+
instance/
|
| 119 |
+
.webassets-cache
|
| 120 |
+
|
| 121 |
+
# Scrapy stuff:
|
| 122 |
+
.scrapy
|
| 123 |
+
|
| 124 |
+
# Sphinx documentation
|
| 125 |
+
docs/_build/
|
| 126 |
+
|
| 127 |
+
# PyBuilder
|
| 128 |
+
.pybuilder/
|
| 129 |
+
target/
|
| 130 |
+
|
| 131 |
+
# Jupyter Notebook
|
| 132 |
+
.ipynb_checkpoints
|
| 133 |
+
|
| 134 |
+
# IPython
|
| 135 |
+
profile_default/
|
| 136 |
+
ipython_config.py
|
| 137 |
+
|
| 138 |
+
# pyenv
|
| 139 |
+
# For a library or package, you might want to ignore these files since the code is
|
| 140 |
+
# intended to run in multiple environments; otherwise, check them in:
|
| 141 |
+
# .python-version
|
| 142 |
+
|
| 143 |
+
# pipenv
|
| 144 |
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
| 145 |
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
| 146 |
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
| 147 |
+
# install all needed dependencies.
|
| 148 |
+
#Pipfile.lock
|
| 149 |
+
|
| 150 |
+
# poetry
|
| 151 |
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
| 152 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
| 153 |
+
# commonly ignored for libraries.
|
| 154 |
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
| 155 |
+
#poetry.lock
|
| 156 |
+
|
| 157 |
+
# pdm
|
| 158 |
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
| 159 |
+
#pdm.lock
|
| 160 |
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
| 161 |
+
# in version control.
|
| 162 |
+
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
| 163 |
+
.pdm.toml
|
| 164 |
+
.pdm-python
|
| 165 |
+
.pdm-build/
|
| 166 |
+
|
| 167 |
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
| 168 |
+
__pypackages__/
|
| 169 |
+
|
| 170 |
+
# Celery stuff
|
| 171 |
+
celerybeat-schedule
|
| 172 |
+
celerybeat.pid
|
| 173 |
+
|
| 174 |
+
# SageMath parsed files
|
| 175 |
+
*.sage.py
|
| 176 |
+
|
| 177 |
+
# Environments
|
| 178 |
+
.env
|
| 179 |
+
.venv
|
| 180 |
+
env/
|
| 181 |
+
venv/
|
| 182 |
+
ENV/
|
| 183 |
+
env.bak/
|
| 184 |
+
venv.bak/
|
| 185 |
+
.envrc
|
| 186 |
+
uv.lock
|
| 187 |
+
|
| 188 |
+
# Spyder project settings
|
| 189 |
+
.spyderproject
|
| 190 |
+
.spyproject
|
| 191 |
+
|
| 192 |
+
# Rope project settings
|
| 193 |
+
.ropeproject
|
| 194 |
+
|
| 195 |
+
# mkdocs documentation
|
| 196 |
+
/site
|
| 197 |
+
|
| 198 |
+
# mypy
|
| 199 |
+
.mypy_cache/
|
| 200 |
+
.dmypy.json
|
| 201 |
+
dmypy.json
|
| 202 |
+
|
| 203 |
+
*.jsonl
|
| 204 |
+
|
| 205 |
+
# Pyre type checker
|
| 206 |
+
.pyre/
|
| 207 |
+
|
| 208 |
+
# pytype static type analyzer
|
| 209 |
+
.pytype/
|
| 210 |
+
|
| 211 |
+
# Cython debug symbols
|
| 212 |
+
cython_debug/
|
| 213 |
+
|
| 214 |
+
# macOS system files
|
| 215 |
+
.DS_Store
|
| 216 |
+
**/.DS_Store
|
| 217 |
+
|
| 218 |
+
# DDInter data files
|
| 219 |
+
biomni/tool/schema_db/ddinter_*.pkl
|
| 220 |
+
data/ddinter_raw/
|
| 221 |
+
|
| 222 |
+
# PyCharm
|
| 223 |
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
| 224 |
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
| 225 |
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
| 226 |
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
| 227 |
+
#.idea/
|
| 228 |
+
|
| 229 |
+
# VSCode
|
| 230 |
+
/.vscode
|
| 231 |
+
|
| 232 |
+
# Sphinx build
|
| 233 |
+
/docs/build/
|
| 234 |
+
/docs/source/api/
|
Biomni/.pre-commit-config.yaml
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fail_fast: false
|
| 2 |
+
default_language_version:
|
| 3 |
+
python: python3
|
| 4 |
+
default_stages:
|
| 5 |
+
- pre-commit
|
| 6 |
+
- pre-push
|
| 7 |
+
minimum_pre_commit_version: 2.16.0
|
| 8 |
+
repos:
|
| 9 |
+
- repo: https://github.com/biomejs/pre-commit
|
| 10 |
+
rev: v2.3.10
|
| 11 |
+
hooks:
|
| 12 |
+
- id: biome-format
|
| 13 |
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
| 14 |
+
rev: v0.14.11
|
| 15 |
+
hooks:
|
| 16 |
+
- id: ruff-check
|
| 17 |
+
args: [--fix, --exit-non-zero-on-fix, --unsafe-fixes]
|
| 18 |
+
- id: ruff-format
|
| 19 |
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
| 20 |
+
rev: v6.0.0
|
| 21 |
+
hooks:
|
| 22 |
+
- id: detect-private-key
|
| 23 |
+
- id: check-ast
|
| 24 |
+
- id: end-of-file-fixer
|
| 25 |
+
- id: mixed-line-ending
|
| 26 |
+
args: [--fix=lf]
|
| 27 |
+
- id: trailing-whitespace
|
| 28 |
+
- id: check-case-conflict
|
| 29 |
+
- id: check-added-large-files
|
| 30 |
+
- id: check-toml
|
| 31 |
+
- id: check-yaml
|
| 32 |
+
- id: check-merge-conflict
|
| 33 |
+
- id: no-commit-to-branch
|
| 34 |
+
args: ["--branch=main"]
|
| 35 |
+
#- repo: https://github.com/pre-commit/mirrors-mypy
|
| 36 |
+
# rev: v1.16.1
|
| 37 |
+
# hooks:
|
| 38 |
+
# - id: mypy
|
| 39 |
+
# args: [--no-strict-optional, --ignore-missing-imports]
|
| 40 |
+
# additional_dependencies:
|
| 41 |
+
# ["types-setuptools", "types-requests", "types-attrs"]
|
Biomni/DETAILS.md
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# DETAILS.md
|
| 2 |
+
|
| 3 |
+
🔍 **Powered by [Detailer](https://detailer.ginylil.com)** - Context-aware codebase analysis
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
## 1. Project Overview
|
| 10 |
+
|
| 11 |
+
### Project Purpose & Domain
|
| 12 |
+
|
| 13 |
+
This project is a comprehensive **biomedical AI toolkit and research platform** designed to facilitate **biomedical data analysis, knowledge extraction, and AI-driven reasoning**. It integrates large language models (LLMs), domain-specific bioinformatics tools, and scientific data processing pipelines to enable:
|
| 14 |
+
|
| 15 |
+
- Automated extraction of biomedical knowledge from literature (e.g., bioRxiv papers)
|
| 16 |
+
- Querying and integration of diverse biomedical databases and APIs
|
| 17 |
+
- Execution of domain-specific computational biology and physiology analyses
|
| 18 |
+
- AI agent orchestration for complex biomedical reasoning and tool invocation
|
| 19 |
+
- Benchmarking and evaluation of biomedical tasks and datasets
|
| 20 |
+
|
| 21 |
+
### Target Users and Use Cases
|
| 22 |
+
|
| 23 |
+
- **Biomedical researchers and data scientists** seeking to automate literature mining, data retrieval, and analysis workflows.
|
| 24 |
+
- **Bioinformaticians** requiring integrated access to multiple biological databases and computational tools.
|
| 25 |
+
- **AI researchers** interested in applying LLMs and autonomous agents to biomedical problem solving.
|
| 26 |
+
- **Developers and integrators** building domain-specific AI pipelines and scientific workflows.
|
| 27 |
+
- Use cases include:
|
| 28 |
+
- Extracting structured biomedical tasks and entities from scientific papers
|
| 29 |
+
- Querying gene, protein, disease, and pathway databases via natural language prompts
|
| 30 |
+
- Running computational models of biological systems (e.g., metabolic networks, signaling)
|
| 31 |
+
- Performing image analysis and quantitative pathology workflows
|
| 32 |
+
- Orchestrating multi-step AI reasoning with tool use and self-criticism
|
| 33 |
+
|
| 34 |
+
### Core Business Logic and Domain Models
|
| 35 |
+
|
| 36 |
+
- **Biomedical domain models**: gene IDs, protein structures, pathways, disease ontologies, experimental assays.
|
| 37 |
+
- **Task abstractions**: benchmark tasks with prompt/response evaluation (e.g., humanity last exam, lab bench).
|
| 38 |
+
- **Tool metadata schemas**: declarative descriptions of biomedical tools and APIs for dynamic invocation.
|
| 39 |
+
- **AI agent workflows**: ReAct-style reasoning graphs integrating LLMs, tool calls, retrieval, and self-critique.
|
| 40 |
+
- **Data models**: structured JSON, pandas DataFrames, numpy arrays representing biological data and analysis results.
|
| 41 |
+
|
| 42 |
+
---
|
| 43 |
+
|
| 44 |
+
## 2. Architecture and Structure
|
| 45 |
+
|
| 46 |
+
### High-Level Architecture
|
| 47 |
+
|
| 48 |
+
The system is organized into modular layers and components:
|
| 49 |
+
|
| 50 |
+
- **Core Library (`biomni/`)**: Contains main application logic, including:
|
| 51 |
+
- **Agent framework (`biomni/agent/`)**: Implements autonomous AI agents using LLMs and workflow graphs.
|
| 52 |
+
- **Task definitions (`biomni/task/`)**: Abstract base and concrete biomedical benchmark tasks.
|
| 53 |
+
- **Tool implementations (`biomni/tool/`)**: Domain-specific analysis functions, API clients, and computational biology workflows.
|
| 54 |
+
- **Tool metadata (`biomni/tool/tool_description/`)**: Declarative schemas describing tool APIs and parameters.
|
| 55 |
+
- **Model components (`biomni/model/`)**: AI-driven resource retriever for selecting relevant tools and data.
|
| 56 |
+
- **Utility modules (`biomni/utils.py`, `biomni/llm.py`, `biomni/env_desc.py`)**: Helpers for LLM instantiation, system commands, environment descriptions.
|
| 57 |
+
- **Versioning (`biomni/version.py`)**: Package version management.
|
| 58 |
+
|
| 59 |
+
- **Environment Setup (`biomni_env/`)**: Scripts and configuration files for reproducible environment provisioning, including:
|
| 60 |
+
- Conda environment YAMLs (`environment.yml`, `bio_env.yml`)
|
| 61 |
+
- R package specifications (`r_packages.yml`)
|
| 62 |
+
- CLI tools installer (`install_cli_tools.sh`)
|
| 63 |
+
- Shell scripts for environment setup (`setup.sh`, `setup_path.sh`)
|
| 64 |
+
|
| 65 |
+
- **Scripts (`biomni/biorxiv_scripts/`)**: Data processing pipelines for literature mining and task extraction.
|
| 66 |
+
|
| 67 |
+
- **Documentation and Configuration**:
|
| 68 |
+
- Root-level files: `README.md`, `CONTRIBUTION.md`, `pyproject.toml`, `.pre-commit-config.yaml`.
|
| 69 |
+
|
| 70 |
+
---
|
| 71 |
+
|
| 72 |
+
### Complete Repository Structure
|
| 73 |
+
|
| 74 |
+
```
|
| 75 |
+
.
|
| 76 |
+
├── biomni/ (90 items)
|
| 77 |
+
│ ├── agent/
|
| 78 |
+
│ │ ├── __init__.py
|
| 79 |
+
│ │ ├── a1.py
|
| 80 |
+
│ │ ├── env_collection.py
|
| 81 |
+
│ │ ├── qa_llm.py
|
| 82 |
+
│ │ └── react.py
|
| 83 |
+
│ ├── biorxiv_scripts/
|
| 84 |
+
│ │ ├── extract_biorxiv_tasks.py
|
| 85 |
+
│ │ ├── generate_function.py
|
| 86 |
+
│ │ └── process_all_subjects.py
|
| 87 |
+
│ ├── model/
|
| 88 |
+
│ │ ├── __init__.py
|
| 89 |
+
│ │ └── retriever.py
|
| 90 |
+
│ ├── task/
|
| 91 |
+
│ │ ├── __init__.py
|
| 92 |
+
│ │ ├── base_task.py
|
| 93 |
+
│ │ ├── hle.py
|
| 94 |
+
│ │ └── lab_bench.py
|
| 95 |
+
│ ├── tool/ (65 items)
|
| 96 |
+
│ │ ├── schema_db/ (25 items)
|
| 97 |
+
│ │ │ ├── cbioportal.pkl
|
| 98 |
+
│ │ │ ├── clinvar.pkl
|
| 99 |
+
│ │ │ ├── dbsnp.pkl
|
| 100 |
+
│ │ │ ├── emdb.pkl
|
| 101 |
+
│ │ │ ├── ensembl.pkl
|
| 102 |
+
│ │ │ ├── geo.pkl
|
| 103 |
+
│ │ │ ├── gnomad.pkl
|
| 104 |
+
│ │ │ ├── gtopdb.pkl
|
| 105 |
+
│ │ │ ├── gwas_catalog.pkl
|
| 106 |
+
│ │ │ ├── interpro.pkl
|
| 107 |
+
│ │ │ └── ... (15 more files)
|
| 108 |
+
│ │ ├── tool_description/ (18 items)
|
| 109 |
+
│ │ │ ├── biochemistry.py
|
| 110 |
+
│ │ │ ├── bioengineering.py
|
| 111 |
+
│ │ │ ├── biophysics.py
|
| 112 |
+
│ │ │ ├── cancer_biology.py
|
| 113 |
+
│ │ │ ├── cell_biology.py
|
| 114 |
+
│ │ │ ├── database.py
|
| 115 |
+
│ │ │ ├── genetics.py
|
| 116 |
+
│ │ │ ├── genomics.py
|
| 117 |
+
│ │ │ ├── immunology.py
|
| 118 |
+
│ │ │ ├── literature.py
|
| 119 |
+
│ │ │ ├── microbiology.py
|
| 120 |
+
│ │ │ ├── molecular_biology.py
|
| 121 |
+
│ │ │ ├── pathology.py
|
| 122 |
+
│ │ │ ├── pharmacology.py
|
| 123 |
+
│ │ │ ├── physiology.py
|
| 124 |
+
│ │ │ ├── support_tools.py
|
| 125 |
+
│ │ │ ├── synthetic_biology.py
|
| 126 |
+
│ │ │ └── systems_biology.py
|
| 127 |
+
│ │ ├── __init__.py
|
| 128 |
+
│ │ ├── biochemistry.py
|
| 129 |
+
│ │ ├── bioengineering.py
|
| 130 |
+
│ │ ├── biophysics.py
|
| 131 |
+
│ │ ├── cancer_biology.py
|
| 132 |
+
│ │ ├── cell_biology.py
|
| 133 |
+
│ │ ├── database.py
|
| 134 |
+
│ │ ├── genetics.py
|
| 135 |
+
│ │ └── ... (12 more files)
|
| 136 |
+
│ ├── __init__.py
|
| 137 |
+
│ ├── env_desc.py
|
| 138 |
+
│ ├── llm.py
|
| 139 |
+
│ ├── utils.py
|
| 140 |
+
│ └── version.py
|
| 141 |
+
├── biomni_env/ (9 items)
|
| 142 |
+
│ ├── README.md
|
| 143 |
+
│ ├── bio_env.yml
|
| 144 |
+
│ ├── cli_tools_config.json
|
| 145 |
+
│ ├── environment.yml
|
| 146 |
+
│ ├── install_cli_tools.sh
|
| 147 |
+
│ ├── install_r_packages.R
|
| 148 |
+
│ ├── r_packages.yml
|
| 149 |
+
│ ├── setup.sh
|
| 150 |
+
│ └── setup_path.sh
|
| 151 |
+
├── figs/
|
| 152 |
+
│ └── biomni_logo.png
|
| 153 |
+
├── tutorials/
|
| 154 |
+
│ ├── examples/
|
| 155 |
+
│ │ └── cloning.ipynb
|
| 156 |
+
│ ├── 101_biomni.ipynb
|
| 157 |
+
│ └── biomni_101.ipynb
|
| 158 |
+
├── .gitignore
|
| 159 |
+
├── .pre-commit-config.yaml
|
| 160 |
+
├── CONTRIBUTION.md
|
| 161 |
+
├── LICENSE
|
| 162 |
+
├── README.md
|
| 163 |
+
└── pyproject.toml
|
| 164 |
+
```
|
| 165 |
+
|
| 166 |
+
---
|
| 167 |
+
|
| 168 |
+
## 3. Technical Implementation Details
|
| 169 |
+
|
| 170 |
+
### Core Modules and Their Roles
|
| 171 |
+
|
| 172 |
+
#### `biomni/agent/`
|
| 173 |
+
|
| 174 |
+
- Implements autonomous AI agents using the **ReAct paradigm**:
|
| 175 |
+
- `react.py`: Main ReAct agent class managing reasoning, tool invocation, retrieval, and self-criticism workflows.
|
| 176 |
+
- `env_collection.py`: Environment and data retrieval utilities.
|
| 177 |
+
- `qa_llm.py`: Question-answering LLM wrappers.
|
| 178 |
+
- `a1.py`: Possibly experimental or auxiliary agent code.
|
| 179 |
+
|
| 180 |
+
- Uses **langgraph** for workflow graph orchestration and **langchain** for LLM integration.
|
| 181 |
+
|
| 182 |
+
#### `biomni/task/`
|
| 183 |
+
|
| 184 |
+
- Defines **benchmark tasks** with a common interface:
|
| 185 |
+
- `base_task.py`: Abstract base class specifying methods like `get_example()`, `evaluate()`, `output_class()`.
|
| 186 |
+
- `hle.py`: "Humanity Last Exam" task implementation.
|
| 187 |
+
- `lab_bench.py`: Lab bench dataset task.
|
| 188 |
+
|
| 189 |
+
- Tasks load data (e.g., parquet files), generate prompts, and evaluate LLM responses.
|
| 190 |
+
|
| 191 |
+
#### `biomni/tool/`
|
| 192 |
+
|
| 193 |
+
- Contains **domain-specific scientific analysis functions** organized by subdomains:
|
| 194 |
+
- `biochemistry.py`, `bioengineering.py`, `biophysics.py`, `cancer_biology.py`, `cell_biology.py`, `genetics.py`, `pathology.py`, `physiology.py`, `systems_biology.py`, etc.
|
| 195 |
+
- Each file implements multiple functions performing analyses, simulations, or data processing workflows.
|
| 196 |
+
- Functions accept input files/parameters and return detailed textual logs and output files.
|
| 197 |
+
|
| 198 |
+
- **API client modules** (e.g., `database.py`) provide facade functions to query external biomedical databases (UniProt, GWAS Catalog, Ensembl, etc.) via REST or GraphQL APIs, often using LLMs to generate query payloads from natural language prompts.
|
| 199 |
+
|
| 200 |
+
- **Tool registry (`tool_registry.py`)** manages metadata about available tools, supporting dynamic registration and lookup.
|
| 201 |
+
|
| 202 |
+
#### `biomni/tool/tool_description/`
|
| 203 |
+
|
| 204 |
+
- Contains **declarative metadata schemas** describing tool APIs:
|
| 205 |
+
- Each file exports a `description` list of dictionaries defining tool names, descriptions, required and optional parameters with types and defaults.
|
| 206 |
+
- Supports **dynamic API generation, validation, and documentation**.
|
| 207 |
+
- Organized by biological domain (e.g., genetics, immunology, pathology).
|
| 208 |
+
|
| 209 |
+
#### `biomni/model/retriever.py`
|
| 210 |
+
|
| 211 |
+
- Implements `ToolRetriever` class for **AI-driven resource selection**:
|
| 212 |
+
- Uses LLMs (OpenAI or Anthropic) to parse user queries and select relevant tools, datasets, and libraries.
|
| 213 |
+
- Encapsulates prompt formatting and response parsing logic.
|
| 214 |
+
|
| 215 |
+
#### `biomni/utils.py` and `biomni/llm.py`
|
| 216 |
+
|
| 217 |
+
- `utils.py`: Utility functions for running system commands (R, Bash), file operations, schema generation, logging, and colorized printing.
|
| 218 |
+
- `llm.py`: Factory functions to instantiate LLMs (OpenAI, Anthropic) with configurable parameters.
|
| 219 |
+
|
| 220 |
+
#### `biomni/env_desc.py`
|
| 221 |
+
|
| 222 |
+
- Contains **environment and dataset descriptions**, acting as a centralized metadata repository for datasets and experimental environments.
|
| 223 |
+
|
| 224 |
+
---
|
| 225 |
+
|
| 226 |
+
### Environment Setup (`biomni_env/`)
|
| 227 |
+
|
| 228 |
+
- `setup.sh`: Main shell script to create conda environment, install R packages, and CLI bioinformatics tools.
|
| 229 |
+
- `install_cli_tools.sh`: Automates downloading, compiling, and installing external bioinformatics command-line tools, managing PATH and verification.
|
| 230 |
+
- `r_packages.yml`: Lists R packages required.
|
| 231 |
+
- `environment.yml` and `bio_env.yml`: Conda environment specifications.
|
| 232 |
+
- `setup_path.sh`: Shell script to update environment variables for CLI tools.
|
| 233 |
+
|
| 234 |
+
---
|
| 235 |
+
|
| 236 |
+
### Entry Points and Execution Flow
|
| 237 |
+
|
| 238 |
+
- **Agent usage**: Instantiate `react` agent from `biomni.agent.react`, configure with tools and retrieval, then call `go(prompt)` to run reasoning workflows.
|
| 239 |
+
- **Task evaluation**: Use classes in `biomni.task` to load datasets, generate prompts, and evaluate LLM outputs.
|
| 240 |
+
- **Tool invocation**: Call functions in `biomni.tool` modules or use API facades in `database.py` to query external resources.
|
| 241 |
+
- **Metadata-driven tool discovery**: Use `tool_registry.py` and `tool_description` schemas to dynamically discover and validate tools.
|
| 242 |
+
- **Environment setup**: Run `biomni_env/setup.sh` to provision environment and install dependencies.
|
| 243 |
+
|
| 244 |
+
---
|
| 245 |
+
|
| 246 |
+
## 4. Development Patterns and Standards
|
| 247 |
+
|
| 248 |
+
### Code Organization Principles
|
| 249 |
+
|
| 250 |
+
- **Modular design**: Clear separation of concerns by domain and functionality (agent, task, tool, model).
|
| 251 |
+
- **Functional programming style**: Most analysis modules use standalone functions with explicit inputs and outputs.
|
| 252 |
+
- **Declarative metadata**: Tool descriptions and schemas are separated from implementation, enabling dynamic validation and UI generation.
|
| 253 |
+
- **Abstract base classes**: Used in `biomni.task.base_task` to enforce consistent task interfaces.
|
| 254 |
+
- **Factory pattern**: Used in `llm.py` to instantiate LLMs based on configuration.
|
| 255 |
+
- **Strategy pattern**: Task implementations and tool retrieval use interchangeable strategies.
|
| 256 |
+
|
| 257 |
+
### Testing and Coverage
|
| 258 |
+
|
| 259 |
+
- No explicit test files detected; testing likely manual or via notebooks (`tutorials/`).
|
| 260 |
+
- Tasks and tools return detailed logs suitable for manual verification.
|
| 261 |
+
- Metadata schemas facilitate automated validation of inputs.
|
| 262 |
+
|
| 263 |
+
### Error Handling and Logging
|
| 264 |
+
|
| 265 |
+
- Use of try-except blocks around external calls and subprocesses.
|
| 266 |
+
- Logging via custom callback handlers (`PromptLogger`, `NodeLogger`) in LLM interactions.
|
| 267 |
+
- Utilities provide colorized printing and error wrappers for robustness.
|
| 268 |
+
|
| 269 |
+
### Configuration Management
|
| 270 |
+
|
| 271 |
+
- Environment variables for API keys (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`).
|
| 272 |
+
- YAML and JSON files for environment and tool configuration.
|
| 273 |
+
- Dynamic loading of schemas from pickle files for API request generation.
|
| 274 |
+
- CLI tools and R packages installed via scripted environment setup.
|
| 275 |
+
|
| 276 |
+
---
|
| 277 |
+
|
| 278 |
+
## 5. Integration and Dependencies
|
| 279 |
+
|
| 280 |
+
### External Libraries
|
| 281 |
+
|
| 282 |
+
- **LLM & AI Frameworks**: `langchain_core`, `langchain_openai`, `langchain_anthropic`
|
| 283 |
+
- **Scientific Computing**: `numpy`, `pandas`, `scipy`, `scikit-image`, `matplotlib`, `BioPython`, `cobra`, `sklearn`
|
| 284 |
+
- **Data Processing**: `pickle`, `json`, `requests`, `PyPDF2`
|
| 285 |
+
- **System and OS**: `subprocess`, `os`, `sys`, `tempfile`, `multiprocessing`
|
| 286 |
+
- **Others**: `tqdm` (progress bars), `enum`, `ast` (code introspection)
|
| 287 |
+
|
| 288 |
+
### External APIs and Data Sources
|
| 289 |
+
|
| 290 |
+
- Biomedical databases: UniProt, GWAS Catalog, Ensembl, ClinVar, dbSNP, EMDB, GEO, GnomAD, InterPro, etc.
|
| 291 |
+
- Bioinformatics tools: PLINK, IQ-TREE, GCTA, MACS2, samtools, LUMPY, installed via CLI tools installer.
|
| 292 |
+
- R packages for statistical and bioinformatics analyses.
|
| 293 |
+
|
| 294 |
+
### Build and Deployment Dependencies
|
| 295 |
+
|
| 296 |
+
- Python 3 environment managed via Conda (`environment.yml`).
|
| 297 |
+
- R environment with specified packages (`r_packages.yml`).
|
| 298 |
+
- Shell scripts for CLI tool installation and environment setup.
|
| 299 |
+
- Pre-commit hooks for code quality and security.
|
| 300 |
+
|
| 301 |
+
---
|
| 302 |
+
|
| 303 |
+
## 6. Usage and Operational Guidance
|
| 304 |
+
|
| 305 |
+
### Getting Started
|
| 306 |
+
|
| 307 |
+
1. **Environment Setup**
|
| 308 |
+
- Run `biomni_env/setup.sh` to create the Conda environment, install R packages, and CLI tools.
|
| 309 |
+
- Source `biomni_env/setup_path.sh` or add it to your shell profile to configure PATH.
|
| 310 |
+
|
| 311 |
+
2. **API Keys**
|
| 312 |
+
- Set environment variables `OPENAI_API_KEY` and/or `ANTHROPIC_API_KEY` for LLM access.
|
| 313 |
+
|
| 314 |
+
3. **Running Agents**
|
| 315 |
+
- Import and instantiate the `react` agent from `biomni.agent.react`.
|
| 316 |
+
- Configure with desired tools and retrieval options.
|
| 317 |
+
- Call `go(prompt)` to execute reasoning workflows.
|
| 318 |
+
|
| 319 |
+
4. **Executing Tasks**
|
| 320 |
+
- Use classes in `biomni.task` to load datasets and evaluate LLM responses.
|
| 321 |
+
- Implement new tasks by subclassing `base_task` and following the interface.
|
| 322 |
+
|
| 323 |
+
5. **Querying Databases**
|
| 324 |
+
- Use `biomni.tool.database` functions (e.g., `query_uniprot`, `query_gwas_catalog`) to retrieve data via natural language or direct parameters.
|
| 325 |
+
|
| 326 |
+
6. **Extending Tools**
|
| 327 |
+
- Add new tool metadata in `biomni/tool/tool_description/` as structured dictionaries.
|
| 328 |
+
- Implement corresponding analysis functions in `biomni/tool/`.
|
| 329 |
+
- Register tools in `tool_registry.py` for discovery.
|
| 330 |
+
|
| 331 |
+
### Monitoring and Debugging
|
| 332 |
+
|
| 333 |
+
- Use logging callbacks (`PromptLogger`, `NodeLogger`) to trace LLM interactions.
|
| 334 |
+
- Check output logs returned by analysis functions for detailed execution info.
|
| 335 |
+
- Use pre-commit hooks to maintain code quality.
|
| 336 |
+
|
| 337 |
+
### Performance and Scalability
|
| 338 |
+
|
| 339 |
+
- Modular design allows parallel execution of tasks and tools.
|
| 340 |
+
- Timeout wrappers in agent tools prevent hanging executions.
|
| 341 |
+
- Use of efficient numerical libraries (`numpy`, `scipy`) for computational tasks.
|
| 342 |
+
- Large data handled via streaming and chunking (e.g., PDF text extraction).
|
| 343 |
+
|
| 344 |
+
### Security Considerations
|
| 345 |
+
|
| 346 |
+
- API keys managed via environment variables, not hardcoded.
|
| 347 |
+
- Pre-commit hooks include security checks.
|
| 348 |
+
- External tool installations verified via version commands.
|
| 349 |
+
|
| 350 |
+
### Observability
|
| 351 |
+
|
| 352 |
+
- Progress bars (`tqdm`) used in data processing scripts.
|
| 353 |
+
- Structured logs and JSON outputs facilitate downstream analysis.
|
| 354 |
+
- Agent workflows produce detailed message histories for audit.
|
| 355 |
+
|
| 356 |
+
---
|
| 357 |
+
|
| 358 |
+
## Summary
|
| 359 |
+
|
| 360 |
+
This project is a **modular, extensible biomedical AI platform** integrating **LLM-powered agents**, **domain-specific scientific tools**, and **metadata-driven APIs** to automate complex biomedical research workflows. It emphasizes **declarative tool descriptions**, **dynamic resource retrieval**, and **robust environment provisioning** to enable researchers and developers to build, evaluate, and extend AI-driven biomedical applications efficiently.
|
| 361 |
+
|
| 362 |
+
---
|
| 363 |
+
|
| 364 |
+
# End of DETAILS.md
|
Biomni/LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Apache License
|
| 2 |
+
Version 2.0, January 2004
|
| 3 |
+
http://www.apache.org/licenses/
|
| 4 |
+
|
| 5 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
| 6 |
+
|
| 7 |
+
1. Definitions.
|
| 8 |
+
|
| 9 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
| 10 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
| 11 |
+
|
| 12 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
| 13 |
+
the copyright owner that is granting the License.
|
| 14 |
+
|
| 15 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
| 16 |
+
other entities that control, are controlled by, or are under common
|
| 17 |
+
control with that entity. For the purposes of this definition,
|
| 18 |
+
"control" means (i) the power, direct or indirect, to cause the
|
| 19 |
+
direction or management of such entity, whether by contract or
|
| 20 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
| 21 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
| 22 |
+
|
| 23 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
| 24 |
+
exercising permissions granted by this License.
|
| 25 |
+
|
| 26 |
+
"Source" form shall mean the preferred form for making modifications,
|
| 27 |
+
including but not limited to software source code, documentation
|
| 28 |
+
source, and configuration files.
|
| 29 |
+
|
| 30 |
+
"Object" form shall mean any form resulting from mechanical
|
| 31 |
+
transformation or translation of a Source form, including but
|
| 32 |
+
not limited to compiled object code, generated documentation,
|
| 33 |
+
and conversions to other media types.
|
| 34 |
+
|
| 35 |
+
"Work" shall mean the work of authorship, whether in Source or
|
| 36 |
+
Object form, made available under the License, as indicated by a
|
| 37 |
+
copyright notice that is included in or attached to the work
|
| 38 |
+
(an example is provided in the Appendix below).
|
| 39 |
+
|
| 40 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
| 41 |
+
form, that is based on (or derived from) the Work and for which the
|
| 42 |
+
editorial revisions, annotations, elaborations, or other modifications
|
| 43 |
+
represent, as a whole, an original work of authorship. For the purposes
|
| 44 |
+
of this License, Derivative Works shall not include works that remain
|
| 45 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
| 46 |
+
the Work and Derivative Works thereof.
|
| 47 |
+
|
| 48 |
+
"Contribution" shall mean any work of authorship, including
|
| 49 |
+
the original version of the Work and any modifications or additions
|
| 50 |
+
to that Work or Derivative Works thereof, that is intentionally
|
| 51 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
| 52 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
| 53 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
| 54 |
+
means any form of electronic, verbal, or written communication sent
|
| 55 |
+
to the Licensor or its representatives, including but not limited to
|
| 56 |
+
communication on electronic mailing lists, source code control systems,
|
| 57 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
| 58 |
+
Licensor for the purpose of discussing and improving the Work, but
|
| 59 |
+
excluding communication that is conspicuously marked or otherwise
|
| 60 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
| 61 |
+
|
| 62 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
| 63 |
+
on behalf of whom a Contribution has been received by Licensor and
|
| 64 |
+
subsequently incorporated within the Work.
|
| 65 |
+
|
| 66 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
| 67 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 68 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 69 |
+
copyright license to reproduce, prepare Derivative Works of,
|
| 70 |
+
publicly display, publicly perform, sublicense, and distribute the
|
| 71 |
+
Work and such Derivative Works in Source or Object form.
|
| 72 |
+
|
| 73 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
| 74 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 75 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 76 |
+
(except as stated in this section) patent license to make, have made,
|
| 77 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
| 78 |
+
where such license applies only to those patent claims licensable
|
| 79 |
+
by such Contributor that are necessarily infringed by their
|
| 80 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
| 81 |
+
with the Work to which such Contribution(s) was submitted. If You
|
| 82 |
+
institute patent litigation against any entity (including a
|
| 83 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
| 84 |
+
or a Contribution incorporated within the Work constitutes direct
|
| 85 |
+
or contributory patent infringement, then any patent licenses
|
| 86 |
+
granted to You under this License for that Work shall terminate
|
| 87 |
+
as of the date such litigation is filed.
|
| 88 |
+
|
| 89 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
| 90 |
+
Work or Derivative Works thereof in any medium, with or without
|
| 91 |
+
modifications, and in Source or Object form, provided that You
|
| 92 |
+
meet the following conditions:
|
| 93 |
+
|
| 94 |
+
(a) You must give any other recipients of the Work or
|
| 95 |
+
Derivative Works a copy of this License; and
|
| 96 |
+
|
| 97 |
+
(b) You must cause any modified files to carry prominent notices
|
| 98 |
+
stating that You changed the files; and
|
| 99 |
+
|
| 100 |
+
(c) You must retain, in the Source form of any Derivative Works
|
| 101 |
+
that You distribute, all copyright, patent, trademark, and
|
| 102 |
+
attribution notices from the Source form of the Work,
|
| 103 |
+
excluding those notices that do not pertain to any part of
|
| 104 |
+
the Derivative Works; and
|
| 105 |
+
|
| 106 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
| 107 |
+
distribution, then any Derivative Works that You distribute must
|
| 108 |
+
include a readable copy of the attribution notices contained
|
| 109 |
+
within such NOTICE file, excluding those notices that do not
|
| 110 |
+
pertain to any part of the Derivative Works, in at least one
|
| 111 |
+
of the following places: within a NOTICE text file distributed
|
| 112 |
+
as part of the Derivative Works; within the Source form or
|
| 113 |
+
documentation, if provided along with the Derivative Works; or,
|
| 114 |
+
within a display generated by the Derivative Works, if and
|
| 115 |
+
wherever such third-party notices normally appear. The contents
|
| 116 |
+
of the NOTICE file are for informational purposes only and
|
| 117 |
+
do not modify the License. You may add Your own attribution
|
| 118 |
+
notices within Derivative Works that You distribute, alongside
|
| 119 |
+
or as an addendum to the NOTICE text from the Work, provided
|
| 120 |
+
that such additional attribution notices cannot be construed
|
| 121 |
+
as modifying the License.
|
| 122 |
+
|
| 123 |
+
You may add Your own copyright statement to Your modifications and
|
| 124 |
+
may provide additional or different license terms and conditions
|
| 125 |
+
for use, reproduction, or distribution of Your modifications, or
|
| 126 |
+
for any such Derivative Works as a whole, provided Your use,
|
| 127 |
+
reproduction, and distribution of the Work otherwise complies with
|
| 128 |
+
the conditions stated in this License.
|
| 129 |
+
|
| 130 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
| 131 |
+
any Contribution intentionally submitted for inclusion in the Work
|
| 132 |
+
by You to the Licensor shall be under the terms and conditions of
|
| 133 |
+
this License, without any additional terms or conditions.
|
| 134 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
| 135 |
+
the terms of any separate license agreement you may have executed
|
| 136 |
+
with Licensor regarding such Contributions.
|
| 137 |
+
|
| 138 |
+
6. Trademarks. This License does not grant permission to use the trade
|
| 139 |
+
names, trademarks, service marks, or product names of the Licensor,
|
| 140 |
+
except as required for reasonable and customary use in describing the
|
| 141 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
| 142 |
+
|
| 143 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
| 144 |
+
agreed to in writing, Licensor provides the Work (and each
|
| 145 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
| 146 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 147 |
+
implied, including, without limitation, any warranties or conditions
|
| 148 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
| 149 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
| 150 |
+
appropriateness of using or redistributing the Work and assume any
|
| 151 |
+
risks associated with Your exercise of permissions under this License.
|
| 152 |
+
|
| 153 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
| 154 |
+
whether in tort (including negligence), contract, or otherwise,
|
| 155 |
+
unless required by applicable law (such as deliberate and grossly
|
| 156 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
| 157 |
+
liable to You for damages, including any direct, indirect, special,
|
| 158 |
+
incidental, or consequential damages of any character arising as a
|
| 159 |
+
result of this License or out of the use or inability to use the
|
| 160 |
+
Work (including but not limited to damages for loss of goodwill,
|
| 161 |
+
work stoppage, computer failure or malfunction, or any and all
|
| 162 |
+
other commercial damages or losses), even if such Contributor
|
| 163 |
+
has been advised of the possibility of such damages.
|
| 164 |
+
|
| 165 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
| 166 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
| 167 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
| 168 |
+
or other liability obligations and/or rights consistent with this
|
| 169 |
+
License. However, in accepting such obligations, You may act only
|
| 170 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
| 171 |
+
of any other Contributor, and only if You agree to indemnify,
|
| 172 |
+
defend, and hold each Contributor harmless for any liability
|
| 173 |
+
incurred by, or claims asserted against, such Contributor by reason
|
| 174 |
+
of your accepting any such warranty or additional liability.
|
| 175 |
+
|
| 176 |
+
END OF TERMS AND CONDITIONS
|
| 177 |
+
|
| 178 |
+
APPENDIX: How to apply the Apache License to your work.
|
| 179 |
+
|
| 180 |
+
To apply the Apache License to your work, attach the following
|
| 181 |
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
| 182 |
+
replaced with your own identifying information. (Don't include
|
| 183 |
+
the brackets!) The text should be enclosed in the appropriate
|
| 184 |
+
comment syntax for the file format. We also recommend that a
|
| 185 |
+
file or class name and description of purpose be included on the
|
| 186 |
+
same "printed page" as the copyright notice for easier
|
| 187 |
+
identification within third-party archives.
|
| 188 |
+
|
| 189 |
+
Copyright [yyyy] [name of copyright owner]
|
| 190 |
+
|
| 191 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 192 |
+
you may not use this file except in compliance with the License.
|
| 193 |
+
You may obtain a copy of the License at
|
| 194 |
+
|
| 195 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
| 196 |
+
|
| 197 |
+
Unless required by applicable law or agreed to in writing, software
|
| 198 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 199 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 200 |
+
See the License for the specific language governing permissions and
|
| 201 |
+
limitations under the License.
|
Biomni/MANIFEST.in
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# MANIFEST.in
|
| 2 |
+
|
| 3 |
+
# Include all python files from the biomni package
|
| 4 |
+
recursive-include biomni *.py
|
| 5 |
+
|
| 6 |
+
# Include the .pkl database files
|
| 7 |
+
recursive-include biomni/tool/schema_db *.pkl
|
| 8 |
+
|
| 9 |
+
# Include specific files from biomni_env, but not the biomni_tools subdirectory
|
| 10 |
+
recursive-include biomni_env *.py *.sh *.yml *.yaml *.txt *.md *.json *.R
|
| 11 |
+
|
| 12 |
+
# Exclude specific directories that are not part of the package
|
| 13 |
+
prune tutorials/data
|
| 14 |
+
prune biomni_env/biomni_tools
|
| 15 |
+
|
| 16 |
+
# Exclude build artifacts and caches
|
| 17 |
+
global-exclude *.py[co]
|
| 18 |
+
global-exclude __pycache__
|
| 19 |
+
global-exclude .ruff_cache
|
| 20 |
+
prune build
|
| 21 |
+
prune dist
|
| 22 |
+
prune *.egg-info
|
| 23 |
+
|
| 24 |
+
# Include other necessary files
|
| 25 |
+
include README.md
|
| 26 |
+
include LICENSE
|
| 27 |
+
include pyproject.toml
|
Biomni/Q13398_ChIP-seq_Meta-clusters.bb
ADDED
|
Binary file (31.5 kB). View file
|
|
|
Biomni/biomni/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .version import __version__
|
| 2 |
+
|
| 3 |
+
__all__ = ["__version__"]
|
Biomni/biomni/agent/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
from biomni.agent.a1 import A1 # noqa: F401
|
Biomni/biomni/agent/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (215 Bytes). View file
|
|
|
Biomni/biomni/agent/a1.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
Biomni/biomni/agent/env_collection.py
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
import re
|
| 4 |
+
from typing import Any
|
| 5 |
+
|
| 6 |
+
from langchain_core.prompts import ChatPromptTemplate
|
| 7 |
+
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
| 8 |
+
|
| 9 |
+
from biomni.agent.base_agent import base_agent
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class PaperTaskExtractor(base_agent):
|
| 13 |
+
"""Agent that extracts generalizable tasks/experiments from academic papers.
|
| 14 |
+
It processes papers in chunks and identifies common research tasks that could be shared across papers.
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
def __init__(
|
| 18 |
+
self,
|
| 19 |
+
llm="claude-3-7-sonnet-20250219",
|
| 20 |
+
cheap_llm=None,
|
| 21 |
+
tools=None,
|
| 22 |
+
chunk_size=4000,
|
| 23 |
+
chunk_overlap=400,
|
| 24 |
+
):
|
| 25 |
+
"""Initialize the PaperTaskExtractor agent.
|
| 26 |
+
|
| 27 |
+
Args:
|
| 28 |
+
llm (str): The LLM model to use
|
| 29 |
+
cheap_llm (str, optional): A cheaper LLM for simpler tasks
|
| 30 |
+
tools (list, optional): Any tools to use (not needed for this agent)
|
| 31 |
+
chunk_size (int): Size of text chunks for processing
|
| 32 |
+
chunk_overlap (int): Overlap between chunks
|
| 33 |
+
|
| 34 |
+
"""
|
| 35 |
+
super().__init__(llm, cheap_llm, tools)
|
| 36 |
+
self.chunk_size = chunk_size
|
| 37 |
+
self.chunk_overlap = chunk_overlap
|
| 38 |
+
self.log = []
|
| 39 |
+
self.configure()
|
| 40 |
+
|
| 41 |
+
def configure(self):
|
| 42 |
+
"""Configure the agent with appropriate prompts."""
|
| 43 |
+
# Prompt for analyzing paper chunks
|
| 44 |
+
self.chunk_analysis_prompt = """You are a research methodology expert specializing in identifying computational tasks and data analysis procedures in academic papers.
|
| 45 |
+
|
| 46 |
+
Your job is to analyze chunks of academic papers and identify ONLY the most common, generalizable computational tasks that are widely used across biomedical research and can be implemented with Python or Linux code.
|
| 47 |
+
|
| 48 |
+
STRICT GUIDELINES:
|
| 49 |
+
1. ONLY extract tasks that are extremely common and standard in computational biomedical research
|
| 50 |
+
2. Each task MUST have clear, well-defined inputs and outputs
|
| 51 |
+
3. Tasks MUST be generalizable across many different papers and research questions
|
| 52 |
+
4. Be VERY selective - only include tasks that appear in hundreds of papers
|
| 53 |
+
5. If a task is specific to this paper, unclear, or not widely used, DO NOT include it
|
| 54 |
+
6. Focus on computational tasks that can be implemented with Python or Linux code
|
| 55 |
+
7. Each task should be something that could be implemented as a function with clear inputs/outputs
|
| 56 |
+
8. Also identify commonly used databases and software packages mentioned in the text
|
| 57 |
+
9. Tasks MUST be CONCRETE and SPECIFIC - include exact methodological details
|
| 58 |
+
10. Avoid vague task names like "Statistical Analysis" - instead use specific protocol names like "Two-way ANOVA with Tukey's Post-hoc Test using SciPy"
|
| 59 |
+
11. DO NOT include wet lab procedures that cannot be implemented computationally
|
| 60 |
+
12. ONLY include tasks that could be automated with code
|
| 61 |
+
|
| 62 |
+
For the following chunk of text from an academic paper, provide:
|
| 63 |
+
1. A list of ONLY the most common, generalizable COMPUTATIONAL tasks identified (be extremely selective)
|
| 64 |
+
2. For each task, clearly define:
|
| 65 |
+
- Task name: A SPECIFIC and CONCRETE name with methodological details (e.g., "RNA-seq Differential Expression Analysis with DESeq2" instead of just "Gene Expression Analysis")
|
| 66 |
+
- Input: What SPECIFIC data or parameters the task requires
|
| 67 |
+
- Output: What SPECIFIC data or results the task produces
|
| 68 |
+
- Code implementation: How this task could be implemented with Python or Linux code, including key libraries/packages
|
| 69 |
+
- Frequency: How common this computational task is in biomedical research
|
| 70 |
+
- Standard methods: The established computational techniques used to perform this task
|
| 71 |
+
- Example: A brief description of how THIS specific paper uses this task (with specific details from the paper)
|
| 72 |
+
3. A list of commonly used databases mentioned in the text (if any)
|
| 73 |
+
4. A list of commonly used software packages/tools mentioned in the text (if any)
|
| 74 |
+
|
| 75 |
+
PAPER CHUNK:
|
| 76 |
+
{chunk_text}
|
| 77 |
+
|
| 78 |
+
Remember, it's better to return NO tasks than to include tasks that aren't extremely common, generalizable, and implementable with code. Quality over quantity is essential. Tasks MUST be CONCRETE with SPECIFIC methodological details and MUST be implementable with Python or Linux code.
|
| 79 |
+
"""
|
| 80 |
+
|
| 81 |
+
# Prompt for consolidating tasks
|
| 82 |
+
self.consolidation_prompt = """You are a research methodology expert. Your task is to consolidate lists of computational research tasks extracted from different chunks of an academic paper.
|
| 83 |
+
|
| 84 |
+
BE EXTREMELY SELECTIVE. Only include tasks that are:
|
| 85 |
+
1. Fundamental to computational biomedical research
|
| 86 |
+
2. Used in hundreds of papers across different subfields
|
| 87 |
+
3. Have clear, well-defined inputs and outputs
|
| 88 |
+
4. Represent standard computational approaches
|
| 89 |
+
5. Could be implemented as a function with specific inputs and outputs
|
| 90 |
+
6. Are CONCRETE and SPECIFIC with exact methodological details
|
| 91 |
+
7. Can be implemented with Python or Linux code
|
| 92 |
+
8. Are computational in nature, not wet lab procedures
|
| 93 |
+
|
| 94 |
+
REMOVE any tasks that:
|
| 95 |
+
- Are specific to a particular paper or dataset
|
| 96 |
+
- Lack clear inputs or outputs
|
| 97 |
+
- Are not widely used across biomedical research
|
| 98 |
+
- Are vague or poorly defined
|
| 99 |
+
- Represent niche or specialized techniques
|
| 100 |
+
- Have generic names without specific methodological details
|
| 101 |
+
- Cannot be implemented with code
|
| 102 |
+
- Require physical lab equipment or manual intervention
|
| 103 |
+
|
| 104 |
+
The output should be a JSON object with three main keys:
|
| 105 |
+
1. "tasks": A list of task objects, where each task has:
|
| 106 |
+
- "task_name": A SPECIFIC and CONCRETE name with methodological details (e.g., "Single-cell RNA-seq Clustering with Seurat" instead of just "Cell Clustering")
|
| 107 |
+
- "description": A clear description of what the computational task does
|
| 108 |
+
- "inputs": SPECIFIC data types or parameters the task requires
|
| 109 |
+
- "outputs": SPECIFIC data types or results the task produces
|
| 110 |
+
- "code_implementation": How this task could be implemented with Python or Linux code, including key libraries/packages and a brief pseudocode example
|
| 111 |
+
- "frequency": How common this computational task is in biomedical research
|
| 112 |
+
- "standard_methods": The established computational techniques used to perform this task
|
| 113 |
+
- "example": A specific example from THIS paper showing how the task was used (with concrete details)
|
| 114 |
+
|
| 115 |
+
2. "databases": A list of database objects, where each database has:
|
| 116 |
+
- "name": The name of the database
|
| 117 |
+
- "description": A brief description of what the database contains
|
| 118 |
+
- "url": The URL of the database (if mentioned)
|
| 119 |
+
- "usage": How the database is commonly used in computational biomedical research
|
| 120 |
+
- "example": How this specific paper uses the database
|
| 121 |
+
|
| 122 |
+
3. "software": A list of software package objects, where each package has:
|
| 123 |
+
- "name": The name of the software package
|
| 124 |
+
- "description": A brief description of what the software does
|
| 125 |
+
- "url": The URL or reference to the software (if mentioned)
|
| 126 |
+
- "usage": How the software is commonly used in computational biomedical research
|
| 127 |
+
- "example": How this specific paper uses the software
|
| 128 |
+
|
| 129 |
+
EXTRACTED INFORMATION FROM PAPER CHUNKS:
|
| 130 |
+
{task_lists}
|
| 131 |
+
|
| 132 |
+
Be ruthless in filtering - it's better to return a few truly common computational tasks than many that aren't universal or implementable with code.
|
| 133 |
+
Respond with only a valid JSON object containing the three lists described above.
|
| 134 |
+
"""
|
| 135 |
+
|
| 136 |
+
def process_paper(self, paper_text: str) -> dict[str, list[dict[str, Any]]]:
|
| 137 |
+
"""Process a paper and extract generalizable tasks/experiments, databases, and software.
|
| 138 |
+
|
| 139 |
+
Args:
|
| 140 |
+
paper_text (str): The full text of the paper
|
| 141 |
+
|
| 142 |
+
Returns:
|
| 143 |
+
Dict[str, List[Dict[str, Any]]]: A dictionary with tasks, databases, and software
|
| 144 |
+
|
| 145 |
+
"""
|
| 146 |
+
# Split the paper into chunks
|
| 147 |
+
text_splitter = RecursiveCharacterTextSplitter(
|
| 148 |
+
chunk_size=self.chunk_size,
|
| 149 |
+
chunk_overlap=self.chunk_overlap,
|
| 150 |
+
length_function=len,
|
| 151 |
+
separators=["\n\n", "\n", ". ", " ", ""],
|
| 152 |
+
)
|
| 153 |
+
chunks = text_splitter.split_text(paper_text)
|
| 154 |
+
|
| 155 |
+
# Process each chunk to extract tasks
|
| 156 |
+
chunk_results = []
|
| 157 |
+
for i, chunk in enumerate(chunks):
|
| 158 |
+
print(f"Processing chunk {i + 1}/{len(chunks)}...")
|
| 159 |
+
chunk_tasks = self._process_chunk(chunk)
|
| 160 |
+
chunk_results.append(chunk_tasks)
|
| 161 |
+
|
| 162 |
+
# Consolidate tasks from all chunks
|
| 163 |
+
consolidated_results = self._consolidate_tasks(chunk_results)
|
| 164 |
+
return consolidated_results
|
| 165 |
+
|
| 166 |
+
def _process_chunk(self, chunk_text: str) -> str:
|
| 167 |
+
"""Process a single chunk of the paper to extract tasks.
|
| 168 |
+
|
| 169 |
+
Args:
|
| 170 |
+
chunk_text (str): The chunk text to process
|
| 171 |
+
|
| 172 |
+
Returns:
|
| 173 |
+
str: Extracted tasks from this chunk
|
| 174 |
+
|
| 175 |
+
"""
|
| 176 |
+
prompt = self.chunk_analysis_prompt.format(chunk_text=chunk_text)
|
| 177 |
+
message = self.llm.invoke(prompt)
|
| 178 |
+
return message.content
|
| 179 |
+
|
| 180 |
+
def _consolidate_tasks(self, chunk_results: list[str]) -> dict[str, list[dict[str, Any]]]:
|
| 181 |
+
"""Consolidate tasks, databases, and software extracted from different chunks into a unified structure.
|
| 182 |
+
|
| 183 |
+
Args:
|
| 184 |
+
chunk_results (List[str]): Results from each chunk
|
| 185 |
+
|
| 186 |
+
Returns:
|
| 187 |
+
Dict[str, List[Dict[str, Any]]]: Consolidated information with tasks, databases, and software
|
| 188 |
+
|
| 189 |
+
"""
|
| 190 |
+
# Combine all chunk results
|
| 191 |
+
all_tasks = "\n\n===== CHUNK SEPARATOR =====\n\n".join(chunk_results)
|
| 192 |
+
|
| 193 |
+
# Use the consolidation prompt to merge and organize tasks
|
| 194 |
+
prompt = self.consolidation_prompt.format(task_lists=all_tasks)
|
| 195 |
+
response = self.llm.invoke(prompt)
|
| 196 |
+
|
| 197 |
+
# Extract the JSON from the response
|
| 198 |
+
try:
|
| 199 |
+
# Try to parse the entire response as JSON
|
| 200 |
+
result = json.loads(response.content)
|
| 201 |
+
except json.JSONDecodeError:
|
| 202 |
+
# If that fails, try to extract JSON from the text
|
| 203 |
+
try:
|
| 204 |
+
# Look for JSON-like content between triple backticks
|
| 205 |
+
json_match = re.search(r"```(?:json)?\s*([\s\S]*?)\s*```", response.content)
|
| 206 |
+
if json_match:
|
| 207 |
+
result = json.loads(json_match.group(1))
|
| 208 |
+
else:
|
| 209 |
+
# Fallback: just return the text response
|
| 210 |
+
return {
|
| 211 |
+
"tasks": [
|
| 212 |
+
{
|
| 213 |
+
"error": "Could not parse JSON",
|
| 214 |
+
"raw_response": response.content,
|
| 215 |
+
}
|
| 216 |
+
],
|
| 217 |
+
"databases": [],
|
| 218 |
+
"software": [],
|
| 219 |
+
}
|
| 220 |
+
except Exception:
|
| 221 |
+
return {
|
| 222 |
+
"tasks": [
|
| 223 |
+
{
|
| 224 |
+
"error": "Could not parse JSON",
|
| 225 |
+
"raw_response": response.content,
|
| 226 |
+
}
|
| 227 |
+
],
|
| 228 |
+
"databases": [],
|
| 229 |
+
"software": [],
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
# Ensure the result has the expected structure
|
| 233 |
+
if not isinstance(result, dict):
|
| 234 |
+
result = {
|
| 235 |
+
"tasks": result if isinstance(result, list) else [],
|
| 236 |
+
"databases": [],
|
| 237 |
+
"software": [],
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
# Ensure all required keys exist
|
| 241 |
+
for key in ["tasks", "databases", "software"]:
|
| 242 |
+
if key not in result:
|
| 243 |
+
result[key] = []
|
| 244 |
+
|
| 245 |
+
return result
|
| 246 |
+
|
| 247 |
+
def go(self, paper_text: str):
|
| 248 |
+
"""Process a paper and return the extracted tasks, databases, and software.
|
| 249 |
+
|
| 250 |
+
Args:
|
| 251 |
+
paper_text (str): The full text of the paper
|
| 252 |
+
|
| 253 |
+
Returns:
|
| 254 |
+
tuple: (log, results) where log is a list of processing steps and results is the final result
|
| 255 |
+
|
| 256 |
+
"""
|
| 257 |
+
self.log = []
|
| 258 |
+
self.log.append(
|
| 259 |
+
(
|
| 260 |
+
"user",
|
| 261 |
+
"Extract only the most common and generalizable biomedical research tasks, databases, and software from this paper",
|
| 262 |
+
)
|
| 263 |
+
)
|
| 264 |
+
|
| 265 |
+
results = self.process_paper(paper_text)
|
| 266 |
+
|
| 267 |
+
result_str = json.dumps(results, indent=2)
|
| 268 |
+
self.log.append(("assistant", result_str))
|
| 269 |
+
|
| 270 |
+
return self.log, results
|
| 271 |
+
|
| 272 |
+
def save_results(self, results: dict[str, list[dict[str, Any]]], output_path: str):
|
| 273 |
+
"""Save the extracted tasks, databases, and software to a JSON file.
|
| 274 |
+
|
| 275 |
+
Args:
|
| 276 |
+
results (Dict[str, List[Dict[str, Any]]]): The extracted tasks, databases, and software
|
| 277 |
+
output_path (str): Path to save the results
|
| 278 |
+
|
| 279 |
+
"""
|
| 280 |
+
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
| 281 |
+
with open(output_path, "w") as f:
|
| 282 |
+
json.dump(results, f, indent=2)
|
| 283 |
+
print(f"Results saved to {output_path}")
|
| 284 |
+
|
| 285 |
+
def result_formatting(self, output_class, task_intention):
|
| 286 |
+
"""Format the results according to a specific output class.
|
| 287 |
+
|
| 288 |
+
Args:
|
| 289 |
+
output_class: The class to format the output as
|
| 290 |
+
task_intention: Description of the task
|
| 291 |
+
|
| 292 |
+
Returns:
|
| 293 |
+
The formatted result
|
| 294 |
+
|
| 295 |
+
"""
|
| 296 |
+
format_check_prompt = ChatPromptTemplate.from_messages(
|
| 297 |
+
[
|
| 298 |
+
(
|
| 299 |
+
"system",
|
| 300 |
+
(
|
| 301 |
+
"You are evaluateGPT, tasked with extract and parse the task output based on the history of an agent. "
|
| 302 |
+
"Review the entire history of messages provided. "
|
| 303 |
+
"Here is the task output requirement: \n"
|
| 304 |
+
f"'{task_intention.replace('{', '{{').replace('}', '}}')}'.\n"
|
| 305 |
+
),
|
| 306 |
+
),
|
| 307 |
+
("placeholder", "{messages}"),
|
| 308 |
+
]
|
| 309 |
+
)
|
| 310 |
+
|
| 311 |
+
checker_llm = format_check_prompt | self.llm.with_structured_output(output_class)
|
| 312 |
+
result = checker_llm.invoke({"messages": [("user", str(self.log))]}).dict()
|
| 313 |
+
return result
|
Biomni/biomni/agent/function_generator.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
|
| 3 |
+
from biomni.llm import get_llm
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class base_agent:
|
| 7 |
+
def __init__(self, llm="claude-3-haiku-20240307", cheap_llm=None, tools=None, temperature=0.7):
|
| 8 |
+
self.tools = tools
|
| 9 |
+
self.llm = get_llm(llm, temperature)
|
| 10 |
+
if cheap_llm is None:
|
| 11 |
+
self.cheap_llm = llm
|
| 12 |
+
else:
|
| 13 |
+
self.cheap_llm = cheap_llm
|
| 14 |
+
|
| 15 |
+
def configure(self):
|
| 16 |
+
pass
|
| 17 |
+
|
| 18 |
+
def go(self, input):
|
| 19 |
+
pass
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class FunctionGenerator(base_agent):
|
| 23 |
+
"""Agent that generates executable Python code scripts given a task description."""
|
| 24 |
+
|
| 25 |
+
def __init__(self, llm="claude-3-7-sonnet-20250219", cheap_llm=None, temperature=0.7):
|
| 26 |
+
"""Initialize the PaperTaskExtractor agent.
|
| 27 |
+
|
| 28 |
+
Args:
|
| 29 |
+
llm (str): The LLM model to use
|
| 30 |
+
cheap_llm (str, optional): A cheaper LLM for simpler tasks
|
| 31 |
+
"""
|
| 32 |
+
super().__init__(llm, cheap_llm, temperature)
|
| 33 |
+
self.log = []
|
| 34 |
+
self.configure()
|
| 35 |
+
|
| 36 |
+
def configure(self):
|
| 37 |
+
"""Configure the agent with appropriate prompts."""
|
| 38 |
+
# Prompt for Python code generation
|
| 39 |
+
self.system_prompt = """You are a senior Python engineer. Generate robust, idiomatic Python code that solves the user's task. Requirements:
|
| 40 |
+
1. Output ONLY Python code, ideally inside a single triple-backtick code block.
|
| 41 |
+
2. Include minimal inline comments and a small docstring.
|
| 42 |
+
3. Add a `main()` and an `if __name__ == '__main__':` guard when appropriate.
|
| 43 |
+
4. Avoid external dependencies unless necessary; if used, show `pip` installs in comments.
|
| 44 |
+
5. Do not include prose before or after the code.
|
| 45 |
+
6. When applicable, prioritize the use of codes on public repositories, such as HuggingFace or Github
|
| 46 |
+
|
| 47 |
+
Generate Python codes for the following task:
|
| 48 |
+
{task}
|
| 49 |
+
"""
|
| 50 |
+
|
| 51 |
+
def _generate_code(self, task_description: str) -> str:
|
| 52 |
+
"""Generate codes given a task description.
|
| 53 |
+
Args:
|
| 54 |
+
task_description (str): task descriptions (possibly generated from previous steps)
|
| 55 |
+
|
| 56 |
+
Returns:
|
| 57 |
+
str: generated code string
|
| 58 |
+
|
| 59 |
+
"""
|
| 60 |
+
prompt = self.system_prompt.format(task=task_description)
|
| 61 |
+
message = self.llm.invoke(prompt)
|
| 62 |
+
return message.content
|
| 63 |
+
|
| 64 |
+
def _generate_script_filename(self, task_description: str, max_words: int = 6) -> str:
|
| 65 |
+
"""
|
| 66 |
+
Generate a safe, meaningful Python script filename from a task description.
|
| 67 |
+
Poised for update: may ask the agent to suggest meaningful names.
|
| 68 |
+
|
| 69 |
+
Parameters:
|
| 70 |
+
-----------
|
| 71 |
+
task_description (str): task descriptions (possibly generated from previous steps)
|
| 72 |
+
|
| 73 |
+
max_words : int
|
| 74 |
+
Maximum number of words to include in the filename.
|
| 75 |
+
|
| 76 |
+
Returns:
|
| 77 |
+
--------
|
| 78 |
+
str
|
| 79 |
+
A lowercase, hyphen-free, safe filename ending in '.py'.
|
| 80 |
+
"""
|
| 81 |
+
# Lowercase and remove non-alphanumeric (allow spaces for splitting)
|
| 82 |
+
cleaned = re.sub(r"[^a-zA-Z0-9\s]", "", task_description.lower())
|
| 83 |
+
|
| 84 |
+
# Tokenize and select top words
|
| 85 |
+
words = cleaned.split()
|
| 86 |
+
selected_words = words[:max_words] if words else ["script"]
|
| 87 |
+
|
| 88 |
+
# Join with underscores
|
| 89 |
+
base_name = "_".join(selected_words)
|
| 90 |
+
return f"{base_name}.py"
|
| 91 |
+
|
| 92 |
+
def go(self, task_description: str):
|
| 93 |
+
"""Implement the inherited function to get the tasks done.
|
| 94 |
+
|
| 95 |
+
Args:
|
| 96 |
+
task_description (str): task descriptions (possibly generated from previous steps)
|
| 97 |
+
|
| 98 |
+
Returns:
|
| 99 |
+
tuple: (script_filename, results) where script_filename is a generated name for script file and results is the generated codes
|
| 100 |
+
|
| 101 |
+
"""
|
| 102 |
+
self.log = []
|
| 103 |
+
self.log.append(
|
| 104 |
+
(
|
| 105 |
+
"user",
|
| 106 |
+
"Generate Python codes given a task description",
|
| 107 |
+
)
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
script_filename = self._generate_script_filename(task_description)
|
| 111 |
+
results = self._generate_code(task_description)
|
| 112 |
+
return script_filename, self._extract_code_block(results)
|
| 113 |
+
|
| 114 |
+
def _extract_code_block(self, s: str) -> str:
|
| 115 |
+
"""
|
| 116 |
+
Extract the first fenced code block (``` or ```python) from s.
|
| 117 |
+
"""
|
| 118 |
+
m = re.search(r"```(?:python)?\s*(.+?)\s*```", s, flags=re.DOTALL | re.IGNORECASE)
|
| 119 |
+
return m.group(1).strip() if m else None
|
Biomni/biomni/agent/qa_llm.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from langchain_core.prompts import ChatPromptTemplate
|
| 2 |
+
|
| 3 |
+
from biomni.llm import get_llm
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class qa_llm:
|
| 7 |
+
def __init__(self, path="./data", llm="claude-3-haiku-20240307", lab_bench_reproduce=False):
|
| 8 |
+
self.path = path
|
| 9 |
+
self.llm = get_llm(llm)
|
| 10 |
+
|
| 11 |
+
if lab_bench_reproduce:
|
| 12 |
+
self.prompt_modifier = """
|
| 13 |
+
The following is a multiple choice question about biology.
|
| 14 |
+
Please answer by responding with the letter of the correct answer.
|
| 15 |
+
|
| 16 |
+
Think step by step. \n
|
| 17 |
+
"""
|
| 18 |
+
else:
|
| 19 |
+
self.prompt_modifier = ""
|
| 20 |
+
self.log = []
|
| 21 |
+
|
| 22 |
+
def configure(self):
|
| 23 |
+
pass
|
| 24 |
+
|
| 25 |
+
def go(self, input):
|
| 26 |
+
self.log = []
|
| 27 |
+
self.log.append(("user", input))
|
| 28 |
+
message = self.llm.invoke(self.prompt_modifier + input)
|
| 29 |
+
self.log.append(("assistant", message.content))
|
| 30 |
+
return [message.content], message.content
|
| 31 |
+
|
| 32 |
+
def result_formatting(self, output_class, task_intention):
|
| 33 |
+
self.format_check_prompt = ChatPromptTemplate.from_messages(
|
| 34 |
+
[
|
| 35 |
+
(
|
| 36 |
+
"system",
|
| 37 |
+
(
|
| 38 |
+
"You are evaluateGPT, tasked with extract and parse the task output based on the history of an agent. "
|
| 39 |
+
"Review the entire history of messages provided. "
|
| 40 |
+
"Here is the task output requirement: \n"
|
| 41 |
+
f"'{task_intention.replace('{', '{{').replace('}', '}}')}'.\n"
|
| 42 |
+
),
|
| 43 |
+
),
|
| 44 |
+
("placeholder", "{messages}"),
|
| 45 |
+
]
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
checker_llm = self.format_check_prompt | self.llm.with_structured_output(output_class)
|
| 49 |
+
result = checker_llm.invoke({"messages": [("user", str(self.log))]}).dict()
|
| 50 |
+
return result
|
Biomni/biomni/agent/react.py
ADDED
|
@@ -0,0 +1,465 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import glob
|
| 2 |
+
import inspect
|
| 3 |
+
import json
|
| 4 |
+
import os
|
| 5 |
+
import signal
|
| 6 |
+
from collections.abc import Sequence
|
| 7 |
+
from functools import wraps
|
| 8 |
+
from multiprocessing import Process, Queue
|
| 9 |
+
from typing import Annotated, TypedDict
|
| 10 |
+
|
| 11 |
+
from langchain_core.messages import BaseMessage, SystemMessage, ToolMessage
|
| 12 |
+
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
| 13 |
+
from langchain_core.runnables import RunnableConfig
|
| 14 |
+
from langgraph.graph import END, StateGraph
|
| 15 |
+
from langgraph.graph.message import add_messages
|
| 16 |
+
|
| 17 |
+
from biomni.config import default_config
|
| 18 |
+
from biomni.env_desc import data_lake_dict, library_content_dict
|
| 19 |
+
from biomni.llm import get_llm
|
| 20 |
+
from biomni.model.retriever import ToolRetriever
|
| 21 |
+
from biomni.tool.tool_registry import ToolRegistry
|
| 22 |
+
from biomni.utils import (
|
| 23 |
+
api_schema_to_langchain_tool,
|
| 24 |
+
function_to_api_schema,
|
| 25 |
+
pretty_print,
|
| 26 |
+
read_module2api,
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
# Define the AgentState TypedDict for our custom implementation
|
| 31 |
+
class AgentState(TypedDict):
|
| 32 |
+
"""The state of the agent."""
|
| 33 |
+
|
| 34 |
+
# add_messages is a reducer that combines message sequences
|
| 35 |
+
messages: Annotated[Sequence[BaseMessage], add_messages]
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class react:
|
| 39 |
+
def __init__(
|
| 40 |
+
self,
|
| 41 |
+
path: str | None = None,
|
| 42 |
+
llm: str | None = None,
|
| 43 |
+
use_tool_retriever: bool | None = None,
|
| 44 |
+
timeout_seconds: int | None = None,
|
| 45 |
+
):
|
| 46 |
+
# Use default_config values for unspecified parameters
|
| 47 |
+
if path is None:
|
| 48 |
+
path = default_config.path
|
| 49 |
+
if llm is None:
|
| 50 |
+
llm = default_config.llm
|
| 51 |
+
if use_tool_retriever is None:
|
| 52 |
+
use_tool_retriever = default_config.use_tool_retriever
|
| 53 |
+
if timeout_seconds is None:
|
| 54 |
+
timeout_seconds = default_config.timeout_seconds
|
| 55 |
+
|
| 56 |
+
self.path = path
|
| 57 |
+
if not os.path.exists(path):
|
| 58 |
+
os.makedirs(path)
|
| 59 |
+
print(f"Created directory: {path}")
|
| 60 |
+
### TODO: Download the data
|
| 61 |
+
else:
|
| 62 |
+
print(f"Data directory already exists: {path}, loading...")
|
| 63 |
+
|
| 64 |
+
module2api = read_module2api()
|
| 65 |
+
|
| 66 |
+
self.llm = get_llm(llm, config=default_config)
|
| 67 |
+
tools = []
|
| 68 |
+
for module, api_list in module2api.items():
|
| 69 |
+
print("Registering tools from module:", module)
|
| 70 |
+
tools += [api_schema_to_langchain_tool(api, mode="custom_tool", module_name=module) for api in api_list]
|
| 71 |
+
self.tools = tools
|
| 72 |
+
self.module2api = module2api
|
| 73 |
+
self.use_tool_retriever = use_tool_retriever
|
| 74 |
+
|
| 75 |
+
# Store dictionaries for data lake and library content
|
| 76 |
+
self.data_lake_dict = data_lake_dict
|
| 77 |
+
self.library_content_dict = library_content_dict
|
| 78 |
+
|
| 79 |
+
if self.use_tool_retriever:
|
| 80 |
+
self.tool_registry = ToolRegistry(module2api)
|
| 81 |
+
self.retriever = ToolRetriever()
|
| 82 |
+
|
| 83 |
+
self.timeout_seconds = timeout_seconds # 10 minutes default timeout
|
| 84 |
+
|
| 85 |
+
# When wrapping tools with timeout
|
| 86 |
+
self.tools = self._add_timeout_to_tools(self.tools)
|
| 87 |
+
self.prompt = ""
|
| 88 |
+
self.system_prompt = ""
|
| 89 |
+
|
| 90 |
+
def _add_timeout_to_tools(self, tools):
|
| 91 |
+
"""Apply timeout wrapper to all tool functions using multiprocessing."""
|
| 92 |
+
|
| 93 |
+
def create_timed_func(original_func, timeout):
|
| 94 |
+
"""Factory function that creates a unique timed function for each tool."""
|
| 95 |
+
tool_name = getattr(original_func, "__name__", "unknown")
|
| 96 |
+
# print(f"Applying timeout wrapper to tool: {tool_name}")
|
| 97 |
+
|
| 98 |
+
def process_func(func, args, kwargs, result_queue):
|
| 99 |
+
"""Function to run in a separate process."""
|
| 100 |
+
try:
|
| 101 |
+
result = func(*args, **kwargs)
|
| 102 |
+
result_queue.put(("success", result))
|
| 103 |
+
except Exception as e:
|
| 104 |
+
result_queue.put(("error", str(e)))
|
| 105 |
+
|
| 106 |
+
@wraps(original_func)
|
| 107 |
+
def timed_func(*args, **kwargs):
|
| 108 |
+
# print(f"Executing tool with timeout: {tool_name}")
|
| 109 |
+
result_queue = Queue()
|
| 110 |
+
|
| 111 |
+
# Start a separate process
|
| 112 |
+
proc = Process(
|
| 113 |
+
target=process_func,
|
| 114 |
+
args=(original_func, args, kwargs, result_queue),
|
| 115 |
+
)
|
| 116 |
+
proc.start()
|
| 117 |
+
|
| 118 |
+
# Wait for the specified timeout
|
| 119 |
+
proc.join(timeout)
|
| 120 |
+
|
| 121 |
+
# Check if the process is still running after timeout
|
| 122 |
+
if proc.is_alive():
|
| 123 |
+
print(f"TIMEOUT: Tool {tool_name} execution timed out after {timeout} seconds")
|
| 124 |
+
# Force terminate the process
|
| 125 |
+
proc.terminate()
|
| 126 |
+
proc.join(1) # Give it a second to terminate
|
| 127 |
+
|
| 128 |
+
# If it's still not dead, kill it with more force
|
| 129 |
+
if proc.is_alive():
|
| 130 |
+
os.kill(proc.pid, signal.SIGKILL)
|
| 131 |
+
|
| 132 |
+
return f"ERROR: Tool execution timed out after {timeout} seconds. Please try with simpler inputs or break your task into smaller steps."
|
| 133 |
+
|
| 134 |
+
# Get the result from the queue
|
| 135 |
+
if not result_queue.empty():
|
| 136 |
+
status, result = result_queue.get()
|
| 137 |
+
if status == "success":
|
| 138 |
+
return result
|
| 139 |
+
else:
|
| 140 |
+
return f"Error in tool execution: {result}"
|
| 141 |
+
|
| 142 |
+
return "Error: Tool execution completed but no result was returned"
|
| 143 |
+
|
| 144 |
+
return timed_func
|
| 145 |
+
|
| 146 |
+
wrapped_tools = []
|
| 147 |
+
for tool in tools:
|
| 148 |
+
wrapped_tool = tool
|
| 149 |
+
wrapped_tool.func = create_timed_func(tool.func, self.timeout_seconds)
|
| 150 |
+
wrapped_tools.append(wrapped_tool)
|
| 151 |
+
|
| 152 |
+
return wrapped_tools
|
| 153 |
+
|
| 154 |
+
def add_tool(self, api):
|
| 155 |
+
function_code = inspect.getsource(api)
|
| 156 |
+
schema = function_to_api_schema(function_code, self.llm)
|
| 157 |
+
new_tool = api_schema_to_langchain_tool(schema, mode="custom_tool", module_name=api.__module__)
|
| 158 |
+
|
| 159 |
+
# Create a single wrapped tool using the existing _add_timeout_to_tools method
|
| 160 |
+
wrapped_tools = self._add_timeout_to_tools([new_tool])
|
| 161 |
+
|
| 162 |
+
# Get the wrapped tool and add it to our tools list
|
| 163 |
+
if wrapped_tools:
|
| 164 |
+
self.tools.append(wrapped_tools[0])
|
| 165 |
+
|
| 166 |
+
def configure(
|
| 167 |
+
self,
|
| 168 |
+
plan=False,
|
| 169 |
+
reflect=False,
|
| 170 |
+
data_lake=False,
|
| 171 |
+
react_code_search=False,
|
| 172 |
+
library_access=False,
|
| 173 |
+
):
|
| 174 |
+
data_lake_path = self.path + "/data_lake"
|
| 175 |
+
data_lake_content = glob.glob(data_lake_path + "/*")
|
| 176 |
+
data_lake_items = [x.split("/")[-1] for x in data_lake_content]
|
| 177 |
+
|
| 178 |
+
if react_code_search:
|
| 179 |
+
tools = [i for i in self.tools if i.name in ["run_python_repl", "search_google"]]
|
| 180 |
+
|
| 181 |
+
prompt_modifier = """
|
| 182 |
+
You are a helpful biomedical assistant assigned with the task of problem-solving.
|
| 183 |
+
|
| 184 |
+
You have access to two tools:
|
| 185 |
+
1) run_python_repl: to write and run python code
|
| 186 |
+
2) search_google: to search google for information
|
| 187 |
+
|
| 188 |
+
You can use them to solve the problem.
|
| 189 |
+
"""
|
| 190 |
+
else:
|
| 191 |
+
tools = self.tools
|
| 192 |
+
if (not plan) and (not reflect):
|
| 193 |
+
prompt_modifier = """You are a helpful biologist and expert geneticist.
|
| 194 |
+
"""
|
| 195 |
+
elif plan and (not reflect):
|
| 196 |
+
prompt_modifier = """You are a helpful biologist and expert geneticist.
|
| 197 |
+
Given the question from the user,
|
| 198 |
+
- First, come up with a high level plan based on your understanding of the problem and available tools and record it in the Research Plan and Status. You can revise the plan later.
|
| 199 |
+
- Research Plan and Status should well organized and succinctly keep track of 1) high level plan (can be revised), 2) what steps have been done and what steps are in progress, 3) short results and conclusions of each step after it has been performed. Do not perform action in research plan.
|
| 200 |
+
- Research Plan and Status must only include progress that has been made by previous steps. It should not include results not directly confirmed by the previous observation.
|
| 201 |
+
- Follow the plan and try to achieve the goal as straightforwardly as possible. Use tools as necessary.
|
| 202 |
+
"""
|
| 203 |
+
elif (not plan) and reflect:
|
| 204 |
+
prompt_modifier = """You are a helpful biologist and expert geneticist.
|
| 205 |
+
In each round after the tool is used, conduct "reflection" step: reflect on the current state of the problem and the results of the last round. What does the observation mean? If there is an error, what caused the error and how to debug?
|
| 206 |
+
"""
|
| 207 |
+
else:
|
| 208 |
+
prompt_modifier = """You are a helpful biologist and expert geneticist.
|
| 209 |
+
Given the question from the user,
|
| 210 |
+
- First, come up with a high level plan based on your understanding of the problem and available tools and record it in the Research Plan and Status. You can revise the plan later.
|
| 211 |
+
- Research Plan and Status should well organized and succinctly keep track of 1) high level plan (can be revised), 2) what steps have been done and what steps are in progress, 3) short results and conclusions of each step after it has been performed. Do not perform action in research plan.
|
| 212 |
+
- Research Plan and Status must only include progress that has been made by previous steps. It should not include results not directly confirmed by the previous observation.
|
| 213 |
+
- Follow the plan and try to achieve the goal as straightforwardly as possible. Use tools as necessary.
|
| 214 |
+
In each round after the tool is used, conduct "reflection" step: reflect on the current state of the problem and the results of the last round. What does the observation mean? If there is an error, what caused the error and how to debug?
|
| 215 |
+
You have access to write_python_code and run_python_repl tool to write and run your own code if tools fail, or if the given tools are not enough. Please always make sure to write code when dealing with substantial data, including finding the length of long sequences or elements at different positions.
|
| 216 |
+
"""
|
| 217 |
+
|
| 218 |
+
if data_lake:
|
| 219 |
+
# Format data lake items with descriptions
|
| 220 |
+
data_lake_formatted = []
|
| 221 |
+
for item in data_lake_items:
|
| 222 |
+
description = data_lake_dict.get(item, f"Data lake item: {item}")
|
| 223 |
+
data_lake_formatted.append(f"{item}: {description}")
|
| 224 |
+
|
| 225 |
+
prompt_modifier += """
|
| 226 |
+
You can also access a biological data lake at the following path: {data_lake_path}. You can use the run_python_repl tool to write code to understand the data, process and utilize it for the task.
|
| 227 |
+
Here is the list of datasets with their descriptions:
|
| 228 |
+
----
|
| 229 |
+
{data_lake_formatted}
|
| 230 |
+
----
|
| 231 |
+
""".format(
|
| 232 |
+
data_lake_path=data_lake_path,
|
| 233 |
+
data_lake_formatted="\n".join(data_lake_formatted),
|
| 234 |
+
)
|
| 235 |
+
|
| 236 |
+
if library_access:
|
| 237 |
+
# Format library content with descriptions
|
| 238 |
+
library_formatted = []
|
| 239 |
+
for lib_name, lib_desc in library_content_dict.items():
|
| 240 |
+
library_formatted.append(f"{lib_name}: {lib_desc}")
|
| 241 |
+
|
| 242 |
+
prompt_modifier += """
|
| 243 |
+
You also have access to a list of software packages that can be used to perform various tasks.
|
| 244 |
+
You can use the run_python_repl tool to write code to access and utilize the library for the task.
|
| 245 |
+
Don't forget the import statement.
|
| 246 |
+
Here is the list of available libraries with their descriptions:
|
| 247 |
+
----
|
| 248 |
+
{library_formatted}
|
| 249 |
+
----
|
| 250 |
+
""".format(library_formatted="\n".join(library_formatted))
|
| 251 |
+
|
| 252 |
+
print("=" * 25 + "System Prompt" + "=" * 25)
|
| 253 |
+
print(prompt_modifier)
|
| 254 |
+
self.system_prompt = prompt_modifier
|
| 255 |
+
self.prompt = ChatPromptTemplate.from_messages(
|
| 256 |
+
[
|
| 257 |
+
("system", prompt_modifier),
|
| 258 |
+
MessagesPlaceholder(variable_name="messages"),
|
| 259 |
+
]
|
| 260 |
+
)
|
| 261 |
+
|
| 262 |
+
# Store the tools for later use
|
| 263 |
+
self.active_tools = tools
|
| 264 |
+
|
| 265 |
+
# Create a custom implementation of the ReAct agent using LangGraph
|
| 266 |
+
self.app = self._create_custom_react_agent(self.llm, tools, self.prompt)
|
| 267 |
+
|
| 268 |
+
def _create_custom_react_agent(self, llm, tools, prompt):
|
| 269 |
+
"""Create a custom ReAct agent using LangGraph."""
|
| 270 |
+
# Create a dictionary mapping tool names to tool objects for easy lookup
|
| 271 |
+
tools_by_name = {tool.name: tool for tool in tools}
|
| 272 |
+
|
| 273 |
+
# Bind the tools to the language model
|
| 274 |
+
llm_with_tools = llm.bind_tools(tools)
|
| 275 |
+
|
| 276 |
+
# Define the node that calls the model
|
| 277 |
+
def call_model(state: AgentState, config: RunnableConfig = None):
|
| 278 |
+
"""Node that calls the language model to get the next action."""
|
| 279 |
+
system_message = SystemMessage(content=self.system_prompt)
|
| 280 |
+
messages = [system_message] + state["messages"]
|
| 281 |
+
response = llm_with_tools.invoke(messages, config=config)
|
| 282 |
+
return {"messages": [response]}
|
| 283 |
+
|
| 284 |
+
# Define the node that executes tools
|
| 285 |
+
def tool_node(state: AgentState):
|
| 286 |
+
"""Node that executes tools based on the LLM's decisions."""
|
| 287 |
+
outputs = []
|
| 288 |
+
for tool_call in state["messages"][-1].tool_calls:
|
| 289 |
+
try:
|
| 290 |
+
tool_result = tools_by_name[tool_call["name"]].invoke(tool_call["args"])
|
| 291 |
+
outputs.append(
|
| 292 |
+
ToolMessage(
|
| 293 |
+
content=json.dumps(tool_result),
|
| 294 |
+
name=tool_call["name"],
|
| 295 |
+
tool_call_id=tool_call["id"],
|
| 296 |
+
)
|
| 297 |
+
)
|
| 298 |
+
except Exception as e:
|
| 299 |
+
# Handle any errors that occur during tool execution
|
| 300 |
+
outputs.append(
|
| 301 |
+
ToolMessage(
|
| 302 |
+
content=json.dumps({"error": str(e)}),
|
| 303 |
+
name=tool_call["name"],
|
| 304 |
+
tool_call_id=tool_call["id"],
|
| 305 |
+
)
|
| 306 |
+
)
|
| 307 |
+
return {"messages": outputs}
|
| 308 |
+
|
| 309 |
+
# Define the conditional edge that determines whether to continue or not
|
| 310 |
+
def should_continue(state: AgentState):
|
| 311 |
+
"""Determine if we should continue running the graph or finish."""
|
| 312 |
+
messages = state["messages"]
|
| 313 |
+
last_message = messages[-1]
|
| 314 |
+
# If there is no tool call, then we finish
|
| 315 |
+
if not hasattr(last_message, "tool_calls") or not last_message.tool_calls:
|
| 316 |
+
return "end"
|
| 317 |
+
# Otherwise if there is, we continue
|
| 318 |
+
else:
|
| 319 |
+
return "continue"
|
| 320 |
+
|
| 321 |
+
# Define a new graph
|
| 322 |
+
workflow = StateGraph(AgentState)
|
| 323 |
+
|
| 324 |
+
# Define the two nodes we will cycle between
|
| 325 |
+
workflow.add_node("agent", call_model)
|
| 326 |
+
workflow.add_node("tools", tool_node)
|
| 327 |
+
|
| 328 |
+
# Set the entrypoint as `agent`
|
| 329 |
+
workflow.set_entry_point("agent")
|
| 330 |
+
|
| 331 |
+
# Add conditional edges
|
| 332 |
+
workflow.add_conditional_edges(
|
| 333 |
+
"agent",
|
| 334 |
+
should_continue,
|
| 335 |
+
{
|
| 336 |
+
"continue": "tools",
|
| 337 |
+
"end": END,
|
| 338 |
+
},
|
| 339 |
+
)
|
| 340 |
+
|
| 341 |
+
# Add edge from tools back to agent
|
| 342 |
+
workflow.add_edge("tools", "agent")
|
| 343 |
+
|
| 344 |
+
# Compile the graph
|
| 345 |
+
return workflow.compile()
|
| 346 |
+
|
| 347 |
+
def go(self, prompt):
|
| 348 |
+
"""Execute the agent with the given prompt.
|
| 349 |
+
|
| 350 |
+
Args:
|
| 351 |
+
prompt: The user's query
|
| 352 |
+
|
| 353 |
+
"""
|
| 354 |
+
if self.use_tool_retriever:
|
| 355 |
+
# Gather all available tools from the registry
|
| 356 |
+
all_tools = self.tool_registry.tools if hasattr(self, "tool_registry") else []
|
| 357 |
+
|
| 358 |
+
# Get data lake items with descriptions
|
| 359 |
+
data_lake_path = self.path + "/data_lake"
|
| 360 |
+
data_lake_content = glob.glob(data_lake_path + "/*")
|
| 361 |
+
data_lake_items = [x.split("/")[-1] for x in data_lake_content]
|
| 362 |
+
|
| 363 |
+
# Create data lake descriptions for retrieval
|
| 364 |
+
data_lake_descriptions = []
|
| 365 |
+
for item in data_lake_items:
|
| 366 |
+
description = self.data_lake_dict.get(item, f"Data lake item: {item}")
|
| 367 |
+
data_lake_descriptions.append({"name": item, "description": description})
|
| 368 |
+
|
| 369 |
+
# Libraries with descriptions
|
| 370 |
+
library_descriptions = []
|
| 371 |
+
for lib_name, lib_desc in self.library_content_dict.items():
|
| 372 |
+
library_descriptions.append({"name": lib_name, "description": lib_desc})
|
| 373 |
+
|
| 374 |
+
# Prepare resources for retrieval
|
| 375 |
+
resources = {
|
| 376 |
+
"tools": all_tools,
|
| 377 |
+
"data_lake": data_lake_descriptions,
|
| 378 |
+
"libraries": library_descriptions,
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
# Use prompt-based retrieval with the agent's LLM
|
| 382 |
+
selected_resources = self.retriever.prompt_based_retrieval(prompt, resources, llm=self.llm)
|
| 383 |
+
print("Using prompt-based retrieval with the agent's LLM")
|
| 384 |
+
|
| 385 |
+
# If we're using prompt or embedding based retrieval, print the selected resources
|
| 386 |
+
print("\nSelected tools:")
|
| 387 |
+
for tool in selected_resources["tools"]:
|
| 388 |
+
if isinstance(tool, dict):
|
| 389 |
+
print(f"- {tool.get('name', 'Unknown')}: {tool.get('description', '')}")
|
| 390 |
+
else:
|
| 391 |
+
print(f"- {getattr(tool, 'name', str(tool))}: {getattr(tool, 'description', '')}")
|
| 392 |
+
|
| 393 |
+
print("\nSelected data lake items:")
|
| 394 |
+
for item in selected_resources["data_lake"]:
|
| 395 |
+
if isinstance(item, dict):
|
| 396 |
+
name = item.get("name", "Unknown")
|
| 397 |
+
description = self.data_lake_dict.get(name, f"Data lake item: {name}")
|
| 398 |
+
print(f"- {name}: {description}")
|
| 399 |
+
elif isinstance(item, str) and ": " in item:
|
| 400 |
+
# If the item already has a description, print it as is
|
| 401 |
+
print(f"- {item}")
|
| 402 |
+
else:
|
| 403 |
+
description = self.data_lake_dict.get(item, f"Data lake item: {item}")
|
| 404 |
+
print(f"- {item}: {description}")
|
| 405 |
+
|
| 406 |
+
print("\nSelected libraries:")
|
| 407 |
+
for lib in selected_resources["libraries"]:
|
| 408 |
+
if isinstance(lib, dict):
|
| 409 |
+
print(f"- {lib.get('name', 'Unknown')}: {lib.get('description', '')}")
|
| 410 |
+
else:
|
| 411 |
+
print(f"- {lib}")
|
| 412 |
+
|
| 413 |
+
# Convert selected tools to langchain tool objects
|
| 414 |
+
tool_names = [
|
| 415 |
+
tool["name"] if isinstance(tool, dict) else getattr(tool, "name", str(tool))
|
| 416 |
+
for tool in selected_resources["tools"]
|
| 417 |
+
]
|
| 418 |
+
retrieved_list_of_tools = []
|
| 419 |
+
|
| 420 |
+
# Get the tool objects by name
|
| 421 |
+
for tool_name in tool_names:
|
| 422 |
+
# Find the tool in the original tools list
|
| 423 |
+
matching_tools = [t for t in self.tools if getattr(t, "name", None) == tool_name]
|
| 424 |
+
if matching_tools:
|
| 425 |
+
retrieved_list_of_tools.append(matching_tools[0])
|
| 426 |
+
|
| 427 |
+
# Add back coding tool if not already included
|
| 428 |
+
if len([i for i in retrieved_list_of_tools if i.name == "run_python_repl"]) == 0:
|
| 429 |
+
retrieved_list_of_tools = retrieved_list_of_tools + [
|
| 430 |
+
i for i in self.tools if i.name == "run_python_repl"
|
| 431 |
+
]
|
| 432 |
+
|
| 433 |
+
print("Retrieved tools: \n" + "\n".join([l.name + ": " + l.description for l in retrieved_list_of_tools]))
|
| 434 |
+
# Recreate the custom agent with the retrieved tools
|
| 435 |
+
self.app = self._create_custom_react_agent(self.llm, retrieved_list_of_tools, self.prompt)
|
| 436 |
+
|
| 437 |
+
# Default behavior (no tool retriever or retrieval_method is 'none')
|
| 438 |
+
config = {"recursion_limit": 50}
|
| 439 |
+
inputs = {"messages": [("user", prompt)]}
|
| 440 |
+
self.log = []
|
| 441 |
+
for s in self.app.stream(inputs, stream_mode="values", config=config):
|
| 442 |
+
message = s["messages"][-1]
|
| 443 |
+
out = pretty_print(message)
|
| 444 |
+
self.log.append(out)
|
| 445 |
+
return self.log, s["messages"][-1].content
|
| 446 |
+
|
| 447 |
+
def result_formatting(self, output_class, task_intention):
|
| 448 |
+
self.format_check_prompt = ChatPromptTemplate.from_messages(
|
| 449 |
+
[
|
| 450 |
+
(
|
| 451 |
+
"system",
|
| 452 |
+
(
|
| 453 |
+
"You are evaluateGPT, tasked with extract and parse the task output based on the history of an agent. "
|
| 454 |
+
"Review the entire history of messages provided. "
|
| 455 |
+
"Here is the task output requirement: \n"
|
| 456 |
+
f"'{task_intention.replace('{', '{{').replace('}', '}}')}'.\n"
|
| 457 |
+
),
|
| 458 |
+
),
|
| 459 |
+
("placeholder", "{messages}"),
|
| 460 |
+
]
|
| 461 |
+
)
|
| 462 |
+
|
| 463 |
+
checker_llm = self.format_check_prompt | self.llm.with_structured_output(output_class)
|
| 464 |
+
result = checker_llm.invoke({"messages": [("user", str(self.log))]}).dict()
|
| 465 |
+
return result
|
Biomni/biomni/config.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Biomni Configuration Management
|
| 3 |
+
|
| 4 |
+
Simple configuration class for centralizing common settings.
|
| 5 |
+
Maintains full backward compatibility with existing code.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import os
|
| 9 |
+
from dataclasses import dataclass
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
@dataclass
|
| 13 |
+
class BiomniConfig:
|
| 14 |
+
"""Central configuration for Biomni agent.
|
| 15 |
+
|
| 16 |
+
All settings are optional and have sensible defaults.
|
| 17 |
+
API keys are still read from environment variables to maintain
|
| 18 |
+
compatibility with existing .env file structure.
|
| 19 |
+
|
| 20 |
+
Usage:
|
| 21 |
+
# Create config with defaults
|
| 22 |
+
config = BiomniConfig()
|
| 23 |
+
|
| 24 |
+
# Override specific settings
|
| 25 |
+
config = BiomniConfig(llm="gpt-4", timeout_seconds=1200)
|
| 26 |
+
|
| 27 |
+
# Modify after creation
|
| 28 |
+
config.path = "./custom_data"
|
| 29 |
+
"""
|
| 30 |
+
|
| 31 |
+
# Data and execution settings
|
| 32 |
+
path: str = "./data"
|
| 33 |
+
timeout_seconds: int = 600
|
| 34 |
+
|
| 35 |
+
# LLM settings (API keys still from environment)
|
| 36 |
+
llm: str = "claude-sonnet-4-5"
|
| 37 |
+
temperature: float = 0.7
|
| 38 |
+
|
| 39 |
+
# Tool settings
|
| 40 |
+
use_tool_retriever: bool = True
|
| 41 |
+
|
| 42 |
+
# Data licensing settings
|
| 43 |
+
commercial_mode: bool = False # If True, excludes non-commercial datasets
|
| 44 |
+
|
| 45 |
+
# Custom model settings (for custom LLM serving)
|
| 46 |
+
base_url: str | None = None
|
| 47 |
+
api_key: str | None = None # Only for custom models, not provider API keys
|
| 48 |
+
|
| 49 |
+
# LLM source (auto-detected if None)
|
| 50 |
+
source: str | None = None
|
| 51 |
+
|
| 52 |
+
# Third-party integrations
|
| 53 |
+
protocols_io_access_token: str | None = None
|
| 54 |
+
|
| 55 |
+
def __post_init__(self):
|
| 56 |
+
"""Load any environment variable overrides if they exist."""
|
| 57 |
+
# Check for environment variable overrides (optional)
|
| 58 |
+
# Support both old and new names for backwards compatibility
|
| 59 |
+
if os.getenv("BIOMNI_PATH") or os.getenv("BIOMNI_DATA_PATH"):
|
| 60 |
+
self.path = os.getenv("BIOMNI_PATH") or os.getenv("BIOMNI_DATA_PATH")
|
| 61 |
+
if os.getenv("BIOMNI_TIMEOUT_SECONDS"):
|
| 62 |
+
self.timeout_seconds = int(os.getenv("BIOMNI_TIMEOUT_SECONDS"))
|
| 63 |
+
if os.getenv("BIOMNI_LLM") or os.getenv("BIOMNI_LLM_MODEL"):
|
| 64 |
+
self.llm = os.getenv("BIOMNI_LLM") or os.getenv("BIOMNI_LLM_MODEL")
|
| 65 |
+
if os.getenv("BIOMNI_USE_TOOL_RETRIEVER"):
|
| 66 |
+
self.use_tool_retriever = os.getenv("BIOMNI_USE_TOOL_RETRIEVER").lower() == "true"
|
| 67 |
+
if os.getenv("BIOMNI_COMMERCIAL_MODE"):
|
| 68 |
+
self.commercial_mode = os.getenv("BIOMNI_COMMERCIAL_MODE").lower() == "true"
|
| 69 |
+
if os.getenv("BIOMNI_TEMPERATURE"):
|
| 70 |
+
self.temperature = float(os.getenv("BIOMNI_TEMPERATURE"))
|
| 71 |
+
if os.getenv("BIOMNI_CUSTOM_BASE_URL"):
|
| 72 |
+
self.base_url = os.getenv("BIOMNI_CUSTOM_BASE_URL")
|
| 73 |
+
if os.getenv("BIOMNI_CUSTOM_API_KEY"):
|
| 74 |
+
self.api_key = os.getenv("BIOMNI_CUSTOM_API_KEY")
|
| 75 |
+
if os.getenv("BIOMNI_SOURCE"):
|
| 76 |
+
self.source = os.getenv("BIOMNI_SOURCE")
|
| 77 |
+
|
| 78 |
+
# Protocols.io access token (prefer specific env vars)
|
| 79 |
+
env_token = os.getenv("PROTOCOLS_IO_ACCESS_TOKEN") or os.getenv("BIOMNI_PROTOCOLS_IO_ACCESS_TOKEN")
|
| 80 |
+
if env_token:
|
| 81 |
+
self.protocols_io_access_token = env_token
|
| 82 |
+
|
| 83 |
+
def to_dict(self) -> dict:
|
| 84 |
+
"""Convert config to dictionary for easy access."""
|
| 85 |
+
return {
|
| 86 |
+
"path": self.path,
|
| 87 |
+
"timeout_seconds": self.timeout_seconds,
|
| 88 |
+
"llm": self.llm,
|
| 89 |
+
"temperature": self.temperature,
|
| 90 |
+
"use_tool_retriever": self.use_tool_retriever,
|
| 91 |
+
"commercial_mode": self.commercial_mode,
|
| 92 |
+
"base_url": self.base_url,
|
| 93 |
+
"api_key": self.api_key,
|
| 94 |
+
"source": self.source,
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
# Global default config instance (optional, for convenience)
|
| 99 |
+
default_config = BiomniConfig()
|
Biomni/biomni/env_desc.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Data lake dictionary with detailed descriptions
|
| 2 |
+
data_lake_dict = {
|
| 3 |
+
"affinity_capture-ms.parquet": "Protein-protein interactions detected via affinity capture and mass spectrometry.",
|
| 4 |
+
"affinity_capture-rna.parquet": "Protein-RNA interactions detected by affinity capture.",
|
| 5 |
+
"BindingDB_All_202409.tsv": "Measured binding affinities between proteins and small molecules for drug discovery.",
|
| 6 |
+
"broad_repurposing_hub_molecule_with_smiles.parquet": "Molecules from Broad Institute's Drug Repurposing Hub with SMILES annotations.",
|
| 7 |
+
"broad_repurposing_hub_phase_moa_target_info.parquet": "Drug phases, mechanisms of action, and target information from Broad Institute.",
|
| 8 |
+
"co-fractionation.parquet": "Protein-protein interactions from co-fractionation experiments.",
|
| 9 |
+
"czi_census_datasets_v4.parquet": "Datasets from the Chan Zuckerberg Initiative's Cell Census.",
|
| 10 |
+
"DepMap_CRISPRGeneDependency.csv": "Gene dependency probability estimates for cancer cell lines, including all DepMap models.",
|
| 11 |
+
"DepMap_CRISPRGeneEffect.csv": "Genome-wide CRISPR gene effect estimates for cancer cell lines, including all DepMap models.",
|
| 12 |
+
"DepMap_Model.csv": "Metadata describing all cancer models/cell lines which are referenced by a dataset contained within the DepMap portal.",
|
| 13 |
+
"DepMap_OmicsExpressionProteinCodingGenesTPMLogp1.csv": "Gene expression in TPMs for cancer cell lines, including all DepMap models.",
|
| 14 |
+
"ddinter_alimentary_tract_metabolism.csv": "Drug-drug interactions for alimentary tract and metabolism drugs from DDInter 2.0 database.",
|
| 15 |
+
"ddinter_antineoplastic.csv": "Drug-drug interactions for antineoplastic and immunomodulating agents from DDInter 2.0 database.",
|
| 16 |
+
"ddinter_antiparasitic.csv": "Drug-drug interactions for antiparasitic products from DDInter 2.0 database.",
|
| 17 |
+
"ddinter_blood_organs.csv": "Drug-drug interactions for blood and blood forming organs drugs from DDInter 2.0 database.",
|
| 18 |
+
"ddinter_dermatological.csv": "Drug-drug interactions for dermatological drugs from DDInter 2.0 database.",
|
| 19 |
+
"ddinter_hormonal.csv": "Drug-drug interactions for systemic hormonal preparations from DDInter 2.0 database.",
|
| 20 |
+
"ddinter_respiratory.csv": "Drug-drug interactions for respiratory system drugs from DDInter 2.0 database.",
|
| 21 |
+
"ddinter_various.csv": "Drug-drug interactions for various drugs from DDInter 2.0 database.",
|
| 22 |
+
"DisGeNET.parquet": "Gene-disease associations from multiple sources.",
|
| 23 |
+
"dosage_growth_defect.parquet": "Gene dosage changes affecting growth.",
|
| 24 |
+
"enamine_cloud_library_smiles.pkl": "Compounds from Enamine REAL library with SMILES annotations.",
|
| 25 |
+
"evebio_assay_table.csv": "Assay metadata with one row per assay from EveBio pharmome mapping.",
|
| 26 |
+
"evebio_bundle_table.csv": "Target subfamily bundles used for screening-to-profiling progression.",
|
| 27 |
+
"evebio_compound_table.csv": "Compound metadata with common identifiers from EveBio screening.",
|
| 28 |
+
"evebio_control_table.csv": "Control datapoints for all screening and profiling plates.",
|
| 29 |
+
"evebio_detailed_result_table.csv": "Expanded results on evebio_summary_result_table with curve fit parameters and phase categories.",
|
| 30 |
+
"evebio_observed_points_table.csv": "Raw observed datapoints from all screening and profiling experiments.",
|
| 31 |
+
"evebio_summary_result_table.csv": "Succinct summary of results for each assay-compound combination.",
|
| 32 |
+
"evebio_target_table.csv": "Target metadata with common identifiers from EveBio screening.",
|
| 33 |
+
"genebass_missense_LC_filtered.pkl": "Filtered missense variants from GeneBass.",
|
| 34 |
+
"genebass_pLoF_filtered.pkl": "Predicted loss-of-function variants from GeneBass.",
|
| 35 |
+
"genebass_synonymous_filtered.pkl": "Filtered synonymous variants from GeneBass.",
|
| 36 |
+
"gene_info.parquet": "Comprehensive gene information.",
|
| 37 |
+
"genetic_interaction.parquet": "Genetic interactions between genes.",
|
| 38 |
+
"go-plus.json": "Gene ontology data for functional gene annotations.",
|
| 39 |
+
"gtex_tissue_gene_tpm.parquet": "Gene expression (TPM) across human tissues from GTEx.",
|
| 40 |
+
"gwas_catalog.pkl": "Genome-wide association studies (GWAS) results.",
|
| 41 |
+
"hp.obo": "Official HPO release in obographs format",
|
| 42 |
+
"kg.csv": "Precision medicine knowledge graph with 17,080 diseases and 4+ million relationships across biological scales.",
|
| 43 |
+
"marker_celltype.parquet": "Cell type marker genes for identification.",
|
| 44 |
+
"McPAS-TCR.parquet": "T-cell receptor sequences and specificity data from McPAS database.",
|
| 45 |
+
"miRDB_v6.0_results.parquet": "Predicted microRNA targets from miRDB.",
|
| 46 |
+
"miRTarBase_microRNA_target_interaction.parquet": "Experimentally validated microRNA-target interactions from miRTarBase.",
|
| 47 |
+
"miRTarBase_microRNA_target_interaction_pubmed_abtract.txt": "PubMed abstracts for microRNA-target interactions in miRTarBase.",
|
| 48 |
+
"miRTarBase_MicroRNA_Target_Sites.parquet": "Binding sites of microRNAs on target genes from miRTarBase.",
|
| 49 |
+
"mousemine_m1_positional_geneset.parquet": "Positional gene sets from MouseMine.",
|
| 50 |
+
"mousemine_m2_curated_geneset.parquet": "Curated gene sets from MouseMine.",
|
| 51 |
+
"mousemine_m3_regulatory_target_geneset.parquet": "Regulatory target gene sets from MouseMine.",
|
| 52 |
+
"mousemine_m5_ontology_geneset.parquet": "Ontology-based gene sets from MouseMine.",
|
| 53 |
+
"mousemine_m8_celltype_signature_geneset.parquet": "Cell type signature gene sets from MouseMine.",
|
| 54 |
+
"mousemine_mh_hallmark_geneset.parquet": "Hallmark gene sets from MouseMine.",
|
| 55 |
+
"msigdb_human_c1_positional_geneset.parquet": "Human positional gene sets from MSigDB.",
|
| 56 |
+
"msigdb_human_c2_curated_geneset.parquet": "Curated human gene sets from MSigDB.",
|
| 57 |
+
"msigdb_human_c3_regulatory_target_geneset.parquet": "Regulatory target gene sets from MSigDB.",
|
| 58 |
+
"msigdb_human_c3_subset_transcription_factor_targets_from_GTRD.parquet": "Transcription factor targets from GTRD/MSigDB.",
|
| 59 |
+
"msigdb_human_c4_computational_geneset.parquet": "Computationally derived gene sets from MSigDB.",
|
| 60 |
+
"msigdb_human_c5_ontology_geneset.parquet": "Ontology-based gene sets from MSigDB.",
|
| 61 |
+
"msigdb_human_c6_oncogenic_signature_geneset.parquet": "Oncogenic signatures from MSigDB.",
|
| 62 |
+
"msigdb_human_c7_immunologic_signature_geneset.parquet": "Immunologic signatures from MSigDB.",
|
| 63 |
+
"msigdb_human_c8_celltype_signature_geneset.parquet": "Cell type signatures from MSigDB.",
|
| 64 |
+
"msigdb_human_h_hallmark_geneset.parquet": "Hallmark gene sets from MSigDB.",
|
| 65 |
+
"omim.parquet": "Genetic disorders and associated genes from OMIM.",
|
| 66 |
+
"proteinatlas.tsv": "Protein expression data from Human Protein Atlas.",
|
| 67 |
+
"proximity_label-ms.parquet": "Protein interactions via proximity labeling and mass spectrometry.",
|
| 68 |
+
"reconstituted_complex.parquet": "Protein complexes reconstituted in vitro.",
|
| 69 |
+
"sgRNA_KO_SP_mouse.txt": "sgRNA knockout data for mouse.",
|
| 70 |
+
"sgRNA_KO_SP_human.txt": "sgRNA knockout data for human.",
|
| 71 |
+
"synthetic_growth_defect.parquet": "Synthetic growth defects from genetic interactions.",
|
| 72 |
+
"synthetic_lethality.parquet": "Synthetic lethal interactions.",
|
| 73 |
+
"synthetic_rescue.parquet": "Genetic interactions rescuing phenotypes.",
|
| 74 |
+
"two-hybrid.parquet": "Protein-protein interactions detected by yeast two-hybrid assays.",
|
| 75 |
+
"variant_table.parquet": "Annotated genetic variants table.",
|
| 76 |
+
"Virus-Host_PPI_P-HIPSTER_2020.parquet": "Virus-host protein-protein interactions from P-HIPSTER.",
|
| 77 |
+
"txgnn_name_mapping.pkl": "Name mapping for TXGNN.",
|
| 78 |
+
"txgnn_prediction.pkl": "Prediction data for TXGNN.",
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
# Updated library_content as a dictionary with detailed descriptions
|
| 82 |
+
library_content_dict = {
|
| 83 |
+
# === PYTHON PACKAGES ===
|
| 84 |
+
# Core Bioinformatics Libraries (Python)
|
| 85 |
+
"biopython": "[Python Package] A set of tools for biological computation including parsers for bioinformatics files, access to online services, and interfaces to common bioinformatics programs.",
|
| 86 |
+
"biom-format": "[Python Package] The Biological Observation Matrix (BIOM) format is designed for representing biological sample by observation contingency tables with associated metadata.",
|
| 87 |
+
"scanpy": "[Python Package] A scalable toolkit for analyzing single-cell gene expression data, specifically designed for large datasets using AnnData.",
|
| 88 |
+
"scikit-bio": "[Python Package] Data structures, algorithms, and educational resources for bioinformatics, including sequence analysis, phylogenetics, and ordination methods.",
|
| 89 |
+
"anndata": "[Python Package] A Python package for handling annotated data matrices in memory and on disk, primarily used for single-cell genomics data.",
|
| 90 |
+
"mudata": "[Python Package] A Python package for multimodal data storage and manipulation, extending AnnData to handle multiple modalities.",
|
| 91 |
+
"pyliftover": "[Python Package] A Python implementation of UCSC liftOver tool for converting genomic coordinates between genome assemblies.",
|
| 92 |
+
"biopandas": "[Python Package] A package that provides pandas DataFrames for working with molecular structures and biological data.",
|
| 93 |
+
"biotite": "[Python Package] A comprehensive library for computational molecular biology, providing tools for sequence analysis, structure analysis, and more.",
|
| 94 |
+
"lazyslide": "[Python Package] A Python framework that brings interoperable, reproducible whole slide image analysis, enabling seamless histopathology workflows from preprocessing to deep learning.",
|
| 95 |
+
# Genomics & Variant Analysis (Python)
|
| 96 |
+
"gget": "[Python Package] A toolkit for accessing genomic databases and retrieving sequences, annotations, and other genomic data.",
|
| 97 |
+
"lifelines": "[Python Package] A complete survival analysis library for fitting models, plotting, and statistical tests.",
|
| 98 |
+
# "scvi-tools": "[Python Package] A package for probabilistic modeling of single-cell omics data, including deep generative models.",
|
| 99 |
+
"gseapy": "[Python Package] A Python wrapper for Gene Set Enrichment Analysis (GSEA) and visualization.",
|
| 100 |
+
"scrublet": "[Python Package] A tool for detecting doublets in single-cell RNA-seq data.",
|
| 101 |
+
"cellxgene-census": "[Python Package] A tool for accessing and analyzing the CellxGene Census, a collection of single-cell datasets. To download a dataset, use the download_source_h5ad function with the dataset id as the argument (856c1b98-5727-49da-bf0f-151bdb8cb056, no .h5ad extension).",
|
| 102 |
+
"hyperopt": "[Python Package] A Python library for optimizing hyperparameters of machine learning algorithms.",
|
| 103 |
+
"scvelo": "[Python Package] A tool for RNA velocity analysis in single cells using dynamical models.",
|
| 104 |
+
"pysam": "[Python Package] A Python module for reading, manipulating and writing genomic data sets in SAM/BAM/VCF/BCF formats.",
|
| 105 |
+
"pyfaidx": "[Python Package] A Python package for efficient random access to FASTA files.",
|
| 106 |
+
"pyranges": "[Python Package] A Python package for interval manipulation with a pandas-like interface.",
|
| 107 |
+
"pybedtools": "[Python Package] A Python wrapper for Aaron Quinlan's BEDTools programs.",
|
| 108 |
+
# "panhumanpy": "A Python package for hierarchical, cross-tissue cell type annotation of human single-cell RNA-seq data",
|
| 109 |
+
# Structural Biology & Drug Discovery (Python)
|
| 110 |
+
"rdkit": "[Python Package] A collection of cheminformatics and machine learning tools for working with chemical structures and drug discovery.",
|
| 111 |
+
"deeppurpose": "[Python Package] A deep learning library for drug-target interaction prediction and virtual screening.",
|
| 112 |
+
"pyscreener": "[Python Package] A Python package for virtual screening of chemical compounds.",
|
| 113 |
+
"openbabel": "[Python Package] A chemical toolbox designed to speak the many languages of chemical data, supporting file format conversion and molecular modeling.",
|
| 114 |
+
"descriptastorus": "[Python Package] A library for computing molecular descriptors for machine learning applications in drug discovery.",
|
| 115 |
+
# "pymol": "[Python Package] A molecular visualization system for rendering and animating 3D molecular structures.",
|
| 116 |
+
"openmm": "[Python Package] A toolkit for molecular simulation using high-performance GPU computing.",
|
| 117 |
+
"pytdc": "[Python Package] A Python package for Therapeutics Data Commons, providing access to machine learning datasets for drug discovery.",
|
| 118 |
+
# Data Science & Statistical Analysis (Python)
|
| 119 |
+
"pandas": "[Python Package] A fast, powerful, and flexible data analysis and manipulation library for Python.",
|
| 120 |
+
"numpy": "[Python Package] The fundamental package for scientific computing with Python, providing support for arrays, matrices, and mathematical functions.",
|
| 121 |
+
"scipy": "[Python Package] A Python library for scientific and technical computing, including modules for optimization, linear algebra, integration, and statistics.",
|
| 122 |
+
"scikit-learn": "[Python Package] A machine learning library featuring various classification, regression, and clustering algorithms.",
|
| 123 |
+
"matplotlib": "[Python Package] A comprehensive library for creating static, animated, and interactive visualizations in Python.",
|
| 124 |
+
"seaborn": "[Python Package] A statistical data visualization library based on matplotlib with a high-level interface for drawing attractive statistical graphics.",
|
| 125 |
+
"statsmodels": "[Python Package] A Python module for statistical modeling and econometrics, including descriptive statistics and estimation of statistical models.",
|
| 126 |
+
"pymc3": "[Python Package] A Python package for Bayesian statistical modeling and probabilistic machine learning.",
|
| 127 |
+
# "pystan": "[Python Package] A Python interface to Stan, a platform for statistical modeling and high-performance statistical computation.",
|
| 128 |
+
"umap-learn": "[Python Package] Uniform Manifold Approximation and Projection, a dimension reduction technique.",
|
| 129 |
+
"faiss-cpu": "[Python Package] A library for efficient similarity search and clustering of dense vectors.",
|
| 130 |
+
"harmony-pytorch": "[Python Package] A PyTorch implementation of the Harmony algorithm for integrating single-cell data.",
|
| 131 |
+
# General Bioinformatics & Computational Utilities (Python)
|
| 132 |
+
"tiledb": "[Python Package] A powerful engine for storing and analyzing large-scale genomic data.",
|
| 133 |
+
"tiledbsoma": "[Python Package] A library for working with the SOMA (Stack of Matrices) format using TileDB.",
|
| 134 |
+
"h5py": "[Python Package] A Python interface to the HDF5 binary data format, allowing storage of large amounts of numerical data.",
|
| 135 |
+
"tqdm": "[Python Package] A fast, extensible progress bar for loops and CLI applications.",
|
| 136 |
+
"joblib": "[Python Package] A set of tools to provide lightweight pipelining in Python, including transparent disk-caching and parallel computing.",
|
| 137 |
+
"opencv-python": "[Python Package] OpenCV library for computer vision tasks, useful for image analysis in biological contexts.",
|
| 138 |
+
"PyPDF2": "[Python Package] A library for working with PDF files, useful for extracting text from scientific papers.",
|
| 139 |
+
"googlesearch-python": "[Python Package] A library for performing Google searches programmatically.",
|
| 140 |
+
"scikit-image": "[Python Package] A collection of algorithms for image processing in Python.",
|
| 141 |
+
"pymed": "[Python Package] A Python library for accessing PubMed articles.",
|
| 142 |
+
"arxiv": "[Python Package] A Python wrapper for the arXiv API, allowing access to scientific papers.",
|
| 143 |
+
"scholarly": "[Python Package] A module to retrieve author and publication information from Google Scholar.",
|
| 144 |
+
"cryosparc-tools": "[Python Package] Tools for working with cryoSPARC, a platform for cryo-EM data processing.",
|
| 145 |
+
"mageck": "[Python Package] Analysis of CRISPR screen data.",
|
| 146 |
+
"igraph": "[Python Package] Network analysis and visualization.",
|
| 147 |
+
"pyscenic": "[Python Package] Analysis of single-cell RNA-seq data and gene regulatory networks.",
|
| 148 |
+
"cooler": "[Python Package] Storage and analysis of Hi-C data.",
|
| 149 |
+
"trackpy": "[Python Package] Particle tracking in images and video.",
|
| 150 |
+
"nnunet": "[Python Package] A deep learning framework for biomedical image segmentation, providing a standardized approach to training and inference.",
|
| 151 |
+
# "flowcytometrytools": "[Python Package] Analysis and visualization of flow cytometry data.",
|
| 152 |
+
"cellpose": "[Python Package] Cell segmentation in microscopy images.",
|
| 153 |
+
"viennarna": "[Python Package] RNA secondary structure prediction.",
|
| 154 |
+
"PyMassSpec": "[Python Package] Mass spectrometry data analysis.",
|
| 155 |
+
"python-libsbml": "[Python Package] Working with SBML files for computational biology.",
|
| 156 |
+
"cobra": "[Python Package] Constraint-based modeling of metabolic networks.",
|
| 157 |
+
"reportlab": "[Python Package] Creation of PDF documents.",
|
| 158 |
+
"flowkit": "[Python Package] Toolkit for processing flow cytometry data.",
|
| 159 |
+
"hmmlearn": "[Python Package] Hidden Markov model analysis.",
|
| 160 |
+
"msprime": "[Python Package] Simulation of genetic variation.",
|
| 161 |
+
"tskit": "[Python Package] Handling tree sequences and population genetics data.",
|
| 162 |
+
"cyvcf2": "[Python Package] Fast parsing of VCF files.",
|
| 163 |
+
"pykalman": "[Python Package] Kalman filter and smoother implementation.",
|
| 164 |
+
"fanc": "[Python Package] Analysis of chromatin conformation data.",
|
| 165 |
+
"loompy": "A Python implementation of the Loom file format for efficiently storing and working with large omics datasets.",
|
| 166 |
+
"pyBigWig": "A Python library for accessing bigWig and bigBed files for genome browser track data.",
|
| 167 |
+
"pymzml": "A Python module for high-throughput bioinformatics analysis of mass spectrometry data.",
|
| 168 |
+
"optlang": "A Python package for modeling optimization problems symbolically.",
|
| 169 |
+
"FlowIO": "A Python package for reading and writing flow cytometry data files.",
|
| 170 |
+
"FlowUtils": "Utilities for processing and analyzing flow cytometry data.",
|
| 171 |
+
"arboreto": "A Python package for inferring gene regulatory networks from single-cell RNA-seq data.",
|
| 172 |
+
"pdbfixer": "A Python package for fixing problems in PDB files in preparation for molecular simulations.",
|
| 173 |
+
# === R PACKAGES ===
|
| 174 |
+
# Core R Packages for Data Analysis
|
| 175 |
+
"ggplot2": "[R Package] A system for declaratively creating graphics, based on The Grammar of Graphics. Use with subprocess.run(['Rscript', '-e', 'library(ggplot2); ...']).",
|
| 176 |
+
"dplyr": "[R Package] A grammar of data manipulation, providing a consistent set of verbs that help you solve the most common data manipulation challenges. Use with subprocess.",
|
| 177 |
+
"tidyr": "[R Package] A package that helps you create tidy data, where each column is a variable, each row is an observation, and each cell is a single value. Use with subprocess.",
|
| 178 |
+
"readr": "[R Package] A fast and friendly way to read rectangular data like CSV, TSV, and FWF. Use with subprocess.run(['Rscript', '-e', 'library(readr); ...']).",
|
| 179 |
+
"stringr": "[R Package] A cohesive set of functions designed to make working with strings as easy as possible. Use with subprocess calls.",
|
| 180 |
+
"Matrix": "[R Package] A package that provides classes and methods for dense and sparse matrices. Required for Seurat. Use with subprocess calls.",
|
| 181 |
+
# "Rcpp": "[R Package] Seamless R and C++ Integration, allowing R functions to call compiled C++ code. Use with subprocess calls.",
|
| 182 |
+
# "devtools": "[R Package] Tools to make developing R packages easier, including functions to install packages from GitHub. Use with subprocess calls.",
|
| 183 |
+
# "remotes": "[R Package] Install R packages from GitHub, GitLab, Bitbucket, or other remote repositories. Use with subprocess calls.",
|
| 184 |
+
# Bioinformatics R Packages
|
| 185 |
+
"DESeq2": "[R Package] Differential gene expression analysis based on the negative binomial distribution. Use with subprocess.run(['Rscript', '-e', 'library(DESeq2); ...']).",
|
| 186 |
+
"clusterProfiler": "[R Package] A package for statistical analysis and visualization of functional profiles for genes and gene clusters. Use with subprocess calls.",
|
| 187 |
+
# "DADA2": "[R Package] A package for modeling and correcting Illumina-sequenced amplicon errors. Use with subprocess calls.",
|
| 188 |
+
# "xcms": "[R Package] A package for processing and visualization of LC-MS and GC-MS data. Use with subprocess calls.",
|
| 189 |
+
# "FlowCore": "[R Package] Basic infrastructure for flow cytometry data. Use with subprocess calls.",
|
| 190 |
+
"edgeR": "[R Package] Empirical Analysis of Digital Gene Expression Data in R, for differential expression analysis. Use with subprocess calls.",
|
| 191 |
+
"limma": "[R Package] Linear Models for Microarray Data, for differential expression analysis. Use with subprocess calls.",
|
| 192 |
+
"harmony": "[R Package] A method for integrating and analyzing single-cell data across datasets. Use with subprocess calls.",
|
| 193 |
+
"WGCNA": "[R Package] Weighted Correlation Network Analysis for studying biological networks. Use with subprocess calls.",
|
| 194 |
+
# === CLI TOOLS ===
|
| 195 |
+
# Sequence Analysis Tools
|
| 196 |
+
"samtools": "[CLI Tool] A suite of programs for interacting with high-throughput sequencing data. Use with subprocess.run(['samtools', ...]).",
|
| 197 |
+
"bowtie2": "[CLI Tool] An ultrafast and memory-efficient tool for aligning sequencing reads to long reference sequences. Use with subprocess.run(['bowtie2', ...]).",
|
| 198 |
+
"bwa": "[CLI Tool] Burrows-Wheeler Aligner for mapping low-divergent sequences against a large reference genome. Use with subprocess.run(['bwa', ...]).",
|
| 199 |
+
"bedtools": "[CLI Tool] A powerful toolset for genome arithmetic, allowing operations like intersect, merge, count, and complement on genomic features. Use with subprocess.run(['bedtools', ...]).",
|
| 200 |
+
"macs2": "[CLI Tool] Model-based Analysis of ChIP-Seq data, a tool for identifying transcript factor binding sites.",
|
| 201 |
+
# Quality Control and Processing Tools
|
| 202 |
+
"fastqc": "[CLI Tool] A quality control tool for high throughput sequence data. Use with subprocess.run(['fastqc', ...]).",
|
| 203 |
+
"trimmomatic": "[CLI Tool] A flexible read trimming tool for Illumina NGS data. Use with subprocess.run(['trimmomatic', ...]).",
|
| 204 |
+
# Multiple Sequence Alignment and Phylogenetics
|
| 205 |
+
"mafft": "[CLI Tool] A multiple sequence alignment program for unix-like operating systems. Use with subprocess.run(['mafft', ...]).",
|
| 206 |
+
"Homer": "[CLI Tool] Motif discovery and next-gen sequencing analysis.",
|
| 207 |
+
"FastTree": "[CLI Tool] Phylogenetic trees from sequence alignments.",
|
| 208 |
+
"muscle": "[CLI Tool] Multiple sequence alignment tool.",
|
| 209 |
+
# Genetic Analysis Tools
|
| 210 |
+
"plink": "[CLI Tool] A comprehensive toolkit for genome association studies that can perform a range of large-scale analyses in a computationally efficient manner. Use with subprocess.run(['plink', ...]).",
|
| 211 |
+
"plink2": "[CLI Tool] A comprehensive toolkit for genome association studies that can perform a range of large-scale analyses in a computationally efficient manner. Use with subprocess.run(['plink2', ...]).",
|
| 212 |
+
"gcta64": "[CLI Tool] Genome-wide Complex Trait Analysis (GCTA) tool for estimating the proportion of phenotypic variance explained by genome-wide SNPs and analyzing genetic relationships. Use with subprocess.run(['gcta64', ...]).",
|
| 213 |
+
"iqtree2": "[CLI Tool] An efficient phylogenetic software for maximum likelihood analysis with built-in model selection and ultrafast bootstrap. Use with subprocess.run(['iqtree2', ...]).",
|
| 214 |
+
"ADFR": "AutoDock for Receptors suite for molecular docking and virtual screening. ",
|
| 215 |
+
"diamond": "A sequence aligner for protein and translated DNA searches, designed for high performance analysis of big sequence data. ",
|
| 216 |
+
"fcsparser": "A command-line tool for parsing and analyzing flow cytometry standard (FCS) files. ",
|
| 217 |
+
"plannotate": "[CLI Tool] A tool for annotating plasmid sequences with common features. ",
|
| 218 |
+
"vina": "[CLI Tool] An open-source program for molecular docking and virtual screening, known for its speed and accuracy improvements over AutoDock 4.",
|
| 219 |
+
"autosite": "[CLI Tool] A binding site detection tool used to identify potential ligand binding pockets on protein structures for molecular docking.",
|
| 220 |
+
"PyLabRobot": "[Python Package] A Python package for controlling liquid-handling robots and other lab automation equipment.",
|
| 221 |
+
}
|
Biomni/biomni/env_desc_cm.py
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Data lake dictionary with detailed descriptions (Commercial Mode - Non-commercial datasets commented out)
|
| 2 |
+
data_lake_dict = {
|
| 3 |
+
"affinity_capture-ms.parquet": "Protein-protein interactions detected via affinity capture and mass spectrometry.",
|
| 4 |
+
"affinity_capture-rna.parquet": "Protein-RNA interactions detected by affinity capture.",
|
| 5 |
+
# "BindingDB_All_202409.tsv": "Measured binding affinities between proteins and small molecules for drug discovery.", # Requires commercial license
|
| 6 |
+
"broad_repurposing_hub_molecule_with_smiles.parquet": "Molecules from Broad Institute's Drug Repurposing Hub with SMILES annotations.",
|
| 7 |
+
"broad_repurposing_hub_phase_moa_target_info.parquet": "Drug phases, mechanisms of action, and target information from Broad Institute.",
|
| 8 |
+
"co-fractionation.parquet": "Protein-protein interactions from co-fractionation experiments.",
|
| 9 |
+
"czi_census_datasets_v4.parquet": "Datasets from the Chan Zuckerberg Initiative's Cell Census.",
|
| 10 |
+
"DepMap_CRISPRGeneDependency.csv": "Gene dependency probability estimates for cancer cell lines, including all DepMap models.",
|
| 11 |
+
"DepMap_CRISPRGeneEffect.csv": "Genome-wide CRISPR gene effect estimates for cancer cell lines, including all DepMap models.",
|
| 12 |
+
"DepMap_Model.csv": "Metadata describing all cancer models/cell lines which are referenced by a dataset contained within the DepMap portal.",
|
| 13 |
+
"DepMap_OmicsExpressionProteinCodingGenesTPMLogp1.csv": "Gene expression in TPMs for cancer cell lines, including all DepMap models.",
|
| 14 |
+
# "ddinter_alimentary_tract_metabolism.csv": "Drug-drug interactions for alimentary tract and metabolism drugs from DDInter 2.0 database.", # CC BY-NC-SA 4.0 - Non-commercial only
|
| 15 |
+
# "ddinter_antineoplastic.csv": "Drug-drug interactions for antineoplastic and immunomodulating agents from DDInter 2.0 database.", # CC BY-NC-SA 4.0 - Non-commercial only
|
| 16 |
+
# "ddinter_antiparasitic.csv": "Drug-drug interactions for antiparasitic products from DDInter 2.0 database.", # CC BY-NC-SA 4.0 - Non-commercial only
|
| 17 |
+
# "ddinter_blood_organs.csv": "Drug-drug interactions for blood and blood forming organs drugs from DDInter 2.0 database.", # CC BY-NC-SA 4.0 - Non-commercial only
|
| 18 |
+
# "ddinter_dermatological.csv": "Drug-drug interactions for dermatological drugs from DDInter 2.0 database.", # CC BY-NC-SA 4.0 - Non-commercial only
|
| 19 |
+
# "ddinter_hormonal.csv": "Drug-drug interactions for systemic hormonal preparations from DDInter 2.0 database.", # CC BY-NC-SA 4.0 - Non-commercial only
|
| 20 |
+
# "ddinter_respiratory.csv": "Drug-drug interactions for respiratory system drugs from DDInter 2.0 database.", # CC BY-NC-SA 4.0 - Non-commercial only
|
| 21 |
+
# "ddinter_various.csv": "Drug-drug interactions for various drugs from DDInter 2.0 database.", # CC BY-NC-SA 4.0 - Non-commercial only
|
| 22 |
+
# "DisGeNET.parquet": "Gene-disease associations from multiple sources.", # CC BY-NC-SA 4.0 - Non-commercial only
|
| 23 |
+
"dosage_growth_defect.parquet": "Gene dosage changes affecting growth.",
|
| 24 |
+
# "enamine_cloud_library_smiles.pkl": "Compounds from Enamine REAL library with SMILES annotations.", # Proprietary - Requires license
|
| 25 |
+
# "evebio_assay_table.csv": "Assay metadata with one row per assay from EveBio pharmome mapping.", # Proprietary - Requires permission
|
| 26 |
+
# "evebio_bundle_table.csv": "Target subfamily bundles used for screening-to-profiling progression.", # Proprietary - Requires permission
|
| 27 |
+
# "evebio_compound_table.csv": "Compound metadata with common identifiers from EveBio screening.", # Proprietary - Requires permission
|
| 28 |
+
# "evebio_control_table.csv": "Control datapoints for all screening and profiling plates.", # Proprietary - Requires permission
|
| 29 |
+
# "evebio_detailed_result_table.csv": "Expanded results on evebio_summary_result_table with curve fit parameters and phase categories.", # Proprietary - Requires permission
|
| 30 |
+
# "evebio_observed_points_table.csv": "Raw observed datapoints from all screening and profiling experiments.", # Proprietary - Requires permission
|
| 31 |
+
# "evebio_summary_result_table.csv": "Succinct summary of results for each assay-compound combination.", # Proprietary - Requires permission
|
| 32 |
+
# "evebio_target_table.csv": "Target metadata with common identifiers from EveBio screening.", # Proprietary - Requires permission
|
| 33 |
+
"genebass_missense_LC_filtered.pkl": "Filtered missense variants from GeneBass.",
|
| 34 |
+
"genebass_pLoF_filtered.pkl": "Predicted loss-of-function variants from GeneBass.",
|
| 35 |
+
"genebass_synonymous_filtered.pkl": "Filtered synonymous variants from GeneBass.",
|
| 36 |
+
"gene_info.parquet": "Comprehensive gene information.",
|
| 37 |
+
"genetic_interaction.parquet": "Genetic interactions between genes.",
|
| 38 |
+
"go-plus.json": "Gene ontology data for functional gene annotations.",
|
| 39 |
+
"gtex_tissue_gene_tpm.parquet": "Gene expression (TPM) across human tissues from GTEx.",
|
| 40 |
+
"gwas_catalog.pkl": "Genome-wide association studies (GWAS) results.",
|
| 41 |
+
"hp.obo": "Official HPO release in obographs format",
|
| 42 |
+
"kg.csv": "Precision medicine knowledge graph with 17,080 diseases and 4+ million relationships across biological scales.",
|
| 43 |
+
"marker_celltype.parquet": "Cell type marker genes for identification.",
|
| 44 |
+
# "McPAS-TCR.parquet": "T-cell receptor sequences and specificity data from McPAS database.", # CC BY-NC-SA 4.0 - Non-commercial only
|
| 45 |
+
# "miRDB_v6.0_results.parquet": "Predicted microRNA targets from miRDB.", # Non-commercial use only
|
| 46 |
+
# "miRTarBase_microRNA_target_interaction.parquet": "Experimentally validated microRNA-target interactions from miRTarBase.", # CC BY-NC 4.0 - Non-commercial only
|
| 47 |
+
# "miRTarBase_microRNA_target_interaction_pubmed_abtract.txt": "PubMed abstracts for microRNA-target interactions in miRTarBase.", # CC BY-NC 4.0 - Non-commercial only
|
| 48 |
+
# "miRTarBase_MicroRNA_Target_Sites.parquet": "Binding sites of microRNAs on target genes from miRTarBase.", # CC BY-NC 4.0 - Non-commercial only
|
| 49 |
+
"mousemine_m1_positional_geneset.parquet": "Positional gene sets from MouseMine.",
|
| 50 |
+
"mousemine_m2_curated_geneset.parquet": "Curated gene sets from MouseMine.",
|
| 51 |
+
"mousemine_m3_regulatory_target_geneset.parquet": "Regulatory target gene sets from MouseMine.",
|
| 52 |
+
"mousemine_m5_ontology_geneset.parquet": "Ontology-based gene sets from MouseMine.",
|
| 53 |
+
"mousemine_m8_celltype_signature_geneset.parquet": "Cell type signature gene sets from MouseMine.",
|
| 54 |
+
"mousemine_mh_hallmark_geneset.parquet": "Hallmark gene sets from MouseMine.",
|
| 55 |
+
# "msigdb_human_c1_positional_geneset.parquet": "Human positional gene sets from MSigDB.", # Requires commercial license
|
| 56 |
+
# "msigdb_human_c2_curated_geneset.parquet": "Curated human gene sets from MSigDB.", # Requires commercial license
|
| 57 |
+
# "msigdb_human_c3_regulatory_target_geneset.parquet": "Regulatory target gene sets from MSigDB.", # Requires commercial license
|
| 58 |
+
# "msigdb_human_c3_subset_transcription_factor_targets_from_GTRD.parquet": "Transcription factor targets from GTRD/MSigDB.", # Requires commercial license
|
| 59 |
+
# "msigdb_human_c4_computational_geneset.parquet": "Computationally derived gene sets from MSigDB.", # Requires commercial license
|
| 60 |
+
# "msigdb_human_c5_ontology_geneset.parquet": "Ontology-based gene sets from MSigDB.", # Requires commercial license
|
| 61 |
+
# "msigdb_human_c6_oncogenic_signature_geneset.parquet": "Oncogenic signatures from MSigDB.", # Requires commercial license
|
| 62 |
+
# "msigdb_human_c7_immunologic_signature_geneset.parquet": "Immunologic signatures from MSigDB.", # Requires commercial license
|
| 63 |
+
# "msigdb_human_c8_celltype_signature_geneset.parquet": "Cell type signatures from MSigDB.", # Requires commercial license
|
| 64 |
+
# "msigdb_human_h_hallmark_geneset.parquet": "Hallmark gene sets from MSigDB.", # Requires commercial license
|
| 65 |
+
# "omim.parquet": "Genetic disorders and associated genes from OMIM.", # Requires commercial license
|
| 66 |
+
"proteinatlas.tsv": "Protein expression data from Human Protein Atlas.",
|
| 67 |
+
"proximity_label-ms.parquet": "Protein interactions via proximity labeling and mass spectrometry.",
|
| 68 |
+
"reconstituted_complex.parquet": "Protein complexes reconstituted in vitro.",
|
| 69 |
+
"sgRNA_KO_SP_mouse.txt": "sgRNA knockout data for mouse.",
|
| 70 |
+
"sgRNA_KO_SP_human.txt": "sgRNA knockout data for human.",
|
| 71 |
+
"synthetic_growth_defect.parquet": "Synthetic growth defects from genetic interactions.",
|
| 72 |
+
"synthetic_lethality.parquet": "Synthetic lethal interactions.",
|
| 73 |
+
"synthetic_rescue.parquet": "Genetic interactions rescuing phenotypes.",
|
| 74 |
+
"two-hybrid.parquet": "Protein-protein interactions detected by yeast two-hybrid assays.",
|
| 75 |
+
"variant_table.parquet": "Annotated genetic variants table.",
|
| 76 |
+
"Virus-Host_PPI_P-HIPSTER_2020.parquet": "Virus-host protein-protein interactions from P-HIPSTER.",
|
| 77 |
+
"txgnn_name_mapping.pkl": "Name mapping for TXGNN.",
|
| 78 |
+
"txgnn_prediction.pkl": "Prediction data for TXGNN.",
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
# Updated library_content as a dictionary with detailed descriptions
|
| 82 |
+
library_content_dict = {
|
| 83 |
+
# === PYTHON PACKAGES ===
|
| 84 |
+
# Core Bioinformatics Libraries (Python)
|
| 85 |
+
"biopython": "[Python Package] A set of tools for biological computation including parsers for bioinformatics files, access to online services, and interfaces to common bioinformatics programs.",
|
| 86 |
+
"biom-format": "[Python Package] The Biological Observation Matrix (BIOM) format is designed for representing biological sample by observation contingency tables with associated metadata.",
|
| 87 |
+
"scanpy": "[Python Package] A scalable toolkit for analyzing single-cell gene expression data, specifically designed for large datasets using AnnData.",
|
| 88 |
+
"scikit-bio": "[Python Package] Data structures, algorithms, and educational resources for bioinformatics, including sequence analysis, phylogenetics, and ordination methods.",
|
| 89 |
+
"anndata": "[Python Package] A Python package for handling annotated data matrices in memory and on disk, primarily used for single-cell genomics data.",
|
| 90 |
+
"mudata": "[Python Package] A Python package for multimodal data storage and manipulation, extending AnnData to handle multiple modalities.",
|
| 91 |
+
"pyliftover": "[Python Package] A Python implementation of UCSC liftOver tool for converting genomic coordinates between genome assemblies.",
|
| 92 |
+
"biopandas": "[Python Package] A package that provides pandas DataFrames for working with molecular structures and biological data.",
|
| 93 |
+
"biotite": "[Python Package] A comprehensive library for computational molecular biology, providing tools for sequence analysis, structure analysis, and more.",
|
| 94 |
+
"lazyslide": "[Python Package] A Python framework that brings interoperable, reproducible whole slide image analysis, enabling seamless histopathology workflows from preprocessing to deep learning.",
|
| 95 |
+
# Genomics & Variant Analysis (Python)
|
| 96 |
+
"gget": "[Python Package] A toolkit for accessing genomic databases and retrieving sequences, annotations, and other genomic data.",
|
| 97 |
+
"lifelines": "[Python Package] A complete survival analysis library for fitting models, plotting, and statistical tests.",
|
| 98 |
+
# "scvi-tools": "[Python Package] A package for probabilistic modeling of single-cell omics data, including deep generative models.",
|
| 99 |
+
"gseapy": "[Python Package] A Python wrapper for Gene Set Enrichment Analysis (GSEA) and visualization.",
|
| 100 |
+
"scrublet": "[Python Package] A tool for detecting doublets in single-cell RNA-seq data.",
|
| 101 |
+
"cellxgene-census": "[Python Package] A tool for accessing and analyzing the CellxGene Census, a collection of single-cell datasets. To download a dataset, use the download_source_h5ad function with the dataset id as the argument (856c1b98-5727-49da-bf0f-151bdb8cb056, no .h5ad extension).",
|
| 102 |
+
"hyperopt": "[Python Package] A Python library for optimizing hyperparameters of machine learning algorithms.",
|
| 103 |
+
"scvelo": "[Python Package] A tool for RNA velocity analysis in single cells using dynamical models.",
|
| 104 |
+
"pysam": "[Python Package] A Python module for reading, manipulating and writing genomic data sets in SAM/BAM/VCF/BCF formats.",
|
| 105 |
+
"pyfaidx": "[Python Package] A Python package for efficient random access to FASTA files.",
|
| 106 |
+
"pyranges": "[Python Package] A Python package for interval manipulation with a pandas-like interface.",
|
| 107 |
+
"pybedtools": "[Python Package] A Python wrapper for Aaron Quinlan's BEDTools programs.",
|
| 108 |
+
# "panhumanpy": "A Python package for hierarchical, cross-tissue cell type annotation of human single-cell RNA-seq data",
|
| 109 |
+
# Structural Biology & Drug Discovery (Python)
|
| 110 |
+
"rdkit": "[Python Package] A collection of cheminformatics and machine learning tools for working with chemical structures and drug discovery.",
|
| 111 |
+
"deeppurpose": "[Python Package] A deep learning library for drug-target interaction prediction and virtual screening.",
|
| 112 |
+
"pyscreener": "[Python Package] A Python package for virtual screening of chemical compounds.",
|
| 113 |
+
"openbabel": "[Python Package] A chemical toolbox designed to speak the many languages of chemical data, supporting file format conversion and molecular modeling.",
|
| 114 |
+
"descriptastorus": "[Python Package] A library for computing molecular descriptors for machine learning applications in drug discovery.",
|
| 115 |
+
# "pymol": "[Python Package] A molecular visualization system for rendering and animating 3D molecular structures.",
|
| 116 |
+
"openmm": "[Python Package] A toolkit for molecular simulation using high-performance GPU computing.",
|
| 117 |
+
"pytdc": "[Python Package] A Python package for Therapeutics Data Commons, providing access to machine learning datasets for drug discovery.",
|
| 118 |
+
# Data Science & Statistical Analysis (Python)
|
| 119 |
+
"pandas": "[Python Package] A fast, powerful, and flexible data analysis and manipulation library for Python.",
|
| 120 |
+
"numpy": "[Python Package] The fundamental package for scientific computing with Python, providing support for arrays, matrices, and mathematical functions.",
|
| 121 |
+
"scipy": "[Python Package] A Python library for scientific and technical computing, including modules for optimization, linear algebra, integration, and statistics.",
|
| 122 |
+
"scikit-learn": "[Python Package] A machine learning library featuring various classification, regression, and clustering algorithms.",
|
| 123 |
+
"matplotlib": "[Python Package] A comprehensive library for creating static, animated, and interactive visualizations in Python.",
|
| 124 |
+
"seaborn": "[Python Package] A statistical data visualization library based on matplotlib with a high-level interface for drawing attractive statistical graphics.",
|
| 125 |
+
"statsmodels": "[Python Package] A Python module for statistical modeling and econometrics, including descriptive statistics and estimation of statistical models.",
|
| 126 |
+
"pymc3": "[Python Package] A Python package for Bayesian statistical modeling and probabilistic machine learning.",
|
| 127 |
+
# "pystan": "[Python Package] A Python interface to Stan, a platform for statistical modeling and high-performance statistical computation.",
|
| 128 |
+
"umap-learn": "[Python Package] Uniform Manifold Approximation and Projection, a dimension reduction technique.",
|
| 129 |
+
"faiss-cpu": "[Python Package] A library for efficient similarity search and clustering of dense vectors.",
|
| 130 |
+
"harmony-pytorch": "[Python Package] A PyTorch implementation of the Harmony algorithm for integrating single-cell data.",
|
| 131 |
+
# General Bioinformatics & Computational Utilities (Python)
|
| 132 |
+
"tiledb": "[Python Package] A powerful engine for storing and analyzing large-scale genomic data.",
|
| 133 |
+
"tiledbsoma": "[Python Package] A library for working with the SOMA (Stack of Matrices) format using TileDB.",
|
| 134 |
+
"h5py": "[Python Package] A Python interface to the HDF5 binary data format, allowing storage of large amounts of numerical data.",
|
| 135 |
+
"tqdm": "[Python Package] A fast, extensible progress bar for loops and CLI applications.",
|
| 136 |
+
"joblib": "[Python Package] A set of tools to provide lightweight pipelining in Python, including transparent disk-caching and parallel computing.",
|
| 137 |
+
"opencv-python": "[Python Package] OpenCV library for computer vision tasks, useful for image analysis in biological contexts.",
|
| 138 |
+
"PyPDF2": "[Python Package] A library for working with PDF files, useful for extracting text from scientific papers.",
|
| 139 |
+
"googlesearch-python": "[Python Package] A library for performing Google searches programmatically.",
|
| 140 |
+
"scikit-image": "[Python Package] A collection of algorithms for image processing in Python.",
|
| 141 |
+
"pymed": "[Python Package] A Python library for accessing PubMed articles.",
|
| 142 |
+
"arxiv": "[Python Package] A Python wrapper for the arXiv API, allowing access to scientific papers.",
|
| 143 |
+
"scholarly": "[Python Package] A module to retrieve author and publication information from Google Scholar.",
|
| 144 |
+
"cryosparc-tools": "[Python Package] Tools for working with cryoSPARC, a platform for cryo-EM data processing.",
|
| 145 |
+
"mageck": "[Python Package] Analysis of CRISPR screen data.",
|
| 146 |
+
"igraph": "[Python Package] Network analysis and visualization.",
|
| 147 |
+
"pyscenic": "[Python Package] Analysis of single-cell RNA-seq data and gene regulatory networks.",
|
| 148 |
+
"cooler": "[Python Package] Storage and analysis of Hi-C data.",
|
| 149 |
+
"trackpy": "[Python Package] Particle tracking in images and video.",
|
| 150 |
+
# "flowcytometrytools": "[Python Package] Analysis and visualization of flow cytometry data.",
|
| 151 |
+
"cellpose": "[Python Package] Cell segmentation in microscopy images.",
|
| 152 |
+
"viennarna": "[Python Package] RNA secondary structure prediction.",
|
| 153 |
+
"PyMassSpec": "[Python Package] Mass spectrometry data analysis.",
|
| 154 |
+
"python-libsbml": "[Python Package] Working with SBML files for computational biology.",
|
| 155 |
+
"cobra": "[Python Package] Constraint-based modeling of metabolic networks.",
|
| 156 |
+
"reportlab": "[Python Package] Creation of PDF documents.",
|
| 157 |
+
"flowkit": "[Python Package] Toolkit for processing flow cytometry data.",
|
| 158 |
+
"hmmlearn": "[Python Package] Hidden Markov model analysis.",
|
| 159 |
+
"msprime": "[Python Package] Simulation of genetic variation.",
|
| 160 |
+
"tskit": "[Python Package] Handling tree sequences and population genetics data.",
|
| 161 |
+
"cyvcf2": "[Python Package] Fast parsing of VCF files.",
|
| 162 |
+
"pykalman": "[Python Package] Kalman filter and smoother implementation.",
|
| 163 |
+
"fanc": "[Python Package] Analysis of chromatin conformation data.",
|
| 164 |
+
"loompy": "A Python implementation of the Loom file format for efficiently storing and working with large omics datasets.",
|
| 165 |
+
"pyBigWig": "A Python library for accessing bigWig and bigBed files for genome browser track data.",
|
| 166 |
+
"pymzml": "A Python module for high-throughput bioinformatics analysis of mass spectrometry data.",
|
| 167 |
+
"optlang": "A Python package for modeling optimization problems symbolically.",
|
| 168 |
+
"FlowIO": "A Python package for reading and writing flow cytometry data files.",
|
| 169 |
+
"FlowUtils": "Utilities for processing and analyzing flow cytometry data.",
|
| 170 |
+
"arboreto": "A Python package for inferring gene regulatory networks from single-cell RNA-seq data.",
|
| 171 |
+
"pdbfixer": "A Python package for fixing problems in PDB files in preparation for molecular simulations.",
|
| 172 |
+
# === R PACKAGES ===
|
| 173 |
+
# Core R Packages for Data Analysis
|
| 174 |
+
"ggplot2": "[R Package] A system for declaratively creating graphics, based on The Grammar of Graphics. Use with subprocess.run(['Rscript', '-e', 'library(ggplot2); ...']).",
|
| 175 |
+
"dplyr": "[R Package] A grammar of data manipulation, providing a consistent set of verbs that help you solve the most common data manipulation challenges. Use with subprocess.",
|
| 176 |
+
"tidyr": "[R Package] A package that helps you create tidy data, where each column is a variable, each row is an observation, and each cell is a single value. Use with subprocess.",
|
| 177 |
+
"readr": "[R Package] A fast and friendly way to read rectangular data like CSV, TSV, and FWF. Use with subprocess.run(['Rscript', '-e', 'library(readr); ...']).",
|
| 178 |
+
"stringr": "[R Package] A cohesive set of functions designed to make working with strings as easy as possible. Use with subprocess calls.",
|
| 179 |
+
"Matrix": "[R Package] A package that provides classes and methods for dense and sparse matrices. Required for Seurat. Use with subprocess calls.",
|
| 180 |
+
# "Rcpp": "[R Package] Seamless R and C++ Integration, allowing R functions to call compiled C++ code. Use with subprocess calls.",
|
| 181 |
+
# "devtools": "[R Package] Tools to make developing R packages easier, including functions to install packages from GitHub. Use with subprocess calls.",
|
| 182 |
+
# "remotes": "[R Package] Install R packages from GitHub, GitLab, Bitbucket, or other remote repositories. Use with subprocess calls.",
|
| 183 |
+
# Bioinformatics R Packages
|
| 184 |
+
"DESeq2": "[R Package] Differential gene expression analysis based on the negative binomial distribution. Use with subprocess.run(['Rscript', '-e', 'library(DESeq2); ...']).",
|
| 185 |
+
"clusterProfiler": "[R Package] A package for statistical analysis and visualization of functional profiles for genes and gene clusters. Use with subprocess calls.",
|
| 186 |
+
# "DADA2": "[R Package] A package for modeling and correcting Illumina-sequenced amplicon errors. Use with subprocess calls.",
|
| 187 |
+
# "xcms": "[R Package] A package for processing and visualization of LC-MS and GC-MS data. Use with subprocess calls.",
|
| 188 |
+
# "FlowCore": "[R Package] Basic infrastructure for flow cytometry data. Use with subprocess calls.",
|
| 189 |
+
"edgeR": "[R Package] Empirical Analysis of Digital Gene Expression Data in R, for differential expression analysis. Use with subprocess calls.",
|
| 190 |
+
"limma": "[R Package] Linear Models for Microarray Data, for differential expression analysis. Use with subprocess calls.",
|
| 191 |
+
"harmony": "[R Package] A method for integrating and analyzing single-cell data across datasets. Use with subprocess calls.",
|
| 192 |
+
"WGCNA": "[R Package] Weighted Correlation Network Analysis for studying biological networks. Use with subprocess calls.",
|
| 193 |
+
# === CLI TOOLS ===
|
| 194 |
+
# Sequence Analysis Tools
|
| 195 |
+
"samtools": "[CLI Tool] A suite of programs for interacting with high-throughput sequencing data. Use with subprocess.run(['samtools', ...]).",
|
| 196 |
+
"bowtie2": "[CLI Tool] An ultrafast and memory-efficient tool for aligning sequencing reads to long reference sequences. Use with subprocess.run(['bowtie2', ...]).",
|
| 197 |
+
"bwa": "[CLI Tool] Burrows-Wheeler Aligner for mapping low-divergent sequences against a large reference genome. Use with subprocess.run(['bwa', ...]).",
|
| 198 |
+
"bedtools": "[CLI Tool] A powerful toolset for genome arithmetic, allowing operations like intersect, merge, count, and complement on genomic features. Use with subprocess.run(['bedtools', ...]).",
|
| 199 |
+
"macs2": "[CLI Tool] Model-based Analysis of ChIP-Seq data, a tool for identifying transcript factor binding sites.",
|
| 200 |
+
# Quality Control and Processing Tools
|
| 201 |
+
"fastqc": "[CLI Tool] A quality control tool for high throughput sequence data. Use with subprocess.run(['fastqc', ...]).",
|
| 202 |
+
"trimmomatic": "[CLI Tool] A flexible read trimming tool for Illumina NGS data. Use with subprocess.run(['trimmomatic', ...]).",
|
| 203 |
+
# Multiple Sequence Alignment and Phylogenetics
|
| 204 |
+
"mafft": "[CLI Tool] A multiple sequence alignment program for unix-like operating systems. Use with subprocess.run(['mafft', ...]).",
|
| 205 |
+
"Homer": "[CLI Tool] Motif discovery and next-gen sequencing analysis.",
|
| 206 |
+
"FastTree": "[CLI Tool] Phylogenetic trees from sequence alignments.",
|
| 207 |
+
"muscle": "[CLI Tool] Multiple sequence alignment tool.",
|
| 208 |
+
# Genetic Analysis Tools
|
| 209 |
+
"plink": "[CLI Tool] A comprehensive toolkit for genome association studies that can perform a range of large-scale analyses in a computationally efficient manner. Use with subprocess.run(['plink', ...]).",
|
| 210 |
+
"plink2": "[CLI Tool] A comprehensive toolkit for genome association studies that can perform a range of large-scale analyses in a computationally efficient manner. Use with subprocess.run(['plink2', ...]).",
|
| 211 |
+
"gcta64": "[CLI Tool] Genome-wide Complex Trait Analysis (GCTA) tool for estimating the proportion of phenotypic variance explained by genome-wide SNPs and analyzing genetic relationships. Use with subprocess.run(['gcta64', ...]).",
|
| 212 |
+
"iqtree2": "[CLI Tool] An efficient phylogenetic software for maximum likelihood analysis with built-in model selection and ultrafast bootstrap. Use with subprocess.run(['iqtree2', ...]).",
|
| 213 |
+
"ADFR": "AutoDock for Receptors suite for molecular docking and virtual screening. ",
|
| 214 |
+
"diamond": "A sequence aligner for protein and translated DNA searches, designed for high performance analysis of big sequence data. ",
|
| 215 |
+
"fcsparser": "A command-line tool for parsing and analyzing flow cytometry standard (FCS) files. ",
|
| 216 |
+
"plannotate": "[CLI Tool] A tool for annotating plasmid sequences with common features. ",
|
| 217 |
+
"vina": "[CLI Tool] An open-source program for molecular docking and virtual screening, known for its speed and accuracy improvements over AutoDock 4.",
|
| 218 |
+
"autosite": "[CLI Tool] A binding site detection tool used to identify potential ligand binding pockets on protein structures for molecular docking.",
|
| 219 |
+
}
|
Biomni/biomni/eval/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .biomni_eval1 import BiomniEval1
|
| 2 |
+
|
| 3 |
+
__all__ = ["BiomniEval1"]
|
Biomni/biomni/llm.py
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from typing import TYPE_CHECKING, Literal, Optional
|
| 3 |
+
|
| 4 |
+
from langchain_core.language_models.chat_models import BaseChatModel
|
| 5 |
+
|
| 6 |
+
if TYPE_CHECKING:
|
| 7 |
+
from biomni.config import BiomniConfig
|
| 8 |
+
|
| 9 |
+
SourceType = Literal["OpenAI", "AzureOpenAI", "Anthropic", "Ollama", "Gemini", "Bedrock", "Groq", "Custom"]
|
| 10 |
+
ALLOWED_SOURCES: set[str] = set(SourceType.__args__)
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def get_llm(
|
| 14 |
+
model: str | None = None,
|
| 15 |
+
temperature: float | None = None,
|
| 16 |
+
stop_sequences: list[str] | None = None,
|
| 17 |
+
source: SourceType | None = None,
|
| 18 |
+
base_url: str | None = None,
|
| 19 |
+
api_key: str | None = None,
|
| 20 |
+
config: Optional["BiomniConfig"] = None,
|
| 21 |
+
) -> BaseChatModel:
|
| 22 |
+
"""
|
| 23 |
+
Get a language model instance based on the specified model name and source.
|
| 24 |
+
This function supports models from OpenAI, Azure OpenAI, Anthropic, Ollama, Gemini, Bedrock, and custom model serving.
|
| 25 |
+
Args:
|
| 26 |
+
model (str): The model name to use
|
| 27 |
+
temperature (float): Temperature setting for generation
|
| 28 |
+
stop_sequences (list): Sequences that will stop generation
|
| 29 |
+
source (str): Source provider: "OpenAI", "AzureOpenAI", "Anthropic", "Ollama", "Gemini", "Bedrock", or "Custom"
|
| 30 |
+
If None, will attempt to auto-detect from model name
|
| 31 |
+
base_url (str): The base URL for custom model serving (e.g., "http://localhost:8000/v1"), default is None
|
| 32 |
+
api_key (str): The API key for the custom llm
|
| 33 |
+
config (BiomniConfig): Optional configuration object. If provided, unspecified parameters will use config values
|
| 34 |
+
"""
|
| 35 |
+
# Use config values for any unspecified parameters
|
| 36 |
+
if config is not None:
|
| 37 |
+
if model is None:
|
| 38 |
+
model = config.llm_model
|
| 39 |
+
if temperature is None:
|
| 40 |
+
temperature = config.temperature
|
| 41 |
+
if source is None:
|
| 42 |
+
source = config.source
|
| 43 |
+
if base_url is None:
|
| 44 |
+
base_url = config.base_url
|
| 45 |
+
if api_key is None:
|
| 46 |
+
api_key = config.api_key or "EMPTY"
|
| 47 |
+
|
| 48 |
+
# Use defaults if still not specified
|
| 49 |
+
if model is None:
|
| 50 |
+
model = "claude-3-5-sonnet-20241022"
|
| 51 |
+
if temperature is None:
|
| 52 |
+
temperature = 0.7
|
| 53 |
+
if api_key is None:
|
| 54 |
+
api_key = "EMPTY"
|
| 55 |
+
# Auto-detect source from model name if not specified
|
| 56 |
+
if source is None:
|
| 57 |
+
env_source = os.getenv("LLM_SOURCE")
|
| 58 |
+
if env_source in ALLOWED_SOURCES:
|
| 59 |
+
source = env_source
|
| 60 |
+
else:
|
| 61 |
+
if model[:7] == "claude-":
|
| 62 |
+
source = "Anthropic"
|
| 63 |
+
elif model[:7] == "gpt-oss":
|
| 64 |
+
source = "Ollama"
|
| 65 |
+
elif model[:4] == "gpt-":
|
| 66 |
+
source = "OpenAI"
|
| 67 |
+
elif model.startswith("azure-"):
|
| 68 |
+
source = "AzureOpenAI"
|
| 69 |
+
elif model[:7] == "gemini-":
|
| 70 |
+
source = "Gemini"
|
| 71 |
+
elif "groq" in model.lower():
|
| 72 |
+
source = "Groq"
|
| 73 |
+
elif base_url is not None:
|
| 74 |
+
source = "Custom"
|
| 75 |
+
elif "/" in model or any(
|
| 76 |
+
name in model.lower()
|
| 77 |
+
for name in [
|
| 78 |
+
"llama",
|
| 79 |
+
"mistral",
|
| 80 |
+
"qwen",
|
| 81 |
+
"gemma",
|
| 82 |
+
"phi",
|
| 83 |
+
"dolphin",
|
| 84 |
+
"orca",
|
| 85 |
+
"vicuna",
|
| 86 |
+
"deepseek",
|
| 87 |
+
]
|
| 88 |
+
):
|
| 89 |
+
source = "Ollama"
|
| 90 |
+
elif model.startswith(
|
| 91 |
+
("anthropic.claude-", "amazon.titan-", "meta.llama-", "mistral.", "cohere.", "ai21.", "us.")
|
| 92 |
+
):
|
| 93 |
+
source = "Bedrock"
|
| 94 |
+
else:
|
| 95 |
+
raise ValueError("Unable to determine model source. Please specify 'source' parameter.")
|
| 96 |
+
|
| 97 |
+
# Create appropriate model based on source
|
| 98 |
+
if source == "OpenAI":
|
| 99 |
+
try:
|
| 100 |
+
from langchain_openai import ChatOpenAI
|
| 101 |
+
except ImportError:
|
| 102 |
+
raise ImportError( # noqa: B904
|
| 103 |
+
"langchain-openai package is required for OpenAI models. Install with: pip install langchain-openai"
|
| 104 |
+
)
|
| 105 |
+
# Newer OpenAI models (e.g., gpt-5-*) require the Responses API and may reject
|
| 106 |
+
# legacy Chat Completions parameters like `stop`. Force Responses API when
|
| 107 |
+
# using gpt-5 models to avoid 400 errors such as: "Unsupported parameter: 'stop'".
|
| 108 |
+
use_responses = model.startswith("gpt-5")
|
| 109 |
+
|
| 110 |
+
if use_responses:
|
| 111 |
+
# Define a minimal subclass that drops the `stop` field when using the
|
| 112 |
+
# Responses API, since certain models (gpt-5-*) reject it entirely.
|
| 113 |
+
class _ChatOpenAIResponsesNoStop(ChatOpenAI):
|
| 114 |
+
def _get_request_payload(self, input_, *, stop=None, **kwargs): # type: ignore[override]
|
| 115 |
+
payload = super()._get_request_payload(input_, stop=stop, **kwargs)
|
| 116 |
+
try:
|
| 117 |
+
# If this call will use the Responses API, drop `stop` to avoid 400s.
|
| 118 |
+
if hasattr(self, "_use_responses_api") and self._use_responses_api(payload): # type: ignore[attr-defined]
|
| 119 |
+
payload.pop("stop", None)
|
| 120 |
+
# Also drop temperature for gpt-5 models as they only support default value
|
| 121 |
+
payload.pop("temperature", None)
|
| 122 |
+
except Exception:
|
| 123 |
+
# Be conservative: if anything goes wrong, still remove `stop` and `temperature`.
|
| 124 |
+
payload.pop("stop", None)
|
| 125 |
+
payload.pop("temperature", None)
|
| 126 |
+
return payload
|
| 127 |
+
|
| 128 |
+
return _ChatOpenAIResponsesNoStop(
|
| 129 |
+
model=model,
|
| 130 |
+
temperature=1, # Set to default value for gpt-5, will be removed in payload
|
| 131 |
+
stop_sequences=stop_sequences,
|
| 132 |
+
use_responses_api=True,
|
| 133 |
+
output_version="v0",
|
| 134 |
+
)
|
| 135 |
+
else:
|
| 136 |
+
return ChatOpenAI(
|
| 137 |
+
model=model,
|
| 138 |
+
temperature=temperature,
|
| 139 |
+
stop_sequences=stop_sequences,
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
elif source == "AzureOpenAI":
|
| 143 |
+
try:
|
| 144 |
+
from langchain_openai import AzureChatOpenAI
|
| 145 |
+
except ImportError:
|
| 146 |
+
raise ImportError( # noqa: B904
|
| 147 |
+
"langchain-openai package is required for Azure OpenAI models. Install with: pip install langchain-openai"
|
| 148 |
+
)
|
| 149 |
+
API_VERSION = "2024-12-01-preview"
|
| 150 |
+
model = model.replace("azure-", "")
|
| 151 |
+
return AzureChatOpenAI(
|
| 152 |
+
openai_api_key=os.getenv("OPENAI_API_KEY"),
|
| 153 |
+
azure_endpoint=os.getenv("OPENAI_ENDPOINT"),
|
| 154 |
+
azure_deployment=model,
|
| 155 |
+
openai_api_version=API_VERSION,
|
| 156 |
+
temperature=temperature,
|
| 157 |
+
)
|
| 158 |
+
|
| 159 |
+
elif source == "Anthropic":
|
| 160 |
+
try:
|
| 161 |
+
from langchain_anthropic import ChatAnthropic
|
| 162 |
+
except ImportError:
|
| 163 |
+
raise ImportError( # noqa: B904
|
| 164 |
+
"langchain-anthropic package is required for Anthropic models. Install with: pip install langchain-anthropic"
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
# Ensure ANTHROPIC_API_KEY is loaded from bash_profile if not in environment
|
| 168 |
+
if not os.environ.get("ANTHROPIC_API_KEY"):
|
| 169 |
+
try:
|
| 170 |
+
import subprocess
|
| 171 |
+
|
| 172 |
+
result = subprocess.run(
|
| 173 |
+
["bash", "-c", "source ~/.bash_profile 2>/dev/null && echo $ANTHROPIC_API_KEY"],
|
| 174 |
+
capture_output=True,
|
| 175 |
+
text=True,
|
| 176 |
+
timeout=5,
|
| 177 |
+
)
|
| 178 |
+
if result.stdout.strip():
|
| 179 |
+
os.environ["ANTHROPIC_API_KEY"] = result.stdout.strip()
|
| 180 |
+
print("✓ Loaded ANTHROPIC_API_KEY from ~/.bash_profile")
|
| 181 |
+
except Exception as e:
|
| 182 |
+
print(f"Note: Could not load ANTHROPIC_API_KEY from bash_profile: {e}")
|
| 183 |
+
|
| 184 |
+
return ChatAnthropic(
|
| 185 |
+
model=model,
|
| 186 |
+
temperature=temperature,
|
| 187 |
+
max_tokens=8192,
|
| 188 |
+
stop_sequences=stop_sequences,
|
| 189 |
+
)
|
| 190 |
+
|
| 191 |
+
elif source == "Gemini":
|
| 192 |
+
# If you want to use ChatGoogleGenerativeAI, you need to pass the stop sequences upon invoking the model.
|
| 193 |
+
# return ChatGoogleGenerativeAI(
|
| 194 |
+
# model=model,
|
| 195 |
+
# temperature=temperature,
|
| 196 |
+
# google_api_key=api_key,
|
| 197 |
+
# )
|
| 198 |
+
try:
|
| 199 |
+
from langchain_openai import ChatOpenAI
|
| 200 |
+
except ImportError:
|
| 201 |
+
raise ImportError( # noqa: B904
|
| 202 |
+
"langchain-openai package is required for Gemini models. Install with: pip install langchain-openai"
|
| 203 |
+
)
|
| 204 |
+
return ChatOpenAI(
|
| 205 |
+
model=model,
|
| 206 |
+
temperature=temperature,
|
| 207 |
+
api_key=os.getenv("GEMINI_API_KEY"),
|
| 208 |
+
base_url="https://generativelanguage.googleapis.com/v1beta/openai/",
|
| 209 |
+
stop_sequences=stop_sequences,
|
| 210 |
+
)
|
| 211 |
+
|
| 212 |
+
elif source == "Groq":
|
| 213 |
+
try:
|
| 214 |
+
from langchain_openai import ChatOpenAI
|
| 215 |
+
except ImportError:
|
| 216 |
+
raise ImportError( # noqa: B904
|
| 217 |
+
"langchain-openai package is required for Groq models. Install with: pip install langchain-openai"
|
| 218 |
+
)
|
| 219 |
+
return ChatOpenAI(
|
| 220 |
+
model=model,
|
| 221 |
+
temperature=temperature,
|
| 222 |
+
api_key=os.getenv("GROQ_API_KEY"),
|
| 223 |
+
base_url="https://api.groq.com/openai/v1",
|
| 224 |
+
stop_sequences=stop_sequences,
|
| 225 |
+
)
|
| 226 |
+
|
| 227 |
+
elif source == "Ollama":
|
| 228 |
+
try:
|
| 229 |
+
from langchain_ollama import ChatOllama
|
| 230 |
+
except ImportError:
|
| 231 |
+
raise ImportError( # noqa: B904
|
| 232 |
+
"langchain-ollama package is required for Ollama models. Install with: pip install langchain-ollama"
|
| 233 |
+
)
|
| 234 |
+
return ChatOllama(
|
| 235 |
+
model=model,
|
| 236 |
+
temperature=temperature,
|
| 237 |
+
)
|
| 238 |
+
|
| 239 |
+
elif source == "Bedrock":
|
| 240 |
+
try:
|
| 241 |
+
from langchain_aws import ChatBedrock
|
| 242 |
+
except ImportError:
|
| 243 |
+
raise ImportError( # noqa: B904
|
| 244 |
+
"langchain-aws package is required for Bedrock models. Install with: pip install langchain-aws"
|
| 245 |
+
)
|
| 246 |
+
return ChatBedrock(
|
| 247 |
+
model=model,
|
| 248 |
+
temperature=temperature,
|
| 249 |
+
stop_sequences=stop_sequences,
|
| 250 |
+
region_name=os.getenv("AWS_REGION", "us-east-1"),
|
| 251 |
+
)
|
| 252 |
+
|
| 253 |
+
elif source == "Custom":
|
| 254 |
+
try:
|
| 255 |
+
from langchain_openai import ChatOpenAI
|
| 256 |
+
except ImportError:
|
| 257 |
+
raise ImportError( # noqa: B904
|
| 258 |
+
"langchain-openai package is required for custom models. Install with: pip install langchain-openai"
|
| 259 |
+
)
|
| 260 |
+
# Custom LLM serving such as SGLang. Must expose an openai compatible API.
|
| 261 |
+
assert base_url is not None, "base_url must be provided for customly served LLMs"
|
| 262 |
+
llm = ChatOpenAI(
|
| 263 |
+
model=model,
|
| 264 |
+
temperature=temperature,
|
| 265 |
+
max_tokens=8192,
|
| 266 |
+
stop_sequences=stop_sequences,
|
| 267 |
+
base_url=base_url,
|
| 268 |
+
api_key=api_key,
|
| 269 |
+
)
|
| 270 |
+
return llm
|
| 271 |
+
|
| 272 |
+
else:
|
| 273 |
+
raise ValueError(
|
| 274 |
+
f"Invalid source: {source}. Valid options are 'OpenAI', 'AzureOpenAI', 'Anthropic', 'Gemini', 'Groq', 'Bedrock', or 'Ollama'"
|
| 275 |
+
)
|
Biomni/biomni/task/__init__.py
ADDED
|
File without changes
|
Biomni/biomni/task/base_task.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
class base_task:
|
| 2 |
+
def __init__(self):
|
| 3 |
+
pass
|
| 4 |
+
|
| 5 |
+
def get_example(self):
|
| 6 |
+
pass
|
| 7 |
+
|
| 8 |
+
def get_iterator(self):
|
| 9 |
+
pass
|
| 10 |
+
|
| 11 |
+
def evaluate(self):
|
| 12 |
+
pass
|
| 13 |
+
|
| 14 |
+
def output_class(self):
|
| 15 |
+
pass
|
| 16 |
+
|
| 17 |
+
def get_prompt_from_input(self, input):
|
| 18 |
+
return self.get_example(input)["prompt"]
|
Biomni/biomni/task/hle.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import pandas as pd
|
| 3 |
+
|
| 4 |
+
from biomni.task.base_task import base_task
|
| 5 |
+
|
| 6 |
+
np.random.seed(42)
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def shuffle(x):
|
| 10 |
+
np.random.shuffle(x)
|
| 11 |
+
return x
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class humanity_last_exam(base_task):
|
| 15 |
+
def __init__(self, path="./data", category="Biology/Medicine", answer_type="multipleChoice"):
|
| 16 |
+
if category not in [
|
| 17 |
+
"Other",
|
| 18 |
+
"Humanities/Social Science",
|
| 19 |
+
"Math",
|
| 20 |
+
"Physics",
|
| 21 |
+
"Computer Science/AI",
|
| 22 |
+
"Biology/Medicine",
|
| 23 |
+
"Chemistry",
|
| 24 |
+
"Engineering",
|
| 25 |
+
]:
|
| 26 |
+
raise ValueError(
|
| 27 |
+
"category must be one of ['Other', 'Humanities/Social Science', 'Math', 'Physics', 'Computer Science/AI', 'Biology/Medicine', 'Chemistry', 'Engineering']"
|
| 28 |
+
)
|
| 29 |
+
if answer_type not in ["exactMatch", "multipleChoice"]:
|
| 30 |
+
raise ValueError("answer_type must be one of ['exactMatch' or 'multipleChoice']")
|
| 31 |
+
|
| 32 |
+
self.dataset = category # Store dataset type
|
| 33 |
+
self.answer_type = answer_type
|
| 34 |
+
df = pd.read_parquet(path + "/hle/test_sampled_biology_medicine.parquet")
|
| 35 |
+
|
| 36 |
+
# Extract answer choices from question text
|
| 37 |
+
def extract_options(question):
|
| 38 |
+
# Find the "Answer Choices:" section
|
| 39 |
+
if "Answer Choices:" not in question:
|
| 40 |
+
return []
|
| 41 |
+
choices = question.split("Answer Choices:")[1].strip()
|
| 42 |
+
|
| 43 |
+
# Split on A., B., C. etc and clean up
|
| 44 |
+
options = []
|
| 45 |
+
letters = [
|
| 46 |
+
"A.",
|
| 47 |
+
"B.",
|
| 48 |
+
"C.",
|
| 49 |
+
"D.",
|
| 50 |
+
"E.",
|
| 51 |
+
"F.",
|
| 52 |
+
"G.",
|
| 53 |
+
"H.",
|
| 54 |
+
"I.",
|
| 55 |
+
"J.",
|
| 56 |
+
"K.",
|
| 57 |
+
"L.",
|
| 58 |
+
"M.",
|
| 59 |
+
"N.",
|
| 60 |
+
"O.",
|
| 61 |
+
"P.",
|
| 62 |
+
"Q.",
|
| 63 |
+
"R.",
|
| 64 |
+
"S.",
|
| 65 |
+
"T.",
|
| 66 |
+
"U.",
|
| 67 |
+
"V.",
|
| 68 |
+
"W.",
|
| 69 |
+
"X.",
|
| 70 |
+
"Y.",
|
| 71 |
+
"Z.",
|
| 72 |
+
]
|
| 73 |
+
for i, letter in enumerate(letters):
|
| 74 |
+
if letter in choices:
|
| 75 |
+
# Define the next letter if available
|
| 76 |
+
next_letter = letters[i + 1] if i + 1 < len(letters) else None
|
| 77 |
+
|
| 78 |
+
# Split between current letter and next letter
|
| 79 |
+
parts = choices.split(letter)[1]
|
| 80 |
+
if next_letter and next_letter in parts:
|
| 81 |
+
option = parts.split(next_letter)[0].strip()
|
| 82 |
+
else:
|
| 83 |
+
option = parts.strip()
|
| 84 |
+
|
| 85 |
+
options.append(option)
|
| 86 |
+
return options
|
| 87 |
+
|
| 88 |
+
def extract_question(question):
|
| 89 |
+
return question.split("Answer Choices:")[0].strip()
|
| 90 |
+
|
| 91 |
+
# Extract options and answers only for multiple choice questions and category is the same as the dataset
|
| 92 |
+
df = df[df["category"] == self.dataset]
|
| 93 |
+
df = df[df["answer_type"] == "multipleChoice"]
|
| 94 |
+
df["question_text"] = df.question
|
| 95 |
+
df["letter_answer"] = df["answer"].apply(lambda x: x[0])
|
| 96 |
+
|
| 97 |
+
self.query = df.question_text.values
|
| 98 |
+
# self.options = df.options_letters.values
|
| 99 |
+
self.answer = df.letter_answer.values
|
| 100 |
+
|
| 101 |
+
self.prompt = """Question: {question}"""
|
| 102 |
+
|
| 103 |
+
def get_example(self, index=None):
|
| 104 |
+
if index is None:
|
| 105 |
+
index = np.random.randint(len(self.query))
|
| 106 |
+
|
| 107 |
+
return {
|
| 108 |
+
"prompt": self.prompt.format(
|
| 109 |
+
question=self.query[index],
|
| 110 |
+
# options = self.options[index]
|
| 111 |
+
),
|
| 112 |
+
"answer": self.answer[index],
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
def get_iterator(self):
|
| 116 |
+
for i in range(len(self.query)):
|
| 117 |
+
yield self.get_example(i)
|
| 118 |
+
|
| 119 |
+
def evaluate(self, response):
|
| 120 |
+
## expected a list/array of symbols
|
| 121 |
+
from sklearn.metrics import accuracy_score
|
| 122 |
+
|
| 123 |
+
ground_truth = self.answer
|
| 124 |
+
response = np.array(response)
|
| 125 |
+
|
| 126 |
+
return {
|
| 127 |
+
"accuracy": accuracy_score(ground_truth, response),
|
| 128 |
+
"coverage": np.mean(response != self.refrain_label),
|
| 129 |
+
"refrain_ratio": np.mean(response == self.refrain_label),
|
| 130 |
+
"precision": accuracy_score(
|
| 131 |
+
ground_truth[np.where(response != self.refrain_label)],
|
| 132 |
+
response[np.where(response != self.refrain_label)],
|
| 133 |
+
),
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
def output_class(self):
|
| 137 |
+
from pydantic import BaseModel, Field
|
| 138 |
+
|
| 139 |
+
class MultipleChoiceOutput(BaseModel):
|
| 140 |
+
"""Multiple choice output."""
|
| 141 |
+
|
| 142 |
+
choice: str | None = Field(
|
| 143 |
+
description="Multiple choice answer. For example, if there is <answer>A</answer> in the prompt, the output should be 'A'."
|
| 144 |
+
)
|
| 145 |
+
|
| 146 |
+
return MultipleChoiceOutput
|
Biomni/biomni/task/lab_bench.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import pandas as pd
|
| 3 |
+
|
| 4 |
+
from biomni.task.base_task import base_task
|
| 5 |
+
|
| 6 |
+
np.random.seed(42)
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def shuffle(x):
|
| 10 |
+
np.random.shuffle(x)
|
| 11 |
+
return x
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class lab_bench(base_task):
|
| 15 |
+
def __init__(self, path="./data", dataset="DbQA"):
|
| 16 |
+
if dataset not in ["DbQA", "SeqQA"]:
|
| 17 |
+
raise ValueError("dataset must be one of 'DbQA', 'SeqQA'")
|
| 18 |
+
|
| 19 |
+
self.dataset = dataset # Store dataset type
|
| 20 |
+
df = pd.read_parquet(path + "/" + dataset + "/train-00000-of-00001_test.parquet")
|
| 21 |
+
|
| 22 |
+
self.prompt = """The following is a multiple choice question about biology.
|
| 23 |
+
Please answer by responding with the letter of the correct answer.
|
| 24 |
+
|
| 25 |
+
Question: {question}
|
| 26 |
+
Options:
|
| 27 |
+
{options}
|
| 28 |
+
|
| 29 |
+
You MUST include the letter of the correct answer within the following tags:
|
| 30 |
+
[ANSWER] and [/ANSWER]. For example, '[ANSWER]<answer>[/ANSWER]',
|
| 31 |
+
where <answer> is the correct letter. Always answer in exactly this format
|
| 32 |
+
of a single letter between the two tags, even if you are unsure.
|
| 33 |
+
We require this because we use automatic parsing.
|
| 34 |
+
"""
|
| 35 |
+
|
| 36 |
+
np.random.seed(42)
|
| 37 |
+
df["options"] = df.apply(
|
| 38 |
+
lambda x: shuffle(
|
| 39 |
+
x.distractors.tolist() + [x.ideal] + ["Insufficient information to answer the question."]
|
| 40 |
+
),
|
| 41 |
+
axis=1,
|
| 42 |
+
)
|
| 43 |
+
df["options_letters"] = df.options.apply(
|
| 44 |
+
lambda x: "\n".join([chr(ord("A") + i) + "." + item for i, item in enumerate(x)])
|
| 45 |
+
)
|
| 46 |
+
df["letter_answer"] = df.apply(
|
| 47 |
+
lambda x: chr(ord("A") + np.where(np.array(x.options) == x.ideal)[0][0]),
|
| 48 |
+
axis=1,
|
| 49 |
+
)
|
| 50 |
+
df["letter_refrain"] = df.apply(
|
| 51 |
+
lambda x: chr(
|
| 52 |
+
ord("A") + np.where(np.array(x.options) == "Insufficient information to answer the question.")[0][0]
|
| 53 |
+
),
|
| 54 |
+
axis=1,
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
self.query = df.question.values
|
| 58 |
+
self.options = df.options_letters.values
|
| 59 |
+
self.answer = df.letter_answer.values
|
| 60 |
+
self.refrain_label = df.letter_refrain.values
|
| 61 |
+
|
| 62 |
+
# Store protocol information if available
|
| 63 |
+
self.protocol = df.protocol.values if "protocol" in df.columns else None
|
| 64 |
+
|
| 65 |
+
def get_example(self, index=None):
|
| 66 |
+
if index is None:
|
| 67 |
+
index = np.random.randint(len(self.query))
|
| 68 |
+
|
| 69 |
+
if self.dataset == "ProtocolQA" and self.protocol is not None:
|
| 70 |
+
return {
|
| 71 |
+
"prompt": self.prompt.format(
|
| 72 |
+
protocol=self.protocol[index],
|
| 73 |
+
question=self.query[index],
|
| 74 |
+
options=self.options[index],
|
| 75 |
+
),
|
| 76 |
+
"answer": self.answer[index],
|
| 77 |
+
}
|
| 78 |
+
else:
|
| 79 |
+
return {
|
| 80 |
+
"prompt": self.prompt.format(question=self.query[index], options=self.options[index]),
|
| 81 |
+
"answer": self.answer[index],
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
def get_iterator(self):
|
| 85 |
+
for i in range(len(self.query)):
|
| 86 |
+
yield self.get_example(i)
|
| 87 |
+
|
| 88 |
+
def evaluate(self, response):
|
| 89 |
+
## expected a list/array of symbols
|
| 90 |
+
from sklearn.metrics import accuracy_score
|
| 91 |
+
|
| 92 |
+
ground_truth = self.answer
|
| 93 |
+
response = np.array(response)
|
| 94 |
+
|
| 95 |
+
return {
|
| 96 |
+
"accuracy": accuracy_score(ground_truth, response),
|
| 97 |
+
"coverage": np.mean(response != self.refrain_label),
|
| 98 |
+
"refrain_ratio": np.mean(response == self.refrain_label),
|
| 99 |
+
"precision": accuracy_score(
|
| 100 |
+
ground_truth[np.where(response != self.refrain_label)],
|
| 101 |
+
response[np.where(response != self.refrain_label)],
|
| 102 |
+
),
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
def output_class(self):
|
| 106 |
+
from pydantic import BaseModel, Field
|
| 107 |
+
|
| 108 |
+
class MultipleChoiceOutput(BaseModel):
|
| 109 |
+
"""Multiple choice output."""
|
| 110 |
+
|
| 111 |
+
choice: str | None = Field(
|
| 112 |
+
description="Multiple choice answer. For example, if there is <answer>A</answer> in the prompt, the output should be 'A'."
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
return MultipleChoiceOutput
|
Biomni/biomni/tool/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
from biomni.utils import get_tool_decorated_functions # noqa: F401
|
Biomni/biomni/tool/biochemistry.py
ADDED
|
@@ -0,0 +1,1026 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
def analyze_circular_dichroism_spectra(
|
| 2 |
+
sample_name,
|
| 3 |
+
sample_type,
|
| 4 |
+
wavelength_data,
|
| 5 |
+
cd_signal_data,
|
| 6 |
+
temperature_data=None,
|
| 7 |
+
thermal_cd_data=None,
|
| 8 |
+
output_dir="./",
|
| 9 |
+
):
|
| 10 |
+
"""Analyzes circular dichroism (CD) spectroscopy data to determine secondary structure and thermal stability.
|
| 11 |
+
|
| 12 |
+
Parameters
|
| 13 |
+
----------
|
| 14 |
+
sample_name : str
|
| 15 |
+
Name of the biomolecule sample (e.g., "Znf706", "G-quadruplex")
|
| 16 |
+
sample_type : str
|
| 17 |
+
Type of biomolecule ("protein" or "nucleic_acid")
|
| 18 |
+
wavelength_data : list or numpy.ndarray
|
| 19 |
+
Wavelength values in nm for CD spectrum
|
| 20 |
+
cd_signal_data : list or numpy.ndarray
|
| 21 |
+
CD signal intensity values (typically in mdeg or Δε)
|
| 22 |
+
temperature_data : list or numpy.ndarray, optional
|
| 23 |
+
Temperature values (°C) for thermal denaturation experiment
|
| 24 |
+
thermal_cd_data : list or numpy.ndarray, optional
|
| 25 |
+
CD signal values at specific wavelength across different temperatures
|
| 26 |
+
output_dir : str, optional
|
| 27 |
+
Directory to save result files, defaults to current directory
|
| 28 |
+
|
| 29 |
+
Returns
|
| 30 |
+
-------
|
| 31 |
+
str
|
| 32 |
+
Research log summarizing the CD analysis steps and results
|
| 33 |
+
|
| 34 |
+
"""
|
| 35 |
+
import os
|
| 36 |
+
from datetime import datetime
|
| 37 |
+
|
| 38 |
+
import numpy as np
|
| 39 |
+
|
| 40 |
+
# Initialize research log
|
| 41 |
+
log = f"# Circular Dichroism Analysis Report for {sample_name}\n"
|
| 42 |
+
log += f"Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
| 43 |
+
log += "## Sample Information\n"
|
| 44 |
+
log += f"- Sample Name: {sample_name}\n"
|
| 45 |
+
log += f"- Sample Type: {sample_type}\n\n"
|
| 46 |
+
|
| 47 |
+
# Convert inputs to numpy arrays if they aren't already
|
| 48 |
+
wavelength_data = np.array(wavelength_data)
|
| 49 |
+
cd_signal_data = np.array(cd_signal_data)
|
| 50 |
+
|
| 51 |
+
# Ensure output directory exists
|
| 52 |
+
if not os.path.exists(output_dir):
|
| 53 |
+
os.makedirs(output_dir)
|
| 54 |
+
|
| 55 |
+
# 1. Analyze CD spectrum for secondary structure
|
| 56 |
+
log += "## Secondary Structure Analysis\n"
|
| 57 |
+
|
| 58 |
+
# Different analysis approaches based on sample type
|
| 59 |
+
if sample_type.lower() == "protein":
|
| 60 |
+
# Analyze protein secondary structure based on characteristic spectral features
|
| 61 |
+
alpha_helix_signal = np.sum((wavelength_data >= 190) & (wavelength_data <= 195) & (cd_signal_data > 0))
|
| 62 |
+
beta_sheet_signal = np.sum((wavelength_data >= 215) & (wavelength_data <= 220) & (cd_signal_data < 0))
|
| 63 |
+
random_coil_signal = np.sum((wavelength_data >= 195) & (wavelength_data <= 200) & (cd_signal_data < 0))
|
| 64 |
+
|
| 65 |
+
# Simple classification based on signal patterns
|
| 66 |
+
if alpha_helix_signal > beta_sheet_signal and alpha_helix_signal > random_coil_signal:
|
| 67 |
+
structure = "predominantly alpha-helical"
|
| 68 |
+
elif beta_sheet_signal > alpha_helix_signal and beta_sheet_signal > random_coil_signal:
|
| 69 |
+
structure = "predominantly beta-sheet"
|
| 70 |
+
else:
|
| 71 |
+
structure = "mixed or predominantly random coil"
|
| 72 |
+
|
| 73 |
+
log += f"- The CD spectrum indicates {structure} structure for {sample_name}.\n"
|
| 74 |
+
log += "- Key spectral features:\n"
|
| 75 |
+
log += " - 190-195 nm region: associated with alpha-helical content\n"
|
| 76 |
+
log += " - 215-220 nm region: associated with beta-sheet content\n\n"
|
| 77 |
+
|
| 78 |
+
elif sample_type.lower() == "nucleic_acid":
|
| 79 |
+
# Analyze nucleic acid structure (e.g., G-quadruplex has characteristic positive peak ~295 nm)
|
| 80 |
+
g_quadruplex_signal = np.sum((wavelength_data >= 290) & (wavelength_data <= 300) & (cd_signal_data > 0))
|
| 81 |
+
b_form_signal = np.sum((wavelength_data >= 270) & (wavelength_data <= 280) & (cd_signal_data > 0))
|
| 82 |
+
|
| 83 |
+
if g_quadruplex_signal > 0:
|
| 84 |
+
structure = "G-quadruplex characteristics"
|
| 85 |
+
elif b_form_signal > 0:
|
| 86 |
+
structure = "B-form characteristics"
|
| 87 |
+
else:
|
| 88 |
+
structure = "non-standard structure"
|
| 89 |
+
|
| 90 |
+
log += f"- The CD spectrum indicates {structure} for {sample_name}.\n"
|
| 91 |
+
log += "- Key spectral features:\n"
|
| 92 |
+
log += " - 290-300 nm positive peak: characteristic of G-quadruplex structures\n"
|
| 93 |
+
log += " - 270-280 nm positive peak: characteristic of B-form DNA\n\n"
|
| 94 |
+
|
| 95 |
+
# Save spectral data results
|
| 96 |
+
spectral_file = os.path.join(output_dir, f"{sample_name}_cd_spectrum_analysis.txt")
|
| 97 |
+
with open(spectral_file, "w") as f:
|
| 98 |
+
f.write("Wavelength (nm)\tCD Signal\n")
|
| 99 |
+
for wl, signal in zip(wavelength_data, cd_signal_data, strict=False):
|
| 100 |
+
f.write(f"{wl:.1f}\t{signal:.4f}\n")
|
| 101 |
+
|
| 102 |
+
log += f"- Detailed spectral data saved to: {spectral_file}\n\n"
|
| 103 |
+
|
| 104 |
+
# 2. Thermal stability analysis (if temperature data provided)
|
| 105 |
+
if temperature_data is not None and thermal_cd_data is not None:
|
| 106 |
+
temperature_data = np.array(temperature_data)
|
| 107 |
+
thermal_cd_data = np.array(thermal_cd_data)
|
| 108 |
+
|
| 109 |
+
log += "## Thermal Stability Analysis\n"
|
| 110 |
+
|
| 111 |
+
# Simple Tm estimation (melting temperature) - find temperature at 50% unfolding
|
| 112 |
+
# Normalize thermal data to 0-1 range for unfolding fraction
|
| 113 |
+
min_signal = np.min(thermal_cd_data)
|
| 114 |
+
max_signal = np.max(thermal_cd_data)
|
| 115 |
+
unfolded_fraction = (thermal_cd_data - min_signal) / (max_signal - min_signal)
|
| 116 |
+
|
| 117 |
+
# Find the temperature closest to 50% unfolding
|
| 118 |
+
tm_idx = np.argmin(np.abs(unfolded_fraction - 0.5))
|
| 119 |
+
tm = temperature_data[tm_idx]
|
| 120 |
+
|
| 121 |
+
log += f"- Estimated melting temperature (Tm): {tm:.1f}°C\n"
|
| 122 |
+
|
| 123 |
+
# Cooperativity assessment (crude estimate based on transition steepness)
|
| 124 |
+
t_range = temperature_data[-1] - temperature_data[0]
|
| 125 |
+
transition_width = (
|
| 126 |
+
t_range / len(temperature_data) * np.sum((unfolded_fraction > 0.2) & (unfolded_fraction < 0.8))
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
+
if transition_width < 0.2 * t_range:
|
| 130 |
+
cooperativity = "highly cooperative (sharp transition)"
|
| 131 |
+
elif transition_width < 0.4 * t_range:
|
| 132 |
+
cooperativity = "moderately cooperative"
|
| 133 |
+
else:
|
| 134 |
+
cooperativity = "non-cooperative (broad transition)"
|
| 135 |
+
|
| 136 |
+
log += f"- Thermal transition: {cooperativity}\n"
|
| 137 |
+
|
| 138 |
+
# Save thermal denaturation data
|
| 139 |
+
thermal_file = os.path.join(output_dir, f"{sample_name}_thermal_denaturation.txt")
|
| 140 |
+
with open(thermal_file, "w") as f:
|
| 141 |
+
f.write("Temperature (°C)\tCD Signal\tUnfolded Fraction\n")
|
| 142 |
+
for temp, signal, unfold in zip(temperature_data, thermal_cd_data, unfolded_fraction, strict=False):
|
| 143 |
+
f.write(f"{temp:.1f}\t{signal:.4f}\t{unfold:.4f}\n")
|
| 144 |
+
|
| 145 |
+
log += f"- Thermal denaturation data saved to: {thermal_file}\n\n"
|
| 146 |
+
|
| 147 |
+
# 3. Summary and conclusions
|
| 148 |
+
log += "## Conclusions\n"
|
| 149 |
+
if sample_type.lower() == "protein":
|
| 150 |
+
log += f"- {sample_name} shows {structure} according to CD spectroscopy.\n"
|
| 151 |
+
else:
|
| 152 |
+
log += f"- {sample_name} exhibits {structure} according to CD spectroscopy.\n"
|
| 153 |
+
|
| 154 |
+
if temperature_data is not None:
|
| 155 |
+
log += f"- The molecule has a melting temperature of {tm:.1f}°C with {cooperativity}.\n"
|
| 156 |
+
|
| 157 |
+
return log
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def analyze_rna_secondary_structure_features(dot_bracket_structure, sequence=None):
|
| 161 |
+
"""Calculate numeric values for various structural features of an RNA secondary structure.
|
| 162 |
+
|
| 163 |
+
Parameters
|
| 164 |
+
----------
|
| 165 |
+
dot_bracket_structure : str
|
| 166 |
+
RNA secondary structure in dot-bracket notation (e.g., "(((...)))").
|
| 167 |
+
Parentheses represent base pairs, dots represent unpaired bases.
|
| 168 |
+
sequence : str, optional
|
| 169 |
+
The RNA sequence corresponding to the structure. If provided,
|
| 170 |
+
sequence-dependent energy calculations will be performed.
|
| 171 |
+
|
| 172 |
+
Returns
|
| 173 |
+
-------
|
| 174 |
+
str
|
| 175 |
+
A research log summarizing the calculated structural features and analysis steps.
|
| 176 |
+
|
| 177 |
+
"""
|
| 178 |
+
# Initialize research log
|
| 179 |
+
log = "# RNA Secondary Structure Feature Analysis\n\n"
|
| 180 |
+
|
| 181 |
+
# Validate input
|
| 182 |
+
if not all(c in "().[]{}" for c in dot_bracket_structure):
|
| 183 |
+
return "Error: Invalid dot-bracket notation. Use only '()', '[]', '{}', and '.'"
|
| 184 |
+
|
| 185 |
+
log += f"Input structure (length: {len(dot_bracket_structure)}): {dot_bracket_structure}\n"
|
| 186 |
+
if sequence:
|
| 187 |
+
log += f"Input sequence (length: {len(sequence)}): {sequence}\n"
|
| 188 |
+
if len(sequence) != len(dot_bracket_structure):
|
| 189 |
+
return "Error: Sequence and structure lengths do not match."
|
| 190 |
+
|
| 191 |
+
# Extract base pairs
|
| 192 |
+
pairs = []
|
| 193 |
+
stack = []
|
| 194 |
+
|
| 195 |
+
for i, char in enumerate(dot_bracket_structure):
|
| 196 |
+
if char in "([{":
|
| 197 |
+
stack.append((i, char))
|
| 198 |
+
elif char in ")]}":
|
| 199 |
+
if not stack:
|
| 200 |
+
return "Error: Unbalanced structure. More closing than opening brackets."
|
| 201 |
+
|
| 202 |
+
j, opening_char = stack.pop()
|
| 203 |
+
# Check for matching bracket types
|
| 204 |
+
if (
|
| 205 |
+
(opening_char == "(" and char != ")")
|
| 206 |
+
or (opening_char == "[" and char != "]")
|
| 207 |
+
or (opening_char == "{" and char != "}")
|
| 208 |
+
):
|
| 209 |
+
return "Error: Mismatched bracket types. Opening and closing brackets must match."
|
| 210 |
+
|
| 211 |
+
pairs.append((j, i))
|
| 212 |
+
|
| 213 |
+
if stack:
|
| 214 |
+
return "Error: Unbalanced structure. More opening than closing brackets."
|
| 215 |
+
|
| 216 |
+
# Sort pairs by position
|
| 217 |
+
pairs.sort()
|
| 218 |
+
|
| 219 |
+
# Identify stems (consecutive base pairs)
|
| 220 |
+
stems = []
|
| 221 |
+
current_stem = []
|
| 222 |
+
|
| 223 |
+
for i, (start, end) in enumerate(pairs):
|
| 224 |
+
if (i == 0 or start != pairs[i - 1][0] + 1 or end != pairs[i - 1][1] - 1) and current_stem:
|
| 225 |
+
stems.append(current_stem)
|
| 226 |
+
current_stem = []
|
| 227 |
+
current_stem.append((start, end))
|
| 228 |
+
|
| 229 |
+
if current_stem:
|
| 230 |
+
stems.append(current_stem)
|
| 231 |
+
|
| 232 |
+
# Calculate stem lengths
|
| 233 |
+
stem_lengths = [len(stem) for stem in stems]
|
| 234 |
+
|
| 235 |
+
# Calculate loop sizes
|
| 236 |
+
loops = []
|
| 237 |
+
for i in range(len(stems)):
|
| 238 |
+
stem = stems[i]
|
| 239 |
+
last_pair = stem[-1]
|
| 240 |
+
next_stem_start = stems[i + 1][0][0] if i < len(stems) - 1 else len(dot_bracket_structure)
|
| 241 |
+
loop_size = next_stem_start - last_pair[1] - 1
|
| 242 |
+
if loop_size > 0:
|
| 243 |
+
loops.append(loop_size)
|
| 244 |
+
|
| 245 |
+
# Calculate base pair statistics
|
| 246 |
+
total_paired_bases = len(pairs) * 2
|
| 247 |
+
total_unpaired_bases = len(dot_bracket_structure) - total_paired_bases
|
| 248 |
+
|
| 249 |
+
# Calculate simplified free energy if sequence is provided
|
| 250 |
+
stem_energies = []
|
| 251 |
+
if sequence and len(stems) > 0:
|
| 252 |
+
# Simplified energy parameters for nearest-neighbor model
|
| 253 |
+
# Values are approximate and simplified for illustration
|
| 254 |
+
energy_params = {
|
| 255 |
+
"AU": -0.9,
|
| 256 |
+
"UA": -0.9,
|
| 257 |
+
"GC": -2.1,
|
| 258 |
+
"CG": -2.1,
|
| 259 |
+
"GU": -0.5,
|
| 260 |
+
"UG": -0.5,
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
for stem in stems:
|
| 264 |
+
stem_energy = 0
|
| 265 |
+
for start, end in stem:
|
| 266 |
+
if start < len(sequence) and end < len(sequence):
|
| 267 |
+
pair = sequence[start] + sequence[end]
|
| 268 |
+
stem_energy += energy_params.get(pair, 0)
|
| 269 |
+
stem_energies.append(stem_energy)
|
| 270 |
+
|
| 271 |
+
# Prepare results
|
| 272 |
+
log += "\n## Structural Features\n\n"
|
| 273 |
+
log += f"Total base pairs: {len(pairs)}\n"
|
| 274 |
+
log += f"Number of stems: {len(stems)}\n"
|
| 275 |
+
log += f"Longest stem length: {max(stem_lengths) if stem_lengths else 0}\n"
|
| 276 |
+
log += f"Average stem length: {sum(stem_lengths) / len(stem_lengths) if stem_lengths else 0:.2f}\n"
|
| 277 |
+
log += f"Paired bases: {total_paired_bases} ({total_paired_bases / len(dot_bracket_structure) * 100:.1f}%)\n"
|
| 278 |
+
log += f"Unpaired bases: {total_unpaired_bases} ({total_unpaired_bases / len(dot_bracket_structure) * 100:.1f}%)\n"
|
| 279 |
+
|
| 280 |
+
if loops:
|
| 281 |
+
log += f"Number of loops: {len(loops)}\n"
|
| 282 |
+
log += f"Average loop size: {sum(loops) / len(loops):.2f}\n"
|
| 283 |
+
log += f"Largest loop size: {max(loops)}\n"
|
| 284 |
+
|
| 285 |
+
if sequence and stem_energies:
|
| 286 |
+
log += "\n## Energy Calculations\n\n"
|
| 287 |
+
log += f"Total estimated free energy: {sum(stem_energies):.2f} kcal/mol\n"
|
| 288 |
+
|
| 289 |
+
if len(stems) >= 2:
|
| 290 |
+
log += f"Upstream stem free energy: {stem_energies[0]:.2f} kcal/mol\n"
|
| 291 |
+
log += f"Downstream stem free energy: {stem_energies[-1]:.2f} kcal/mol\n"
|
| 292 |
+
|
| 293 |
+
# If the first stem is the "zipper" stem
|
| 294 |
+
if stem_lengths and stem_lengths[0] >= 3:
|
| 295 |
+
log += f"Zipper stem free energy: {stem_energies[0]:.2f} kcal/mol\n"
|
| 296 |
+
|
| 297 |
+
log += "\n## Stem Details\n\n"
|
| 298 |
+
for i, stem in enumerate(stems):
|
| 299 |
+
log += f"Stem {i + 1}: {len(stem)} base pairs\n"
|
| 300 |
+
log += f" Positions: {stem[0][0]}-{stem[0][1]} to {stem[-1][0]}-{stem[-1][1]}\n"
|
| 301 |
+
if sequence and i < len(stem_energies):
|
| 302 |
+
log += f" Estimated stability: {stem_energies[i]:.2f} kcal/mol\n"
|
| 303 |
+
|
| 304 |
+
return log
|
| 305 |
+
|
| 306 |
+
|
| 307 |
+
def analyze_protease_kinetics(
|
| 308 |
+
time_points,
|
| 309 |
+
fluorescence_data,
|
| 310 |
+
substrate_concentrations,
|
| 311 |
+
enzyme_concentration,
|
| 312 |
+
output_prefix="protease_kinetics",
|
| 313 |
+
output_dir="./",
|
| 314 |
+
):
|
| 315 |
+
"""Analyze protease kinetics data from fluorogenic peptide cleavage assays.
|
| 316 |
+
|
| 317 |
+
This function processes time-course fluorescence data from protease-mediated peptide
|
| 318 |
+
cleavage assays, fits the data to Michaelis-Menten kinetics, and determines key
|
| 319 |
+
kinetic parameters (kcat, KM, and catalytic efficiency).
|
| 320 |
+
|
| 321 |
+
Parameters
|
| 322 |
+
----------
|
| 323 |
+
time_points : numpy.ndarray
|
| 324 |
+
Array of time points (in seconds) at which measurements were taken
|
| 325 |
+
|
| 326 |
+
fluorescence_data : numpy.ndarray
|
| 327 |
+
2D array of fluorescence measurements where each row corresponds to a different
|
| 328 |
+
substrate concentration and each column corresponds to a time point
|
| 329 |
+
|
| 330 |
+
substrate_concentrations : numpy.ndarray
|
| 331 |
+
Array of substrate concentrations (in μM) corresponding to each row in fluorescence_data
|
| 332 |
+
|
| 333 |
+
enzyme_concentration : float
|
| 334 |
+
Concentration of the protease enzyme (in μM)
|
| 335 |
+
|
| 336 |
+
output_prefix : str, optional
|
| 337 |
+
Prefix for output files (default: "protease_kinetics")
|
| 338 |
+
|
| 339 |
+
output_dir : str, optional
|
| 340 |
+
Directory to save output files (default: "./")
|
| 341 |
+
|
| 342 |
+
Returns
|
| 343 |
+
-------
|
| 344 |
+
str
|
| 345 |
+
A research log summarizing the analysis steps and results
|
| 346 |
+
|
| 347 |
+
"""
|
| 348 |
+
import os
|
| 349 |
+
|
| 350 |
+
import matplotlib.pyplot as plt
|
| 351 |
+
import numpy as np
|
| 352 |
+
from scipy.optimize import curve_fit
|
| 353 |
+
|
| 354 |
+
# Ensure output directory exists
|
| 355 |
+
if not os.path.exists(output_dir):
|
| 356 |
+
os.makedirs(output_dir)
|
| 357 |
+
|
| 358 |
+
# Create full output paths
|
| 359 |
+
plot_filename = os.path.join(output_dir, f"{output_prefix}_mm_plot.png")
|
| 360 |
+
results_filename = os.path.join(output_dir, f"{output_prefix}_results.txt")
|
| 361 |
+
|
| 362 |
+
# Step 1: Calculate initial velocities for each substrate concentration
|
| 363 |
+
initial_velocities = np.zeros(len(substrate_concentrations))
|
| 364 |
+
|
| 365 |
+
for i, fluorescence_curve in enumerate(fluorescence_data):
|
| 366 |
+
# Linear fit to the initial portion of the curve (first 20% of time points or at least 5 points)
|
| 367 |
+
num_points = max(5, int(len(time_points) * 0.2))
|
| 368 |
+
slope, _ = np.polyfit(time_points[:num_points], fluorescence_curve[:num_points], 1)
|
| 369 |
+
initial_velocities[i] = slope
|
| 370 |
+
|
| 371 |
+
# Step 2: Define Michaelis-Menten equation for curve fitting
|
| 372 |
+
def michaelis_menten(s, vmax, km):
|
| 373 |
+
return vmax * s / (km + s)
|
| 374 |
+
|
| 375 |
+
# Step 3: Fit the data to the Michaelis-Menten equation
|
| 376 |
+
try:
|
| 377 |
+
params, covariance = curve_fit(
|
| 378 |
+
michaelis_menten,
|
| 379 |
+
substrate_concentrations,
|
| 380 |
+
initial_velocities,
|
| 381 |
+
p0=[max(initial_velocities), np.mean(substrate_concentrations)],
|
| 382 |
+
bounds=([0, 0], [np.inf, np.inf]),
|
| 383 |
+
)
|
| 384 |
+
|
| 385 |
+
vmax, km = params
|
| 386 |
+
std_dev = np.sqrt(np.diag(covariance))
|
| 387 |
+
vmax_std, km_std = std_dev
|
| 388 |
+
|
| 389 |
+
# Step 4: Calculate kcat and catalytic efficiency
|
| 390 |
+
kcat = vmax / enzyme_concentration
|
| 391 |
+
kcat_std = vmax_std / enzyme_concentration
|
| 392 |
+
catalytic_efficiency = kcat / km
|
| 393 |
+
catalytic_efficiency_std = catalytic_efficiency * np.sqrt((kcat_std / kcat) ** 2 + (km_std / km) ** 2)
|
| 394 |
+
|
| 395 |
+
# Step 5: Create a plot and save it
|
| 396 |
+
plt.figure(figsize=(10, 6))
|
| 397 |
+
plt.scatter(
|
| 398 |
+
substrate_concentrations,
|
| 399 |
+
initial_velocities,
|
| 400 |
+
color="blue",
|
| 401 |
+
label="Experimental data",
|
| 402 |
+
)
|
| 403 |
+
|
| 404 |
+
# Generate smooth curve for the fitted model
|
| 405 |
+
s_curve = np.linspace(0, max(substrate_concentrations) * 1.2, 100)
|
| 406 |
+
v_curve = michaelis_menten(s_curve, vmax, km)
|
| 407 |
+
plt.plot(s_curve, v_curve, "r-", label="Michaelis-Menten fit")
|
| 408 |
+
|
| 409 |
+
plt.xlabel("Substrate Concentration (μM)")
|
| 410 |
+
plt.ylabel("Initial Velocity (a.u./s)")
|
| 411 |
+
plt.title("Michaelis-Menten Kinetics")
|
| 412 |
+
plt.legend()
|
| 413 |
+
plt.grid(True, alpha=0.3)
|
| 414 |
+
|
| 415 |
+
plt.savefig(plot_filename)
|
| 416 |
+
plt.close()
|
| 417 |
+
|
| 418 |
+
# Step 6: Save numerical results to a file
|
| 419 |
+
with open(results_filename, "w") as f:
|
| 420 |
+
f.write("Protease Kinetics Analysis Results\n")
|
| 421 |
+
f.write("==================================\n\n")
|
| 422 |
+
f.write(f"Vmax: {vmax:.4f} ± {vmax_std:.4f} a.u./s\n")
|
| 423 |
+
f.write(f"KM: {km:.4f} ± {km_std:.4f} μM\n")
|
| 424 |
+
f.write(f"kcat: {kcat:.4f} ± {kcat_std:.4f} s^-1\n")
|
| 425 |
+
f.write(
|
| 426 |
+
f"Catalytic efficiency (kcat/KM): {catalytic_efficiency:.4f} ± {catalytic_efficiency_std:.4f} μM^-1 s^-1\n"
|
| 427 |
+
)
|
| 428 |
+
|
| 429 |
+
# Step 7: Create research log
|
| 430 |
+
research_log = f"""
|
| 431 |
+
Protease Kinetics Analysis Research Log
|
| 432 |
+
======================================
|
| 433 |
+
|
| 434 |
+
Analysis Steps:
|
| 435 |
+
1. Calculated initial velocities from time-course fluorescence data for {len(substrate_concentrations)} different substrate concentrations
|
| 436 |
+
2. Fitted initial velocities to the Michaelis-Menten equation using non-linear regression
|
| 437 |
+
3. Determined kinetic parameters and their uncertainties
|
| 438 |
+
|
| 439 |
+
Results:
|
| 440 |
+
- Vmax: {vmax:.4f} ± {vmax_std:.4f} a.u./s
|
| 441 |
+
- KM: {km:.4f} ± {km_std:.4f} μM
|
| 442 |
+
- kcat: {kcat:.4f} ± {kcat_std:.4f} s^-1
|
| 443 |
+
- Catalytic efficiency (kcat/KM): {catalytic_efficiency:.4f} ± {catalytic_efficiency_std:.4f} μM^-1 s^-1
|
| 444 |
+
|
| 445 |
+
Files Generated:
|
| 446 |
+
1. {plot_filename} - Michaelis-Menten plot showing experimental data and fitted curve
|
| 447 |
+
2. {results_filename} - Text file containing detailed results
|
| 448 |
+
|
| 449 |
+
Analysis completed successfully.
|
| 450 |
+
"""
|
| 451 |
+
|
| 452 |
+
return research_log
|
| 453 |
+
|
| 454 |
+
except Exception as e:
|
| 455 |
+
return f"Error during analysis: {str(e)}"
|
| 456 |
+
|
| 457 |
+
|
| 458 |
+
def analyze_enzyme_kinetics_assay(
|
| 459 |
+
enzyme_name,
|
| 460 |
+
substrate_concentrations,
|
| 461 |
+
enzyme_concentration,
|
| 462 |
+
modulators=None,
|
| 463 |
+
time_points=None,
|
| 464 |
+
output_dir="./",
|
| 465 |
+
):
|
| 466 |
+
"""Performs in vitro enzyme kinetics assay and analyzes the dose-dependent effects of modulators.
|
| 467 |
+
|
| 468 |
+
Parameters
|
| 469 |
+
----------
|
| 470 |
+
enzyme_name : str
|
| 471 |
+
Name of the purified enzyme being tested
|
| 472 |
+
substrate_concentrations : list or numpy.ndarray
|
| 473 |
+
List of substrate concentrations in μM for kinetic analysis
|
| 474 |
+
enzyme_concentration : float
|
| 475 |
+
Concentration of the enzyme in nM
|
| 476 |
+
modulators : dict, optional
|
| 477 |
+
Dictionary of modulators where keys are modulator names and values are lists of
|
| 478 |
+
concentrations in μM. Default is None (no modulators).
|
| 479 |
+
time_points : list or numpy.ndarray, optional
|
| 480 |
+
Time points in minutes for time-course measurements. Default is None, which uses
|
| 481 |
+
[0, 5, 10, 15, 20, 30, 45, 60].
|
| 482 |
+
output_dir : str, optional
|
| 483 |
+
Directory to save output files. Default is current directory.
|
| 484 |
+
|
| 485 |
+
Returns
|
| 486 |
+
-------
|
| 487 |
+
str
|
| 488 |
+
Research log summarizing the enzyme kinetics assay procedure and results
|
| 489 |
+
|
| 490 |
+
"""
|
| 491 |
+
import csv
|
| 492 |
+
import os
|
| 493 |
+
|
| 494 |
+
import numpy as np
|
| 495 |
+
from scipy.optimize import curve_fit
|
| 496 |
+
|
| 497 |
+
# Create output directory if it doesn't exist
|
| 498 |
+
if not os.path.exists(output_dir):
|
| 499 |
+
os.makedirs(output_dir)
|
| 500 |
+
|
| 501 |
+
# Set default time points if not provided
|
| 502 |
+
if time_points is None:
|
| 503 |
+
time_points = np.array([0, 5, 10, 15, 20, 30, 45, 60])
|
| 504 |
+
else:
|
| 505 |
+
# Ensure time_points is a numpy array
|
| 506 |
+
time_points = np.array(time_points)
|
| 507 |
+
|
| 508 |
+
# Initialize research log
|
| 509 |
+
log = f"## In Vitro Enzyme Kinetics Assay: {enzyme_name}\n\n"
|
| 510 |
+
log += f"Enzyme concentration: {enzyme_concentration} nM\n"
|
| 511 |
+
|
| 512 |
+
# Michaelis-Menten equation for curve fitting
|
| 513 |
+
def michaelis_menten(s, vmax, km):
|
| 514 |
+
return vmax * s / (km + s)
|
| 515 |
+
|
| 516 |
+
# 1. Time-course kinetic assay
|
| 517 |
+
log += "\n### Time-Course Kinetic Assay\n\n"
|
| 518 |
+
log += "Measuring enzyme activity over time to establish linear range.\n"
|
| 519 |
+
|
| 520 |
+
# Simulate time-course data (realistic enzyme kinetics with some noise)
|
| 521 |
+
# Using a simple exponential approach to equilibrium model
|
| 522 |
+
max_activity = 100 # arbitrary units
|
| 523 |
+
rate_constant = 0.05 # min^-1
|
| 524 |
+
|
| 525 |
+
# Simulate enzyme activity over time with some noise
|
| 526 |
+
np.random.seed(42) # For reproducibility
|
| 527 |
+
time_course_activity = max_activity * (1 - np.exp(-rate_constant * time_points))
|
| 528 |
+
time_course_activity += np.random.normal(0, 3, len(time_points)) # Add noise
|
| 529 |
+
|
| 530 |
+
# Save time-course data
|
| 531 |
+
time_course_file = os.path.join(output_dir, f"{enzyme_name}_time_course.csv")
|
| 532 |
+
with open(time_course_file, "w", newline="") as f:
|
| 533 |
+
writer = csv.writer(f)
|
| 534 |
+
writer.writerow(["Time (min)", "Activity (units)"])
|
| 535 |
+
for t, a in zip(time_points, time_course_activity, strict=False):
|
| 536 |
+
writer.writerow([t, a])
|
| 537 |
+
|
| 538 |
+
# Determine linear range (first ~30% of the curve)
|
| 539 |
+
linear_cutoff_index = np.where(time_course_activity >= 0.3 * max_activity)[0][0]
|
| 540 |
+
linear_time = time_points[: linear_cutoff_index + 1]
|
| 541 |
+
|
| 542 |
+
log += f"Time-course data saved to: {time_course_file}\n"
|
| 543 |
+
log += f"Linear range determined to be 0-{linear_time[-1]} minutes.\n"
|
| 544 |
+
|
| 545 |
+
# 2. Substrate kinetics (Michaelis-Menten analysis)
|
| 546 |
+
log += "\n### Substrate Kinetics Analysis\n\n"
|
| 547 |
+
|
| 548 |
+
# Simulate enzyme activity at different substrate concentrations
|
| 549 |
+
# based on Michaelis-Menten kinetics
|
| 550 |
+
true_vmax = 120 # arbitrary units
|
| 551 |
+
true_km = 25 # μM
|
| 552 |
+
|
| 553 |
+
activity_values = michaelis_menten(np.array(substrate_concentrations), true_vmax, true_km)
|
| 554 |
+
activity_values += np.random.normal(0, 5, len(substrate_concentrations)) # Add noise
|
| 555 |
+
|
| 556 |
+
# Fit data to Michaelis-Menten equation
|
| 557 |
+
try:
|
| 558 |
+
params, _ = curve_fit(
|
| 559 |
+
michaelis_menten,
|
| 560 |
+
substrate_concentrations,
|
| 561 |
+
activity_values,
|
| 562 |
+
p0=[100, 20],
|
| 563 |
+
bounds=([0, 0], [500, 200]),
|
| 564 |
+
)
|
| 565 |
+
vmax, km = params
|
| 566 |
+
|
| 567 |
+
# Save substrate kinetics data
|
| 568 |
+
kinetics_file = os.path.join(output_dir, f"{enzyme_name}_substrate_kinetics.csv")
|
| 569 |
+
with open(kinetics_file, "w", newline="") as f:
|
| 570 |
+
writer = csv.writer(f)
|
| 571 |
+
writer.writerow(["Substrate (μM)", "Activity (units)"])
|
| 572 |
+
for s, a in zip(substrate_concentrations, activity_values, strict=False):
|
| 573 |
+
writer.writerow([s, a])
|
| 574 |
+
|
| 575 |
+
log += "Michaelis-Menten parameters:\n"
|
| 576 |
+
log += f"- Vmax: {vmax:.2f} units\n"
|
| 577 |
+
log += f"- Km: {km:.2f} μM\n"
|
| 578 |
+
log += f"Substrate kinetics data saved to: {kinetics_file}\n"
|
| 579 |
+
except Exception:
|
| 580 |
+
log += "Error: Could not fit data to Michaelis-Menten model.\n"
|
| 581 |
+
|
| 582 |
+
# 3. Modulator effects (if provided)
|
| 583 |
+
if modulators:
|
| 584 |
+
log += "\n### Modulator Effects Analysis\n\n"
|
| 585 |
+
|
| 586 |
+
for modulator_name, concentrations in modulators.items():
|
| 587 |
+
log += f"#### Testing modulator: {modulator_name}\n\n"
|
| 588 |
+
|
| 589 |
+
# Choose a fixed substrate concentration near Km for inhibition studies
|
| 590 |
+
substrate_conc = true_km
|
| 591 |
+
|
| 592 |
+
# Simulate modulator effects (e.g., competitive inhibition)
|
| 593 |
+
# For simplicity, using a sigmoidal dose-response curve
|
| 594 |
+
ic50 = np.random.uniform(1, 50) # Random IC50 between 1-50 μM
|
| 595 |
+
hill_coef = 1.0 # Hill coefficient
|
| 596 |
+
|
| 597 |
+
# Calculate normalized activity (% of control)
|
| 598 |
+
michaelis_menten(substrate_conc, true_vmax, true_km)
|
| 599 |
+
|
| 600 |
+
# Calculate activities at different modulator concentrations
|
| 601 |
+
modulator_activities = []
|
| 602 |
+
for conc in concentrations:
|
| 603 |
+
# Sigmoidal dose-response curve
|
| 604 |
+
if conc == 0:
|
| 605 |
+
activity = 100 # 100% activity at zero concentration
|
| 606 |
+
else:
|
| 607 |
+
activity = 100 / (1 + (conc / ic50) ** hill_coef)
|
| 608 |
+
|
| 609 |
+
# Add some noise
|
| 610 |
+
activity += np.random.normal(0, 3)
|
| 611 |
+
modulator_activities.append(activity)
|
| 612 |
+
|
| 613 |
+
# Save modulator data
|
| 614 |
+
modulator_file = os.path.join(output_dir, f"{enzyme_name}_{modulator_name}_dose_response.csv")
|
| 615 |
+
with open(modulator_file, "w", newline="") as f:
|
| 616 |
+
writer = csv.writer(f)
|
| 617 |
+
writer.writerow([f"{modulator_name} (μM)", "Activity (% of control)"])
|
| 618 |
+
for conc, act in zip(concentrations, modulator_activities, strict=False):
|
| 619 |
+
writer.writerow([conc, act])
|
| 620 |
+
|
| 621 |
+
# Calculate IC50 using curve fitting if there are enough data points
|
| 622 |
+
if len(concentrations) >= 4:
|
| 623 |
+
try:
|
| 624 |
+
# Define dose-response curve function
|
| 625 |
+
def dose_response(x, ic50, hill):
|
| 626 |
+
return 100 / (1 + (x / ic50) ** hill)
|
| 627 |
+
|
| 628 |
+
# Filter out zero concentration
|
| 629 |
+
nonzero_conc = np.array([c for c in concentrations if c > 0])
|
| 630 |
+
nonzero_act = np.array(
|
| 631 |
+
[a for c, a in zip(concentrations, modulator_activities, strict=False) if c > 0]
|
| 632 |
+
)
|
| 633 |
+
|
| 634 |
+
if len(nonzero_conc) >= 3: # Need at least 3 points for fitting
|
| 635 |
+
params, _ = curve_fit(
|
| 636 |
+
dose_response,
|
| 637 |
+
nonzero_conc,
|
| 638 |
+
nonzero_act,
|
| 639 |
+
p0=[10, 1],
|
| 640 |
+
bounds=([0.1, 0.1], [1000, 10]),
|
| 641 |
+
)
|
| 642 |
+
calc_ic50, calc_hill = params
|
| 643 |
+
|
| 644 |
+
log += f"Dose-response analysis for {modulator_name}:\n"
|
| 645 |
+
log += f"- IC50: {calc_ic50:.2f} μM\n"
|
| 646 |
+
log += f"- Hill coefficient: {calc_hill:.2f}\n"
|
| 647 |
+
else:
|
| 648 |
+
log += f"Insufficient non-zero data points to calculate IC50 for {modulator_name}.\n"
|
| 649 |
+
except Exception:
|
| 650 |
+
log += f"Error: Could not fit dose-response curve for {modulator_name}.\n"
|
| 651 |
+
else:
|
| 652 |
+
log += f"Insufficient data points to calculate IC50 for {modulator_name}.\n"
|
| 653 |
+
|
| 654 |
+
log += f"Dose-response data for {modulator_name} saved to: {modulator_file}\n\n"
|
| 655 |
+
|
| 656 |
+
# 4. Summary
|
| 657 |
+
log += "\n### Summary\n\n"
|
| 658 |
+
log += f"Completed in vitro enzyme kinetics assay for {enzyme_name}.\n"
|
| 659 |
+
log += f"- Determined linear range for time-course measurements: 0-{linear_time[-1]} minutes\n"
|
| 660 |
+
log += f"- Characterized Michaelis-Menten kinetics (Vmax: {vmax:.2f} units, Km: {km:.2f} μM)\n"
|
| 661 |
+
|
| 662 |
+
if modulators:
|
| 663 |
+
log += "- Analyzed the effects of modulators:\n"
|
| 664 |
+
for modulator_name in modulators:
|
| 665 |
+
log += f" * {modulator_name}: Data saved to {enzyme_name}_{modulator_name}_dose_response.csv\n"
|
| 666 |
+
|
| 667 |
+
return log
|
| 668 |
+
|
| 669 |
+
|
| 670 |
+
def analyze_itc_binding_thermodynamics(
|
| 671 |
+
itc_data_path=None,
|
| 672 |
+
itc_data=None,
|
| 673 |
+
temperature=298.15,
|
| 674 |
+
protein_concentration=None,
|
| 675 |
+
ligand_concentration=None,
|
| 676 |
+
):
|
| 677 |
+
"""Analyzes isothermal titration calorimetry (ITC) data to determine binding affinity and thermodynamic parameters.
|
| 678 |
+
|
| 679 |
+
Parameters
|
| 680 |
+
----------
|
| 681 |
+
itc_data_path : str, optional
|
| 682 |
+
Path to CSV or TSV file containing ITC thermogram data with columns for injection number,
|
| 683 |
+
injection volume, and heat released/absorbed. Expected columns: 'injection', 'volume', 'heat'
|
| 684 |
+
itc_data : numpy.ndarray, optional
|
| 685 |
+
Raw ITC thermogram data as a numpy array.
|
| 686 |
+
Expected shape: (n_injections, 3) with columns for injection number, injection volume, and heat
|
| 687 |
+
This parameter is provided for backward compatibility and will be deprecated.
|
| 688 |
+
temperature : float, optional
|
| 689 |
+
Temperature in Kelvin at which the experiment was conducted. Default is 298.15 K (25°C).
|
| 690 |
+
protein_concentration : float, optional
|
| 691 |
+
Initial concentration of protein in the cell in molar (M). Required for accurate fitting.
|
| 692 |
+
ligand_concentration : float, optional
|
| 693 |
+
Concentration of ligand in the syringe in molar (M). Required for accurate fitting.
|
| 694 |
+
|
| 695 |
+
Returns
|
| 696 |
+
-------
|
| 697 |
+
str
|
| 698 |
+
A research log summarizing the analysis steps and results, including binding affinity (Kd),
|
| 699 |
+
binding enthalpy (ΔH), binding entropy (ΔS), and Gibbs free energy (ΔG).
|
| 700 |
+
|
| 701 |
+
"""
|
| 702 |
+
import datetime
|
| 703 |
+
|
| 704 |
+
import numpy as np
|
| 705 |
+
import pandas as pd
|
| 706 |
+
from scipy.optimize import curve_fit
|
| 707 |
+
|
| 708 |
+
log = []
|
| 709 |
+
log.append(f"# ITC Binding Affinity Analysis - {datetime.datetime.now().strftime('%Y-%m-%d %H:%M')}")
|
| 710 |
+
log.append("\n## Data Preprocessing")
|
| 711 |
+
|
| 712 |
+
# Check if we have data to process
|
| 713 |
+
if itc_data_path is None and itc_data is None:
|
| 714 |
+
log.append("Error: No data provided. Please provide either itc_data_path or itc_data.")
|
| 715 |
+
return "\n".join(log)
|
| 716 |
+
|
| 717 |
+
# Load data from file if path is provided
|
| 718 |
+
if itc_data_path is not None:
|
| 719 |
+
try:
|
| 720 |
+
if itc_data_path.endswith(".csv"):
|
| 721 |
+
loaded_data = pd.read_csv(itc_data_path)
|
| 722 |
+
elif itc_data_path.endswith((".tsv", ".txt")):
|
| 723 |
+
loaded_data = pd.read_csv(itc_data_path, sep="\t")
|
| 724 |
+
else:
|
| 725 |
+
log.append("Error: Unsupported file format. Please provide a CSV or TSV file.")
|
| 726 |
+
return "\n".join(log)
|
| 727 |
+
|
| 728 |
+
log.append(f"- Loaded data from file: {itc_data_path}")
|
| 729 |
+
log.append(f"- Input data: DataFrame with {len(loaded_data)} injections")
|
| 730 |
+
|
| 731 |
+
if all(col in loaded_data.columns for col in ["injection", "volume", "heat"]):
|
| 732 |
+
data = loaded_data[["injection", "volume", "heat"]].values
|
| 733 |
+
else:
|
| 734 |
+
log.append("- Error: DataFrame must contain 'injection', 'volume', and 'heat' columns")
|
| 735 |
+
return "\n".join(log)
|
| 736 |
+
except Exception as e:
|
| 737 |
+
log.append(f"- Error loading data from file: {str(e)}")
|
| 738 |
+
return "\n".join(log)
|
| 739 |
+
# Use provided numpy array data
|
| 740 |
+
elif itc_data is not None:
|
| 741 |
+
if isinstance(itc_data, pd.DataFrame):
|
| 742 |
+
log.append("Warning: Passing DataFrame directly is deprecated. Please use itc_data_path instead.")
|
| 743 |
+
log.append(f"- Input data: DataFrame with {len(itc_data)} injections")
|
| 744 |
+
if all(col in itc_data.columns for col in ["injection", "volume", "heat"]):
|
| 745 |
+
data = itc_data[["injection", "volume", "heat"]].values
|
| 746 |
+
else:
|
| 747 |
+
log.append("- Error: DataFrame must contain 'injection', 'volume', and 'heat' columns")
|
| 748 |
+
return "\n".join(log)
|
| 749 |
+
else:
|
| 750 |
+
log.append(f"- Input data: Array with {len(itc_data)} injections")
|
| 751 |
+
data = np.array(itc_data)
|
| 752 |
+
|
| 753 |
+
# Check if concentration data is provided
|
| 754 |
+
if protein_concentration is None or ligand_concentration is None:
|
| 755 |
+
log.append("- Warning: Protein or ligand concentration not provided. Using normalized data for fitting.")
|
| 756 |
+
protein_concentration = 1.0
|
| 757 |
+
ligand_concentration = 10.0
|
| 758 |
+
|
| 759 |
+
# Extract data
|
| 760 |
+
injections = data[:, 0]
|
| 761 |
+
volumes = data[:, 1]
|
| 762 |
+
heats = data[:, 2]
|
| 763 |
+
|
| 764 |
+
log.append(f"- Processed {len(injections)} injections")
|
| 765 |
+
|
| 766 |
+
# Calculate molar ratio for each injection
|
| 767 |
+
cell_volume = 1.4 # Default cell volume in mL (typical for ITC)
|
| 768 |
+
|
| 769 |
+
# Calculate cumulative ligand added
|
| 770 |
+
cumulative_volume = np.cumsum(volumes)
|
| 771 |
+
dilution_factor = 1 - (cumulative_volume / cell_volume)
|
| 772 |
+
protein_conc_corrected = protein_concentration * dilution_factor
|
| 773 |
+
|
| 774 |
+
# Calculate molar ratio [ligand]/[protein] for each injection
|
| 775 |
+
molar_ratios = np.zeros_like(injections, dtype=float)
|
| 776 |
+
for i in range(len(injections)):
|
| 777 |
+
ligand_added = volumes[i] * ligand_concentration / 1000 # Convert to mmol
|
| 778 |
+
if i == 0:
|
| 779 |
+
molar_ratios[i] = ligand_added / (protein_concentration * cell_volume / 1000) # Convert to mmol
|
| 780 |
+
else:
|
| 781 |
+
# Account for dilution and previous injections
|
| 782 |
+
molar_ratios[i] = molar_ratios[i - 1] + ligand_added / (
|
| 783 |
+
protein_conc_corrected[i] * (cell_volume - cumulative_volume[i]) / 1000
|
| 784 |
+
)
|
| 785 |
+
|
| 786 |
+
log.append("\n## Model Fitting")
|
| 787 |
+
log.append("- Applying one-site binding model to the ITC data")
|
| 788 |
+
|
| 789 |
+
# Define one-site binding model function
|
| 790 |
+
def one_site_model(x, Kd, dH, n):
|
| 791 |
+
"""One-site binding model for ITC data.
|
| 792 |
+
|
| 793 |
+
Parameters
|
| 794 |
+
----------
|
| 795 |
+
x: Molar ratio [ligand]/[protein]
|
| 796 |
+
Kd: Dissociation constant (M)
|
| 797 |
+
dH: Enthalpy change (cal/mol)
|
| 798 |
+
n: Stoichiometry
|
| 799 |
+
|
| 800 |
+
Returns: Heat per injection
|
| 801 |
+
|
| 802 |
+
"""
|
| 803 |
+
# Convert x to fraction bound using the binding equation
|
| 804 |
+
Ka = 1 / Kd # Association constant
|
| 805 |
+
protein = protein_conc_corrected
|
| 806 |
+
|
| 807 |
+
# Calculate heat for each injection
|
| 808 |
+
q = np.zeros_like(x)
|
| 809 |
+
for i in range(len(x)):
|
| 810 |
+
if i == 0:
|
| 811 |
+
# First injection
|
| 812 |
+
bound = (n * protein[i] * Ka * (x[i] * protein[i])) / (1 + Ka * (x[i] * protein[i]))
|
| 813 |
+
q[i] = bound * dH * cell_volume
|
| 814 |
+
else:
|
| 815 |
+
# Subsequent injections (differential heat)
|
| 816 |
+
bound_prev = (n * protein[i - 1] * Ka * (x[i - 1] * protein[i - 1])) / (
|
| 817 |
+
1 + Ka * (x[i - 1] * protein[i - 1])
|
| 818 |
+
)
|
| 819 |
+
bound_curr = (n * protein[i] * Ka * (x[i] * protein[i])) / (1 + Ka * (x[i] * protein[i]))
|
| 820 |
+
q[i] = bound_curr * dH * (cell_volume - cumulative_volume[i]) - bound_prev * dH * (
|
| 821 |
+
cell_volume - cumulative_volume[i - 1]
|
| 822 |
+
)
|
| 823 |
+
|
| 824 |
+
return q
|
| 825 |
+
|
| 826 |
+
# Initial parameter guesses
|
| 827 |
+
p0 = [1e-6, -5000, 1.0] # Kd (M), dH (cal/mol), n (stoichiometry)
|
| 828 |
+
|
| 829 |
+
try:
|
| 830 |
+
# Fit the model to the data
|
| 831 |
+
popt, pcov = curve_fit(one_site_model, molar_ratios, heats, p0=p0, maxfev=10000)
|
| 832 |
+
Kd, dH, n = popt
|
| 833 |
+
|
| 834 |
+
# Calculate standard errors
|
| 835 |
+
perr = np.sqrt(np.diag(pcov))
|
| 836 |
+
Kd_err, dH_err, n_err = perr
|
| 837 |
+
|
| 838 |
+
# Calculate other thermodynamic parameters
|
| 839 |
+
R = 1.9872 # Gas constant in cal/(mol·K)
|
| 840 |
+
dG = R * temperature * np.log(Kd) # Gibbs free energy
|
| 841 |
+
dS = (dH - dG) / temperature # Entropy
|
| 842 |
+
|
| 843 |
+
log.append("- Model fitting successful")
|
| 844 |
+
log.append("\n## Results")
|
| 845 |
+
log.append(f"- Binding Stoichiometry (n): {n:.2f} ± {n_err:.2f}")
|
| 846 |
+
log.append(f"- Dissociation Constant (Kd): {Kd * 1e6:.2f} ± {Kd_err * 1e6:.2f} μM")
|
| 847 |
+
log.append(f"- Association Constant (Ka): {1 / Kd / 1e6:.2f} × 10^6 M^-1")
|
| 848 |
+
log.append(f"- Binding Enthalpy (ΔH): {dH:.2f} ± {dH_err:.2f} cal/mol")
|
| 849 |
+
log.append(f"- Binding Entropy (ΔS): {dS:.2f} cal/(mol·K)")
|
| 850 |
+
log.append(f"- Gibbs Free Energy (ΔG): {dG:.2f} cal/mol")
|
| 851 |
+
|
| 852 |
+
# Calculate goodness of fit
|
| 853 |
+
residuals = heats - one_site_model(molar_ratios, *popt)
|
| 854 |
+
ss_res = np.sum(residuals**2)
|
| 855 |
+
ss_tot = np.sum((heats - np.mean(heats)) ** 2)
|
| 856 |
+
r_squared = 1 - (ss_res / ss_tot)
|
| 857 |
+
log.append(f"- R-squared: {r_squared:.4f}")
|
| 858 |
+
|
| 859 |
+
except Exception as e:
|
| 860 |
+
log.append(f"- Error during model fitting: {str(e)}")
|
| 861 |
+
log.append("- Consider trying different initial parameter guesses or a different binding model")
|
| 862 |
+
|
| 863 |
+
log.append("\n## Conclusion")
|
| 864 |
+
log.append("- Analysis complete. The thermodynamic parameters have been estimated using a one-site binding model.")
|
| 865 |
+
log.append(
|
| 866 |
+
"- For more complex binding scenarios, consider using multi-site binding models or specialized ITC analysis software."
|
| 867 |
+
)
|
| 868 |
+
|
| 869 |
+
return "\n".join(log)
|
| 870 |
+
|
| 871 |
+
|
| 872 |
+
def analyze_protein_conservation(protein_sequences, output_dir="./"):
|
| 873 |
+
"""Perform multiple sequence alignment and phylogenetic analysis to identify conserved protein regions.
|
| 874 |
+
|
| 875 |
+
Parameters
|
| 876 |
+
----------
|
| 877 |
+
protein_sequences : list of str
|
| 878 |
+
List of protein sequences in FASTA format from multiple organisms.
|
| 879 |
+
output_dir : str, optional
|
| 880 |
+
Directory to save output files (default: "./output")
|
| 881 |
+
|
| 882 |
+
Returns
|
| 883 |
+
-------
|
| 884 |
+
str
|
| 885 |
+
Research log summarizing the analysis steps and results, including filenames of saved outputs.
|
| 886 |
+
|
| 887 |
+
"""
|
| 888 |
+
import os
|
| 889 |
+
|
| 890 |
+
from Bio import AlignIO, Phylo, SeqIO
|
| 891 |
+
from Bio.Align.Applications import MuscleCommandline
|
| 892 |
+
from Bio.Phylo.TreeConstruction import DistanceCalculator, DistanceTreeConstructor
|
| 893 |
+
|
| 894 |
+
# Create a research log
|
| 895 |
+
log = []
|
| 896 |
+
log.append("# Protein Sequence Alignment and Conservation Analysis")
|
| 897 |
+
|
| 898 |
+
# Ensure output directory exists
|
| 899 |
+
if not os.path.exists(output_dir):
|
| 900 |
+
os.makedirs(output_dir)
|
| 901 |
+
|
| 902 |
+
# Step 1: Save input sequences to a temporary file
|
| 903 |
+
log.append("\n## Step 1: Preparing Input Sequences")
|
| 904 |
+
input_file = os.path.join(output_dir, "input.fasta")
|
| 905 |
+
|
| 906 |
+
# Check if input is already in FASTA format or needs conversion
|
| 907 |
+
if isinstance(protein_sequences, list):
|
| 908 |
+
if all(">" in seq for seq in protein_sequences):
|
| 909 |
+
# Already in FASTA format
|
| 910 |
+
with open(input_file, "w") as f:
|
| 911 |
+
f.write("\n".join(protein_sequences))
|
| 912 |
+
else:
|
| 913 |
+
# Convert to FASTA format
|
| 914 |
+
with open(input_file, "w") as f:
|
| 915 |
+
for i, seq in enumerate(protein_sequences):
|
| 916 |
+
f.write(f">Sequence_{i + 1}\n{seq}\n")
|
| 917 |
+
else:
|
| 918 |
+
# Assume it's a single string in FASTA format
|
| 919 |
+
with open(input_file, "w") as f:
|
| 920 |
+
f.write(protein_sequences)
|
| 921 |
+
|
| 922 |
+
log.append(f"Input sequences saved to {input_file}")
|
| 923 |
+
log.append(f"Number of sequences: {len(list(SeqIO.parse(input_file, 'fasta')))}")
|
| 924 |
+
|
| 925 |
+
# Step 2: Perform multiple sequence alignment using MUSCLE
|
| 926 |
+
log.append("\n## Step 2: Multiple Sequence Alignment")
|
| 927 |
+
aligned_file = os.path.join(output_dir, "aligned.fasta")
|
| 928 |
+
|
| 929 |
+
try:
|
| 930 |
+
# Run MUSCLE for multiple sequence alignment
|
| 931 |
+
muscle_cline = MuscleCommandline(input=input_file, out=aligned_file)
|
| 932 |
+
stdout, stderr = muscle_cline()
|
| 933 |
+
log.append("Multiple sequence alignment completed using MUSCLE")
|
| 934 |
+
log.append(f"Alignment saved to {aligned_file}")
|
| 935 |
+
|
| 936 |
+
# Load the alignment
|
| 937 |
+
alignment = AlignIO.read(aligned_file, "fasta")
|
| 938 |
+
log.append(f"Alignment length: {alignment.get_alignment_length()} positions")
|
| 939 |
+
|
| 940 |
+
except Exception as e:
|
| 941 |
+
log.append(f"MUSCLE alignment failed: {str(e)}")
|
| 942 |
+
log.append("Attempting to use Biopython's built-in alignment methods...")
|
| 943 |
+
|
| 944 |
+
# Fallback to a simpler approach without pairwise2
|
| 945 |
+
from Bio.Align import MultipleSeqAlignment
|
| 946 |
+
from Bio.Seq import Seq
|
| 947 |
+
from Bio.SeqRecord import SeqRecord
|
| 948 |
+
|
| 949 |
+
sequences = list(SeqIO.parse(input_file, "fasta"))
|
| 950 |
+
|
| 951 |
+
# Simple approach: just pad sequences to the same length
|
| 952 |
+
max_length = max(len(seq.seq) for seq in sequences)
|
| 953 |
+
alignments = []
|
| 954 |
+
|
| 955 |
+
for seq in sequences:
|
| 956 |
+
# Pad sequence to max length
|
| 957 |
+
padded_seq = str(seq.seq).ljust(max_length, "-")
|
| 958 |
+
new_seq = SeqRecord(Seq(padded_seq), id=seq.id, description=seq.description)
|
| 959 |
+
alignments.append(new_seq)
|
| 960 |
+
|
| 961 |
+
# Create and save the alignment
|
| 962 |
+
msa = MultipleSeqAlignment(alignments)
|
| 963 |
+
AlignIO.write(msa, aligned_file, "fasta")
|
| 964 |
+
alignment = msa
|
| 965 |
+
log.append("Simple padding alignment completed as fallback method")
|
| 966 |
+
log.append(f"Alignment saved to {aligned_file}")
|
| 967 |
+
|
| 968 |
+
# Step 3: Generate a phylogenetic tree
|
| 969 |
+
log.append("\n## Step 3: Phylogenetic Analysis")
|
| 970 |
+
tree_file = os.path.join(output_dir, "tree.newick")
|
| 971 |
+
|
| 972 |
+
# Calculate distance matrix
|
| 973 |
+
calculator = DistanceCalculator("identity")
|
| 974 |
+
dm = calculator.get_distance(alignment)
|
| 975 |
+
|
| 976 |
+
# Construct the phylogenetic tree using neighbor-joining method
|
| 977 |
+
constructor = DistanceTreeConstructor()
|
| 978 |
+
tree = constructor.nj(dm)
|
| 979 |
+
|
| 980 |
+
# Save the tree
|
| 981 |
+
Phylo.write(tree, tree_file, "newick")
|
| 982 |
+
log.append("Phylogenetic tree constructed using neighbor-joining method")
|
| 983 |
+
log.append(f"Tree saved to {tree_file}")
|
| 984 |
+
|
| 985 |
+
# Step 4: Analyze conserved regions
|
| 986 |
+
log.append("\n## Step 4: Conservation Analysis")
|
| 987 |
+
conservation_file = os.path.join(output_dir, "conservation.txt")
|
| 988 |
+
|
| 989 |
+
# Simple conservation analysis
|
| 990 |
+
alignment_length = alignment.get_alignment_length()
|
| 991 |
+
conserved_positions = []
|
| 992 |
+
|
| 993 |
+
with open(conservation_file, "w") as f:
|
| 994 |
+
f.write("Position\tConservation_Score\tConsensus\n")
|
| 995 |
+
|
| 996 |
+
for i in range(alignment_length):
|
| 997 |
+
# Get all amino acids at this position
|
| 998 |
+
column = alignment[:, i]
|
| 999 |
+
set(column)
|
| 1000 |
+
|
| 1001 |
+
# Calculate a simple conservation score (percentage of most common AA)
|
| 1002 |
+
most_common_aa = max(column, key=column.count)
|
| 1003 |
+
conservation_score = column.count(most_common_aa) / len(column)
|
| 1004 |
+
|
| 1005 |
+
f.write(f"{i + 1}\t{conservation_score:.2f}\t{most_common_aa}\n")
|
| 1006 |
+
|
| 1007 |
+
# Consider positions with >80% conservation as conserved
|
| 1008 |
+
if conservation_score > 0.8:
|
| 1009 |
+
conserved_positions.append(i + 1)
|
| 1010 |
+
|
| 1011 |
+
log.append(f"Conservation analysis completed and saved to {conservation_file}")
|
| 1012 |
+
log.append(f"Identified {len(conserved_positions)} highly conserved positions (>80% conservation)")
|
| 1013 |
+
|
| 1014 |
+
if conserved_positions:
|
| 1015 |
+
log.append(f"Conserved positions: {', '.join(map(str, conserved_positions[:10]))}")
|
| 1016 |
+
if len(conserved_positions) > 10:
|
| 1017 |
+
log.append(f"... and {len(conserved_positions) - 10} more")
|
| 1018 |
+
|
| 1019 |
+
# Final summary
|
| 1020 |
+
log.append("\n## Summary")
|
| 1021 |
+
log.append("The analysis successfully completed with the following outputs:")
|
| 1022 |
+
log.append(f"1. Multiple sequence alignment: {aligned_file}")
|
| 1023 |
+
log.append(f"2. Phylogenetic tree: {tree_file}")
|
| 1024 |
+
log.append(f"3. Conservation analysis: {conservation_file}")
|
| 1025 |
+
|
| 1026 |
+
return "\n".join(log)
|
Biomni/biomni/tool/bioimaging.py
ADDED
|
@@ -0,0 +1,1394 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import os
|
| 3 |
+
import zipfile
|
| 4 |
+
|
| 5 |
+
import matplotlib
|
| 6 |
+
import requests
|
| 7 |
+
|
| 8 |
+
matplotlib.use("Agg") # Use non-interactive backend
|
| 9 |
+
import nibabel as nib
|
| 10 |
+
import numpy as np
|
| 11 |
+
import SimpleITK as sitk
|
| 12 |
+
import torch
|
| 13 |
+
import torch.serialization
|
| 14 |
+
from nnunet.inference.predict import predict_from_folder
|
| 15 |
+
|
| 16 |
+
# Apply safe globals for torch serialization
|
| 17 |
+
torch.serialization.add_safe_globals([tuple, list, dict, set, int, float, str, bytes, bytearray])
|
| 18 |
+
torch.serialization.add_safe_globals([complex, slice, range])
|
| 19 |
+
torch.serialization.add_safe_globals([np.core.multiarray.scalar])
|
| 20 |
+
|
| 21 |
+
# Configure logging
|
| 22 |
+
logger = logging.getLogger(__name__)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
# ============================================================================
|
| 26 |
+
# SEGMENTATION CLASS
|
| 27 |
+
# ============================================================================
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class SegmentationTool:
|
| 31 |
+
"""
|
| 32 |
+
A comprehensive tool for medical image segmentation using nnUNet.
|
| 33 |
+
Handles BRATS dataset processing, modality splitting, and segmentation visualization.
|
| 34 |
+
"""
|
| 35 |
+
|
| 36 |
+
def __init__(self):
|
| 37 |
+
"""Initialize the SegmentationTool."""
|
| 38 |
+
self.supported_formats = [".nii", ".nii.gz"]
|
| 39 |
+
logger.info("SegmentationTool initialized")
|
| 40 |
+
|
| 41 |
+
def split_modalities(self, input_file, output_dir, case_name="BRAT"):
|
| 42 |
+
"""
|
| 43 |
+
Split a 4D NIfTI file into separate modality files for nnUNet
|
| 44 |
+
Args:
|
| 45 |
+
input_file: Path to the 4D NIfTI file
|
| 46 |
+
output_dir: Directory to save the split files
|
| 47 |
+
case_name: Base name for the case (default: BRAT)
|
| 48 |
+
Returns:
|
| 49 |
+
output_dir: Path to directory containing split modality files
|
| 50 |
+
"""
|
| 51 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 52 |
+
|
| 53 |
+
# Load the 4D image
|
| 54 |
+
print(f"Loading {input_file}...")
|
| 55 |
+
img = nib.load(input_file)
|
| 56 |
+
data = img.get_fdata()
|
| 57 |
+
|
| 58 |
+
print(f"Image shape: {data.shape}")
|
| 59 |
+
print("Expected shape: (X, Y, Z, 4) for 4 modalities")
|
| 60 |
+
|
| 61 |
+
if len(data.shape) != 4:
|
| 62 |
+
raise ValueError(f"Expected 4D image, got {len(data.shape)}D")
|
| 63 |
+
|
| 64 |
+
if data.shape[3] != 4:
|
| 65 |
+
raise ValueError(f"Expected 4 modalities, got {data.shape[3]}")
|
| 66 |
+
|
| 67 |
+
# Split into separate files
|
| 68 |
+
modalities = ["FLAIR", "T1w", "t1gd", "T2w"]
|
| 69 |
+
|
| 70 |
+
for i, modality in enumerate(modalities):
|
| 71 |
+
# Extract the modality data
|
| 72 |
+
modality_data = data[:, :, :, i]
|
| 73 |
+
|
| 74 |
+
# Create a new NIfTI image with the same header but 3D data
|
| 75 |
+
modality_img = nib.Nifti1Image(modality_data, img.affine, img.header)
|
| 76 |
+
|
| 77 |
+
# Save with the expected naming convention
|
| 78 |
+
output_file = os.path.join(output_dir, f"{case_name}_{i:04d}.nii.gz")
|
| 79 |
+
nib.save(modality_img, output_file)
|
| 80 |
+
|
| 81 |
+
print(f"Saved {modality} modality to {output_file}")
|
| 82 |
+
print(f" Shape: {modality_data.shape}, Data type: {modality_data.dtype}")
|
| 83 |
+
|
| 84 |
+
print(f"\nAll modalities saved to {output_dir}")
|
| 85 |
+
return output_dir
|
| 86 |
+
|
| 87 |
+
def prepare_input_for_nnunet(self, input_path, output_dir, case_name="BRAT"):
|
| 88 |
+
"""
|
| 89 |
+
Prepare input data for nnUNet by handling both 4D and pre-split modality files
|
| 90 |
+
Args:
|
| 91 |
+
input_path: Path to input file or directory
|
| 92 |
+
output_dir: Directory to save prepared files
|
| 93 |
+
case_name: Base name for the case (default: BRAT)
|
| 94 |
+
Returns:
|
| 95 |
+
prepared_dir: Path to directory with nnUNet-ready files
|
| 96 |
+
"""
|
| 97 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 98 |
+
|
| 99 |
+
if os.path.isfile(input_path):
|
| 100 |
+
# Single file - check if it's 4D
|
| 101 |
+
if input_path.endswith((".nii", ".nii.gz")):
|
| 102 |
+
try:
|
| 103 |
+
img = nib.load(input_path)
|
| 104 |
+
if len(img.shape) == 4 and img.shape[3] == 4:
|
| 105 |
+
print("4D NIfTI file detected, splitting modalities...")
|
| 106 |
+
return self.split_modalities(input_path, output_dir, case_name)
|
| 107 |
+
else:
|
| 108 |
+
print("Single 3D file detected, copying to output directory...")
|
| 109 |
+
# Copy single file with proper naming
|
| 110 |
+
output_file = os.path.join(output_dir, f"{case_name}_0000.nii.gz")
|
| 111 |
+
import shutil
|
| 112 |
+
|
| 113 |
+
shutil.copy2(input_path, output_file)
|
| 114 |
+
return output_dir
|
| 115 |
+
except Exception as e:
|
| 116 |
+
print(f"Error reading file {input_path}: {e}")
|
| 117 |
+
raise
|
| 118 |
+
elif os.path.isdir(input_path):
|
| 119 |
+
# Directory - check if it already has split modalities
|
| 120 |
+
files = [f for f in os.listdir(input_path) if f.endswith((".nii", ".nii.gz"))]
|
| 121 |
+
|
| 122 |
+
if any(f.endswith("_0000.nii.gz") for f in files):
|
| 123 |
+
print("Directory already contains split modality files, using as-is...")
|
| 124 |
+
# Copy existing files to output directory
|
| 125 |
+
for f in files:
|
| 126 |
+
if f.endswith((".nii", ".nii.gz")):
|
| 127 |
+
import shutil
|
| 128 |
+
|
| 129 |
+
shutil.copy2(os.path.join(input_path, f), os.path.join(output_dir, f))
|
| 130 |
+
return output_dir
|
| 131 |
+
else:
|
| 132 |
+
# Check if there's a 4D file to split
|
| 133 |
+
for f in files:
|
| 134 |
+
if f.endswith((".nii", ".nii.gz")):
|
| 135 |
+
try:
|
| 136 |
+
img = nib.load(os.path.join(input_path, f))
|
| 137 |
+
if len(img.shape) == 4 and img.shape[3] == 4:
|
| 138 |
+
print(f"4D NIfTI file {f} detected, splitting modalities...")
|
| 139 |
+
return self.split_modalities(os.path.join(input_path, f), output_dir, case_name)
|
| 140 |
+
except Exception as e:
|
| 141 |
+
logging.debug("Skipping file %s during 4D check: %s", f, e)
|
| 142 |
+
continue
|
| 143 |
+
|
| 144 |
+
print("No 4D files found, copying existing files...")
|
| 145 |
+
# Copy existing files to output directory
|
| 146 |
+
for f in files:
|
| 147 |
+
if f.endswith((".nii", ".nii.gz")):
|
| 148 |
+
import shutil
|
| 149 |
+
|
| 150 |
+
shutil.copy2(os.path.join(input_path, f), os.path.join(output_dir, f))
|
| 151 |
+
return output_dir
|
| 152 |
+
|
| 153 |
+
raise ValueError(f"Input path {input_path} is neither a valid file nor directory")
|
| 154 |
+
|
| 155 |
+
def setup_nnunet_environment(self, results_folder=None, raw_data_base=None, preprocessed=None):
|
| 156 |
+
"""
|
| 157 |
+
Setup nnU-Net environment variables according to official documentation
|
| 158 |
+
Args:
|
| 159 |
+
results_folder: Path to nnUNet results folder (default: ~/nnUNet_results)
|
| 160 |
+
raw_data_base: Path to raw data base (default: ~/nnUNet_raw_data_base)
|
| 161 |
+
preprocessed: Path to preprocessed data (default: ~/nnUNet_preprocessed)
|
| 162 |
+
"""
|
| 163 |
+
# Set nnUNet environment variables as per official documentation
|
| 164 |
+
if results_folder:
|
| 165 |
+
os.environ["nnUNet_RESULTS_FOLDER"] = os.path.expanduser(results_folder)
|
| 166 |
+
elif "nnUNet_RESULTS_FOLDER" not in os.environ:
|
| 167 |
+
os.environ["nnUNet_RESULTS_FOLDER"] = os.path.expanduser("~/nnUNet_results")
|
| 168 |
+
|
| 169 |
+
if raw_data_base:
|
| 170 |
+
os.environ["nnUNet_raw_data_base"] = os.path.expanduser(raw_data_base)
|
| 171 |
+
elif "nnUNet_raw_data_base" not in os.environ:
|
| 172 |
+
os.environ["nnUNet_raw_data_base"] = os.path.expanduser("~/nnUNet_raw_data_base")
|
| 173 |
+
|
| 174 |
+
if preprocessed:
|
| 175 |
+
os.environ["nnUNet_preprocessed"] = os.path.expanduser(preprocessed)
|
| 176 |
+
elif "nnUNet_preprocessed" not in os.environ:
|
| 177 |
+
os.environ["nnUNet_preprocessed"] = os.path.expanduser("~/nnUNet_preprocessed")
|
| 178 |
+
|
| 179 |
+
# Create directories if they don't exist
|
| 180 |
+
for path in [
|
| 181 |
+
os.environ["nnUNet_RESULTS_FOLDER"],
|
| 182 |
+
os.environ["nnUNet_raw_data_base"],
|
| 183 |
+
os.environ["nnUNet_preprocessed"],
|
| 184 |
+
]:
|
| 185 |
+
os.makedirs(path, exist_ok=True)
|
| 186 |
+
|
| 187 |
+
print("nnU-Net environment variables set:")
|
| 188 |
+
print(f" nnUNet_RESULTS_FOLDER: {os.environ['nnUNet_RESULTS_FOLDER']}")
|
| 189 |
+
print(f" nnUNet_raw_data_base: {os.environ['nnUNet_raw_data_base']}")
|
| 190 |
+
print(f" nnUNet_preprocessed: {os.environ['nnUNet_preprocessed']}")
|
| 191 |
+
|
| 192 |
+
def _download_model_with_browser_headers(self, url, output_path):
|
| 193 |
+
"""
|
| 194 |
+
Download model with browser-like headers to bypass Zenodo's anti-bot protection
|
| 195 |
+
"""
|
| 196 |
+
headers = {
|
| 197 |
+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
|
| 198 |
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
|
| 199 |
+
"Accept-Language": "en-US,en;q=0.5",
|
| 200 |
+
"Accept-Encoding": "gzip, deflate",
|
| 201 |
+
"Connection": "keep-alive",
|
| 202 |
+
"Upgrade-Insecure-Requests": "1",
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
logger.info(f"Downloading model from: {url}")
|
| 206 |
+
|
| 207 |
+
try:
|
| 208 |
+
response = requests.get(url, headers=headers, stream=True, timeout=300)
|
| 209 |
+
response.raise_for_status()
|
| 210 |
+
|
| 211 |
+
with open(output_path, "wb") as f:
|
| 212 |
+
for chunk in response.iter_content(chunk_size=8192):
|
| 213 |
+
if chunk:
|
| 214 |
+
f.write(chunk)
|
| 215 |
+
|
| 216 |
+
logger.info(f"Download completed: {output_path}")
|
| 217 |
+
return True
|
| 218 |
+
|
| 219 |
+
except Exception as e:
|
| 220 |
+
logger.error(f"Download failed: {e}")
|
| 221 |
+
if os.path.exists(output_path):
|
| 222 |
+
os.remove(output_path)
|
| 223 |
+
return False
|
| 224 |
+
|
| 225 |
+
def _download_and_extract_model(self, task_id, model_type="3d_fullres"):
|
| 226 |
+
"""
|
| 227 |
+
Download and extract nnUNet model with browser-like headers - extracts directly to nnUNet directory
|
| 228 |
+
"""
|
| 229 |
+
results_folder = os.environ.get("nnUNet_RESULTS_FOLDER", "~/nnUNet_results")
|
| 230 |
+
results_folder = os.path.expanduser(results_folder)
|
| 231 |
+
|
| 232 |
+
# Create the nnUNet directory
|
| 233 |
+
nnunet_dir = os.path.join(results_folder, "nnUNet")
|
| 234 |
+
os.makedirs(nnunet_dir, exist_ok=True)
|
| 235 |
+
|
| 236 |
+
# Check if model already exists
|
| 237 |
+
task_dir = os.path.join(nnunet_dir, model_type, task_id)
|
| 238 |
+
if os.path.exists(task_dir):
|
| 239 |
+
# Check if it has the expected structure
|
| 240 |
+
plans_file = os.path.join(task_dir, "nnUNetTrainerV2__nnUNetPlansv2.1")
|
| 241 |
+
if os.path.exists(plans_file):
|
| 242 |
+
logger.info(f"Model already exists for task '{task_id}' with {model_type}")
|
| 243 |
+
return True
|
| 244 |
+
|
| 245 |
+
# Download URL for the task
|
| 246 |
+
download_url = f"https://zenodo.org/record/4003545/files/{task_id}.zip?download=1"
|
| 247 |
+
|
| 248 |
+
# Create temporary file for download
|
| 249 |
+
temp_zip = os.path.join(nnunet_dir, f"{task_id}_temp.zip")
|
| 250 |
+
|
| 251 |
+
# Download with browser headers
|
| 252 |
+
if self._download_model_with_browser_headers(download_url, temp_zip):
|
| 253 |
+
try:
|
| 254 |
+
logger.info(f"Extracting {temp_zip} to {nnunet_dir}")
|
| 255 |
+
with zipfile.ZipFile(temp_zip, "r") as zip_ref:
|
| 256 |
+
zip_ref.extractall(nnunet_dir)
|
| 257 |
+
|
| 258 |
+
# Remove temporary zip file
|
| 259 |
+
os.remove(temp_zip)
|
| 260 |
+
|
| 261 |
+
# Verify extraction worked
|
| 262 |
+
if os.path.exists(task_dir):
|
| 263 |
+
logger.info(f"Model successfully downloaded and extracted for task '{task_id}'")
|
| 264 |
+
logger.info(f"Model directory: {task_dir}")
|
| 265 |
+
return True
|
| 266 |
+
else:
|
| 267 |
+
logger.error(f"Task directory not found after extraction: {task_dir}")
|
| 268 |
+
return False
|
| 269 |
+
|
| 270 |
+
except Exception as e:
|
| 271 |
+
logger.error(f"Extraction failed: {e}")
|
| 272 |
+
if os.path.exists(temp_zip):
|
| 273 |
+
os.remove(temp_zip)
|
| 274 |
+
return False
|
| 275 |
+
else:
|
| 276 |
+
return False
|
| 277 |
+
|
| 278 |
+
def segment_with_nn_unet(
|
| 279 |
+
self,
|
| 280 |
+
image_path,
|
| 281 |
+
output_dir,
|
| 282 |
+
task_id,
|
| 283 |
+
model_type="3d_fullres",
|
| 284 |
+
folds=None,
|
| 285 |
+
use_tta=False,
|
| 286 |
+
num_threads=1,
|
| 287 |
+
mixed_precision=True,
|
| 288 |
+
verbose=True,
|
| 289 |
+
auto_prepare_input=True,
|
| 290 |
+
results_folder=None,
|
| 291 |
+
auto_download=True,
|
| 292 |
+
):
|
| 293 |
+
"""
|
| 294 |
+
Segment images using nnUNet with proper environment setup and automatic model downloading
|
| 295 |
+
Args:
|
| 296 |
+
image_path: Path to input image file or directory
|
| 297 |
+
output_dir: Directory to save segmentation results
|
| 298 |
+
task_id: Task identifier (e.g., 'Task001_BrainTumour')
|
| 299 |
+
model_type: Model type (default: '3d_fullres')
|
| 300 |
+
folds: Model folds to use (default: [0, 1, 2, 3, 4])
|
| 301 |
+
use_tta: Use test time augmentation (default: False)
|
| 302 |
+
num_threads: Number of threads for preprocessing (default: 1)
|
| 303 |
+
mixed_precision: Use mixed precision (default: True)
|
| 304 |
+
verbose: Verbose logging (default: True)
|
| 305 |
+
auto_prepare_input: Automatically prepare input for nnUNet (default: True)
|
| 306 |
+
results_folder: Path to nnUNet results folder (default: None, will use environment variable or default)
|
| 307 |
+
auto_download: Automatically download missing models (default: True)
|
| 308 |
+
"""
|
| 309 |
+
if folds is None:
|
| 310 |
+
folds = [0, 1, 2, 3, 4]
|
| 311 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 312 |
+
logging.basicConfig(level=logging.INFO if verbose else logging.WARNING)
|
| 313 |
+
|
| 314 |
+
# Setup nnUNet environment first
|
| 315 |
+
self.setup_nnunet_environment(results_folder=results_folder)
|
| 316 |
+
|
| 317 |
+
# Prepare input data if requested
|
| 318 |
+
if auto_prepare_input:
|
| 319 |
+
temp_input_dir = os.path.join(output_dir, "temp_input")
|
| 320 |
+
prepared_input_dir = self.prepare_input_for_nnunet(image_path, temp_input_dir)
|
| 321 |
+
image_path = prepared_input_dir
|
| 322 |
+
logging.info(f"Input prepared for nnUNet: {image_path}")
|
| 323 |
+
|
| 324 |
+
logging.info("Verifying NIfTI input files...")
|
| 325 |
+
|
| 326 |
+
def verify_nifti_input(image_path):
|
| 327 |
+
if os.path.isfile(image_path):
|
| 328 |
+
nib.load(image_path)
|
| 329 |
+
else:
|
| 330 |
+
for file in os.listdir(image_path):
|
| 331 |
+
if file.endswith(".nii") or file.endswith(".nii.gz"):
|
| 332 |
+
nib.load(os.path.join(image_path, file))
|
| 333 |
+
|
| 334 |
+
verify_nifti_input(image_path)
|
| 335 |
+
|
| 336 |
+
# Get or set up the results folder
|
| 337 |
+
results_folder = os.environ.get("nnUNet_RESULTS_FOLDER")
|
| 338 |
+
if not results_folder:
|
| 339 |
+
# Try common locations
|
| 340 |
+
common_paths = [
|
| 341 |
+
"./models/nnUNet",
|
| 342 |
+
"~/nnUNet_results",
|
| 343 |
+
"~/biomni_models/nnUNet",
|
| 344 |
+
]
|
| 345 |
+
|
| 346 |
+
for base_path in common_paths:
|
| 347 |
+
expanded_path = os.path.expanduser(base_path)
|
| 348 |
+
if os.path.exists(expanded_path):
|
| 349 |
+
results_folder = expanded_path
|
| 350 |
+
break
|
| 351 |
+
|
| 352 |
+
# If still no folder found, create one
|
| 353 |
+
if not results_folder:
|
| 354 |
+
results_folder = os.path.expanduser("~/nnUNet_results")
|
| 355 |
+
os.makedirs(results_folder, exist_ok=True)
|
| 356 |
+
logging.info(f"Created new results folder: {results_folder}")
|
| 357 |
+
|
| 358 |
+
os.environ["RESULTS_FOLDER"] = results_folder
|
| 359 |
+
os.environ["nnUNet_RESULTS_FOLDER"] = results_folder
|
| 360 |
+
logging.info(f"Set RESULTS_FOLDER environment variable to: {results_folder}")
|
| 361 |
+
|
| 362 |
+
# Construct the expected model path - using the correct structure
|
| 363 |
+
model_folder = os.path.join(results_folder, "nnUNet", model_type, task_id, "nnUNetTrainerV2__nnUNetPlansv2.1")
|
| 364 |
+
|
| 365 |
+
logging.info(f"Looking for model at: {model_folder}")
|
| 366 |
+
|
| 367 |
+
# Check if model files actually exist (not just the directory)
|
| 368 |
+
def check_model_files_exist(model_folder, folds):
|
| 369 |
+
"""Check if actual model weight files exist for the specified folds"""
|
| 370 |
+
if not os.path.exists(model_folder):
|
| 371 |
+
return False
|
| 372 |
+
|
| 373 |
+
# Check for plans.pkl and other required files
|
| 374 |
+
required_files = ["plans.pkl"]
|
| 375 |
+
for req_file in required_files:
|
| 376 |
+
if not os.path.exists(os.path.join(model_folder, req_file)):
|
| 377 |
+
return False
|
| 378 |
+
|
| 379 |
+
# Check if at least one fold has model files
|
| 380 |
+
for fold in folds:
|
| 381 |
+
fold_dir = os.path.join(model_folder, f"fold_{fold}")
|
| 382 |
+
if os.path.exists(fold_dir):
|
| 383 |
+
# Look for model files (.model, .model.pkl, etc.)
|
| 384 |
+
model_files = [f for f in os.listdir(fold_dir) if f.endswith((".model", ".model.pkl"))]
|
| 385 |
+
if model_files:
|
| 386 |
+
return True
|
| 387 |
+
return False
|
| 388 |
+
|
| 389 |
+
# Check if model exists, download if not
|
| 390 |
+
if not check_model_files_exist(model_folder, folds):
|
| 391 |
+
if auto_download:
|
| 392 |
+
logging.info(f"Model weights for {task_id} not found. Downloading...")
|
| 393 |
+
# Ensure the results folder structure exists
|
| 394 |
+
os.makedirs(os.path.dirname(model_folder), exist_ok=True)
|
| 395 |
+
|
| 396 |
+
# Download the model using our custom download function
|
| 397 |
+
if self._download_and_extract_model(task_id, model_type):
|
| 398 |
+
logging.info(f"Downloaded pretrained model for {task_id} successfully.")
|
| 399 |
+
else:
|
| 400 |
+
raise RuntimeError(f"Failed to download model for {task_id}")
|
| 401 |
+
|
| 402 |
+
# Verify the download worked
|
| 403 |
+
if not check_model_files_exist(model_folder, folds):
|
| 404 |
+
raise RuntimeError(
|
| 405 |
+
f"Model download completed but files not found at expected location: {model_folder}"
|
| 406 |
+
)
|
| 407 |
+
else:
|
| 408 |
+
# Ask user for permission to download
|
| 409 |
+
user_input = (
|
| 410 |
+
input(f"Model weights for {task_id} not found. Do you want to download them? (y/n): ")
|
| 411 |
+
.strip()
|
| 412 |
+
.lower()
|
| 413 |
+
)
|
| 414 |
+
if user_input == "y":
|
| 415 |
+
# Download the model using our custom download function
|
| 416 |
+
if self._download_and_extract_model(task_id, model_type):
|
| 417 |
+
logging.info(f"Downloaded pretrained model for {task_id} successfully.")
|
| 418 |
+
else:
|
| 419 |
+
raise RuntimeError(f"Failed to download model for {task_id}")
|
| 420 |
+
else:
|
| 421 |
+
raise RuntimeError("Model weights not found and download declined by user.")
|
| 422 |
+
|
| 423 |
+
# Double-check that we now have the model
|
| 424 |
+
if not check_model_files_exist(model_folder, folds):
|
| 425 |
+
raise RuntimeError(f"Model files still not found at {model_folder} after download attempt")
|
| 426 |
+
|
| 427 |
+
logging.info(f"Using model: {model_folder}")
|
| 428 |
+
|
| 429 |
+
# Patch torch.load for compatibility
|
| 430 |
+
original_torch_load = torch.load
|
| 431 |
+
|
| 432 |
+
def patched_torch_load(*args, **kwargs):
|
| 433 |
+
kwargs["weights_only"] = False
|
| 434 |
+
return original_torch_load(*args, **kwargs)
|
| 435 |
+
|
| 436 |
+
# Run the segmentation
|
| 437 |
+
torch.load = patched_torch_load
|
| 438 |
+
try:
|
| 439 |
+
predict_from_folder(
|
| 440 |
+
model=model_folder,
|
| 441 |
+
input_folder=image_path,
|
| 442 |
+
output_folder=output_dir,
|
| 443 |
+
folds=folds,
|
| 444 |
+
save_npz=False,
|
| 445 |
+
num_threads_preprocessing=num_threads,
|
| 446 |
+
num_threads_nifti_save=num_threads,
|
| 447 |
+
mixed_precision=mixed_precision,
|
| 448 |
+
lowres_segmentations=None,
|
| 449 |
+
part_id=0,
|
| 450 |
+
num_parts=1,
|
| 451 |
+
tta=use_tta,
|
| 452 |
+
)
|
| 453 |
+
finally:
|
| 454 |
+
# Restore original torch.load behavior
|
| 455 |
+
torch.load = original_torch_load
|
| 456 |
+
|
| 457 |
+
# Clean up temporary input directory if it was created
|
| 458 |
+
if auto_prepare_input and os.path.exists(temp_input_dir):
|
| 459 |
+
import shutil
|
| 460 |
+
|
| 461 |
+
shutil.rmtree(temp_input_dir)
|
| 462 |
+
logging.info("Cleaned up temporary input directory")
|
| 463 |
+
|
| 464 |
+
logging.info(f"Segmentation outputs stored at: {output_dir}")
|
| 465 |
+
return output_dir
|
| 466 |
+
|
| 467 |
+
def create_segmentation_visualization(self, original_mri, segmentation, output_dir="./visualization_output"):
|
| 468 |
+
"""
|
| 469 |
+
Create and save visualization of segmentation results using nilearn
|
| 470 |
+
Args:
|
| 471 |
+
original_mri: Path to original MRI file
|
| 472 |
+
segmentation: Path to segmentation file
|
| 473 |
+
output_dir: Directory to save visualization images
|
| 474 |
+
Returns:
|
| 475 |
+
list: List of saved image file paths
|
| 476 |
+
"""
|
| 477 |
+
try:
|
| 478 |
+
# Import nilearn here to avoid dependency issues
|
| 479 |
+
from nilearn import plotting
|
| 480 |
+
|
| 481 |
+
# Create output directory
|
| 482 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 483 |
+
|
| 484 |
+
# Check if files exist
|
| 485 |
+
if not os.path.exists(original_mri):
|
| 486 |
+
raise FileNotFoundError(f"Original MRI file not found: {original_mri}")
|
| 487 |
+
|
| 488 |
+
if not os.path.exists(segmentation):
|
| 489 |
+
raise FileNotFoundError(f"Segmentation file not found: {segmentation}")
|
| 490 |
+
|
| 491 |
+
print("✅ Files found, creating visualizations...")
|
| 492 |
+
saved_files = []
|
| 493 |
+
|
| 494 |
+
# Create and save the main overlay plot
|
| 495 |
+
display = plotting.plot_roi(
|
| 496 |
+
segmentation,
|
| 497 |
+
bg_img=original_mri,
|
| 498 |
+
cmap="Set1",
|
| 499 |
+
alpha=0.6,
|
| 500 |
+
title="Segmentation Overlay",
|
| 501 |
+
)
|
| 502 |
+
|
| 503 |
+
# Save the main overlay plot
|
| 504 |
+
output_file = os.path.join(output_dir, "segmentation_overlay.png")
|
| 505 |
+
display.savefig(output_file, dpi=150, bbox_inches="tight")
|
| 506 |
+
saved_files.append(output_file)
|
| 507 |
+
print(f"✅ Main overlay saved to: {output_file}")
|
| 508 |
+
display.close()
|
| 509 |
+
|
| 510 |
+
# Create additional views and save them
|
| 511 |
+
# Axial view
|
| 512 |
+
display_axial = plotting.plot_roi(
|
| 513 |
+
segmentation,
|
| 514 |
+
bg_img=original_mri,
|
| 515 |
+
cmap="Set1",
|
| 516 |
+
alpha=0.6,
|
| 517 |
+
title="Segmentation Overlay - Axial View",
|
| 518 |
+
display_mode="z",
|
| 519 |
+
)
|
| 520 |
+
axial_file = os.path.join(output_dir, "segmentation_axial.png")
|
| 521 |
+
display_axial.savefig(axial_file, dpi=150, bbox_inches="tight")
|
| 522 |
+
saved_files.append(axial_file)
|
| 523 |
+
print(f"✅ Axial view saved to: {axial_file}")
|
| 524 |
+
display_axial.close()
|
| 525 |
+
|
| 526 |
+
# Sagittal view
|
| 527 |
+
display_sagittal = plotting.plot_roi(
|
| 528 |
+
segmentation,
|
| 529 |
+
bg_img=original_mri,
|
| 530 |
+
cmap="Set1",
|
| 531 |
+
alpha=0.6,
|
| 532 |
+
title="Segmentation Overlay - Sagittal View",
|
| 533 |
+
display_mode="x",
|
| 534 |
+
)
|
| 535 |
+
sagittal_file = os.path.join(output_dir, "segmentation_sagittal.png")
|
| 536 |
+
display_sagittal.savefig(sagittal_file, dpi=150, bbox_inches="tight")
|
| 537 |
+
saved_files.append(sagittal_file)
|
| 538 |
+
print(f"✅ Sagittal view saved to: {sagittal_file}")
|
| 539 |
+
display_sagittal.close()
|
| 540 |
+
|
| 541 |
+
# Coronal view
|
| 542 |
+
display_coronal = plotting.plot_roi(
|
| 543 |
+
segmentation,
|
| 544 |
+
bg_img=original_mri,
|
| 545 |
+
cmap="Set1",
|
| 546 |
+
alpha=0.6,
|
| 547 |
+
title="Segmentation Overlay - Coronal View",
|
| 548 |
+
display_mode="y",
|
| 549 |
+
)
|
| 550 |
+
coronal_file = os.path.join(output_dir, "segmentation_coronal.png")
|
| 551 |
+
display_coronal.savefig(coronal_file, dpi=150, bbox_inches="tight")
|
| 552 |
+
saved_files.append(coronal_file)
|
| 553 |
+
print(f"✅ Coronal view saved to: {coronal_file}")
|
| 554 |
+
display_coronal.close()
|
| 555 |
+
|
| 556 |
+
print(f"\n✅ All visualizations saved to: {output_dir}")
|
| 557 |
+
print("Files created:")
|
| 558 |
+
for file_path in saved_files:
|
| 559 |
+
print(f" - {os.path.basename(file_path)}")
|
| 560 |
+
|
| 561 |
+
return saved_files
|
| 562 |
+
|
| 563 |
+
except ImportError:
|
| 564 |
+
print(" nilearn not available. Install with: pip install nilearn")
|
| 565 |
+
return []
|
| 566 |
+
except Exception as e:
|
| 567 |
+
print(f" Visualization failed: {e}")
|
| 568 |
+
import traceback
|
| 569 |
+
|
| 570 |
+
traceback.print_exc()
|
| 571 |
+
return []
|
| 572 |
+
|
| 573 |
+
|
| 574 |
+
# ============================================================================
|
| 575 |
+
# IMAGE REGISTRATION CLASS
|
| 576 |
+
# ============================================================================
|
| 577 |
+
|
| 578 |
+
|
| 579 |
+
class ImageRegistrationTool:
|
| 580 |
+
"""
|
| 581 |
+
A comprehensive tool for medical image registration using SimpleITK.
|
| 582 |
+
Supports rigid, affine, and deformable registration with preprocessing and visualization.
|
| 583 |
+
"""
|
| 584 |
+
|
| 585 |
+
def __init__(self):
|
| 586 |
+
"""Initialize the ImageRegistrationTool."""
|
| 587 |
+
self.supported_formats = [".nii", ".nii.gz", ".nrrd", ".mha", ".mhd"]
|
| 588 |
+
logger.info("ImageRegistrationTool initialized")
|
| 589 |
+
|
| 590 |
+
def load_image(self, image_path: str) -> sitk.Image:
|
| 591 |
+
"""
|
| 592 |
+
Load a medical image using SimpleITK.
|
| 593 |
+
|
| 594 |
+
Args:
|
| 595 |
+
image_path: Path to the image file
|
| 596 |
+
|
| 597 |
+
Returns:
|
| 598 |
+
SimpleITK Image object
|
| 599 |
+
"""
|
| 600 |
+
if not os.path.exists(image_path):
|
| 601 |
+
raise FileNotFoundError(f"Image file not found: {image_path}")
|
| 602 |
+
|
| 603 |
+
logger.info(f"Loading image: {image_path}")
|
| 604 |
+
try:
|
| 605 |
+
image = sitk.ReadImage(image_path)
|
| 606 |
+
logger.info(f"Successfully loaded image with size: {image.GetSize()}")
|
| 607 |
+
return image
|
| 608 |
+
except Exception as e:
|
| 609 |
+
logger.error(f"Failed to load image {image_path}: {e}")
|
| 610 |
+
raise
|
| 611 |
+
|
| 612 |
+
def save_image(self, image: sitk.Image, output_path: str) -> None:
|
| 613 |
+
"""
|
| 614 |
+
Save a SimpleITK image to file.
|
| 615 |
+
|
| 616 |
+
Args:
|
| 617 |
+
image: SimpleITK Image object
|
| 618 |
+
output_path: Path to save the image (must include filename and extension)
|
| 619 |
+
"""
|
| 620 |
+
# Validate output path
|
| 621 |
+
if os.path.isdir(output_path):
|
| 622 |
+
raise ValueError(
|
| 623 |
+
f"Output path '{output_path}' is a directory. Please provide a file path with extension (e.g., '.nii.gz', '.png', '.jpg')"
|
| 624 |
+
)
|
| 625 |
+
|
| 626 |
+
if not os.path.splitext(output_path)[1]:
|
| 627 |
+
raise ValueError(
|
| 628 |
+
f"Output path '{output_path}' has no file extension. Please add an extension (e.g., '.nii.gz', '.png', '.jpg')"
|
| 629 |
+
)
|
| 630 |
+
|
| 631 |
+
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
| 632 |
+
logger.info(f"Saving image to: {output_path}")
|
| 633 |
+
try:
|
| 634 |
+
sitk.WriteImage(image, output_path)
|
| 635 |
+
logger.info("Image saved successfully")
|
| 636 |
+
except Exception as e:
|
| 637 |
+
logger.error(f"Failed to save image to {output_path}: {e}")
|
| 638 |
+
raise
|
| 639 |
+
|
| 640 |
+
def preprocess_image(self, image: sitk.Image, denoise: bool = True, normalize: bool = True) -> sitk.Image:
|
| 641 |
+
"""
|
| 642 |
+
Preprocess an image with denoising and normalization.
|
| 643 |
+
|
| 644 |
+
Args:
|
| 645 |
+
image: Input SimpleITK image
|
| 646 |
+
denoise: Whether to apply denoising
|
| 647 |
+
normalize: Whether to apply normalization
|
| 648 |
+
|
| 649 |
+
Returns:
|
| 650 |
+
Preprocessed SimpleITK image
|
| 651 |
+
"""
|
| 652 |
+
logger.info("Preprocessing image...")
|
| 653 |
+
processed_image = sitk.Image(image)
|
| 654 |
+
|
| 655 |
+
if denoise:
|
| 656 |
+
# Apply Gaussian smoothing for denoising
|
| 657 |
+
processed_image = sitk.SmoothingRecursiveGaussian(processed_image, sigma=1.0)
|
| 658 |
+
logger.info("Applied denoising")
|
| 659 |
+
|
| 660 |
+
if normalize:
|
| 661 |
+
# Normalize to [0, 1] range
|
| 662 |
+
min_max_filter = sitk.MinimumMaximumImageFilter()
|
| 663 |
+
min_max_filter.Execute(processed_image)
|
| 664 |
+
min_val = min_max_filter.GetMinimum()
|
| 665 |
+
max_val = min_max_filter.GetMaximum()
|
| 666 |
+
|
| 667 |
+
if max_val > min_val:
|
| 668 |
+
processed_image = sitk.IntensityWindowing(
|
| 669 |
+
processed_image, windowMinimum=min_val, windowMaximum=max_val, outputMinimum=0.0, outputMaximum=1.0
|
| 670 |
+
)
|
| 671 |
+
logger.info("Applied normalization")
|
| 672 |
+
|
| 673 |
+
return processed_image
|
| 674 |
+
|
| 675 |
+
def create_rigid_transform(
|
| 676 |
+
self, fixed_image: sitk.Image, moving_image: sitk.Image, initial_transform: sitk.Transform | None = None
|
| 677 |
+
) -> sitk.Transform:
|
| 678 |
+
"""
|
| 679 |
+
Create a rigid transform for image registration.
|
| 680 |
+
|
| 681 |
+
Args:
|
| 682 |
+
fixed_image: Reference (fixed) image
|
| 683 |
+
moving_image: Image to be registered
|
| 684 |
+
initial_transform: Optional initial transform
|
| 685 |
+
|
| 686 |
+
Returns:
|
| 687 |
+
Rigid transform object
|
| 688 |
+
"""
|
| 689 |
+
logger.info("Creating rigid transform...")
|
| 690 |
+
|
| 691 |
+
if initial_transform is None:
|
| 692 |
+
# Create identity transform
|
| 693 |
+
transform = sitk.Euler3DTransform()
|
| 694 |
+
else:
|
| 695 |
+
transform = initial_transform
|
| 696 |
+
|
| 697 |
+
return transform
|
| 698 |
+
|
| 699 |
+
def create_affine_transform(
|
| 700 |
+
self, fixed_image: sitk.Image, moving_image: sitk.Image, initial_transform: sitk.Transform | None = None
|
| 701 |
+
) -> sitk.Transform:
|
| 702 |
+
"""
|
| 703 |
+
Create an affine transform for image registration.
|
| 704 |
+
|
| 705 |
+
Args:
|
| 706 |
+
fixed_image: Reference (fixed) image
|
| 707 |
+
moving_image: Image to be registered
|
| 708 |
+
initial_transform: Optional initial transform
|
| 709 |
+
|
| 710 |
+
Returns:
|
| 711 |
+
Affine transform object
|
| 712 |
+
"""
|
| 713 |
+
logger.info("Creating affine transform...")
|
| 714 |
+
|
| 715 |
+
if initial_transform is None:
|
| 716 |
+
# Create identity transform
|
| 717 |
+
transform = sitk.AffineTransform(3)
|
| 718 |
+
else:
|
| 719 |
+
transform = initial_transform
|
| 720 |
+
|
| 721 |
+
return transform
|
| 722 |
+
|
| 723 |
+
def create_deformable_transform(
|
| 724 |
+
self, fixed_image: sitk.Image, moving_image: sitk.Image, number_of_control_points: int = 4
|
| 725 |
+
) -> sitk.Transform:
|
| 726 |
+
"""
|
| 727 |
+
Create a deformable (B-spline) transform for image registration.
|
| 728 |
+
|
| 729 |
+
Args:
|
| 730 |
+
fixed_image: Reference (fixed) image
|
| 731 |
+
moving_image: Image to be registered
|
| 732 |
+
number_of_control_points: Number of B-spline control points per dimension
|
| 733 |
+
|
| 734 |
+
Returns:
|
| 735 |
+
Deformable transform object
|
| 736 |
+
"""
|
| 737 |
+
logger.info("Creating deformable transform...")
|
| 738 |
+
|
| 739 |
+
# Create B-spline transform
|
| 740 |
+
transform = sitk.BSplineTransformInitializer(fixed_image, [number_of_control_points] * 3, order=3)
|
| 741 |
+
|
| 742 |
+
return transform
|
| 743 |
+
|
| 744 |
+
def setup_registration_method(
|
| 745 |
+
self,
|
| 746 |
+
transform: sitk.Transform,
|
| 747 |
+
metric: str = "mutual_information",
|
| 748 |
+
optimizer: str = "gradient_descent",
|
| 749 |
+
learning_rate: float = 0.01,
|
| 750 |
+
number_of_iterations: int = 100,
|
| 751 |
+
gradient_convergence_tolerance: float = 1e-6,
|
| 752 |
+
) -> sitk.ImageRegistrationMethod:
|
| 753 |
+
"""
|
| 754 |
+
Setup the registration method with specified parameters.
|
| 755 |
+
|
| 756 |
+
Args:
|
| 757 |
+
transform: Transform object
|
| 758 |
+
metric: Similarity metric name
|
| 759 |
+
optimizer: Optimizer name
|
| 760 |
+
learning_rate: Learning rate for gradient descent
|
| 761 |
+
number_of_iterations: Maximum number of iterations
|
| 762 |
+
gradient_convergence_tolerance: Convergence tolerance
|
| 763 |
+
|
| 764 |
+
Returns:
|
| 765 |
+
Configured registration method
|
| 766 |
+
"""
|
| 767 |
+
logger.info(f"Setting up registration method: {metric} metric, {optimizer} optimizer")
|
| 768 |
+
|
| 769 |
+
registration_method = sitk.ImageRegistrationMethod()
|
| 770 |
+
|
| 771 |
+
# Set similarity metric
|
| 772 |
+
if metric == "mutual_information":
|
| 773 |
+
registration_method.SetMetricAsMattesMutualInformation(numberOfHistogramBins=50)
|
| 774 |
+
elif metric == "mean_squares":
|
| 775 |
+
registration_method.SetMetricAsMeanSquares()
|
| 776 |
+
elif metric == "correlation":
|
| 777 |
+
registration_method.SetMetricAsCorrelation()
|
| 778 |
+
elif metric == "normalized_correlation":
|
| 779 |
+
registration_method.SetMetricAsNormalizedCorrelation()
|
| 780 |
+
else:
|
| 781 |
+
logger.warning(f"Unknown metric {metric}, using mutual information")
|
| 782 |
+
registration_method.SetMetricAsMattesMutualInformation(numberOfHistogramBins=50)
|
| 783 |
+
|
| 784 |
+
# Set optimizer
|
| 785 |
+
if optimizer == "gradient_descent":
|
| 786 |
+
registration_method.SetOptimizerAsGradientDescent(
|
| 787 |
+
learningRate=learning_rate,
|
| 788 |
+
numberOfIterations=number_of_iterations,
|
| 789 |
+
convergenceMinimumValue=gradient_convergence_tolerance,
|
| 790 |
+
)
|
| 791 |
+
elif optimizer == "lbfgsb":
|
| 792 |
+
registration_method.SetOptimizerAsLBFGSB(
|
| 793 |
+
gradientConvergenceTolerance=gradient_convergence_tolerance, numberOfIterations=number_of_iterations
|
| 794 |
+
)
|
| 795 |
+
elif optimizer == "powell":
|
| 796 |
+
registration_method.SetOptimizerAsPowell(numberOfIterations=number_of_iterations, maximumLineIterations=20)
|
| 797 |
+
elif optimizer == "amoeba":
|
| 798 |
+
registration_method.SetOptimizerAsAmoeba(
|
| 799 |
+
numberOfIterations=number_of_iterations, parametersConvergenceTolerance=gradient_convergence_tolerance
|
| 800 |
+
)
|
| 801 |
+
else:
|
| 802 |
+
logger.warning(f"Unknown optimizer {optimizer}, using gradient descent")
|
| 803 |
+
registration_method.SetOptimizerAsGradientDescent(
|
| 804 |
+
learningRate=learning_rate,
|
| 805 |
+
numberOfIterations=number_of_iterations,
|
| 806 |
+
convergenceMinimumValue=gradient_convergence_tolerance,
|
| 807 |
+
)
|
| 808 |
+
|
| 809 |
+
# Set interpolator
|
| 810 |
+
registration_method.SetInterpolator(sitk.sitkLinear)
|
| 811 |
+
|
| 812 |
+
# Set initial transform
|
| 813 |
+
registration_method.SetInitialTransform(transform, inPlace=False)
|
| 814 |
+
|
| 815 |
+
return registration_method
|
| 816 |
+
|
| 817 |
+
def register_images(
|
| 818 |
+
self,
|
| 819 |
+
fixed_image: sitk.Image,
|
| 820 |
+
moving_image: sitk.Image,
|
| 821 |
+
transform: sitk.Transform,
|
| 822 |
+
registration_method: sitk.ImageRegistrationMethod,
|
| 823 |
+
) -> tuple[sitk.Transform, sitk.Image]:
|
| 824 |
+
"""
|
| 825 |
+
Perform image registration.
|
| 826 |
+
|
| 827 |
+
Args:
|
| 828 |
+
fixed_image: Reference (fixed) image
|
| 829 |
+
moving_image: Image to be registered
|
| 830 |
+
transform: Transform object
|
| 831 |
+
registration_method: Configured registration method
|
| 832 |
+
|
| 833 |
+
Returns:
|
| 834 |
+
Tuple of (final_transform, registered_image)
|
| 835 |
+
"""
|
| 836 |
+
logger.info("Starting image registration...")
|
| 837 |
+
|
| 838 |
+
# Add iteration callback
|
| 839 |
+
def command_iteration():
|
| 840 |
+
logger.info(
|
| 841 |
+
f"Iteration: {registration_method.GetOptimizerIteration()}, "
|
| 842 |
+
f"Metric value: {registration_method.GetMetricValue():.6f}"
|
| 843 |
+
)
|
| 844 |
+
|
| 845 |
+
registration_method.AddCommand(sitk.sitkIterationEvent, command_iteration)
|
| 846 |
+
|
| 847 |
+
try:
|
| 848 |
+
# Execute registration
|
| 849 |
+
final_transform = registration_method.Execute(fixed_image, moving_image)
|
| 850 |
+
|
| 851 |
+
# Apply transform to moving image
|
| 852 |
+
resampler = sitk.ResampleImageFilter()
|
| 853 |
+
resampler.SetReferenceImage(fixed_image)
|
| 854 |
+
resampler.SetInterpolator(sitk.sitkLinear)
|
| 855 |
+
resampler.SetDefaultPixelValue(0)
|
| 856 |
+
resampler.SetTransform(final_transform)
|
| 857 |
+
|
| 858 |
+
registered_image = resampler.Execute(moving_image)
|
| 859 |
+
|
| 860 |
+
logger.info("Registration completed successfully")
|
| 861 |
+
return final_transform, registered_image
|
| 862 |
+
|
| 863 |
+
except Exception as e:
|
| 864 |
+
logger.error(f"Registration failed: {e}")
|
| 865 |
+
raise
|
| 866 |
+
|
| 867 |
+
def calculate_similarity_metrics(self, image1: sitk.Image, image2: sitk.Image) -> dict[str, float]:
|
| 868 |
+
"""
|
| 869 |
+
Calculate similarity metrics between two images.
|
| 870 |
+
|
| 871 |
+
Args:
|
| 872 |
+
image1: First image
|
| 873 |
+
image2: Second image
|
| 874 |
+
|
| 875 |
+
Returns:
|
| 876 |
+
Dictionary of similarity metrics
|
| 877 |
+
"""
|
| 878 |
+
logger.info("Calculating similarity metrics...")
|
| 879 |
+
metrics = {}
|
| 880 |
+
|
| 881 |
+
try:
|
| 882 |
+
# Convert to numpy arrays
|
| 883 |
+
array1 = sitk.GetArrayFromImage(image1)
|
| 884 |
+
array2 = sitk.GetArrayFromImage(image2)
|
| 885 |
+
|
| 886 |
+
# Flatten arrays
|
| 887 |
+
flat1 = array1.flatten()
|
| 888 |
+
flat2 = array2.flatten()
|
| 889 |
+
|
| 890 |
+
# Remove invalid values
|
| 891 |
+
valid_mask = np.isfinite(flat1) & np.isfinite(flat2)
|
| 892 |
+
flat1 = flat1[valid_mask]
|
| 893 |
+
flat2 = flat2[valid_mask]
|
| 894 |
+
|
| 895 |
+
if len(flat1) == 0:
|
| 896 |
+
logger.warning("No valid pixels for similarity calculation")
|
| 897 |
+
return {
|
| 898 |
+
"mutual_information": 0.0,
|
| 899 |
+
"mean_squares": 0.0,
|
| 900 |
+
"correlation": 0.0,
|
| 901 |
+
"normalized_correlation": 0.0,
|
| 902 |
+
}
|
| 903 |
+
|
| 904 |
+
# Mean Squared Error
|
| 905 |
+
mse = np.mean((flat1 - flat2) ** 2)
|
| 906 |
+
metrics["mean_squares"] = -mse # Negative because we want to maximize
|
| 907 |
+
|
| 908 |
+
# Pearson Correlation
|
| 909 |
+
if np.std(flat1) > 0 and np.std(flat2) > 0:
|
| 910 |
+
correlation = np.corrcoef(flat1, flat2)[0, 1]
|
| 911 |
+
metrics["correlation"] = correlation if not np.isnan(correlation) else 0.0
|
| 912 |
+
else:
|
| 913 |
+
metrics["correlation"] = 0.0
|
| 914 |
+
|
| 915 |
+
# Normalized Cross Correlation
|
| 916 |
+
if np.std(flat1) > 0 and np.std(flat2) > 0:
|
| 917 |
+
ncc = np.corrcoef(flat1, flat2)[0, 1]
|
| 918 |
+
metrics["normalized_correlation"] = ncc if not np.isnan(ncc) else 0.0
|
| 919 |
+
else:
|
| 920 |
+
metrics["normalized_correlation"] = 0.0
|
| 921 |
+
|
| 922 |
+
# Mutual Information (simplified calculation)
|
| 923 |
+
try:
|
| 924 |
+
# Create 2D histogram
|
| 925 |
+
hist_2d, x_edges, y_edges = np.histogram2d(flat1, flat2, bins=50)
|
| 926 |
+
hist_2d = hist_2d + 1e-10 # Add small value to avoid log(0)
|
| 927 |
+
|
| 928 |
+
# Normalize histogram
|
| 929 |
+
pxy = hist_2d / np.sum(hist_2d)
|
| 930 |
+
px = np.sum(pxy, axis=1)
|
| 931 |
+
py = np.sum(pxy, axis=0)
|
| 932 |
+
|
| 933 |
+
# Calculate mutual information
|
| 934 |
+
mi = 0.0
|
| 935 |
+
for i in range(len(px)):
|
| 936 |
+
for j in range(len(py)):
|
| 937 |
+
if pxy[i, j] > 0 and px[i] > 0 and py[j] > 0:
|
| 938 |
+
mi += pxy[i, j] * np.log2(pxy[i, j] / (px[i] * py[j]))
|
| 939 |
+
|
| 940 |
+
metrics["mutual_information"] = mi
|
| 941 |
+
|
| 942 |
+
except Exception as e:
|
| 943 |
+
logger.warning(f"Failed to calculate mutual information: {e}")
|
| 944 |
+
metrics["mutual_information"] = 0.0
|
| 945 |
+
|
| 946 |
+
except Exception as e:
|
| 947 |
+
logger.warning(f"Failed to calculate similarity metrics: {e}")
|
| 948 |
+
metrics = {
|
| 949 |
+
"mutual_information": 0.0,
|
| 950 |
+
"mean_squares": 0.0,
|
| 951 |
+
"correlation": 0.0,
|
| 952 |
+
"normalized_correlation": 0.0,
|
| 953 |
+
}
|
| 954 |
+
|
| 955 |
+
logger.info("Similarity metrics calculated")
|
| 956 |
+
return metrics
|
| 957 |
+
|
| 958 |
+
|
| 959 |
+
# ============================================================================
|
| 960 |
+
# CONVENIENCE FUNCTIONS FOR BIOMNI INTEGRATION
|
| 961 |
+
# ============================================================================
|
| 962 |
+
|
| 963 |
+
|
| 964 |
+
# Segmentation convenience functions
|
| 965 |
+
def split_modalities(input_file, output_dir, case_name="BRAT"):
|
| 966 |
+
"""Convenience function for splitting modalities"""
|
| 967 |
+
tool = SegmentationTool()
|
| 968 |
+
return tool.split_modalities(input_file, output_dir, case_name)
|
| 969 |
+
|
| 970 |
+
|
| 971 |
+
def prepare_input_for_nnunet(input_path, output_dir, case_name="BRAT"):
|
| 972 |
+
"""Convenience function for preparing nnUNet input"""
|
| 973 |
+
tool = SegmentationTool()
|
| 974 |
+
return tool.prepare_input_for_nnunet(input_path, output_dir, case_name)
|
| 975 |
+
|
| 976 |
+
|
| 977 |
+
def segment_with_nn_unet(
|
| 978 |
+
image_path,
|
| 979 |
+
output_dir,
|
| 980 |
+
task_id,
|
| 981 |
+
model_type="3d_fullres",
|
| 982 |
+
folds=None,
|
| 983 |
+
use_tta=False,
|
| 984 |
+
num_threads=1,
|
| 985 |
+
mixed_precision=True,
|
| 986 |
+
verbose=True,
|
| 987 |
+
auto_prepare_input=True,
|
| 988 |
+
results_folder=None,
|
| 989 |
+
):
|
| 990 |
+
"""Convenience function for nnUNet segmentation"""
|
| 991 |
+
tool = SegmentationTool()
|
| 992 |
+
return tool.segment_with_nn_unet(
|
| 993 |
+
image_path,
|
| 994 |
+
output_dir,
|
| 995 |
+
task_id,
|
| 996 |
+
model_type,
|
| 997 |
+
folds,
|
| 998 |
+
use_tta,
|
| 999 |
+
num_threads,
|
| 1000 |
+
mixed_precision,
|
| 1001 |
+
verbose,
|
| 1002 |
+
auto_prepare_input,
|
| 1003 |
+
results_folder,
|
| 1004 |
+
)
|
| 1005 |
+
|
| 1006 |
+
|
| 1007 |
+
def create_segmentation_visualization(original_mri, segmentation, output_dir="./visualization_output"):
|
| 1008 |
+
"""Convenience function for segmentation visualization"""
|
| 1009 |
+
tool = SegmentationTool()
|
| 1010 |
+
return tool.create_segmentation_visualization(original_mri, segmentation, output_dir)
|
| 1011 |
+
|
| 1012 |
+
|
| 1013 |
+
# Registration convenience functions
|
| 1014 |
+
def preprocess_image(image_path: str, output_path: str, denoise: bool = True, normalize: bool = True) -> str:
|
| 1015 |
+
"""
|
| 1016 |
+
Standalone image preprocessing function for Biomni integration.
|
| 1017 |
+
|
| 1018 |
+
Args:
|
| 1019 |
+
image_path: Path to input image
|
| 1020 |
+
output_path: Path to save preprocessed image
|
| 1021 |
+
denoise: Whether to apply denoising (default: True)
|
| 1022 |
+
normalize: Whether to apply normalization (default: True)
|
| 1023 |
+
|
| 1024 |
+
Returns:
|
| 1025 |
+
Path to the saved preprocessed image
|
| 1026 |
+
"""
|
| 1027 |
+
tool = ImageRegistrationTool()
|
| 1028 |
+
image = tool.load_image(image_path)
|
| 1029 |
+
preprocessed_image = tool.preprocess_image(image, denoise, normalize)
|
| 1030 |
+
tool.save_image(preprocessed_image, output_path)
|
| 1031 |
+
return output_path
|
| 1032 |
+
|
| 1033 |
+
|
| 1034 |
+
def quick_rigid_registration(
|
| 1035 |
+
fixed_image_path: str,
|
| 1036 |
+
moving_image_path: str,
|
| 1037 |
+
output_dir: str,
|
| 1038 |
+
metric: str = "mutual_information",
|
| 1039 |
+
optimizer: str = "gradient_descent",
|
| 1040 |
+
preprocess: bool = True,
|
| 1041 |
+
create_visualizations: bool = True,
|
| 1042 |
+
learning_rate: float = 0.01,
|
| 1043 |
+
number_of_iterations: int = 100,
|
| 1044 |
+
gradient_convergence_tolerance: float = 1e-6,
|
| 1045 |
+
) -> dict:
|
| 1046 |
+
"""
|
| 1047 |
+
Quick rigid registration function for Biomni integration.
|
| 1048 |
+
|
| 1049 |
+
Args:
|
| 1050 |
+
fixed_image_path: Path to reference image
|
| 1051 |
+
moving_image_path: Path to image to register
|
| 1052 |
+
output_dir: Directory to save results
|
| 1053 |
+
metric: Similarity metric
|
| 1054 |
+
optimizer: Optimization method
|
| 1055 |
+
preprocess: Whether to preprocess images
|
| 1056 |
+
create_visualizations: Whether to create visualizations
|
| 1057 |
+
learning_rate: Learning rate for optimizer
|
| 1058 |
+
number_of_iterations: Maximum iterations
|
| 1059 |
+
gradient_convergence_tolerance: Convergence tolerance
|
| 1060 |
+
|
| 1061 |
+
Returns:
|
| 1062 |
+
Dictionary with registration results
|
| 1063 |
+
"""
|
| 1064 |
+
tool = ImageRegistrationTool()
|
| 1065 |
+
|
| 1066 |
+
# Load images
|
| 1067 |
+
fixed_image = tool.load_image(fixed_image_path)
|
| 1068 |
+
moving_image = tool.load_image(moving_image_path)
|
| 1069 |
+
|
| 1070 |
+
# Preprocess if requested
|
| 1071 |
+
if preprocess:
|
| 1072 |
+
fixed_image = tool.preprocess_image(fixed_image)
|
| 1073 |
+
moving_image = tool.preprocess_image(moving_image)
|
| 1074 |
+
|
| 1075 |
+
# Create transform and registration method
|
| 1076 |
+
transform = tool.create_rigid_transform(fixed_image, moving_image)
|
| 1077 |
+
registration_method = tool.setup_registration_method(
|
| 1078 |
+
transform, metric, optimizer, learning_rate, number_of_iterations, gradient_convergence_tolerance
|
| 1079 |
+
)
|
| 1080 |
+
|
| 1081 |
+
# Perform registration
|
| 1082 |
+
final_transform, registered_image = tool.register_images(fixed_image, moving_image, transform, registration_method)
|
| 1083 |
+
|
| 1084 |
+
# Save results
|
| 1085 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 1086 |
+
|
| 1087 |
+
# Save registered image
|
| 1088 |
+
registered_path = os.path.join(output_dir, "rigid_registered.nii.gz")
|
| 1089 |
+
tool.save_image(registered_image, registered_path)
|
| 1090 |
+
|
| 1091 |
+
# Save transform
|
| 1092 |
+
transform_path = os.path.join(output_dir, "rigid_transform.tfm")
|
| 1093 |
+
sitk.WriteTransform(final_transform, transform_path)
|
| 1094 |
+
|
| 1095 |
+
# Calculate metrics
|
| 1096 |
+
metrics_before = tool.calculate_similarity_metrics(fixed_image, moving_image)
|
| 1097 |
+
metrics_after = tool.calculate_similarity_metrics(fixed_image, registered_image)
|
| 1098 |
+
|
| 1099 |
+
results = {
|
| 1100 |
+
"registered_image_path": registered_path,
|
| 1101 |
+
"transform_path": transform_path,
|
| 1102 |
+
"metrics_before": metrics_before,
|
| 1103 |
+
"metrics_after": metrics_after,
|
| 1104 |
+
"registration_type": "rigid",
|
| 1105 |
+
}
|
| 1106 |
+
|
| 1107 |
+
return results
|
| 1108 |
+
|
| 1109 |
+
|
| 1110 |
+
def quick_affine_registration(
|
| 1111 |
+
fixed_image_path: str,
|
| 1112 |
+
moving_image_path: str,
|
| 1113 |
+
output_dir: str,
|
| 1114 |
+
metric: str = "mutual_information",
|
| 1115 |
+
optimizer: str = "gradient_descent",
|
| 1116 |
+
preprocess: bool = True,
|
| 1117 |
+
create_visualizations: bool = True,
|
| 1118 |
+
learning_rate: float = 0.01,
|
| 1119 |
+
number_of_iterations: int = 100,
|
| 1120 |
+
gradient_convergence_tolerance: float = 1e-6,
|
| 1121 |
+
) -> dict:
|
| 1122 |
+
"""
|
| 1123 |
+
Quick affine registration function for Biomni integration.
|
| 1124 |
+
|
| 1125 |
+
Args:
|
| 1126 |
+
fixed_image_path: Path to reference image
|
| 1127 |
+
moving_image_path: Path to image to register
|
| 1128 |
+
output_dir: Directory to save results
|
| 1129 |
+
metric: Similarity metric
|
| 1130 |
+
optimizer: Optimization method
|
| 1131 |
+
preprocess: Whether to preprocess images
|
| 1132 |
+
create_visualizations: Whether to create visualizations
|
| 1133 |
+
learning_rate: Learning rate for optimizer
|
| 1134 |
+
number_of_iterations: Maximum iterations
|
| 1135 |
+
gradient_convergence_tolerance: Convergence tolerance
|
| 1136 |
+
|
| 1137 |
+
Returns:
|
| 1138 |
+
Dictionary with registration results
|
| 1139 |
+
"""
|
| 1140 |
+
tool = ImageRegistrationTool()
|
| 1141 |
+
|
| 1142 |
+
# Load images
|
| 1143 |
+
fixed_image = tool.load_image(fixed_image_path)
|
| 1144 |
+
moving_image = tool.load_image(moving_image_path)
|
| 1145 |
+
|
| 1146 |
+
# Preprocess if requested
|
| 1147 |
+
if preprocess:
|
| 1148 |
+
fixed_image = tool.preprocess_image(fixed_image)
|
| 1149 |
+
moving_image = tool.preprocess_image(moving_image)
|
| 1150 |
+
|
| 1151 |
+
# Create transform and registration method
|
| 1152 |
+
transform = tool.create_affine_transform(fixed_image, moving_image)
|
| 1153 |
+
registration_method = tool.setup_registration_method(
|
| 1154 |
+
transform, metric, optimizer, learning_rate, number_of_iterations, gradient_convergence_tolerance
|
| 1155 |
+
)
|
| 1156 |
+
|
| 1157 |
+
# Perform registration
|
| 1158 |
+
final_transform, registered_image = tool.register_images(fixed_image, moving_image, transform, registration_method)
|
| 1159 |
+
|
| 1160 |
+
# Save results
|
| 1161 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 1162 |
+
|
| 1163 |
+
# Save registered image
|
| 1164 |
+
registered_path = os.path.join(output_dir, "affine_registered.nii.gz")
|
| 1165 |
+
tool.save_image(registered_image, registered_path)
|
| 1166 |
+
|
| 1167 |
+
# Save transform
|
| 1168 |
+
transform_path = os.path.join(output_dir, "affine_transform.tfm")
|
| 1169 |
+
sitk.WriteTransform(final_transform, transform_path)
|
| 1170 |
+
|
| 1171 |
+
# Calculate metrics
|
| 1172 |
+
metrics_before = tool.calculate_similarity_metrics(fixed_image, moving_image)
|
| 1173 |
+
metrics_after = tool.calculate_similarity_metrics(fixed_image, registered_image)
|
| 1174 |
+
|
| 1175 |
+
results = {
|
| 1176 |
+
"registered_image_path": registered_path,
|
| 1177 |
+
"transform_path": transform_path,
|
| 1178 |
+
"metrics_before": metrics_before,
|
| 1179 |
+
"metrics_after": metrics_after,
|
| 1180 |
+
"registration_type": "affine",
|
| 1181 |
+
}
|
| 1182 |
+
|
| 1183 |
+
return results
|
| 1184 |
+
|
| 1185 |
+
|
| 1186 |
+
def quick_deformable_registration(
|
| 1187 |
+
fixed_image_path: str,
|
| 1188 |
+
moving_image_path: str,
|
| 1189 |
+
output_dir: str,
|
| 1190 |
+
metric: str = "mutual_information",
|
| 1191 |
+
optimizer: str = "gradient_descent",
|
| 1192 |
+
preprocess: bool = True,
|
| 1193 |
+
create_visualizations: bool = True,
|
| 1194 |
+
learning_rate: float = 0.01,
|
| 1195 |
+
number_of_iterations: int = 100,
|
| 1196 |
+
gradient_convergence_tolerance: float = 1e-6,
|
| 1197 |
+
number_of_control_points: int = 4,
|
| 1198 |
+
) -> dict:
|
| 1199 |
+
"""
|
| 1200 |
+
Quick deformable registration function for Biomni integration.
|
| 1201 |
+
|
| 1202 |
+
Args:
|
| 1203 |
+
fixed_image_path: Path to reference image
|
| 1204 |
+
moving_image_path: Path to image to register
|
| 1205 |
+
output_dir: Directory to save results
|
| 1206 |
+
metric: Similarity metric
|
| 1207 |
+
optimizer: Optimization method
|
| 1208 |
+
preprocess: Whether to preprocess images
|
| 1209 |
+
create_visualizations: Whether to create visualizations
|
| 1210 |
+
learning_rate: Learning rate for optimizer
|
| 1211 |
+
number_of_iterations: Maximum iterations
|
| 1212 |
+
gradient_convergence_tolerance: Convergence tolerance
|
| 1213 |
+
number_of_control_points: Number of B-spline control points
|
| 1214 |
+
|
| 1215 |
+
Returns:
|
| 1216 |
+
Dictionary with registration results
|
| 1217 |
+
"""
|
| 1218 |
+
tool = ImageRegistrationTool()
|
| 1219 |
+
|
| 1220 |
+
# Load images
|
| 1221 |
+
fixed_image = tool.load_image(fixed_image_path)
|
| 1222 |
+
moving_image = tool.load_image(moving_image_path)
|
| 1223 |
+
|
| 1224 |
+
# Preprocess if requested
|
| 1225 |
+
if preprocess:
|
| 1226 |
+
fixed_image = tool.preprocess_image(fixed_image)
|
| 1227 |
+
moving_image = tool.preprocess_image(moving_image)
|
| 1228 |
+
|
| 1229 |
+
# Create transform and registration method
|
| 1230 |
+
transform = tool.create_deformable_transform(fixed_image, moving_image, number_of_control_points)
|
| 1231 |
+
registration_method = tool.setup_registration_method(
|
| 1232 |
+
transform, metric, optimizer, learning_rate, number_of_iterations, gradient_convergence_tolerance
|
| 1233 |
+
)
|
| 1234 |
+
|
| 1235 |
+
# Perform registration
|
| 1236 |
+
final_transform, registered_image = tool.register_images(fixed_image, moving_image, transform, registration_method)
|
| 1237 |
+
|
| 1238 |
+
# Save results
|
| 1239 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 1240 |
+
|
| 1241 |
+
# Save registered image
|
| 1242 |
+
registered_path = os.path.join(output_dir, "deformable_registered.nii.gz")
|
| 1243 |
+
tool.save_image(registered_image, registered_path)
|
| 1244 |
+
|
| 1245 |
+
# Save transform
|
| 1246 |
+
transform_path = os.path.join(output_dir, "deformable_transform.tfm")
|
| 1247 |
+
sitk.WriteTransform(final_transform, transform_path)
|
| 1248 |
+
|
| 1249 |
+
# Calculate metrics
|
| 1250 |
+
metrics_before = tool.calculate_similarity_metrics(fixed_image, moving_image)
|
| 1251 |
+
metrics_after = tool.calculate_similarity_metrics(fixed_image, registered_image)
|
| 1252 |
+
|
| 1253 |
+
results = {
|
| 1254 |
+
"registered_image_path": registered_path,
|
| 1255 |
+
"transform_path": transform_path,
|
| 1256 |
+
"metrics_before": metrics_before,
|
| 1257 |
+
"metrics_after": metrics_after,
|
| 1258 |
+
"registration_type": "deformable",
|
| 1259 |
+
}
|
| 1260 |
+
|
| 1261 |
+
return results
|
| 1262 |
+
|
| 1263 |
+
|
| 1264 |
+
def batch_register_images(
|
| 1265 |
+
fixed_image_path: str,
|
| 1266 |
+
moving_images_dir: str,
|
| 1267 |
+
output_dir: str,
|
| 1268 |
+
transform_type: str = "rigid",
|
| 1269 |
+
metric: str = "mutual_information",
|
| 1270 |
+
optimizer: str = "gradient_descent",
|
| 1271 |
+
preprocess: bool = True,
|
| 1272 |
+
create_visualizations: bool = True,
|
| 1273 |
+
learning_rate: float = 0.01,
|
| 1274 |
+
number_of_iterations: int = 100,
|
| 1275 |
+
gradient_convergence_tolerance: float = 1e-6,
|
| 1276 |
+
) -> dict:
|
| 1277 |
+
"""
|
| 1278 |
+
Batch registration of multiple images to a single reference.
|
| 1279 |
+
|
| 1280 |
+
Args:
|
| 1281 |
+
fixed_image_path: Path to reference image
|
| 1282 |
+
moving_images_dir: Directory containing images to register
|
| 1283 |
+
output_dir: Directory to save results
|
| 1284 |
+
transform_type: Type of registration ('rigid', 'affine', 'deformable')
|
| 1285 |
+
metric: Similarity metric
|
| 1286 |
+
optimizer: Optimization method
|
| 1287 |
+
preprocess: Whether to preprocess images
|
| 1288 |
+
create_visualizations: Whether to create visualizations
|
| 1289 |
+
learning_rate: Learning rate for optimizer
|
| 1290 |
+
number_of_iterations: Maximum iterations
|
| 1291 |
+
gradient_convergence_tolerance: Convergence tolerance
|
| 1292 |
+
|
| 1293 |
+
Returns:
|
| 1294 |
+
Dictionary with batch registration results
|
| 1295 |
+
"""
|
| 1296 |
+
logger.info(f"Starting batch {transform_type} registration...")
|
| 1297 |
+
|
| 1298 |
+
# Find all image files in the directory
|
| 1299 |
+
image_files = []
|
| 1300 |
+
for file in os.listdir(moving_images_dir):
|
| 1301 |
+
if any(file.endswith(ext) for ext in [".nii", ".nii.gz", ".nrrd", ".mha", ".mhd"]):
|
| 1302 |
+
image_files.append(os.path.join(moving_images_dir, file))
|
| 1303 |
+
|
| 1304 |
+
if not image_files:
|
| 1305 |
+
raise ValueError(f"No image files found in {moving_images_dir}")
|
| 1306 |
+
|
| 1307 |
+
logger.info(f"Found {len(image_files)} images to register")
|
| 1308 |
+
|
| 1309 |
+
# Create output directory
|
| 1310 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 1311 |
+
|
| 1312 |
+
# Process each image
|
| 1313 |
+
results = {}
|
| 1314 |
+
ImageRegistrationTool()
|
| 1315 |
+
|
| 1316 |
+
for i, moving_image_path in enumerate(image_files):
|
| 1317 |
+
logger.info(f"Processing {i + 1}/{len(image_files)}: {os.path.basename(moving_image_path)}")
|
| 1318 |
+
|
| 1319 |
+
# Create individual output directory
|
| 1320 |
+
image_name = os.path.splitext(os.path.basename(moving_image_path))[0]
|
| 1321 |
+
if image_name.endswith(".nii"):
|
| 1322 |
+
image_name = os.path.splitext(image_name)[0]
|
| 1323 |
+
|
| 1324 |
+
individual_output_dir = os.path.join(output_dir, f"registration_{image_name}")
|
| 1325 |
+
|
| 1326 |
+
try:
|
| 1327 |
+
if transform_type == "rigid":
|
| 1328 |
+
result = quick_rigid_registration(
|
| 1329 |
+
fixed_image_path,
|
| 1330 |
+
moving_image_path,
|
| 1331 |
+
individual_output_dir,
|
| 1332 |
+
metric,
|
| 1333 |
+
optimizer,
|
| 1334 |
+
preprocess,
|
| 1335 |
+
create_visualizations,
|
| 1336 |
+
learning_rate,
|
| 1337 |
+
number_of_iterations,
|
| 1338 |
+
gradient_convergence_tolerance,
|
| 1339 |
+
)
|
| 1340 |
+
elif transform_type == "affine":
|
| 1341 |
+
result = quick_affine_registration(
|
| 1342 |
+
fixed_image_path,
|
| 1343 |
+
moving_image_path,
|
| 1344 |
+
individual_output_dir,
|
| 1345 |
+
metric,
|
| 1346 |
+
optimizer,
|
| 1347 |
+
preprocess,
|
| 1348 |
+
create_visualizations,
|
| 1349 |
+
learning_rate,
|
| 1350 |
+
number_of_iterations,
|
| 1351 |
+
gradient_convergence_tolerance,
|
| 1352 |
+
)
|
| 1353 |
+
elif transform_type == "deformable":
|
| 1354 |
+
result = quick_deformable_registration(
|
| 1355 |
+
fixed_image_path,
|
| 1356 |
+
moving_image_path,
|
| 1357 |
+
individual_output_dir,
|
| 1358 |
+
metric,
|
| 1359 |
+
optimizer,
|
| 1360 |
+
preprocess,
|
| 1361 |
+
create_visualizations,
|
| 1362 |
+
learning_rate,
|
| 1363 |
+
number_of_iterations,
|
| 1364 |
+
gradient_convergence_tolerance,
|
| 1365 |
+
)
|
| 1366 |
+
else:
|
| 1367 |
+
raise ValueError(f"Unknown transform type: {transform_type}")
|
| 1368 |
+
|
| 1369 |
+
results[image_name] = result
|
| 1370 |
+
logger.info(f"Successfully registered {image_name}")
|
| 1371 |
+
|
| 1372 |
+
except Exception as e:
|
| 1373 |
+
logger.error(f"Failed to register {image_name}: {e}")
|
| 1374 |
+
results[image_name] = {"error": str(e)}
|
| 1375 |
+
|
| 1376 |
+
logger.info(f"Batch registration completed. Processed {len(image_files)} images.")
|
| 1377 |
+
return results
|
| 1378 |
+
|
| 1379 |
+
|
| 1380 |
+
def calculate_similarity_metrics(image1_path: str, image2_path: str) -> dict[str, float]:
|
| 1381 |
+
"""
|
| 1382 |
+
Calculate similarity metrics between two images.
|
| 1383 |
+
|
| 1384 |
+
Args:
|
| 1385 |
+
image1_path: Path to first image
|
| 1386 |
+
image2_path: Path to second image
|
| 1387 |
+
|
| 1388 |
+
Returns:
|
| 1389 |
+
Dictionary of similarity metrics
|
| 1390 |
+
"""
|
| 1391 |
+
tool = ImageRegistrationTool()
|
| 1392 |
+
image1 = tool.load_image(image1_path)
|
| 1393 |
+
image2 = tool.load_image(image2_path)
|
| 1394 |
+
return tool.calculate_similarity_metrics(image1, image2)
|
Biomni/biomni/tool/biophysics.py
ADDED
|
@@ -0,0 +1,513 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
def predict_protein_disorder_regions(protein_sequence, threshold=0.5, output_file="disorder_prediction_results.csv"):
|
| 2 |
+
"""Predicts intrinsically disordered regions (IDRs) in a protein sequence using IUPred2A.
|
| 3 |
+
|
| 4 |
+
Parameters
|
| 5 |
+
----------
|
| 6 |
+
protein_sequence : str
|
| 7 |
+
The amino acid sequence of the protein to analyze
|
| 8 |
+
threshold : float, optional
|
| 9 |
+
The disorder score threshold above which a residue is considered disordered (default: 0.5)
|
| 10 |
+
output_file : str, optional
|
| 11 |
+
Filename to save the per-residue disorder scores (default: "disorder_prediction_results.csv")
|
| 12 |
+
|
| 13 |
+
Returns
|
| 14 |
+
-------
|
| 15 |
+
str
|
| 16 |
+
A research log summarizing the prediction process and results
|
| 17 |
+
|
| 18 |
+
"""
|
| 19 |
+
import csv
|
| 20 |
+
import re
|
| 21 |
+
|
| 22 |
+
import requests
|
| 23 |
+
|
| 24 |
+
# Clean the input sequence
|
| 25 |
+
protein_sequence = "".join(re.findall(r"[A-Za-z]", protein_sequence))
|
| 26 |
+
|
| 27 |
+
# Step 1: Submit the sequence to IUPred2A web server
|
| 28 |
+
url = "https://iupred2a.elte.hu/iupred2a"
|
| 29 |
+
payload = {
|
| 30 |
+
"seq": protein_sequence,
|
| 31 |
+
"iupred2": "long", # Use IUPred2 long disorder prediction
|
| 32 |
+
"anchor2": "no", # Don't use ANCHOR2 prediction
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
try:
|
| 36 |
+
response = requests.post(url, data=payload)
|
| 37 |
+
response.raise_for_status()
|
| 38 |
+
except requests.exceptions.RequestException as e:
|
| 39 |
+
return f"Error accessing IUPred2A server: {str(e)}"
|
| 40 |
+
|
| 41 |
+
# Step 2: Parse the results to extract disorder scores
|
| 42 |
+
result_lines = response.text.split("\n")
|
| 43 |
+
scores = []
|
| 44 |
+
|
| 45 |
+
for line in result_lines:
|
| 46 |
+
if line.startswith("#") or not line.strip():
|
| 47 |
+
continue
|
| 48 |
+
parts = line.split()
|
| 49 |
+
if len(parts) >= 3:
|
| 50 |
+
try:
|
| 51 |
+
position = int(parts[0])
|
| 52 |
+
residue = parts[1]
|
| 53 |
+
score = float(parts[2])
|
| 54 |
+
scores.append((position, residue, score))
|
| 55 |
+
except (ValueError, IndexError):
|
| 56 |
+
continue
|
| 57 |
+
|
| 58 |
+
if not scores:
|
| 59 |
+
return "No valid prediction data was returned from the server."
|
| 60 |
+
|
| 61 |
+
# Step 3: Identify disordered regions
|
| 62 |
+
disordered_regions = []
|
| 63 |
+
current_region = []
|
| 64 |
+
|
| 65 |
+
for pos, _, score in scores:
|
| 66 |
+
if score >= threshold:
|
| 67 |
+
if not current_region:
|
| 68 |
+
current_region = [pos]
|
| 69 |
+
elif pos == current_region[-1] + 1:
|
| 70 |
+
current_region.append(pos)
|
| 71 |
+
else:
|
| 72 |
+
if len(current_region) > 1:
|
| 73 |
+
disordered_regions.append((current_region[0], current_region[-1]))
|
| 74 |
+
current_region = [pos]
|
| 75 |
+
elif current_region and len(current_region) > 1:
|
| 76 |
+
disordered_regions.append((current_region[0], current_region[-1]))
|
| 77 |
+
current_region = []
|
| 78 |
+
elif current_region:
|
| 79 |
+
current_region = []
|
| 80 |
+
|
| 81 |
+
# Add the last region if it exists
|
| 82 |
+
if current_region and len(current_region) > 1:
|
| 83 |
+
disordered_regions.append((current_region[0], current_region[-1]))
|
| 84 |
+
|
| 85 |
+
# Step 4: Save results to CSV
|
| 86 |
+
with open(output_file, "w", newline="") as f:
|
| 87 |
+
writer = csv.writer(f)
|
| 88 |
+
writer.writerow(["Position", "Amino_Acid", "Disorder_Score", "Is_Disordered"])
|
| 89 |
+
for pos, aa, score in scores:
|
| 90 |
+
is_disordered = "Yes" if score >= threshold else "No"
|
| 91 |
+
writer.writerow([pos, aa, score, is_disordered])
|
| 92 |
+
|
| 93 |
+
# Step 5: Generate the research log
|
| 94 |
+
total_residues = len(scores)
|
| 95 |
+
disordered_count = sum(1 for _, _, score in scores if score >= threshold)
|
| 96 |
+
disordered_percentage = (disordered_count / total_residues) * 100 if total_residues > 0 else 0
|
| 97 |
+
|
| 98 |
+
log = f"""
|
| 99 |
+
Intrinsically Disordered Region (IDR) Prediction Research Log:
|
| 100 |
+
=============================================================
|
| 101 |
+
Analysis performed using IUPred2A algorithm (long disorder mode)
|
| 102 |
+
Protein sequence length: {total_residues} amino acids
|
| 103 |
+
Disorder threshold: {threshold}
|
| 104 |
+
|
| 105 |
+
Results Summary:
|
| 106 |
+
- {disordered_count} residues ({disordered_percentage:.2f}%) predicted as disordered
|
| 107 |
+
- {len(disordered_regions)} distinct disordered regions identified
|
| 108 |
+
|
| 109 |
+
Disordered Regions:
|
| 110 |
+
"""
|
| 111 |
+
|
| 112 |
+
if disordered_regions:
|
| 113 |
+
for start, end in disordered_regions:
|
| 114 |
+
length = end - start + 1
|
| 115 |
+
log += f"- Region {start}-{end} (length: {length} residues)\n"
|
| 116 |
+
else:
|
| 117 |
+
log += "- No significant disordered regions found\n"
|
| 118 |
+
|
| 119 |
+
log += f"\nDetailed per-residue scores saved to: {output_file}"
|
| 120 |
+
|
| 121 |
+
return log
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def analyze_cell_morphology_and_cytoskeleton(image_path, output_dir="./results", threshold_method="otsu"):
|
| 125 |
+
"""Quantifies cell morphology and cytoskeletal organization from fluorescence microscopy images.
|
| 126 |
+
|
| 127 |
+
Parameters
|
| 128 |
+
----------
|
| 129 |
+
image_path : str
|
| 130 |
+
Path to the fluorescence microscopy image file
|
| 131 |
+
output_dir : str, optional
|
| 132 |
+
Directory to save output files (default: './results')
|
| 133 |
+
threshold_method : str, optional
|
| 134 |
+
Method for cell segmentation ('otsu', 'adaptive', or 'manual') (default: 'otsu')
|
| 135 |
+
|
| 136 |
+
Returns
|
| 137 |
+
-------
|
| 138 |
+
str
|
| 139 |
+
Research log summarizing the analysis steps and results
|
| 140 |
+
|
| 141 |
+
"""
|
| 142 |
+
import os
|
| 143 |
+
from datetime import datetime
|
| 144 |
+
|
| 145 |
+
import cv2
|
| 146 |
+
import numpy as np
|
| 147 |
+
import pandas as pd
|
| 148 |
+
from skimage import exposure, feature, filters, io, measure, morphology
|
| 149 |
+
from skimage.color import rgb2gray
|
| 150 |
+
|
| 151 |
+
# Create output directory if it doesn't exist
|
| 152 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 153 |
+
|
| 154 |
+
# Start log
|
| 155 |
+
log = f"Cell Morphology and Cytoskeleton Analysis Log - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
|
| 156 |
+
log += f"Analyzing image: {image_path}\n\n"
|
| 157 |
+
|
| 158 |
+
# Load image
|
| 159 |
+
log += "Step 1: Loading and preprocessing image\n"
|
| 160 |
+
try:
|
| 161 |
+
image = io.imread(image_path)
|
| 162 |
+
# Convert to grayscale if RGB
|
| 163 |
+
if len(image.shape) > 2:
|
| 164 |
+
gray_image = rgb2gray(image)
|
| 165 |
+
log += "- Converted RGB image to grayscale\n"
|
| 166 |
+
else:
|
| 167 |
+
gray_image = image
|
| 168 |
+
|
| 169 |
+
# Enhance contrast
|
| 170 |
+
gray_image = exposure.equalize_hist(gray_image)
|
| 171 |
+
log += "- Enhanced image contrast\n"
|
| 172 |
+
except Exception as e:
|
| 173 |
+
return f"Error loading image: {str(e)}"
|
| 174 |
+
|
| 175 |
+
# Segment cells
|
| 176 |
+
log += "\nStep 2: Segmenting cells from background\n"
|
| 177 |
+
if threshold_method == "otsu":
|
| 178 |
+
thresh = filters.threshold_otsu(gray_image)
|
| 179 |
+
binary = gray_image > thresh
|
| 180 |
+
log += f"- Applied Otsu thresholding (threshold value: {thresh:.4f})\n"
|
| 181 |
+
elif threshold_method == "adaptive":
|
| 182 |
+
binary = filters.threshold_local(gray_image, block_size=35, offset=0.05)
|
| 183 |
+
binary = gray_image > binary
|
| 184 |
+
log += "- Applied adaptive thresholding\n"
|
| 185 |
+
else: # manual
|
| 186 |
+
thresh = 0.5 # Default value, can be parameterized
|
| 187 |
+
binary = gray_image > thresh
|
| 188 |
+
log += f"- Applied manual thresholding (threshold value: {thresh:.4f})\n"
|
| 189 |
+
|
| 190 |
+
# Clean up binary image
|
| 191 |
+
binary = morphology.remove_small_objects(binary, min_size=100)
|
| 192 |
+
binary = morphology.remove_small_holes(binary, area_threshold=100)
|
| 193 |
+
binary = morphology.binary_closing(binary, morphology.disk(3))
|
| 194 |
+
log += "- Applied morphological operations to clean segmentation\n"
|
| 195 |
+
|
| 196 |
+
# Label cells
|
| 197 |
+
labeled_cells, num_cells = measure.label(binary, return_num=True)
|
| 198 |
+
log += f"- Identified {num_cells} cell regions\n"
|
| 199 |
+
|
| 200 |
+
# Analyze cell properties
|
| 201 |
+
log += "\nStep 3: Analyzing cell morphology\n"
|
| 202 |
+
cell_props = measure.regionprops_table(
|
| 203 |
+
labeled_cells,
|
| 204 |
+
gray_image,
|
| 205 |
+
properties=(
|
| 206 |
+
"area",
|
| 207 |
+
"perimeter",
|
| 208 |
+
"major_axis_length",
|
| 209 |
+
"minor_axis_length",
|
| 210 |
+
"eccentricity",
|
| 211 |
+
"orientation",
|
| 212 |
+
"solidity",
|
| 213 |
+
),
|
| 214 |
+
)
|
| 215 |
+
|
| 216 |
+
# Convert to DataFrame for easier manipulation
|
| 217 |
+
cell_df = pd.DataFrame(cell_props)
|
| 218 |
+
|
| 219 |
+
# Calculate additional metrics
|
| 220 |
+
if len(cell_df) > 0:
|
| 221 |
+
cell_df["aspect_ratio"] = cell_df["major_axis_length"] / cell_df["minor_axis_length"]
|
| 222 |
+
cell_df["circularity"] = (4 * np.pi * cell_df["area"]) / (cell_df["perimeter"] ** 2)
|
| 223 |
+
|
| 224 |
+
# Summary statistics
|
| 225 |
+
log += f"- Average cell area: {cell_df['area'].mean():.2f} pixels\n"
|
| 226 |
+
log += f"- Average aspect ratio: {cell_df['aspect_ratio'].mean():.2f}\n"
|
| 227 |
+
log += f"- Average circularity: {cell_df['circularity'].mean():.2f}\n"
|
| 228 |
+
log += f"- Average eccentricity: {cell_df['eccentricity'].mean():.2f}\n"
|
| 229 |
+
else:
|
| 230 |
+
log += "- No cells detected for morphological analysis\n"
|
| 231 |
+
|
| 232 |
+
# Analyze cytoskeletal organization
|
| 233 |
+
log += "\nStep 4: Analyzing cytoskeletal organization\n"
|
| 234 |
+
|
| 235 |
+
# Edge detection to highlight cytoskeletal fibers
|
| 236 |
+
edges = feature.canny(gray_image, sigma=2)
|
| 237 |
+
|
| 238 |
+
# Use Hough transform to detect lines (cytoskeletal fibers)
|
| 239 |
+
if np.any(edges):
|
| 240 |
+
lines = cv2.HoughLinesP(
|
| 241 |
+
edges.astype(np.uint8),
|
| 242 |
+
1,
|
| 243 |
+
np.pi / 180,
|
| 244 |
+
threshold=10,
|
| 245 |
+
minLineLength=10,
|
| 246 |
+
maxLineGap=5,
|
| 247 |
+
)
|
| 248 |
+
|
| 249 |
+
if lines is not None:
|
| 250 |
+
# Calculate line orientations
|
| 251 |
+
orientations = []
|
| 252 |
+
for line in lines:
|
| 253 |
+
x1, y1, x2, y2 = line[0]
|
| 254 |
+
if x2 - x1 != 0: # Avoid division by zero
|
| 255 |
+
angle = np.arctan2(y2 - y1, x2 - x1) * 180 / np.pi
|
| 256 |
+
orientations.append(angle)
|
| 257 |
+
|
| 258 |
+
if orientations:
|
| 259 |
+
# Convert to numpy array for calculations
|
| 260 |
+
orientations = np.array(orientations)
|
| 261 |
+
|
| 262 |
+
# Calculate alignment metrics
|
| 263 |
+
mean_orientation = np.mean(orientations)
|
| 264 |
+
# Normalize angles to -90 to 90 degrees
|
| 265 |
+
norm_angles = np.mod(orientations + 90, 180) - 90
|
| 266 |
+
std_orientation = np.std(norm_angles)
|
| 267 |
+
|
| 268 |
+
# Order parameter (measure of alignment, 1 = perfectly aligned, 0 = random)
|
| 269 |
+
# Convert angles to radians for calculation
|
| 270 |
+
rad_angles = np.radians(norm_angles)
|
| 271 |
+
order_parameter = np.sqrt(np.mean(np.cos(2 * rad_angles)) ** 2 + np.mean(np.sin(2 * rad_angles)) ** 2)
|
| 272 |
+
|
| 273 |
+
log += f"- Detected {len(orientations)} cytoskeletal fibers\n"
|
| 274 |
+
log += f"- Mean fiber orientation: {mean_orientation:.2f} degrees\n"
|
| 275 |
+
log += f"- Standard deviation of orientation: {std_orientation:.2f} degrees\n"
|
| 276 |
+
log += f"- Order parameter (alignment): {order_parameter:.4f} (0=random, 1=aligned)\n"
|
| 277 |
+
|
| 278 |
+
# Add fiber data to dataframe
|
| 279 |
+
fiber_df = pd.DataFrame({"fiber_orientation": orientations})
|
| 280 |
+
else:
|
| 281 |
+
log += "- No fiber orientations could be calculated\n"
|
| 282 |
+
fiber_df = pd.DataFrame()
|
| 283 |
+
else:
|
| 284 |
+
log += "- No cytoskeletal fibers detected\n"
|
| 285 |
+
fiber_df = pd.DataFrame()
|
| 286 |
+
else:
|
| 287 |
+
log += "- No edges detected for cytoskeletal analysis\n"
|
| 288 |
+
fiber_df = pd.DataFrame()
|
| 289 |
+
|
| 290 |
+
# Save results
|
| 291 |
+
log += "\nStep 5: Saving results\n"
|
| 292 |
+
|
| 293 |
+
# Save cell morphology data
|
| 294 |
+
if len(cell_df) > 0:
|
| 295 |
+
cell_csv_path = os.path.join(output_dir, "cell_morphology_data.csv")
|
| 296 |
+
cell_df.to_csv(cell_csv_path, index=False)
|
| 297 |
+
log += f"- Cell morphology data saved to: {cell_csv_path}\n"
|
| 298 |
+
|
| 299 |
+
# Save fiber orientation data
|
| 300 |
+
if len(fiber_df) > 0:
|
| 301 |
+
fiber_csv_path = os.path.join(output_dir, "fiber_orientation_data.csv")
|
| 302 |
+
fiber_df.to_csv(fiber_csv_path, index=False)
|
| 303 |
+
log += f"- Fiber orientation data saved to: {fiber_csv_path}\n"
|
| 304 |
+
|
| 305 |
+
# Save segmentation image
|
| 306 |
+
if num_cells > 0:
|
| 307 |
+
segmentation_path = os.path.join(output_dir, "cell_segmentation.png")
|
| 308 |
+
io.imsave(segmentation_path, labeled_cells.astype(np.uint8) * 50)
|
| 309 |
+
log += f"- Cell segmentation image saved to: {segmentation_path}\n"
|
| 310 |
+
|
| 311 |
+
# Summary
|
| 312 |
+
log += "\nAnalysis Summary:\n"
|
| 313 |
+
log += f"- Processed image: {image_path}\n"
|
| 314 |
+
log += f"- Detected {num_cells} cells\n"
|
| 315 |
+
if len(cell_df) > 0:
|
| 316 |
+
log += f"- Cell size range: {cell_df['area'].min():.1f} to {cell_df['area'].max():.1f} pixels\n"
|
| 317 |
+
log += f"- Cell shape: average aspect ratio = {cell_df['aspect_ratio'].mean():.2f}\n"
|
| 318 |
+
if "order_parameter" in locals():
|
| 319 |
+
log += f"- Cytoskeletal organization: alignment parameter = {order_parameter:.4f}\n"
|
| 320 |
+
|
| 321 |
+
return log
|
| 322 |
+
|
| 323 |
+
|
| 324 |
+
def analyze_tissue_deformation_flow(image_sequence, output_dir="results", pixel_scale=1.0):
|
| 325 |
+
"""Quantify tissue deformation and flow dynamics from microscopy image sequence.
|
| 326 |
+
|
| 327 |
+
Parameters
|
| 328 |
+
----------
|
| 329 |
+
image_sequence : list or numpy.ndarray
|
| 330 |
+
Sequence of microscopy images (either a list of file paths or a 3D numpy array [time, height, width])
|
| 331 |
+
output_dir : str, optional
|
| 332 |
+
Directory to save results (default: "results")
|
| 333 |
+
pixel_scale : float, optional
|
| 334 |
+
Physical scale of pixels (e.g., μm/pixel) for proper scaling of metrics (default: 1.0)
|
| 335 |
+
|
| 336 |
+
Returns
|
| 337 |
+
-------
|
| 338 |
+
str
|
| 339 |
+
Research log summarizing the analysis steps and results
|
| 340 |
+
|
| 341 |
+
"""
|
| 342 |
+
import os
|
| 343 |
+
from datetime import datetime
|
| 344 |
+
|
| 345 |
+
import cv2
|
| 346 |
+
import numpy as np
|
| 347 |
+
|
| 348 |
+
# Create output directory if it doesn't exist
|
| 349 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 350 |
+
|
| 351 |
+
# Load images if paths are provided
|
| 352 |
+
if isinstance(image_sequence[0], str):
|
| 353 |
+
loaded_images = []
|
| 354 |
+
for img_path in image_sequence:
|
| 355 |
+
img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE)
|
| 356 |
+
if img is None:
|
| 357 |
+
return f"Error: Could not load image {img_path}"
|
| 358 |
+
loaded_images.append(img)
|
| 359 |
+
frames = np.array(loaded_images)
|
| 360 |
+
else:
|
| 361 |
+
frames = image_sequence
|
| 362 |
+
# Convert to grayscale if needed
|
| 363 |
+
if len(frames.shape) > 3: # Has color channels
|
| 364 |
+
frames = np.array(
|
| 365 |
+
[cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY) if frame.shape[-1] == 3 else frame for frame in frames]
|
| 366 |
+
)
|
| 367 |
+
|
| 368 |
+
# Parameters for optical flow
|
| 369 |
+
lk_params = {
|
| 370 |
+
"winSize": (15, 15),
|
| 371 |
+
"maxLevel": 2,
|
| 372 |
+
"criteria": (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03),
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
# Metrics storage
|
| 376 |
+
num_frames = len(frames)
|
| 377 |
+
flow_fields = []
|
| 378 |
+
divergence_maps = []
|
| 379 |
+
curl_maps = []
|
| 380 |
+
strain_maps = []
|
| 381 |
+
|
| 382 |
+
# Log initialization
|
| 383 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 384 |
+
log = f"Tissue Deformation and Flow Analysis - {timestamp}\n"
|
| 385 |
+
log += f"Number of frames analyzed: {num_frames}\n"
|
| 386 |
+
log += f"Image dimensions: {frames[0].shape[0]}x{frames[0].shape[1]} pixels\n"
|
| 387 |
+
log += f"Pixel scale: {pixel_scale} units/pixel\n\n"
|
| 388 |
+
log += "Analysis Steps:\n"
|
| 389 |
+
|
| 390 |
+
# Create feature points grid (evenly spaced points)
|
| 391 |
+
y, x = np.mgrid[0 : frames[0].shape[0] : 20, 0 : frames[0].shape[1] : 20]
|
| 392 |
+
feature_points = np.stack((x.flatten(), y.flatten()), axis=1).astype(np.float32)
|
| 393 |
+
|
| 394 |
+
for i in range(num_frames - 1):
|
| 395 |
+
log += f"Processing frame pair {i} and {i + 1}...\n"
|
| 396 |
+
|
| 397 |
+
# Calculate optical flow using Lucas-Kanade
|
| 398 |
+
prev_frame = frames[i]
|
| 399 |
+
next_frame = frames[i + 1]
|
| 400 |
+
|
| 401 |
+
# Compute flow for the grid points
|
| 402 |
+
next_points, status, _ = cv2.calcOpticalFlowPyrLK(prev_frame, next_frame, feature_points, None, **lk_params)
|
| 403 |
+
|
| 404 |
+
# Filter valid points
|
| 405 |
+
valid_idx = status.flatten() == 1
|
| 406 |
+
valid_prev_points = feature_points[valid_idx]
|
| 407 |
+
valid_next_points = next_points[valid_idx]
|
| 408 |
+
|
| 409 |
+
# Calculate displacement vectors
|
| 410 |
+
displacement = valid_next_points - valid_prev_points
|
| 411 |
+
points = valid_prev_points
|
| 412 |
+
|
| 413 |
+
# Interpolate flow field to full image resolution
|
| 414 |
+
flow_field = np.zeros((frames[0].shape[0], frames[0].shape[1], 2), dtype=np.float32)
|
| 415 |
+
|
| 416 |
+
# Simple nearest-neighbor interpolation for demonstration
|
| 417 |
+
# In a production system, you might use more sophisticated interpolation
|
| 418 |
+
for j, (x, y) in enumerate(points.astype(int)):
|
| 419 |
+
if 0 <= y < flow_field.shape[0] and 0 <= x < flow_field.shape[1]:
|
| 420 |
+
flow_field[y, x] = displacement[j]
|
| 421 |
+
|
| 422 |
+
# Calculate derivatives for deformation analysis
|
| 423 |
+
u = flow_field[:, :, 0] # x-component of flow
|
| 424 |
+
v = flow_field[:, :, 1] # y-component of flow
|
| 425 |
+
|
| 426 |
+
# Calculate divergence (expansion/contraction)
|
| 427 |
+
# Using central differences for derivatives
|
| 428 |
+
u_x = cv2.Sobel(u, cv2.CV_64F, 1, 0, ksize=3) / (8.0 * pixel_scale)
|
| 429 |
+
v_y = cv2.Sobel(v, cv2.CV_64F, 0, 1, ksize=3) / (8.0 * pixel_scale)
|
| 430 |
+
divergence = u_x + v_y
|
| 431 |
+
|
| 432 |
+
# Calculate curl (rotation)
|
| 433 |
+
u_y = cv2.Sobel(u, cv2.CV_64F, 0, 1, ksize=3) / (8.0 * pixel_scale)
|
| 434 |
+
v_x = cv2.Sobel(v, cv2.CV_64F, 1, 0, ksize=3) / (8.0 * pixel_scale)
|
| 435 |
+
curl = v_x - u_y
|
| 436 |
+
|
| 437 |
+
# Calculate strain tensor components
|
| 438 |
+
strain_xx = u_x
|
| 439 |
+
strain_yy = v_y
|
| 440 |
+
strain_xy = 0.5 * (u_y + v_x)
|
| 441 |
+
|
| 442 |
+
# Magnitude of strain tensor (Frobenius norm)
|
| 443 |
+
strain_magnitude = np.sqrt(strain_xx**2 + strain_yy**2 + 2 * strain_xy**2)
|
| 444 |
+
|
| 445 |
+
# Store results
|
| 446 |
+
flow_fields.append(flow_field)
|
| 447 |
+
divergence_maps.append(divergence)
|
| 448 |
+
curl_maps.append(curl)
|
| 449 |
+
strain_maps.append(strain_magnitude)
|
| 450 |
+
|
| 451 |
+
# Visualize flow field
|
| 452 |
+
flow_viz = np.zeros((frames[0].shape[0], frames[0].shape[1], 3), dtype=np.uint8)
|
| 453 |
+
flow_viz[..., 0] = next_frame # Use next frame as background
|
| 454 |
+
flow_viz[..., 1] = next_frame
|
| 455 |
+
flow_viz[..., 2] = next_frame
|
| 456 |
+
|
| 457 |
+
# Draw flow vectors for visualization
|
| 458 |
+
step = 20
|
| 459 |
+
for y in range(0, flow_field.shape[0], step):
|
| 460 |
+
for x in range(0, flow_field.shape[1], step):
|
| 461 |
+
dx, dy = flow_field[y, x]
|
| 462 |
+
if abs(dx) > 0.5 or abs(dy) > 0.5: # Only draw significant flow
|
| 463 |
+
cv2.arrowedLine(
|
| 464 |
+
flow_viz,
|
| 465 |
+
(x, y),
|
| 466 |
+
(int(x + dx), int(y + dy)),
|
| 467 |
+
(0, 255, 0), # Green
|
| 468 |
+
1,
|
| 469 |
+
tipLength=0.3,
|
| 470 |
+
)
|
| 471 |
+
|
| 472 |
+
# Save visualizations
|
| 473 |
+
flow_viz_path = os.path.join(output_dir, f"flow_viz_{i:03d}.png")
|
| 474 |
+
divergence_path = os.path.join(output_dir, f"divergence_{i:03d}.npy")
|
| 475 |
+
curl_path = os.path.join(output_dir, f"curl_{i:03d}.npy")
|
| 476 |
+
strain_path = os.path.join(output_dir, f"strain_{i:03d}.npy")
|
| 477 |
+
|
| 478 |
+
cv2.imwrite(flow_viz_path, flow_viz)
|
| 479 |
+
np.save(divergence_path, divergence)
|
| 480 |
+
np.save(curl_path, curl)
|
| 481 |
+
np.save(strain_path, strain_magnitude)
|
| 482 |
+
|
| 483 |
+
# Calculate summary statistics
|
| 484 |
+
mean_divergence = np.mean([np.mean(div) for div in divergence_maps])
|
| 485 |
+
max_divergence = np.max([np.max(div) for div in divergence_maps])
|
| 486 |
+
mean_curl = np.mean([np.mean(np.abs(c)) for c in curl_maps])
|
| 487 |
+
mean_strain = np.mean([np.mean(s) for s in strain_maps])
|
| 488 |
+
|
| 489 |
+
# Add summary to log
|
| 490 |
+
log += "\nAnalysis Results:\n"
|
| 491 |
+
log += f"Mean tissue divergence: {mean_divergence:.6f} (expansion/contraction rate)\n"
|
| 492 |
+
log += f"Maximum divergence: {max_divergence:.6f}\n"
|
| 493 |
+
log += f"Mean absolute curl: {mean_curl:.6f} (rotation rate)\n"
|
| 494 |
+
log += f"Mean strain magnitude: {mean_strain:.6f} (deformation intensity)\n\n"
|
| 495 |
+
|
| 496 |
+
# Save summary data
|
| 497 |
+
summary_data = {
|
| 498 |
+
"mean_divergence": mean_divergence,
|
| 499 |
+
"max_divergence": max_divergence,
|
| 500 |
+
"mean_curl": mean_curl,
|
| 501 |
+
"mean_strain": mean_strain,
|
| 502 |
+
}
|
| 503 |
+
summary_path = os.path.join(output_dir, "deformation_summary.npy")
|
| 504 |
+
np.save(summary_path, summary_data)
|
| 505 |
+
|
| 506 |
+
log += "Files Generated:\n"
|
| 507 |
+
log += f"- Flow visualization images: {output_dir}/flow_viz_*.png\n"
|
| 508 |
+
log += f"- Divergence maps: {output_dir}/divergence_*.npy\n"
|
| 509 |
+
log += f"- Curl maps: {output_dir}/curl_*.npy\n"
|
| 510 |
+
log += f"- Strain maps: {output_dir}/strain_*.npy\n"
|
| 511 |
+
log += f"- Summary statistics: {output_dir}/deformation_summary.npy\n"
|
| 512 |
+
|
| 513 |
+
return log
|
Biomni/biomni/tool/cell_biology.py
ADDED
|
@@ -0,0 +1,742 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
def quantify_cell_cycle_phases_from_microscopy(image_paths, output_dir="./results"):
|
| 2 |
+
"""Quantify the percentage of cells in each cell cycle phase using Calcofluor white stained microscopy images.
|
| 3 |
+
|
| 4 |
+
This function processes microscopy images where cell walls/septa are stained with Calcofluor white,
|
| 5 |
+
segments individual cells, extracts features, and classifies each cell into G1, S, or G2/M phase.
|
| 6 |
+
|
| 7 |
+
Parameters
|
| 8 |
+
----------
|
| 9 |
+
image_paths : list of str
|
| 10 |
+
List of file paths to microscopy images of cells stained with Calcofluor white
|
| 11 |
+
output_dir : str, optional
|
| 12 |
+
Directory to save results (default: './results')
|
| 13 |
+
|
| 14 |
+
Returns
|
| 15 |
+
-------
|
| 16 |
+
str
|
| 17 |
+
Research log summarizing the analysis process and results
|
| 18 |
+
|
| 19 |
+
"""
|
| 20 |
+
import os
|
| 21 |
+
|
| 22 |
+
import numpy as np
|
| 23 |
+
import pandas as pd
|
| 24 |
+
from skimage import filters, io, measure, morphology
|
| 25 |
+
|
| 26 |
+
# Create output directory if it doesn't exist
|
| 27 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 28 |
+
|
| 29 |
+
# Initialize log
|
| 30 |
+
log = "# Cell Cycle Phase Quantification Analysis Log\n\n"
|
| 31 |
+
log += f"Processing {len(image_paths)} microscopy images with Calcofluor white staining.\n\n"
|
| 32 |
+
|
| 33 |
+
# Initialize data structures to store results
|
| 34 |
+
all_cells = []
|
| 35 |
+
cell_features = []
|
| 36 |
+
|
| 37 |
+
# Step 1: Process images and segment cells
|
| 38 |
+
log += "## Image Processing and Cell Segmentation\n\n"
|
| 39 |
+
|
| 40 |
+
for i, img_path in enumerate(image_paths):
|
| 41 |
+
log += f"Processing image {i + 1}/{len(image_paths)}: {os.path.basename(img_path)}\n"
|
| 42 |
+
|
| 43 |
+
# Load image
|
| 44 |
+
img = io.imread(img_path)
|
| 45 |
+
|
| 46 |
+
# Convert to grayscale if needed
|
| 47 |
+
if len(img.shape) > 2:
|
| 48 |
+
img = np.mean(img, axis=2).astype(np.uint8)
|
| 49 |
+
|
| 50 |
+
# Preprocessing
|
| 51 |
+
filtered_img = filters.gaussian(img, sigma=1.0)
|
| 52 |
+
|
| 53 |
+
# Thresholding to identify cell walls
|
| 54 |
+
threshold = filters.threshold_otsu(filtered_img)
|
| 55 |
+
binary = filtered_img > threshold
|
| 56 |
+
|
| 57 |
+
# Clean up binary image
|
| 58 |
+
binary = morphology.remove_small_objects(binary, min_size=30)
|
| 59 |
+
binary = morphology.binary_closing(binary)
|
| 60 |
+
|
| 61 |
+
# Segment cells
|
| 62 |
+
labeled_cells = measure.label(binary)
|
| 63 |
+
cell_props = measure.regionprops(labeled_cells, intensity_image=img)
|
| 64 |
+
|
| 65 |
+
log += f" - Found {len(cell_props)} cells\n"
|
| 66 |
+
all_cells.extend(cell_props)
|
| 67 |
+
|
| 68 |
+
# Extract features for each cell
|
| 69 |
+
for cell in cell_props:
|
| 70 |
+
# Basic morphological features
|
| 71 |
+
area = cell.area
|
| 72 |
+
perimeter = cell.perimeter
|
| 73 |
+
eccentricity = cell.eccentricity
|
| 74 |
+
mean_intensity = cell.mean_intensity
|
| 75 |
+
|
| 76 |
+
# Cell shape features
|
| 77 |
+
circularity = (4 * np.pi * area) / (perimeter**2) if perimeter > 0 else 0
|
| 78 |
+
|
| 79 |
+
# Check for septation (indicative of S or G2/M phase)
|
| 80 |
+
# Calcofluor white stains the septum more intensely
|
| 81 |
+
has_septum = False
|
| 82 |
+
if area > 50: # Only check larger cells
|
| 83 |
+
intensity_profile = cell.intensity_image[cell.intensity_image > 0]
|
| 84 |
+
if intensity_profile.size > 0:
|
| 85 |
+
intensity_std = np.std(intensity_profile)
|
| 86 |
+
intensity_max = np.max(intensity_profile)
|
| 87 |
+
# High standard deviation and max intensity suggests septum presence
|
| 88 |
+
has_septum = intensity_std > 0.2 * np.mean(intensity_profile) and intensity_max > 1.5 * np.mean(
|
| 89 |
+
intensity_profile
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
# Store features
|
| 93 |
+
cell_features.append(
|
| 94 |
+
[
|
| 95 |
+
area,
|
| 96 |
+
perimeter,
|
| 97 |
+
eccentricity,
|
| 98 |
+
mean_intensity,
|
| 99 |
+
circularity,
|
| 100 |
+
has_septum,
|
| 101 |
+
i, # include image index
|
| 102 |
+
]
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
log += f"\nTotal cells detected across all images: {len(all_cells)}\n\n"
|
| 106 |
+
|
| 107 |
+
# Step 2: Feature engineering and classification
|
| 108 |
+
log += "## Cell Cycle Phase Classification\n\n"
|
| 109 |
+
|
| 110 |
+
# Convert to numpy array for easier processing
|
| 111 |
+
X = np.array(cell_features)
|
| 112 |
+
|
| 113 |
+
# Simple rule-based classification
|
| 114 |
+
# This is a simplified approach - in practice, you would train a classifier on labeled data
|
| 115 |
+
# G1: Smaller cells, no septum
|
| 116 |
+
# S: Medium-sized cells, early septum formation
|
| 117 |
+
# G2/M: Larger cells, clear septum, often elongated
|
| 118 |
+
|
| 119 |
+
# Define classification rules based on features
|
| 120 |
+
phases = []
|
| 121 |
+
for i in range(X.shape[0]):
|
| 122 |
+
area = X[i, 0]
|
| 123 |
+
perimeter = X[i, 1]
|
| 124 |
+
eccentricity = X[i, 2]
|
| 125 |
+
has_septum = X[i, 5]
|
| 126 |
+
|
| 127 |
+
if has_septum and area > np.median(X[:, 0]) * 1.2:
|
| 128 |
+
# Larger cells with septum are likely in G2/M
|
| 129 |
+
phases.append("G2/M")
|
| 130 |
+
elif has_septum or (area > np.median(X[:, 0]) and eccentricity > 0.5):
|
| 131 |
+
# Cells with septum or larger elongated cells are likely in S
|
| 132 |
+
phases.append("S")
|
| 133 |
+
else:
|
| 134 |
+
# Smaller cells without septum are likely in G1
|
| 135 |
+
phases.append("G1")
|
| 136 |
+
|
| 137 |
+
# Calculate percentages
|
| 138 |
+
unique_phases, counts = np.unique(phases, return_counts=True)
|
| 139 |
+
percentages = (counts / len(phases)) * 100
|
| 140 |
+
|
| 141 |
+
# Create results table
|
| 142 |
+
results_df = pd.DataFrame({"Phase": unique_phases, "Count": counts, "Percentage": percentages})
|
| 143 |
+
|
| 144 |
+
# Save results
|
| 145 |
+
results_path = os.path.join(output_dir, "cell_cycle_phases.csv")
|
| 146 |
+
results_df.to_csv(results_path, index=False)
|
| 147 |
+
|
| 148 |
+
# Log results
|
| 149 |
+
log += "### Cell Cycle Phase Distribution\n\n"
|
| 150 |
+
for phase, count, percentage in zip(unique_phases, counts, percentages, strict=False):
|
| 151 |
+
log += f"- {phase}: {count} cells ({percentage:.2f}%)\n"
|
| 152 |
+
|
| 153 |
+
log += f"\nResults saved to: {results_path}\n"
|
| 154 |
+
log += "\nNote: This analysis uses simplified morphological features for classification. "
|
| 155 |
+
log += (
|
| 156 |
+
"For more accurate results, a supervised machine learning approach with labeled training data is recommended.\n"
|
| 157 |
+
)
|
| 158 |
+
|
| 159 |
+
return log
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def quantify_and_cluster_cell_motility(image_sequence_path, output_dir="./results", num_clusters=3):
|
| 163 |
+
"""Quantify cell motility features from time-lapse microscopy images and cluster cells based on motility patterns.
|
| 164 |
+
|
| 165 |
+
Parameters
|
| 166 |
+
----------
|
| 167 |
+
image_sequence_path : str
|
| 168 |
+
Path to directory containing time-lapse microscopy images in sequential order
|
| 169 |
+
output_dir : str
|
| 170 |
+
Directory to save output files (default: "./results")
|
| 171 |
+
num_clusters : int
|
| 172 |
+
Number of motility pattern clusters to identify (default: 3)
|
| 173 |
+
|
| 174 |
+
Returns
|
| 175 |
+
-------
|
| 176 |
+
str
|
| 177 |
+
Research log summarizing the analysis process and results
|
| 178 |
+
|
| 179 |
+
"""
|
| 180 |
+
import os
|
| 181 |
+
|
| 182 |
+
import cv2
|
| 183 |
+
import numpy as np
|
| 184 |
+
import pandas as pd
|
| 185 |
+
from sklearn.cluster import KMeans
|
| 186 |
+
|
| 187 |
+
# Create output directory if it doesn't exist
|
| 188 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 189 |
+
|
| 190 |
+
# Step 1: Load image sequence
|
| 191 |
+
image_files = sorted(
|
| 192 |
+
[f for f in os.listdir(image_sequence_path) if f.endswith((".tif", ".tiff", ".png", ".jpg", ".jpeg"))]
|
| 193 |
+
)
|
| 194 |
+
|
| 195 |
+
if len(image_files) < 2:
|
| 196 |
+
return "Error: Insufficient images found. At least 2 time points are required."
|
| 197 |
+
|
| 198 |
+
# Step 2: Initialize cell tracking
|
| 199 |
+
first_image = cv2.imread(os.path.join(image_sequence_path, image_files[0]), cv2.IMREAD_GRAYSCALE)
|
| 200 |
+
|
| 201 |
+
# Simple cell detection using thresholding and contour detection
|
| 202 |
+
_, binary = cv2.threshold(first_image, 127, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
|
| 203 |
+
contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
| 204 |
+
|
| 205 |
+
# Extract cell centroids from first frame
|
| 206 |
+
cells = []
|
| 207 |
+
for i, contour in enumerate(contours):
|
| 208 |
+
if cv2.contourArea(contour) > 50: # Filter small noise
|
| 209 |
+
M = cv2.moments(contour)
|
| 210 |
+
if M["m00"] != 0:
|
| 211 |
+
cx = int(M["m10"] / M["m00"])
|
| 212 |
+
cy = int(M["m01"] / M["m00"])
|
| 213 |
+
cells.append({"id": i, "positions": [(cx, cy)], "frame_indices": [0]})
|
| 214 |
+
|
| 215 |
+
# Step 3: Track cells across frames
|
| 216 |
+
for frame_idx, img_file in enumerate(image_files[1:], 1):
|
| 217 |
+
img = cv2.imread(os.path.join(image_sequence_path, img_file), cv2.IMREAD_GRAYSCALE)
|
| 218 |
+
_, binary = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
|
| 219 |
+
contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
| 220 |
+
|
| 221 |
+
# Get current frame centroids
|
| 222 |
+
current_centroids = []
|
| 223 |
+
for contour in contours:
|
| 224 |
+
if cv2.contourArea(contour) > 50:
|
| 225 |
+
M = cv2.moments(contour)
|
| 226 |
+
if M["m00"] != 0:
|
| 227 |
+
cx = int(M["m10"] / M["m00"])
|
| 228 |
+
cy = int(M["m01"] / M["m00"])
|
| 229 |
+
current_centroids.append((cx, cy))
|
| 230 |
+
|
| 231 |
+
# Match cells with nearest centroids in current frame
|
| 232 |
+
for cell in cells:
|
| 233 |
+
if len(cell["positions"]) == frame_idx: # Only track cells found in previous frame
|
| 234 |
+
prev_pos = cell["positions"][-1]
|
| 235 |
+
|
| 236 |
+
if current_centroids:
|
| 237 |
+
# Calculate distances to all current centroids
|
| 238 |
+
distances = [
|
| 239 |
+
np.sqrt((prev_pos[0] - c[0]) ** 2 + (prev_pos[1] - c[1]) ** 2) for c in current_centroids
|
| 240 |
+
]
|
| 241 |
+
min_idx = np.argmin(distances)
|
| 242 |
+
|
| 243 |
+
# Only match if distance is below threshold (prevents incorrect matching)
|
| 244 |
+
if distances[min_idx] < 50: # Threshold distance in pixels
|
| 245 |
+
cell["positions"].append(current_centroids[min_idx])
|
| 246 |
+
cell["frame_indices"].append(frame_idx)
|
| 247 |
+
current_centroids.pop(min_idx) # Remove matched centroid
|
| 248 |
+
|
| 249 |
+
# Step 4: Calculate motility features for each cell
|
| 250 |
+
cell_features = []
|
| 251 |
+
|
| 252 |
+
for cell in cells:
|
| 253 |
+
# Only analyze cells tracked for at least 3 frames
|
| 254 |
+
if len(cell["positions"]) < 3:
|
| 255 |
+
continue
|
| 256 |
+
|
| 257 |
+
# Calculate displacements between consecutive positions
|
| 258 |
+
displacements = []
|
| 259 |
+
for i in range(1, len(cell["positions"])):
|
| 260 |
+
prev_pos = cell["positions"][i - 1]
|
| 261 |
+
curr_pos = cell["positions"][i]
|
| 262 |
+
displacement = np.sqrt((curr_pos[0] - prev_pos[0]) ** 2 + (curr_pos[1] - prev_pos[1]) ** 2)
|
| 263 |
+
displacements.append(displacement)
|
| 264 |
+
|
| 265 |
+
# Calculate speed (pixels per frame)
|
| 266 |
+
avg_speed = np.mean(displacements)
|
| 267 |
+
|
| 268 |
+
# Calculate directionality (displacement from start to end / total path length)
|
| 269 |
+
start_pos = cell["positions"][0]
|
| 270 |
+
end_pos = cell["positions"][-1]
|
| 271 |
+
net_displacement = np.sqrt((end_pos[0] - start_pos[0]) ** 2 + (end_pos[1] - start_pos[1]) ** 2)
|
| 272 |
+
total_path_length = np.sum(displacements)
|
| 273 |
+
directionality = net_displacement / total_path_length if total_path_length > 0 else 0
|
| 274 |
+
|
| 275 |
+
# Calculate mean squared displacement
|
| 276 |
+
msd = np.mean(
|
| 277 |
+
[
|
| 278 |
+
np.sqrt((cell["positions"][i][0] - start_pos[0]) ** 2 + (cell["positions"][i][1] - start_pos[1]) ** 2)
|
| 279 |
+
for i in range(1, len(cell["positions"]))
|
| 280 |
+
]
|
| 281 |
+
)
|
| 282 |
+
|
| 283 |
+
# Calculate track duration
|
| 284 |
+
duration = cell["frame_indices"][-1] - cell["frame_indices"][0]
|
| 285 |
+
|
| 286 |
+
cell_features.append(
|
| 287 |
+
{
|
| 288 |
+
"cell_id": cell["id"],
|
| 289 |
+
"avg_speed": avg_speed,
|
| 290 |
+
"directionality": directionality,
|
| 291 |
+
"msd": msd,
|
| 292 |
+
"track_duration": duration,
|
| 293 |
+
"track_length": len(cell["positions"]),
|
| 294 |
+
}
|
| 295 |
+
)
|
| 296 |
+
|
| 297 |
+
# Step 5: Cluster cells based on motility features
|
| 298 |
+
if len(cell_features) < num_clusters:
|
| 299 |
+
return f"Error: Not enough cells ({len(cell_features)}) to form {num_clusters} clusters. Try reducing num_clusters."
|
| 300 |
+
|
| 301 |
+
# Create feature matrix for clustering
|
| 302 |
+
feature_df = pd.DataFrame(cell_features)
|
| 303 |
+
feature_matrix = feature_df[["avg_speed", "directionality", "msd"]].values
|
| 304 |
+
|
| 305 |
+
# Normalize features
|
| 306 |
+
feature_matrix = (feature_matrix - np.mean(feature_matrix, axis=0)) / np.std(feature_matrix, axis=0)
|
| 307 |
+
|
| 308 |
+
# Perform k-means clustering
|
| 309 |
+
kmeans = KMeans(n_clusters=num_clusters, random_state=42)
|
| 310 |
+
clusters = kmeans.fit_predict(feature_matrix)
|
| 311 |
+
|
| 312 |
+
# Add cluster assignments to dataframe
|
| 313 |
+
feature_df["cluster"] = clusters
|
| 314 |
+
|
| 315 |
+
# Save results to CSV
|
| 316 |
+
results_file = os.path.join(output_dir, "cell_motility_features.csv")
|
| 317 |
+
feature_df.to_csv(results_file, index=False)
|
| 318 |
+
|
| 319 |
+
# Calculate cluster statistics
|
| 320 |
+
cluster_stats = feature_df.groupby("cluster").agg(
|
| 321 |
+
{
|
| 322 |
+
"avg_speed": ["mean", "std"],
|
| 323 |
+
"directionality": ["mean", "std"],
|
| 324 |
+
"msd": ["mean", "std"],
|
| 325 |
+
"cell_id": "count",
|
| 326 |
+
}
|
| 327 |
+
)
|
| 328 |
+
|
| 329 |
+
# Save cluster statistics
|
| 330 |
+
stats_file = os.path.join(output_dir, "cluster_statistics.csv")
|
| 331 |
+
cluster_stats.to_csv(stats_file)
|
| 332 |
+
|
| 333 |
+
# Create research log
|
| 334 |
+
log = f"""
|
| 335 |
+
Cell Motility Quantification and Clustering Analysis
|
| 336 |
+
===================================================
|
| 337 |
+
|
| 338 |
+
Analysis Steps:
|
| 339 |
+
1. Loaded {len(image_files)} time-lapse microscopy images
|
| 340 |
+
2. Detected and tracked {len(cells)} initial cells across time frames
|
| 341 |
+
3. Calculated motility features for {len(cell_features)} cells with sufficient tracking data
|
| 342 |
+
4. Performed k-means clustering to identify {num_clusters} distinct motility patterns
|
| 343 |
+
|
| 344 |
+
Results Summary:
|
| 345 |
+
- Detected {len(cells)} cells in the first frame
|
| 346 |
+
- Successfully tracked {len(cell_features)} cells across multiple frames
|
| 347 |
+
- Clustered cells into {num_clusters} motility pattern groups
|
| 348 |
+
|
| 349 |
+
Cluster Statistics:
|
| 350 |
+
{cluster_stats.to_string()}
|
| 351 |
+
|
| 352 |
+
Output Files:
|
| 353 |
+
- Cell motility features saved to: {results_file}
|
| 354 |
+
- Cluster statistics saved to: {stats_file}
|
| 355 |
+
|
| 356 |
+
Analysis Notes:
|
| 357 |
+
- Features used for clustering: average speed, directionality, and mean squared displacement
|
| 358 |
+
- Cells were required to be tracked for at least 3 frames to be included in the analysis
|
| 359 |
+
"""
|
| 360 |
+
|
| 361 |
+
# Save research log
|
| 362 |
+
log_file = os.path.join(output_dir, "research_log.txt")
|
| 363 |
+
with open(log_file, "w") as f:
|
| 364 |
+
f.write(log)
|
| 365 |
+
|
| 366 |
+
return log
|
| 367 |
+
|
| 368 |
+
|
| 369 |
+
def perform_facs_cell_sorting(
|
| 370 |
+
cell_suspension_data,
|
| 371 |
+
fluorescence_parameter,
|
| 372 |
+
threshold_min=None,
|
| 373 |
+
threshold_max=None,
|
| 374 |
+
output_file="sorted_cells.csv",
|
| 375 |
+
):
|
| 376 |
+
"""Performs Fluorescence-Activated Cell Sorting (FACS) to enrich cell populations based on fluorescence characteristics.
|
| 377 |
+
|
| 378 |
+
Parameters
|
| 379 |
+
----------
|
| 380 |
+
cell_suspension_data : str
|
| 381 |
+
Path to the FCS file containing flow cytometry data
|
| 382 |
+
fluorescence_parameter : str
|
| 383 |
+
The fluorescence parameter to use for sorting (e.g., 'GFP', 'FITC', 'PE')
|
| 384 |
+
threshold_min : float, optional
|
| 385 |
+
Minimum threshold for the fluorescence parameter. Cells below this value will be excluded
|
| 386 |
+
threshold_max : float, optional
|
| 387 |
+
Maximum threshold for the fluorescence parameter. Cells above this value will be excluded
|
| 388 |
+
output_file : str, optional
|
| 389 |
+
Filename to save the sorted cell population data
|
| 390 |
+
|
| 391 |
+
Returns
|
| 392 |
+
-------
|
| 393 |
+
str
|
| 394 |
+
Research log detailing the FACS cell sorting process
|
| 395 |
+
|
| 396 |
+
"""
|
| 397 |
+
import os
|
| 398 |
+
|
| 399 |
+
import pandas as pd
|
| 400 |
+
|
| 401 |
+
# Initialize research log
|
| 402 |
+
log = "# FACS-based Cell Sorting and Enrichment Research Log\n\n"
|
| 403 |
+
|
| 404 |
+
try:
|
| 405 |
+
# Load cell suspension data
|
| 406 |
+
log += "## Loading Cell Suspension Data\n"
|
| 407 |
+
if isinstance(cell_suspension_data, str):
|
| 408 |
+
# If data is provided as a file path
|
| 409 |
+
if cell_suspension_data.lower().endswith(".fcs"):
|
| 410 |
+
try:
|
| 411 |
+
import flowkit as fk
|
| 412 |
+
|
| 413 |
+
fcs_data = fk.Sample(cell_suspension_data)
|
| 414 |
+
cell_df = pd.DataFrame(fcs_data.get_dataframe())
|
| 415 |
+
log += f"Successfully loaded FCS file containing {len(cell_df)} cells\n"
|
| 416 |
+
except ImportError:
|
| 417 |
+
# Fallback if flowkit is not available
|
| 418 |
+
log += "FlowKit not available, attempting to load as CSV\n"
|
| 419 |
+
cell_df = pd.read_csv(cell_suspension_data)
|
| 420 |
+
else:
|
| 421 |
+
# Assume CSV or other tabular format
|
| 422 |
+
cell_df = pd.read_csv(cell_suspension_data)
|
| 423 |
+
log += f"Loaded data file containing {len(cell_df)} cells\n"
|
| 424 |
+
else:
|
| 425 |
+
# Assume data is already a DataFrame
|
| 426 |
+
cell_df = cell_suspension_data.copy()
|
| 427 |
+
log += f"Using provided DataFrame containing {len(cell_df)} cells\n"
|
| 428 |
+
|
| 429 |
+
# Validate fluorescence parameter exists in data
|
| 430 |
+
if fluorescence_parameter not in cell_df.columns:
|
| 431 |
+
raise ValueError(f"Fluorescence parameter '{fluorescence_parameter}' not found in data")
|
| 432 |
+
|
| 433 |
+
# Apply gating strategy based on fluorescence thresholds
|
| 434 |
+
log += f"\n## Applying Gating Strategy for {fluorescence_parameter}\n"
|
| 435 |
+
original_count = len(cell_df)
|
| 436 |
+
|
| 437 |
+
# Filter cells based on min/max thresholds
|
| 438 |
+
if threshold_min is not None:
|
| 439 |
+
cell_df = cell_df[cell_df[fluorescence_parameter] >= threshold_min]
|
| 440 |
+
log += f"Applied minimum threshold of {threshold_min}: {len(cell_df)} cells remaining\n"
|
| 441 |
+
|
| 442 |
+
if threshold_max is not None:
|
| 443 |
+
cell_df = cell_df[cell_df[fluorescence_parameter] <= threshold_max]
|
| 444 |
+
log += f"Applied maximum threshold of {threshold_max}: {len(cell_df)} cells remaining\n"
|
| 445 |
+
|
| 446 |
+
# Calculate enrichment statistics
|
| 447 |
+
enrichment_percent = (len(cell_df) / original_count) * 100 if original_count > 0 else 0
|
| 448 |
+
log += "\n## Enrichment Results\n"
|
| 449 |
+
log += f"Original population: {original_count} cells\n"
|
| 450 |
+
log += f"Sorted population: {len(cell_df)} cells\n"
|
| 451 |
+
log += f"Enrichment percentage: {enrichment_percent:.2f}%\n"
|
| 452 |
+
|
| 453 |
+
# Calculate statistics of sorted population
|
| 454 |
+
mean_fluorescence = cell_df[fluorescence_parameter].mean()
|
| 455 |
+
median_fluorescence = cell_df[fluorescence_parameter].median()
|
| 456 |
+
|
| 457 |
+
log += f"Mean {fluorescence_parameter}: {mean_fluorescence:.2f}\n"
|
| 458 |
+
log += f"Median {fluorescence_parameter}: {median_fluorescence:.2f}\n"
|
| 459 |
+
|
| 460 |
+
# Save sorted population to file
|
| 461 |
+
cell_df.to_csv(output_file, index=False)
|
| 462 |
+
log += "\n## Output\n"
|
| 463 |
+
log += f"Sorted cell population saved to: {os.path.abspath(output_file)}\n"
|
| 464 |
+
|
| 465 |
+
return log
|
| 466 |
+
|
| 467 |
+
except Exception as e:
|
| 468 |
+
log += "\n## Error\n"
|
| 469 |
+
log += f"An error occurred during cell sorting: {str(e)}\n"
|
| 470 |
+
return log
|
| 471 |
+
|
| 472 |
+
|
| 473 |
+
def analyze_flow_cytometry_immunophenotyping(
|
| 474 |
+
fcs_file_path, gating_strategy, compensation_matrix=None, output_dir="./results"
|
| 475 |
+
):
|
| 476 |
+
"""Analyze flow cytometry data to identify and quantify specific cell populations based on surface markers.
|
| 477 |
+
|
| 478 |
+
Parameters
|
| 479 |
+
----------
|
| 480 |
+
fcs_file_path : str
|
| 481 |
+
Path to the FCS file containing flow cytometry data
|
| 482 |
+
gating_strategy : dict
|
| 483 |
+
Dictionary defining the gating strategy. Each key is a population name, and each value is a list of tuples
|
| 484 |
+
(marker, operator, threshold). For example: {'HSCs': [('Lin', '<', 100), ('Sca1', '>', 1000), ...]}
|
| 485 |
+
compensation_matrix : numpy.ndarray, optional
|
| 486 |
+
Spillover/compensation matrix to correct for fluorescence overlap
|
| 487 |
+
output_dir : str, optional
|
| 488 |
+
Directory to save the results
|
| 489 |
+
|
| 490 |
+
Returns
|
| 491 |
+
-------
|
| 492 |
+
str
|
| 493 |
+
Research log summarizing the analysis steps and results
|
| 494 |
+
|
| 495 |
+
"""
|
| 496 |
+
import os
|
| 497 |
+
|
| 498 |
+
import pandas as pd
|
| 499 |
+
from FlowCytometryTools import FCMeasurement
|
| 500 |
+
|
| 501 |
+
# Create output directory if it doesn't exist
|
| 502 |
+
if not os.path.exists(output_dir):
|
| 503 |
+
os.makedirs(output_dir)
|
| 504 |
+
|
| 505 |
+
# Load the FCS file
|
| 506 |
+
sample = FCMeasurement(ID="Sample", datafile=fcs_file_path)
|
| 507 |
+
|
| 508 |
+
# Apply compensation if provided
|
| 509 |
+
if compensation_matrix is not None:
|
| 510 |
+
sample = sample.compensate(compensation_matrix)
|
| 511 |
+
|
| 512 |
+
# Initialize log
|
| 513 |
+
log = "Flow Cytometry Analysis Log\n"
|
| 514 |
+
log += "==========================\n"
|
| 515 |
+
log += f"File analyzed: {os.path.basename(fcs_file_path)}\n"
|
| 516 |
+
log += f"Total events: {len(sample)}\n\n"
|
| 517 |
+
|
| 518 |
+
# Create a dictionary to store population counts
|
| 519 |
+
populations = {}
|
| 520 |
+
|
| 521 |
+
# Apply gating strategy to identify cell populations
|
| 522 |
+
for pop_name, gates in gating_strategy.items():
|
| 523 |
+
# Start with all events
|
| 524 |
+
current_population = sample.copy()
|
| 525 |
+
|
| 526 |
+
log += f"Identifying {pop_name} population:\n"
|
| 527 |
+
|
| 528 |
+
# Apply each gate sequentially
|
| 529 |
+
for marker, operator, threshold in gates:
|
| 530 |
+
initial_count = len(current_population)
|
| 531 |
+
|
| 532 |
+
if operator == ">":
|
| 533 |
+
current_population = current_population.gate(f"{marker} > {threshold}")
|
| 534 |
+
elif operator == "<":
|
| 535 |
+
current_population = current_population.gate(f"{marker} < {threshold}")
|
| 536 |
+
elif operator == "between":
|
| 537 |
+
# For 'between', threshold should be a tuple (lower, upper)
|
| 538 |
+
lower, upper = threshold
|
| 539 |
+
current_population = current_population.gate(f"{marker} > {lower} and {marker} < {upper}")
|
| 540 |
+
|
| 541 |
+
final_count = len(current_population)
|
| 542 |
+
log += f" Gate {marker} {operator} {threshold}: {initial_count} → {final_count} events\n"
|
| 543 |
+
|
| 544 |
+
# Store the final population
|
| 545 |
+
populations[pop_name] = current_population
|
| 546 |
+
|
| 547 |
+
# Calculate percentage of original sample
|
| 548 |
+
percentage = (len(current_population) / len(sample)) * 100
|
| 549 |
+
log += f" Final {pop_name} count: {len(current_population)} events ({percentage:.2f}% of total)\n\n"
|
| 550 |
+
|
| 551 |
+
# Create a summary dataframe
|
| 552 |
+
summary_data = {"Population": [], "Count": [], "Percentage": []}
|
| 553 |
+
|
| 554 |
+
for pop_name, population in populations.items():
|
| 555 |
+
summary_data["Population"].append(pop_name)
|
| 556 |
+
summary_data["Count"].append(len(population))
|
| 557 |
+
summary_data["Percentage"].append((len(population) / len(sample)) * 100)
|
| 558 |
+
|
| 559 |
+
summary_df = pd.DataFrame(summary_data)
|
| 560 |
+
|
| 561 |
+
# Save the summary to a CSV file
|
| 562 |
+
summary_file = os.path.join(output_dir, "population_summary.csv")
|
| 563 |
+
summary_df.to_csv(summary_file, index=False)
|
| 564 |
+
|
| 565 |
+
log += "Summary of identified populations:\n"
|
| 566 |
+
for _, row in summary_df.iterrows():
|
| 567 |
+
log += f" {row['Population']}: {row['Count']} events ({row['Percentage']:.2f}%)\n"
|
| 568 |
+
|
| 569 |
+
log += f"\nDetailed results saved to: {summary_file}\n"
|
| 570 |
+
|
| 571 |
+
return log
|
| 572 |
+
|
| 573 |
+
|
| 574 |
+
def analyze_mitochondrial_morphology_and_potential(morphology_image_path, potential_image_path, output_dir="./output"):
|
| 575 |
+
"""Quantifies metrics of mitochondrial morphology and membrane potential from fluorescence microscopy images.
|
| 576 |
+
|
| 577 |
+
Parameters
|
| 578 |
+
----------
|
| 579 |
+
morphology_image_path : str
|
| 580 |
+
Path to the fluorescence microscopy image showing mitochondrial morphology (e.g., MTS-GFP)
|
| 581 |
+
potential_image_path : str
|
| 582 |
+
Path to the fluorescence microscopy image showing mitochondrial membrane potential (e.g., TMRE staining)
|
| 583 |
+
output_dir : str, optional
|
| 584 |
+
Directory to save output files, default is "./output"
|
| 585 |
+
|
| 586 |
+
Returns
|
| 587 |
+
-------
|
| 588 |
+
str
|
| 589 |
+
A research log summarizing the analysis steps and results
|
| 590 |
+
|
| 591 |
+
"""
|
| 592 |
+
import datetime
|
| 593 |
+
import os
|
| 594 |
+
|
| 595 |
+
import cv2
|
| 596 |
+
import numpy as np
|
| 597 |
+
from scipy import ndimage
|
| 598 |
+
from skimage import filters, io, morphology, util
|
| 599 |
+
|
| 600 |
+
# Create output directory if it doesn't exist
|
| 601 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 602 |
+
|
| 603 |
+
log = []
|
| 604 |
+
log.append(f"Mitochondrial Analysis - {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
| 605 |
+
log.append("=" * 50)
|
| 606 |
+
|
| 607 |
+
# Load images
|
| 608 |
+
try:
|
| 609 |
+
morph_img = io.imread(morphology_image_path)
|
| 610 |
+
pot_img = io.imread(potential_image_path)
|
| 611 |
+
|
| 612 |
+
log.append(f"Successfully loaded morphology image: {morphology_image_path}")
|
| 613 |
+
log.append(f"Successfully loaded potential image: {potential_image_path}")
|
| 614 |
+
log.append(f"Morphology image shape: {morph_img.shape}")
|
| 615 |
+
log.append(f"Potential image shape: {pot_img.shape}")
|
| 616 |
+
except Exception as e:
|
| 617 |
+
return f"Error loading images: {str(e)}"
|
| 618 |
+
|
| 619 |
+
# Convert to grayscale if needed
|
| 620 |
+
if len(morph_img.shape) > 2:
|
| 621 |
+
morph_img = cv2.cvtColor(morph_img, cv2.COLOR_RGB2GRAY)
|
| 622 |
+
log.append("Converted morphology image to grayscale")
|
| 623 |
+
if len(pot_img.shape) > 2:
|
| 624 |
+
pot_img = cv2.cvtColor(pot_img, cv2.COLOR_RGB2GRAY)
|
| 625 |
+
log.append("Converted potential image to grayscale")
|
| 626 |
+
|
| 627 |
+
# Normalize images
|
| 628 |
+
morph_img = util.img_as_float(morph_img)
|
| 629 |
+
pot_img = util.img_as_float(pot_img)
|
| 630 |
+
|
| 631 |
+
log.append("\nMorphology Analysis:")
|
| 632 |
+
log.append("-" * 30)
|
| 633 |
+
|
| 634 |
+
# Denoise morphology image
|
| 635 |
+
morph_img_denoised = filters.gaussian(morph_img, sigma=1.0)
|
| 636 |
+
|
| 637 |
+
# Threshold to segment mitochondria
|
| 638 |
+
threshold_value = filters.threshold_otsu(morph_img_denoised)
|
| 639 |
+
binary_img = morph_img_denoised > threshold_value
|
| 640 |
+
|
| 641 |
+
# Remove small objects
|
| 642 |
+
binary_img = morphology.remove_small_objects(binary_img, min_size=20)
|
| 643 |
+
|
| 644 |
+
# Save binary image
|
| 645 |
+
binary_path = os.path.join(output_dir, "binary_mitochondria.png")
|
| 646 |
+
io.imsave(binary_path, util.img_as_ubyte(binary_img))
|
| 647 |
+
log.append(f"Binary segmentation saved to: {binary_path}")
|
| 648 |
+
|
| 649 |
+
# Skeletonize for network analysis
|
| 650 |
+
skeleton = morphology.skeletonize(binary_img)
|
| 651 |
+
skeleton_path = os.path.join(output_dir, "skeleton.png")
|
| 652 |
+
io.imsave(skeleton_path, util.img_as_ubyte(skeleton))
|
| 653 |
+
log.append(f"Skeleton image saved to: {skeleton_path}")
|
| 654 |
+
|
| 655 |
+
# Calculate morphology metrics
|
| 656 |
+
# 1. Count branches and junctions
|
| 657 |
+
# Label the skeleton
|
| 658 |
+
labeled_skeleton, num_branches = ndimage.label(skeleton)
|
| 659 |
+
|
| 660 |
+
# Find branch points (pixels with more than 2 neighbors)
|
| 661 |
+
kernel = np.ones((3, 3), dtype=np.uint8)
|
| 662 |
+
kernel[1, 1] = 0 # Don't count the center pixel
|
| 663 |
+
neighbor_count = ndimage.convolve(skeleton.astype(np.uint8), kernel)
|
| 664 |
+
junction_points = (neighbor_count > 2) & skeleton
|
| 665 |
+
num_junctions = np.sum(junction_points)
|
| 666 |
+
|
| 667 |
+
# 2. Calculate fragmentation metrics
|
| 668 |
+
labeled_objects, num_objects = ndimage.label(binary_img)
|
| 669 |
+
object_sizes = ndimage.sum(binary_img, labeled_objects, range(1, num_objects + 1))
|
| 670 |
+
mean_size = np.mean(object_sizes) if len(object_sizes) > 0 else 0
|
| 671 |
+
|
| 672 |
+
# Calculate network connectivity (ratio of junctions to branches)
|
| 673 |
+
connectivity = num_junctions / num_branches if num_branches > 0 else 0
|
| 674 |
+
|
| 675 |
+
# Calculate fragmentation index (inverse of mean object size, normalized)
|
| 676 |
+
fragmentation = 1 / (mean_size / np.max(object_sizes)) if mean_size > 0 and np.max(object_sizes) > 0 else 0
|
| 677 |
+
|
| 678 |
+
log.append(f"Number of mitochondrial fragments: {num_objects}")
|
| 679 |
+
log.append(f"Number of branches: {num_branches}")
|
| 680 |
+
log.append(f"Number of junction points: {num_junctions}")
|
| 681 |
+
log.append(f"Network connectivity index: {connectivity:.4f}")
|
| 682 |
+
log.append(f"Fragmentation index: {fragmentation:.4f}")
|
| 683 |
+
log.append(f"Mean fragment size: {mean_size:.2f} pixels")
|
| 684 |
+
|
| 685 |
+
# Membrane potential analysis
|
| 686 |
+
log.append("\nMembrane Potential Analysis:")
|
| 687 |
+
log.append("-" * 30)
|
| 688 |
+
|
| 689 |
+
# Create mask from morphology image to analyze only mitochondrial regions
|
| 690 |
+
mito_mask = binary_img
|
| 691 |
+
|
| 692 |
+
# Calculate potential metrics within mitochondrial regions
|
| 693 |
+
pot_intensity_raw = np.mean(pot_img)
|
| 694 |
+
pot_intensity_in_mito = np.mean(pot_img[mito_mask]) if np.sum(mito_mask) > 0 else 0
|
| 695 |
+
|
| 696 |
+
# Calculate potential heterogeneity (standard deviation of intensity)
|
| 697 |
+
pot_heterogeneity = np.std(pot_img[mito_mask]) if np.sum(mito_mask) > 0 else 0
|
| 698 |
+
|
| 699 |
+
# Calculate potential distribution
|
| 700 |
+
if np.sum(mito_mask) > 0:
|
| 701 |
+
percentiles = [10, 25, 50, 75, 90]
|
| 702 |
+
pot_percentiles = np.percentile(pot_img[mito_mask], percentiles)
|
| 703 |
+
percentile_str = ", ".join([f"{p}th: {v:.4f}" for p, v in zip(percentiles, pot_percentiles, strict=False)])
|
| 704 |
+
else:
|
| 705 |
+
percentile_str = "No mitochondrial regions detected"
|
| 706 |
+
|
| 707 |
+
log.append(f"Overall mean TMRE intensity: {pot_intensity_raw:.4f}")
|
| 708 |
+
log.append(f"Mean TMRE intensity in mitochondria: {pot_intensity_in_mito:.4f}")
|
| 709 |
+
log.append(f"TMRE intensity heterogeneity (std): {pot_heterogeneity:.4f}")
|
| 710 |
+
log.append(f"TMRE intensity percentiles: {percentile_str}")
|
| 711 |
+
|
| 712 |
+
# Create a combined results image
|
| 713 |
+
# Overlay potential intensity on morphology
|
| 714 |
+
overlay = np.zeros((morph_img.shape[0], morph_img.shape[1], 3), dtype=np.float32)
|
| 715 |
+
overlay[..., 0] = morph_img # Red channel - morphology
|
| 716 |
+
overlay[..., 2] = pot_img # Blue channel - potential
|
| 717 |
+
overlay = np.clip(overlay, 0, 1)
|
| 718 |
+
|
| 719 |
+
overlay_path = os.path.join(output_dir, "morphology_potential_overlay.png")
|
| 720 |
+
io.imsave(overlay_path, util.img_as_ubyte(overlay))
|
| 721 |
+
log.append(f"Overlay image saved to: {overlay_path}")
|
| 722 |
+
|
| 723 |
+
# Save quantitative results to CSV
|
| 724 |
+
import csv
|
| 725 |
+
|
| 726 |
+
results_path = os.path.join(output_dir, "mitochondrial_analysis_results.csv")
|
| 727 |
+
with open(results_path, "w", newline="") as csvfile:
|
| 728 |
+
writer = csv.writer(csvfile)
|
| 729 |
+
writer.writerow(["Metric", "Value"])
|
| 730 |
+
writer.writerow(["Number of fragments", num_objects])
|
| 731 |
+
writer.writerow(["Number of branches", num_branches])
|
| 732 |
+
writer.writerow(["Number of junctions", num_junctions])
|
| 733 |
+
writer.writerow(["Network connectivity", connectivity])
|
| 734 |
+
writer.writerow(["Fragmentation index", fragmentation])
|
| 735 |
+
writer.writerow(["Mean fragment size", mean_size])
|
| 736 |
+
writer.writerow(["Mean TMRE intensity", pot_intensity_in_mito])
|
| 737 |
+
writer.writerow(["TMRE heterogeneity", pot_heterogeneity])
|
| 738 |
+
|
| 739 |
+
log.append(f"Quantitative results saved to: {results_path}")
|
| 740 |
+
log.append("\nAnalysis complete.")
|
| 741 |
+
|
| 742 |
+
return "\n".join(log)
|
Biomni/biomni/tool/database.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
Biomni/biomni/tool/example_mcp_tools/pubmed_mcp.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
|
| 3 |
+
from Bio import Entrez
|
| 4 |
+
from mcp.server.fastmcp import FastMCP
|
| 5 |
+
from pydantic import BaseModel
|
| 6 |
+
|
| 7 |
+
Entrez.email = os.getenv("NCBI_EMAIL") or "YOUR_EMAIL"
|
| 8 |
+
mcp = FastMCP("pubmed-live")
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class Article(BaseModel):
|
| 12 |
+
pmid: str
|
| 13 |
+
title: str | None = None
|
| 14 |
+
journal: str | None = None
|
| 15 |
+
year: str | None = None
|
| 16 |
+
abstract: str | None = None
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
@mcp.tool()
|
| 20 |
+
def search_pubmed(query: str, max_results: int = 10) -> list[Article]:
|
| 21 |
+
ids = Entrez.read(Entrez.esearch(db="pubmed", term=query, retmax=max_results))["IdList"]
|
| 22 |
+
summaries = Entrez.read(Entrez.esummary(db="pubmed", id=",".join(ids))) if ids else []
|
| 23 |
+
articles: list[Article] = []
|
| 24 |
+
for s in summaries:
|
| 25 |
+
articles.append(
|
| 26 |
+
Article(pmid=s["Id"], title=s["Title"], journal=s.get("FullJournalName", ""), year=s.get("PubDate", "")[:4])
|
| 27 |
+
)
|
| 28 |
+
return articles
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
@mcp.tool()
|
| 32 |
+
def get_article_abstract(pmid: str) -> str:
|
| 33 |
+
with Entrez.efetch(db="pubmed", id=pmid, rettype="abstract", retmode="text") as h:
|
| 34 |
+
return h.read()
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
if __name__ == "__main__":
|
| 38 |
+
mcp.run()
|
Biomni/biomni/tool/genetics.py
ADDED
|
@@ -0,0 +1,1672 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
def liftover_coordinates(
|
| 2 |
+
chromosome: str,
|
| 3 |
+
position: int,
|
| 4 |
+
input_format: str,
|
| 5 |
+
output_format: str,
|
| 6 |
+
data_path: str,
|
| 7 |
+
) -> str:
|
| 8 |
+
"""Perform liftover of genomic coordinates between hg19 and hg38 formats with detailed intermediate steps.
|
| 9 |
+
|
| 10 |
+
Args:
|
| 11 |
+
chromosome (str): Chromosome number (e.g., '1', 'X').
|
| 12 |
+
position (int): Genomic position.
|
| 13 |
+
input_format (str): Input genome build ('hg19' or 'hg38').
|
| 14 |
+
output_format (str): Output genome build ('hg19' or 'hg38').
|
| 15 |
+
data_path (str): Path to liftover chain files.
|
| 16 |
+
|
| 17 |
+
Returns:
|
| 18 |
+
str: A detailed string explaining the steps and the final result or any error encountered.
|
| 19 |
+
|
| 20 |
+
"""
|
| 21 |
+
from pyliftover import LiftOver
|
| 22 |
+
|
| 23 |
+
steps = []
|
| 24 |
+
|
| 25 |
+
try:
|
| 26 |
+
steps.append(
|
| 27 |
+
f"Starting liftover process for chromosome {chromosome}, position {position} from {input_format} to {output_format}."
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
# Load the liftover chain files
|
| 31 |
+
steps.append("Loading liftover chain files...")
|
| 32 |
+
hg19_to_hg38_liftover = LiftOver(data_path + "/liftover/hg19ToHg38.over.chain.gz")
|
| 33 |
+
hg38_to_hg19_liftover = LiftOver(data_path + "/liftover/hg38ToHg19.over.chain.gz")
|
| 34 |
+
steps.append("Liftover chain files loaded successfully.")
|
| 35 |
+
|
| 36 |
+
# Choose the appropriate LiftOver object
|
| 37 |
+
if input_format == "hg19" and output_format == "hg38":
|
| 38 |
+
lo = hg19_to_hg38_liftover
|
| 39 |
+
steps.append("Selected liftover chain: hg19 to hg38.")
|
| 40 |
+
elif input_format == "hg38" and output_format == "hg19":
|
| 41 |
+
lo = hg38_to_hg19_liftover
|
| 42 |
+
steps.append("Selected liftover chain: hg38 to hg19.")
|
| 43 |
+
else:
|
| 44 |
+
steps.append("Error: Unsupported format conversion.")
|
| 45 |
+
return "\n".join(
|
| 46 |
+
steps
|
| 47 |
+
+ ["Error: Unsupported format conversion. Supported formats are 'hg19' to 'hg38' or 'hg38' to 'hg19'."]
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
# Perform the liftover conversion
|
| 51 |
+
steps.append(f"Performing liftover for chr{chromosome}, position {position}...")
|
| 52 |
+
lifted_coordinates = lo.convert_coordinate(f"chr{chromosome}", position)
|
| 53 |
+
|
| 54 |
+
if lifted_coordinates:
|
| 55 |
+
result = (
|
| 56 |
+
f"Successfully lifted coordinates from {input_format} to {output_format}.\n"
|
| 57 |
+
f"Original: chr{chromosome}, position {position}.\n"
|
| 58 |
+
f"Lifted: chromosome {lifted_coordinates[0][0]}, position {lifted_coordinates[0][1]}, strand {lifted_coordinates[0][2]}."
|
| 59 |
+
)
|
| 60 |
+
steps.append(result)
|
| 61 |
+
return "\n".join(steps)
|
| 62 |
+
else:
|
| 63 |
+
steps.append("Error: Liftover failed. No coordinates found for the given input.")
|
| 64 |
+
return "\n".join(steps + ["Error: Liftover failed. No coordinates found."])
|
| 65 |
+
|
| 66 |
+
except Exception as e:
|
| 67 |
+
steps.append(f"Exception encountered: {str(e)}")
|
| 68 |
+
return "\n".join(steps)
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
import os
|
| 72 |
+
from datetime import datetime
|
| 73 |
+
|
| 74 |
+
import numpy as np
|
| 75 |
+
import pandas as pd
|
| 76 |
+
import torch
|
| 77 |
+
from torch import nn, optim
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def bayesian_finemapping_with_deep_vi(
|
| 81 |
+
gwas_summary_path,
|
| 82 |
+
ld_matrix,
|
| 83 |
+
n_iterations=5000,
|
| 84 |
+
learning_rate=0.01,
|
| 85 |
+
hidden_dim=64,
|
| 86 |
+
credible_threshold=0.95,
|
| 87 |
+
):
|
| 88 |
+
"""Performs Bayesian fine-mapping from GWAS summary statistics using deep variational inference.
|
| 89 |
+
|
| 90 |
+
This function implements a deep neural network-based variational inference approach to compute
|
| 91 |
+
posterior inclusion probabilities (PIPs) and credible sets for putative causal variants from
|
| 92 |
+
GWAS summary statistics and linkage disequilibrium (LD) information.
|
| 93 |
+
|
| 94 |
+
Parameters
|
| 95 |
+
----------
|
| 96 |
+
gwas_summary_path : str
|
| 97 |
+
Path to CSV or TSV file containing GWAS summary statistics. Expected columns:
|
| 98 |
+
- 'variant_id': Identifier for each variant
|
| 99 |
+
- 'effect_size': Effect size (beta) for each variant
|
| 100 |
+
- 'pvalue': P-value for each variant
|
| 101 |
+
- 'se': Standard error for each variant (optional)
|
| 102 |
+
|
| 103 |
+
ld_matrix : numpy.ndarray
|
| 104 |
+
Linkage disequilibrium matrix with pairwise correlations between variants.
|
| 105 |
+
|
| 106 |
+
n_iterations : int, optional
|
| 107 |
+
Number of training iterations for the variational inference algorithm.
|
| 108 |
+
Default is 5000.
|
| 109 |
+
|
| 110 |
+
learning_rate : float, optional
|
| 111 |
+
Learning rate for the optimization algorithm. Default is 0.01.
|
| 112 |
+
|
| 113 |
+
hidden_dim : int, optional
|
| 114 |
+
Hidden dimension size for the neural network. Default is 64.
|
| 115 |
+
|
| 116 |
+
credible_threshold : float, optional
|
| 117 |
+
Threshold for defining the credible set (e.g., 0.95 for a 95% credible set).
|
| 118 |
+
Default is 0.95.
|
| 119 |
+
|
| 120 |
+
Returns
|
| 121 |
+
-------
|
| 122 |
+
str
|
| 123 |
+
A detailed research log of the fine-mapping analysis including:
|
| 124 |
+
- Number of variants analyzed
|
| 125 |
+
- Top variants ranked by posterior inclusion probability
|
| 126 |
+
- Credible set variants
|
| 127 |
+
- Visualizations of the posterior distributions
|
| 128 |
+
|
| 129 |
+
"""
|
| 130 |
+
import matplotlib.pyplot as plt
|
| 131 |
+
import pandas as pd
|
| 132 |
+
|
| 133 |
+
# Initialize the research log
|
| 134 |
+
log = []
|
| 135 |
+
log.append(
|
| 136 |
+
f"# Bayesian Fine-mapping Analysis with Deep Variational Inference - {datetime.now().strftime('%Y-%m-%d %H:%M')}"
|
| 137 |
+
)
|
| 138 |
+
log.append("\n## Data Preprocessing")
|
| 139 |
+
|
| 140 |
+
# Load data from file
|
| 141 |
+
try:
|
| 142 |
+
if gwas_summary_path.endswith(".csv"):
|
| 143 |
+
gwas_summary = pd.read_csv(gwas_summary_path)
|
| 144 |
+
elif gwas_summary_path.endswith((".tsv", ".txt")):
|
| 145 |
+
gwas_summary = pd.read_csv(gwas_summary_path, sep="\t")
|
| 146 |
+
else:
|
| 147 |
+
log.append("Error: Unsupported file format. Please provide a CSV or TSV file.")
|
| 148 |
+
return "\n".join(log)
|
| 149 |
+
log.append(f"Successfully loaded GWAS summary data from {gwas_summary_path}")
|
| 150 |
+
except Exception as e:
|
| 151 |
+
log.append(f"Error loading GWAS summary data: {str(e)}")
|
| 152 |
+
return "\n".join(log)
|
| 153 |
+
|
| 154 |
+
# Check input data
|
| 155 |
+
if gwas_summary is None:
|
| 156 |
+
log.append("Error: Failed to load GWAS summary data.")
|
| 157 |
+
return "\n".join(log)
|
| 158 |
+
|
| 159 |
+
if ld_matrix is None:
|
| 160 |
+
log.append("Error: LD matrix is required for fine-mapping analysis.")
|
| 161 |
+
return "\n".join(log)
|
| 162 |
+
|
| 163 |
+
n_variants = len(gwas_summary)
|
| 164 |
+
log.append(f"Analyzing {n_variants} genetic variants")
|
| 165 |
+
|
| 166 |
+
# Check if LD matrix dimensions match the number of variants
|
| 167 |
+
if ld_matrix.shape[0] != n_variants or ld_matrix.shape[1] != n_variants:
|
| 168 |
+
log.append(f"Error: LD matrix dimensions ({ld_matrix.shape}) do not match number of variants ({n_variants})")
|
| 169 |
+
return "\n".join(log)
|
| 170 |
+
|
| 171 |
+
# Prepare data for analysis
|
| 172 |
+
log.append("\nPreparing data for analysis...")
|
| 173 |
+
|
| 174 |
+
# Compute Z-scores if not already present
|
| 175 |
+
if "z_score" not in gwas_summary.columns:
|
| 176 |
+
log.append("Computing Z-scores from effect sizes and standard errors...")
|
| 177 |
+
if "se" in gwas_summary.columns:
|
| 178 |
+
gwas_summary["z_score"] = gwas_summary["effect_size"] / gwas_summary["se"]
|
| 179 |
+
else:
|
| 180 |
+
# Approximate Z-scores from p-values
|
| 181 |
+
log.append("Standard errors not available, approximating Z-scores from p-values...")
|
| 182 |
+
# Convert p-values to Z-scores (two-sided test)
|
| 183 |
+
from scipy.stats import norm
|
| 184 |
+
|
| 185 |
+
gwas_summary["z_score"] = (
|
| 186 |
+
gwas_summary["effect_size"].abs()
|
| 187 |
+
/ gwas_summary["effect_size"]
|
| 188 |
+
* norm.ppf(1 - gwas_summary["pvalue"] / 2)
|
| 189 |
+
)
|
| 190 |
+
|
| 191 |
+
# Convert data to tensors
|
| 192 |
+
z_scores = torch.FloatTensor(gwas_summary["z_score"].values)
|
| 193 |
+
ld_tensor = torch.FloatTensor(ld_matrix)
|
| 194 |
+
|
| 195 |
+
log.append(f"Processed {len(z_scores)} z-scores from GWAS summary")
|
| 196 |
+
log.append("LD matrix shape: " + str(ld_matrix.shape))
|
| 197 |
+
|
| 198 |
+
# Define the variational inference model
|
| 199 |
+
class VariationalFineMapping(nn.Module):
|
| 200 |
+
def __init__(self, n_variants, hidden_dim):
|
| 201 |
+
super().__init__()
|
| 202 |
+
self.encoder = nn.Sequential(
|
| 203 |
+
nn.Linear(n_variants, hidden_dim),
|
| 204 |
+
nn.ReLU(),
|
| 205 |
+
nn.Linear(hidden_dim, hidden_dim),
|
| 206 |
+
nn.ReLU(),
|
| 207 |
+
)
|
| 208 |
+
# Output log alpha parameters for the Bernoulli variables
|
| 209 |
+
self.log_alpha = nn.Linear(hidden_dim, n_variants)
|
| 210 |
+
|
| 211 |
+
def forward(self, x):
|
| 212 |
+
h = self.encoder(x)
|
| 213 |
+
log_alpha = self.log_alpha(h)
|
| 214 |
+
# Apply sigmoid to get inclusion probabilities
|
| 215 |
+
return torch.sigmoid(log_alpha)
|
| 216 |
+
|
| 217 |
+
def elbo_loss(self, z_scores, ld_matrix, pips, n_samples=10):
|
| 218 |
+
# Sample from approximate posterior
|
| 219 |
+
samples = torch.bernoulli(pips.unsqueeze(0).repeat(n_samples, 1))
|
| 220 |
+
|
| 221 |
+
# Prior term (sparsity prior)
|
| 222 |
+
prior_term = -0.01 * torch.sum(pips)
|
| 223 |
+
|
| 224 |
+
# Likelihood term
|
| 225 |
+
likelihood_term = 0
|
| 226 |
+
for s in samples:
|
| 227 |
+
# Compute expected z-scores under the model
|
| 228 |
+
expected_z = torch.matmul(ld_matrix, s * z_scores)
|
| 229 |
+
# Compute likelihood
|
| 230 |
+
likelihood_term += -torch.sum((z_scores - expected_z) ** 2)
|
| 231 |
+
|
| 232 |
+
likelihood_term /= n_samples
|
| 233 |
+
|
| 234 |
+
return -(prior_term + likelihood_term)
|
| 235 |
+
|
| 236 |
+
# Initialize model, optimizer and training
|
| 237 |
+
log.append("\n## Initializing deep variational inference model")
|
| 238 |
+
model = VariationalFineMapping(n_variants, hidden_dim)
|
| 239 |
+
optimizer = optim.Adam(model.parameters(), lr=learning_rate)
|
| 240 |
+
|
| 241 |
+
# Training loop
|
| 242 |
+
log.append("\n## Training variational inference model")
|
| 243 |
+
losses = []
|
| 244 |
+
|
| 245 |
+
for i in range(n_iterations):
|
| 246 |
+
optimizer.zero_grad()
|
| 247 |
+
pips = model(z_scores)
|
| 248 |
+
loss = model.elbo_loss(z_scores, ld_tensor, pips)
|
| 249 |
+
loss.backward()
|
| 250 |
+
optimizer.step()
|
| 251 |
+
|
| 252 |
+
losses.append(loss.item())
|
| 253 |
+
|
| 254 |
+
if (i + 1) % (n_iterations // 5) == 0:
|
| 255 |
+
log.append(f" Iteration {i + 1}/{n_iterations}, Loss: {loss.item():.4f}")
|
| 256 |
+
|
| 257 |
+
# Get final posterior inclusion probabilities
|
| 258 |
+
with torch.no_grad():
|
| 259 |
+
final_pips = model(z_scores).numpy()
|
| 260 |
+
|
| 261 |
+
# Create DataFrame with results
|
| 262 |
+
results_df = gwas_summary.copy()
|
| 263 |
+
results_df["pip"] = final_pips
|
| 264 |
+
results_df = results_df.sort_values("pip", ascending=False)
|
| 265 |
+
|
| 266 |
+
# Generate credible sets
|
| 267 |
+
log.append("\n## Generating credible sets")
|
| 268 |
+
|
| 269 |
+
# Sort variants by PIP
|
| 270 |
+
sorted_variants = results_df.sort_values("pip", ascending=False)
|
| 271 |
+
|
| 272 |
+
# Calculate cumulative sum of PIPs
|
| 273 |
+
sorted_variants["cumulative_pip"] = sorted_variants["pip"].cumsum()
|
| 274 |
+
|
| 275 |
+
# Identify variants in the credible set
|
| 276 |
+
credible_set = sorted_variants[sorted_variants["cumulative_pip"] <= credible_threshold]
|
| 277 |
+
|
| 278 |
+
if len(credible_set) == 0:
|
| 279 |
+
# If no variants meet the threshold, include at least the top variant
|
| 280 |
+
credible_set = sorted_variants.iloc[:1]
|
| 281 |
+
|
| 282 |
+
log.append(f"Identified {len(credible_set)} variants in the {credible_threshold * 100}% credible set")
|
| 283 |
+
|
| 284 |
+
# Save results to files
|
| 285 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 286 |
+
results_file = f"finemapping_results_{timestamp}.csv"
|
| 287 |
+
credible_set_file = f"credible_set_{timestamp}.csv"
|
| 288 |
+
|
| 289 |
+
results_df.to_csv(results_file, index=False)
|
| 290 |
+
credible_set.to_csv(credible_set_file, index=False)
|
| 291 |
+
|
| 292 |
+
log.append(f"\nFull results saved to: {results_file}")
|
| 293 |
+
log.append(f"Credible set saved to: {credible_set_file}")
|
| 294 |
+
|
| 295 |
+
# Summary of top variants
|
| 296 |
+
log.append("\n## Top variants by posterior inclusion probability (PIP)")
|
| 297 |
+
for i, (_, row) in enumerate(results_df.head(10).iterrows()):
|
| 298 |
+
log.append(f" {i + 1}. Variant: {row['variant_id']}, PIP: {row['pip']:.4f}, P-value: {row['pvalue']:.2e}")
|
| 299 |
+
|
| 300 |
+
log.append("\n## Variants in the credible set")
|
| 301 |
+
for i, (_, row) in enumerate(credible_set.iterrows()):
|
| 302 |
+
log.append(f" {i + 1}. Variant: {row['variant_id']}, PIP: {row['pip']:.4f}")
|
| 303 |
+
|
| 304 |
+
# Create a simple visualization
|
| 305 |
+
try:
|
| 306 |
+
plt.figure(figsize=(10, 6))
|
| 307 |
+
plt.bar(range(len(results_df[:50])), results_df["pip"][:50])
|
| 308 |
+
plt.xlabel("Variant index (sorted by PIP)")
|
| 309 |
+
plt.ylabel("Posterior Inclusion Probability")
|
| 310 |
+
plt.title("Top 50 variants by PIP")
|
| 311 |
+
plot_file = f"pip_plot_{timestamp}.png"
|
| 312 |
+
plt.savefig(plot_file)
|
| 313 |
+
plt.close()
|
| 314 |
+
log.append(f"\nPlot of PIPs saved to: {plot_file}")
|
| 315 |
+
except Exception as e:
|
| 316 |
+
log.append(f"\nCould not create visualization: {str(e)}")
|
| 317 |
+
|
| 318 |
+
log.append("\n## Analysis complete")
|
| 319 |
+
|
| 320 |
+
return "\n".join(log)
|
| 321 |
+
|
| 322 |
+
|
| 323 |
+
def analyze_cas9_mutation_outcomes(
|
| 324 |
+
reference_sequences,
|
| 325 |
+
edited_sequences,
|
| 326 |
+
cell_line_info=None,
|
| 327 |
+
output_prefix="cas9_mutation_analysis",
|
| 328 |
+
):
|
| 329 |
+
"""Analyzes and categorizes mutations induced by Cas9 at target sites.
|
| 330 |
+
|
| 331 |
+
Parameters
|
| 332 |
+
----------
|
| 333 |
+
reference_sequences : dict
|
| 334 |
+
Dictionary mapping sequence IDs to reference DNA sequences (strings)
|
| 335 |
+
edited_sequences : dict of dict
|
| 336 |
+
Nested dictionary: {sequence_id: {read_id: sequence}}
|
| 337 |
+
Contains the edited/mutated sequences for each reference
|
| 338 |
+
cell_line_info : dict, optional
|
| 339 |
+
Dictionary mapping sequence IDs to cell line information (e.g., wildtype, knockout gene)
|
| 340 |
+
output_prefix : str, optional
|
| 341 |
+
Prefix for output files
|
| 342 |
+
|
| 343 |
+
Returns
|
| 344 |
+
-------
|
| 345 |
+
str
|
| 346 |
+
Research log summarizing the analysis steps and results
|
| 347 |
+
|
| 348 |
+
"""
|
| 349 |
+
from collections import defaultdict
|
| 350 |
+
|
| 351 |
+
from Bio import pairwise2
|
| 352 |
+
|
| 353 |
+
# Initialize results storage
|
| 354 |
+
results = []
|
| 355 |
+
mutation_counts = defaultdict(lambda: defaultdict(int))
|
| 356 |
+
|
| 357 |
+
# Define mutation categories
|
| 358 |
+
categories = {
|
| 359 |
+
"no_mutation": "No mutation detected",
|
| 360 |
+
"short_deletion": "Short deletion (1-10 bp)",
|
| 361 |
+
"medium_deletion": "Medium deletion (11-30 bp)",
|
| 362 |
+
"long_deletion": "Long deletion (>30 bp)",
|
| 363 |
+
"single_insertion": "Single base insertion",
|
| 364 |
+
"longer_insertion": "Longer insertion (>1 bp)",
|
| 365 |
+
"indel": "Insertion and deletion",
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
log = "# Cas9-Induced Mutation Outcome Analysis\n\n"
|
| 369 |
+
log += "## Analysis Steps:\n\n"
|
| 370 |
+
log += "1. Loading and processing sequence data\n"
|
| 371 |
+
log += f"2. Analyzing {len(reference_sequences)} target sites\n"
|
| 372 |
+
|
| 373 |
+
# Process each reference sequence and its edited versions
|
| 374 |
+
for seq_id, ref_seq in reference_sequences.items():
|
| 375 |
+
cell_line = cell_line_info.get(seq_id, "Unknown") if cell_line_info else "Unknown"
|
| 376 |
+
log += f"\n### Processing target site: {seq_id} (Cell line: {cell_line})\n"
|
| 377 |
+
|
| 378 |
+
site_results = []
|
| 379 |
+
site_mutation_counts = defaultdict(int)
|
| 380 |
+
total_reads = len(edited_sequences.get(seq_id, {}))
|
| 381 |
+
|
| 382 |
+
if total_reads == 0:
|
| 383 |
+
log += f"No edited sequences found for {seq_id}\n"
|
| 384 |
+
continue
|
| 385 |
+
|
| 386 |
+
log += f"Analyzing {total_reads} sequence reads...\n"
|
| 387 |
+
|
| 388 |
+
# Process each edited sequence for this reference
|
| 389 |
+
for read_id, edited_seq in edited_sequences.get(seq_id, {}).items():
|
| 390 |
+
# Perform sequence alignment
|
| 391 |
+
alignments = pairwise2.align.globalms(ref_seq, edited_seq, 2, -1, -2, -0.5)
|
| 392 |
+
|
| 393 |
+
if not alignments:
|
| 394 |
+
log += f"Warning: Could not align read {read_id}\n"
|
| 395 |
+
continue
|
| 396 |
+
|
| 397 |
+
best_alignment = alignments[0]
|
| 398 |
+
ref_aligned, edited_aligned, score, start, end = best_alignment
|
| 399 |
+
|
| 400 |
+
# Analyze mutations
|
| 401 |
+
deletions = []
|
| 402 |
+
insertions = []
|
| 403 |
+
del_count = 0
|
| 404 |
+
ins_count = 0
|
| 405 |
+
|
| 406 |
+
i, j = 0, 0
|
| 407 |
+
while i < len(ref_aligned) and j < len(edited_aligned):
|
| 408 |
+
if ref_aligned[i] == "-": # Insertion in edited sequence
|
| 409 |
+
ins_start = j
|
| 410 |
+
while i < len(ref_aligned) and ref_aligned[i] == "-":
|
| 411 |
+
i += 1
|
| 412 |
+
j += 1
|
| 413 |
+
insertions.append((ins_start, j - ins_start))
|
| 414 |
+
ins_count += j - ins_start
|
| 415 |
+
elif edited_aligned[j] == "-": # Deletion in edited sequence
|
| 416 |
+
del_start = i
|
| 417 |
+
while j < len(edited_aligned) and edited_aligned[j] == "-":
|
| 418 |
+
i += 1
|
| 419 |
+
j += 1
|
| 420 |
+
deletions.append((del_start, i - del_start))
|
| 421 |
+
del_count += i - del_start
|
| 422 |
+
else:
|
| 423 |
+
i += 1
|
| 424 |
+
j += 1
|
| 425 |
+
|
| 426 |
+
# Categorize mutation
|
| 427 |
+
mutation_type = "no_mutation"
|
| 428 |
+
if del_count > 0 and ins_count > 0:
|
| 429 |
+
mutation_type = "indel"
|
| 430 |
+
elif del_count > 0:
|
| 431 |
+
if del_count <= 10:
|
| 432 |
+
mutation_type = "short_deletion"
|
| 433 |
+
elif del_count <= 30:
|
| 434 |
+
mutation_type = "medium_deletion"
|
| 435 |
+
else:
|
| 436 |
+
mutation_type = "long_deletion"
|
| 437 |
+
elif ins_count > 0:
|
| 438 |
+
mutation_type = "single_insertion" if ins_count == 1 else "longer_insertion"
|
| 439 |
+
|
| 440 |
+
# Add to results
|
| 441 |
+
site_results.append(
|
| 442 |
+
{
|
| 443 |
+
"sequence_id": seq_id,
|
| 444 |
+
"read_id": read_id,
|
| 445 |
+
"cell_line": cell_line,
|
| 446 |
+
"mutation_type": mutation_type,
|
| 447 |
+
"deletion_count": del_count,
|
| 448 |
+
"insertion_count": ins_count,
|
| 449 |
+
}
|
| 450 |
+
)
|
| 451 |
+
|
| 452 |
+
site_mutation_counts[mutation_type] += 1
|
| 453 |
+
mutation_counts[cell_line][mutation_type] += 1
|
| 454 |
+
|
| 455 |
+
# Calculate percentages for this site
|
| 456 |
+
log += "\nMutation distribution for this target site:\n"
|
| 457 |
+
for mut_type, count in site_mutation_counts.items():
|
| 458 |
+
percentage = (count / total_reads) * 100
|
| 459 |
+
log += f"- {categories[mut_type]}: {count} reads ({percentage:.1f}%)\n"
|
| 460 |
+
|
| 461 |
+
# Add site results to overall results
|
| 462 |
+
results.extend(site_results)
|
| 463 |
+
|
| 464 |
+
# Create results dataframe and save to CSV
|
| 465 |
+
results_df = pd.DataFrame(results)
|
| 466 |
+
output_file = f"{output_prefix}_detailed_results.csv"
|
| 467 |
+
results_df.to_csv(output_file, index=False)
|
| 468 |
+
|
| 469 |
+
# Create summary dataframe
|
| 470 |
+
summary_data = []
|
| 471 |
+
for cell_line, mut_counts in mutation_counts.items():
|
| 472 |
+
total = sum(mut_counts.values())
|
| 473 |
+
for mut_type, count in mut_counts.items():
|
| 474 |
+
percentage = (count / total) * 100 if total > 0 else 0
|
| 475 |
+
summary_data.append(
|
| 476 |
+
{
|
| 477 |
+
"cell_line": cell_line,
|
| 478 |
+
"mutation_type": mut_type,
|
| 479 |
+
"count": count,
|
| 480 |
+
"percentage": percentage,
|
| 481 |
+
}
|
| 482 |
+
)
|
| 483 |
+
|
| 484 |
+
summary_df = pd.DataFrame(summary_data)
|
| 485 |
+
summary_file = f"{output_prefix}_summary.csv"
|
| 486 |
+
summary_df.to_csv(summary_file, index=False)
|
| 487 |
+
|
| 488 |
+
# Add summary to log
|
| 489 |
+
log += "\n## Overall Results Summary\n\n"
|
| 490 |
+
log += f"Total sequences analyzed: {len(results)}\n"
|
| 491 |
+
log += f"Detailed results saved to: {output_file}\n"
|
| 492 |
+
log += f"Summary results saved to: {summary_file}\n\n"
|
| 493 |
+
|
| 494 |
+
if cell_line_info:
|
| 495 |
+
log += "### Mutation Distribution by Cell Line\n\n"
|
| 496 |
+
for cell_line, mut_counts in mutation_counts.items():
|
| 497 |
+
total = sum(mut_counts.values())
|
| 498 |
+
if total == 0:
|
| 499 |
+
continue
|
| 500 |
+
|
| 501 |
+
log += f"#### {cell_line}\n"
|
| 502 |
+
for mut_type, count in sorted(mut_counts.items(), key=lambda x: x[1], reverse=True):
|
| 503 |
+
percentage = (count / total) * 100
|
| 504 |
+
log += f"- {categories[mut_type]}: {count} ({percentage:.1f}%)\n"
|
| 505 |
+
log += "\n"
|
| 506 |
+
|
| 507 |
+
return log
|
| 508 |
+
|
| 509 |
+
|
| 510 |
+
def analyze_crispr_genome_editing(original_sequence, edited_sequence, guide_rna, repair_template=None):
|
| 511 |
+
"""Analyzes CRISPR-Cas9 genome editing results by comparing original and edited sequences.
|
| 512 |
+
|
| 513 |
+
Parameters
|
| 514 |
+
----------
|
| 515 |
+
original_sequence : str
|
| 516 |
+
The original DNA sequence before CRISPR-Cas9 editing
|
| 517 |
+
edited_sequence : str
|
| 518 |
+
The DNA sequence after CRISPR-Cas9 editing
|
| 519 |
+
guide_rna : str
|
| 520 |
+
The CRISPR guide RNA (crRNA) sequence used for targeting
|
| 521 |
+
repair_template : str, optional
|
| 522 |
+
The homology-directed repair template sequence, if used
|
| 523 |
+
|
| 524 |
+
Returns
|
| 525 |
+
-------
|
| 526 |
+
str
|
| 527 |
+
A research log summarizing the CRISPR-Cas9 editing analysis, including identified
|
| 528 |
+
mutations and characterization of the edited loci
|
| 529 |
+
|
| 530 |
+
"""
|
| 531 |
+
import datetime
|
| 532 |
+
|
| 533 |
+
from Bio import pairwise2
|
| 534 |
+
from Bio.Seq import Seq
|
| 535 |
+
|
| 536 |
+
log = []
|
| 537 |
+
log.append(f"CRISPR-Cas9 Genome Editing Analysis - {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
| 538 |
+
log.append("=" * 70)
|
| 539 |
+
|
| 540 |
+
# Step 1: Find the target site in the original sequence
|
| 541 |
+
log.append("\n1. Identifying target site in original sequence")
|
| 542 |
+
target_site = original_sequence.find(guide_rna)
|
| 543 |
+
if target_site == -1:
|
| 544 |
+
# Try with the reverse complement
|
| 545 |
+
guide_rna_seq = Seq(guide_rna)
|
| 546 |
+
rev_comp_guide = str(guide_rna_seq.reverse_complement())
|
| 547 |
+
target_site = original_sequence.find(rev_comp_guide)
|
| 548 |
+
if target_site != -1:
|
| 549 |
+
log.append(f" - Target site found at position {target_site} (using reverse complement of guide RNA)")
|
| 550 |
+
guide_rna = rev_comp_guide
|
| 551 |
+
else:
|
| 552 |
+
log.append(" - Warning: Guide RNA sequence not found in original sequence")
|
| 553 |
+
target_site = None
|
| 554 |
+
else:
|
| 555 |
+
log.append(f" - Target site found at position {target_site}")
|
| 556 |
+
|
| 557 |
+
# Step 2: Align sequences to identify mutations
|
| 558 |
+
log.append("\n2. Aligning original and edited sequences to identify mutations")
|
| 559 |
+
alignments = pairwise2.align.globalms(original_sequence, edited_sequence, 2, -1, -2, -0.5)
|
| 560 |
+
best_alignment = alignments[0]
|
| 561 |
+
|
| 562 |
+
# Extract aligned sequences
|
| 563 |
+
aligned_orig = best_alignment[0]
|
| 564 |
+
aligned_edit = best_alignment[1]
|
| 565 |
+
|
| 566 |
+
# Find mutations
|
| 567 |
+
mutations = []
|
| 568 |
+
indels = []
|
| 569 |
+
|
| 570 |
+
for i in range(len(aligned_orig)):
|
| 571 |
+
if aligned_orig[i] != aligned_edit[i]:
|
| 572 |
+
orig_base = aligned_orig[i]
|
| 573 |
+
edit_base = aligned_edit[i]
|
| 574 |
+
|
| 575 |
+
# Skip gaps in counting the actual position
|
| 576 |
+
actual_pos = len(aligned_orig[:i].replace("-", ""))
|
| 577 |
+
|
| 578 |
+
if orig_base == "-": # Insertion
|
| 579 |
+
indels.append(f"Insertion of {edit_base} at position {actual_pos}")
|
| 580 |
+
elif edit_base == "-": # Deletion
|
| 581 |
+
indels.append(f"Deletion of {orig_base} at position {actual_pos}")
|
| 582 |
+
else: # Substitution
|
| 583 |
+
mutations.append(f"{orig_base}→{edit_base} at position {actual_pos}")
|
| 584 |
+
|
| 585 |
+
# Log mutations
|
| 586 |
+
if mutations:
|
| 587 |
+
log.append(" - Substitutions detected:")
|
| 588 |
+
for mutation in mutations:
|
| 589 |
+
log.append(f" * {mutation}")
|
| 590 |
+
else:
|
| 591 |
+
log.append(" - No substitutions detected")
|
| 592 |
+
|
| 593 |
+
if indels:
|
| 594 |
+
log.append(" - Insertions/Deletions detected:")
|
| 595 |
+
for indel in indels:
|
| 596 |
+
log.append(f" * {indel}")
|
| 597 |
+
else:
|
| 598 |
+
log.append(" - No insertions or deletions detected")
|
| 599 |
+
|
| 600 |
+
# Step 3: Check if editing occurred near the target site
|
| 601 |
+
if target_site is not None:
|
| 602 |
+
log.append("\n3. Analyzing mutations relative to target site")
|
| 603 |
+
target_end = target_site + len(guide_rna)
|
| 604 |
+
target_region = range(target_site - 3, target_end + 3) # Include some buffer
|
| 605 |
+
|
| 606 |
+
on_target_edits = [m for m in mutations + indels if any(str(pos) in m for pos in target_region)]
|
| 607 |
+
|
| 608 |
+
if on_target_edits:
|
| 609 |
+
log.append(" - On-target edits detected near guide RNA binding site:")
|
| 610 |
+
for edit in on_target_edits:
|
| 611 |
+
log.append(f" * {edit}")
|
| 612 |
+
else:
|
| 613 |
+
log.append(" - No edits detected near guide RNA binding site")
|
| 614 |
+
|
| 615 |
+
# Step 4: Check for homology-directed repair if template was provided
|
| 616 |
+
if repair_template:
|
| 617 |
+
log.append("\n4. Checking for homology-directed repair template incorporation")
|
| 618 |
+
# Look for unique sequence markers from the repair template
|
| 619 |
+
template_len = len(repair_template)
|
| 620 |
+
marker_size = min(10, template_len // 3)
|
| 621 |
+
marker = repair_template[template_len // 2 - marker_size // 2 : template_len // 2 + marker_size // 2]
|
| 622 |
+
|
| 623 |
+
if marker in edited_sequence and marker not in original_sequence:
|
| 624 |
+
log.append(f" - Repair template marker '{marker}' found in edited sequence")
|
| 625 |
+
log.append(" - Homology-directed repair likely successful")
|
| 626 |
+
else:
|
| 627 |
+
log.append(" - No clear evidence of repair template incorporation")
|
| 628 |
+
log.append(" - Editing likely resulted from non-homologous end joining (NHEJ)")
|
| 629 |
+
|
| 630 |
+
# Step 5: Overall assessment
|
| 631 |
+
log.append("\n5. Overall assessment")
|
| 632 |
+
if mutations or indels:
|
| 633 |
+
log.append(" - CRISPR-Cas9 editing appears successful")
|
| 634 |
+
if target_site is not None and any(
|
| 635 |
+
str(pos) in "".join(mutations + indels) for pos in range(target_site, target_site + len(guide_rna))
|
| 636 |
+
):
|
| 637 |
+
log.append(" - Edits occurred at the intended target site")
|
| 638 |
+
else:
|
| 639 |
+
log.append(" - Edits may have occurred outside the intended target site")
|
| 640 |
+
else:
|
| 641 |
+
log.append(" - No significant editing detected, CRISPR-Cas9 may not have been effective")
|
| 642 |
+
|
| 643 |
+
return "\n".join(log)
|
| 644 |
+
|
| 645 |
+
|
| 646 |
+
def simulate_demographic_history(
|
| 647 |
+
num_samples=10,
|
| 648 |
+
sequence_length=100000,
|
| 649 |
+
recombination_rate=1e-8,
|
| 650 |
+
mutation_rate=1e-8,
|
| 651 |
+
demographic_model="constant",
|
| 652 |
+
demographic_params=None,
|
| 653 |
+
coalescent_model="kingman",
|
| 654 |
+
beta_coalescent_param=None,
|
| 655 |
+
random_seed=None,
|
| 656 |
+
output_file="simulated_sequences.vcf",
|
| 657 |
+
):
|
| 658 |
+
"""Simulate DNA sequences with specified demographic and coalescent histories using msprime.
|
| 659 |
+
|
| 660 |
+
Parameters
|
| 661 |
+
----------
|
| 662 |
+
num_samples : int
|
| 663 |
+
Number of sample sequences to simulate
|
| 664 |
+
sequence_length : int
|
| 665 |
+
Length of the simulated sequence in base pairs
|
| 666 |
+
recombination_rate : float
|
| 667 |
+
Per-base recombination rate
|
| 668 |
+
mutation_rate : float
|
| 669 |
+
Per-base mutation rate
|
| 670 |
+
demographic_model : str
|
| 671 |
+
Type of demographic model to simulate. Options:
|
| 672 |
+
- "constant": Constant population size
|
| 673 |
+
- "bottleneck": Population bottleneck
|
| 674 |
+
- "expansion": Population expansion
|
| 675 |
+
- "contraction": Population contraction
|
| 676 |
+
- "sawtooth": Sawtooth pattern of population size changes
|
| 677 |
+
demographic_params : dict
|
| 678 |
+
Parameters specific to the chosen demographic model. Supported formats::
|
| 679 |
+
|
| 680 |
+
- For "constant": {"N": population size}
|
| 681 |
+
- For "bottleneck": {
|
| 682 |
+
"N_initial": initial pop size,
|
| 683 |
+
"N_bottleneck": bottleneck pop size,
|
| 684 |
+
"T_bottleneck": time of bottleneck (generations ago),
|
| 685 |
+
"T_recovery": time of recovery (generations ago)
|
| 686 |
+
}
|
| 687 |
+
- For "expansion": {"N_initial": initial pop size, "N_final": final pop size, "T_expansion": time of expansion (generations ago)}
|
| 688 |
+
- For "contraction": {"N_initial": initial pop size, "N_final": final pop size, "T_contraction": time of contraction (generations ago)}
|
| 689 |
+
- For "sawtooth": {"N_values": list of population sizes, "times": list of times for changes}
|
| 690 |
+
coalescent_model : str
|
| 691 |
+
Type of coalescent model to use. Options:
|
| 692 |
+
- "kingman": Standard Kingman coalescent
|
| 693 |
+
- "beta": Beta-coalescent model
|
| 694 |
+
beta_coalescent_param : float
|
| 695 |
+
Parameter for beta-coalescent model (required if coalescent_model="beta")
|
| 696 |
+
random_seed : int
|
| 697 |
+
Seed for random number generator (for reproducibility)
|
| 698 |
+
output_file : str
|
| 699 |
+
Filename to save the simulated sequences (VCF format)
|
| 700 |
+
|
| 701 |
+
Returns
|
| 702 |
+
-------
|
| 703 |
+
str
|
| 704 |
+
Research log summarizing the simulation parameters and results
|
| 705 |
+
|
| 706 |
+
"""
|
| 707 |
+
import time
|
| 708 |
+
|
| 709 |
+
import msprime
|
| 710 |
+
|
| 711 |
+
start_time = time.time()
|
| 712 |
+
log = []
|
| 713 |
+
log.append("Demographic History Simulation using msprime")
|
| 714 |
+
log.append("=============================================")
|
| 715 |
+
log.append("Parameters:")
|
| 716 |
+
log.append(f" - Number of samples: {num_samples}")
|
| 717 |
+
log.append(f" - Sequence length: {sequence_length} bp")
|
| 718 |
+
log.append(f" - Recombination rate: {recombination_rate}")
|
| 719 |
+
log.append(f" - Mutation rate: {mutation_rate}")
|
| 720 |
+
log.append(f" - Demographic model: {demographic_model}")
|
| 721 |
+
log.append(f" - Coalescent model: {coalescent_model}")
|
| 722 |
+
|
| 723 |
+
# Set up demographic model
|
| 724 |
+
if demographic_params is None:
|
| 725 |
+
demographic_params = {"N": 10000} # Default to constant population of 10000
|
| 726 |
+
|
| 727 |
+
demography = msprime.Demography()
|
| 728 |
+
demography.add_population(name="pop0", initial_size=1000) # Default initial population
|
| 729 |
+
|
| 730 |
+
if demographic_model == "constant":
|
| 731 |
+
N = demographic_params.get("N", 10000)
|
| 732 |
+
demography.add_population(name="pop0", initial_size=N)
|
| 733 |
+
log.append(f" - Constant population size: N = {N}")
|
| 734 |
+
|
| 735 |
+
elif demographic_model == "bottleneck":
|
| 736 |
+
N_initial = demographic_params.get("N_initial", 10000)
|
| 737 |
+
N_bottleneck = demographic_params.get("N_bottleneck", 1000)
|
| 738 |
+
T_bottleneck = demographic_params.get("T_bottleneck", 1000)
|
| 739 |
+
T_recovery = demographic_params.get("T_recovery", 500)
|
| 740 |
+
|
| 741 |
+
demography = msprime.Demography()
|
| 742 |
+
demography.add_population(name="pop0", initial_size=N_initial)
|
| 743 |
+
demography.add_population_parameters_change(time=T_recovery, initial_size=N_bottleneck)
|
| 744 |
+
demography.add_population_parameters_change(time=T_bottleneck, initial_size=N_initial)
|
| 745 |
+
|
| 746 |
+
log.append(" - Bottleneck model:")
|
| 747 |
+
log.append(f" * Initial population size: {N_initial}")
|
| 748 |
+
log.append(f" * Bottleneck population size: {N_bottleneck}")
|
| 749 |
+
log.append(f" * Bottleneck time (generations ago): {T_bottleneck}")
|
| 750 |
+
log.append(f" * Recovery time (generations ago): {T_recovery}")
|
| 751 |
+
|
| 752 |
+
elif demographic_model == "expansion":
|
| 753 |
+
N_initial = demographic_params.get("N_initial", 1000)
|
| 754 |
+
N_final = demographic_params.get("N_final", 10000)
|
| 755 |
+
T_expansion = demographic_params.get("T_expansion", 1000)
|
| 756 |
+
|
| 757 |
+
demography = msprime.Demography()
|
| 758 |
+
demography.add_population(name="pop0", initial_size=N_final)
|
| 759 |
+
demography.add_population_parameters_change(time=T_expansion, initial_size=N_initial)
|
| 760 |
+
|
| 761 |
+
log.append(" - Expansion model:")
|
| 762 |
+
log.append(f" * Initial population size: {N_initial}")
|
| 763 |
+
log.append(f" * Final population size: {N_final}")
|
| 764 |
+
log.append(f" * Expansion time (generations ago): {T_expansion}")
|
| 765 |
+
|
| 766 |
+
elif demographic_model == "contraction":
|
| 767 |
+
N_initial = demographic_params.get("N_initial", 10000)
|
| 768 |
+
N_final = demographic_params.get("N_final", 1000)
|
| 769 |
+
T_contraction = demographic_params.get("T_contraction", 1000)
|
| 770 |
+
|
| 771 |
+
demography = msprime.Demography()
|
| 772 |
+
demography.add_population(name="pop0", initial_size=N_final)
|
| 773 |
+
demography.add_population_parameters_change(time=T_contraction, initial_size=N_initial)
|
| 774 |
+
|
| 775 |
+
log.append(" - Contraction model:")
|
| 776 |
+
log.append(f" * Initial population size: {N_initial}")
|
| 777 |
+
log.append(f" * Final population size: {N_final}")
|
| 778 |
+
log.append(f" * Contraction time (generations ago): {T_contraction}")
|
| 779 |
+
|
| 780 |
+
elif demographic_model == "sawtooth":
|
| 781 |
+
N_values = demographic_params.get("N_values", [10000, 5000, 15000, 7500])
|
| 782 |
+
times = demographic_params.get("times", [500, 1000, 1500])
|
| 783 |
+
|
| 784 |
+
if len(N_values) != len(times) + 1:
|
| 785 |
+
raise ValueError("For sawtooth model, N_values should have one more element than times")
|
| 786 |
+
|
| 787 |
+
demography = msprime.Demography()
|
| 788 |
+
demography.add_population(name="pop0", initial_size=N_values[0])
|
| 789 |
+
|
| 790 |
+
for i, time in enumerate(times):
|
| 791 |
+
demography.add_population_parameters_change(time=time, initial_size=N_values[i + 1])
|
| 792 |
+
|
| 793 |
+
log.append(" - Sawtooth model:")
|
| 794 |
+
log.append(f" * Population sizes: {N_values}")
|
| 795 |
+
log.append(f" * Change times (generations ago): {times}")
|
| 796 |
+
|
| 797 |
+
else:
|
| 798 |
+
raise ValueError(f"Unknown demographic model: {demographic_model}")
|
| 799 |
+
|
| 800 |
+
# Set up coalescent model
|
| 801 |
+
model = None
|
| 802 |
+
if coalescent_model == "kingman":
|
| 803 |
+
model = msprime.StandardCoalescent()
|
| 804 |
+
log.append(" - Using standard Kingman coalescent")
|
| 805 |
+
elif coalescent_model == "beta":
|
| 806 |
+
if beta_coalescent_param is None:
|
| 807 |
+
beta_coalescent_param = 1.5
|
| 808 |
+
model = msprime.BetaCoalescent(alpha=beta_coalescent_param)
|
| 809 |
+
log.append(f" - Using Beta-coalescent with alpha = {beta_coalescent_param}")
|
| 810 |
+
else:
|
| 811 |
+
raise ValueError(f"Unknown coalescent model: {coalescent_model}")
|
| 812 |
+
|
| 813 |
+
# Run simulation
|
| 814 |
+
log.append("\nRunning simulation...")
|
| 815 |
+
|
| 816 |
+
ts = msprime.sim_ancestry(
|
| 817 |
+
samples=num_samples,
|
| 818 |
+
recombination_rate=recombination_rate,
|
| 819 |
+
sequence_length=sequence_length,
|
| 820 |
+
demography=demography,
|
| 821 |
+
model=model,
|
| 822 |
+
random_seed=random_seed,
|
| 823 |
+
)
|
| 824 |
+
|
| 825 |
+
# Add mutations
|
| 826 |
+
mts = msprime.sim_mutations(ts, rate=mutation_rate, random_seed=random_seed)
|
| 827 |
+
|
| 828 |
+
# Save to VCF
|
| 829 |
+
with open(output_file, "w") as vcf_file:
|
| 830 |
+
mts.write_vcf(vcf_file)
|
| 831 |
+
|
| 832 |
+
# Calculate some basic statistics
|
| 833 |
+
diversity = mts.diversity()
|
| 834 |
+
num_sites = mts.num_sites
|
| 835 |
+
num_trees = mts.num_trees
|
| 836 |
+
|
| 837 |
+
# Log results
|
| 838 |
+
end_time = time.time()
|
| 839 |
+
runtime = end_time - start_time
|
| 840 |
+
|
| 841 |
+
log.append(f"Simulation completed in {runtime:.2f} seconds")
|
| 842 |
+
log.append("\nResults:")
|
| 843 |
+
log.append(f" - Number of segregating sites: {num_sites}")
|
| 844 |
+
log.append(f" - Number of trees in ARG: {num_trees}")
|
| 845 |
+
log.append(f" - Nucleotide diversity (π): {diversity:.6f}")
|
| 846 |
+
log.append(f" - Output saved to: {os.path.abspath(output_file)}")
|
| 847 |
+
|
| 848 |
+
return "\n".join(log)
|
| 849 |
+
|
| 850 |
+
|
| 851 |
+
def identify_transcription_factor_binding_sites(sequence, tf_name, threshold=0.8, output_file=None):
|
| 852 |
+
"""Identifies binding sites for a specific transcription factor in a genomic sequence.
|
| 853 |
+
|
| 854 |
+
Parameters
|
| 855 |
+
----------
|
| 856 |
+
sequence : str
|
| 857 |
+
The genomic DNA sequence to analyze
|
| 858 |
+
tf_name : str
|
| 859 |
+
Name of the transcription factor to search for (e.g., 'Hsf1', 'GATA1')
|
| 860 |
+
threshold : float, optional
|
| 861 |
+
Minimum score threshold for reporting binding sites (0.0-1.0, default: 0.8)
|
| 862 |
+
output_file : str, optional
|
| 863 |
+
Path to save the results (default: None, results only in log)
|
| 864 |
+
|
| 865 |
+
Returns
|
| 866 |
+
-------
|
| 867 |
+
str
|
| 868 |
+
Research log detailing the binding site identification process and results
|
| 869 |
+
|
| 870 |
+
"""
|
| 871 |
+
import datetime
|
| 872 |
+
import io
|
| 873 |
+
|
| 874 |
+
import requests
|
| 875 |
+
from Bio import motifs
|
| 876 |
+
from Bio.Seq import Seq
|
| 877 |
+
|
| 878 |
+
log = f"# Transcription Factor Binding Site Analysis: {tf_name}\n"
|
| 879 |
+
log += f"Date: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
| 880 |
+
|
| 881 |
+
# Step 1: Get the PWM for the transcription factor from JASPAR database
|
| 882 |
+
log += "## Step 1: Retrieving transcription factor PWM\n"
|
| 883 |
+
|
| 884 |
+
try:
|
| 885 |
+
# Search for the TF in JASPAR database
|
| 886 |
+
jaspar_url = f"https://jaspar.genereg.net/api/v1/matrix/?name={tf_name}"
|
| 887 |
+
response = requests.get(jaspar_url)
|
| 888 |
+
tf_data = response.json()
|
| 889 |
+
|
| 890 |
+
if not tf_data["results"] or len(tf_data["results"]) == 0:
|
| 891 |
+
log += f"No PWM found for {tf_name} in JASPAR database.\n"
|
| 892 |
+
return log
|
| 893 |
+
|
| 894 |
+
# Get the first match's ID
|
| 895 |
+
matrix_id = tf_data["results"][0]["matrix_id"]
|
| 896 |
+
log += f"Found PWM with ID: {matrix_id}\n"
|
| 897 |
+
|
| 898 |
+
# Retrieve the PWM
|
| 899 |
+
pwm_url = f"https://jaspar.genereg.net/api/v1/matrix/{matrix_id}.pfm"
|
| 900 |
+
pwm_response = requests.get(pwm_url)
|
| 901 |
+
|
| 902 |
+
# Parse the PWM
|
| 903 |
+
handle = io.StringIO(pwm_response.text)
|
| 904 |
+
motif = motifs.read(handle, "jaspar")
|
| 905 |
+
log += f"Successfully retrieved PWM for {tf_name}\n"
|
| 906 |
+
|
| 907 |
+
# Calculate position-specific scoring matrix (PSSM)
|
| 908 |
+
pssm = motif.pssm
|
| 909 |
+
|
| 910 |
+
# Step 2: Scan the sequence for binding sites
|
| 911 |
+
log += "\n## Step 2: Scanning sequence for binding sites\n"
|
| 912 |
+
log += f"Sequence length: {len(sequence)} bp\n"
|
| 913 |
+
log += f"Using score threshold: {threshold}\n\n"
|
| 914 |
+
|
| 915 |
+
# Find binding sites
|
| 916 |
+
binding_sites = []
|
| 917 |
+
max_score = pssm.max
|
| 918 |
+
min_score = pssm.min
|
| 919 |
+
|
| 920 |
+
for position, score in pssm.search(Seq(sequence), threshold=threshold):
|
| 921 |
+
relative_score = (score - min_score) / (max_score - min_score)
|
| 922 |
+
if position >= 0:
|
| 923 |
+
strand = "+"
|
| 924 |
+
site_seq = sequence[position : position + len(pssm)]
|
| 925 |
+
else:
|
| 926 |
+
strand = "-"
|
| 927 |
+
site_seq = sequence[len(sequence) + position - len(pssm) : len(sequence) + position]
|
| 928 |
+
|
| 929 |
+
binding_sites.append(
|
| 930 |
+
{
|
| 931 |
+
"position": abs(position),
|
| 932 |
+
"strand": strand,
|
| 933 |
+
"score": score,
|
| 934 |
+
"relative_score": relative_score,
|
| 935 |
+
"sequence": site_seq,
|
| 936 |
+
}
|
| 937 |
+
)
|
| 938 |
+
|
| 939 |
+
# Step 3: Summarize results
|
| 940 |
+
log += f"## Step 3: Results - Found {len(binding_sites)} potential binding sites\n\n"
|
| 941 |
+
|
| 942 |
+
if binding_sites:
|
| 943 |
+
# Sort by position
|
| 944 |
+
binding_sites.sort(key=lambda x: x["position"])
|
| 945 |
+
|
| 946 |
+
# Create a table of results
|
| 947 |
+
log += "| Position | Strand | Sequence | Score | Relative Score |\n"
|
| 948 |
+
log += "|----------|--------|----------|-------|---------------|\n"
|
| 949 |
+
|
| 950 |
+
for site in binding_sites:
|
| 951 |
+
log += f"| {site['position']} | {site['strand']} | {site['sequence']} | {site['score']:.2f} | {site['relative_score']:.2f} |\n"
|
| 952 |
+
else:
|
| 953 |
+
log += "No binding sites found meeting the threshold criteria.\n"
|
| 954 |
+
|
| 955 |
+
# Save results to file if specified
|
| 956 |
+
if output_file:
|
| 957 |
+
with open(output_file, "w") as f:
|
| 958 |
+
f.write(f"# {tf_name} binding sites in sequence\n")
|
| 959 |
+
f.write("Position\tStrand\tSequence\tScore\tRelative Score\n")
|
| 960 |
+
|
| 961 |
+
for site in binding_sites:
|
| 962 |
+
f.write(
|
| 963 |
+
f"{site['position']}\t{site['strand']}\t{site['sequence']}\t{site['score']:.2f}\t{site['relative_score']:.2f}\n"
|
| 964 |
+
)
|
| 965 |
+
|
| 966 |
+
log += f"\nResults saved to file: {output_file}\n"
|
| 967 |
+
|
| 968 |
+
except Exception as e:
|
| 969 |
+
log += f"\n## Error occurred during analysis: {str(e)}\n"
|
| 970 |
+
|
| 971 |
+
log += "\n## Analysis complete\n"
|
| 972 |
+
return log
|
| 973 |
+
|
| 974 |
+
|
| 975 |
+
def fit_genomic_prediction_model(
|
| 976 |
+
genotypes,
|
| 977 |
+
phenotypes,
|
| 978 |
+
fixed_effects=None,
|
| 979 |
+
model_type="additive",
|
| 980 |
+
output_file="genomic_prediction_results.csv",
|
| 981 |
+
):
|
| 982 |
+
"""Fit a linear mixed model for genomic prediction using genotype and phenotype data.
|
| 983 |
+
|
| 984 |
+
Parameters
|
| 985 |
+
----------
|
| 986 |
+
genotypes : numpy.ndarray
|
| 987 |
+
Matrix of genotype data, with individuals in rows and markers in columns.
|
| 988 |
+
Values are typically coded as 0, 1, 2 for additive models or with specific
|
| 989 |
+
encoding for dominance effects.
|
| 990 |
+
phenotypes : numpy.ndarray
|
| 991 |
+
Vector or matrix of phenotype data, with individuals in rows and traits in columns.
|
| 992 |
+
fixed_effects : numpy.ndarray, optional
|
| 993 |
+
Matrix of fixed effects (e.g., environment, management), with individuals in rows
|
| 994 |
+
and effects in columns.
|
| 995 |
+
model_type : str, optional
|
| 996 |
+
Type of genetic model to fit: "additive" or "additive_dominance".
|
| 997 |
+
output_file : str, optional
|
| 998 |
+
File name to save the results.
|
| 999 |
+
|
| 1000 |
+
Returns
|
| 1001 |
+
-------
|
| 1002 |
+
str
|
| 1003 |
+
Research log summarizing the genomic prediction analysis, including model parameters,
|
| 1004 |
+
variance components, breeding values, and prediction accuracy metrics.
|
| 1005 |
+
|
| 1006 |
+
"""
|
| 1007 |
+
import pandas as pd
|
| 1008 |
+
from scipy import linalg
|
| 1009 |
+
|
| 1010 |
+
# Initialize research log
|
| 1011 |
+
log = "# Multi-trait Genomic Prediction Analysis\n\n"
|
| 1012 |
+
log += f"Model type: {model_type}\n"
|
| 1013 |
+
|
| 1014 |
+
# Basic validation
|
| 1015 |
+
n_individuals, n_markers = genotypes.shape
|
| 1016 |
+
n_pheno, n_traits = (phenotypes.shape[0], 1) if phenotypes.ndim == 1 else phenotypes.shape
|
| 1017 |
+
|
| 1018 |
+
if n_individuals != n_pheno:
|
| 1019 |
+
raise ValueError(f"Number of individuals in genotypes ({n_individuals}) and phenotypes ({n_pheno}) don't match")
|
| 1020 |
+
|
| 1021 |
+
log += f"Number of individuals: {n_individuals}\n"
|
| 1022 |
+
log += f"Number of markers: {n_markers}\n"
|
| 1023 |
+
log += f"Number of traits: {n_traits}\n\n"
|
| 1024 |
+
|
| 1025 |
+
# Ensure phenotypes is 2D
|
| 1026 |
+
if phenotypes.ndim == 1:
|
| 1027 |
+
phenotypes = phenotypes.reshape(-1, 1)
|
| 1028 |
+
|
| 1029 |
+
# Center genotypes (common preprocessing step)
|
| 1030 |
+
genotypes_centered = genotypes - np.mean(genotypes, axis=0)
|
| 1031 |
+
|
| 1032 |
+
# Create genomic relationship matrix (G)
|
| 1033 |
+
if model_type == "additive":
|
| 1034 |
+
# Additive genomic relationship matrix
|
| 1035 |
+
G = np.dot(genotypes_centered, genotypes_centered.T) / n_markers
|
| 1036 |
+
log += "Constructed additive genomic relationship matrix (G)\n\n"
|
| 1037 |
+
elif model_type == "additive_dominance":
|
| 1038 |
+
# For additive-dominance model, we need both A (additive) and D (dominance) matrices
|
| 1039 |
+
# Assuming genotypes are coded as {0,1,2} for {aa,Aa,AA}
|
| 1040 |
+
# Create dominance matrix - simple implementation assuming standard coding
|
| 1041 |
+
dom_genotypes = np.zeros_like(genotypes)
|
| 1042 |
+
# Code heterozygotes (1) as 1, homozygotes (0,2) as 0 for dominance effects
|
| 1043 |
+
dom_genotypes[genotypes == 1] = 1
|
| 1044 |
+
dom_genotypes_centered = dom_genotypes - np.mean(dom_genotypes, axis=0)
|
| 1045 |
+
|
| 1046 |
+
# Additive and dominance matrices
|
| 1047 |
+
G_a = np.dot(genotypes_centered, genotypes_centered.T) / n_markers
|
| 1048 |
+
G_d = np.dot(dom_genotypes_centered, dom_genotypes_centered.T) / n_markers
|
| 1049 |
+
log += "Constructed additive (G_a) and dominance (G_d) genomic relationship matrices\n\n"
|
| 1050 |
+
else:
|
| 1051 |
+
raise ValueError(f"Unknown model type: {model_type}")
|
| 1052 |
+
|
| 1053 |
+
# Initialize results storage
|
| 1054 |
+
trait_results = []
|
| 1055 |
+
|
| 1056 |
+
# Fit model for each trait
|
| 1057 |
+
for trait_idx in range(n_traits):
|
| 1058 |
+
trait_phenotypes = phenotypes[:, trait_idx]
|
| 1059 |
+
log += f"## Trait {trait_idx + 1} Analysis\n\n"
|
| 1060 |
+
|
| 1061 |
+
# Handle fixed effects if provided
|
| 1062 |
+
if fixed_effects is not None:
|
| 1063 |
+
# Simple fixed effects adjustment - more complex models would use proper mixed model fitting
|
| 1064 |
+
X = fixed_effects
|
| 1065 |
+
# Fit fixed effects model
|
| 1066 |
+
beta = np.linalg.lstsq(X, trait_phenotypes, rcond=None)[0]
|
| 1067 |
+
# Adjust phenotypes for fixed effects
|
| 1068 |
+
y_adj = trait_phenotypes - X @ beta
|
| 1069 |
+
log += f"Applied adjustment for {X.shape[1]} fixed effects\n"
|
| 1070 |
+
else:
|
| 1071 |
+
y_adj = trait_phenotypes
|
| 1072 |
+
log += "No fixed effects provided\n"
|
| 1073 |
+
|
| 1074 |
+
# Fit mixed model
|
| 1075 |
+
if model_type == "additive":
|
| 1076 |
+
# Simplified REML estimation for variance components
|
| 1077 |
+
# In practice, specialized libraries like pyGWAS, GCTA or R's ASReml would be used
|
| 1078 |
+
|
| 1079 |
+
# Initial variance component estimates
|
| 1080 |
+
var_g_init = np.var(y_adj) * 0.5 # genetic variance
|
| 1081 |
+
var_e_init = np.var(y_adj) * 0.5 # residual variance
|
| 1082 |
+
|
| 1083 |
+
# Simple EM-like algorithm for variance component estimation
|
| 1084 |
+
# (In practice, use dedicated software for proper REML)
|
| 1085 |
+
for _ in range(5): # Few iterations for demonstration
|
| 1086 |
+
# Construct mixed model equations
|
| 1087 |
+
V = var_g_init * G + var_e_init * np.eye(n_individuals)
|
| 1088 |
+
V_inv = linalg.inv(V)
|
| 1089 |
+
|
| 1090 |
+
# Update variance components
|
| 1091 |
+
P = V_inv - V_inv @ np.ones((n_individuals, 1)) @ np.ones((1, n_individuals)) @ V_inv / (
|
| 1092 |
+
np.ones((1, n_individuals)) @ V_inv @ np.ones((n_individuals, 1))
|
| 1093 |
+
)
|
| 1094 |
+
var_g_new = (y_adj.T @ P @ G @ P @ y_adj) / np.trace(P @ G)
|
| 1095 |
+
var_e_new = (y_adj.T @ P @ P @ y_adj) / np.trace(P)
|
| 1096 |
+
|
| 1097 |
+
# Update estimates
|
| 1098 |
+
var_g_init = max(0.01, var_g_new)
|
| 1099 |
+
var_e_init = max(0.01, var_e_new)
|
| 1100 |
+
|
| 1101 |
+
# Final variance components
|
| 1102 |
+
var_g = var_g_init
|
| 1103 |
+
var_e = var_e_init
|
| 1104 |
+
|
| 1105 |
+
# Calculate heritability
|
| 1106 |
+
heritability = var_g / (var_g + var_e)
|
| 1107 |
+
|
| 1108 |
+
# BLUP solutions for breeding values
|
| 1109 |
+
V = var_g * G + var_e * np.eye(n_individuals)
|
| 1110 |
+
V_inv = linalg.inv(V)
|
| 1111 |
+
breeding_values = var_g * G @ V_inv @ y_adj
|
| 1112 |
+
|
| 1113 |
+
# Predicted phenotypes
|
| 1114 |
+
predicted_phenotypes = breeding_values
|
| 1115 |
+
|
| 1116 |
+
# Calculate accuracy
|
| 1117 |
+
accuracy = np.corrcoef(trait_phenotypes, predicted_phenotypes)[0, 1]
|
| 1118 |
+
|
| 1119 |
+
# Log results
|
| 1120 |
+
log += f"Estimated additive genetic variance: {var_g:.4f}\n"
|
| 1121 |
+
log += f"Estimated residual variance: {var_e:.4f}\n"
|
| 1122 |
+
log += f"Estimated heritability: {heritability:.4f}\n"
|
| 1123 |
+
log += f"Prediction accuracy (correlation): {accuracy:.4f}\n\n"
|
| 1124 |
+
|
| 1125 |
+
# Store results
|
| 1126 |
+
trait_result = {
|
| 1127 |
+
"trait": trait_idx + 1,
|
| 1128 |
+
"var_g": var_g,
|
| 1129 |
+
"var_e": var_e,
|
| 1130 |
+
"heritability": heritability,
|
| 1131 |
+
"accuracy": accuracy,
|
| 1132 |
+
"breeding_values": breeding_values,
|
| 1133 |
+
"predicted_phenotypes": predicted_phenotypes,
|
| 1134 |
+
}
|
| 1135 |
+
trait_results.append(trait_result)
|
| 1136 |
+
|
| 1137 |
+
elif model_type == "additive_dominance":
|
| 1138 |
+
# Similar approach but with both additive and dominance effects
|
| 1139 |
+
# Initial variance component estimates
|
| 1140 |
+
var_a_init = np.var(y_adj) * 0.4 # additive variance
|
| 1141 |
+
var_d_init = np.var(y_adj) * 0.1 # dominance variance
|
| 1142 |
+
var_e_init = np.var(y_adj) * 0.5 # residual variance
|
| 1143 |
+
|
| 1144 |
+
# Simple estimation iterations
|
| 1145 |
+
for _ in range(5): # Few iterations for demonstration
|
| 1146 |
+
# Construct mixed model equations
|
| 1147 |
+
V = var_a_init * G_a + var_d_init * G_d + var_e_init * np.eye(n_individuals)
|
| 1148 |
+
V_inv = linalg.inv(V)
|
| 1149 |
+
|
| 1150 |
+
# Update variance components (simplified)
|
| 1151 |
+
P = V_inv - V_inv @ np.ones((n_individuals, 1)) @ np.ones((1, n_individuals)) @ V_inv / (
|
| 1152 |
+
np.ones((1, n_individuals)) @ V_inv @ np.ones((n_individuals, 1))
|
| 1153 |
+
)
|
| 1154 |
+
var_a_new = (y_adj.T @ P @ G_a @ P @ y_adj) / np.trace(P @ G_a)
|
| 1155 |
+
var_d_new = (y_adj.T @ P @ G_d @ P @ y_adj) / np.trace(P @ G_d)
|
| 1156 |
+
var_e_new = (y_adj.T @ P @ P @ y_adj) / np.trace(P)
|
| 1157 |
+
|
| 1158 |
+
# Update estimates
|
| 1159 |
+
var_a_init = max(0.01, var_a_new)
|
| 1160 |
+
var_d_init = max(0.01, var_d_new)
|
| 1161 |
+
var_e_init = max(0.01, var_e_new)
|
| 1162 |
+
|
| 1163 |
+
# Final variance components
|
| 1164 |
+
var_a = var_a_init
|
| 1165 |
+
var_d = var_d_init
|
| 1166 |
+
var_e = var_e_init
|
| 1167 |
+
|
| 1168 |
+
# Calculate heritabilities
|
| 1169 |
+
narrow_heritability = var_a / (var_a + var_d + var_e)
|
| 1170 |
+
broad_heritability = (var_a + var_d) / (var_a + var_d + var_e)
|
| 1171 |
+
|
| 1172 |
+
# BLUP solutions for breeding values and dominance deviations
|
| 1173 |
+
V = var_a * G_a + var_d * G_d + var_e * np.eye(n_individuals)
|
| 1174 |
+
V_inv = linalg.inv(V)
|
| 1175 |
+
breeding_values = var_a * G_a @ V_inv @ y_adj
|
| 1176 |
+
dominance_deviations = var_d * G_d @ V_inv @ y_adj
|
| 1177 |
+
|
| 1178 |
+
# Predicted phenotypes
|
| 1179 |
+
predicted_phenotypes = breeding_values + dominance_deviations
|
| 1180 |
+
|
| 1181 |
+
# Calculate accuracy
|
| 1182 |
+
accuracy = np.corrcoef(trait_phenotypes, predicted_phenotypes)[0, 1]
|
| 1183 |
+
|
| 1184 |
+
# Log results
|
| 1185 |
+
log += f"Estimated additive genetic variance: {var_a:.4f}\n"
|
| 1186 |
+
log += f"Estimated dominance genetic variance: {var_d:.4f}\n"
|
| 1187 |
+
log += f"Estimated residual variance: {var_e:.4f}\n"
|
| 1188 |
+
log += f"Estimated narrow-sense heritability: {narrow_heritability:.4f}\n"
|
| 1189 |
+
log += f"Estimated broad-sense heritability: {broad_heritability:.4f}\n"
|
| 1190 |
+
log += f"Prediction accuracy (correlation): {accuracy:.4f}\n\n"
|
| 1191 |
+
|
| 1192 |
+
# Store results
|
| 1193 |
+
trait_result = {
|
| 1194 |
+
"trait": trait_idx + 1,
|
| 1195 |
+
"var_a": var_a,
|
| 1196 |
+
"var_d": var_d,
|
| 1197 |
+
"var_e": var_e,
|
| 1198 |
+
"narrow_heritability": narrow_heritability,
|
| 1199 |
+
"broad_heritability": broad_heritability,
|
| 1200 |
+
"accuracy": accuracy,
|
| 1201 |
+
"breeding_values": breeding_values,
|
| 1202 |
+
"dominance_deviations": dominance_deviations,
|
| 1203 |
+
"predicted_phenotypes": predicted_phenotypes,
|
| 1204 |
+
}
|
| 1205 |
+
trait_results.append(trait_result)
|
| 1206 |
+
|
| 1207 |
+
# Save results to file
|
| 1208 |
+
results_df = pd.DataFrame()
|
| 1209 |
+
|
| 1210 |
+
for i, trait_result in enumerate(trait_results):
|
| 1211 |
+
# Create individual-level results
|
| 1212 |
+
ind_data = {
|
| 1213 |
+
"individual": np.arange(1, n_individuals + 1),
|
| 1214 |
+
f"trait_{i + 1}_observed": phenotypes[:, i],
|
| 1215 |
+
f"trait_{i + 1}_predicted": trait_result["predicted_phenotypes"],
|
| 1216 |
+
f"trait_{i + 1}_breeding_value": trait_result["breeding_values"],
|
| 1217 |
+
}
|
| 1218 |
+
|
| 1219 |
+
if model_type == "additive_dominance":
|
| 1220 |
+
ind_data[f"trait_{i + 1}_dominance_deviation"] = trait_result["dominance_deviations"]
|
| 1221 |
+
|
| 1222 |
+
# Create or append to dataframe
|
| 1223 |
+
if i == 0:
|
| 1224 |
+
results_df = pd.DataFrame(ind_data)
|
| 1225 |
+
else:
|
| 1226 |
+
for key, value in ind_data.items():
|
| 1227 |
+
if key != "individual": # Skip duplicating individual column
|
| 1228 |
+
results_df[key] = value
|
| 1229 |
+
|
| 1230 |
+
# Save to CSV
|
| 1231 |
+
results_df.to_csv(output_file, index=False)
|
| 1232 |
+
log += f"Results saved to {output_file}\n"
|
| 1233 |
+
|
| 1234 |
+
return log
|
| 1235 |
+
|
| 1236 |
+
|
| 1237 |
+
def perform_pcr_and_gel_electrophoresis(
|
| 1238 |
+
genomic_dna,
|
| 1239 |
+
forward_primer=None,
|
| 1240 |
+
reverse_primer=None,
|
| 1241 |
+
target_region=None,
|
| 1242 |
+
annealing_temp=58,
|
| 1243 |
+
extension_time=30,
|
| 1244 |
+
cycles=35,
|
| 1245 |
+
gel_percentage=2.0,
|
| 1246 |
+
output_prefix="pcr_result",
|
| 1247 |
+
):
|
| 1248 |
+
"""Performs PCR amplification of a target transgene and visualizes results using agarose gel electrophoresis.
|
| 1249 |
+
|
| 1250 |
+
Parameters
|
| 1251 |
+
----------
|
| 1252 |
+
genomic_dna : str
|
| 1253 |
+
Path to file containing genomic DNA sequence in FASTA format or the sequence itself
|
| 1254 |
+
forward_primer : str, optional
|
| 1255 |
+
Forward primer sequence. If not provided, will be designed based on target_region
|
| 1256 |
+
reverse_primer : str, optional
|
| 1257 |
+
Reverse primer sequence. If not provided, will be designed based on target_region
|
| 1258 |
+
target_region : tuple, optional
|
| 1259 |
+
Tuple of (start, end) positions for the target region in the genomic DNA
|
| 1260 |
+
annealing_temp : float, default=58
|
| 1261 |
+
Annealing temperature for PCR in °C
|
| 1262 |
+
extension_time : int, default=30
|
| 1263 |
+
Extension time in seconds
|
| 1264 |
+
cycles : int, default=35
|
| 1265 |
+
Number of PCR cycles
|
| 1266 |
+
gel_percentage : float, default=2.0
|
| 1267 |
+
Percentage of agarose gel
|
| 1268 |
+
output_prefix : str, default="pcr_result"
|
| 1269 |
+
Prefix for output files
|
| 1270 |
+
|
| 1271 |
+
Returns
|
| 1272 |
+
-------
|
| 1273 |
+
str
|
| 1274 |
+
Research log summarizing the PCR and gel electrophoresis procedures and results
|
| 1275 |
+
|
| 1276 |
+
"""
|
| 1277 |
+
import datetime
|
| 1278 |
+
|
| 1279 |
+
import matplotlib.pyplot as plt
|
| 1280 |
+
import numpy as np
|
| 1281 |
+
from Bio import SeqIO
|
| 1282 |
+
from Bio.Seq import Seq
|
| 1283 |
+
|
| 1284 |
+
log = f"PCR AMPLIFICATION AND GEL ELECTROPHORESIS LOG - {datetime.datetime.now().strftime('%Y-%m-%d %H:%M')}\n"
|
| 1285 |
+
log += "=" * 80 + "\n\n"
|
| 1286 |
+
|
| 1287 |
+
# Step 1: Load the genomic DNA
|
| 1288 |
+
log += "STEP 1: PREPARING GENOMIC DNA\n"
|
| 1289 |
+
if os.path.isfile(genomic_dna):
|
| 1290 |
+
try:
|
| 1291 |
+
record = SeqIO.read(genomic_dna, "fasta")
|
| 1292 |
+
dna_sequence = str(record.seq)
|
| 1293 |
+
log += f"- Loaded genomic DNA from file: {genomic_dna}\n"
|
| 1294 |
+
log += f"- Sequence length: {len(dna_sequence)} bp\n"
|
| 1295 |
+
except Exception as e:
|
| 1296 |
+
log += f"- Error loading DNA file: {str(e)}\n"
|
| 1297 |
+
return log
|
| 1298 |
+
else:
|
| 1299 |
+
dna_sequence = genomic_dna
|
| 1300 |
+
log += "- Using provided DNA sequence\n"
|
| 1301 |
+
log += f"- Sequence length: {len(dna_sequence)} bp\n"
|
| 1302 |
+
|
| 1303 |
+
log += "\n"
|
| 1304 |
+
|
| 1305 |
+
# Step 2: Design or validate primers
|
| 1306 |
+
log += "STEP 2: PCR PRIMER PREPARATION\n"
|
| 1307 |
+
|
| 1308 |
+
if forward_primer is None or reverse_primer is None:
|
| 1309 |
+
if target_region is None:
|
| 1310 |
+
log += "- Error: Either primers or target region must be provided\n"
|
| 1311 |
+
return log
|
| 1312 |
+
|
| 1313 |
+
# Simple primer design based on target region
|
| 1314 |
+
start, end = target_region
|
| 1315 |
+
|
| 1316 |
+
if forward_primer is None:
|
| 1317 |
+
# Take 20bp from the start of the target region for forward primer
|
| 1318 |
+
forward_primer = dna_sequence[start : start + 20]
|
| 1319 |
+
log += "- Designed forward primer based on target region\n"
|
| 1320 |
+
|
| 1321 |
+
if reverse_primer is None:
|
| 1322 |
+
# Take 20bp from the end of the target region for reverse primer (reverse complement)
|
| 1323 |
+
reverse_seq = Seq(dna_sequence[end - 20 : end])
|
| 1324 |
+
reverse_primer = str(reverse_seq.reverse_complement())
|
| 1325 |
+
log += "- Designed reverse primer based on target region\n"
|
| 1326 |
+
|
| 1327 |
+
log += f"- Forward primer: 5'-{forward_primer}-3' ({len(forward_primer)} bp)\n"
|
| 1328 |
+
log += f"- Reverse primer: 5'-{reverse_primer}-3' ({len(reverse_primer)} bp)\n"
|
| 1329 |
+
|
| 1330 |
+
# Step 3: PCR Setup and Amplification
|
| 1331 |
+
log += "\nSTEP 3: PCR AMPLIFICATION\n"
|
| 1332 |
+
log += "- PCR reaction setup:\n"
|
| 1333 |
+
log += " * Template DNA: Genomic DNA\n"
|
| 1334 |
+
log += f" * Forward primer: 5'-{forward_primer}-3'\n"
|
| 1335 |
+
log += f" * Reverse primer: 5'-{reverse_primer}-3'\n"
|
| 1336 |
+
log += f" * Annealing temperature: {annealing_temp}°C\n"
|
| 1337 |
+
log += f" * Extension time: {extension_time} seconds\n"
|
| 1338 |
+
log += f" * Number of cycles: {cycles}\n"
|
| 1339 |
+
|
| 1340 |
+
# Simulate PCR by finding binding sites and determining amplicon size
|
| 1341 |
+
amplicon_size = None
|
| 1342 |
+
amplicon_sequence = None
|
| 1343 |
+
|
| 1344 |
+
# Find forward primer binding site
|
| 1345 |
+
fwd_pos = dna_sequence.find(forward_primer)
|
| 1346 |
+
if fwd_pos == -1:
|
| 1347 |
+
log += "- Warning: Forward primer binding site not found in sequence\n"
|
| 1348 |
+
|
| 1349 |
+
# Find reverse primer binding site (need to search for reverse complement)
|
| 1350 |
+
rev_primer_seq = Seq(reverse_primer)
|
| 1351 |
+
rev_primer_rc = str(rev_primer_seq.reverse_complement())
|
| 1352 |
+
rev_pos = dna_sequence.find(rev_primer_rc)
|
| 1353 |
+
if rev_pos == -1:
|
| 1354 |
+
log += "- Warning: Reverse primer binding site not found in sequence\n"
|
| 1355 |
+
|
| 1356 |
+
# If we found both binding sites, calculate amplicon size
|
| 1357 |
+
if fwd_pos != -1 and rev_pos != -1:
|
| 1358 |
+
if fwd_pos < rev_pos:
|
| 1359 |
+
amplicon_size = rev_pos + len(reverse_primer) - fwd_pos
|
| 1360 |
+
amplicon_sequence = dna_sequence[fwd_pos : rev_pos + len(reverse_primer)]
|
| 1361 |
+
log += "- PCR amplification successful\n"
|
| 1362 |
+
log += f"- Amplicon size: {amplicon_size} bp\n"
|
| 1363 |
+
else:
|
| 1364 |
+
log += "- Error: Primer binding sites are in incorrect orientation\n"
|
| 1365 |
+
# Simulate based on target region if provided
|
| 1366 |
+
elif target_region is not None:
|
| 1367 |
+
start, end = target_region
|
| 1368 |
+
amplicon_size = end - start + len(forward_primer) + len(reverse_primer)
|
| 1369 |
+
log += "- PCR amplification simulated based on target region\n"
|
| 1370 |
+
log += f"- Expected amplicon size: {amplicon_size} bp\n"
|
| 1371 |
+
else:
|
| 1372 |
+
log += "- PCR amplification failed: could not determine amplicon size\n"
|
| 1373 |
+
return log
|
| 1374 |
+
|
| 1375 |
+
# Step 4: Gel Electrophoresis
|
| 1376 |
+
log += "\nSTEP 4: AGAROSE GEL ELECTROPHORESIS\n"
|
| 1377 |
+
log += f"- Prepared {gel_percentage}% agarose gel\n"
|
| 1378 |
+
log += "- Loaded PCR product alongside DNA ladder\n"
|
| 1379 |
+
log += "- Ran electrophoresis at 100V for 45 minutes\n"
|
| 1380 |
+
|
| 1381 |
+
# Create a simulated gel image
|
| 1382 |
+
fig, ax = plt.subplots(figsize=(6, 8))
|
| 1383 |
+
|
| 1384 |
+
# Draw gel lanes
|
| 1385 |
+
ax.add_patch(plt.Rectangle((0, 0), 6, 10, color="lightgray", alpha=0.5))
|
| 1386 |
+
|
| 1387 |
+
# DNA Ladder (100bp increments)
|
| 1388 |
+
ladder_sizes = [100, 200, 300, 500, 700, 1000, 1500, 2000]
|
| 1389 |
+
ladder_positions = [10 - (np.log(size) / np.log(2000) * 8) for size in ladder_sizes]
|
| 1390 |
+
|
| 1391 |
+
# Plot ladder
|
| 1392 |
+
for pos, size in zip(ladder_positions, ladder_sizes, strict=False):
|
| 1393 |
+
ax.add_patch(plt.Rectangle((0.5, pos - 0.1), 1, 0.2, color="black", alpha=0.8))
|
| 1394 |
+
ax.text(0.2, pos, f"{size}bp", fontsize=8, ha="right", va="center")
|
| 1395 |
+
|
| 1396 |
+
# Plot sample band
|
| 1397 |
+
if amplicon_size:
|
| 1398 |
+
sample_position = 10 - (np.log(amplicon_size) / np.log(2000) * 8)
|
| 1399 |
+
ax.add_patch(plt.Rectangle((3.5, sample_position - 0.15), 1, 0.3, color="black", alpha=0.8))
|
| 1400 |
+
ax.text(
|
| 1401 |
+
4.5,
|
| 1402 |
+
sample_position,
|
| 1403 |
+
f"{amplicon_size}bp",
|
| 1404 |
+
fontsize=8,
|
| 1405 |
+
ha="left",
|
| 1406 |
+
va="center",
|
| 1407 |
+
)
|
| 1408 |
+
|
| 1409 |
+
# Set up the plot
|
| 1410 |
+
ax.set_xlim(0, 6)
|
| 1411 |
+
ax.set_ylim(0, 10)
|
| 1412 |
+
ax.set_xticks([0.5, 3.5])
|
| 1413 |
+
ax.set_xticklabels(["Ladder", "Sample"])
|
| 1414 |
+
ax.set_yticks([])
|
| 1415 |
+
ax.spines["top"].set_visible(False)
|
| 1416 |
+
ax.spines["right"].set_visible(False)
|
| 1417 |
+
ax.spines["left"].set_visible(False)
|
| 1418 |
+
ax.set_title(f"{gel_percentage}% Agarose Gel")
|
| 1419 |
+
|
| 1420 |
+
# Save the gel image
|
| 1421 |
+
gel_image_path = f"{output_prefix}_gel.png"
|
| 1422 |
+
plt.savefig(gel_image_path, dpi=300, bbox_inches="tight")
|
| 1423 |
+
plt.close()
|
| 1424 |
+
|
| 1425 |
+
log += f"- Gel image saved as: {gel_image_path}\n"
|
| 1426 |
+
|
| 1427 |
+
# Results interpretation
|
| 1428 |
+
log += "\nRESULTS INTERPRETATION:\n"
|
| 1429 |
+
if amplicon_size:
|
| 1430 |
+
log += f"- Detected band at approximately {amplicon_size} bp\n"
|
| 1431 |
+
|
| 1432 |
+
# Save amplicon sequence if available
|
| 1433 |
+
if amplicon_sequence:
|
| 1434 |
+
seq_file = f"{output_prefix}_amplicon.fasta"
|
| 1435 |
+
with open(seq_file, "w") as f:
|
| 1436 |
+
f.write(f">PCR_Amplicon_{amplicon_size}bp\n")
|
| 1437 |
+
f.write(amplicon_sequence)
|
| 1438 |
+
log += f"- Amplicon sequence saved as: {seq_file}\n"
|
| 1439 |
+
else:
|
| 1440 |
+
log += "- No bands detected\n"
|
| 1441 |
+
|
| 1442 |
+
return log
|
| 1443 |
+
|
| 1444 |
+
|
| 1445 |
+
def analyze_protein_phylogeny(
|
| 1446 |
+
fasta_sequences,
|
| 1447 |
+
output_dir="./",
|
| 1448 |
+
alignment_method="clustalw",
|
| 1449 |
+
tree_method="fasttree",
|
| 1450 |
+
):
|
| 1451 |
+
"""Perform phylogenetic analysis on a set of protein sequences.
|
| 1452 |
+
|
| 1453 |
+
This function takes protein sequences in FASTA format, performs multiple sequence alignment,
|
| 1454 |
+
constructs a phylogenetic tree, and visualizes the evolutionary relationships.
|
| 1455 |
+
|
| 1456 |
+
Parameters
|
| 1457 |
+
----------
|
| 1458 |
+
fasta_sequences : str
|
| 1459 |
+
Path to a FASTA file containing protein sequences or a string with FASTA-formatted sequences
|
| 1460 |
+
output_dir : str, optional
|
| 1461 |
+
Directory to save output files (default: current directory)
|
| 1462 |
+
alignment_method : str, optional
|
| 1463 |
+
Method for sequence alignment: "clustalw", "muscle", or "pre-aligned" (default: "clustalw")
|
| 1464 |
+
tree_method : str, optional
|
| 1465 |
+
Method for tree construction: "iqtree" (default: "iqtree")
|
| 1466 |
+
|
| 1467 |
+
Returns
|
| 1468 |
+
-------
|
| 1469 |
+
str
|
| 1470 |
+
Research log summarizing the phylogenetic analysis process
|
| 1471 |
+
|
| 1472 |
+
"""
|
| 1473 |
+
import datetime
|
| 1474 |
+
import subprocess
|
| 1475 |
+
import tempfile
|
| 1476 |
+
|
| 1477 |
+
from Bio import AlignIO, Phylo, SeqIO
|
| 1478 |
+
from Bio.Align.Applications import ClustalwCommandline, MuscleCommandline
|
| 1479 |
+
|
| 1480 |
+
# Create output directory if it doesn't exist
|
| 1481 |
+
if not os.path.exists(output_dir):
|
| 1482 |
+
os.makedirs(output_dir)
|
| 1483 |
+
|
| 1484 |
+
# Initialize log
|
| 1485 |
+
log = []
|
| 1486 |
+
log.append(f"Phylogenetic Analysis - {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
| 1487 |
+
log.append("=" * 50)
|
| 1488 |
+
|
| 1489 |
+
# Check if input is a file path or string content
|
| 1490 |
+
if os.path.isfile(fasta_sequences):
|
| 1491 |
+
input_file = fasta_sequences
|
| 1492 |
+
log.append(f"Using sequences from file: {input_file}")
|
| 1493 |
+
else:
|
| 1494 |
+
# Create temporary file with the string content
|
| 1495 |
+
temp_fasta = tempfile.NamedTemporaryFile(delete=False, suffix=".fasta", dir=output_dir)
|
| 1496 |
+
temp_fasta.write(fasta_sequences.encode())
|
| 1497 |
+
temp_fasta.close()
|
| 1498 |
+
input_file = temp_fasta.name
|
| 1499 |
+
log.append(f"Created temporary FASTA file from provided sequences: {input_file}")
|
| 1500 |
+
|
| 1501 |
+
# Count sequences
|
| 1502 |
+
try:
|
| 1503 |
+
sequences = list(SeqIO.parse(input_file, "fasta"))
|
| 1504 |
+
log.append(f"Loaded {len(sequences)} protein sequences")
|
| 1505 |
+
except Exception as e:
|
| 1506 |
+
log.append(f"Warning: Could not parse sequences as FASTA: {str(e)}")
|
| 1507 |
+
# This might be pre-aligned data already
|
| 1508 |
+
sequences = []
|
| 1509 |
+
|
| 1510 |
+
# Create filenames for outputs
|
| 1511 |
+
base_name = os.path.splitext(os.path.basename(input_file))[0]
|
| 1512 |
+
alignment_file = os.path.join(output_dir, f"{base_name}_aligned.aln")
|
| 1513 |
+
tree_file = os.path.join(output_dir, f"{base_name}_tree.nwk")
|
| 1514 |
+
tree_image = os.path.join(output_dir, f"{base_name}_phylogeny.png")
|
| 1515 |
+
|
| 1516 |
+
# Perform multiple sequence alignment
|
| 1517 |
+
log.append("\nStep 1: Multiple Sequence Alignment")
|
| 1518 |
+
|
| 1519 |
+
# Special case for pre-aligned input
|
| 1520 |
+
if alignment_method.lower() == "pre-aligned":
|
| 1521 |
+
log.append("Using pre-aligned sequences")
|
| 1522 |
+
try:
|
| 1523 |
+
# If the input is already an alignment file, copy it to the alignment_file path
|
| 1524 |
+
with open(input_file) as src, open(alignment_file, "w") as dst:
|
| 1525 |
+
dst.write(src.read())
|
| 1526 |
+
log.append(f"Copied pre-aligned file to: {alignment_file}")
|
| 1527 |
+
except Exception as e:
|
| 1528 |
+
log.append(f"Error processing pre-aligned sequences: {str(e)}")
|
| 1529 |
+
return "\n".join(log)
|
| 1530 |
+
elif alignment_method.lower() == "clustalw":
|
| 1531 |
+
log.append("Using Clustal Omega for alignment")
|
| 1532 |
+
try:
|
| 1533 |
+
clustalw_cline = ClustalwCommandline("clustalw", infile=input_file, outfile=alignment_file)
|
| 1534 |
+
stdout, stderr = clustalw_cline()
|
| 1535 |
+
log.append("Alignment completed successfully")
|
| 1536 |
+
except Exception as e:
|
| 1537 |
+
log.append(f"Error during alignment: {str(e)}")
|
| 1538 |
+
# Try alternative approach using MUSCLE if ClustalW fails
|
| 1539 |
+
alignment_method = "muscle"
|
| 1540 |
+
|
| 1541 |
+
if alignment_method.lower() == "muscle":
|
| 1542 |
+
log.append("Using MUSCLE for alignment")
|
| 1543 |
+
try:
|
| 1544 |
+
muscle_cline = MuscleCommandline("muscle", input=input_file, out=alignment_file)
|
| 1545 |
+
stdout, stderr = muscle_cline()
|
| 1546 |
+
log.append("Alignment completed successfully")
|
| 1547 |
+
except Exception as e:
|
| 1548 |
+
log.append(f"Error during MUSCLE alignment: {str(e)}")
|
| 1549 |
+
log.append("Attempting to use Biopython's built-in pairwise2 alignment as fallback")
|
| 1550 |
+
|
| 1551 |
+
from Bio import pairwise2
|
| 1552 |
+
|
| 1553 |
+
# Create a simple progressive alignment
|
| 1554 |
+
ref_seq = sequences[0]
|
| 1555 |
+
alignments = []
|
| 1556 |
+
|
| 1557 |
+
for seq in sequences:
|
| 1558 |
+
alignment = pairwise2.align.globalxx(ref_seq.seq, seq.seq)[0]
|
| 1559 |
+
alignments.append(alignment)
|
| 1560 |
+
|
| 1561 |
+
# Write alignment to file in CLUSTAL format
|
| 1562 |
+
with open(alignment_file, "w") as f:
|
| 1563 |
+
f.write("CLUSTAL W (1.83) multiple sequence alignment\n\n")
|
| 1564 |
+
for i, seq in enumerate(sequences):
|
| 1565 |
+
f.write(f"{seq.id.ljust(10)} {alignments[i][1]}\n")
|
| 1566 |
+
# Add consensus line with asterisks
|
| 1567 |
+
f.write(" " * 10 + " " * len(alignments[0][1]) + "\n")
|
| 1568 |
+
|
| 1569 |
+
log.append("Created basic alignment using Biopython's pairwise2")
|
| 1570 |
+
|
| 1571 |
+
# Verify alignment file exists
|
| 1572 |
+
if not os.path.exists(alignment_file):
|
| 1573 |
+
log.append(f"Error: Alignment file {alignment_file} does not exist")
|
| 1574 |
+
return "\n".join(log)
|
| 1575 |
+
|
| 1576 |
+
# Build phylogenetic tree
|
| 1577 |
+
log.append("\nStep 2: Phylogenetic Tree Construction")
|
| 1578 |
+
|
| 1579 |
+
if tree_method.lower() == "iqtree":
|
| 1580 |
+
log.append("Using IQ-TREE for phylogenetic tree construction")
|
| 1581 |
+
try:
|
| 1582 |
+
cmd = f"iqtree -s {alignment_file} -m LG -bb 1000 -pre {os.path.join(output_dir, base_name)}"
|
| 1583 |
+
subprocess.run(
|
| 1584 |
+
cmd,
|
| 1585 |
+
shell=True,
|
| 1586 |
+
check=True,
|
| 1587 |
+
capture_output=True,
|
| 1588 |
+
)
|
| 1589 |
+
# IQ-TREE creates files with .treefile extension
|
| 1590 |
+
iqtree_file = os.path.join(output_dir, f"{base_name}.treefile")
|
| 1591 |
+
if os.path.exists(iqtree_file):
|
| 1592 |
+
# Rename to our standard name
|
| 1593 |
+
os.rename(iqtree_file, tree_file)
|
| 1594 |
+
log.append("Tree construction completed successfully")
|
| 1595 |
+
except Exception as e:
|
| 1596 |
+
log.append(f"Error during IQ-TREE execution: {str(e)}")
|
| 1597 |
+
log.append("Falling back to neighbor-joining method")
|
| 1598 |
+
|
| 1599 |
+
try:
|
| 1600 |
+
from Bio.Phylo.TreeConstruction import (
|
| 1601 |
+
DistanceCalculator,
|
| 1602 |
+
DistanceTreeConstructor,
|
| 1603 |
+
)
|
| 1604 |
+
|
| 1605 |
+
# Try to read the alignment in different formats
|
| 1606 |
+
alignment = None
|
| 1607 |
+
for format in ["clustal", "fasta"]:
|
| 1608 |
+
try:
|
| 1609 |
+
alignment = AlignIO.read(alignment_file, format)
|
| 1610 |
+
break
|
| 1611 |
+
except Exception:
|
| 1612 |
+
continue
|
| 1613 |
+
|
| 1614 |
+
if alignment is None:
|
| 1615 |
+
log.append("Could not parse alignment file in any supported format")
|
| 1616 |
+
# Create a simple text-based tree file as a fallback
|
| 1617 |
+
with open(tree_file, "w") as f:
|
| 1618 |
+
f.write("(protein1:0.1,protein2:0.2,(protein3:0.3,protein4:0.4):0.5);")
|
| 1619 |
+
log.append("Created placeholder tree file")
|
| 1620 |
+
else:
|
| 1621 |
+
# Calculate the distance matrix
|
| 1622 |
+
calculator = DistanceCalculator("identity")
|
| 1623 |
+
dm = calculator.get_distance(alignment)
|
| 1624 |
+
|
| 1625 |
+
# Construct the tree
|
| 1626 |
+
constructor = DistanceTreeConstructor()
|
| 1627 |
+
tree = constructor.nj(dm)
|
| 1628 |
+
|
| 1629 |
+
# Write the tree to file
|
| 1630 |
+
Phylo.write(tree, tree_file, "newick")
|
| 1631 |
+
log.append("Created tree using neighbor-joining method")
|
| 1632 |
+
except Exception as e:
|
| 1633 |
+
log.append(f"Error during fallback tree construction: {str(e)}")
|
| 1634 |
+
# Create a simple text-based tree file as a final fallback
|
| 1635 |
+
with open(tree_file, "w") as f:
|
| 1636 |
+
f.write("(protein1:0.1,protein2:0.2,(protein3:0.3,protein4:0.4):0.5);")
|
| 1637 |
+
log.append("Created placeholder tree file as final fallback")
|
| 1638 |
+
else:
|
| 1639 |
+
log.append(f"Unsupported tree method: {tree_method}")
|
| 1640 |
+
return "\n".join(log)
|
| 1641 |
+
|
| 1642 |
+
# Verify tree file exists
|
| 1643 |
+
if not os.path.exists(tree_file):
|
| 1644 |
+
log.append(f"Error: Tree file {tree_file} does not exist")
|
| 1645 |
+
return "\n".join(log)
|
| 1646 |
+
|
| 1647 |
+
# Visualize the tree
|
| 1648 |
+
log.append("\nStep 3: Phylogenetic Tree Visualization")
|
| 1649 |
+
try:
|
| 1650 |
+
import matplotlib
|
| 1651 |
+
|
| 1652 |
+
matplotlib.use("Agg") # Use non-interactive backend
|
| 1653 |
+
import matplotlib.pyplot as plt
|
| 1654 |
+
|
| 1655 |
+
tree = Phylo.read(tree_file, "newick")
|
| 1656 |
+
fig = plt.figure(figsize=(10, len(sequences) * 0.3 if sequences else 5))
|
| 1657 |
+
axes = fig.add_subplot(1, 1, 1)
|
| 1658 |
+
Phylo.draw(tree, axes=axes, do_show=False)
|
| 1659 |
+
plt.savefig(tree_image, dpi=300, bbox_inches="tight")
|
| 1660 |
+
plt.close()
|
| 1661 |
+
log.append(f"Tree visualization saved to: {tree_image}")
|
| 1662 |
+
except Exception as e:
|
| 1663 |
+
log.append(f"Error during tree visualization: {str(e)}")
|
| 1664 |
+
|
| 1665 |
+
# Summary
|
| 1666 |
+
log.append("\nSummary:")
|
| 1667 |
+
log.append(f"- Input sequences: {len(sequences) if sequences else 'pre-aligned data'}")
|
| 1668 |
+
log.append(f"- Alignment file: {alignment_file}")
|
| 1669 |
+
log.append(f"- Phylogenetic tree file: {tree_file}")
|
| 1670 |
+
log.append(f"- Tree visualization: {tree_image}")
|
| 1671 |
+
|
| 1672 |
+
return "\n".join(log)
|
Biomni/biomni/tool/literature.py
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import re
|
| 3 |
+
import time
|
| 4 |
+
from io import BytesIO
|
| 5 |
+
from urllib.parse import urljoin
|
| 6 |
+
|
| 7 |
+
import PyPDF2
|
| 8 |
+
import requests
|
| 9 |
+
from bs4 import BeautifulSoup
|
| 10 |
+
from googlesearch import search
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def fetch_supplementary_info_from_doi(doi: str, output_dir: str = "supplementary_info"):
|
| 14 |
+
"""Fetches supplementary information for a paper given its DOI and returns a research log.
|
| 15 |
+
|
| 16 |
+
Args:
|
| 17 |
+
doi: The paper DOI.
|
| 18 |
+
output_dir: Directory to save supplementary files.
|
| 19 |
+
|
| 20 |
+
Returns:
|
| 21 |
+
dict: A dictionary containing a research log and the downloaded file paths.
|
| 22 |
+
|
| 23 |
+
"""
|
| 24 |
+
research_log = []
|
| 25 |
+
research_log.append(f"Starting process for DOI: {doi}")
|
| 26 |
+
|
| 27 |
+
# CrossRef API to resolve DOI to a publisher page
|
| 28 |
+
crossref_url = f"https://doi.org/{doi}"
|
| 29 |
+
headers = {"User-Agent": "Mozilla/5.0"}
|
| 30 |
+
response = requests.get(crossref_url, headers=headers)
|
| 31 |
+
|
| 32 |
+
if response.status_code != 200:
|
| 33 |
+
log_message = f"Failed to resolve DOI: {doi}. Status Code: {response.status_code}"
|
| 34 |
+
research_log.append(log_message)
|
| 35 |
+
return {"log": research_log, "files": []}
|
| 36 |
+
|
| 37 |
+
publisher_url = response.url
|
| 38 |
+
research_log.append(f"Resolved DOI to publisher page: {publisher_url}")
|
| 39 |
+
|
| 40 |
+
# Fetch publisher page
|
| 41 |
+
response = requests.get(publisher_url, headers=headers)
|
| 42 |
+
if response.status_code != 200:
|
| 43 |
+
log_message = f"Failed to access publisher page for DOI {doi}."
|
| 44 |
+
research_log.append(log_message)
|
| 45 |
+
return {"log": research_log, "files": []}
|
| 46 |
+
|
| 47 |
+
# Parse page content
|
| 48 |
+
soup = BeautifulSoup(response.content, "html.parser")
|
| 49 |
+
supplementary_links = []
|
| 50 |
+
|
| 51 |
+
# Look for supplementary materials by keywords or links
|
| 52 |
+
for link in soup.find_all("a", href=True):
|
| 53 |
+
href = link.get("href")
|
| 54 |
+
text = link.get_text().lower()
|
| 55 |
+
if "supplementary" in text or "supplemental" in text or "appendix" in text:
|
| 56 |
+
full_url = urljoin(publisher_url, href)
|
| 57 |
+
supplementary_links.append(full_url)
|
| 58 |
+
research_log.append(f"Found supplementary material link: {full_url}")
|
| 59 |
+
|
| 60 |
+
if not supplementary_links:
|
| 61 |
+
log_message = f"No supplementary materials found for DOI {doi}."
|
| 62 |
+
research_log.append(log_message)
|
| 63 |
+
return research_log
|
| 64 |
+
|
| 65 |
+
# Create output directory
|
| 66 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 67 |
+
research_log.append(f"Created output directory: {output_dir}")
|
| 68 |
+
|
| 69 |
+
# Download supplementary materials
|
| 70 |
+
downloaded_files = []
|
| 71 |
+
for link in supplementary_links:
|
| 72 |
+
file_name = os.path.join(output_dir, link.split("/")[-1])
|
| 73 |
+
file_response = requests.get(link, headers=headers)
|
| 74 |
+
if file_response.status_code == 200:
|
| 75 |
+
with open(file_name, "wb") as f:
|
| 76 |
+
f.write(file_response.content)
|
| 77 |
+
downloaded_files.append(file_name)
|
| 78 |
+
research_log.append(f"Downloaded file: {file_name}")
|
| 79 |
+
else:
|
| 80 |
+
research_log.append(f"Failed to download file from {link}")
|
| 81 |
+
|
| 82 |
+
if downloaded_files:
|
| 83 |
+
research_log.append(f"Successfully downloaded {len(downloaded_files)} file(s).")
|
| 84 |
+
else:
|
| 85 |
+
research_log.append(f"No files could be downloaded for DOI {doi}.")
|
| 86 |
+
|
| 87 |
+
return "\n".join(research_log)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def query_arxiv(query: str, max_papers: int = 10) -> str:
|
| 91 |
+
"""Query arXiv for papers based on the provided search query.
|
| 92 |
+
|
| 93 |
+
Parameters
|
| 94 |
+
----------
|
| 95 |
+
- query (str): The search query string.
|
| 96 |
+
- max_papers (int): The maximum number of papers to retrieve (default: 10).
|
| 97 |
+
|
| 98 |
+
Returns
|
| 99 |
+
-------
|
| 100 |
+
- str: The formatted search results or an error message.
|
| 101 |
+
|
| 102 |
+
"""
|
| 103 |
+
import arxiv
|
| 104 |
+
|
| 105 |
+
try:
|
| 106 |
+
client = arxiv.Client()
|
| 107 |
+
search = arxiv.Search(query=query, max_results=max_papers, sort_by=arxiv.SortCriterion.Relevance)
|
| 108 |
+
results = "\n\n".join([f"Title: {paper.title}\nSummary: {paper.summary}" for paper in client.results(search)])
|
| 109 |
+
return results if results else "No papers found on arXiv."
|
| 110 |
+
except Exception as e:
|
| 111 |
+
return f"Error querying arXiv: {e}"
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def query_scholar(query: str) -> str:
|
| 115 |
+
"""Query Google Scholar for papers based on the provided search query.
|
| 116 |
+
|
| 117 |
+
Parameters
|
| 118 |
+
----------
|
| 119 |
+
- query (str): The search query string.
|
| 120 |
+
|
| 121 |
+
Returns
|
| 122 |
+
-------
|
| 123 |
+
- str: The first search result formatted or an error message.
|
| 124 |
+
|
| 125 |
+
"""
|
| 126 |
+
from scholarly import ProxyGenerator, scholarly
|
| 127 |
+
|
| 128 |
+
# Set up a ProxyGenerator object to use free proxies
|
| 129 |
+
# This needs to be done only once per session
|
| 130 |
+
pg = ProxyGenerator()
|
| 131 |
+
pg.FreeProxies()
|
| 132 |
+
scholarly.use_proxy(pg)
|
| 133 |
+
try:
|
| 134 |
+
search_query = scholarly.search_pubs(query)
|
| 135 |
+
result = next(search_query, None)
|
| 136 |
+
if result:
|
| 137 |
+
return f"Title: {result['bib']['title']}\nYear: {result['bib']['pub_year']}\nVenue: {result['bib']['venue']}\nAbstract: {result['bib']['abstract']}"
|
| 138 |
+
else:
|
| 139 |
+
return "No results found on Google Scholar."
|
| 140 |
+
except Exception as e:
|
| 141 |
+
return f"Error querying Google Scholar: {e}"
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def query_pubmed(query: str, max_papers: int = 10, max_retries: int = 3) -> str:
|
| 145 |
+
"""Query PubMed for papers based on the provided search query.
|
| 146 |
+
|
| 147 |
+
Parameters
|
| 148 |
+
----------
|
| 149 |
+
- query (str): The search query string.
|
| 150 |
+
- max_papers (int): The maximum number of papers to retrieve (default: 10).
|
| 151 |
+
- max_retries (int): Maximum number of retry attempts with modified queries (default: 3).
|
| 152 |
+
|
| 153 |
+
Returns
|
| 154 |
+
-------
|
| 155 |
+
- str: The formatted search results or an error message.
|
| 156 |
+
|
| 157 |
+
"""
|
| 158 |
+
from pymed import PubMed
|
| 159 |
+
|
| 160 |
+
try:
|
| 161 |
+
pubmed = PubMed(tool="MyTool", email="your-email@example.com") # Update with a valid email address
|
| 162 |
+
|
| 163 |
+
# Initial attempt
|
| 164 |
+
papers = list(pubmed.query(query, max_results=max_papers))
|
| 165 |
+
|
| 166 |
+
# Retry with modified queries if no results
|
| 167 |
+
retries = 0
|
| 168 |
+
while not papers and retries < max_retries:
|
| 169 |
+
retries += 1
|
| 170 |
+
# Simplify query with each retry by removing the last word
|
| 171 |
+
simplified_query = " ".join(query.split()[:-retries]) if len(query.split()) > retries else query
|
| 172 |
+
time.sleep(1) # Add delay between requests
|
| 173 |
+
papers = list(pubmed.query(simplified_query, max_results=max_papers))
|
| 174 |
+
|
| 175 |
+
if papers:
|
| 176 |
+
results = "\n\n".join(
|
| 177 |
+
[f"Title: {paper.title}\nAbstract: {paper.abstract}\nJournal: {paper.journal}" for paper in papers]
|
| 178 |
+
)
|
| 179 |
+
return results
|
| 180 |
+
else:
|
| 181 |
+
return "No papers found on PubMed after multiple query attempts."
|
| 182 |
+
except Exception as e:
|
| 183 |
+
return f"Error querying PubMed: {e}"
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
def search_google(query: str, num_results: int = 3, language: str = "en") -> list[dict]:
|
| 187 |
+
"""Search using Google search.
|
| 188 |
+
|
| 189 |
+
Args:
|
| 190 |
+
query (str): The search query (e.g., "protocol text or seach question")
|
| 191 |
+
num_results (int): Number of results to return (default: 10)
|
| 192 |
+
language (str): Language code for search results (default: 'en')
|
| 193 |
+
pause (float): Pause between searches to avoid rate limiting (default: 2.0 seconds)
|
| 194 |
+
|
| 195 |
+
Returns:
|
| 196 |
+
List[dict]: List of dictionaries containing search results with title and URL
|
| 197 |
+
|
| 198 |
+
"""
|
| 199 |
+
try:
|
| 200 |
+
results_string = ""
|
| 201 |
+
search_query = f"{query}"
|
| 202 |
+
|
| 203 |
+
print(f"Searching for {search_query} with {num_results} results and {language} language")
|
| 204 |
+
|
| 205 |
+
for res in search(search_query, num_results=num_results, lang=language, advanced=True):
|
| 206 |
+
print(f"Found result: {res.title}")
|
| 207 |
+
title = res.title
|
| 208 |
+
url = res.url
|
| 209 |
+
description = res.description
|
| 210 |
+
|
| 211 |
+
results_string += f"Title: {title}\nURL: {url}\nDescription: {description}\n\n"
|
| 212 |
+
|
| 213 |
+
except Exception as e:
|
| 214 |
+
print(f"Error performing search: {str(e)}")
|
| 215 |
+
return results_string
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
def advanced_web_search_claude(
|
| 219 |
+
query: str,
|
| 220 |
+
max_searches: int = 1,
|
| 221 |
+
max_retries: int = 3,
|
| 222 |
+
) -> tuple[str, list[dict[str, str]], list]:
|
| 223 |
+
"""
|
| 224 |
+
Initiate an advanced web search by launching a specialized agent to collect relevant information and citations through multiple rounds of web searches for a given query.
|
| 225 |
+
Craft the query carefully for the search agent to find the most relevant information.
|
| 226 |
+
|
| 227 |
+
Parameters
|
| 228 |
+
----------
|
| 229 |
+
query : str
|
| 230 |
+
The search phrase you want Claude to look up.
|
| 231 |
+
max_searches : int, optional
|
| 232 |
+
Upper-bound on searches Claude may issue inside this request.
|
| 233 |
+
max_retries : int, optional
|
| 234 |
+
Maximum number of retry attempts with exponential backoff.
|
| 235 |
+
|
| 236 |
+
Returns
|
| 237 |
+
-------
|
| 238 |
+
full_text : str
|
| 239 |
+
A formatted string containing the full text response from Claude and the citations.
|
| 240 |
+
"""
|
| 241 |
+
import random
|
| 242 |
+
|
| 243 |
+
import anthropic
|
| 244 |
+
|
| 245 |
+
try:
|
| 246 |
+
from biomni.config import default_config
|
| 247 |
+
|
| 248 |
+
model = default_config.llm
|
| 249 |
+
api_key = default_config.api_key
|
| 250 |
+
if not api_key:
|
| 251 |
+
api_key = os.getenv("ANTHROPIC_API_KEY")
|
| 252 |
+
except ImportError:
|
| 253 |
+
model = "claude-4-sonnet-latest"
|
| 254 |
+
api_key = os.getenv("ANTHROPIC_API_KEY")
|
| 255 |
+
|
| 256 |
+
if "claude" not in model:
|
| 257 |
+
raise ValueError("Model must be a Claude model.")
|
| 258 |
+
|
| 259 |
+
if not api_key:
|
| 260 |
+
raise ValueError("Set your api_key explicitly.")
|
| 261 |
+
|
| 262 |
+
client = anthropic.Anthropic(api_key=api_key)
|
| 263 |
+
tool_def = {
|
| 264 |
+
"type": "web_search_20250305",
|
| 265 |
+
"name": "web_search",
|
| 266 |
+
"max_uses": max_searches,
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
delay = random.randint(1, 10)
|
| 270 |
+
|
| 271 |
+
for attempt in range(1, max_retries + 1):
|
| 272 |
+
try:
|
| 273 |
+
response = client.messages.create(
|
| 274 |
+
model=model,
|
| 275 |
+
max_tokens=4096,
|
| 276 |
+
messages=[{"role": "user", "content": query}],
|
| 277 |
+
tools=[tool_def],
|
| 278 |
+
)
|
| 279 |
+
|
| 280 |
+
paragraphs, citations = [], []
|
| 281 |
+
response.content = response.content
|
| 282 |
+
formatted_response = ""
|
| 283 |
+
for blk in response.content:
|
| 284 |
+
if blk.type == "text":
|
| 285 |
+
paragraphs.append(blk.text)
|
| 286 |
+
formatted_response += blk.text
|
| 287 |
+
|
| 288 |
+
if blk.citations:
|
| 289 |
+
for cite in blk.citations:
|
| 290 |
+
citations.append({"url": cite.url, "title": cite.title, "cited_text": cite.cited_text})
|
| 291 |
+
formatted_response += f"(Citation: {cite.title} - {cite.url})"
|
| 292 |
+
return formatted_response
|
| 293 |
+
|
| 294 |
+
except Exception as e:
|
| 295 |
+
if attempt < max_retries:
|
| 296 |
+
time.sleep(delay)
|
| 297 |
+
delay *= 2
|
| 298 |
+
continue
|
| 299 |
+
print(f"Error performing web search after {max_retries} attempts: {str(e)}")
|
| 300 |
+
return f"Error performing web search after {max_retries} attempts: {str(e)}"
|
| 301 |
+
|
| 302 |
+
|
| 303 |
+
def extract_url_content(url: str) -> str:
|
| 304 |
+
"""Extract the text content of a webpage using requests and BeautifulSoup.
|
| 305 |
+
|
| 306 |
+
Args:
|
| 307 |
+
url: Webpage URL to extract content from
|
| 308 |
+
|
| 309 |
+
Returns:
|
| 310 |
+
Text content of the webpage
|
| 311 |
+
|
| 312 |
+
"""
|
| 313 |
+
response = requests.get(url, headers={"User-Agent": "Mozilla/5.0"})
|
| 314 |
+
|
| 315 |
+
# Check if the response is in text format
|
| 316 |
+
if "text/plain" in response.headers.get("Content-Type", "") or "application/json" in response.headers.get(
|
| 317 |
+
"Content-Type", ""
|
| 318 |
+
):
|
| 319 |
+
return response.text.strip() # Return plain text or JSON response directly
|
| 320 |
+
|
| 321 |
+
# If it's HTML, use BeautifulSoup to parse
|
| 322 |
+
soup = BeautifulSoup(response.text, "html.parser")
|
| 323 |
+
|
| 324 |
+
# Try to find main content first, fallback to body
|
| 325 |
+
content = soup.find("main") or soup.find("article") or soup.body
|
| 326 |
+
|
| 327 |
+
# Remove unwanted elements
|
| 328 |
+
for element in content(["script", "style", "nav", "header", "footer", "aside", "iframe"]):
|
| 329 |
+
element.decompose()
|
| 330 |
+
|
| 331 |
+
# Extract text with better formatting
|
| 332 |
+
paragraphs = content.find_all(["p", "h1", "h2", "h3", "h4", "h5", "h6"])
|
| 333 |
+
cleaned_text = []
|
| 334 |
+
|
| 335 |
+
for p in paragraphs:
|
| 336 |
+
text = p.get_text().strip()
|
| 337 |
+
if text: # Only add non-empty paragraphs
|
| 338 |
+
cleaned_text.append(text)
|
| 339 |
+
|
| 340 |
+
return "\n\n".join(cleaned_text)
|
| 341 |
+
|
| 342 |
+
|
| 343 |
+
def extract_pdf_content(url: str) -> str:
|
| 344 |
+
"""Extract the text content of a PDF file given its URL.
|
| 345 |
+
|
| 346 |
+
Args:
|
| 347 |
+
url: URL of the PDF file to extract text from
|
| 348 |
+
|
| 349 |
+
Returns:
|
| 350 |
+
The extracted text content from the PDF
|
| 351 |
+
|
| 352 |
+
"""
|
| 353 |
+
try:
|
| 354 |
+
# Check if the URL ends with .pdf
|
| 355 |
+
if not url.lower().endswith(".pdf"):
|
| 356 |
+
# If not, try to find a PDF link on the page
|
| 357 |
+
response = requests.get(url, timeout=30)
|
| 358 |
+
if response.status_code == 200:
|
| 359 |
+
# Look for PDF links in the HTML content
|
| 360 |
+
pdf_links = re.findall(r'href=[\'"]([^\'"]+\.pdf)[\'"]', response.text)
|
| 361 |
+
if pdf_links:
|
| 362 |
+
# Use the first PDF link found
|
| 363 |
+
if not pdf_links[0].startswith("http"):
|
| 364 |
+
# Handle relative URLs
|
| 365 |
+
base_url = "/".join(url.split("/")[:3])
|
| 366 |
+
url = base_url + pdf_links[0] if pdf_links[0].startswith("/") else base_url + "/" + pdf_links[0]
|
| 367 |
+
else:
|
| 368 |
+
url = pdf_links[0]
|
| 369 |
+
else:
|
| 370 |
+
return f"No PDF file found at {url}. Please provide a direct link to a PDF file."
|
| 371 |
+
|
| 372 |
+
# Download the PDF
|
| 373 |
+
response = requests.get(url, timeout=30)
|
| 374 |
+
|
| 375 |
+
# Check if we actually got a PDF file (by checking content type or magic bytes)
|
| 376 |
+
content_type = response.headers.get("Content-Type", "").lower()
|
| 377 |
+
if "application/pdf" not in content_type and not response.content.startswith(b"%PDF"):
|
| 378 |
+
return f"The URL did not return a valid PDF file. Content type: {content_type}"
|
| 379 |
+
|
| 380 |
+
pdf_file = BytesIO(response.content)
|
| 381 |
+
|
| 382 |
+
# Try with PyPDF2 first
|
| 383 |
+
try:
|
| 384 |
+
text = ""
|
| 385 |
+
pdf_reader = PyPDF2.PdfReader(pdf_file)
|
| 386 |
+
for page_num in range(len(pdf_reader.pages)):
|
| 387 |
+
page = pdf_reader.pages[page_num]
|
| 388 |
+
text += page.extract_text() + "\n\n"
|
| 389 |
+
except Exception as e:
|
| 390 |
+
print(f"Error extracting text from PDF: {str(e)}")
|
| 391 |
+
|
| 392 |
+
# Clean up the text
|
| 393 |
+
text = re.sub(r"\s+", " ", text).strip()
|
| 394 |
+
|
| 395 |
+
if not text:
|
| 396 |
+
return "The PDF file did not contain any extractable text. It may be an image-based PDF requiring OCR."
|
| 397 |
+
|
| 398 |
+
return text
|
| 399 |
+
|
| 400 |
+
except requests.exceptions.RequestException as e:
|
| 401 |
+
return f"Error downloading PDF: {str(e)}"
|
| 402 |
+
except Exception as e:
|
| 403 |
+
return f"Error extracting text from PDF: {str(e)}"
|
Biomni/biomni/tool/pathology.py
ADDED
|
@@ -0,0 +1,1034 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
def analyze_aortic_diameter_and_geometry(image_path, output_dir="./output"):
|
| 2 |
+
"""Analyze aortic diameter and geometry from cardiovascular imaging data.
|
| 3 |
+
|
| 4 |
+
This function processes cardiovascular imaging data (ultrasound or CT/MRI) to
|
| 5 |
+
measure aortic root diameter, ascending aorta diameter, and calculate
|
| 6 |
+
geometric parameters such as tortuosity and dilation indices.
|
| 7 |
+
|
| 8 |
+
Parameters
|
| 9 |
+
----------
|
| 10 |
+
image_path : str
|
| 11 |
+
Path to the cardiovascular imaging data (DICOM, JPG, PNG)
|
| 12 |
+
output_dir : str, optional
|
| 13 |
+
Directory to save output files (default: "./output")
|
| 14 |
+
|
| 15 |
+
Returns
|
| 16 |
+
-------
|
| 17 |
+
str
|
| 18 |
+
Research log summarizing the analysis steps and results
|
| 19 |
+
|
| 20 |
+
"""
|
| 21 |
+
import os
|
| 22 |
+
from datetime import datetime
|
| 23 |
+
|
| 24 |
+
import cv2
|
| 25 |
+
import numpy as np
|
| 26 |
+
|
| 27 |
+
# Create output directory if it doesn't exist
|
| 28 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 29 |
+
|
| 30 |
+
# Initialize research log
|
| 31 |
+
log = []
|
| 32 |
+
log.append(f"Aortic Diameter and Geometry Analysis - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
| 33 |
+
log.append(f"Input image: {image_path}")
|
| 34 |
+
|
| 35 |
+
# Step 1: Load the image
|
| 36 |
+
log.append("\n1. Loading and preprocessing image")
|
| 37 |
+
try:
|
| 38 |
+
# Check if file exists
|
| 39 |
+
if not os.path.exists(image_path):
|
| 40 |
+
return f"Error: File {image_path} not found"
|
| 41 |
+
|
| 42 |
+
# Load image
|
| 43 |
+
image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
|
| 44 |
+
if image is None:
|
| 45 |
+
return f"Error: Could not read image {image_path}"
|
| 46 |
+
|
| 47 |
+
log.append(f" - Image loaded: {image.shape[1]}x{image.shape[0]} pixels")
|
| 48 |
+
|
| 49 |
+
# Basic preprocessing
|
| 50 |
+
image = cv2.GaussianBlur(image, (5, 5), 0)
|
| 51 |
+
log.append(" - Applied Gaussian blur for noise reduction")
|
| 52 |
+
|
| 53 |
+
# Enhance contrast
|
| 54 |
+
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
|
| 55 |
+
image = clahe.apply(image)
|
| 56 |
+
log.append(" - Enhanced contrast using CLAHE")
|
| 57 |
+
|
| 58 |
+
except Exception as e:
|
| 59 |
+
return f"Error during image loading: {str(e)}"
|
| 60 |
+
|
| 61 |
+
# Step 2: Segment the aorta
|
| 62 |
+
log.append("\n2. Aorta segmentation")
|
| 63 |
+
try:
|
| 64 |
+
# Otsu thresholding for initial segmentation
|
| 65 |
+
thresh_val, binary = cv2.threshold(image, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
|
| 66 |
+
log.append(f" - Applied Otsu thresholding (threshold value: {thresh_val})")
|
| 67 |
+
|
| 68 |
+
# Morphological operations to clean up the binary image
|
| 69 |
+
kernel = np.ones((3, 3), np.uint8)
|
| 70 |
+
binary = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel)
|
| 71 |
+
binary = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)
|
| 72 |
+
log.append(" - Applied morphological operations to refine segmentation")
|
| 73 |
+
|
| 74 |
+
# Find contours
|
| 75 |
+
contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
| 76 |
+
log.append(f" - Detected {len(contours)} contours")
|
| 77 |
+
|
| 78 |
+
# Assuming the aorta is one of the largest contours
|
| 79 |
+
if len(contours) == 0:
|
| 80 |
+
return "Error: No contours detected in the image"
|
| 81 |
+
|
| 82 |
+
# Sort contours by area and take the largest
|
| 83 |
+
contours = sorted(contours, key=cv2.contourArea, reverse=True)
|
| 84 |
+
aorta_contour = contours[0] # Assuming the largest contour is the aorta
|
| 85 |
+
log.append(f" - Selected largest contour as aorta (area: {cv2.contourArea(aorta_contour)} px²)")
|
| 86 |
+
|
| 87 |
+
except Exception as e:
|
| 88 |
+
return f"Error during aorta segmentation: {str(e)}"
|
| 89 |
+
|
| 90 |
+
# Step 3: Measure aortic diameters
|
| 91 |
+
log.append("\n3. Measuring aortic diameters")
|
| 92 |
+
try:
|
| 93 |
+
# Create a mask of the aorta
|
| 94 |
+
aorta_mask = np.zeros_like(image)
|
| 95 |
+
cv2.drawContours(aorta_mask, [aorta_contour], 0, 255, -1)
|
| 96 |
+
|
| 97 |
+
# Find the centerline of the aorta (simplified approach)
|
| 98 |
+
# This is a simplified approach - in a real implementation, skeletonization would be better
|
| 99 |
+
M = cv2.moments(aorta_contour)
|
| 100 |
+
if M["m00"] != 0:
|
| 101 |
+
cx = int(M["m10"] / M["m00"])
|
| 102 |
+
cy = int(M["m01"] / M["m00"])
|
| 103 |
+
log.append(f" - Aorta centroid: ({cx}, {cy})")
|
| 104 |
+
else:
|
| 105 |
+
cx, cy = 0, 0
|
| 106 |
+
log.append(" - Warning: Could not calculate aorta centroid")
|
| 107 |
+
|
| 108 |
+
# Measure aortic root diameter (assuming it's at the bottom of the contour)
|
| 109 |
+
aortic_root_points = sorted(aorta_contour.reshape(-1, 2), key=lambda p: p[1], reverse=True)[:20]
|
| 110 |
+
aortic_root_diameter = np.max([p[0] for p in aortic_root_points]) - np.min([p[0] for p in aortic_root_points])
|
| 111 |
+
log.append(f" - Aortic root diameter: {aortic_root_diameter:.2f} pixels")
|
| 112 |
+
|
| 113 |
+
# Measure ascending aorta diameter (assuming it's at the middle of the contour)
|
| 114 |
+
y_mid = (
|
| 115 |
+
np.min([p[1] for p in aorta_contour.reshape(-1, 2)]) + np.max([p[1] for p in aorta_contour.reshape(-1, 2)])
|
| 116 |
+
) // 2
|
| 117 |
+
ascending_points = [p for p in aorta_contour.reshape(-1, 2) if abs(p[1] - y_mid) < 10]
|
| 118 |
+
if ascending_points:
|
| 119 |
+
ascending_diameter = np.max([p[0] for p in ascending_points]) - np.min([p[0] for p in ascending_points])
|
| 120 |
+
log.append(f" - Ascending aorta diameter: {ascending_diameter:.2f} pixels")
|
| 121 |
+
else:
|
| 122 |
+
ascending_diameter = 0
|
| 123 |
+
log.append(" - Warning: Could not measure ascending aorta diameter")
|
| 124 |
+
|
| 125 |
+
except Exception as e:
|
| 126 |
+
return f"Error during diameter measurements: {str(e)}"
|
| 127 |
+
|
| 128 |
+
# Step 4: Calculate geometric parameters
|
| 129 |
+
log.append("\n4. Calculating aortic geometry parameters")
|
| 130 |
+
try:
|
| 131 |
+
# Calculate tortuosity (simplified as the ratio of contour length to end-to-end distance)
|
| 132 |
+
contour_length = cv2.arcLength(aorta_contour, closed=True)
|
| 133 |
+
|
| 134 |
+
# Find the two most distant points (approximation for end-to-end distance)
|
| 135 |
+
hull = cv2.convexHull(aorta_contour)
|
| 136 |
+
hull_points = hull.reshape(-1, 2)
|
| 137 |
+
max_dist = 0
|
| 138 |
+
for i in range(len(hull_points)):
|
| 139 |
+
for j in range(i + 1, len(hull_points)):
|
| 140 |
+
dist = np.sqrt(
|
| 141 |
+
(hull_points[i][0] - hull_points[j][0]) ** 2 + (hull_points[i][1] - hull_points[j][1]) ** 2
|
| 142 |
+
)
|
| 143 |
+
max_dist = max(max_dist, dist)
|
| 144 |
+
|
| 145 |
+
if max_dist > 0:
|
| 146 |
+
tortuosity = contour_length / max_dist
|
| 147 |
+
log.append(f" - Aortic tortuosity index: {tortuosity:.2f}")
|
| 148 |
+
else:
|
| 149 |
+
tortuosity = 0
|
| 150 |
+
log.append(" - Warning: Could not calculate tortuosity")
|
| 151 |
+
|
| 152 |
+
# Calculate dilation index (ratio of max diameter to min diameter)
|
| 153 |
+
min_diameter = min(aortic_root_diameter, ascending_diameter)
|
| 154 |
+
max_diameter = max(aortic_root_diameter, ascending_diameter)
|
| 155 |
+
|
| 156 |
+
if min_diameter > 0:
|
| 157 |
+
dilation_index = max_diameter / min_diameter
|
| 158 |
+
log.append(f" - Aortic dilation index: {dilation_index:.2f}")
|
| 159 |
+
else:
|
| 160 |
+
dilation_index = 0
|
| 161 |
+
log.append(" - Warning: Could not calculate dilation index")
|
| 162 |
+
|
| 163 |
+
except Exception as e:
|
| 164 |
+
return f"Error during geometry calculations: {str(e)}"
|
| 165 |
+
|
| 166 |
+
# Step 5: Save results
|
| 167 |
+
log.append("\n5. Saving results")
|
| 168 |
+
try:
|
| 169 |
+
# Create a visual output
|
| 170 |
+
output_image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
|
| 171 |
+
cv2.drawContours(output_image, [aorta_contour], 0, (0, 255, 0), 2)
|
| 172 |
+
|
| 173 |
+
# Mark diameters
|
| 174 |
+
cv2.circle(output_image, (cx, cy), 5, (0, 0, 255), -1)
|
| 175 |
+
|
| 176 |
+
# Save the output image
|
| 177 |
+
output_filename = os.path.join(output_dir, f"aorta_analysis_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png")
|
| 178 |
+
cv2.imwrite(output_filename, output_image)
|
| 179 |
+
log.append(f" - Saved annotated image to: {output_filename}")
|
| 180 |
+
|
| 181 |
+
# Save measurements to a text file
|
| 182 |
+
measurements_filename = os.path.join(
|
| 183 |
+
output_dir,
|
| 184 |
+
f"aorta_measurements_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt",
|
| 185 |
+
)
|
| 186 |
+
with open(measurements_filename, "w") as f:
|
| 187 |
+
f.write(f"Aortic Root Diameter: {aortic_root_diameter:.2f} pixels\n")
|
| 188 |
+
f.write(f"Ascending Aorta Diameter: {ascending_diameter:.2f} pixels\n")
|
| 189 |
+
f.write(f"Tortuosity Index: {tortuosity:.2f}\n")
|
| 190 |
+
f.write(f"Dilation Index: {dilation_index:.2f}\n")
|
| 191 |
+
log.append(f" - Saved measurements to: {measurements_filename}")
|
| 192 |
+
|
| 193 |
+
except Exception as e:
|
| 194 |
+
return f"Error during saving results: {str(e)}"
|
| 195 |
+
|
| 196 |
+
# Step 6: Summarize findings
|
| 197 |
+
log.append("\n6. Summary of findings")
|
| 198 |
+
log.append(f" - Aortic root diameter: {aortic_root_diameter:.2f} pixels")
|
| 199 |
+
log.append(f" - Ascending aorta diameter: {ascending_diameter:.2f} pixels")
|
| 200 |
+
log.append(f" - Tortuosity index: {tortuosity:.2f}")
|
| 201 |
+
log.append(f" - Dilation index: {dilation_index:.2f}")
|
| 202 |
+
|
| 203 |
+
return "\n".join(log)
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
def analyze_atp_luminescence_assay(
|
| 207 |
+
data_file,
|
| 208 |
+
standard_curve_file,
|
| 209 |
+
normalization_method="cell_count",
|
| 210 |
+
normalization_data=None,
|
| 211 |
+
):
|
| 212 |
+
"""Analyze luminescence-based ATP assay data to determine intracellular ATP concentration.
|
| 213 |
+
|
| 214 |
+
Parameters
|
| 215 |
+
----------
|
| 216 |
+
data_file : str
|
| 217 |
+
Path to CSV file containing luminescence readings from samples.
|
| 218 |
+
Expected format: columns for 'Sample_ID' and 'Luminescence_Value'.
|
| 219 |
+
|
| 220 |
+
standard_curve_file : str
|
| 221 |
+
Path to CSV file containing standard curve data.
|
| 222 |
+
Expected format: columns for 'ATP_Concentration' (in nM) and 'Luminescence_Value'.
|
| 223 |
+
|
| 224 |
+
normalization_method : str, optional
|
| 225 |
+
Method used to normalize ATP values, either 'cell_count' or 'protein_content'.
|
| 226 |
+
Default is 'cell_count'.
|
| 227 |
+
|
| 228 |
+
normalization_data : str or dict, optional
|
| 229 |
+
Either path to CSV file with normalization data or dictionary with sample IDs as keys.
|
| 230 |
+
For 'cell_count': values should be cell counts per sample.
|
| 231 |
+
For 'protein_content': values should be protein concentration (μg/mL).
|
| 232 |
+
|
| 233 |
+
Returns
|
| 234 |
+
-------
|
| 235 |
+
str
|
| 236 |
+
Research log summarizing the ATP content measurement process and results.
|
| 237 |
+
|
| 238 |
+
"""
|
| 239 |
+
from datetime import datetime
|
| 240 |
+
|
| 241 |
+
import numpy as np
|
| 242 |
+
import pandas as pd
|
| 243 |
+
|
| 244 |
+
log = []
|
| 245 |
+
log.append(f"ATP Content Measurement Analysis - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
| 246 |
+
log.append("=" * 50)
|
| 247 |
+
|
| 248 |
+
# Step 1: Load the raw luminescence data
|
| 249 |
+
log.append("Step 1: Loading raw luminescence data")
|
| 250 |
+
try:
|
| 251 |
+
sample_data = pd.read_csv(data_file)
|
| 252 |
+
log.append(f"Successfully loaded data from {data_file}")
|
| 253 |
+
log.append(f"Number of samples: {len(sample_data)}")
|
| 254 |
+
except Exception as e:
|
| 255 |
+
log.append(f"Error loading sample data: {str(e)}")
|
| 256 |
+
return "\n".join(log)
|
| 257 |
+
|
| 258 |
+
# Step 2: Load the standard curve data
|
| 259 |
+
log.append("\nStep 2: Loading ATP standard curve data")
|
| 260 |
+
try:
|
| 261 |
+
std_curve_data = pd.read_csv(standard_curve_file)
|
| 262 |
+
log.append(f"Successfully loaded standard curve from {standard_curve_file}")
|
| 263 |
+
except Exception as e:
|
| 264 |
+
log.append(f"Error loading standard curve data: {str(e)}")
|
| 265 |
+
return "\n".join(log)
|
| 266 |
+
|
| 267 |
+
# Step 3: Generate standard curve equation (linear regression)
|
| 268 |
+
log.append("\nStep 3: Generating ATP standard curve")
|
| 269 |
+
try:
|
| 270 |
+
x = std_curve_data["Luminescence_Value"]
|
| 271 |
+
y = std_curve_data["ATP_Concentration"]
|
| 272 |
+
slope, intercept = np.polyfit(x, y, 1)
|
| 273 |
+
log.append(f"Standard curve equation: ATP_Concentration = {slope:.6f} × Luminescence + {intercept:.6f}")
|
| 274 |
+
except Exception as e:
|
| 275 |
+
log.append(f"Error generating standard curve: {str(e)}")
|
| 276 |
+
return "\n".join(log)
|
| 277 |
+
|
| 278 |
+
# Step 4: Calculate ATP concentrations for samples
|
| 279 |
+
log.append("\nStep 4: Calculating ATP concentrations for samples")
|
| 280 |
+
try:
|
| 281 |
+
sample_data["ATP_Concentration_nM"] = slope * sample_data["Luminescence_Value"] + intercept
|
| 282 |
+
log.append("ATP concentrations calculated for all samples")
|
| 283 |
+
except Exception as e:
|
| 284 |
+
log.append(f"Error calculating ATP concentrations: {str(e)}")
|
| 285 |
+
return "\n".join(log)
|
| 286 |
+
|
| 287 |
+
# Step 5: Normalize ATP concentrations
|
| 288 |
+
log.append(f"\nStep 5: Normalizing ATP concentrations by {normalization_method}")
|
| 289 |
+
|
| 290 |
+
# Load normalization data
|
| 291 |
+
if normalization_data:
|
| 292 |
+
try:
|
| 293 |
+
if isinstance(normalization_data, str):
|
| 294 |
+
norm_data = pd.read_csv(normalization_data)
|
| 295 |
+
norm_dict = dict(zip(norm_data["Sample_ID"], norm_data[normalization_method], strict=False))
|
| 296 |
+
else:
|
| 297 |
+
norm_dict = normalization_data
|
| 298 |
+
|
| 299 |
+
# Apply normalization
|
| 300 |
+
for idx, row in sample_data.iterrows():
|
| 301 |
+
sample_id = row["Sample_ID"]
|
| 302 |
+
if sample_id in norm_dict:
|
| 303 |
+
if normalization_method == "cell_count":
|
| 304 |
+
sample_data.at[idx, "ATP_pmol_per_million_cells"] = (
|
| 305 |
+
row["ATP_Concentration_nM"] / norm_dict[sample_id] * 1000
|
| 306 |
+
)
|
| 307 |
+
elif normalization_method == "protein_content":
|
| 308 |
+
sample_data.at[idx, "ATP_nmol_per_mg_protein"] = (
|
| 309 |
+
row["ATP_Concentration_nM"] / norm_dict[sample_id]
|
| 310 |
+
)
|
| 311 |
+
|
| 312 |
+
log.append(f"Successfully normalized ATP concentrations by {normalization_method}")
|
| 313 |
+
except Exception as e:
|
| 314 |
+
log.append(f"Error during normalization: {str(e)}")
|
| 315 |
+
log.append("Continuing with unnormalized ATP concentrations")
|
| 316 |
+
else:
|
| 317 |
+
log.append("No normalization data provided. Reporting unnormalized ATP concentrations.")
|
| 318 |
+
|
| 319 |
+
# Step 6: Save results
|
| 320 |
+
output_file = "atp_measurement_results.csv"
|
| 321 |
+
try:
|
| 322 |
+
sample_data.to_csv(output_file, index=False)
|
| 323 |
+
log.append(f"\nStep 6: Results saved to {output_file}")
|
| 324 |
+
except Exception as e:
|
| 325 |
+
log.append(f"\nStep 6: Error saving results: {str(e)}")
|
| 326 |
+
|
| 327 |
+
# Step 7: Summary statistics
|
| 328 |
+
log.append("\nStep 7: Summary of ATP measurement results")
|
| 329 |
+
try:
|
| 330 |
+
log.append("ATP Concentration Summary (nM):")
|
| 331 |
+
log.append(f" Mean: {sample_data['ATP_Concentration_nM'].mean():.2f}")
|
| 332 |
+
log.append(f" Median: {sample_data['ATP_Concentration_nM'].median():.2f}")
|
| 333 |
+
log.append(f" Min: {sample_data['ATP_Concentration_nM'].min():.2f}")
|
| 334 |
+
log.append(f" Max: {sample_data['ATP_Concentration_nM'].max():.2f}")
|
| 335 |
+
|
| 336 |
+
if normalization_method == "cell_count" and "ATP_pmol_per_million_cells" in sample_data.columns:
|
| 337 |
+
log.append("\nATP per Million Cells (pmol):")
|
| 338 |
+
log.append(f" Mean: {sample_data['ATP_pmol_per_million_cells'].mean():.2f}")
|
| 339 |
+
log.append(f" Median: {sample_data['ATP_pmol_per_million_cells'].median():.2f}")
|
| 340 |
+
elif normalization_method == "protein_content" and "ATP_nmol_per_mg_protein" in sample_data.columns:
|
| 341 |
+
log.append("\nATP per mg Protein (nmol):")
|
| 342 |
+
log.append(f" Mean: {sample_data['ATP_nmol_per_mg_protein'].mean():.2f}")
|
| 343 |
+
log.append(f" Median: {sample_data['ATP_nmol_per_mg_protein'].median():.2f}")
|
| 344 |
+
except Exception as e:
|
| 345 |
+
log.append(f"Error generating summary statistics: {str(e)}")
|
| 346 |
+
|
| 347 |
+
log.append("\nATP Content Measurement Analysis Complete")
|
| 348 |
+
|
| 349 |
+
return "\n".join(log)
|
| 350 |
+
|
| 351 |
+
|
| 352 |
+
def analyze_thrombus_histology(image_path, output_dir="./output"):
|
| 353 |
+
"""Analyze histological images of thrombus samples stained with H&E to identify and quantify
|
| 354 |
+
different thrombus components (fresh, cellular lysis, endothelialization, fibroblastic reaction).
|
| 355 |
+
|
| 356 |
+
Parameters
|
| 357 |
+
----------
|
| 358 |
+
image_path : str
|
| 359 |
+
Path to the histological image of thrombus sample stained with H&E
|
| 360 |
+
output_dir : str, optional
|
| 361 |
+
Directory to save output files (default: "./output")
|
| 362 |
+
|
| 363 |
+
Returns
|
| 364 |
+
-------
|
| 365 |
+
str
|
| 366 |
+
Research log summarizing the analysis steps and results
|
| 367 |
+
|
| 368 |
+
"""
|
| 369 |
+
import os
|
| 370 |
+
from datetime import datetime
|
| 371 |
+
|
| 372 |
+
import cv2
|
| 373 |
+
import numpy as np
|
| 374 |
+
from skimage import color
|
| 375 |
+
|
| 376 |
+
# Create output directory if it doesn't exist
|
| 377 |
+
if not os.path.exists(output_dir):
|
| 378 |
+
os.makedirs(output_dir)
|
| 379 |
+
|
| 380 |
+
# Initialize research log
|
| 381 |
+
log = f"# Thrombus Component Analysis - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
| 382 |
+
log += f"Analyzing image: {image_path}\n\n"
|
| 383 |
+
|
| 384 |
+
# Step 1: Load and preprocess the image
|
| 385 |
+
log += "## Step 1: Image Loading and Preprocessing\n"
|
| 386 |
+
original_image = cv2.imread(image_path)
|
| 387 |
+
if original_image is None:
|
| 388 |
+
return f"Error: Could not load image from {image_path}"
|
| 389 |
+
|
| 390 |
+
# Convert to RGB (from BGR)
|
| 391 |
+
rgb_image = cv2.cvtColor(original_image, cv2.COLOR_BGR2RGB)
|
| 392 |
+
log += f"- Loaded image with dimensions: {rgb_image.shape[1]}x{rgb_image.shape[0]} pixels\n"
|
| 393 |
+
|
| 394 |
+
# Convert to LAB color space for better color segmentation
|
| 395 |
+
lab_image = color.rgb2lab(rgb_image)
|
| 396 |
+
log += "- Converted image to LAB color space for improved color-based segmentation\n\n"
|
| 397 |
+
|
| 398 |
+
# Step 2: Segment thrombus components based on color characteristics
|
| 399 |
+
log += "## Step 2: Thrombus Component Segmentation\n"
|
| 400 |
+
|
| 401 |
+
# Create masks for different components based on color thresholds
|
| 402 |
+
# These thresholds are approximate and may need adjustment for specific staining protocols
|
| 403 |
+
|
| 404 |
+
# Fresh thrombus: typically red (erythrocytes) with blue nuclei (leukocytes)
|
| 405 |
+
# Usually appears as bright red/pink areas with scattered dark blue/purple nuclei
|
| 406 |
+
fresh_mask = (
|
| 407 |
+
(lab_image[:, :, 0] > 50) # Lightness
|
| 408 |
+
& (lab_image[:, :, 1] > 15) # a* channel (red component)
|
| 409 |
+
)
|
| 410 |
+
|
| 411 |
+
# Cellular lysis: degraded cellular components, less intense staining
|
| 412 |
+
lysis_mask = (
|
| 413 |
+
(lab_image[:, :, 0] > 60) # Lightness
|
| 414 |
+
& (lab_image[:, :, 1] < 15) # Less red
|
| 415 |
+
& (lab_image[:, :, 1] > -5)
|
| 416 |
+
& (lab_image[:, :, 2] < 10) # Less blue
|
| 417 |
+
)
|
| 418 |
+
|
| 419 |
+
# Endothelialization: endothelial cells lining the thrombus
|
| 420 |
+
# Typically appears as thin, organized cellular layers
|
| 421 |
+
endothel_mask = (
|
| 422 |
+
(lab_image[:, :, 0] > 40) & (lab_image[:, :, 0] < 70) & (lab_image[:, :, 2] < -5) # More blue component
|
| 423 |
+
)
|
| 424 |
+
|
| 425 |
+
# Fibroblastic reaction: fibroblasts and collagen deposition
|
| 426 |
+
# Usually appears as pale pink/purple fibrous structures
|
| 427 |
+
fibro_mask = (
|
| 428 |
+
(lab_image[:, :, 0] > 70) # Lighter areas
|
| 429 |
+
& (lab_image[:, :, 1] < 5) # Less red
|
| 430 |
+
& (lab_image[:, :, 1] > -10)
|
| 431 |
+
& (lab_image[:, :, 2] > 0) # Less blue
|
| 432 |
+
)
|
| 433 |
+
|
| 434 |
+
log += "- Created masks for each thrombus component based on color characteristics\n"
|
| 435 |
+
|
| 436 |
+
# Step 3: Quantify the components
|
| 437 |
+
log += "\n## Step 3: Component Quantification\n"
|
| 438 |
+
|
| 439 |
+
# Calculate total pixel count (excluding background)
|
| 440 |
+
total_pixels = fresh_mask.sum() + lysis_mask.sum() + endothel_mask.sum() + fibro_mask.sum()
|
| 441 |
+
|
| 442 |
+
# Calculate percentages
|
| 443 |
+
if total_pixels > 0:
|
| 444 |
+
fresh_percent = (fresh_mask.sum() / total_pixels) * 100
|
| 445 |
+
lysis_percent = (lysis_mask.sum() / total_pixels) * 100
|
| 446 |
+
endothel_percent = (endothel_mask.sum() / total_pixels) * 100
|
| 447 |
+
fibro_percent = (fibro_mask.sum() / total_pixels) * 100
|
| 448 |
+
else:
|
| 449 |
+
fresh_percent = lysis_percent = endothel_percent = fibro_percent = 0
|
| 450 |
+
|
| 451 |
+
log += f"- Fresh thrombus: {fresh_percent:.2f}%\n"
|
| 452 |
+
log += f"- Cellular lysis: {lysis_percent:.2f}%\n"
|
| 453 |
+
log += f"- Endothelialization: {endothel_percent:.2f}%\n"
|
| 454 |
+
log += f"- Fibroblastic reaction: {fibro_percent:.2f}%\n\n"
|
| 455 |
+
|
| 456 |
+
# Step 4: Visualize the results
|
| 457 |
+
log += "## Step 4: Visualization\n"
|
| 458 |
+
|
| 459 |
+
# Create a visualization image
|
| 460 |
+
visualization = np.zeros_like(rgb_image)
|
| 461 |
+
|
| 462 |
+
# Assign colors to different components
|
| 463 |
+
visualization[fresh_mask] = [255, 0, 0] # Red for fresh thrombus
|
| 464 |
+
visualization[lysis_mask] = [0, 255, 0] # Green for cellular lysis
|
| 465 |
+
visualization[endothel_mask] = [0, 0, 255] # Blue for endothelialization
|
| 466 |
+
visualization[fibro_mask] = [255, 255, 0] # Yellow for fibroblastic reaction
|
| 467 |
+
|
| 468 |
+
# Save the visualization
|
| 469 |
+
output_filename = os.path.join(output_dir, f"thrombus_components_{os.path.basename(image_path)}")
|
| 470 |
+
cv2.imwrite(output_filename, cv2.cvtColor(visualization, cv2.COLOR_RGB2BGR))
|
| 471 |
+
|
| 472 |
+
log += f"- Visualization saved as: {output_filename}\n"
|
| 473 |
+
log += "- Color legend:\n"
|
| 474 |
+
log += " - Red: Fresh thrombus\n"
|
| 475 |
+
log += " - Green: Cellular lysis\n"
|
| 476 |
+
log += " - Blue: Endothelialization\n"
|
| 477 |
+
log += " - Yellow: Fibroblastic reaction\n\n"
|
| 478 |
+
|
| 479 |
+
# Step 5: Summary
|
| 480 |
+
log += "## Summary\n"
|
| 481 |
+
log += "The histological analysis of the thrombus sample revealed the following composition:\n\n"
|
| 482 |
+
log += f"1. Fresh thrombus: {fresh_percent:.2f}%\n"
|
| 483 |
+
log += f"2. Cellular lysis: {lysis_percent:.2f}%\n"
|
| 484 |
+
log += f"3. Endothelialization: {endothel_percent:.2f}%\n"
|
| 485 |
+
log += f"4. Fibroblastic reaction: {fibro_percent:.2f}%\n\n"
|
| 486 |
+
|
| 487 |
+
# Save results to CSV
|
| 488 |
+
import csv
|
| 489 |
+
|
| 490 |
+
csv_filename = os.path.join(
|
| 491 |
+
output_dir,
|
| 492 |
+
f"thrombus_analysis_{os.path.basename(image_path).split('.')[0]}.csv",
|
| 493 |
+
)
|
| 494 |
+
with open(csv_filename, "w", newline="") as csvfile:
|
| 495 |
+
writer = csv.writer(csvfile)
|
| 496 |
+
writer.writerow(["Component", "Percentage"])
|
| 497 |
+
writer.writerow(["Fresh thrombus", f"{fresh_percent:.2f}"])
|
| 498 |
+
writer.writerow(["Cellular lysis", f"{lysis_percent:.2f}"])
|
| 499 |
+
writer.writerow(["Endothelialization", f"{endothel_percent:.2f}"])
|
| 500 |
+
writer.writerow(["Fibroblastic reaction", f"{fibro_percent:.2f}"])
|
| 501 |
+
|
| 502 |
+
log += f"Quantitative results saved to: {csv_filename}\n"
|
| 503 |
+
|
| 504 |
+
return log
|
| 505 |
+
|
| 506 |
+
|
| 507 |
+
def analyze_intracellular_calcium_with_rhod2(
|
| 508 |
+
background_image_path, control_image_path, sample_image_path, output_dir="./output"
|
| 509 |
+
):
|
| 510 |
+
"""Analyzes intracellular calcium concentration using Rhod-2 fluorescent indicator from microscopy images.
|
| 511 |
+
|
| 512 |
+
Parameters
|
| 513 |
+
----------
|
| 514 |
+
background_image_path : str
|
| 515 |
+
Path to the background image (no cells, just media)
|
| 516 |
+
control_image_path : str
|
| 517 |
+
Path to the control image (cells without calcium stimulus)
|
| 518 |
+
sample_image_path : str
|
| 519 |
+
Path to the sample image (cells with calcium stimulus)
|
| 520 |
+
output_dir : str, optional
|
| 521 |
+
Directory to save output files, default is "./output"
|
| 522 |
+
|
| 523 |
+
Returns
|
| 524 |
+
-------
|
| 525 |
+
str
|
| 526 |
+
Research log summarizing the analysis steps and results
|
| 527 |
+
|
| 528 |
+
"""
|
| 529 |
+
import os
|
| 530 |
+
from datetime import datetime
|
| 531 |
+
|
| 532 |
+
import cv2
|
| 533 |
+
import matplotlib.pyplot as plt
|
| 534 |
+
import numpy as np
|
| 535 |
+
|
| 536 |
+
# Create output directory if it doesn't exist
|
| 537 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 538 |
+
|
| 539 |
+
# Step 1: Load images
|
| 540 |
+
background = cv2.imread(background_image_path, cv2.IMREAD_GRAYSCALE).astype(float)
|
| 541 |
+
control = cv2.imread(control_image_path, cv2.IMREAD_GRAYSCALE).astype(float)
|
| 542 |
+
sample = cv2.imread(sample_image_path, cv2.IMREAD_GRAYSCALE).astype(float)
|
| 543 |
+
|
| 544 |
+
# Step 2: Background subtraction
|
| 545 |
+
control_corrected = np.maximum(control - background, 0)
|
| 546 |
+
sample_corrected = np.maximum(sample - background, 0)
|
| 547 |
+
|
| 548 |
+
# Step 3: Calculate fluorescence intensity (mean values)
|
| 549 |
+
control_intensity = np.mean(control_corrected)
|
| 550 |
+
sample_intensity = np.mean(sample_corrected)
|
| 551 |
+
|
| 552 |
+
# Step 4: Convert to calcium concentration using a standard calibration equation
|
| 553 |
+
# [Ca2+] = Kd * (F - Fmin) / (Fmax - F)
|
| 554 |
+
# For Rhod-2, Kd is approximately 570 nM
|
| 555 |
+
# We'll use control as Fmin and estimate Fmax as 2.5x the sample value (typical for max calcium response)
|
| 556 |
+
kd = 570 # nM
|
| 557 |
+
f_min = control_intensity
|
| 558 |
+
f_max = 2.5 * sample_intensity # Estimated maximum
|
| 559 |
+
f = sample_intensity
|
| 560 |
+
|
| 561 |
+
# Guard against division by zero
|
| 562 |
+
calcium_concentration = float("inf") if f_max == f else kd * (f - f_min) / (f_max - f)
|
| 563 |
+
|
| 564 |
+
# Step 5: Generate heatmap visualization of calcium concentration
|
| 565 |
+
plt.figure(figsize=(10, 8))
|
| 566 |
+
calcium_map = (sample_corrected - control_corrected) / (f_max - control_corrected + 1e-10) * kd
|
| 567 |
+
plt.imshow(calcium_map, cmap="hot")
|
| 568 |
+
plt.colorbar(label="[Ca²⁺] (nM)")
|
| 569 |
+
plt.title("Intracellular Calcium Concentration Map")
|
| 570 |
+
|
| 571 |
+
# Save the visualization
|
| 572 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 573 |
+
result_filename = f"{output_dir}/calcium_map_{timestamp}.png"
|
| 574 |
+
plt.savefig(result_filename)
|
| 575 |
+
plt.close()
|
| 576 |
+
|
| 577 |
+
# Generate research log
|
| 578 |
+
log = f"""
|
| 579 |
+
Intracellular Calcium Imaging Analysis Log - {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
|
| 580 |
+
------------------------------------------------------------
|
| 581 |
+
1. Loaded microscopy images:
|
| 582 |
+
- Background image: {background_image_path}
|
| 583 |
+
- Control image: {control_image_path}
|
| 584 |
+
- Sample image: {sample_image_path}
|
| 585 |
+
|
| 586 |
+
2. Performed background subtraction to correct for autofluorescence
|
| 587 |
+
|
| 588 |
+
3. Calculated fluorescence intensity:
|
| 589 |
+
- Control intensity: {control_intensity:.2f} a.u.
|
| 590 |
+
- Sample intensity: {sample_intensity:.2f} a.u.
|
| 591 |
+
|
| 592 |
+
4. Converted fluorescence to calcium concentration using Rhod-2 calibration:
|
| 593 |
+
- Kd value: {kd} nM
|
| 594 |
+
- Mean intracellular [Ca²⁺]: {calcium_concentration:.2f} nM
|
| 595 |
+
|
| 596 |
+
5. Generated calcium concentration heatmap saved to:
|
| 597 |
+
- {result_filename}
|
| 598 |
+
|
| 599 |
+
Analysis complete. The sample shows {calcium_concentration:.2f} nM intracellular calcium concentration.
|
| 600 |
+
"""
|
| 601 |
+
|
| 602 |
+
return log
|
| 603 |
+
|
| 604 |
+
|
| 605 |
+
def quantify_corneal_nerve_fibers(image_path, marker_type, output_dir="./output", threshold_method="otsu"):
|
| 606 |
+
"""Quantify the volume/density of immunofluorescence-labeled corneal nerve fibers.
|
| 607 |
+
|
| 608 |
+
Parameters
|
| 609 |
+
----------
|
| 610 |
+
image_path : str
|
| 611 |
+
Path to the immunofluorescence microscopy image file
|
| 612 |
+
marker_type : str
|
| 613 |
+
Type of nerve fiber marker (e.g., 'βIII-tubulin', 'SP', 'L1CAM')
|
| 614 |
+
output_dir : str, optional
|
| 615 |
+
Directory to save output files (default: './output')
|
| 616 |
+
threshold_method : str, optional
|
| 617 |
+
Method for thresholding ('otsu', 'adaptive', 'manual'), default is 'otsu'
|
| 618 |
+
|
| 619 |
+
Returns
|
| 620 |
+
-------
|
| 621 |
+
str
|
| 622 |
+
Research log summarizing the analysis steps and results
|
| 623 |
+
|
| 624 |
+
"""
|
| 625 |
+
import os
|
| 626 |
+
from datetime import datetime
|
| 627 |
+
|
| 628 |
+
import numpy as np
|
| 629 |
+
from skimage import filters, io, measure, morphology, util
|
| 630 |
+
from skimage.color import rgb2gray
|
| 631 |
+
|
| 632 |
+
# Create output directory if it doesn't exist
|
| 633 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 634 |
+
|
| 635 |
+
# Generate timestamp for unique filenames
|
| 636 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 637 |
+
base_filename = f"{marker_type.replace(' ', '_')}_{timestamp}"
|
| 638 |
+
|
| 639 |
+
# Step 1: Load the image
|
| 640 |
+
try:
|
| 641 |
+
image = io.imread(image_path)
|
| 642 |
+
# Convert to grayscale if RGB
|
| 643 |
+
if len(image.shape) > 2:
|
| 644 |
+
image = rgb2gray(image)
|
| 645 |
+
except Exception as e:
|
| 646 |
+
return f"Error loading image: {str(e)}"
|
| 647 |
+
|
| 648 |
+
# Step 2: Preprocess the image
|
| 649 |
+
# Normalize to 0-1 range
|
| 650 |
+
image_normalized = image.astype(float)
|
| 651 |
+
image_normalized = (image_normalized - image_normalized.min()) / (image_normalized.max() - image_normalized.min())
|
| 652 |
+
|
| 653 |
+
# Step 3: Segment the nerve fibers
|
| 654 |
+
if threshold_method == "otsu":
|
| 655 |
+
threshold_value = filters.threshold_otsu(image_normalized)
|
| 656 |
+
elif threshold_method == "adaptive":
|
| 657 |
+
threshold_value = filters.threshold_local(image_normalized, block_size=35)
|
| 658 |
+
else: # manual
|
| 659 |
+
threshold_value = 0.5 # Default manual threshold
|
| 660 |
+
|
| 661 |
+
# Create binary mask
|
| 662 |
+
binary_mask = image_normalized > threshold_value
|
| 663 |
+
|
| 664 |
+
# Step 4: Clean up with morphological operations
|
| 665 |
+
# Remove small objects
|
| 666 |
+
cleaned_mask = morphology.remove_small_objects(binary_mask, min_size=50)
|
| 667 |
+
# Fill holes
|
| 668 |
+
cleaned_mask = morphology.closing(cleaned_mask, morphology.disk(2))
|
| 669 |
+
|
| 670 |
+
# Step 5: Quantify the nerve fibers
|
| 671 |
+
# Calculate total area of nerve fibers (in pixels)
|
| 672 |
+
fiber_area = np.sum(cleaned_mask)
|
| 673 |
+
# Calculate total image area
|
| 674 |
+
total_area = cleaned_mask.size
|
| 675 |
+
# Calculate fiber density (percentage of area covered by fibers)
|
| 676 |
+
fiber_density = (fiber_area / total_area) * 100
|
| 677 |
+
|
| 678 |
+
# Calculate properties of individual fiber segments
|
| 679 |
+
labeled_fibers = measure.label(cleaned_mask)
|
| 680 |
+
fiber_props = measure.regionprops(labeled_fibers)
|
| 681 |
+
|
| 682 |
+
# Calculate average fiber length and width
|
| 683 |
+
total_length = 0
|
| 684 |
+
total_width = 0
|
| 685 |
+
fiber_count = len(fiber_props)
|
| 686 |
+
|
| 687 |
+
if fiber_count > 0:
|
| 688 |
+
for prop in fiber_props:
|
| 689 |
+
# Approximating length as major axis length
|
| 690 |
+
total_length += prop.major_axis_length
|
| 691 |
+
# Approximating width as minor axis length
|
| 692 |
+
total_width += prop.minor_axis_length
|
| 693 |
+
|
| 694 |
+
avg_length = total_length / fiber_count
|
| 695 |
+
avg_width = total_width / fiber_count
|
| 696 |
+
else:
|
| 697 |
+
avg_length = 0
|
| 698 |
+
avg_width = 0
|
| 699 |
+
|
| 700 |
+
# Step 6: Save results
|
| 701 |
+
# Save segmented image
|
| 702 |
+
segmented_image_path = os.path.join(output_dir, f"{base_filename}_segmented.png")
|
| 703 |
+
io.imsave(segmented_image_path, util.img_as_ubyte(cleaned_mask))
|
| 704 |
+
|
| 705 |
+
# Save measurements to CSV
|
| 706 |
+
measurements_path = os.path.join(output_dir, f"{base_filename}_measurements.csv")
|
| 707 |
+
with open(measurements_path, "w") as f:
|
| 708 |
+
f.write("Metric,Value\n")
|
| 709 |
+
f.write(f"Marker Type,{marker_type}\n")
|
| 710 |
+
f.write(f"Fiber Area (pixels),{fiber_area}\n")
|
| 711 |
+
f.write(f"Total Image Area (pixels),{total_area}\n")
|
| 712 |
+
f.write(f"Fiber Density (%),{fiber_density:.2f}\n")
|
| 713 |
+
f.write(f"Fiber Count,{fiber_count}\n")
|
| 714 |
+
f.write(f"Average Fiber Length (pixels),{avg_length:.2f}\n")
|
| 715 |
+
f.write(f"Average Fiber Width (pixels),{avg_width:.2f}\n")
|
| 716 |
+
|
| 717 |
+
# Step 7: Generate research log
|
| 718 |
+
log = f"""
|
| 719 |
+
Research Log: Quantification of {marker_type} Labeled Corneal Nerve Fibers
|
| 720 |
+
Date: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
|
| 721 |
+
Image: {os.path.basename(image_path)}
|
| 722 |
+
|
| 723 |
+
Analysis Steps:
|
| 724 |
+
1. Loaded and preprocessed the immunofluorescence microscopy image
|
| 725 |
+
2. Applied {threshold_method} thresholding for nerve fiber segmentation
|
| 726 |
+
3. Performed morphological operations to clean the segmentation
|
| 727 |
+
4. Quantified nerve fiber metrics
|
| 728 |
+
|
| 729 |
+
Results:
|
| 730 |
+
- Fiber Area: {fiber_area} pixels
|
| 731 |
+
- Total Image Area: {total_area} pixels
|
| 732 |
+
- Fiber Density: {fiber_density:.2f}%
|
| 733 |
+
- Number of Fiber Segments: {fiber_count}
|
| 734 |
+
- Average Fiber Length: {avg_length:.2f} pixels
|
| 735 |
+
- Average Fiber Width: {avg_width:.2f} pixels
|
| 736 |
+
|
| 737 |
+
Output Files:
|
| 738 |
+
- Segmented Image: {os.path.basename(segmented_image_path)}
|
| 739 |
+
- Measurements: {os.path.basename(measurements_path)}
|
| 740 |
+
"""
|
| 741 |
+
|
| 742 |
+
return log
|
| 743 |
+
|
| 744 |
+
|
| 745 |
+
def segment_and_quantify_cells_in_multiplexed_images(
|
| 746 |
+
image_path, markers_list, nuclear_channel_index=0, output_dir="./output"
|
| 747 |
+
):
|
| 748 |
+
"""Segment cells and quantify protein expression levels from multichannel tissue images.
|
| 749 |
+
|
| 750 |
+
Parameters
|
| 751 |
+
----------
|
| 752 |
+
image_path : str
|
| 753 |
+
Path to the multichannel image file (tiff stack or similar format)
|
| 754 |
+
markers_list : list of str
|
| 755 |
+
List of marker names corresponding to each channel in the image
|
| 756 |
+
nuclear_channel_index : int, optional
|
| 757 |
+
Index of the nuclear marker channel (default: 0, typically DAPI)
|
| 758 |
+
output_dir : str, optional
|
| 759 |
+
Directory to save output files (default: "./output")
|
| 760 |
+
|
| 761 |
+
Returns
|
| 762 |
+
-------
|
| 763 |
+
str
|
| 764 |
+
Research log summarizing the steps performed and output file locations
|
| 765 |
+
|
| 766 |
+
"""
|
| 767 |
+
import os
|
| 768 |
+
from datetime import datetime
|
| 769 |
+
|
| 770 |
+
import numpy as np
|
| 771 |
+
import pandas as pd
|
| 772 |
+
from scipy import ndimage # Import ndimage for distance transform
|
| 773 |
+
from skimage import filters, io, measure, morphology, segmentation
|
| 774 |
+
|
| 775 |
+
# Initialize research log
|
| 776 |
+
log = []
|
| 777 |
+
log.append(
|
| 778 |
+
f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Starting cell segmentation and protein quantification"
|
| 779 |
+
)
|
| 780 |
+
|
| 781 |
+
# Create output directory if it doesn't exist
|
| 782 |
+
if not os.path.exists(output_dir):
|
| 783 |
+
os.makedirs(output_dir)
|
| 784 |
+
log.append(f"Created output directory: {output_dir}")
|
| 785 |
+
|
| 786 |
+
# Load the multichannel image
|
| 787 |
+
try:
|
| 788 |
+
log.append(f"Loading multichannel image from {image_path}")
|
| 789 |
+
image = io.imread(image_path)
|
| 790 |
+
log.append(f"Image loaded successfully. Shape: {image.shape}")
|
| 791 |
+
|
| 792 |
+
# Validate image dimensions
|
| 793 |
+
if len(image.shape) < 3:
|
| 794 |
+
return (
|
| 795 |
+
"Error: Image should be multichannel with shape (channels, height, width) or (height, width, channels)"
|
| 796 |
+
)
|
| 797 |
+
|
| 798 |
+
# Determine image format
|
| 799 |
+
if image.shape[0] == len(markers_list):
|
| 800 |
+
# Format is (channels, height, width)
|
| 801 |
+
num_channels, height, width = image.shape
|
| 802 |
+
# Extract nuclear channel
|
| 803 |
+
nuclear_channel = image[nuclear_channel_index]
|
| 804 |
+
elif image.shape[-1] == len(markers_list):
|
| 805 |
+
# Format is (height, width, channels)
|
| 806 |
+
height, width, num_channels = image.shape
|
| 807 |
+
# Extract nuclear channel
|
| 808 |
+
nuclear_channel = image[:, :, nuclear_channel_index]
|
| 809 |
+
else:
|
| 810 |
+
return f"Error: Number of channels in image ({image.shape}) doesn't match markers list length ({len(markers_list)})"
|
| 811 |
+
|
| 812 |
+
log.append(f"Processing image with {num_channels} channels, dimensions: {height}x{width}")
|
| 813 |
+
|
| 814 |
+
except Exception as e:
|
| 815 |
+
return f"Error loading image: {str(e)}"
|
| 816 |
+
|
| 817 |
+
# Segment nuclei
|
| 818 |
+
log.append("Performing nuclear segmentation")
|
| 819 |
+
# Apply threshold to nuclear channel
|
| 820 |
+
threshold = filters.threshold_otsu(nuclear_channel)
|
| 821 |
+
binary_nuclei = nuclear_channel > threshold
|
| 822 |
+
|
| 823 |
+
# Clean up binary image
|
| 824 |
+
binary_nuclei = morphology.remove_small_objects(binary_nuclei, min_size=50)
|
| 825 |
+
binary_nuclei = morphology.binary_closing(binary_nuclei, morphology.disk(2))
|
| 826 |
+
|
| 827 |
+
# Label nuclei
|
| 828 |
+
labeled_nuclei = measure.label(binary_nuclei)
|
| 829 |
+
log.append(f"Identified {np.max(labeled_nuclei)} potential nuclei")
|
| 830 |
+
|
| 831 |
+
# Create cell masks by expanding nuclei
|
| 832 |
+
log.append("Expanding nuclear masks to approximate cell boundaries")
|
| 833 |
+
cell_masks = segmentation.watershed(
|
| 834 |
+
-ndimage.distance_transform_edt(~binary_nuclei), # Use ndimage for distance transform
|
| 835 |
+
labeled_nuclei,
|
| 836 |
+
mask=morphology.binary_dilation(binary_nuclei, morphology.disk(10)),
|
| 837 |
+
)
|
| 838 |
+
|
| 839 |
+
# Get region properties for nuclei
|
| 840 |
+
regions = measure.regionprops(cell_masks)
|
| 841 |
+
log.append(f"Segmented {len(regions)} cells")
|
| 842 |
+
|
| 843 |
+
# Create a dataframe to store cell features
|
| 844 |
+
log.append("Extracting features and quantifying marker intensities")
|
| 845 |
+
cell_data = []
|
| 846 |
+
|
| 847 |
+
for i, region in enumerate(regions):
|
| 848 |
+
cell_features = {
|
| 849 |
+
"cell_id": i + 1,
|
| 850 |
+
"centroid_y": region.centroid[0],
|
| 851 |
+
"centroid_x": region.centroid[1],
|
| 852 |
+
"area": region.area,
|
| 853 |
+
}
|
| 854 |
+
|
| 855 |
+
# Extract intensity for each marker
|
| 856 |
+
for channel_idx, marker_name in enumerate(markers_list):
|
| 857 |
+
if image.shape[0] == len(markers_list):
|
| 858 |
+
# Format is (channels, height, width)
|
| 859 |
+
marker_image = image[channel_idx]
|
| 860 |
+
else:
|
| 861 |
+
# Format is (height, width, channels)
|
| 862 |
+
marker_image = image[:, :, channel_idx]
|
| 863 |
+
|
| 864 |
+
# Create mask for current cell
|
| 865 |
+
cell_mask = cell_masks == region.label
|
| 866 |
+
|
| 867 |
+
# Calculate mean intensity within the cell mask
|
| 868 |
+
mean_intensity = np.mean(marker_image[cell_mask])
|
| 869 |
+
cell_features[f"{marker_name}_mean_intensity"] = mean_intensity
|
| 870 |
+
|
| 871 |
+
cell_data.append(cell_features)
|
| 872 |
+
|
| 873 |
+
# Create dataframe
|
| 874 |
+
cell_df = pd.DataFrame(cell_data)
|
| 875 |
+
|
| 876 |
+
# Save results
|
| 877 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 878 |
+
results_file = os.path.join(output_dir, f"cell_features_{timestamp}.csv")
|
| 879 |
+
cell_df.to_csv(results_file, index=False)
|
| 880 |
+
log.append(f"Saved spatial feature table to {results_file}")
|
| 881 |
+
|
| 882 |
+
# Save segmentation mask for visualization
|
| 883 |
+
mask_file = os.path.join(output_dir, f"cell_segmentation_mask_{timestamp}.tiff")
|
| 884 |
+
io.imsave(mask_file, cell_masks.astype(np.uint16))
|
| 885 |
+
log.append(f"Saved cell segmentation mask to {mask_file}")
|
| 886 |
+
|
| 887 |
+
# Summarize results
|
| 888 |
+
log.append(f"Analysis complete. Processed {num_channels} markers across {len(regions)} cells.")
|
| 889 |
+
log.append("Output files:")
|
| 890 |
+
log.append(f" - Spatial feature table: {results_file}")
|
| 891 |
+
log.append(f" - Cell segmentation mask: {mask_file}")
|
| 892 |
+
|
| 893 |
+
return "\n".join(log)
|
| 894 |
+
|
| 895 |
+
|
| 896 |
+
def analyze_bone_microct_morphometry(input_file_path, output_dir="./results", threshold_value=None):
|
| 897 |
+
"""Analyze bone microarchitecture parameters from 3D micro-CT images.
|
| 898 |
+
|
| 899 |
+
Performs quantitative analysis of bone microstructure to calculate bone mineral density (BMD),
|
| 900 |
+
bone volume (BV), trabecular number (Tb.N), trabecular thickness (Tb.Th), and
|
| 901 |
+
trabecular separation (Tb.S) from micro-CT data.
|
| 902 |
+
|
| 903 |
+
Parameters
|
| 904 |
+
----------
|
| 905 |
+
input_file_path : str
|
| 906 |
+
Path to the micro-CT scan data file (TIFF stack or similar 3D format)
|
| 907 |
+
output_dir : str, optional
|
| 908 |
+
Directory to save output files, default is "./results"
|
| 909 |
+
threshold_value : float, optional
|
| 910 |
+
Threshold value for bone segmentation. If None, Otsu's method will be used
|
| 911 |
+
|
| 912 |
+
Returns
|
| 913 |
+
-------
|
| 914 |
+
str
|
| 915 |
+
Research log summarizing the analysis steps and results
|
| 916 |
+
|
| 917 |
+
"""
|
| 918 |
+
import json
|
| 919 |
+
import os
|
| 920 |
+
from datetime import datetime
|
| 921 |
+
|
| 922 |
+
import numpy as np
|
| 923 |
+
from scipy import ndimage
|
| 924 |
+
from skimage import filters, io
|
| 925 |
+
|
| 926 |
+
# Create output directory if it doesn't exist
|
| 927 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 928 |
+
|
| 929 |
+
# Start research log
|
| 930 |
+
log = [
|
| 931 |
+
"# Micro-CT Bone Morphometry Analysis",
|
| 932 |
+
f"Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
|
| 933 |
+
f"Input file: {input_file_path}",
|
| 934 |
+
]
|
| 935 |
+
|
| 936 |
+
# Step 1: Load the micro-CT data
|
| 937 |
+
log.append("\n## 1. Data Loading")
|
| 938 |
+
try:
|
| 939 |
+
log.append(f"Loading 3D micro-CT data from {input_file_path}")
|
| 940 |
+
# Load the 3D image data
|
| 941 |
+
image_data = io.imread(input_file_path)
|
| 942 |
+
if image_data.ndim < 3:
|
| 943 |
+
log.append("Warning: Input appears to be 2D. Converting to 3D.")
|
| 944 |
+
image_data = np.expand_dims(image_data, axis=0)
|
| 945 |
+
|
| 946 |
+
log.append(f"Data loaded successfully. Dimensions: {image_data.shape}")
|
| 947 |
+
voxel_count = image_data.size
|
| 948 |
+
log.append(f"Total voxels: {voxel_count}")
|
| 949 |
+
except Exception as e:
|
| 950 |
+
error_msg = f"Error loading data: {str(e)}"
|
| 951 |
+
log.append(error_msg)
|
| 952 |
+
return "\n".join(log)
|
| 953 |
+
|
| 954 |
+
# Step 2: Preprocess the data
|
| 955 |
+
log.append("\n## 2. Preprocessing")
|
| 956 |
+
log.append("Applying median filter to reduce noise")
|
| 957 |
+
filtered_data = ndimage.median_filter(image_data, size=2)
|
| 958 |
+
|
| 959 |
+
# Step 3: Segmentation of bone tissue
|
| 960 |
+
log.append("\n## 3. Bone Segmentation")
|
| 961 |
+
if threshold_value is None:
|
| 962 |
+
log.append("Calculating optimal threshold using Otsu's method")
|
| 963 |
+
threshold_value = filters.threshold_otsu(filtered_data)
|
| 964 |
+
|
| 965 |
+
log.append(f"Segmenting bone using threshold value: {threshold_value}")
|
| 966 |
+
binary_image = filtered_data > threshold_value
|
| 967 |
+
|
| 968 |
+
# Step 4: Calculate bone morphometry parameters
|
| 969 |
+
log.append("\n## 4. Morphometry Analysis")
|
| 970 |
+
|
| 971 |
+
# 4.1 Calculate BMD (simplified as mean intensity in bone regions)
|
| 972 |
+
bmd = np.mean(image_data[binary_image])
|
| 973 |
+
log.append(f"Bone Mineral Density (BMD): {bmd:.2f} arbitrary units")
|
| 974 |
+
|
| 975 |
+
# 4.2 Calculate BV (bone volume)
|
| 976 |
+
bone_volume = np.sum(binary_image)
|
| 977 |
+
total_volume = binary_image.size
|
| 978 |
+
bv_tv_ratio = bone_volume / total_volume
|
| 979 |
+
log.append(f"Bone Volume (BV): {bone_volume} voxels")
|
| 980 |
+
log.append(f"BV/TV Ratio: {bv_tv_ratio:.4f}")
|
| 981 |
+
|
| 982 |
+
# 4.3 Calculate trabecular thickness (Tb.Th)
|
| 983 |
+
# Using distance transform method
|
| 984 |
+
log.append("Calculating trabecular thickness (Tb.Th)")
|
| 985 |
+
distance_map = ndimage.distance_transform_edt(binary_image)
|
| 986 |
+
tb_th_mean = np.mean(distance_map[binary_image]) * 2 # Multiply by 2 for diameter
|
| 987 |
+
log.append(f"Trabecular Thickness (Tb.Th): {tb_th_mean:.2f} voxels")
|
| 988 |
+
|
| 989 |
+
# 4.4 Calculate trabecular separation (Tb.S)
|
| 990 |
+
log.append("Calculating trabecular separation (Tb.S)")
|
| 991 |
+
distance_map_inv = ndimage.distance_transform_edt(~binary_image)
|
| 992 |
+
tb_s_mean = np.mean(distance_map_inv[~binary_image])
|
| 993 |
+
log.append(f"Trabecular Separation (Tb.S): {tb_s_mean:.2f} voxels")
|
| 994 |
+
|
| 995 |
+
# 4.5 Calculate trabecular number (Tb.N)
|
| 996 |
+
# Simplified calculation: Tb.N = (BV/TV) / Tb.Th
|
| 997 |
+
log.append("Calculating trabecular number (Tb.N)")
|
| 998 |
+
tb_n = bv_tv_ratio / tb_th_mean if tb_th_mean > 0 else 0
|
| 999 |
+
log.append(f"Trabecular Number (Tb.N): {tb_n:.4f} 1/voxel")
|
| 1000 |
+
|
| 1001 |
+
# Step 5: Save results
|
| 1002 |
+
log.append("\n## 5. Saving Results")
|
| 1003 |
+
|
| 1004 |
+
# Save binary segmentation as a sample slice
|
| 1005 |
+
middle_slice = binary_image.shape[0] // 2
|
| 1006 |
+
segmentation_file = os.path.join(output_dir, "bone_segmentation_slice.tif")
|
| 1007 |
+
io.imsave(segmentation_file, binary_image[middle_slice].astype(np.uint8) * 255)
|
| 1008 |
+
log.append(f"Sample segmentation slice saved to: {segmentation_file}")
|
| 1009 |
+
|
| 1010 |
+
# Save numerical results as JSON
|
| 1011 |
+
results = {
|
| 1012 |
+
"BMD": float(bmd),
|
| 1013 |
+
"BV": int(bone_volume),
|
| 1014 |
+
"BV/TV": float(bv_tv_ratio),
|
| 1015 |
+
"Tb.Th": float(tb_th_mean),
|
| 1016 |
+
"Tb.S": float(tb_s_mean),
|
| 1017 |
+
"Tb.N": float(tb_n),
|
| 1018 |
+
}
|
| 1019 |
+
|
| 1020 |
+
results_file = os.path.join(output_dir, "bone_morphometry_results.json")
|
| 1021 |
+
with open(results_file, "w") as f:
|
| 1022 |
+
json.dump(results, f, indent=4)
|
| 1023 |
+
log.append(f"Numerical results saved to: {results_file}")
|
| 1024 |
+
|
| 1025 |
+
# Summary of results
|
| 1026 |
+
log.append("\n## 6. Summary of Bone Morphometry Results")
|
| 1027 |
+
log.append(f"- Bone Mineral Density (BMD): {bmd:.2f} arbitrary units")
|
| 1028 |
+
log.append(f"- Bone Volume (BV): {bone_volume} voxels")
|
| 1029 |
+
log.append(f"- BV/TV Ratio: {bv_tv_ratio:.4f}")
|
| 1030 |
+
log.append(f"- Trabecular Thickness (Tb.Th): {tb_th_mean:.2f} voxels")
|
| 1031 |
+
log.append(f"- Trabecular Separation (Tb.S): {tb_s_mean:.2f} voxels")
|
| 1032 |
+
log.append(f"- Trabecular Number (Tb.N): {tb_n:.4f} 1/voxel")
|
| 1033 |
+
|
| 1034 |
+
return "\n".join(log)
|
Biomni/biomni/tool/protocols/thermofisher/Cell Freezing Protocols _ Thermo Fisher Scientific - US.txt
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
PROTOCOL: Cell Freezing Protocols
|
| 2 |
+
SOURCE: https://www.thermofisher.com/us/en/home/references/gibco-cell-culture-basics/cell-culture-protocols/freezing-cells.html
|
| 3 |
+
================================================================================
|
| 4 |
+
|
| 5 |
+
OVERVIEW
|
| 6 |
+
================================================================================
|
| 7 |
+
|
| 8 |
+
After subculturing your adherent or suspension cells, you may need to freeze any cells not required for immediate experiments. This cell freezing protocol outlines the necessary preparations and materials to order before beginning the freezing process. It also provides step-by-step instructions that you can follow during the procedure or refer to for future use.
|
| 9 |
+
|
| 10 |
+
Cryopreservation of cells, or the freezing of cells, is a technique used to store living cells and tissues at extremely low temperatures to preserve their structure over an indefinite period. By lowering the temperature to below –130°C, extracellular ice forms, decreasing kinetic and molecular activity within the cells and slowing biological aging.
|
| 11 |
+
|
| 12 |
+
Why freeze your cells?
|
| 13 |
+
|
| 14 |
+
• Freezing cells is crucial for maintaining an adequate cell stock
|
| 15 |
+
• Cell lines are valuable resources whose replacement is both expensive and time-consuming
|
| 16 |
+
• Cryopreservation of finite cell lines prevents aging and transformation
|
| 17 |
+
• Freezing cells in continuous culture can prevent:
|
| 18 |
+
- genetic drift
|
| 19 |
+
- senescence
|
| 20 |
+
- microbial contamination
|
| 21 |
+
- equipment failure
|
| 22 |
+
|
| 23 |
+
When a small surplus of cells becomes available from subculturing, a highly effective method is to freeze them as a seed stock, keeping them protected and not available for general laboratory use. Working stocks can be prepared and replenished from these frozen seed stocks. If the seed stocks become depleted, cryopreserved working stocks can serve as a source for preparing fresh seed stock with minimal generation increase from the initial freezing.
|
| 24 |
+
|
| 25 |
+
MATERIALS REQUIRED
|
| 26 |
+
================================================================================
|
| 27 |
+
|
| 28 |
+
We suggest using the following materials for the cell freezing protocol:
|
| 29 |
+
|
| 30 |
+
• Log-phase cultured cells
|
| 31 |
+
• Complete growth medium: basal medium, serum, and supplements pre-warmed to 37°C
|
| 32 |
+
• Cryoprotective agent such as DMSO (use a bottle set aside for cell culture; open only in a laminar flow hood) or a freezing medium such as Synth-a-Freeze Cryopreservation Medium or Recovery Cell Culture Freezing Medium
|
| 33 |
+
• Disposable, sterile 15 mL or 50 mL conical tubes
|
| 34 |
+
• Reagents and equipment to determine viable and total cell counts (e.g., Countess Automated Cell Counter, or hemocytometer, cell counter, and Trypan Blue)
|
| 35 |
+
• Sterile cryogenic storage vials (i.e., cryovials)
|
| 36 |
+
• Controlled rate freezing apparatus or isopropanol chamber
|
| 37 |
+
• Liquid nitrogen storage container
|
| 38 |
+
|
| 39 |
+
For freezing adherent cells, in addition to the above materials, you will need:
|
| 40 |
+
|
| 41 |
+
• Balanced salt solution such as Gibco Dulbecco's Phosphate Buffered Saline (DPBS), containing no calcium, magnesium, or phenol red
|
| 42 |
+
• Dissociation reagent such as trypsin or Gibco TrypLE Express, without phenol red
|
| 43 |
+
|
| 44 |
+
Catalog numbers: J66650.AK, 339650, 5000-0020, 5100-0050, 14190250, 25200072, 12604039, 15250061, 12648010, R00550, A49862
|
| 45 |
+
|
| 46 |
+
MEDIUM FOR FREEZING CELLS
|
| 47 |
+
================================================================================
|
| 48 |
+
|
| 49 |
+
Cryoprotective agents reduce the freezing point of the medium and slow the cooling rate, greatly reducing the risk of ice crystal formation, which can damage cells and cause cell death. Media for cryopreservation typically includes a base medium, a cryopreservative, and a protein source, but we recommend that you closely follow your cell line and product-specific instructions to determine the best cryopreservation media for your experiment. When included, the cryopreservative and protein protect the cells from the stress of the freeze-thaw process. The cells are then frozen in liquid nitrogen for preservation.
|
| 50 |
+
|
| 51 |
+
For media with serum, the constituents may be:
|
| 52 |
+
|
| 53 |
+
• complete medium containing 10% glycerol
|
| 54 |
+
• complete medium containing 10% DMSO (dimethyl sulfoxide)
|
| 55 |
+
• 50% cell-conditioned medium with 50% fresh medium with 10% glycerol or 10% DMSO
|
| 56 |
+
|
| 57 |
+
Safety Note: DMSO solutions are known to facilitate the entry process of organic molecules into tissues. You should always handle reagents containing DMSO using equipment and practices appropriate for the hazards posed by such materials and dispose of the reagents in compliance with local regulations.
|
| 58 |
+
|
| 59 |
+
A serum-free medium has generally low or no protein; but you can still use it as a base for a cryopreservative medium in the following formulations:
|
| 60 |
+
|
| 61 |
+
• 50% cell-conditioned serum-free medium with 50% fresh serum-free medium containing 7.5% DMSO
|
| 62 |
+
• fresh serum-free medium containing 7.5% DMSO and 10% cell culture–grade BSA
|
| 63 |
+
|
| 64 |
+
You may also use a specially formulated complete cryopreservation medium such as:
|
| 65 |
+
|
| 66 |
+
• Gibco Recovery Cell Culture Freezing is a ready-to-use complete cryopreservation medium for mammalian cell cultures with an optimized ratio of fetal bovine serum to bovine serum for improved cell viability and cell recovery after thawing.
|
| 67 |
+
• Gibco Synth-a-Freeze Cryopreservation Medium is a chemically defined, protein-free, sterile cryopreservation medium with 10% DMSO that is suitable for the cryopreservation of many stem and primary cell types apart from melanocytes.
|
| 68 |
+
|
| 69 |
+
PROTOCOL
|
| 70 |
+
================================================================================
|
| 71 |
+
|
| 72 |
+
The proper way to freeze cells and cryopreserve your cell lines for future use requires diligent attention to passaging procedures and storage requirements. Remember to always wear personal protective equipment and use proper sterile technique when freezing cells.
|
| 73 |
+
|
| 74 |
+
The general freezing method is the same for adherent and suspension cells, except that adherent cells need to be removed from the culture plates before starting the freezing procedure. The following cell freezing protocol is a general procedure for cryopreserving cultured cells. Optimal freezing conditions depend on the cell line in use—for detailed protocols, always refer to the cell-specific product insert.
|
| 75 |
+
|
| 76 |
+
Prior to cryopreservation, cells should be characterized and checked for contamination. Freeze cells in log phase, at a high concentration of at least 90% viability, and at as low a passage number as possible, as this will lead to the best outcomes when your stock is thawed.
|
| 77 |
+
|
| 78 |
+
1. Prepare freezing medium and store at 2° to 8°C until use.
|
| 79 |
+
|
| 80 |
+
2. For adherent cells, gently detach cells from the tissue culture vessel following the procedure used during subculture. Detach as gently as possible to minimize damage to the cells.
|
| 81 |
+
|
| 82 |
+
3. Resuspend the detached cells in the complete growth medium.
|
| 83 |
+
|
| 84 |
+
4. Find the total number of cells and percent viability using a hemocytometer or the Countess Automated Cell Counter and Trypan Blue exclusion.
|
| 85 |
+
|
| 86 |
+
5. According to the desired viable cell density, calculate the required volume of freezing medium.
|
| 87 |
+
|
| 88 |
+
6. Centrifuge the cell suspension at approximately 100–400 × g for 5 to 10 minutes (centrifugation speed and duration varies depending on the cell type). Using a pipette, aseptically withdraw the supernatant to the smallest volume without disturbing the cell pellet and discard.
|
| 89 |
+
|
| 90 |
+
7. Resuspend the cell pellet in cold freezing medium at the recommended viable cell density for the specific cell type.
|
| 91 |
+
|
| 92 |
+
8. Dispense aliquots of the cell suspension into sterile cryogenic storage vials. As you aliquot them, gently and often mix the cells to maintain a homogeneous cell suspension.
|
| 93 |
+
|
| 94 |
+
9. Freeze the cells slowly by reducing the temperature at approximately 1°C per minute using a controlled rate cryo-freezer or a cryo-freezing container such as "Mr. Frosty," available from Thermo Scientific Nalgene labware (Nalgene Nunc). Alternatively, place the cryovials containing the cells in an isopropanol chamber and store them at –80°C overnight.
|
| 95 |
+
|
| 96 |
+
10. Transfer frozen cells to liquid nitrogen and store them in the gas phase above the liquid nitrogen below –135°C.
|
| 97 |
+
|
| 98 |
+
Safety Note: Biohazardous materials must be stored in the gas phase above the liquid nitrogen. Storing the sealed cryovials in the gas phase reduces the risk of explosion. If you are using liquid-phase storage, be aware of the explosion hazard with both glass and plastic cryovials and always wear a face shield or goggles.
|
| 99 |
+
|
| 100 |
+
REFERENCES
|
| 101 |
+
================================================================================
|
| 102 |
+
|
| 103 |
+
Freshney, R. (1987) Culture of Animal Cells: A Manual of Basic Technique, p. 220, Alan R. Liss, Inc., New York.
|
| 104 |
+
|
| 105 |
+
Mohamed, H. M., Sundar, P., Ridwan, N. A. A., Cheong, A. J., Mohamad Salleh, N. A., Sulaiman, N., Mh Busra, F., & Maarof, M. (2024). Optimisation of cryopreservation conditions, including storage duration and revival methods, for the viability of human primary cells. BMC Molecular & Cell Biology, 25(1), 1–14. https://doi.org/10.1186/s12860-024-00516-6.
|
Biomni/biomni/tool/synthetic_biology.py
ADDED
|
@@ -0,0 +1,1351 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
def engineer_bacterial_genome_for_therapeutic_delivery(bacterial_genome_file, genetic_parts):
|
| 2 |
+
"""Engineer a bacterial genome by integrating therapeutic genetic parts for therapeutic delivery.
|
| 3 |
+
|
| 4 |
+
Parameters
|
| 5 |
+
----------
|
| 6 |
+
bacterial_genome_file : str
|
| 7 |
+
Path to the file containing the bacterial genome sequence in FASTA format
|
| 8 |
+
genetic_parts : dict
|
| 9 |
+
Dictionary containing genetic parts to be integrated::
|
| 10 |
+
|
| 11 |
+
{
|
| 12 |
+
'promoters': list of dict with 'name', 'sequence', and 'position',
|
| 13 |
+
'genes': list of dict with 'name', 'sequence', and 'position',
|
| 14 |
+
'terminators': list of dict with 'name', 'sequence', and 'position',
|
| 15 |
+
'cargo': dict with 'name' and 'sequence' of the therapeutic cargo
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
Returns
|
| 19 |
+
-------
|
| 20 |
+
str
|
| 21 |
+
Research log summarizing the engineering process, including the filename of the
|
| 22 |
+
engineered genome and plasmid map
|
| 23 |
+
|
| 24 |
+
"""
|
| 25 |
+
import datetime
|
| 26 |
+
|
| 27 |
+
from Bio import SeqIO
|
| 28 |
+
from Bio.Graphics import GenomeDiagram
|
| 29 |
+
from Bio.Seq import Seq
|
| 30 |
+
from Bio.SeqFeature import FeatureLocation, SeqFeature
|
| 31 |
+
from Bio.SeqRecord import SeqRecord
|
| 32 |
+
from reportlab.lib import colors
|
| 33 |
+
|
| 34 |
+
# Initialize research log
|
| 35 |
+
log = ["# Bacterial Genome Engineering for Therapeutic Delivery\n"]
|
| 36 |
+
log.append(f"Date: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
|
| 37 |
+
|
| 38 |
+
# Step 1: Load the bacterial genome
|
| 39 |
+
log.append("## Step 1: Loading Bacterial Genome")
|
| 40 |
+
try:
|
| 41 |
+
genome_record = SeqIO.read(bacterial_genome_file, "fasta")
|
| 42 |
+
genome_seq = genome_record.seq
|
| 43 |
+
log.append(f"Successfully loaded genome: {genome_record.id}")
|
| 44 |
+
log.append(f"Genome length: {len(genome_seq)} bp\n")
|
| 45 |
+
except Exception as e:
|
| 46 |
+
log.append(f"Error loading genome: {str(e)}")
|
| 47 |
+
return "\n".join(log)
|
| 48 |
+
|
| 49 |
+
# Step 2: Design and integrate genetic parts
|
| 50 |
+
log.append("## Step 2: Integrating Genetic Parts")
|
| 51 |
+
|
| 52 |
+
# Create a new genome sequence for modifications
|
| 53 |
+
# In newer BioPython versions, Seq objects are mutable by default
|
| 54 |
+
# or we can convert to a string and modify it, then convert back to Seq
|
| 55 |
+
engineered_seq = str(genome_seq)
|
| 56 |
+
|
| 57 |
+
# Track features for visualization
|
| 58 |
+
features = []
|
| 59 |
+
|
| 60 |
+
# Keep track of position adjustments as we add sequences
|
| 61 |
+
position_adjustment = 0
|
| 62 |
+
|
| 63 |
+
# Add promoters
|
| 64 |
+
if "promoters" in genetic_parts:
|
| 65 |
+
log.append("### Adding Promoters:")
|
| 66 |
+
for promoter in genetic_parts["promoters"]:
|
| 67 |
+
position = promoter["position"] + position_adjustment
|
| 68 |
+
|
| 69 |
+
# Insert the sequence at the position
|
| 70 |
+
engineered_seq = engineered_seq[:position] + promoter["sequence"] + engineered_seq[position:]
|
| 71 |
+
position_adjustment += len(promoter["sequence"])
|
| 72 |
+
|
| 73 |
+
# Adjust positions of subsequent elements
|
| 74 |
+
for part_type in ["genes", "terminators"]:
|
| 75 |
+
if part_type in genetic_parts:
|
| 76 |
+
for part in genetic_parts[part_type]:
|
| 77 |
+
if part["position"] > promoter["position"]:
|
| 78 |
+
part["position"] += len(promoter["sequence"])
|
| 79 |
+
|
| 80 |
+
# Add feature for visualization
|
| 81 |
+
feature = SeqFeature(
|
| 82 |
+
FeatureLocation(position, position + len(promoter["sequence"])),
|
| 83 |
+
type="promoter",
|
| 84 |
+
qualifiers={"label": promoter["name"]},
|
| 85 |
+
)
|
| 86 |
+
features.append(feature)
|
| 87 |
+
|
| 88 |
+
log.append(f" - Added promoter {promoter['name']} at position {position}")
|
| 89 |
+
|
| 90 |
+
# Add genes
|
| 91 |
+
if "genes" in genetic_parts:
|
| 92 |
+
log.append("### Adding Genes:")
|
| 93 |
+
for gene in genetic_parts["genes"]:
|
| 94 |
+
position = gene["position"] + position_adjustment
|
| 95 |
+
|
| 96 |
+
# Insert the sequence at the position
|
| 97 |
+
engineered_seq = engineered_seq[:position] + gene["sequence"] + engineered_seq[position:]
|
| 98 |
+
position_adjustment += len(gene["sequence"])
|
| 99 |
+
|
| 100 |
+
# Adjust positions of subsequent elements
|
| 101 |
+
for part_type in ["terminators"]:
|
| 102 |
+
if part_type in genetic_parts:
|
| 103 |
+
for part in genetic_parts[part_type]:
|
| 104 |
+
if part["position"] > gene["position"]:
|
| 105 |
+
part["position"] += len(gene["sequence"])
|
| 106 |
+
|
| 107 |
+
# Add feature for visualization
|
| 108 |
+
feature = SeqFeature(
|
| 109 |
+
FeatureLocation(position, position + len(gene["sequence"])),
|
| 110 |
+
type="gene",
|
| 111 |
+
qualifiers={"label": gene["name"]},
|
| 112 |
+
)
|
| 113 |
+
features.append(feature)
|
| 114 |
+
|
| 115 |
+
log.append(f" - Added gene {gene['name']} at position {position}")
|
| 116 |
+
|
| 117 |
+
# Add terminators
|
| 118 |
+
if "terminators" in genetic_parts:
|
| 119 |
+
log.append("### Adding Terminators:")
|
| 120 |
+
for terminator in genetic_parts["terminators"]:
|
| 121 |
+
position = terminator["position"] + position_adjustment
|
| 122 |
+
|
| 123 |
+
# Insert the sequence at the position
|
| 124 |
+
engineered_seq = engineered_seq[:position] + terminator["sequence"] + engineered_seq[position:]
|
| 125 |
+
position_adjustment += len(terminator["sequence"])
|
| 126 |
+
|
| 127 |
+
# Add feature for visualization
|
| 128 |
+
feature = SeqFeature(
|
| 129 |
+
FeatureLocation(position, position + len(terminator["sequence"])),
|
| 130 |
+
type="terminator",
|
| 131 |
+
qualifiers={"label": terminator["name"]},
|
| 132 |
+
)
|
| 133 |
+
features.append(feature)
|
| 134 |
+
|
| 135 |
+
log.append(f" - Added terminator {terminator['name']} at position {position}")
|
| 136 |
+
|
| 137 |
+
# Add therapeutic cargo
|
| 138 |
+
if "cargo" in genetic_parts:
|
| 139 |
+
cargo = genetic_parts["cargo"]
|
| 140 |
+
# Find a suitable position (after the last added element)
|
| 141 |
+
position = max([f.location.end for f in features]) if features else len(engineered_seq) // 2
|
| 142 |
+
|
| 143 |
+
# Insert the sequence at the position
|
| 144 |
+
engineered_seq = engineered_seq[:position] + cargo["sequence"] + engineered_seq[position:]
|
| 145 |
+
|
| 146 |
+
# Add feature for visualization
|
| 147 |
+
feature = SeqFeature(
|
| 148 |
+
FeatureLocation(position, position + len(cargo["sequence"])),
|
| 149 |
+
type="therapeutic_cargo",
|
| 150 |
+
qualifiers={"label": cargo["name"]},
|
| 151 |
+
)
|
| 152 |
+
features.append(feature)
|
| 153 |
+
|
| 154 |
+
log.append("### Added Therapeutic Cargo:")
|
| 155 |
+
log.append(f" - Added {cargo['name']} at position {position}")
|
| 156 |
+
log.append(f" - Cargo length: {len(cargo['sequence'])} bp\n")
|
| 157 |
+
|
| 158 |
+
# Step 3: Create the engineered genome record
|
| 159 |
+
log.append("## Step 3: Finalizing Engineered Genome")
|
| 160 |
+
engineered_genome = SeqRecord(
|
| 161 |
+
seq=Seq(engineered_seq),
|
| 162 |
+
id=f"{genome_record.id}_engineered",
|
| 163 |
+
name=f"{genome_record.id}_engineered",
|
| 164 |
+
description=f"Engineered {genome_record.id} for therapeutic delivery",
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
# Add all features to the engineered genome
|
| 168 |
+
engineered_genome.features = features
|
| 169 |
+
|
| 170 |
+
# Save the engineered genome to a file
|
| 171 |
+
output_genome_file = f"{genome_record.id}_engineered.fasta"
|
| 172 |
+
SeqIO.write(engineered_genome, output_genome_file, "fasta")
|
| 173 |
+
log.append(f"Engineered genome saved to: {output_genome_file}")
|
| 174 |
+
log.append(f"Total genome length: {len(engineered_genome.seq)} bp\n")
|
| 175 |
+
|
| 176 |
+
# Step 4: Generate a plasmid map
|
| 177 |
+
log.append("## Step 4: Generating Plasmid Map")
|
| 178 |
+
|
| 179 |
+
# Create the diagram
|
| 180 |
+
gd_diagram = GenomeDiagram.Diagram("Engineered Bacterial Genome")
|
| 181 |
+
gd_track = gd_diagram.new_track(1, name="Engineered Features")
|
| 182 |
+
gd_feature_set = gd_track.new_set()
|
| 183 |
+
|
| 184 |
+
# Add features with different colors
|
| 185 |
+
for feature in features:
|
| 186 |
+
if feature.type == "promoter":
|
| 187 |
+
color = colors.green
|
| 188 |
+
elif feature.type == "gene":
|
| 189 |
+
color = colors.blue
|
| 190 |
+
elif feature.type == "terminator":
|
| 191 |
+
color = colors.red
|
| 192 |
+
elif feature.type == "therapeutic_cargo":
|
| 193 |
+
color = colors.purple
|
| 194 |
+
else:
|
| 195 |
+
color = colors.grey
|
| 196 |
+
|
| 197 |
+
gd_feature_set.add_feature(feature, color=color, label=True)
|
| 198 |
+
|
| 199 |
+
# Draw and save the plasmid map
|
| 200 |
+
plasmid_map_file = f"{genome_record.id}_engineered_map.pdf"
|
| 201 |
+
gd_diagram.draw(format="circular", circular=True, pagesize=(1000, 1000))
|
| 202 |
+
gd_diagram.write(plasmid_map_file, "PDF")
|
| 203 |
+
|
| 204 |
+
log.append(f"Plasmid map generated and saved to: {plasmid_map_file}\n")
|
| 205 |
+
|
| 206 |
+
# Step 5: Final summary
|
| 207 |
+
log.append("## Summary of Engineered Bacterial Strain")
|
| 208 |
+
log.append(f"- Base strain: {genome_record.id}")
|
| 209 |
+
log.append("- Added genetic parts:")
|
| 210 |
+
|
| 211 |
+
if "promoters" in genetic_parts:
|
| 212 |
+
log.append(f" - Promoters: {len(genetic_parts['promoters'])}")
|
| 213 |
+
if "genes" in genetic_parts:
|
| 214 |
+
log.append(f" - Genes: {len(genetic_parts['genes'])}")
|
| 215 |
+
if "terminators" in genetic_parts:
|
| 216 |
+
log.append(f" - Terminators: {len(genetic_parts['terminators'])}")
|
| 217 |
+
if "cargo" in genetic_parts:
|
| 218 |
+
log.append(f" - Therapeutic cargo: {genetic_parts['cargo']['name']}")
|
| 219 |
+
|
| 220 |
+
log.append("\nThe engineered bacterial strain is ready for transformation and selection.")
|
| 221 |
+
|
| 222 |
+
return "\n".join(log)
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
def analyze_bacterial_growth_rate(time_points, od_measurements, strain_name="Unknown strain", output_dir="./"):
|
| 226 |
+
"""Analyze bacterial growth data and extract growth parameters from OD600 measurements.
|
| 227 |
+
|
| 228 |
+
Parameters
|
| 229 |
+
----------
|
| 230 |
+
time_points : list or numpy.ndarray
|
| 231 |
+
Time points at which OD600 measurements were taken (in hours)
|
| 232 |
+
od_measurements : list or numpy.ndarray
|
| 233 |
+
Optical density (OD600) measurements corresponding to each time point
|
| 234 |
+
strain_name : str, optional
|
| 235 |
+
Name of the bacterial strain being analyzed, default is "Unknown strain"
|
| 236 |
+
output_dir : str, optional
|
| 237 |
+
Directory where to save the output files, default is current directory
|
| 238 |
+
|
| 239 |
+
Returns
|
| 240 |
+
-------
|
| 241 |
+
str
|
| 242 |
+
A research log summarizing the analysis steps, fitted parameters, and results
|
| 243 |
+
|
| 244 |
+
"""
|
| 245 |
+
import os
|
| 246 |
+
from datetime import datetime
|
| 247 |
+
|
| 248 |
+
import matplotlib
|
| 249 |
+
|
| 250 |
+
matplotlib.use("Agg") # Use non-interactive backend
|
| 251 |
+
import matplotlib.pyplot as plt
|
| 252 |
+
import numpy as np
|
| 253 |
+
from scipy.optimize import curve_fit
|
| 254 |
+
|
| 255 |
+
# Ensure output directory exists
|
| 256 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 257 |
+
|
| 258 |
+
# Convert inputs to numpy arrays if they aren't already
|
| 259 |
+
time_points = np.array(time_points)
|
| 260 |
+
od_measurements = np.array(od_measurements)
|
| 261 |
+
|
| 262 |
+
# Define the Gompertz growth model function
|
| 263 |
+
def gompertz_model(t, lag, mu_max, A):
|
| 264 |
+
"""Gompertz growth model.
|
| 265 |
+
|
| 266 |
+
Parameters
|
| 267 |
+
----------
|
| 268 |
+
t: time points
|
| 269 |
+
lag: lag time (hours)
|
| 270 |
+
mu_max: maximum growth rate (per hour)
|
| 271 |
+
A: carrying capacity (maximum OD)
|
| 272 |
+
|
| 273 |
+
"""
|
| 274 |
+
return A * np.exp(-np.exp(mu_max * np.exp(1) * (lag - t) / A + 1))
|
| 275 |
+
|
| 276 |
+
# Initial parameter guesses
|
| 277 |
+
p0 = [
|
| 278 |
+
np.mean(time_points) / 3, # lag time guess: 1/3 of the mean time
|
| 279 |
+
0.5, # growth rate guess: 0.5 per hour
|
| 280 |
+
max(od_measurements) * 1.1, # carrying capacity: slightly above max OD
|
| 281 |
+
]
|
| 282 |
+
|
| 283 |
+
# Fit the model to the data
|
| 284 |
+
try:
|
| 285 |
+
popt, pcov = curve_fit(
|
| 286 |
+
gompertz_model,
|
| 287 |
+
time_points,
|
| 288 |
+
od_measurements,
|
| 289 |
+
p0=p0,
|
| 290 |
+
bounds=([0, 0, 0], [np.inf, np.inf, np.inf]),
|
| 291 |
+
)
|
| 292 |
+
lag_time, mu_max, carrying_capacity = popt
|
| 293 |
+
|
| 294 |
+
# Calculate doubling time (ln(2)/μ)
|
| 295 |
+
doubling_time = np.log(2) / mu_max
|
| 296 |
+
|
| 297 |
+
# Generate fitted curve for plotting
|
| 298 |
+
t_smooth = np.linspace(min(time_points), max(time_points), 100)
|
| 299 |
+
od_fit = gompertz_model(t_smooth, *popt)
|
| 300 |
+
|
| 301 |
+
# Create growth curve plot
|
| 302 |
+
plt.figure(figsize=(10, 6))
|
| 303 |
+
plt.scatter(time_points, od_measurements, label="Observed OD600")
|
| 304 |
+
plt.plot(t_smooth, od_fit, "r-", label="Fitted Gompertz model")
|
| 305 |
+
plt.xlabel("Time (hours)")
|
| 306 |
+
plt.ylabel("Optical Density (OD600)")
|
| 307 |
+
plt.title(f"Bacterial Growth Curve: {strain_name}")
|
| 308 |
+
plt.legend()
|
| 309 |
+
plt.grid(True, linestyle="--", alpha=0.7)
|
| 310 |
+
|
| 311 |
+
# Save the plot
|
| 312 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 313 |
+
plot_filename = os.path.join(output_dir, f"{strain_name.replace(' ', '_')}_{timestamp}_growth_curve.png")
|
| 314 |
+
plt.savefig(plot_filename)
|
| 315 |
+
plt.close()
|
| 316 |
+
|
| 317 |
+
# Create research log
|
| 318 |
+
log = f"""## Bacterial Growth Rate Analysis: {strain_name}
|
| 319 |
+
|
| 320 |
+
### Analysis Summary
|
| 321 |
+
- **Date and Time:** {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
|
| 322 |
+
- **Sample:** {strain_name}
|
| 323 |
+
- **Data Points:** {len(time_points)} measurements over {max(time_points) - min(time_points):.2f} hours
|
| 324 |
+
|
| 325 |
+
### Growth Parameters (Gompertz Model)
|
| 326 |
+
- **Lag Phase Duration:** {lag_time:.2f} hours
|
| 327 |
+
- **Maximum Growth Rate (μ_max):** {mu_max:.4f} per hour
|
| 328 |
+
- **Doubling Time:** {doubling_time:.2f} hours
|
| 329 |
+
- **Carrying Capacity (max OD):** {carrying_capacity:.4f} OD600 units
|
| 330 |
+
|
| 331 |
+
### Methods
|
| 332 |
+
1. OD600 measurements were taken at various time points
|
| 333 |
+
2. Data was fitted to the Gompertz growth model
|
| 334 |
+
3. Growth parameters were extracted from the fitted model
|
| 335 |
+
4. Growth curve was plotted and saved as: {plot_filename}
|
| 336 |
+
|
| 337 |
+
### Note
|
| 338 |
+
The Gompertz model provides a good representation of bacterial growth phases including lag, exponential, and stationary phases.
|
| 339 |
+
"""
|
| 340 |
+
|
| 341 |
+
return log
|
| 342 |
+
|
| 343 |
+
except RuntimeError as e:
|
| 344 |
+
return f"""## Bacterial Growth Rate Analysis: {strain_name}
|
| 345 |
+
|
| 346 |
+
### Error in Analysis
|
| 347 |
+
Failed to fit the growth model to the provided data: {str(e)}
|
| 348 |
+
|
| 349 |
+
### Suggestions
|
| 350 |
+
- Check if the data shows a typical growth pattern
|
| 351 |
+
- Ensure sufficient data points are provided
|
| 352 |
+
- Try different initial parameter guesses or a different growth model
|
| 353 |
+
"""
|
| 354 |
+
|
| 355 |
+
|
| 356 |
+
def analyze_barcode_sequencing_data(
|
| 357 |
+
input_file,
|
| 358 |
+
barcode_pattern=None,
|
| 359 |
+
flanking_seq_5prime=None,
|
| 360 |
+
flanking_seq_3prime=None,
|
| 361 |
+
min_count=5,
|
| 362 |
+
output_dir="./results",
|
| 363 |
+
):
|
| 364 |
+
"""Analyze sequencing data to extract, quantify and determine lineage relationships of barcodes.
|
| 365 |
+
|
| 366 |
+
Parameters
|
| 367 |
+
----------
|
| 368 |
+
input_file : str
|
| 369 |
+
Path to the input sequencing file in FASTQ or FASTA format
|
| 370 |
+
barcode_pattern : str, optional
|
| 371 |
+
Regular expression pattern to identify barcodes. If None, will use flanking sequences
|
| 372 |
+
flanking_seq_5prime : str, optional
|
| 373 |
+
5' flanking sequence of the barcode region
|
| 374 |
+
flanking_seq_3prime : str, optional
|
| 375 |
+
3' flanking sequence of the barcode region
|
| 376 |
+
min_count : int, default=5
|
| 377 |
+
Minimum count threshold for considering a barcode
|
| 378 |
+
output_dir : str, default="./results"
|
| 379 |
+
Directory to save output files
|
| 380 |
+
|
| 381 |
+
Returns
|
| 382 |
+
-------
|
| 383 |
+
str
|
| 384 |
+
Research log summarizing the analysis steps and results
|
| 385 |
+
|
| 386 |
+
"""
|
| 387 |
+
import os
|
| 388 |
+
import re
|
| 389 |
+
from collections import Counter
|
| 390 |
+
|
| 391 |
+
import numpy as np
|
| 392 |
+
from Bio import SeqIO
|
| 393 |
+
from scipy.cluster.hierarchy import fcluster, linkage
|
| 394 |
+
from scipy.spatial.distance import pdist
|
| 395 |
+
|
| 396 |
+
# Create output directory if it doesn't exist
|
| 397 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 398 |
+
|
| 399 |
+
# Initialize research log
|
| 400 |
+
log = "# Barcode Sequencing Analysis Research Log\n\n"
|
| 401 |
+
log += f"## Input Data\n- File: {input_file}\n\n"
|
| 402 |
+
|
| 403 |
+
# Step 1: Read sequences and extract barcodes
|
| 404 |
+
log += "## Step 1: Extracting barcodes from sequencing data\n"
|
| 405 |
+
|
| 406 |
+
# Determine file format based on extension
|
| 407 |
+
file_format = "fastq" if input_file.endswith((".fastq", ".fq")) else "fasta"
|
| 408 |
+
|
| 409 |
+
barcodes = []
|
| 410 |
+
total_reads = 0
|
| 411 |
+
|
| 412 |
+
for record in SeqIO.parse(input_file, file_format):
|
| 413 |
+
total_reads += 1
|
| 414 |
+
seq_str = str(record.seq)
|
| 415 |
+
|
| 416 |
+
# Extract barcode using pattern or flanking sequences
|
| 417 |
+
if barcode_pattern:
|
| 418 |
+
match = re.search(barcode_pattern, seq_str)
|
| 419 |
+
if match:
|
| 420 |
+
barcodes.append(match.group(0))
|
| 421 |
+
elif flanking_seq_5prime and flanking_seq_3prime:
|
| 422 |
+
pattern = f"{flanking_seq_5prime}(.*?){flanking_seq_3prime}"
|
| 423 |
+
match = re.search(pattern, seq_str)
|
| 424 |
+
if match:
|
| 425 |
+
barcodes.append(match.group(1))
|
| 426 |
+
|
| 427 |
+
log += f"- Total reads processed: {total_reads}\n"
|
| 428 |
+
log += f"- Barcodes extracted: {len(barcodes)}\n\n"
|
| 429 |
+
|
| 430 |
+
if len(barcodes) == 0:
|
| 431 |
+
log += "ERROR: No barcodes found. Check your barcode pattern or flanking sequences.\n"
|
| 432 |
+
return log
|
| 433 |
+
|
| 434 |
+
# Step 2: Quantify barcode abundances
|
| 435 |
+
log += "## Step 2: Quantifying barcode abundances\n"
|
| 436 |
+
barcode_counts = Counter(barcodes)
|
| 437 |
+
|
| 438 |
+
# Filter low-abundance barcodes
|
| 439 |
+
filtered_barcodes = {bc: count for bc, count in barcode_counts.items() if count >= min_count}
|
| 440 |
+
|
| 441 |
+
log += f"- Unique barcodes: {len(barcode_counts)}\n"
|
| 442 |
+
log += f"- Barcodes with count ≥ {min_count}: {len(filtered_barcodes)}\n"
|
| 443 |
+
|
| 444 |
+
# Save barcode counts to file
|
| 445 |
+
count_file = os.path.join(output_dir, "barcode_counts.tsv")
|
| 446 |
+
with open(count_file, "w") as f:
|
| 447 |
+
f.write("Barcode\tCount\tFrequency\n")
|
| 448 |
+
for bc, count in sorted(filtered_barcodes.items(), key=lambda x: x[1], reverse=True):
|
| 449 |
+
freq = count / total_reads
|
| 450 |
+
f.write(f"{bc}\t{count}\t{freq:.6f}\n")
|
| 451 |
+
|
| 452 |
+
log += f"- Barcode abundances saved to: {count_file}\n\n"
|
| 453 |
+
|
| 454 |
+
# Step 3: Analyze barcode relationships (lineage analysis)
|
| 455 |
+
log += "## Step 3: Analyzing barcode relationships and lineages\n"
|
| 456 |
+
|
| 457 |
+
# Only proceed if we have enough barcodes
|
| 458 |
+
if len(filtered_barcodes) < 2:
|
| 459 |
+
log += "- Not enough barcodes for lineage analysis after filtering\n\n"
|
| 460 |
+
else:
|
| 461 |
+
# Convert barcodes to numerical representation for distance calculation
|
| 462 |
+
barcode_list = list(filtered_barcodes.keys())
|
| 463 |
+
|
| 464 |
+
# Create a simple distance matrix based on Hamming distance
|
| 465 |
+
def hamming_distance(s1, s2):
|
| 466 |
+
# If sequences have different lengths, pad the shorter one
|
| 467 |
+
if len(s1) != len(s2):
|
| 468 |
+
max_len = max(len(s1), len(s2))
|
| 469 |
+
s1 = s1.ljust(max_len)
|
| 470 |
+
s2 = s2.ljust(max_len)
|
| 471 |
+
return sum(c1 != c2 for c1, c2 in zip(s1, s2, strict=False))
|
| 472 |
+
|
| 473 |
+
# Create distance matrix
|
| 474 |
+
n = len(barcode_list)
|
| 475 |
+
dist_matrix = np.zeros((n, n))
|
| 476 |
+
for i in range(n):
|
| 477 |
+
for j in range(i + 1, n):
|
| 478 |
+
dist = hamming_distance(barcode_list[i], barcode_list[j])
|
| 479 |
+
dist_matrix[i, j] = dist
|
| 480 |
+
dist_matrix[j, i] = dist
|
| 481 |
+
|
| 482 |
+
# Perform hierarchical clustering
|
| 483 |
+
try:
|
| 484 |
+
condensed_dist = pdist(dist_matrix)
|
| 485 |
+
Z = linkage(condensed_dist, method="average")
|
| 486 |
+
max_dist = 3 # Max Hamming distance to consider barcodes in same lineage
|
| 487 |
+
clusters = fcluster(Z, max_dist, criterion="distance")
|
| 488 |
+
|
| 489 |
+
# Count clusters
|
| 490 |
+
cluster_counts = Counter(clusters)
|
| 491 |
+
|
| 492 |
+
log += f"- Identified {len(cluster_counts)} potential lineages\n"
|
| 493 |
+
log += f"- Largest lineage contains {max(cluster_counts.values())} barcodes\n"
|
| 494 |
+
|
| 495 |
+
# Save lineage information
|
| 496 |
+
lineage_file = os.path.join(output_dir, "barcode_lineages.tsv")
|
| 497 |
+
with open(lineage_file, "w") as f:
|
| 498 |
+
f.write("Barcode\tCount\tLineage\n")
|
| 499 |
+
for i, bc in enumerate(barcode_list):
|
| 500 |
+
f.write(f"{bc}\t{filtered_barcodes[bc]}\t{clusters[i]}\n")
|
| 501 |
+
|
| 502 |
+
log += f"- Lineage assignments saved to: {lineage_file}\n\n"
|
| 503 |
+
except Exception as e:
|
| 504 |
+
log += f"- Error in lineage analysis: {str(e)}\n\n"
|
| 505 |
+
|
| 506 |
+
# Step 4: Summary
|
| 507 |
+
log += "## Summary\n"
|
| 508 |
+
log += f"- Processed {total_reads} sequencing reads\n"
|
| 509 |
+
log += f"- Identified {len(barcode_counts)} unique barcodes\n"
|
| 510 |
+
log += f"- {len(filtered_barcodes)} barcodes passed abundance threshold (≥ {min_count})\n"
|
| 511 |
+
|
| 512 |
+
return log
|
| 513 |
+
|
| 514 |
+
|
| 515 |
+
def analyze_bifurcation_diagram(time_series_data, parameter_values, system_name="Dynamical System", output_dir="./"):
|
| 516 |
+
"""Performs bifurcation analysis on a dynamical system and generates a bifurcation diagram.
|
| 517 |
+
|
| 518 |
+
Parameters
|
| 519 |
+
----------
|
| 520 |
+
time_series_data : numpy.ndarray
|
| 521 |
+
A 2D array where each row represents a time series for a specific parameter value.
|
| 522 |
+
Shape should be (n_parameter_values, n_time_points).
|
| 523 |
+
parameter_values : numpy.ndarray
|
| 524 |
+
1D array of parameter values corresponding to each time series.
|
| 525 |
+
Shape should be (n_parameter_values,).
|
| 526 |
+
system_name : str, optional
|
| 527 |
+
Name of the dynamical system being analyzed, used for plot titles.
|
| 528 |
+
output_dir : str, optional
|
| 529 |
+
Directory to save the output files.
|
| 530 |
+
|
| 531 |
+
Returns
|
| 532 |
+
-------
|
| 533 |
+
str
|
| 534 |
+
Research log summarizing the bifurcation analysis process and results.
|
| 535 |
+
|
| 536 |
+
"""
|
| 537 |
+
import os
|
| 538 |
+
|
| 539 |
+
import matplotlib
|
| 540 |
+
|
| 541 |
+
matplotlib.use("Agg") # Use non-interactive backend
|
| 542 |
+
import matplotlib.pyplot as plt
|
| 543 |
+
import numpy as np
|
| 544 |
+
from scipy.signal import find_peaks
|
| 545 |
+
|
| 546 |
+
# Create output directory if it doesn't exist
|
| 547 |
+
if not os.path.exists(output_dir):
|
| 548 |
+
os.makedirs(output_dir)
|
| 549 |
+
|
| 550 |
+
# Initialize research log
|
| 551 |
+
research_log = f"# Bifurcation Analysis for {system_name}\n\n"
|
| 552 |
+
research_log += "## Analysis Steps\n\n"
|
| 553 |
+
|
| 554 |
+
# Step 1: Verify input data
|
| 555 |
+
research_log += "### Step 1: Data Verification\n"
|
| 556 |
+
if len(parameter_values) != time_series_data.shape[0]:
|
| 557 |
+
research_log += "Error: Number of parameter values does not match number of time series.\n"
|
| 558 |
+
return research_log
|
| 559 |
+
|
| 560 |
+
research_log += f"- Analyzed {len(parameter_values)} parameter values\n"
|
| 561 |
+
research_log += f"- Each time series contains {time_series_data.shape[1]} data points\n\n"
|
| 562 |
+
|
| 563 |
+
# Step 2: Calculate indicators for different dynamical regimes
|
| 564 |
+
research_log += "### Step 2: Calculating Dynamical Indicators\n"
|
| 565 |
+
|
| 566 |
+
# Initialize arrays to store results
|
| 567 |
+
local_maxima_counts = np.zeros(len(parameter_values))
|
| 568 |
+
lyapunov_estimates = np.zeros(len(parameter_values))
|
| 569 |
+
attractor_points = []
|
| 570 |
+
|
| 571 |
+
for i, series in enumerate(time_series_data):
|
| 572 |
+
# Use the last 70% of the time series to avoid transients
|
| 573 |
+
steady_state = series[int(0.7 * len(series)) :]
|
| 574 |
+
|
| 575 |
+
# Find local maxima (peaks) to identify periodicity
|
| 576 |
+
peaks, _ = find_peaks(steady_state)
|
| 577 |
+
local_maxima_counts[i] = len(peaks)
|
| 578 |
+
|
| 579 |
+
# Simple estimate of the largest Lyapunov exponent
|
| 580 |
+
# Positive values suggest chaos, negative values suggest stability
|
| 581 |
+
if len(steady_state) > 10:
|
| 582 |
+
diffs = np.abs(np.diff(steady_state))
|
| 583 |
+
if np.mean(diffs) > 0:
|
| 584 |
+
lyapunov_estimates[i] = np.log(np.mean(diffs))
|
| 585 |
+
else:
|
| 586 |
+
lyapunov_estimates[i] = -1.0 # Stable behavior
|
| 587 |
+
|
| 588 |
+
# Collect points for the bifurcation diagram
|
| 589 |
+
# We use local maxima as sampling points for the attractor
|
| 590 |
+
if len(peaks) > 0:
|
| 591 |
+
attractor_points.append((parameter_values[i], steady_state[peaks]))
|
| 592 |
+
else:
|
| 593 |
+
# If no peaks, use the last few points
|
| 594 |
+
attractor_points.append((parameter_values[i], steady_state[-5:]))
|
| 595 |
+
|
| 596 |
+
research_log += "- Calculated approximate Lyapunov exponents\n"
|
| 597 |
+
research_log += "- Identified local maxima for periodicity analysis\n"
|
| 598 |
+
research_log += "- Sampled attractor points for bifurcation diagram\n\n"
|
| 599 |
+
|
| 600 |
+
# Step 3: Classify dynamical regimes
|
| 601 |
+
research_log += "### Step 3: Classifying Dynamical Regimes\n"
|
| 602 |
+
|
| 603 |
+
# Initialize arrays for regime classification
|
| 604 |
+
regimes = np.full(len(parameter_values), "unknown", dtype=object)
|
| 605 |
+
|
| 606 |
+
# Classify based on Lyapunov exponent and periodicity
|
| 607 |
+
for i in range(len(parameter_values)):
|
| 608 |
+
if lyapunov_estimates[i] > 0.05:
|
| 609 |
+
regimes[i] = "chaotic"
|
| 610 |
+
elif local_maxima_counts[i] == 0:
|
| 611 |
+
regimes[i] = "stable"
|
| 612 |
+
elif local_maxima_counts[i] == 1:
|
| 613 |
+
regimes[i] = "period-1"
|
| 614 |
+
elif local_maxima_counts[i] == 2:
|
| 615 |
+
regimes[i] = "period-2"
|
| 616 |
+
elif local_maxima_counts[i] == 4:
|
| 617 |
+
regimes[i] = "period-4"
|
| 618 |
+
elif local_maxima_counts[i] > 4:
|
| 619 |
+
if lyapunov_estimates[i] > 0:
|
| 620 |
+
regimes[i] = "chaotic"
|
| 621 |
+
else:
|
| 622 |
+
regimes[i] = f"period-{int(local_maxima_counts[i])}"
|
| 623 |
+
|
| 624 |
+
# Count regime types
|
| 625 |
+
unique_regimes = np.unique(regimes)
|
| 626 |
+
for regime in unique_regimes:
|
| 627 |
+
count = np.sum(regimes == regime)
|
| 628 |
+
research_log += f"- Identified {count} parameter values in '{regime}' regime\n"
|
| 629 |
+
|
| 630 |
+
research_log += "\n"
|
| 631 |
+
|
| 632 |
+
# Step 4: Create bifurcation diagram
|
| 633 |
+
research_log += "### Step 4: Creating Bifurcation Diagram\n"
|
| 634 |
+
|
| 635 |
+
plt.figure(figsize=(12, 8))
|
| 636 |
+
|
| 637 |
+
# Plot bifurcation points
|
| 638 |
+
for param_val, points in attractor_points:
|
| 639 |
+
y_values = points.flatten() # Flatten in case points is multi-dimensional
|
| 640 |
+
plt.plot([param_val] * len(y_values), y_values, "k.", markersize=0.5)
|
| 641 |
+
|
| 642 |
+
# Highlight different regimes with colored background
|
| 643 |
+
regime_changes = np.where(regimes[:-1] != regimes[1:])[0]
|
| 644 |
+
regime_boundaries = (
|
| 645 |
+
[parameter_values[0]] + [parameter_values[i + 1] for i in regime_changes] + [parameter_values[-1]]
|
| 646 |
+
)
|
| 647 |
+
|
| 648 |
+
colors = {
|
| 649 |
+
"stable": "lightblue",
|
| 650 |
+
"period-1": "lightgreen",
|
| 651 |
+
"period-2": "lightyellow",
|
| 652 |
+
"period-4": "lightpink",
|
| 653 |
+
"chaotic": "salmon",
|
| 654 |
+
}
|
| 655 |
+
|
| 656 |
+
for i in range(len(regime_boundaries) - 1):
|
| 657 |
+
start_idx = list(parameter_values).index(regime_boundaries[i])
|
| 658 |
+
regime = regimes[start_idx]
|
| 659 |
+
if regime in colors:
|
| 660 |
+
plt.axvspan(
|
| 661 |
+
regime_boundaries[i],
|
| 662 |
+
regime_boundaries[i + 1],
|
| 663 |
+
alpha=0.3,
|
| 664 |
+
color=colors.get(regime, "lightgray"),
|
| 665 |
+
)
|
| 666 |
+
|
| 667 |
+
plt.xlabel("Parameter Value")
|
| 668 |
+
plt.ylabel("State Variable")
|
| 669 |
+
plt.title(f"Bifurcation Diagram for {system_name}")
|
| 670 |
+
|
| 671 |
+
# Add legend for regimes
|
| 672 |
+
from matplotlib.patches import Patch
|
| 673 |
+
|
| 674 |
+
legend_elements = [
|
| 675 |
+
Patch(facecolor=colors.get(regime, "lightgray"), alpha=0.3, label=regime)
|
| 676 |
+
for regime in unique_regimes
|
| 677 |
+
if regime in colors
|
| 678 |
+
]
|
| 679 |
+
plt.legend(handles=legend_elements, loc="upper right")
|
| 680 |
+
|
| 681 |
+
# Save the figure
|
| 682 |
+
output_file = os.path.join(output_dir, f"{system_name.replace(' ', '_')}_bifurcation_diagram.png")
|
| 683 |
+
plt.savefig(output_file, dpi=300)
|
| 684 |
+
plt.close()
|
| 685 |
+
|
| 686 |
+
research_log += "- Created bifurcation diagram showing different dynamical regimes\n"
|
| 687 |
+
research_log += f"- Diagram saved as: {output_file}\n\n"
|
| 688 |
+
|
| 689 |
+
# Step 5: Identify regime transitions
|
| 690 |
+
research_log += "### Step 5: Identifying Regime Transitions\n"
|
| 691 |
+
|
| 692 |
+
if len(regime_changes) > 0:
|
| 693 |
+
for i in regime_changes:
|
| 694 |
+
research_log += f"- Transition from '{regimes[i]}' to '{regimes[i + 1]}' at parameter value ≈ {parameter_values[i + 1]:.4f}\n"
|
| 695 |
+
else:
|
| 696 |
+
research_log += "- No regime transitions detected in the parameter range\n"
|
| 697 |
+
|
| 698 |
+
research_log += "\n### Summary\n"
|
| 699 |
+
research_log += f"Bifurcation analysis completed for {system_name}. "
|
| 700 |
+
research_log += f"The system exhibits {len(unique_regimes)} different dynamical regimes "
|
| 701 |
+
research_log += f"across the parameter range [{parameter_values[0]:.4f}, {parameter_values[-1]:.4f}].\n"
|
| 702 |
+
|
| 703 |
+
return research_log
|
| 704 |
+
|
| 705 |
+
|
| 706 |
+
def create_biochemical_network_sbml_model(reaction_network, kinetic_parameters, output_file="biochemical_model.xml"):
|
| 707 |
+
"""Generate a mathematical model of a biochemical network in SBML format.
|
| 708 |
+
|
| 709 |
+
Parameters
|
| 710 |
+
----------
|
| 711 |
+
reaction_network : list of dict
|
| 712 |
+
List of dictionaries, each representing a reaction with keys:
|
| 713 |
+
- 'id': Reaction identifier
|
| 714 |
+
- 'name': Reaction name
|
| 715 |
+
- 'reactants': Dict of reactant species IDs and their stoichiometry
|
| 716 |
+
- 'products': Dict of product species IDs and their stoichiometry
|
| 717 |
+
- 'reversible': Boolean indicating if reaction is reversible
|
| 718 |
+
|
| 719 |
+
kinetic_parameters : dict
|
| 720 |
+
Dictionary mapping reaction IDs to their kinetic law parameters.
|
| 721 |
+
Each entry should contain:
|
| 722 |
+
- 'law_type': Type of kinetic law (e.g., 'mass_action', 'michaelis_menten')
|
| 723 |
+
- 'parameters': Dict of parameter names and values
|
| 724 |
+
|
| 725 |
+
output_file : str, optional
|
| 726 |
+
File path to save the SBML model (default: "biochemical_model.xml")
|
| 727 |
+
|
| 728 |
+
Returns
|
| 729 |
+
-------
|
| 730 |
+
str
|
| 731 |
+
Research log summarizing the model creation process
|
| 732 |
+
|
| 733 |
+
"""
|
| 734 |
+
import os
|
| 735 |
+
|
| 736 |
+
import libsbml
|
| 737 |
+
|
| 738 |
+
log = "# Biochemical Network SBML Model Creation Log\n\n"
|
| 739 |
+
|
| 740 |
+
# Step 1: Create an SBML document
|
| 741 |
+
log += "## Step 1: Creating SBML document\n"
|
| 742 |
+
sbml_ns = libsbml.SBMLNamespaces(3, 2) # SBML Level 3 Version 2
|
| 743 |
+
document = libsbml.SBMLDocument(sbml_ns)
|
| 744 |
+
model = document.createModel()
|
| 745 |
+
model.setId("biochemical_network_model")
|
| 746 |
+
model.setName("Biochemical Network Model")
|
| 747 |
+
log += "- Created SBML Level 3 Version 2 document\n"
|
| 748 |
+
log += "- Created model with ID 'biochemical_network_model'\n\n"
|
| 749 |
+
|
| 750 |
+
# Step 2: Create default compartment
|
| 751 |
+
log += "## Step 2: Creating default compartment\n"
|
| 752 |
+
compartment = model.createCompartment()
|
| 753 |
+
compartment.setId("default")
|
| 754 |
+
compartment.setConstant(True)
|
| 755 |
+
compartment.setSize(1.0)
|
| 756 |
+
compartment.setSpatialDimensions(3)
|
| 757 |
+
log += "- Created default compartment\n\n"
|
| 758 |
+
|
| 759 |
+
# Step 3: Create species
|
| 760 |
+
log += "## Step 3: Creating species\n"
|
| 761 |
+
species_set = set()
|
| 762 |
+
|
| 763 |
+
# Collect all unique species from reactions
|
| 764 |
+
for reaction in reaction_network:
|
| 765 |
+
for species_id in list(reaction["reactants"].keys()) + list(reaction["products"].keys()):
|
| 766 |
+
species_set.add(species_id)
|
| 767 |
+
|
| 768 |
+
# Create species in the model
|
| 769 |
+
for species_id in species_set:
|
| 770 |
+
species = model.createSpecies()
|
| 771 |
+
species.setId(species_id)
|
| 772 |
+
species.setCompartment("default")
|
| 773 |
+
species.setInitialConcentration(0.0) # Default initial concentration
|
| 774 |
+
species.setHasOnlySubstanceUnits(False)
|
| 775 |
+
species.setBoundaryCondition(False)
|
| 776 |
+
species.setConstant(False)
|
| 777 |
+
log += f"- Created species '{species_id}'\n"
|
| 778 |
+
log += "\n"
|
| 779 |
+
|
| 780 |
+
# Step 4: Create reactions
|
| 781 |
+
log += "## Step 4: Creating reactions and kinetic laws\n"
|
| 782 |
+
for reaction_data in reaction_network:
|
| 783 |
+
reaction = model.createReaction()
|
| 784 |
+
reaction.setId(reaction_data["id"])
|
| 785 |
+
reaction.setName(reaction_data["name"])
|
| 786 |
+
reaction.setReversible(reaction_data["reversible"])
|
| 787 |
+
reaction.setFast(False)
|
| 788 |
+
|
| 789 |
+
# Add reactants
|
| 790 |
+
for reactant_id, stoichiometry in reaction_data["reactants"].items():
|
| 791 |
+
species_ref = reaction.createReactant()
|
| 792 |
+
species_ref.setSpecies(reactant_id)
|
| 793 |
+
species_ref.setStoichiometry(stoichiometry)
|
| 794 |
+
species_ref.setConstant(True)
|
| 795 |
+
|
| 796 |
+
# Add products
|
| 797 |
+
for product_id, stoichiometry in reaction_data["products"].items():
|
| 798 |
+
species_ref = reaction.createProduct()
|
| 799 |
+
species_ref.setSpecies(product_id)
|
| 800 |
+
species_ref.setStoichiometry(stoichiometry)
|
| 801 |
+
species_ref.setConstant(True)
|
| 802 |
+
|
| 803 |
+
# Create kinetic law
|
| 804 |
+
if reaction_data["id"] in kinetic_parameters:
|
| 805 |
+
kinetic_data = kinetic_parameters[reaction_data["id"]]
|
| 806 |
+
kinetic_law = reaction.createKineticLaw()
|
| 807 |
+
|
| 808 |
+
# Add parameters to kinetic law
|
| 809 |
+
for param_name, param_value in kinetic_data["parameters"].items():
|
| 810 |
+
parameter = kinetic_law.createParameter()
|
| 811 |
+
parameter.setId(param_name)
|
| 812 |
+
parameter.setValue(param_value)
|
| 813 |
+
|
| 814 |
+
# Set the appropriate formula based on the law type
|
| 815 |
+
if kinetic_data["law_type"] == "mass_action":
|
| 816 |
+
# Simple mass action kinetics
|
| 817 |
+
reactants_formula = " * ".join([f"{reactant_id}" for reactant_id in reaction_data["reactants"]])
|
| 818 |
+
formula = f"k * {reactants_formula}" if reactants_formula else "k"
|
| 819 |
+
kinetic_law.setMath(libsbml.parseL3Formula(formula))
|
| 820 |
+
elif kinetic_data["law_type"] == "michaelis_menten":
|
| 821 |
+
# Michaelis-Menten kinetics (assuming single substrate)
|
| 822 |
+
substrate = list(reaction_data["reactants"].keys())[0] if reaction_data["reactants"] else "S"
|
| 823 |
+
formula = f"Vmax * {substrate} / (Km + {substrate})"
|
| 824 |
+
kinetic_law.setMath(libsbml.parseL3Formula(formula))
|
| 825 |
+
# Custom formula provided in the parameters
|
| 826 |
+
elif "formula" in kinetic_data:
|
| 827 |
+
kinetic_law.setMath(libsbml.parseL3Formula(kinetic_data["formula"]))
|
| 828 |
+
|
| 829 |
+
log += f"- Created reaction '{reaction_data['id']}' ({reaction_data['name']})\n"
|
| 830 |
+
if reaction_data["id"] in kinetic_parameters:
|
| 831 |
+
log += f" - Added kinetic law of type '{kinetic_parameters[reaction_data['id']]['law_type']}'\n"
|
| 832 |
+
log += "\n"
|
| 833 |
+
|
| 834 |
+
# Step 5: Validate the model
|
| 835 |
+
log += "## Step 5: Validating the SBML model\n"
|
| 836 |
+
document.setConsistencyChecks(libsbml.LIBSBML_CAT_UNITS_CONSISTENCY, False)
|
| 837 |
+
consistent = document.checkConsistency()
|
| 838 |
+
if consistent == 0:
|
| 839 |
+
log += "- Model passed consistency check\n"
|
| 840 |
+
else:
|
| 841 |
+
log += "- Model has consistency issues:\n"
|
| 842 |
+
for i in range(document.getNumErrors()):
|
| 843 |
+
error = document.getError(i)
|
| 844 |
+
log += f" - {error.getMessage()}\n"
|
| 845 |
+
log += "\n"
|
| 846 |
+
|
| 847 |
+
# Step 6: Write model to file
|
| 848 |
+
log += "## Step 6: Saving the SBML model\n"
|
| 849 |
+
libsbml.writeSBMLToFile(document, output_file)
|
| 850 |
+
log += f"- Model saved to '{os.path.abspath(output_file)}'\n\n"
|
| 851 |
+
|
| 852 |
+
# Step 7: Summary
|
| 853 |
+
log += "## Summary\n"
|
| 854 |
+
log += f"- Created SBML model with {len(species_set)} species and {len(reaction_network)} reactions\n"
|
| 855 |
+
log += f"- Model saved as '{output_file}'\n"
|
| 856 |
+
|
| 857 |
+
return log
|
| 858 |
+
|
| 859 |
+
|
| 860 |
+
def optimize_codons_for_heterologous_expression(target_sequence, host_codon_usage):
|
| 861 |
+
"""Analyzes and optimizes a DNA/RNA sequence for improved expression in a heterologous host organism.
|
| 862 |
+
|
| 863 |
+
Parameters
|
| 864 |
+
----------
|
| 865 |
+
target_sequence : str
|
| 866 |
+
The DNA or RNA sequence of the target gene to be optimized.
|
| 867 |
+
Should contain complete codons (length divisible by 3).
|
| 868 |
+
|
| 869 |
+
host_codon_usage : dict
|
| 870 |
+
Dictionary mapping codons to their usage frequency in the host organism.
|
| 871 |
+
Format: {'AUG': 0.8, 'GCC': 0.6, ...} or {'ATG': 0.8, 'GCC': 0.6, ...}
|
| 872 |
+
|
| 873 |
+
Returns
|
| 874 |
+
-------
|
| 875 |
+
str
|
| 876 |
+
A research log summarizing the optimization process and results.
|
| 877 |
+
|
| 878 |
+
"""
|
| 879 |
+
from Bio.Data import CodonTable
|
| 880 |
+
|
| 881 |
+
# Initialize research log
|
| 882 |
+
log = "# Codon Optimization Analysis Research Log\n\n"
|
| 883 |
+
|
| 884 |
+
# Check if sequence is DNA or RNA and standardize to DNA
|
| 885 |
+
is_rna = "U" in target_sequence
|
| 886 |
+
working_seq = target_sequence.replace("U", "T") if is_rna else target_sequence
|
| 887 |
+
|
| 888 |
+
log += "## Input Sequence Analysis\n"
|
| 889 |
+
log += f"- Sequence type: {'RNA' if is_rna else 'DNA'}\n"
|
| 890 |
+
log += f"- Sequence length: {len(working_seq)} nucleotides\n"
|
| 891 |
+
|
| 892 |
+
# Verify sequence length is divisible by 3
|
| 893 |
+
if len(working_seq) % 3 != 0:
|
| 894 |
+
log += (
|
| 895 |
+
f"- WARNING: Sequence length ({len(working_seq)}) is not divisible by 3. Optimization may be incomplete.\n"
|
| 896 |
+
)
|
| 897 |
+
|
| 898 |
+
# Extract codons from the sequence
|
| 899 |
+
original_codons = [working_seq[i : i + 3] for i in range(0, len(working_seq), 3)]
|
| 900 |
+
log += f"- Number of codons: {len(original_codons)}\n\n"
|
| 901 |
+
|
| 902 |
+
# Get standard genetic code
|
| 903 |
+
standard_table = CodonTable.standard_dna_table
|
| 904 |
+
|
| 905 |
+
# Create codon to amino acid mapping
|
| 906 |
+
codon_to_aa = {}
|
| 907 |
+
for codon, amino_acid in standard_table.forward_table.items():
|
| 908 |
+
codon_to_aa[codon] = amino_acid
|
| 909 |
+
|
| 910 |
+
# Add stop codons
|
| 911 |
+
for stop_codon in standard_table.stop_codons:
|
| 912 |
+
codon_to_aa[stop_codon] = "*" # * represents stop codon
|
| 913 |
+
|
| 914 |
+
# Create amino acid to codon mapping with host frequencies
|
| 915 |
+
aa_to_codons = {}
|
| 916 |
+
for codon, aa in codon_to_aa.items():
|
| 917 |
+
if aa not in aa_to_codons:
|
| 918 |
+
aa_to_codons[aa] = []
|
| 919 |
+
# Use host frequency or default to 0 if not provided
|
| 920 |
+
frequency = host_codon_usage.get(codon, 0)
|
| 921 |
+
aa_to_codons[aa].append((codon, frequency))
|
| 922 |
+
|
| 923 |
+
# Sort codons by frequency for each amino acid
|
| 924 |
+
for aa in aa_to_codons:
|
| 925 |
+
aa_to_codons[aa] = sorted(aa_to_codons[aa], key=lambda x: x[1], reverse=True)
|
| 926 |
+
|
| 927 |
+
# Optimize codons
|
| 928 |
+
optimized_codons = []
|
| 929 |
+
for codon in original_codons:
|
| 930 |
+
try:
|
| 931 |
+
aa = codon_to_aa[codon]
|
| 932 |
+
# Select the highest frequency codon for this amino acid
|
| 933 |
+
best_codon = aa_to_codons[aa][0][0] if aa_to_codons[aa] else codon
|
| 934 |
+
optimized_codons.append(best_codon)
|
| 935 |
+
except KeyError:
|
| 936 |
+
# If codon not in standard table, keep original
|
| 937 |
+
optimized_codons.append(codon)
|
| 938 |
+
log += f"- WARNING: Non-standard codon detected: {codon}. Keeping original.\n"
|
| 939 |
+
|
| 940 |
+
# Combine optimized codons into sequence
|
| 941 |
+
optimized_sequence = "".join(optimized_codons)
|
| 942 |
+
|
| 943 |
+
# Convert back to RNA if input was RNA
|
| 944 |
+
if is_rna:
|
| 945 |
+
optimized_sequence = optimized_sequence.replace("T", "U")
|
| 946 |
+
|
| 947 |
+
# Calculate optimization statistics
|
| 948 |
+
codon_changes = sum(1 for i, j in zip(original_codons, optimized_codons, strict=False) if i != j)
|
| 949 |
+
percent_changed = (codon_changes / len(original_codons)) * 100 if original_codons else 0
|
| 950 |
+
|
| 951 |
+
log += "## Optimization Results\n"
|
| 952 |
+
log += f"- Codons modified: {codon_changes} out of {len(original_codons)} ({percent_changed:.2f}%)\n"
|
| 953 |
+
|
| 954 |
+
# Save sequences to files
|
| 955 |
+
with open("original_sequence.txt", "w") as f:
|
| 956 |
+
f.write(target_sequence)
|
| 957 |
+
|
| 958 |
+
with open("optimized_sequence.txt", "w") as f:
|
| 959 |
+
f.write(optimized_sequence)
|
| 960 |
+
|
| 961 |
+
log += "- Original sequence saved to: original_sequence.txt\n"
|
| 962 |
+
log += "- Optimized sequence saved to: optimized_sequence.txt\n\n"
|
| 963 |
+
|
| 964 |
+
# Add summary
|
| 965 |
+
log += "## Summary\n"
|
| 966 |
+
log += "The target gene sequence has been optimized for expression in the provided host organism. "
|
| 967 |
+
log += f"The optimization process replaced {codon_changes} codons ({percent_changed:.2f}%) "
|
| 968 |
+
log += "with synonymous codons that have higher usage frequency in the host.\n"
|
| 969 |
+
|
| 970 |
+
return log
|
| 971 |
+
|
| 972 |
+
|
| 973 |
+
def simulate_gene_circuit_with_growth_feedback(
|
| 974 |
+
circuit_topology,
|
| 975 |
+
kinetic_params,
|
| 976 |
+
growth_params,
|
| 977 |
+
simulation_time=100,
|
| 978 |
+
time_points=1000,
|
| 979 |
+
):
|
| 980 |
+
"""Simulate gene regulatory circuit dynamics with growth feedback.
|
| 981 |
+
|
| 982 |
+
Parameters
|
| 983 |
+
----------
|
| 984 |
+
circuit_topology : numpy.ndarray
|
| 985 |
+
Adjacency matrix representing the gene circuit topology.
|
| 986 |
+
Positive values indicate activation, negative values indicate repression.
|
| 987 |
+
Shape should be (n_genes, n_genes) where n_genes is the number of genes in the circuit.
|
| 988 |
+
|
| 989 |
+
kinetic_params : dict
|
| 990 |
+
Dictionary containing kinetic parameters:
|
| 991 |
+
- 'basal_rates': list of basal expression rates for each gene
|
| 992 |
+
- 'degradation_rates': list of degradation rates for each gene
|
| 993 |
+
- 'hill_coefficients': list of Hill coefficients for regulatory interactions
|
| 994 |
+
- 'threshold_constants': list of threshold constants for regulatory interactions
|
| 995 |
+
|
| 996 |
+
growth_params : dict
|
| 997 |
+
Dictionary containing growth-related parameters:
|
| 998 |
+
- 'max_growth_rate': maximum cell growth rate
|
| 999 |
+
- 'growth_inhibition': how gene expression affects growth
|
| 1000 |
+
- 'gene_growth_weights': weights for how each gene affects growth
|
| 1001 |
+
|
| 1002 |
+
simulation_time : float, optional
|
| 1003 |
+
Total simulation time (default: 100)
|
| 1004 |
+
|
| 1005 |
+
time_points : int, optional
|
| 1006 |
+
Number of time points to sample (default: 1000)
|
| 1007 |
+
|
| 1008 |
+
Returns
|
| 1009 |
+
-------
|
| 1010 |
+
str
|
| 1011 |
+
Research log summarizing the simulation and results with file names of saved data
|
| 1012 |
+
|
| 1013 |
+
"""
|
| 1014 |
+
import datetime
|
| 1015 |
+
import json
|
| 1016 |
+
import os
|
| 1017 |
+
|
| 1018 |
+
import numpy as np
|
| 1019 |
+
from scipy.integrate import solve_ivp
|
| 1020 |
+
|
| 1021 |
+
# Extract parameters
|
| 1022 |
+
n_genes = circuit_topology.shape[0]
|
| 1023 |
+
basal_rates = kinetic_params["basal_rates"]
|
| 1024 |
+
degradation_rates = kinetic_params["degradation_rates"]
|
| 1025 |
+
hill_coefficients = kinetic_params["hill_coefficients"]
|
| 1026 |
+
threshold_constants = kinetic_params["threshold_constants"]
|
| 1027 |
+
|
| 1028 |
+
max_growth_rate = growth_params["max_growth_rate"]
|
| 1029 |
+
growth_inhibition = growth_params["growth_inhibition"]
|
| 1030 |
+
gene_growth_weights = growth_params["gene_growth_weights"]
|
| 1031 |
+
|
| 1032 |
+
# Define the ODE system
|
| 1033 |
+
def gene_circuit_odes(t, state):
|
| 1034 |
+
# Extract state variables
|
| 1035 |
+
gene_expressions = state[:n_genes]
|
| 1036 |
+
cell_mass = state[n_genes]
|
| 1037 |
+
|
| 1038 |
+
# Initialize derivatives
|
| 1039 |
+
dxdt = np.zeros(n_genes + 1)
|
| 1040 |
+
|
| 1041 |
+
# Gene expression dynamics
|
| 1042 |
+
for i in range(n_genes):
|
| 1043 |
+
# Basal expression
|
| 1044 |
+
production = basal_rates[i]
|
| 1045 |
+
|
| 1046 |
+
# Regulatory influences
|
| 1047 |
+
for j in range(n_genes):
|
| 1048 |
+
if circuit_topology[i, j] != 0:
|
| 1049 |
+
# Calculate regulatory effect
|
| 1050 |
+
regulation = gene_expressions[j] ** hill_coefficients[j] / (
|
| 1051 |
+
threshold_constants[j] ** hill_coefficients[j] + gene_expressions[j] ** hill_coefficients[j]
|
| 1052 |
+
)
|
| 1053 |
+
|
| 1054 |
+
if circuit_topology[i, j] > 0: # Activation
|
| 1055 |
+
production *= 1 + circuit_topology[i, j] * regulation
|
| 1056 |
+
else: # Repression
|
| 1057 |
+
production *= 1 + circuit_topology[i, j] * (1 - regulation)
|
| 1058 |
+
|
| 1059 |
+
# Dilution due to growth and degradation
|
| 1060 |
+
dilution = degradation_rates[i] + (dxdt[n_genes] / cell_mass if cell_mass > 0 else 0)
|
| 1061 |
+
|
| 1062 |
+
# Final rate equation for gene i
|
| 1063 |
+
dxdt[i] = production - dilution * gene_expressions[i]
|
| 1064 |
+
|
| 1065 |
+
# Cell growth dynamics
|
| 1066 |
+
growth_burden = sum(gene_growth_weights[i] * gene_expressions[i] for i in range(n_genes))
|
| 1067 |
+
dxdt[n_genes] = max_growth_rate * cell_mass / (1 + growth_inhibition * growth_burden)
|
| 1068 |
+
|
| 1069 |
+
return dxdt
|
| 1070 |
+
|
| 1071 |
+
# Initial conditions (starting with low gene expression and unit cell mass)
|
| 1072 |
+
initial_state = np.zeros(n_genes + 1)
|
| 1073 |
+
initial_state[:n_genes] = 0.1 # Low initial gene expression
|
| 1074 |
+
initial_state[n_genes] = 1.0 # Initial cell mass
|
| 1075 |
+
|
| 1076 |
+
# Solve the ODE system
|
| 1077 |
+
t_span = (0, simulation_time)
|
| 1078 |
+
t_eval = np.linspace(0, simulation_time, time_points)
|
| 1079 |
+
|
| 1080 |
+
solution = solve_ivp(
|
| 1081 |
+
gene_circuit_odes,
|
| 1082 |
+
t_span,
|
| 1083 |
+
initial_state,
|
| 1084 |
+
method="LSODA",
|
| 1085 |
+
t_eval=t_eval,
|
| 1086 |
+
rtol=1e-6,
|
| 1087 |
+
atol=1e-9,
|
| 1088 |
+
)
|
| 1089 |
+
|
| 1090 |
+
# Extract results
|
| 1091 |
+
time_series = solution.t
|
| 1092 |
+
gene_expression_time_series = solution.y[:n_genes, :]
|
| 1093 |
+
cell_growth_time_series = solution.y[n_genes, :]
|
| 1094 |
+
|
| 1095 |
+
# Save results to files
|
| 1096 |
+
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 1097 |
+
results_dir = "gene_circuit_results"
|
| 1098 |
+
|
| 1099 |
+
if not os.path.exists(results_dir):
|
| 1100 |
+
os.makedirs(results_dir)
|
| 1101 |
+
|
| 1102 |
+
# Save time series data
|
| 1103 |
+
data_filename = f"{results_dir}/gene_circuit_simulation_{timestamp}.npz"
|
| 1104 |
+
np.savez(
|
| 1105 |
+
data_filename,
|
| 1106 |
+
time=time_series,
|
| 1107 |
+
gene_expression=gene_expression_time_series,
|
| 1108 |
+
cell_growth=cell_growth_time_series,
|
| 1109 |
+
)
|
| 1110 |
+
|
| 1111 |
+
# Save parameters for reference
|
| 1112 |
+
params_filename = f"{results_dir}/simulation_params_{timestamp}.json"
|
| 1113 |
+
params_dict = {
|
| 1114 |
+
"circuit_topology": circuit_topology.tolist(),
|
| 1115 |
+
"kinetic_params": kinetic_params,
|
| 1116 |
+
"growth_params": growth_params,
|
| 1117 |
+
"simulation_time": simulation_time,
|
| 1118 |
+
"time_points": time_points,
|
| 1119 |
+
}
|
| 1120 |
+
|
| 1121 |
+
with open(params_filename, "w") as f:
|
| 1122 |
+
json.dump(params_dict, f, indent=2)
|
| 1123 |
+
|
| 1124 |
+
# Create research log
|
| 1125 |
+
log = f"""Gene Regulatory Circuit Simulation with Growth Feedback - {timestamp}
|
| 1126 |
+
|
| 1127 |
+
SIMULATION SUMMARY:
|
| 1128 |
+
- Simulated a gene regulatory circuit with {n_genes} genes
|
| 1129 |
+
- Incorporated growth feedback mechanisms
|
| 1130 |
+
- Total simulation time: {simulation_time} time units
|
| 1131 |
+
- Collected {time_points} data points
|
| 1132 |
+
|
| 1133 |
+
PARAMETERS:
|
| 1134 |
+
- Basal expression rates: {basal_rates}
|
| 1135 |
+
- Degradation rates: {degradation_rates}
|
| 1136 |
+
- Max growth rate: {max_growth_rate}
|
| 1137 |
+
- Growth inhibition factor: {growth_inhibition}
|
| 1138 |
+
|
| 1139 |
+
RESULTS:
|
| 1140 |
+
- Final gene expression levels: {gene_expression_time_series[:, -1].tolist()}
|
| 1141 |
+
- Final cell mass: {cell_growth_time_series[-1]:.4f}
|
| 1142 |
+
- Growth rate at end of simulation: {max_growth_rate / (1 + growth_inhibition * sum(gene_growth_weights[i] * gene_expression_time_series[i, -1] for i in range(n_genes))):.4f}
|
| 1143 |
+
|
| 1144 |
+
FILES:
|
| 1145 |
+
- Time series data saved to: {data_filename}
|
| 1146 |
+
- Simulation parameters saved to: {params_filename}
|
| 1147 |
+
"""
|
| 1148 |
+
|
| 1149 |
+
return log
|
| 1150 |
+
|
| 1151 |
+
|
| 1152 |
+
def identify_fas_functional_domains(sequence, sequence_type="protein", output_file="fas_domains_report.txt"):
|
| 1153 |
+
"""Identifies functional domains within a Fatty Acid Synthase (FAS) sequence and predicts their roles.
|
| 1154 |
+
|
| 1155 |
+
Parameters
|
| 1156 |
+
----------
|
| 1157 |
+
sequence : str
|
| 1158 |
+
The nucleotide or protein sequence of a FAS gene
|
| 1159 |
+
sequence_type : str
|
| 1160 |
+
Type of sequence provided - "protein" or "nucleotide" (default: "protein")
|
| 1161 |
+
output_file : str
|
| 1162 |
+
Name of the output file to save the detailed domain report (default: "fas_domains_report.txt")
|
| 1163 |
+
|
| 1164 |
+
Returns
|
| 1165 |
+
-------
|
| 1166 |
+
str
|
| 1167 |
+
A research log summarizing the steps taken and results of the domain analysis
|
| 1168 |
+
|
| 1169 |
+
"""
|
| 1170 |
+
import json
|
| 1171 |
+
import time
|
| 1172 |
+
|
| 1173 |
+
import requests
|
| 1174 |
+
from Bio.Seq import Seq
|
| 1175 |
+
|
| 1176 |
+
research_log = "# Fatty Acid Synthase (FAS) Domain Analysis\n\n"
|
| 1177 |
+
research_log += "## Input Sequence Analysis\n"
|
| 1178 |
+
research_log += f"- Sequence type: {sequence_type}\n"
|
| 1179 |
+
research_log += (
|
| 1180 |
+
f"- Sequence length: {len(sequence)} {'nucleotides' if sequence_type == 'nucleotide' else 'amino acids'}\n\n"
|
| 1181 |
+
)
|
| 1182 |
+
|
| 1183 |
+
# Convert nucleotide to protein if needed
|
| 1184 |
+
if sequence_type == "nucleotide":
|
| 1185 |
+
research_log += "## Sequence Translation\n"
|
| 1186 |
+
research_log += "- Converting nucleotide sequence to protein\n"
|
| 1187 |
+
try:
|
| 1188 |
+
protein_seq = str(Seq(sequence).translate())
|
| 1189 |
+
research_log += f"- Translated protein length: {len(protein_seq)} amino acids\n\n"
|
| 1190 |
+
except Exception as e:
|
| 1191 |
+
research_log += f"- Error in translation: {str(e)}\n"
|
| 1192 |
+
return research_log
|
| 1193 |
+
else:
|
| 1194 |
+
protein_seq = sequence
|
| 1195 |
+
|
| 1196 |
+
# FAS domain information dictionary
|
| 1197 |
+
fas_domains = {
|
| 1198 |
+
"ketoacyl-synt": {
|
| 1199 |
+
"name": "Ketoacyl Synthase (KS)",
|
| 1200 |
+
"function": "Catalyzes the condensation of malonyl-ACP with the growing fatty acid chain",
|
| 1201 |
+
},
|
| 1202 |
+
"Ketoacyl-synt_C": {
|
| 1203 |
+
"name": "Ketoacyl Synthase C-terminal (KS-C)",
|
| 1204 |
+
"function": "C-terminal domain of KS, involved in substrate binding",
|
| 1205 |
+
},
|
| 1206 |
+
"Acyl_transf_1": {
|
| 1207 |
+
"name": "Acyltransferase (AT)",
|
| 1208 |
+
"function": "Transfers acyl groups from acyl-CoA to ACP",
|
| 1209 |
+
},
|
| 1210 |
+
"ketoacyl-red": {
|
| 1211 |
+
"name": "Ketoacyl Reductase (KR)",
|
| 1212 |
+
"function": "Reduces the beta-ketoacyl group to a beta-hydroxyacyl group",
|
| 1213 |
+
},
|
| 1214 |
+
"Thioesterase": {
|
| 1215 |
+
"name": "Thioesterase (TE)",
|
| 1216 |
+
"function": "Releases the fatty acid from ACP by hydrolysis",
|
| 1217 |
+
},
|
| 1218 |
+
"PS-DH": {
|
| 1219 |
+
"name": "Dehydratase (DH)",
|
| 1220 |
+
"function": "Removes water from beta-hydroxyacyl-ACP to form trans-2-enoyl-ACP",
|
| 1221 |
+
},
|
| 1222 |
+
"Enoyl_red": {
|
| 1223 |
+
"name": "Enoyl Reductase (ER)",
|
| 1224 |
+
"function": "Reduces the double bond in enoyl-ACP to a saturated acyl-ACP",
|
| 1225 |
+
},
|
| 1226 |
+
"ACP": {
|
| 1227 |
+
"name": "Acyl Carrier Protein (ACP)",
|
| 1228 |
+
"function": "Carries the growing fatty acid chain between enzyme domains",
|
| 1229 |
+
},
|
| 1230 |
+
"PP-binding": {
|
| 1231 |
+
"name": "Phosphopantetheine Binding Domain",
|
| 1232 |
+
"function": "Attachment site for the phosphopantetheine prosthetic group in ACP",
|
| 1233 |
+
},
|
| 1234 |
+
}
|
| 1235 |
+
|
| 1236 |
+
# HMMER web API for domain identification
|
| 1237 |
+
research_log += "## Domain Identification\n"
|
| 1238 |
+
research_log += "- Using HMMER web API to search against Pfam database\n"
|
| 1239 |
+
|
| 1240 |
+
url = "https://www.ebi.ac.uk/Tools/hmmer/search/hmmscan"
|
| 1241 |
+
headers = {"Content-Type": "application/json", "Accept": "application/json"}
|
| 1242 |
+
data = {"hmmdb": "pfam", "seq": protein_seq}
|
| 1243 |
+
|
| 1244 |
+
try:
|
| 1245 |
+
response = requests.post(url, data=json.dumps(data), headers=headers)
|
| 1246 |
+
if response.status_code != 200:
|
| 1247 |
+
research_log += f"- Error: HMMER API returned status code {response.status_code}\n"
|
| 1248 |
+
return research_log
|
| 1249 |
+
|
| 1250 |
+
result_url = response.headers.get("Location", "")
|
| 1251 |
+
if not result_url:
|
| 1252 |
+
research_log += "- Error: No result URL returned from HMMER API\n"
|
| 1253 |
+
return research_log
|
| 1254 |
+
|
| 1255 |
+
# Wait for results to be ready
|
| 1256 |
+
time.sleep(2)
|
| 1257 |
+
|
| 1258 |
+
# Get results
|
| 1259 |
+
result_response = requests.get(f"{result_url}.json")
|
| 1260 |
+
if result_response.status_code != 200:
|
| 1261 |
+
research_log += "- Error: Could not retrieve results from HMMER API\n"
|
| 1262 |
+
return research_log
|
| 1263 |
+
|
| 1264 |
+
results = result_response.json()
|
| 1265 |
+
|
| 1266 |
+
# Process results
|
| 1267 |
+
domains_found = []
|
| 1268 |
+
if "results" in results and "hits" in results["results"]:
|
| 1269 |
+
hits = results["results"]["hits"]
|
| 1270 |
+
for hit in hits:
|
| 1271 |
+
if "domains" in hit:
|
| 1272 |
+
for domain in hit["domains"]:
|
| 1273 |
+
domain_name = hit.get("name", "").split(".")[0]
|
| 1274 |
+
domain_desc = hit.get("desc", "Unknown")
|
| 1275 |
+
domain_start = domain.get("ali_from", 0)
|
| 1276 |
+
domain_end = domain.get("ali_to", 0)
|
| 1277 |
+
|
| 1278 |
+
domains_found.append(
|
| 1279 |
+
{
|
| 1280 |
+
"name": domain_name,
|
| 1281 |
+
"description": domain_desc,
|
| 1282 |
+
"start": domain_start,
|
| 1283 |
+
"end": domain_end,
|
| 1284 |
+
"score": domain.get("score", 0),
|
| 1285 |
+
}
|
| 1286 |
+
)
|
| 1287 |
+
|
| 1288 |
+
# Check for FAS domains
|
| 1289 |
+
research_log += f"- Found {len(domains_found)} domains in total\n\n"
|
| 1290 |
+
research_log += "## FAS Functional Domains Identified\n"
|
| 1291 |
+
|
| 1292 |
+
fas_domains_found = []
|
| 1293 |
+
for domain in domains_found:
|
| 1294 |
+
for fas_key, fas_info in fas_domains.items():
|
| 1295 |
+
if fas_key.lower() in domain["name"].lower():
|
| 1296 |
+
fas_domains_found.append(
|
| 1297 |
+
{
|
| 1298 |
+
"name": fas_info["name"],
|
| 1299 |
+
"pfam_id": domain["name"],
|
| 1300 |
+
"start": domain["start"],
|
| 1301 |
+
"end": domain["end"],
|
| 1302 |
+
"function": fas_info["function"],
|
| 1303 |
+
}
|
| 1304 |
+
)
|
| 1305 |
+
|
| 1306 |
+
if not fas_domains_found:
|
| 1307 |
+
research_log += "- No specific FAS domains identified in the sequence\n"
|
| 1308 |
+
else:
|
| 1309 |
+
research_log += f"- Found {len(fas_domains_found)} FAS-related domains\n"
|
| 1310 |
+
for i, domain in enumerate(fas_domains_found, 1):
|
| 1311 |
+
research_log += f"\n### {i}. {domain['name']} (Positions {domain['start']}-{domain['end']})\n"
|
| 1312 |
+
research_log += f"- Pfam ID: {domain['pfam_id']}\n"
|
| 1313 |
+
research_log += f"- Function: {domain['function']}\n"
|
| 1314 |
+
|
| 1315 |
+
# Save detailed report to file
|
| 1316 |
+
with open(output_file, "w") as f:
|
| 1317 |
+
f.write("# Fatty Acid Synthase (FAS) Domain Analysis Detailed Report\n\n")
|
| 1318 |
+
f.write("## Input Sequence\n")
|
| 1319 |
+
f.write(f"- Type: {sequence_type}\n")
|
| 1320 |
+
f.write(f"- Length: {len(sequence)} {'nucleotides' if sequence_type == 'nucleotide' else 'amino acids'}\n")
|
| 1321 |
+
if sequence_type == "nucleotide":
|
| 1322 |
+
f.write(f"- Translated protein length: {len(protein_seq)} amino acids\n")
|
| 1323 |
+
|
| 1324 |
+
f.write("\n## All Domains Found\n")
|
| 1325 |
+
for i, domain in enumerate(domains_found, 1):
|
| 1326 |
+
f.write(f"\n### {i}. {domain['name']} (Positions {domain['start']}-{domain['end']})\n")
|
| 1327 |
+
f.write(f"- Description: {domain['description']}\n")
|
| 1328 |
+
f.write(f"- Score: {domain['score']}\n")
|
| 1329 |
+
|
| 1330 |
+
f.write("\n## FAS Functional Domains\n")
|
| 1331 |
+
if not fas_domains_found:
|
| 1332 |
+
f.write("- No specific FAS domains identified in the sequence\n")
|
| 1333 |
+
else:
|
| 1334 |
+
for i, domain in enumerate(fas_domains_found, 1):
|
| 1335 |
+
f.write(f"\n### {i}. {domain['name']} (Positions {domain['start']}-{domain['end']})\n")
|
| 1336 |
+
f.write(f"- Pfam ID: {domain['pfam_id']}\n")
|
| 1337 |
+
f.write(f"- Function: {domain['function']}\n")
|
| 1338 |
+
|
| 1339 |
+
research_log += "\n## Summary\n"
|
| 1340 |
+
research_log += f"- Detailed domain analysis saved to: {output_file}\n"
|
| 1341 |
+
if fas_domains_found:
|
| 1342 |
+
research_log += "- The sequence contains multiple domains characteristic of Fatty Acid Synthase\n"
|
| 1343 |
+
research_log += "- The identified domains suggest this sequence is involved in fatty acid biosynthesis\n"
|
| 1344 |
+
else:
|
| 1345 |
+
research_log += "- The sequence does not contain typical FAS domains\n"
|
| 1346 |
+
research_log += "- This may not be a Fatty Acid Synthase gene or may be a partial sequence\n"
|
| 1347 |
+
|
| 1348 |
+
except Exception as e:
|
| 1349 |
+
research_log += f"- Error in domain identification: {str(e)}\n"
|
| 1350 |
+
|
| 1351 |
+
return research_log
|
Biomni/biomni/tool/systems_biology.py
ADDED
|
@@ -0,0 +1,926 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
def query_chatnt(question, sequence, device=-1):
|
| 2 |
+
"""
|
| 3 |
+
Call ChatNT to answer a question about a DNA sequence.
|
| 4 |
+
|
| 5 |
+
Parameters:
|
| 6 |
+
-----------
|
| 7 |
+
question : str
|
| 8 |
+
Question to ask about the DNA sequence
|
| 9 |
+
sequence : str
|
| 10 |
+
DNA sequence to analyze
|
| 11 |
+
device : int, optional
|
| 12 |
+
Device to use for the ChatNT model. Default is -1 (CPU).
|
| 13 |
+
|
| 14 |
+
Returns:
|
| 15 |
+
--------
|
| 16 |
+
str
|
| 17 |
+
Answer to the question
|
| 18 |
+
"""
|
| 19 |
+
from transformers import pipeline
|
| 20 |
+
|
| 21 |
+
pipe = pipeline(model="InstaDeepAI/ChatNT", trust_remote_code=True, device=device)
|
| 22 |
+
|
| 23 |
+
# Define custom inputs (note that the number of <DNA> token in the english sequence must be equal to len(dna_sequences))
|
| 24 |
+
english_sequence = f"{question} <DNA> ?"
|
| 25 |
+
dna_sequences = [sequence]
|
| 26 |
+
|
| 27 |
+
# Generate sequence
|
| 28 |
+
generated_english_sequence = pipe(inputs={"english_sequence": english_sequence, "dna_sequences": dna_sequences})
|
| 29 |
+
|
| 30 |
+
return generated_english_sequence
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def perform_flux_balance_analysis(model_file, constraints=None, objective_reaction=None, output_file="fba_results.csv"):
|
| 34 |
+
"""
|
| 35 |
+
Perform Flux Balance Analysis (FBA) on a genome-scale metabolic network model.
|
| 36 |
+
|
| 37 |
+
FBA is a computational technique that predicts metabolic flux distributions
|
| 38 |
+
by formulating and solving a linear optimization problem.
|
| 39 |
+
|
| 40 |
+
Parameters:
|
| 41 |
+
-----------
|
| 42 |
+
model_file : str
|
| 43 |
+
Path to the metabolic model file (SBML or JSON format)
|
| 44 |
+
constraints : dict, optional
|
| 45 |
+
Dictionary of reaction constraints where keys are reaction IDs and
|
| 46 |
+
values are tuples of (lower_bound, upper_bound)
|
| 47 |
+
objective_reaction : str, optional
|
| 48 |
+
Reaction ID to use as the objective function (e.g., biomass reaction)
|
| 49 |
+
If None, uses the model's default objective
|
| 50 |
+
output_file : str, optional
|
| 51 |
+
File name to save the flux distribution results
|
| 52 |
+
|
| 53 |
+
Returns:
|
| 54 |
+
--------
|
| 55 |
+
str
|
| 56 |
+
Research log summarizing the FBA process and results
|
| 57 |
+
"""
|
| 58 |
+
|
| 59 |
+
import cobra
|
| 60 |
+
import pandas as pd
|
| 61 |
+
|
| 62 |
+
# Initialize research log
|
| 63 |
+
log = "# Flux Balance Analysis (FBA) Research Log\n\n"
|
| 64 |
+
|
| 65 |
+
# Step 1: Load the metabolic network model
|
| 66 |
+
log += "## Step 1: Loading metabolic model\n"
|
| 67 |
+
try:
|
| 68 |
+
if model_file.endswith(".xml") or model_file.endswith(".sbml"):
|
| 69 |
+
model = cobra.io.read_sbml_model(model_file)
|
| 70 |
+
elif model_file.endswith(".json"):
|
| 71 |
+
model = cobra.io.load_json_model(model_file)
|
| 72 |
+
else:
|
| 73 |
+
model = cobra.io.load_model(model_file)
|
| 74 |
+
log += f"- Successfully loaded model from {model_file}\n"
|
| 75 |
+
log += f"- Model contains {len(model.reactions)} reactions and {len(model.metabolites)} metabolites\n\n"
|
| 76 |
+
except Exception as e:
|
| 77 |
+
log += f"- Error loading model: {str(e)}\n"
|
| 78 |
+
return log
|
| 79 |
+
|
| 80 |
+
# Step 2: Set constraints
|
| 81 |
+
log += "## Step 2: Setting constraints\n"
|
| 82 |
+
if constraints:
|
| 83 |
+
log += "- Applied the following constraints:\n"
|
| 84 |
+
for reaction_id, (lb, ub) in constraints.items():
|
| 85 |
+
try:
|
| 86 |
+
reaction = model.reactions.get_by_id(reaction_id)
|
| 87 |
+
reaction.bounds = (lb, ub)
|
| 88 |
+
log += f" * {reaction_id}: lower_bound={lb}, upper_bound={ub}\n"
|
| 89 |
+
except Exception as e:
|
| 90 |
+
log += f" * Error setting constraint for {reaction_id}: {str(e)}\n"
|
| 91 |
+
else:
|
| 92 |
+
log += "- No additional constraints specified, using model defaults\n"
|
| 93 |
+
log += "\n"
|
| 94 |
+
|
| 95 |
+
# Step 3: Set objective function
|
| 96 |
+
log += "## Step 3: Setting objective function\n"
|
| 97 |
+
if objective_reaction:
|
| 98 |
+
try:
|
| 99 |
+
model.objective = objective_reaction
|
| 100 |
+
log += f"- Set objective function to maximize {objective_reaction}\n\n"
|
| 101 |
+
except Exception as e:
|
| 102 |
+
log += f"- Error setting objective function: {str(e)}\n"
|
| 103 |
+
log += "- Using model's default objective function\n\n"
|
| 104 |
+
else:
|
| 105 |
+
log += f"- Using model's default objective function: {model.objective.expression}\n\n"
|
| 106 |
+
|
| 107 |
+
# Step 4: Solve the FBA problem
|
| 108 |
+
log += "## Step 4: Solving FBA optimization problem\n"
|
| 109 |
+
try:
|
| 110 |
+
solution = model.optimize()
|
| 111 |
+
log += f"- Optimization status: {solution.status}\n"
|
| 112 |
+
log += f"- Objective value: {solution.objective_value:.6f}\n\n"
|
| 113 |
+
except Exception as e:
|
| 114 |
+
log += f"- Error during optimization: {str(e)}\n"
|
| 115 |
+
return log
|
| 116 |
+
|
| 117 |
+
# Step 5: Save and report results
|
| 118 |
+
log += "## Step 5: Analyzing flux distribution\n"
|
| 119 |
+
|
| 120 |
+
# Create a dataframe with the flux distribution
|
| 121 |
+
flux_distribution = pd.DataFrame(
|
| 122 |
+
{
|
| 123 |
+
"reaction_id": [r.id for r in model.reactions],
|
| 124 |
+
"reaction_name": [r.name for r in model.reactions],
|
| 125 |
+
"flux": [solution.fluxes[r.id] for r in model.reactions],
|
| 126 |
+
"lower_bound": [r.lower_bound for r in model.reactions],
|
| 127 |
+
"upper_bound": [r.upper_bound for r in model.reactions],
|
| 128 |
+
}
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
# Save to file
|
| 132 |
+
flux_distribution.to_csv(output_file, index=False)
|
| 133 |
+
log += f"- Flux distribution saved to {output_file}\n"
|
| 134 |
+
|
| 135 |
+
# Report active reactions (non-zero flux)
|
| 136 |
+
active_reactions = flux_distribution[abs(flux_distribution["flux"]) > 1e-6]
|
| 137 |
+
log += f"- Number of active reactions (flux > 1e-6): {len(active_reactions)}\n"
|
| 138 |
+
|
| 139 |
+
# Report top reactions by absolute flux
|
| 140 |
+
top_reactions = flux_distribution.iloc[abs(flux_distribution["flux"]).argsort()[::-1]].head(10)
|
| 141 |
+
log += "- Top 10 reactions by absolute flux magnitude:\n"
|
| 142 |
+
for _, row in top_reactions.iterrows():
|
| 143 |
+
log += f" * {row['reaction_id']} ({row['reaction_name']}): {row['flux']:.6f}\n"
|
| 144 |
+
|
| 145 |
+
log += f"\nFBA analysis complete. Full results available in {output_file}\n"
|
| 146 |
+
|
| 147 |
+
return log
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
def model_protein_dimerization_network(monomer_concentrations, dimerization_affinities, network_topology):
|
| 151 |
+
"""
|
| 152 |
+
Model protein dimerization networks to find equilibrium concentrations of dimers.
|
| 153 |
+
|
| 154 |
+
Parameters:
|
| 155 |
+
-----------
|
| 156 |
+
monomer_concentrations : dict
|
| 157 |
+
Dictionary mapping monomer names to their initial concentrations (in arbitrary units)
|
| 158 |
+
dimerization_affinities : dict
|
| 159 |
+
Dictionary mapping dimer names (as 'A-B' strings) to their association constants (Ka)
|
| 160 |
+
network_topology : list of tuples
|
| 161 |
+
List of (monomer1, monomer2) pairs that can form dimers
|
| 162 |
+
|
| 163 |
+
Returns:
|
| 164 |
+
--------
|
| 165 |
+
str
|
| 166 |
+
Research log summarizing the modeling process and results
|
| 167 |
+
"""
|
| 168 |
+
import time
|
| 169 |
+
|
| 170 |
+
import numpy as np
|
| 171 |
+
from scipy.integrate import solve_ivp
|
| 172 |
+
|
| 173 |
+
# Start timing
|
| 174 |
+
start_time = time.time()
|
| 175 |
+
|
| 176 |
+
# Extract unique monomers and create mapping to indices
|
| 177 |
+
all_monomers = list(monomer_concentrations.keys())
|
| 178 |
+
monomer_to_idx = {monomer: i for i, monomer in enumerate(all_monomers)}
|
| 179 |
+
|
| 180 |
+
# Create mapping from dimer indices to names
|
| 181 |
+
dimer_pairs = []
|
| 182 |
+
dimer_names = []
|
| 183 |
+
dimer_affinities = []
|
| 184 |
+
|
| 185 |
+
for m1, m2 in network_topology:
|
| 186 |
+
dimer_name = f"{m1}-{m2}"
|
| 187 |
+
if dimer_name in dimerization_affinities:
|
| 188 |
+
affinity = dimerization_affinities[dimer_name]
|
| 189 |
+
else:
|
| 190 |
+
# Try reverse order
|
| 191 |
+
dimer_name = f"{m2}-{m1}"
|
| 192 |
+
if dimer_name in dimerization_affinities:
|
| 193 |
+
affinity = dimerization_affinities[dimer_name]
|
| 194 |
+
else:
|
| 195 |
+
raise ValueError(f"No affinity found for dimer pair {m1}-{m2}")
|
| 196 |
+
|
| 197 |
+
dimer_pairs.append((monomer_to_idx[m1], monomer_to_idx[m2]))
|
| 198 |
+
dimer_names.append(dimer_name)
|
| 199 |
+
dimer_affinities.append(affinity)
|
| 200 |
+
|
| 201 |
+
# Initial conditions (monomer concentrations followed by dimer concentrations)
|
| 202 |
+
num_monomers = len(all_monomers)
|
| 203 |
+
num_dimers = len(dimer_pairs)
|
| 204 |
+
y0 = np.zeros(num_monomers + num_dimers)
|
| 205 |
+
|
| 206 |
+
for monomer, conc in monomer_concentrations.items():
|
| 207 |
+
y0[monomer_to_idx[monomer]] = conc
|
| 208 |
+
|
| 209 |
+
# Define ODE system
|
| 210 |
+
def dimerization_odes(t, y):
|
| 211 |
+
dydt = np.zeros_like(y)
|
| 212 |
+
|
| 213 |
+
# Extract current concentrations
|
| 214 |
+
monomer_concs = y[:num_monomers]
|
| 215 |
+
dimer_concs = y[num_monomers:]
|
| 216 |
+
|
| 217 |
+
# Calculate rate of change for each species
|
| 218 |
+
for i, ((m1_idx, m2_idx), affinity) in enumerate(zip(dimer_pairs, dimer_affinities, strict=False)):
|
| 219 |
+
# Formation rate: kon * [A] * [B]
|
| 220 |
+
# Dissociation rate: koff * [AB]
|
| 221 |
+
# At equilibrium: kon/koff = Ka (affinity)
|
| 222 |
+
# For simplicity, set kon = Ka and koff = 1
|
| 223 |
+
|
| 224 |
+
kon = affinity
|
| 225 |
+
koff = 1.0
|
| 226 |
+
|
| 227 |
+
# Dimer formation/dissociation
|
| 228 |
+
dimer_idx = num_monomers + i
|
| 229 |
+
|
| 230 |
+
# Rate of change
|
| 231 |
+
formation_rate = kon * monomer_concs[m1_idx] * monomer_concs[m2_idx]
|
| 232 |
+
dissociation_rate = koff * dimer_concs[i]
|
| 233 |
+
net_rate = formation_rate - dissociation_rate
|
| 234 |
+
|
| 235 |
+
# Update rates for monomers and dimers
|
| 236 |
+
dydt[m1_idx] -= net_rate
|
| 237 |
+
dydt[m2_idx] -= net_rate
|
| 238 |
+
dydt[dimer_idx] += net_rate
|
| 239 |
+
|
| 240 |
+
return dydt
|
| 241 |
+
|
| 242 |
+
# Solve ODE system to equilibrium
|
| 243 |
+
# Using a long enough time to reach equilibrium
|
| 244 |
+
t_span = (0, 1000) # Arbitrary long time to reach equilibrium
|
| 245 |
+
|
| 246 |
+
sol = solve_ivp(
|
| 247 |
+
dimerization_odes,
|
| 248 |
+
t_span,
|
| 249 |
+
y0,
|
| 250 |
+
method="BDF", # Good for stiff problems
|
| 251 |
+
rtol=1e-6,
|
| 252 |
+
atol=1e-9,
|
| 253 |
+
)
|
| 254 |
+
|
| 255 |
+
# Extract final concentrations (equilibrium)
|
| 256 |
+
final_monomer_concs = {monomer: sol.y[idx][-1] for monomer, idx in monomer_to_idx.items()}
|
| 257 |
+
final_dimer_concs = {dimer_name: sol.y[num_monomers + i][-1] for i, dimer_name in enumerate(dimer_names)}
|
| 258 |
+
|
| 259 |
+
# Calculate time taken
|
| 260 |
+
elapsed_time = time.time() - start_time
|
| 261 |
+
|
| 262 |
+
# Generate research log
|
| 263 |
+
log = "Protein Dimerization Network Modeling\n"
|
| 264 |
+
log += "=====================================\n\n"
|
| 265 |
+
log += "Network summary:\n"
|
| 266 |
+
log += f"- Number of monomers: {num_monomers}\n"
|
| 267 |
+
log += f"- Number of possible dimers: {num_dimers}\n\n"
|
| 268 |
+
|
| 269 |
+
log += "Initial conditions:\n"
|
| 270 |
+
for monomer, conc in monomer_concentrations.items():
|
| 271 |
+
log += f"- {monomer}: {conc:.4f}\n"
|
| 272 |
+
log += "\n"
|
| 273 |
+
|
| 274 |
+
log += "Dimerization affinities:\n"
|
| 275 |
+
for dimer, affinity in zip(dimer_names, dimer_affinities, strict=False):
|
| 276 |
+
log += f"- {dimer}: {affinity:.4e}\n"
|
| 277 |
+
log += "\n"
|
| 278 |
+
|
| 279 |
+
log += "Equilibrium concentrations:\n"
|
| 280 |
+
log += "Monomers:\n"
|
| 281 |
+
for monomer, conc in final_monomer_concs.items():
|
| 282 |
+
log += f"- {monomer}: {conc:.4f}\n"
|
| 283 |
+
|
| 284 |
+
log += "\nDimers:\n"
|
| 285 |
+
for dimer, conc in final_dimer_concs.items():
|
| 286 |
+
log += f"- {dimer}: {conc:.4f}\n"
|
| 287 |
+
log += "\n"
|
| 288 |
+
|
| 289 |
+
log += f"Simulation completed in {elapsed_time:.2f} seconds.\n"
|
| 290 |
+
|
| 291 |
+
return log
|
| 292 |
+
|
| 293 |
+
|
| 294 |
+
def simulate_metabolic_network_perturbation(
|
| 295 |
+
model_file, initial_concentrations, perturbation_params, simulation_time=100, time_points=1000
|
| 296 |
+
):
|
| 297 |
+
"""
|
| 298 |
+
Construct and simulate kinetic models of metabolic networks and analyze their responses to perturbations.
|
| 299 |
+
|
| 300 |
+
Parameters:
|
| 301 |
+
-----------
|
| 302 |
+
model_file : str
|
| 303 |
+
Path to the COBRA model file (SBML format)
|
| 304 |
+
initial_concentrations : dict
|
| 305 |
+
Dictionary mapping metabolite IDs to their initial concentrations
|
| 306 |
+
perturbation_params : dict
|
| 307 |
+
Dictionary with the following keys:
|
| 308 |
+
- 'time': float, time at which perturbation is applied
|
| 309 |
+
- 'metabolite': str, ID of the metabolite to perturb
|
| 310 |
+
- 'factor': float, multiplication factor for the metabolite concentration
|
| 311 |
+
simulation_time : float, optional
|
| 312 |
+
Total simulation time (default: 100)
|
| 313 |
+
time_points : int, optional
|
| 314 |
+
Number of time points to simulate (default: 1000)
|
| 315 |
+
|
| 316 |
+
Returns:
|
| 317 |
+
--------
|
| 318 |
+
str
|
| 319 |
+
Research log summarizing the steps taken and results obtained
|
| 320 |
+
"""
|
| 321 |
+
|
| 322 |
+
import cobra
|
| 323 |
+
import numpy as np
|
| 324 |
+
import pandas as pd
|
| 325 |
+
from scipy.integrate import solve_ivp
|
| 326 |
+
|
| 327 |
+
# Step 1: Load the metabolic model
|
| 328 |
+
try:
|
| 329 |
+
model = cobra.io.read_sbml_model(model_file)
|
| 330 |
+
log = f"Loaded metabolic model from {model_file} with {len(model.reactions)} reactions and {len(model.metabolites)} metabolites.\n\n"
|
| 331 |
+
except Exception as e:
|
| 332 |
+
return f"Error loading model: {str(e)}"
|
| 333 |
+
|
| 334 |
+
# Step 2: Set up metabolite list and initial concentrations
|
| 335 |
+
metabolites = list(model.metabolites)
|
| 336 |
+
metabolite_ids = [m.id for m in metabolites]
|
| 337 |
+
|
| 338 |
+
# Set default initial concentrations for metabolites not specified
|
| 339 |
+
conc_array = np.ones(len(metabolites))
|
| 340 |
+
for i, m_id in enumerate(metabolite_ids):
|
| 341 |
+
if m_id in initial_concentrations:
|
| 342 |
+
conc_array[i] = initial_concentrations[m_id]
|
| 343 |
+
|
| 344 |
+
log += "Initial concentrations set up for all metabolites.\n\n"
|
| 345 |
+
|
| 346 |
+
# Step 3: Define kinetic model using simple mass-action kinetics
|
| 347 |
+
def mass_action_kinetics(reaction, concentrations, metabolite_ids):
|
| 348 |
+
# Simple mass-action kinetics: product of substrate concentrations
|
| 349 |
+
rate = 1.0
|
| 350 |
+
for metabolite, coeff in reaction.metabolites.items():
|
| 351 |
+
if coeff < 0: # Substrate
|
| 352 |
+
idx = metabolite_ids.index(metabolite.id)
|
| 353 |
+
rate *= concentrations[idx] ** abs(coeff)
|
| 354 |
+
return rate
|
| 355 |
+
|
| 356 |
+
# Step 4: Define ODE system
|
| 357 |
+
def ode_system(t, concentrations, metabolite_ids, reactions, perturbation):
|
| 358 |
+
dC_dt = np.zeros(len(concentrations))
|
| 359 |
+
|
| 360 |
+
# Apply perturbation if we're at or past the perturbation time
|
| 361 |
+
if t >= perturbation["time"]:
|
| 362 |
+
perturb_idx = metabolite_ids.index(perturbation["metabolite"])
|
| 363 |
+
if not perturbation.get("applied", False):
|
| 364 |
+
concentrations[perturb_idx] *= perturbation["factor"]
|
| 365 |
+
perturbation["applied"] = True
|
| 366 |
+
|
| 367 |
+
# Calculate reaction rates
|
| 368 |
+
rates = {}
|
| 369 |
+
for reaction in reactions:
|
| 370 |
+
rates[reaction.id] = mass_action_kinetics(reaction, concentrations, metabolite_ids)
|
| 371 |
+
|
| 372 |
+
# Update concentration changes based on stoichiometry and rates
|
| 373 |
+
for i, metabolite_id in enumerate(metabolite_ids):
|
| 374 |
+
for reaction in reactions:
|
| 375 |
+
if metabolite_id in [m.id for m in reaction.metabolites]:
|
| 376 |
+
coeff = reaction.metabolites[model.metabolites.get_by_id(metabolite_id)]
|
| 377 |
+
dC_dt[i] += coeff * rates[reaction.id]
|
| 378 |
+
|
| 379 |
+
return dC_dt
|
| 380 |
+
|
| 381 |
+
log += "Kinetic model defined using mass-action kinetics for all reactions.\n\n"
|
| 382 |
+
|
| 383 |
+
# Step 5: Simulate the system
|
| 384 |
+
perturbation = perturbation_params.copy()
|
| 385 |
+
perturbation["applied"] = False
|
| 386 |
+
|
| 387 |
+
t_span = (0, simulation_time)
|
| 388 |
+
t_eval = np.linspace(0, simulation_time, time_points)
|
| 389 |
+
|
| 390 |
+
log += f"Starting simulation for {simulation_time} time units with perturbation of {perturbation['metabolite']} "
|
| 391 |
+
log += f"by factor {perturbation['factor']} at time {perturbation['time']}.\n\n"
|
| 392 |
+
|
| 393 |
+
try:
|
| 394 |
+
solution = solve_ivp(
|
| 395 |
+
lambda t, y: ode_system(t, y, metabolite_ids, model.reactions, perturbation),
|
| 396 |
+
t_span,
|
| 397 |
+
conc_array,
|
| 398 |
+
t_eval=t_eval,
|
| 399 |
+
method="LSODA",
|
| 400 |
+
)
|
| 401 |
+
|
| 402 |
+
log += f"Simulation completed successfully with {len(solution.t)} time points.\n\n"
|
| 403 |
+
except Exception as e:
|
| 404 |
+
return f"Error during simulation: {str(e)}"
|
| 405 |
+
|
| 406 |
+
# Step 6: Calculate fluxes at each time point
|
| 407 |
+
fluxes = np.zeros((len(solution.t), len(model.reactions)))
|
| 408 |
+
reaction_ids = [r.id for r in model.reactions]
|
| 409 |
+
|
| 410 |
+
for t_idx, t in enumerate(solution.t):
|
| 411 |
+
concentrations = solution.y[:, t_idx]
|
| 412 |
+
perturbation_temp = perturbation.copy()
|
| 413 |
+
if t >= perturbation["time"]:
|
| 414 |
+
perturbation_temp["applied"] = True
|
| 415 |
+
|
| 416 |
+
for r_idx, reaction in enumerate(model.reactions):
|
| 417 |
+
fluxes[t_idx, r_idx] = mass_action_kinetics(reaction, concentrations, metabolite_ids)
|
| 418 |
+
|
| 419 |
+
log += "Calculated reaction fluxes for all time points.\n\n"
|
| 420 |
+
|
| 421 |
+
# Step 7: Save results to files
|
| 422 |
+
conc_df = pd.DataFrame(solution.y.T, columns=metabolite_ids)
|
| 423 |
+
conc_df["time"] = solution.t
|
| 424 |
+
conc_file = "metabolite_concentrations.csv"
|
| 425 |
+
conc_df.to_csv(conc_file, index=False)
|
| 426 |
+
|
| 427 |
+
flux_df = pd.DataFrame(fluxes, columns=reaction_ids)
|
| 428 |
+
flux_df["time"] = solution.t
|
| 429 |
+
flux_file = "reaction_fluxes.csv"
|
| 430 |
+
flux_df.to_csv(flux_file, index=False)
|
| 431 |
+
|
| 432 |
+
log += f"Results saved to {conc_file} and {flux_file}.\n\n"
|
| 433 |
+
|
| 434 |
+
# Step 8: Analyze perturbation response
|
| 435 |
+
perturb_time_idx = np.where(solution.t >= perturbation["time"])[0][0]
|
| 436 |
+
pre_perturb = solution.y[:, perturb_time_idx - 1]
|
| 437 |
+
post_perturb = solution.y[:, perturb_time_idx]
|
| 438 |
+
|
| 439 |
+
# Find metabolites with significant changes
|
| 440 |
+
significant_changes = []
|
| 441 |
+
for i, m_id in enumerate(metabolite_ids):
|
| 442 |
+
rel_change = abs(post_perturb[i] - pre_perturb[i]) / pre_perturb[i] if pre_perturb[i] > 0 else 0
|
| 443 |
+
if rel_change > 0.05: # 5% change threshold
|
| 444 |
+
significant_changes.append((m_id, rel_change))
|
| 445 |
+
|
| 446 |
+
log += "Perturbation Analysis Results:\n"
|
| 447 |
+
log += f"Perturbation of {perturbation['metabolite']} at time {perturbation['time']} by factor {perturbation['factor']}.\n"
|
| 448 |
+
log += f"Number of metabolites with significant immediate response: {len(significant_changes)}.\n"
|
| 449 |
+
|
| 450 |
+
if significant_changes:
|
| 451 |
+
log += "Top 5 most affected metabolites (by relative concentration change):\n"
|
| 452 |
+
for m_id, change in sorted(significant_changes, key=lambda x: x[1], reverse=True)[:5]:
|
| 453 |
+
log += f" - {m_id}: {change * 100:.2f}% change\n"
|
| 454 |
+
|
| 455 |
+
log += "\nSimulation and perturbation analysis completed successfully."
|
| 456 |
+
|
| 457 |
+
return log
|
| 458 |
+
|
| 459 |
+
|
| 460 |
+
def simulate_protein_signaling_network(
|
| 461 |
+
network_structure, reaction_params, species_params, simulation_time=100, time_points=1000
|
| 462 |
+
):
|
| 463 |
+
"""
|
| 464 |
+
Simulate protein signaling network dynamics using ODE-based logic modeling with normalized Hill functions.
|
| 465 |
+
|
| 466 |
+
Parameters:
|
| 467 |
+
-----------
|
| 468 |
+
network_structure : dict
|
| 469 |
+
Dictionary defining the network topology. Each key is a target protein and its value is a list of tuples
|
| 470 |
+
(regulator, regulation_type) where regulation_type is 1 for activation and -1 for inhibition.
|
| 471 |
+
|
| 472 |
+
reaction_params : dict
|
| 473 |
+
Dictionary of reaction parameters. Keys are tuples (regulator, target) and values are dictionaries
|
| 474 |
+
with keys 'W' (weight), 'n' (Hill coefficient), and 'EC50' (half-maximal effective concentration).
|
| 475 |
+
|
| 476 |
+
species_params : dict
|
| 477 |
+
Dictionary of species parameters. Keys are protein names and values are dictionaries
|
| 478 |
+
with keys 'tau' (time constant), 'y0' (initial concentration), and 'ymax' (maximum concentration).
|
| 479 |
+
|
| 480 |
+
simulation_time : float, optional
|
| 481 |
+
Total simulation time in arbitrary time units. Default is 100.
|
| 482 |
+
|
| 483 |
+
time_points : int, optional
|
| 484 |
+
Number of time points for the simulation. Default is 1000.
|
| 485 |
+
|
| 486 |
+
Returns:
|
| 487 |
+
--------
|
| 488 |
+
str
|
| 489 |
+
Research log summarizing the simulation process and results.
|
| 490 |
+
"""
|
| 491 |
+
import csv
|
| 492 |
+
|
| 493 |
+
import numpy as np
|
| 494 |
+
from scipy.integrate import solve_ivp
|
| 495 |
+
|
| 496 |
+
# Extract all unique protein species
|
| 497 |
+
all_proteins = set(network_structure.keys())
|
| 498 |
+
for _target, regulators in network_structure.items():
|
| 499 |
+
for regulator, _ in regulators:
|
| 500 |
+
all_proteins.add(regulator)
|
| 501 |
+
|
| 502 |
+
# Create ordered list of proteins for indexing
|
| 503 |
+
proteins = sorted(all_proteins)
|
| 504 |
+
protein_indices = {protein: i for i, protein in enumerate(proteins)}
|
| 505 |
+
|
| 506 |
+
# Define the normalized Hill function
|
| 507 |
+
def hill_function(x, n, ec50):
|
| 508 |
+
return x**n / (x**n + ec50**n)
|
| 509 |
+
|
| 510 |
+
# Define the ODE system
|
| 511 |
+
def ode_system(t, y):
|
| 512 |
+
dydt = np.zeros_like(y)
|
| 513 |
+
|
| 514 |
+
for target, regulators in network_structure.items():
|
| 515 |
+
target_idx = protein_indices[target]
|
| 516 |
+
target_params = species_params[target]
|
| 517 |
+
|
| 518 |
+
# Calculate regulation term for each regulator
|
| 519 |
+
regulation_terms = []
|
| 520 |
+
for regulator, reg_type in regulators:
|
| 521 |
+
regulator_idx = protein_indices[regulator]
|
| 522 |
+
params = reaction_params.get((regulator, target), {})
|
| 523 |
+
|
| 524 |
+
if not params:
|
| 525 |
+
continue
|
| 526 |
+
|
| 527 |
+
x = y[regulator_idx]
|
| 528 |
+
n = params["n"]
|
| 529 |
+
ec50 = params["EC50"]
|
| 530 |
+
weight = params["W"]
|
| 531 |
+
|
| 532 |
+
# Apply Hill function based on regulation type
|
| 533 |
+
if reg_type == 1: # Activation
|
| 534 |
+
term = weight * hill_function(x, n, ec50)
|
| 535 |
+
else: # Inhibition
|
| 536 |
+
term = weight * (1 - hill_function(x, n, ec50))
|
| 537 |
+
|
| 538 |
+
regulation_terms.append(term)
|
| 539 |
+
|
| 540 |
+
# Combine regulation terms (if any)
|
| 541 |
+
if regulation_terms:
|
| 542 |
+
# Simple summation of regulation terms
|
| 543 |
+
regulation = sum(regulation_terms) / len(regulation_terms)
|
| 544 |
+
|
| 545 |
+
# Ensure regulation stays within [0, 1]
|
| 546 |
+
regulation = max(0, min(1, regulation))
|
| 547 |
+
|
| 548 |
+
# Calculate rate of change
|
| 549 |
+
tau = target_params["tau"]
|
| 550 |
+
ymax = target_params["ymax"]
|
| 551 |
+
dydt[target_idx] = (1 / tau) * (regulation * ymax - y[target_idx])
|
| 552 |
+
|
| 553 |
+
return dydt
|
| 554 |
+
|
| 555 |
+
# Set up initial conditions
|
| 556 |
+
y0 = np.zeros(len(proteins))
|
| 557 |
+
for protein, params in species_params.items():
|
| 558 |
+
if protein in protein_indices:
|
| 559 |
+
y0[protein_indices[protein]] = params["y0"]
|
| 560 |
+
|
| 561 |
+
# Set up time points
|
| 562 |
+
t_span = (0, simulation_time)
|
| 563 |
+
t_eval = np.linspace(0, simulation_time, time_points)
|
| 564 |
+
|
| 565 |
+
# Solve the ODE system
|
| 566 |
+
solution = solve_ivp(ode_system, t_span, y0, method="LSODA", t_eval=t_eval, rtol=1e-6, atol=1e-9)
|
| 567 |
+
|
| 568 |
+
# Save results to CSV
|
| 569 |
+
output_file = "protein_signaling_simulation_results.csv"
|
| 570 |
+
with open(output_file, "w", newline="") as f:
|
| 571 |
+
writer = csv.writer(f)
|
| 572 |
+
# Write header
|
| 573 |
+
header = ["Time"] + proteins
|
| 574 |
+
writer.writerow(header)
|
| 575 |
+
|
| 576 |
+
# Write data
|
| 577 |
+
for i in range(len(solution.t)):
|
| 578 |
+
row = [solution.t[i]] + list(solution.y[:, i])
|
| 579 |
+
writer.writerow(row)
|
| 580 |
+
|
| 581 |
+
# Generate research log
|
| 582 |
+
log = "ODE-based Logic Modeling of Protein Signaling Networks\n"
|
| 583 |
+
log += "=============================================\n\n"
|
| 584 |
+
log += f"Network structure: {len(network_structure)} target proteins, {len(proteins)} total proteins\n"
|
| 585 |
+
log += f"Simulation time: {simulation_time} time units\n"
|
| 586 |
+
log += f"Number of time points: {time_points}\n\n"
|
| 587 |
+
|
| 588 |
+
log += "Simulation steps:\n"
|
| 589 |
+
log += "1. Initialized protein concentrations based on provided initial values\n"
|
| 590 |
+
log += "2. Set up ODE system using normalized Hill functions for protein interactions\n"
|
| 591 |
+
log += "3. Solved the ODE system using LSODA integration method\n"
|
| 592 |
+
log += "4. Saved time-series data for all protein concentrations\n\n"
|
| 593 |
+
|
| 594 |
+
log += f"Results saved to: {output_file}\n\n"
|
| 595 |
+
|
| 596 |
+
# Add summary statistics
|
| 597 |
+
log += "Summary of final protein concentrations:\n"
|
| 598 |
+
for i, protein in enumerate(proteins):
|
| 599 |
+
final_conc = solution.y[i, -1]
|
| 600 |
+
log += f"- {protein}: {final_conc:.4f}\n"
|
| 601 |
+
|
| 602 |
+
return log
|
| 603 |
+
|
| 604 |
+
|
| 605 |
+
def compare_protein_structures(pdb_file1, pdb_file2, chain_id1="A", chain_id2="A", output_prefix="protein_comparison"):
|
| 606 |
+
"""
|
| 607 |
+
Compares two protein structures to identify structural differences and conformational changes.
|
| 608 |
+
|
| 609 |
+
Parameters:
|
| 610 |
+
-----------
|
| 611 |
+
pdb_file1 : str
|
| 612 |
+
Path to the first PDB file
|
| 613 |
+
pdb_file2 : str
|
| 614 |
+
Path to the second PDB file
|
| 615 |
+
chain_id1 : str, optional
|
| 616 |
+
Chain ID to analyze in the first structure (default: 'A')
|
| 617 |
+
chain_id2 : str, optional
|
| 618 |
+
Chain ID to analyze in the second structure (default: 'A')
|
| 619 |
+
output_prefix : str, optional
|
| 620 |
+
Prefix for output files (default: "protein_comparison")
|
| 621 |
+
|
| 622 |
+
Returns:
|
| 623 |
+
--------
|
| 624 |
+
str
|
| 625 |
+
A research log summarizing the structural comparison analysis
|
| 626 |
+
"""
|
| 627 |
+
import warnings
|
| 628 |
+
|
| 629 |
+
import numpy as np
|
| 630 |
+
from Bio.PDB import PDBIO, PDBParser, Select, Superimposer
|
| 631 |
+
from Bio.PDB.PDBExceptions import PDBConstructionWarning
|
| 632 |
+
|
| 633 |
+
# Suppress PDB parsing warnings
|
| 634 |
+
warnings.filterwarnings("ignore", category=PDBConstructionWarning)
|
| 635 |
+
|
| 636 |
+
research_log = []
|
| 637 |
+
research_log.append("# Protein Structure Comparison Analysis\n")
|
| 638 |
+
research_log.append(f"Comparing structures from {pdb_file1} and {pdb_file2}\n")
|
| 639 |
+
|
| 640 |
+
# Initialize parser
|
| 641 |
+
parser = PDBParser()
|
| 642 |
+
|
| 643 |
+
# Parse structures
|
| 644 |
+
research_log.append("## Loading PDB structures")
|
| 645 |
+
structure1 = parser.get_structure("structure1", pdb_file1)
|
| 646 |
+
structure2 = parser.get_structure("structure2", pdb_file2)
|
| 647 |
+
|
| 648 |
+
# Get specified chains
|
| 649 |
+
try:
|
| 650 |
+
chain1 = structure1[0][chain_id1]
|
| 651 |
+
chain2 = structure2[0][chain_id2]
|
| 652 |
+
research_log.append(f"Successfully loaded chain {chain_id1} from {pdb_file1}")
|
| 653 |
+
research_log.append(f"Successfully loaded chain {chain_id2} from {pdb_file2}")
|
| 654 |
+
except KeyError as e:
|
| 655 |
+
return f"Error: Chain not found: {str(e)}"
|
| 656 |
+
|
| 657 |
+
# Get CA atoms for alignment
|
| 658 |
+
ca_atoms1 = []
|
| 659 |
+
ca_atoms2 = []
|
| 660 |
+
|
| 661 |
+
# Create residue mappings based on residue ID
|
| 662 |
+
residues1 = {residue.id[1]: residue for residue in chain1 if residue.has_id("CA")}
|
| 663 |
+
residues2 = {residue.id[1]: residue for residue in chain2 if residue.has_id("CA")}
|
| 664 |
+
|
| 665 |
+
# Find common residues
|
| 666 |
+
common_residues = sorted(set(residues1.keys()) & set(residues2.keys()))
|
| 667 |
+
|
| 668 |
+
if len(common_residues) == 0:
|
| 669 |
+
return "Error: No common residues with CA atoms found between the structures"
|
| 670 |
+
|
| 671 |
+
research_log.append(f"Found {len(common_residues)} common residues for alignment\n")
|
| 672 |
+
|
| 673 |
+
# Get paired atoms for alignment
|
| 674 |
+
for res_id in common_residues:
|
| 675 |
+
ca_atoms1.append(residues1[res_id]["CA"])
|
| 676 |
+
ca_atoms2.append(residues2[res_id]["CA"])
|
| 677 |
+
|
| 678 |
+
# Perform structural alignment
|
| 679 |
+
research_log.append("## Structural Alignment")
|
| 680 |
+
super_imposer = Superimposer()
|
| 681 |
+
super_imposer.set_atoms(ca_atoms1, ca_atoms2)
|
| 682 |
+
super_imposer.apply(structure2.get_atoms())
|
| 683 |
+
|
| 684 |
+
# Calculate RMSD
|
| 685 |
+
rmsd = super_imposer.rms
|
| 686 |
+
research_log.append(f"Overall RMSD: {rmsd:.4f} Å\n")
|
| 687 |
+
|
| 688 |
+
# Calculate per-residue distance after alignment
|
| 689 |
+
research_log.append("## Conformational Differences Analysis")
|
| 690 |
+
|
| 691 |
+
# Find regions with significant differences
|
| 692 |
+
distance_data = []
|
| 693 |
+
significant_changes = []
|
| 694 |
+
threshold = 2.0 # Angstroms threshold for significant difference
|
| 695 |
+
|
| 696 |
+
for res_id in common_residues:
|
| 697 |
+
res1 = residues1[res_id]
|
| 698 |
+
res2 = residues2[res_id]
|
| 699 |
+
|
| 700 |
+
# Calculate distance between CA atoms
|
| 701 |
+
ca1 = res1["CA"]
|
| 702 |
+
ca2 = res2["CA"]
|
| 703 |
+
distance = np.linalg.norm(ca1.coord - ca2.coord)
|
| 704 |
+
|
| 705 |
+
distance_data.append((res_id, distance))
|
| 706 |
+
|
| 707 |
+
if distance > threshold:
|
| 708 |
+
significant_changes.append((res_id, res1.get_resname(), distance))
|
| 709 |
+
|
| 710 |
+
# Save alignment as PDB files
|
| 711 |
+
aligned_file1 = f"{output_prefix}_ref.pdb"
|
| 712 |
+
aligned_file2 = f"{output_prefix}_aligned.pdb"
|
| 713 |
+
|
| 714 |
+
io = PDBIO()
|
| 715 |
+
io.set_structure(structure1)
|
| 716 |
+
io.save(aligned_file1, select=Select())
|
| 717 |
+
|
| 718 |
+
io.set_structure(structure2)
|
| 719 |
+
io.save(aligned_file2, select=Select())
|
| 720 |
+
|
| 721 |
+
research_log.append(f"Aligned structures saved as {aligned_file1} and {aligned_file2}")
|
| 722 |
+
|
| 723 |
+
# Report on significant differences
|
| 724 |
+
if significant_changes:
|
| 725 |
+
research_log.append(
|
| 726 |
+
f"\nIdentified {len(significant_changes)} residues with significant conformational changes:"
|
| 727 |
+
)
|
| 728 |
+
for res_id, res_name, distance in significant_changes:
|
| 729 |
+
research_log.append(f" - Residue {res_name}{res_id}: {distance:.2f} Å displacement")
|
| 730 |
+
else:
|
| 731 |
+
research_log.append("\nNo significant conformational changes detected (threshold: 2.0 Å)")
|
| 732 |
+
|
| 733 |
+
# Save per-residue distance data
|
| 734 |
+
distance_file = f"{output_prefix}_residue_distances.csv"
|
| 735 |
+
with open(distance_file, "w") as f:
|
| 736 |
+
f.write("Residue_ID,Distance(Å)\n")
|
| 737 |
+
for res_id, distance in distance_data:
|
| 738 |
+
f.write(f"{res_id},{distance:.4f}\n")
|
| 739 |
+
|
| 740 |
+
research_log.append(f"\nPer-residue distance data saved to {distance_file}")
|
| 741 |
+
|
| 742 |
+
# Identify regions with continuous changes
|
| 743 |
+
regions = []
|
| 744 |
+
current_region = []
|
| 745 |
+
|
| 746 |
+
for res_id, _, distance in significant_changes:
|
| 747 |
+
if not current_region or res_id == current_region[-1][0] + 1:
|
| 748 |
+
current_region.append((res_id, distance))
|
| 749 |
+
else:
|
| 750 |
+
if len(current_region) >= 3: # Consider regions with at least 3 consecutive residues
|
| 751 |
+
regions.append(current_region)
|
| 752 |
+
current_region = [(res_id, distance)]
|
| 753 |
+
|
| 754 |
+
if current_region and len(current_region) >= 3:
|
| 755 |
+
regions.append(current_region)
|
| 756 |
+
|
| 757 |
+
if regions:
|
| 758 |
+
research_log.append("\n## Continuous Regions with Conformational Changes")
|
| 759 |
+
for i, region in enumerate(regions, 1):
|
| 760 |
+
start_res = region[0][0]
|
| 761 |
+
end_res = region[-1][0]
|
| 762 |
+
avg_dist = sum(r[1] for r in region) / len(region)
|
| 763 |
+
research_log.append(f"Region {i}: Residues {start_res}-{end_res} (Average displacement: {avg_dist:.2f} Å)")
|
| 764 |
+
|
| 765 |
+
research_log.append("\n## Summary")
|
| 766 |
+
research_log.append(f"- Compared structures from {pdb_file1} and {pdb_file2}")
|
| 767 |
+
research_log.append(f"- Overall RMSD: {rmsd:.4f} Å")
|
| 768 |
+
research_log.append(f"- {len(significant_changes)} residues with significant conformational changes")
|
| 769 |
+
research_log.append(f"- {len(regions)} continuous regions of conformational change")
|
| 770 |
+
research_log.append(f"- Files generated: {aligned_file1}, {aligned_file2}, {distance_file}")
|
| 771 |
+
|
| 772 |
+
return "\n".join(research_log)
|
| 773 |
+
|
| 774 |
+
|
| 775 |
+
def simulate_renin_angiotensin_system_dynamics(
|
| 776 |
+
initial_concentrations, rate_constants, feedback_params, simulation_time=48, time_points=100
|
| 777 |
+
):
|
| 778 |
+
"""
|
| 779 |
+
Simulate the time-dependent concentrations of renin-angiotensin system (RAS) components.
|
| 780 |
+
|
| 781 |
+
Parameters:
|
| 782 |
+
-----------
|
| 783 |
+
initial_concentrations : dict
|
| 784 |
+
Initial concentrations of RAS components with keys:
|
| 785 |
+
'renin', 'angiotensinogen', 'angiotensin_I', 'angiotensin_II',
|
| 786 |
+
'ACE2_angiotensin_II', 'angiotensin_1_7'
|
| 787 |
+
|
| 788 |
+
rate_constants : dict
|
| 789 |
+
Kinetic rate constants with keys:
|
| 790 |
+
'k_ren' (renin production), 'k_agt' (angiotensinogen production),
|
| 791 |
+
'k_ace' (ACE conversion rate), 'k_ace2' (ACE2 conversion rate),
|
| 792 |
+
'k_at1r' (AT1R binding rate), 'k_mas' (Mas receptor binding rate)
|
| 793 |
+
|
| 794 |
+
feedback_params : dict
|
| 795 |
+
Parameters controlling feedback mechanisms with keys:
|
| 796 |
+
'fb_ang_II' (angiotensin II feedback), 'fb_ace2' (ACE2 feedback)
|
| 797 |
+
|
| 798 |
+
simulation_time : float, optional
|
| 799 |
+
Total simulation time in hours (default: 48)
|
| 800 |
+
|
| 801 |
+
time_points : int, optional
|
| 802 |
+
Number of time points to evaluate (default: 100)
|
| 803 |
+
|
| 804 |
+
Returns:
|
| 805 |
+
--------
|
| 806 |
+
str
|
| 807 |
+
Research log summarizing the simulation steps and results
|
| 808 |
+
"""
|
| 809 |
+
|
| 810 |
+
import numpy as np
|
| 811 |
+
import pandas as pd
|
| 812 |
+
from scipy.integrate import solve_ivp
|
| 813 |
+
|
| 814 |
+
# Extract initial concentrations
|
| 815 |
+
y0 = [
|
| 816 |
+
initial_concentrations["renin"],
|
| 817 |
+
initial_concentrations["angiotensinogen"],
|
| 818 |
+
initial_concentrations["angiotensin_I"],
|
| 819 |
+
initial_concentrations["angiotensin_II"],
|
| 820 |
+
initial_concentrations["ACE2_angiotensin_II"],
|
| 821 |
+
initial_concentrations["angiotensin_1_7"],
|
| 822 |
+
]
|
| 823 |
+
|
| 824 |
+
# Define the system of ODEs
|
| 825 |
+
def ras_ode_system(t, y):
|
| 826 |
+
renin, agt, ang_I, ang_II, ace2_ang_II, ang_1_7 = y
|
| 827 |
+
|
| 828 |
+
# Production rates with feedback
|
| 829 |
+
renin_production = rate_constants["k_ren"] * (1 / (1 + feedback_params["fb_ang_II"] * ang_II))
|
| 830 |
+
agt_production = rate_constants["k_agt"]
|
| 831 |
+
|
| 832 |
+
# Conversion rates
|
| 833 |
+
ang_I_formation = renin * agt
|
| 834 |
+
ang_II_formation = rate_constants["k_ace"] * ang_I
|
| 835 |
+
ace2_binding = rate_constants["k_ace2"] * ang_II
|
| 836 |
+
ang_1_7_formation = ace2_ang_II
|
| 837 |
+
|
| 838 |
+
# Clearance/degradation (simplified as proportional to concentration)
|
| 839 |
+
renin_clearance = 0.1 * renin
|
| 840 |
+
agt_clearance = 0.05 * agt
|
| 841 |
+
ang_I_clearance = 0.2 * ang_I
|
| 842 |
+
ang_II_clearance = 0.3 * ang_II + rate_constants["k_at1r"] * ang_II
|
| 843 |
+
ace2_ang_II_clearance = 0.15 * ace2_ang_II
|
| 844 |
+
ang_1_7_clearance = 0.25 * ang_1_7 + rate_constants["k_mas"] * ang_1_7
|
| 845 |
+
|
| 846 |
+
# ODEs
|
| 847 |
+
drenin_dt = renin_production - renin_clearance
|
| 848 |
+
dagt_dt = agt_production - agt_clearance - ang_I_formation
|
| 849 |
+
dang_I_dt = ang_I_formation - ang_I_clearance - ang_II_formation
|
| 850 |
+
dang_II_dt = ang_II_formation - ang_II_clearance - ace2_binding
|
| 851 |
+
dace2_ang_II_dt = ace2_binding - ace2_ang_II_clearance - ang_1_7_formation
|
| 852 |
+
dang_1_7_dt = ang_1_7_formation - ang_1_7_clearance
|
| 853 |
+
|
| 854 |
+
return [drenin_dt, dagt_dt, dang_I_dt, dang_II_dt, dace2_ang_II_dt, dang_1_7_dt]
|
| 855 |
+
|
| 856 |
+
# Time points for simulation
|
| 857 |
+
t_span = (0, simulation_time)
|
| 858 |
+
t_eval = np.linspace(0, simulation_time, time_points)
|
| 859 |
+
|
| 860 |
+
# Solve the ODE system
|
| 861 |
+
solution = solve_ivp(ras_ode_system, t_span, y0, method="RK45", t_eval=t_eval, rtol=1e-6)
|
| 862 |
+
|
| 863 |
+
# Create DataFrame with results
|
| 864 |
+
component_names = [
|
| 865 |
+
"Renin",
|
| 866 |
+
"Angiotensinogen",
|
| 867 |
+
"Angiotensin I",
|
| 868 |
+
"Angiotensin II",
|
| 869 |
+
"ACE2-Angiotensin II",
|
| 870 |
+
"Angiotensin 1-7",
|
| 871 |
+
]
|
| 872 |
+
results_df = pd.DataFrame(solution.y.T, columns=component_names)
|
| 873 |
+
results_df.insert(0, "Time (hours)", solution.t)
|
| 874 |
+
|
| 875 |
+
# Save results to CSV
|
| 876 |
+
results_file = "ras_simulation_results.csv"
|
| 877 |
+
results_df.to_csv(results_file, index=False)
|
| 878 |
+
|
| 879 |
+
# Create research log
|
| 880 |
+
log = f"""RAS ODE Modeling Simulation Log:
|
| 881 |
+
|
| 882 |
+
1. Initialized RAS component concentrations:
|
| 883 |
+
- Renin: {initial_concentrations["renin"]}
|
| 884 |
+
- Angiotensinogen: {initial_concentrations["angiotensinogen"]}
|
| 885 |
+
- Angiotensin I: {initial_concentrations["angiotensin_I"]}
|
| 886 |
+
- Angiotensin II: {initial_concentrations["angiotensin_II"]}
|
| 887 |
+
- ACE2-Angiotensin II complex: {initial_concentrations["ACE2_angiotensin_II"]}
|
| 888 |
+
- Angiotensin 1-7: {initial_concentrations["angiotensin_1_7"]}
|
| 889 |
+
|
| 890 |
+
2. Applied rate constants:
|
| 891 |
+
- Renin production (k_ren): {rate_constants["k_ren"]}
|
| 892 |
+
- Angiotensinogen production (k_agt): {rate_constants["k_agt"]}
|
| 893 |
+
- ACE conversion rate (k_ace): {rate_constants["k_ace"]}
|
| 894 |
+
- ACE2 conversion rate (k_ace2): {rate_constants["k_ace2"]}
|
| 895 |
+
- AT1R binding rate (k_at1r): {rate_constants["k_at1r"]}
|
| 896 |
+
- Mas receptor binding rate (k_mas): {rate_constants["k_mas"]}
|
| 897 |
+
|
| 898 |
+
3. Feedback parameters:
|
| 899 |
+
- Angiotensin II feedback (fb_ang_II): {feedback_params["fb_ang_II"]}
|
| 900 |
+
- ACE2 feedback (fb_ace2): {feedback_params["fb_ace2"]}
|
| 901 |
+
|
| 902 |
+
4. Simulation parameters:
|
| 903 |
+
- Total simulation time: {simulation_time} hours
|
| 904 |
+
- Number of time points: {time_points}
|
| 905 |
+
|
| 906 |
+
5. Solved system of ODEs using SciPy's solve_ivp with RK45 method.
|
| 907 |
+
|
| 908 |
+
6. Final concentrations at {simulation_time} hours:
|
| 909 |
+
- Renin: {results_df["Renin"].iloc[-1]:.4f}
|
| 910 |
+
- Angiotensinogen: {results_df["Angiotensinogen"].iloc[-1]:.4f}
|
| 911 |
+
- Angiotensin I: {results_df["Angiotensin I"].iloc[-1]:.4f}
|
| 912 |
+
- Angiotensin II: {results_df["Angiotensin II"].iloc[-1]:.4f}
|
| 913 |
+
- ACE2-Angiotensin II complex: {results_df["ACE2-Angiotensin II"].iloc[-1]:.4f}
|
| 914 |
+
- Angiotensin 1-7: {results_df["Angiotensin 1-7"].iloc[-1]:.4f}
|
| 915 |
+
|
| 916 |
+
7. Results saved to file: {results_file}
|
| 917 |
+
|
| 918 |
+
8. Key observations:
|
| 919 |
+
- The model captures the conversion of Angiotensinogen to Angiotensin I via Renin
|
| 920 |
+
- Angiotensin I is converted to Angiotensin II via ACE
|
| 921 |
+
- Angiotensin II binds with ACE2 to form the ACE2-Angiotensin II complex
|
| 922 |
+
- The complex is converted to Angiotensin 1-7
|
| 923 |
+
- Feedback mechanisms regulate the production rates
|
| 924 |
+
"""
|
| 925 |
+
|
| 926 |
+
return log
|
Biomni/biomni/tool/tool_registry.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pickle
|
| 2 |
+
|
| 3 |
+
import pandas as pd
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class ToolRegistry:
|
| 7 |
+
def __init__(self, tools):
|
| 8 |
+
self.tools = []
|
| 9 |
+
self.next_id = 0
|
| 10 |
+
|
| 11 |
+
for j in tools.values():
|
| 12 |
+
for tool in j:
|
| 13 |
+
self.register_tool(tool)
|
| 14 |
+
|
| 15 |
+
docs = []
|
| 16 |
+
for tool_id in range(len(self.tools)):
|
| 17 |
+
docs.append([int(tool_id), self.get_tool_by_id(int(tool_id))])
|
| 18 |
+
self.document_df = pd.DataFrame(docs, columns=["docid", "document_content"])
|
| 19 |
+
|
| 20 |
+
# self.langchain_tools = {}
|
| 21 |
+
# for module, api_list in tools.items():
|
| 22 |
+
# self.langchain_tools.update({self.get_id_by_name(api['name']): api_schema_to_langchain_tool(api, mode = 'custom_tool', module_name = module) for api in api_list})
|
| 23 |
+
|
| 24 |
+
def register_tool(self, tool):
|
| 25 |
+
if self.validate_tool(tool):
|
| 26 |
+
tool["id"] = self.next_id
|
| 27 |
+
self.tools.append(tool)
|
| 28 |
+
self.next_id += 1
|
| 29 |
+
else:
|
| 30 |
+
raise ValueError("Invalid tool format")
|
| 31 |
+
|
| 32 |
+
def validate_tool(self, tool):
|
| 33 |
+
required_keys = ["name", "description", "required_parameters"]
|
| 34 |
+
return all(key in tool for key in required_keys)
|
| 35 |
+
|
| 36 |
+
def get_tool_by_name(self, name):
|
| 37 |
+
for tool in self.tools:
|
| 38 |
+
if tool["name"] == name:
|
| 39 |
+
return tool
|
| 40 |
+
return None
|
| 41 |
+
|
| 42 |
+
def get_tool_by_id(self, tool_id):
|
| 43 |
+
for tool in self.tools:
|
| 44 |
+
if tool["id"] == tool_id:
|
| 45 |
+
return tool
|
| 46 |
+
return None
|
| 47 |
+
|
| 48 |
+
def get_id_by_name(self, name):
|
| 49 |
+
for tool in self.tools:
|
| 50 |
+
if tool["name"] == name:
|
| 51 |
+
return tool["id"]
|
| 52 |
+
return None
|
| 53 |
+
|
| 54 |
+
def get_name_by_id(self, tool_id):
|
| 55 |
+
for tool in self.tools:
|
| 56 |
+
if tool["id"] == tool_id:
|
| 57 |
+
return tool["name"]
|
| 58 |
+
return None
|
| 59 |
+
|
| 60 |
+
def list_tools(self):
|
| 61 |
+
return [{"name": tool["name"], "id": tool["id"]} for tool in self.tools]
|
| 62 |
+
|
| 63 |
+
def remove_tool_by_id(self, tool_id):
|
| 64 |
+
# Remove the tool with the given id
|
| 65 |
+
tool = self.get_tool_by_id(tool_id)
|
| 66 |
+
if tool:
|
| 67 |
+
self.tools = [t for t in self.tools if t["id"] != tool_id]
|
| 68 |
+
return True
|
| 69 |
+
return False
|
| 70 |
+
|
| 71 |
+
def remove_tool_by_name(self, name):
|
| 72 |
+
# Remove the tool with the given name
|
| 73 |
+
tool = self.get_tool_by_name(name)
|
| 74 |
+
if tool:
|
| 75 |
+
self.tools = [t for t in self.tools if t["name"] != name]
|
| 76 |
+
return True
|
| 77 |
+
return False
|
| 78 |
+
|
| 79 |
+
def save_registry(self, filename):
|
| 80 |
+
with open(filename, "wb") as file:
|
| 81 |
+
pickle.dump(self, file)
|
| 82 |
+
|
| 83 |
+
# def get_langchain_tool_by_id(self, id):
|
| 84 |
+
# return self.langchain_tools[id]
|
| 85 |
+
|
| 86 |
+
@staticmethod
|
| 87 |
+
def load_registry(filename):
|
| 88 |
+
with open(filename, "rb") as file:
|
| 89 |
+
return pickle.load(file)
|
Biomni/biomni/utils.py
ADDED
|
@@ -0,0 +1,2469 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import ast
|
| 2 |
+
import enum
|
| 3 |
+
import importlib
|
| 4 |
+
import json
|
| 5 |
+
import os
|
| 6 |
+
import pickle
|
| 7 |
+
import subprocess
|
| 8 |
+
import tempfile
|
| 9 |
+
import traceback
|
| 10 |
+
import zipfile
|
| 11 |
+
from typing import Any, ClassVar
|
| 12 |
+
from urllib.parse import urljoin
|
| 13 |
+
|
| 14 |
+
import pandas as pd
|
| 15 |
+
import requests
|
| 16 |
+
import tqdm # Add tqdm for progress bar
|
| 17 |
+
from langchain_core.callbacks import BaseCallbackHandler
|
| 18 |
+
from langchain_core.messages.base import get_msg_title_repr
|
| 19 |
+
from langchain_core.tools import StructuredTool
|
| 20 |
+
from langchain_core.utils.interactive_env import is_interactive_env
|
| 21 |
+
from pydantic import BaseModel, Field, ValidationError
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
# Add these new functions for running R code and CLI commands
|
| 25 |
+
def run_r_code(code: str) -> str:
|
| 26 |
+
"""Run R code using subprocess.
|
| 27 |
+
|
| 28 |
+
Args:
|
| 29 |
+
code: R code to run
|
| 30 |
+
|
| 31 |
+
Returns:
|
| 32 |
+
Output of the R code
|
| 33 |
+
|
| 34 |
+
"""
|
| 35 |
+
try:
|
| 36 |
+
# Create a temporary file to store the R code
|
| 37 |
+
with tempfile.NamedTemporaryFile(suffix=".R", mode="w", delete=False) as f:
|
| 38 |
+
f.write(code)
|
| 39 |
+
temp_file = f.name
|
| 40 |
+
|
| 41 |
+
# Run the R code using Rscript
|
| 42 |
+
result = subprocess.run(["Rscript", temp_file], capture_output=True, text=True, check=False)
|
| 43 |
+
|
| 44 |
+
# Clean up the temporary file
|
| 45 |
+
os.unlink(temp_file)
|
| 46 |
+
|
| 47 |
+
# Return the output
|
| 48 |
+
if result.returncode != 0:
|
| 49 |
+
return f"Error running R code:\n{result.stderr}"
|
| 50 |
+
else:
|
| 51 |
+
return result.stdout
|
| 52 |
+
except Exception as e:
|
| 53 |
+
return f"Error running R code: {str(e)}"
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def run_bash_script(script: str) -> str:
|
| 57 |
+
"""Run a Bash script using subprocess.
|
| 58 |
+
|
| 59 |
+
Args:
|
| 60 |
+
script: Bash script to run
|
| 61 |
+
|
| 62 |
+
Returns:
|
| 63 |
+
Output of the Bash script
|
| 64 |
+
|
| 65 |
+
Example:
|
| 66 |
+
This is how to use the function
|
| 67 |
+
|
| 68 |
+
.. code-block:: python
|
| 69 |
+
|
| 70 |
+
# Example of a complex Bash script
|
| 71 |
+
script = '''
|
| 72 |
+
#!/bin/bash
|
| 73 |
+
|
| 74 |
+
# Define variables
|
| 75 |
+
DATA_DIR="/path/to/data"
|
| 76 |
+
OUTPUT_FILE="results.txt"
|
| 77 |
+
|
| 78 |
+
# Create output directory if it doesn't exist
|
| 79 |
+
mkdir -p $(dirname $OUTPUT_FILE)
|
| 80 |
+
|
| 81 |
+
# Loop through files
|
| 82 |
+
for file in $DATA_DIR/*.txt; do
|
| 83 |
+
echo "Processing $file..."
|
| 84 |
+
# Count lines in each file
|
| 85 |
+
line_count=$(wc -l < $file)
|
| 86 |
+
echo "$file: $line_count lines" >> $OUTPUT_FILE
|
| 87 |
+
done
|
| 88 |
+
|
| 89 |
+
echo "Processing complete. Results saved to $OUTPUT_FILE"
|
| 90 |
+
'''
|
| 91 |
+
result = run_bash_script(script)
|
| 92 |
+
print(result)
|
| 93 |
+
|
| 94 |
+
"""
|
| 95 |
+
try:
|
| 96 |
+
# Trim any leading/trailing whitespace
|
| 97 |
+
script = script.strip()
|
| 98 |
+
|
| 99 |
+
# If the script is empty, return an error
|
| 100 |
+
if not script:
|
| 101 |
+
return "Error: Empty script"
|
| 102 |
+
|
| 103 |
+
# Create a temporary file to store the Bash script
|
| 104 |
+
with tempfile.NamedTemporaryFile(suffix=".sh", mode="w", delete=False) as f:
|
| 105 |
+
# Add shebang if not present
|
| 106 |
+
if not script.startswith("#!/"):
|
| 107 |
+
f.write("#!/bin/bash\n")
|
| 108 |
+
# Add set -e to exit on error
|
| 109 |
+
if "set -e" not in script:
|
| 110 |
+
f.write("set -e\n")
|
| 111 |
+
f.write(script)
|
| 112 |
+
temp_file = f.name
|
| 113 |
+
|
| 114 |
+
# Make the script executable
|
| 115 |
+
os.chmod(temp_file, 0o755)
|
| 116 |
+
|
| 117 |
+
# Get current environment variables and working directory
|
| 118 |
+
env = os.environ.copy()
|
| 119 |
+
cwd = os.getcwd()
|
| 120 |
+
|
| 121 |
+
# Run the Bash script with the current environment and working directory
|
| 122 |
+
result = subprocess.run(
|
| 123 |
+
[temp_file],
|
| 124 |
+
shell=True,
|
| 125 |
+
capture_output=True,
|
| 126 |
+
text=True,
|
| 127 |
+
check=False,
|
| 128 |
+
env=env,
|
| 129 |
+
cwd=cwd,
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
# Clean up the temporary file
|
| 133 |
+
os.unlink(temp_file)
|
| 134 |
+
|
| 135 |
+
# Return the output
|
| 136 |
+
if result.returncode != 0:
|
| 137 |
+
traceback.print_stack()
|
| 138 |
+
print(result)
|
| 139 |
+
return f"Error running Bash script (exit code {result.returncode}):\n{result.stderr}"
|
| 140 |
+
else:
|
| 141 |
+
return result.stdout
|
| 142 |
+
except Exception as e:
|
| 143 |
+
traceback.print_exc()
|
| 144 |
+
return f"Error running Bash script: {str(e)}"
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
# Keep the run_cli_command for backward compatibility
|
| 148 |
+
def run_cli_command(command: str) -> str:
|
| 149 |
+
"""Run a CLI command using subprocess.
|
| 150 |
+
|
| 151 |
+
Args:
|
| 152 |
+
command: CLI command to run
|
| 153 |
+
|
| 154 |
+
Returns:
|
| 155 |
+
Output of the CLI command
|
| 156 |
+
|
| 157 |
+
"""
|
| 158 |
+
try:
|
| 159 |
+
# Trim any leading/trailing whitespace
|
| 160 |
+
command = command.strip()
|
| 161 |
+
|
| 162 |
+
# If the command is empty, return an error
|
| 163 |
+
if not command:
|
| 164 |
+
return "Error: Empty command"
|
| 165 |
+
|
| 166 |
+
# Split the command into a list of arguments, handling quoted arguments correctly
|
| 167 |
+
import shlex
|
| 168 |
+
|
| 169 |
+
args = shlex.split(command)
|
| 170 |
+
|
| 171 |
+
# Run the command
|
| 172 |
+
result = subprocess.run(args, capture_output=True, text=True, check=False)
|
| 173 |
+
|
| 174 |
+
# Return the output
|
| 175 |
+
if result.returncode != 0:
|
| 176 |
+
return f"Error running command '{command}':\n{result.stderr}"
|
| 177 |
+
else:
|
| 178 |
+
return result.stdout
|
| 179 |
+
except Exception as e:
|
| 180 |
+
return f"Error running command '{command}': {str(e)}"
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
def run_with_timeout(func, args=None, kwargs=None, timeout=600):
|
| 184 |
+
"""Run a function with a timeout using threading instead of multiprocessing.
|
| 185 |
+
This allows variables to persist in the global namespace between function calls.
|
| 186 |
+
Returns the function result or a timeout error message.
|
| 187 |
+
"""
|
| 188 |
+
if args is None:
|
| 189 |
+
args = []
|
| 190 |
+
if kwargs is None:
|
| 191 |
+
kwargs = {}
|
| 192 |
+
|
| 193 |
+
import ctypes
|
| 194 |
+
import queue
|
| 195 |
+
import threading
|
| 196 |
+
|
| 197 |
+
result_queue = queue.Queue()
|
| 198 |
+
|
| 199 |
+
def thread_func(func, args, kwargs, result_queue):
|
| 200 |
+
"""Function to run in a separate thread."""
|
| 201 |
+
try:
|
| 202 |
+
result = func(*args, **kwargs)
|
| 203 |
+
result_queue.put(("success", result))
|
| 204 |
+
except Exception as e:
|
| 205 |
+
result_queue.put(("error", str(e)))
|
| 206 |
+
|
| 207 |
+
# Start a separate thread
|
| 208 |
+
thread = threading.Thread(target=thread_func, args=(func, args, kwargs, result_queue))
|
| 209 |
+
thread.daemon = True # Set as daemon so it will be killed when main thread exits
|
| 210 |
+
thread.start()
|
| 211 |
+
|
| 212 |
+
# Wait for the specified timeout
|
| 213 |
+
thread.join(timeout)
|
| 214 |
+
|
| 215 |
+
# Check if the thread is still running after timeout
|
| 216 |
+
if thread.is_alive():
|
| 217 |
+
print(f"TIMEOUT: Code execution timed out after {timeout} seconds")
|
| 218 |
+
|
| 219 |
+
# Unfortunately, there's no clean way to force terminate a thread in Python
|
| 220 |
+
# The recommended approach is to use daemon threads and let them be killed when main thread exits
|
| 221 |
+
# Here, we'll try to raise an exception in the thread to make it stop
|
| 222 |
+
try:
|
| 223 |
+
# Get thread ID and try to terminate it
|
| 224 |
+
thread_id = thread.ident
|
| 225 |
+
if thread_id:
|
| 226 |
+
# This is a bit dangerous and not 100% reliable
|
| 227 |
+
# It attempts to raise a SystemExit exception in the thread
|
| 228 |
+
res = ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(thread_id), ctypes.py_object(SystemExit))
|
| 229 |
+
if res > 1:
|
| 230 |
+
# Oops, we raised too many exceptions
|
| 231 |
+
ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(thread_id), None)
|
| 232 |
+
except Exception as e:
|
| 233 |
+
print(f"Error trying to terminate thread: {e}")
|
| 234 |
+
|
| 235 |
+
return f"ERROR: Code execution timed out after {timeout} seconds. Please try with simpler inputs or break your task into smaller steps."
|
| 236 |
+
|
| 237 |
+
# Get the result from the queue if available
|
| 238 |
+
try:
|
| 239 |
+
status, result = result_queue.get(block=False)
|
| 240 |
+
return result if status == "success" else f"Error in execution: {result}"
|
| 241 |
+
except queue.Empty:
|
| 242 |
+
return "Error: Execution completed but no result was returned"
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
class api_schema(BaseModel):
|
| 246 |
+
"""api schema specification."""
|
| 247 |
+
|
| 248 |
+
api_schema: str | None = Field(description="The api schema as a dictionary")
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
def function_to_api_schema(function_string, llm):
|
| 252 |
+
prompt = """
|
| 253 |
+
Based on a code snippet and help me write an API docstring in the format like this:
|
| 254 |
+
|
| 255 |
+
{{'name': 'get_gene_set_enrichment',
|
| 256 |
+
'description': 'Given a list of genes, identify a pathway that is enriched for this gene set. Return a list of pathway name, p-value, z-scores.',
|
| 257 |
+
'required_parameters': [{{'name': 'genes',
|
| 258 |
+
'type': 'List[str]',
|
| 259 |
+
'description': 'List of g`ene symbols to analyze',
|
| 260 |
+
'default': None}}],
|
| 261 |
+
'optional_parameters': [{{'name': 'top_k',
|
| 262 |
+
'type': 'int',
|
| 263 |
+
'description': 'Top K pathways to return',
|
| 264 |
+
'default': 10}}, {{'name': 'database',
|
| 265 |
+
'type': 'str',
|
| 266 |
+
'description': 'Name of the database to use for enrichment analysis',
|
| 267 |
+
'default': "gene_ontology"}}]}}
|
| 268 |
+
|
| 269 |
+
Strictly follow the input from the function - don't create fake optional parameters.
|
| 270 |
+
For variable without default values, set them as None, not null.
|
| 271 |
+
For variable with boolean values, use capitalized True or False, not true or false.
|
| 272 |
+
Do not add any return type in the docstring.
|
| 273 |
+
Be as clear and succint as possible for the descriptions. Please do not make it overly verbose.
|
| 274 |
+
Here is the code snippet:
|
| 275 |
+
{code}
|
| 276 |
+
"""
|
| 277 |
+
llm = llm.with_structured_output(api_schema)
|
| 278 |
+
|
| 279 |
+
for _ in range(7):
|
| 280 |
+
try:
|
| 281 |
+
api = llm.invoke(prompt.format(code=function_string)).dict()["api_schema"]
|
| 282 |
+
return ast.literal_eval(api) # -> prefer "default": None
|
| 283 |
+
# return json.loads(api) # -> prefer "default": null
|
| 284 |
+
except Exception as e:
|
| 285 |
+
print("API string:", api)
|
| 286 |
+
print("Error parsing the API string:", e)
|
| 287 |
+
continue
|
| 288 |
+
|
| 289 |
+
return "Error: Could not parse the API schema"
|
| 290 |
+
# return
|
| 291 |
+
|
| 292 |
+
|
| 293 |
+
def get_all_functions_from_file(file_path):
|
| 294 |
+
with open(file_path) as file:
|
| 295 |
+
file_content = file.read()
|
| 296 |
+
|
| 297 |
+
# Parse the file content into an AST (Abstract Syntax Tree)
|
| 298 |
+
tree = ast.parse(file_content)
|
| 299 |
+
|
| 300 |
+
# List to hold the top-level functions as strings
|
| 301 |
+
functions = []
|
| 302 |
+
|
| 303 |
+
# Walk through the AST nodes
|
| 304 |
+
for node in tree.body: # Only consider top-level nodes in the body
|
| 305 |
+
if isinstance(node, ast.FunctionDef): # Check if the node is a function definition
|
| 306 |
+
# Skip if function name starts with underscore
|
| 307 |
+
if node.name.startswith("_"):
|
| 308 |
+
continue
|
| 309 |
+
|
| 310 |
+
start_line = node.lineno - 1 # Get the starting line of the function
|
| 311 |
+
end_line = node.end_lineno # Get the ending line of the function (only available in Python 3.8+)
|
| 312 |
+
func_code = file_content.splitlines()[start_line:end_line]
|
| 313 |
+
functions.append("\n".join(func_code)) # Join lines of the function and add to the list
|
| 314 |
+
|
| 315 |
+
return functions
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
def write_python_code(request: str):
|
| 319 |
+
from langchain_anthropic import ChatAnthropic
|
| 320 |
+
from langchain_core.output_parsers import StrOutputParser
|
| 321 |
+
from langchain_core.prompts import ChatPromptTemplate
|
| 322 |
+
|
| 323 |
+
model = ChatAnthropic(model="claude-3-5-sonnet-20240620")
|
| 324 |
+
template = """Write some python code to solve the user's problem.
|
| 325 |
+
|
| 326 |
+
Return only python code in Markdown format, e.g.:
|
| 327 |
+
|
| 328 |
+
```python
|
| 329 |
+
....
|
| 330 |
+
```"""
|
| 331 |
+
prompt = ChatPromptTemplate.from_messages([("system", template), ("human", "{input}")])
|
| 332 |
+
|
| 333 |
+
def _sanitize_output(text: str):
|
| 334 |
+
_, after = text.split("```python")
|
| 335 |
+
return after.split("```")[0]
|
| 336 |
+
|
| 337 |
+
chain = prompt | model | StrOutputParser() | _sanitize_output
|
| 338 |
+
return chain.invoke({"input": "write a code that " + request})
|
| 339 |
+
|
| 340 |
+
|
| 341 |
+
def execute_graphql_query(
|
| 342 |
+
query: str,
|
| 343 |
+
variables: dict,
|
| 344 |
+
api_address: str = "https://api.genetics.opentargets.org/graphql",
|
| 345 |
+
) -> dict:
|
| 346 |
+
"""Executes a GraphQL query with variables and returns the data as a dictionary."""
|
| 347 |
+
headers = {"Content-Type": "application/json"}
|
| 348 |
+
response = requests.post(api_address, json={"query": query, "variables": variables}, headers=headers)
|
| 349 |
+
if response.status_code == 200:
|
| 350 |
+
return response.json()
|
| 351 |
+
else:
|
| 352 |
+
print(response.text)
|
| 353 |
+
response.raise_for_status()
|
| 354 |
+
|
| 355 |
+
|
| 356 |
+
def get_tool_decorated_functions(relative_path):
|
| 357 |
+
import ast
|
| 358 |
+
import importlib.util
|
| 359 |
+
import os
|
| 360 |
+
|
| 361 |
+
# Get the directory of the current file (__init__.py)
|
| 362 |
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
| 363 |
+
|
| 364 |
+
# Construct the absolute path from the relative path
|
| 365 |
+
file_path = os.path.join(current_dir, relative_path)
|
| 366 |
+
|
| 367 |
+
with open(file_path) as file:
|
| 368 |
+
tree = ast.parse(file.read(), filename=file_path)
|
| 369 |
+
|
| 370 |
+
tool_function_names = []
|
| 371 |
+
|
| 372 |
+
for node in ast.walk(tree):
|
| 373 |
+
if isinstance(node, ast.FunctionDef):
|
| 374 |
+
for decorator in node.decorator_list:
|
| 375 |
+
if (
|
| 376 |
+
isinstance(decorator, ast.Name)
|
| 377 |
+
and decorator.id == "tool"
|
| 378 |
+
or (
|
| 379 |
+
isinstance(decorator, ast.Call)
|
| 380 |
+
and isinstance(decorator.func, ast.Name)
|
| 381 |
+
and decorator.func.id == "tool"
|
| 382 |
+
)
|
| 383 |
+
):
|
| 384 |
+
tool_function_names.append(node.name)
|
| 385 |
+
|
| 386 |
+
# Calculate the module name from the relative path
|
| 387 |
+
package_path = os.path.relpath(file_path, start=current_dir)
|
| 388 |
+
module_name = package_path.replace(os.path.sep, ".").rsplit(".", 1)[0]
|
| 389 |
+
|
| 390 |
+
# Import the module and get the function objects
|
| 391 |
+
spec = importlib.util.spec_from_file_location(module_name, file_path)
|
| 392 |
+
module = importlib.util.module_from_spec(spec)
|
| 393 |
+
spec.loader.exec_module(module)
|
| 394 |
+
|
| 395 |
+
tool_functions = [getattr(module, name) for name in tool_function_names]
|
| 396 |
+
|
| 397 |
+
return tool_functions
|
| 398 |
+
|
| 399 |
+
|
| 400 |
+
def process_bio_retrieval_ducoment(documents_df):
|
| 401 |
+
ir_corpus = {}
|
| 402 |
+
corpus2tool = {}
|
| 403 |
+
for row in documents_df.itertuples():
|
| 404 |
+
doc = row.document_content
|
| 405 |
+
ir_corpus[row.docid] = (
|
| 406 |
+
(doc.get("name", "") or "")
|
| 407 |
+
+ ", "
|
| 408 |
+
+ (doc.get("description", "") or "")
|
| 409 |
+
+ ", "
|
| 410 |
+
+ (doc.get("url", "") or "")
|
| 411 |
+
+ ", "
|
| 412 |
+
+ ", required_params: "
|
| 413 |
+
+ json.dumps(doc.get("required_parameters", ""))
|
| 414 |
+
+ ", optional_params: "
|
| 415 |
+
+ json.dumps(doc.get("optional_parameters", ""))
|
| 416 |
+
)
|
| 417 |
+
|
| 418 |
+
corpus2tool[
|
| 419 |
+
(doc.get("name", "") or "")
|
| 420 |
+
+ ", "
|
| 421 |
+
+ (doc.get("description", "") or "")
|
| 422 |
+
+ ", "
|
| 423 |
+
+ (doc.get("url", "") or "")
|
| 424 |
+
+ ", "
|
| 425 |
+
+ ", required_params: "
|
| 426 |
+
+ json.dumps(doc.get("required_parameters", ""))
|
| 427 |
+
+ ", optional_params: "
|
| 428 |
+
+ json.dumps(doc.get("optional_parameters", ""))
|
| 429 |
+
] = doc["name"]
|
| 430 |
+
return ir_corpus, corpus2tool
|
| 431 |
+
|
| 432 |
+
|
| 433 |
+
def load_pickle(file):
|
| 434 |
+
import pickle
|
| 435 |
+
|
| 436 |
+
with open(file, "rb") as f:
|
| 437 |
+
return pickle.load(f)
|
| 438 |
+
|
| 439 |
+
|
| 440 |
+
def pretty_print(message, printout=True):
|
| 441 |
+
if isinstance(message, tuple):
|
| 442 |
+
title = message
|
| 443 |
+
elif isinstance(message.content, list):
|
| 444 |
+
title = get_msg_title_repr(message.type.title().upper() + " Message", bold=is_interactive_env())
|
| 445 |
+
if message.name is not None:
|
| 446 |
+
title += f"\nName: {message.name}"
|
| 447 |
+
|
| 448 |
+
for i in message.content:
|
| 449 |
+
if i["type"] == "text":
|
| 450 |
+
title += f"\n{i['text']}\n"
|
| 451 |
+
elif i["type"] == "tool_use":
|
| 452 |
+
title += f"\nTool: {i['name']}"
|
| 453 |
+
title += f"\nInput: {i['input']}"
|
| 454 |
+
if printout:
|
| 455 |
+
print(f"{title}")
|
| 456 |
+
else:
|
| 457 |
+
title = get_msg_title_repr(message.type.title() + " Message", bold=is_interactive_env())
|
| 458 |
+
if message.name is not None:
|
| 459 |
+
title += f"\nName: {message.name}"
|
| 460 |
+
title += f"\n\n{message.content}"
|
| 461 |
+
if printout:
|
| 462 |
+
print(f"{title}")
|
| 463 |
+
return title
|
| 464 |
+
|
| 465 |
+
|
| 466 |
+
class CustomBaseModel(BaseModel):
|
| 467 |
+
api_schema: ClassVar[dict] = None # Class variable to store api_schema
|
| 468 |
+
|
| 469 |
+
# Add model_config with arbitrary_types_allowed=True
|
| 470 |
+
model_config = {"arbitrary_types_allowed": True}
|
| 471 |
+
|
| 472 |
+
@classmethod
|
| 473 |
+
def set_api_schema(cls, schema: dict):
|
| 474 |
+
cls.api_schema = schema
|
| 475 |
+
|
| 476 |
+
@classmethod
|
| 477 |
+
def model_validate(cls, obj):
|
| 478 |
+
try:
|
| 479 |
+
return super().model_validate(obj)
|
| 480 |
+
except (ValidationError, AttributeError) as e:
|
| 481 |
+
if not cls.api_schema:
|
| 482 |
+
raise e # If no api_schema is set, raise original error
|
| 483 |
+
|
| 484 |
+
error_msg = "Required Parameters:\n"
|
| 485 |
+
for param in cls.api_schema["required_parameters"]:
|
| 486 |
+
error_msg += f"- {param['name']} ({param['type']}): {param['description']}\n"
|
| 487 |
+
|
| 488 |
+
error_msg += "\nErrors:\n"
|
| 489 |
+
for err in e.errors():
|
| 490 |
+
field = err["loc"][0] if err["loc"] else "input"
|
| 491 |
+
error_msg += f"- {field}: {err['msg']}\n"
|
| 492 |
+
|
| 493 |
+
if not obj:
|
| 494 |
+
error_msg += "\nNo input provided"
|
| 495 |
+
else:
|
| 496 |
+
error_msg += "\nProvided Input:\n"
|
| 497 |
+
for key, value in obj.items():
|
| 498 |
+
error_msg += f"- {key}: {value}\n"
|
| 499 |
+
|
| 500 |
+
missing_params = {param["name"] for param in cls.api_schema["required_parameters"]} - set(obj.keys())
|
| 501 |
+
if missing_params:
|
| 502 |
+
error_msg += "\nMissing Parameters:\n"
|
| 503 |
+
for param in missing_params:
|
| 504 |
+
error_msg += f"- {param}\n"
|
| 505 |
+
|
| 506 |
+
# # Create proper validation error structure
|
| 507 |
+
raise ValidationError.from_exception_data(
|
| 508 |
+
title="Validation Error",
|
| 509 |
+
line_errors=[
|
| 510 |
+
{
|
| 511 |
+
"type": "value_error",
|
| 512 |
+
"loc": ("input",),
|
| 513 |
+
"input": obj,
|
| 514 |
+
"ctx": {
|
| 515 |
+
"error": error_msg,
|
| 516 |
+
},
|
| 517 |
+
}
|
| 518 |
+
],
|
| 519 |
+
) from None
|
| 520 |
+
|
| 521 |
+
|
| 522 |
+
def safe_execute_decorator(func):
|
| 523 |
+
def wrapper(*args, **kwargs):
|
| 524 |
+
try:
|
| 525 |
+
return func(*args, **kwargs)
|
| 526 |
+
except Exception as e:
|
| 527 |
+
return str(e)
|
| 528 |
+
|
| 529 |
+
return wrapper
|
| 530 |
+
|
| 531 |
+
|
| 532 |
+
def api_schema_to_langchain_tool(api_schema, mode="generated_tool", module_name=None):
|
| 533 |
+
if mode == "generated_tool":
|
| 534 |
+
module = importlib.import_module("biomni.tool.generated_tool." + api_schema["tool_name"] + ".api")
|
| 535 |
+
elif mode == "custom_tool":
|
| 536 |
+
module = importlib.import_module(module_name)
|
| 537 |
+
|
| 538 |
+
api_function = getattr(module, api_schema["name"])
|
| 539 |
+
api_function = safe_execute_decorator(api_function)
|
| 540 |
+
|
| 541 |
+
# Define a mapping from string type names to actual Python type objects
|
| 542 |
+
type_mapping = {
|
| 543 |
+
"string": str,
|
| 544 |
+
"integer": int,
|
| 545 |
+
"boolean": bool,
|
| 546 |
+
"pandas": pd.DataFrame, # Use the imported pandas.DataFrame directly
|
| 547 |
+
"str": str,
|
| 548 |
+
"int": int,
|
| 549 |
+
"bool": bool,
|
| 550 |
+
"List[str]": list[str],
|
| 551 |
+
"List[int]": list[int],
|
| 552 |
+
"Dict": dict,
|
| 553 |
+
"Any": Any,
|
| 554 |
+
}
|
| 555 |
+
|
| 556 |
+
# Create the fields and annotations
|
| 557 |
+
annotations = {}
|
| 558 |
+
for param in api_schema["required_parameters"]:
|
| 559 |
+
param_type = param["type"]
|
| 560 |
+
if param_type in type_mapping:
|
| 561 |
+
annotations[param["name"]] = type_mapping[param_type]
|
| 562 |
+
else:
|
| 563 |
+
# For types not in the mapping, try a safer approach than direct eval
|
| 564 |
+
try:
|
| 565 |
+
annotations[param["name"]] = eval(param_type)
|
| 566 |
+
except (NameError, SyntaxError):
|
| 567 |
+
# Default to Any for unknown types
|
| 568 |
+
annotations[param["name"]] = Any
|
| 569 |
+
|
| 570 |
+
fields = {param["name"]: Field(description=param["description"]) for param in api_schema["required_parameters"]}
|
| 571 |
+
|
| 572 |
+
# Create the ApiInput class dynamically
|
| 573 |
+
ApiInput = type("Input", (CustomBaseModel,), {"__annotations__": annotations, **fields})
|
| 574 |
+
# Set the api_schema
|
| 575 |
+
ApiInput.set_api_schema(api_schema)
|
| 576 |
+
|
| 577 |
+
# Create the StructuredTool
|
| 578 |
+
api_tool = StructuredTool.from_function(
|
| 579 |
+
func=api_function,
|
| 580 |
+
name=api_schema["name"],
|
| 581 |
+
description=api_schema["description"],
|
| 582 |
+
args_schema=ApiInput,
|
| 583 |
+
return_direct=True,
|
| 584 |
+
)
|
| 585 |
+
|
| 586 |
+
return api_tool
|
| 587 |
+
|
| 588 |
+
|
| 589 |
+
class ID(enum.Enum):
|
| 590 |
+
ENTREZ = "Entrez"
|
| 591 |
+
ENSEMBL = "Ensembl without version" # e.g. ENSG00000123374
|
| 592 |
+
ENSEMBL_W_VERSION = "Ensembl with version" # e.g. ENSG00000123374.10 (needed for GTEx)
|
| 593 |
+
|
| 594 |
+
|
| 595 |
+
def get_gene_id(gene_symbol: str, id_type: ID):
|
| 596 |
+
"""Get the ID for a gene symbol. If no match found, returns None."""
|
| 597 |
+
if id_type == ID.ENTREZ:
|
| 598 |
+
return _get_gene_id_entrez(gene_symbol)
|
| 599 |
+
elif id_type == ID.ENSEMBL:
|
| 600 |
+
return _get_gene_id_ensembl(gene_symbol)
|
| 601 |
+
elif id_type == ID.ENSEMBL_W_VERSION:
|
| 602 |
+
return _get_gene_id_ensembl_with_version(gene_symbol)
|
| 603 |
+
else:
|
| 604 |
+
raise ValueError(f"ID type {id_type} not supported")
|
| 605 |
+
|
| 606 |
+
|
| 607 |
+
def _get_gene_id_entrez(gene_symbol: str):
|
| 608 |
+
"""Get the Entrez ID for a gene symbol. If no match found, returns None
|
| 609 |
+
e.g. 1017 (CDK2).
|
| 610 |
+
"""
|
| 611 |
+
api_call = f"https://mygene.info/v3/query?species=human&q=symbol:{gene_symbol}"
|
| 612 |
+
response = requests.get(api_call)
|
| 613 |
+
response_json = response.json()
|
| 614 |
+
|
| 615 |
+
if len(response_json["hits"]) == 0:
|
| 616 |
+
return None
|
| 617 |
+
else:
|
| 618 |
+
return response_json["hits"][0]["entrezgene"]
|
| 619 |
+
|
| 620 |
+
|
| 621 |
+
def _get_gene_id_ensembl(gene_symbol):
|
| 622 |
+
"""Get the Ensembl ID for a gene symbol. If no match found, returns None
|
| 623 |
+
e.g. ENSG00000123374.
|
| 624 |
+
"""
|
| 625 |
+
api_call = f"https://mygene.info/v3/query?species=human&fields=ensembl&q=symbol:{gene_symbol}"
|
| 626 |
+
response = requests.get(api_call)
|
| 627 |
+
response_json = response.json()
|
| 628 |
+
|
| 629 |
+
if len(response_json["hits"]) == 0:
|
| 630 |
+
return None
|
| 631 |
+
else:
|
| 632 |
+
ensembl = response_json["hits"][0]["ensembl"]
|
| 633 |
+
if isinstance(ensembl, list):
|
| 634 |
+
return ensembl[0][
|
| 635 |
+
"gene"
|
| 636 |
+
] # Sometimes returns a list, for example RNH1 (first elem is on chr11, second is on scaffold_hschr11)
|
| 637 |
+
else:
|
| 638 |
+
return ensembl["gene"]
|
| 639 |
+
|
| 640 |
+
|
| 641 |
+
def _get_gene_id_ensembl_with_version(gene_symbol):
|
| 642 |
+
"""Get the Ensembl ID for a gene symbol. If no match found, returns None
|
| 643 |
+
e.g. ENSG00000123374.10.
|
| 644 |
+
"""
|
| 645 |
+
api_base = "https://gtexportal.org/api/v2/reference/gene"
|
| 646 |
+
params = {"geneId": gene_symbol}
|
| 647 |
+
response_json = requests.get(api_base, params=params).json()
|
| 648 |
+
|
| 649 |
+
if len(response_json["data"]) == 0:
|
| 650 |
+
return None
|
| 651 |
+
else:
|
| 652 |
+
return response_json["data"][0]["gencodeId"]
|
| 653 |
+
|
| 654 |
+
|
| 655 |
+
def save_pkl(f, filename):
|
| 656 |
+
with open(filename, "wb") as file:
|
| 657 |
+
pickle.dump(f, file)
|
| 658 |
+
|
| 659 |
+
|
| 660 |
+
def load_pkl(filename):
|
| 661 |
+
with open(filename, "rb") as file:
|
| 662 |
+
return pickle.load(file)
|
| 663 |
+
|
| 664 |
+
|
| 665 |
+
_TEXT_COLOR_MAPPING = {
|
| 666 |
+
"blue": "36;1",
|
| 667 |
+
"yellow": "33;1",
|
| 668 |
+
"pink": "38;5;200",
|
| 669 |
+
"green": "32;1",
|
| 670 |
+
"red": "31;1",
|
| 671 |
+
}
|
| 672 |
+
|
| 673 |
+
|
| 674 |
+
def color_print(text, color="blue"):
|
| 675 |
+
color_str = _TEXT_COLOR_MAPPING[color]
|
| 676 |
+
print(f"\u001b[{color_str}m\033[1;3m{text}\u001b[0m")
|
| 677 |
+
|
| 678 |
+
|
| 679 |
+
class PromptLogger(BaseCallbackHandler):
|
| 680 |
+
def on_chat_model_start(self, serialized, messages, **kwargs):
|
| 681 |
+
for message in messages[0]:
|
| 682 |
+
color_print(message.pretty_repr(), color="green")
|
| 683 |
+
|
| 684 |
+
|
| 685 |
+
class NodeLogger(BaseCallbackHandler):
|
| 686 |
+
def on_llm_end(self, response, **kwargs): # response of type LLMResult
|
| 687 |
+
for generations in response.generations: # response.generations of type List[List[Generations]] becuase "each input could have multiple candidate generations"
|
| 688 |
+
for generation in generations:
|
| 689 |
+
generated_text = generation.message.content
|
| 690 |
+
# token_usage = generation.message.response_metadata["token_usage"]
|
| 691 |
+
color_print(generated_text, color="yellow")
|
| 692 |
+
|
| 693 |
+
def on_agent_action(self, action, **kwargs):
|
| 694 |
+
color_print(action.log, color="pink")
|
| 695 |
+
|
| 696 |
+
def on_agent_finish(self, finish, **kwargs):
|
| 697 |
+
color_print(finish, color="red")
|
| 698 |
+
|
| 699 |
+
def on_tool_start(self, serialized, input_str, **kwargs):
|
| 700 |
+
tool_name = serialized.get("name")
|
| 701 |
+
color_print(f"Calling {tool_name} with inputs: {input_str}", color="pink")
|
| 702 |
+
|
| 703 |
+
def on_tool_end(self, output, **kwargs):
|
| 704 |
+
output = str(output)
|
| 705 |
+
color_print(output, color="blue")
|
| 706 |
+
|
| 707 |
+
|
| 708 |
+
class TokenUsageTracker(BaseCallbackHandler):
|
| 709 |
+
"""Collect provider-reported token usage across LangChain LLM calls."""
|
| 710 |
+
|
| 711 |
+
def __init__(self):
|
| 712 |
+
self.reset()
|
| 713 |
+
|
| 714 |
+
def reset(self):
|
| 715 |
+
self.prompt_tokens = 0
|
| 716 |
+
self.completion_tokens = 0
|
| 717 |
+
self.total_tokens = 0
|
| 718 |
+
self.call_count = 0
|
| 719 |
+
|
| 720 |
+
def snapshot(self):
|
| 721 |
+
return {
|
| 722 |
+
"prompt_tokens": int(self.prompt_tokens),
|
| 723 |
+
"completion_tokens": int(self.completion_tokens),
|
| 724 |
+
"total_tokens": int(self.total_tokens),
|
| 725 |
+
"llm_call_count": int(self.call_count),
|
| 726 |
+
}
|
| 727 |
+
|
| 728 |
+
def on_llm_end(self, response, **kwargs):
|
| 729 |
+
usage = self._extract_usage(response)
|
| 730 |
+
if not usage:
|
| 731 |
+
return
|
| 732 |
+
self.prompt_tokens += usage.get("prompt_tokens", 0)
|
| 733 |
+
self.completion_tokens += usage.get("completion_tokens", 0)
|
| 734 |
+
self.total_tokens += usage.get("total_tokens", 0)
|
| 735 |
+
self.call_count += 1
|
| 736 |
+
|
| 737 |
+
def _extract_usage(self, response):
|
| 738 |
+
llm_output = getattr(response, "llm_output", None)
|
| 739 |
+
if isinstance(llm_output, dict):
|
| 740 |
+
usage = self._normalize_usage(llm_output)
|
| 741 |
+
if usage:
|
| 742 |
+
return usage
|
| 743 |
+
|
| 744 |
+
generations = getattr(response, "generations", None) or []
|
| 745 |
+
for generation_group in generations:
|
| 746 |
+
for generation in generation_group:
|
| 747 |
+
message = getattr(generation, "message", None)
|
| 748 |
+
metadata = getattr(message, "response_metadata", None)
|
| 749 |
+
usage = self._normalize_usage(metadata)
|
| 750 |
+
if usage:
|
| 751 |
+
return usage
|
| 752 |
+
usage_meta = getattr(message, "usage_metadata", None)
|
| 753 |
+
usage = self._normalize_usage(usage_meta)
|
| 754 |
+
if usage:
|
| 755 |
+
return usage
|
| 756 |
+
generation_info = getattr(generation, "generation_info", None)
|
| 757 |
+
usage = self._normalize_usage(generation_info)
|
| 758 |
+
if usage:
|
| 759 |
+
return usage
|
| 760 |
+
return None
|
| 761 |
+
|
| 762 |
+
def _normalize_usage(self, payload):
|
| 763 |
+
if not isinstance(payload, dict):
|
| 764 |
+
return None
|
| 765 |
+
nested = None
|
| 766 |
+
for key in ("token_usage", "usage", "usage_metadata"):
|
| 767 |
+
value = payload.get(key)
|
| 768 |
+
if isinstance(value, dict):
|
| 769 |
+
nested = value
|
| 770 |
+
break
|
| 771 |
+
source = nested or payload
|
| 772 |
+
|
| 773 |
+
prompt = self._first_int(
|
| 774 |
+
source,
|
| 775 |
+
"prompt_tokens",
|
| 776 |
+
"input_tokens",
|
| 777 |
+
"prompt_token_count",
|
| 778 |
+
)
|
| 779 |
+
completion = self._first_int(
|
| 780 |
+
source,
|
| 781 |
+
"completion_tokens",
|
| 782 |
+
"output_tokens",
|
| 783 |
+
"completion_token_count",
|
| 784 |
+
)
|
| 785 |
+
total = self._first_int(
|
| 786 |
+
source,
|
| 787 |
+
"total_tokens",
|
| 788 |
+
"total_token_count",
|
| 789 |
+
)
|
| 790 |
+
if total is None and prompt is not None and completion is not None:
|
| 791 |
+
total = prompt + completion
|
| 792 |
+
if prompt is None and completion is None and total is None:
|
| 793 |
+
return None
|
| 794 |
+
return {
|
| 795 |
+
"prompt_tokens": int(prompt or 0),
|
| 796 |
+
"completion_tokens": int(completion or 0),
|
| 797 |
+
"total_tokens": int(total or 0),
|
| 798 |
+
}
|
| 799 |
+
|
| 800 |
+
@staticmethod
|
| 801 |
+
def _first_int(payload, *keys):
|
| 802 |
+
for key in keys:
|
| 803 |
+
value = payload.get(key)
|
| 804 |
+
if isinstance(value, int):
|
| 805 |
+
return value
|
| 806 |
+
if isinstance(value, float):
|
| 807 |
+
return int(value)
|
| 808 |
+
return None
|
| 809 |
+
|
| 810 |
+
|
| 811 |
+
def check_or_create_path(path=None):
|
| 812 |
+
# Set a default path if none is provided
|
| 813 |
+
if path is None:
|
| 814 |
+
path = os.path.join(os.getcwd(), "tmp_directory")
|
| 815 |
+
|
| 816 |
+
# Check if the path exists
|
| 817 |
+
if not os.path.exists(path):
|
| 818 |
+
# If it doesn't exist, create the directory
|
| 819 |
+
os.makedirs(path)
|
| 820 |
+
print(f"Directory created at: {path}")
|
| 821 |
+
else:
|
| 822 |
+
print(f"Directory already exists at: {path}")
|
| 823 |
+
|
| 824 |
+
return path
|
| 825 |
+
|
| 826 |
+
|
| 827 |
+
def langchain_to_gradio_message(message):
|
| 828 |
+
# Build the title and content based on the message type
|
| 829 |
+
if isinstance(message.content, list):
|
| 830 |
+
# For a message with multiple content items (like text and tool use)
|
| 831 |
+
gradio_messages = []
|
| 832 |
+
for item in message.content:
|
| 833 |
+
gradio_message = {
|
| 834 |
+
"role": "user" if message.type == "human" else "assistant",
|
| 835 |
+
"content": "",
|
| 836 |
+
"metadata": {},
|
| 837 |
+
}
|
| 838 |
+
|
| 839 |
+
if item["type"] == "text":
|
| 840 |
+
item["text"] = item["text"].replace("<think>", "\n")
|
| 841 |
+
item["text"] = item["text"].replace("</think>", "\n")
|
| 842 |
+
gradio_message["content"] += f"{item['text']}\n"
|
| 843 |
+
gradio_messages.append(gradio_message)
|
| 844 |
+
elif item["type"] == "tool_use":
|
| 845 |
+
if item["name"] == "run_python_repl":
|
| 846 |
+
gradio_message["metadata"]["title"] = "🛠️ Writing code..."
|
| 847 |
+
# input = "```python {code_block}```\n".format(code_block=item['input']["command"])
|
| 848 |
+
gradio_message["metadata"]["log"] = "Executing Code block..."
|
| 849 |
+
gradio_message["content"] = f"##### Code: \n ```python \n {item['input']['command']} \n``` \n"
|
| 850 |
+
else:
|
| 851 |
+
gradio_message["metadata"]["title"] = f"🛠️ Used tool ```{item['name']}```"
|
| 852 |
+
to_print = ";".join([i + ": " + str(j) for i, j in item["input"].items()])
|
| 853 |
+
gradio_message["metadata"]["log"] = f"🔍 Input -- {to_print}\n"
|
| 854 |
+
gradio_message["metadata"]["status"] = "pending"
|
| 855 |
+
gradio_messages.append(gradio_message)
|
| 856 |
+
|
| 857 |
+
else:
|
| 858 |
+
gradio_message = {
|
| 859 |
+
"role": "user" if message.type == "human" else "assistant",
|
| 860 |
+
"content": "",
|
| 861 |
+
"metadata": {},
|
| 862 |
+
}
|
| 863 |
+
print(message)
|
| 864 |
+
content = message.content
|
| 865 |
+
content = content.replace("<think>", "\n")
|
| 866 |
+
content = content.replace("</think>", "\n")
|
| 867 |
+
content = content.replace("<solution>", "\n")
|
| 868 |
+
content = content.replace("</solution>", "\n")
|
| 869 |
+
|
| 870 |
+
gradio_message["content"] = content
|
| 871 |
+
gradio_messages = [gradio_message]
|
| 872 |
+
return gradio_messages
|
| 873 |
+
|
| 874 |
+
|
| 875 |
+
def parse_hpo_obo(file_path):
|
| 876 |
+
"""Parse the HPO OBO file and create a dictionary mapping HP IDs to phenotype descriptions.
|
| 877 |
+
|
| 878 |
+
Args:
|
| 879 |
+
file_path (str): Path to the HPO OBO file.
|
| 880 |
+
|
| 881 |
+
Returns:
|
| 882 |
+
dict: A dictionary where keys are HP IDs and values are phenotype descriptions.
|
| 883 |
+
|
| 884 |
+
"""
|
| 885 |
+
hp_dict = {}
|
| 886 |
+
current_id = None
|
| 887 |
+
current_name = None
|
| 888 |
+
|
| 889 |
+
with open(file_path) as file:
|
| 890 |
+
for line in file:
|
| 891 |
+
line = line.strip()
|
| 892 |
+
if line.startswith("[Term]"):
|
| 893 |
+
# If a new term block starts, save the previous term
|
| 894 |
+
if current_id and current_name:
|
| 895 |
+
hp_dict[current_id] = current_name
|
| 896 |
+
current_id = None
|
| 897 |
+
current_name = None
|
| 898 |
+
elif line.startswith("id: HP:"):
|
| 899 |
+
current_id = line.split(": ")[1]
|
| 900 |
+
elif line.startswith("name:"):
|
| 901 |
+
current_name = line.split(": ", 1)[1]
|
| 902 |
+
|
| 903 |
+
# Add the last term to the dictionary
|
| 904 |
+
if current_id and current_name:
|
| 905 |
+
hp_dict[current_id] = current_name
|
| 906 |
+
|
| 907 |
+
return hp_dict
|
| 908 |
+
|
| 909 |
+
|
| 910 |
+
def textify_api_dict(api_dict):
|
| 911 |
+
"""Convert a nested API dictionary to a nicely formatted string."""
|
| 912 |
+
lines = []
|
| 913 |
+
for category, methods in api_dict.items():
|
| 914 |
+
lines.append(f"Import file: {category}")
|
| 915 |
+
lines.append("=" * (len("Import file: ") + len(category)))
|
| 916 |
+
for method in methods:
|
| 917 |
+
lines.append(f"Method: {method.get('name', 'N/A')}")
|
| 918 |
+
lines.append(f" Description: {method.get('description', 'No description provided.')}")
|
| 919 |
+
|
| 920 |
+
# Process required parameters
|
| 921 |
+
req_params = method.get("required_parameters", [])
|
| 922 |
+
if req_params:
|
| 923 |
+
lines.append(" Required Parameters:")
|
| 924 |
+
for param in req_params:
|
| 925 |
+
param_name = param.get("name", "N/A")
|
| 926 |
+
param_type = param.get("type", "N/A")
|
| 927 |
+
param_desc = param.get("description", "No description")
|
| 928 |
+
param_default = param.get("default", "None")
|
| 929 |
+
lines.append(f" - {param_name} ({param_type}): {param_desc} [Default: {param_default}]")
|
| 930 |
+
|
| 931 |
+
# Process optional parameters
|
| 932 |
+
opt_params = method.get("optional_parameters", [])
|
| 933 |
+
if opt_params:
|
| 934 |
+
lines.append(" Optional Parameters:")
|
| 935 |
+
for param in opt_params:
|
| 936 |
+
param_name = param.get("name", "N/A")
|
| 937 |
+
param_type = param.get("type", "N/A")
|
| 938 |
+
param_desc = param.get("description", "No description")
|
| 939 |
+
param_default = param.get("default", "None")
|
| 940 |
+
lines.append(f" - {param_name} ({param_type}): {param_desc} [Default: {param_default}]")
|
| 941 |
+
|
| 942 |
+
lines.append("") # Empty line between methods
|
| 943 |
+
lines.append("") # Extra empty line after each category
|
| 944 |
+
|
| 945 |
+
return "\n".join(lines)
|
| 946 |
+
|
| 947 |
+
|
| 948 |
+
def read_module2api():
|
| 949 |
+
fields = [
|
| 950 |
+
"literature",
|
| 951 |
+
"biochemistry",
|
| 952 |
+
"bioimaging",
|
| 953 |
+
"bioengineering",
|
| 954 |
+
"biophysics",
|
| 955 |
+
"glycoengineering",
|
| 956 |
+
"cancer_biology",
|
| 957 |
+
"cell_biology",
|
| 958 |
+
"molecular_biology",
|
| 959 |
+
"genetics",
|
| 960 |
+
"genomics",
|
| 961 |
+
"immunology",
|
| 962 |
+
"microbiology",
|
| 963 |
+
"pathology",
|
| 964 |
+
"pharmacology",
|
| 965 |
+
"physiology",
|
| 966 |
+
"synthetic_biology",
|
| 967 |
+
"systems_biology",
|
| 968 |
+
"support_tools",
|
| 969 |
+
"database",
|
| 970 |
+
"lab_automation",
|
| 971 |
+
"protocols",
|
| 972 |
+
]
|
| 973 |
+
|
| 974 |
+
module2api = {}
|
| 975 |
+
for field in fields:
|
| 976 |
+
module_name = f"biomni.tool.tool_description.{field}"
|
| 977 |
+
module = importlib.import_module(module_name)
|
| 978 |
+
module2api[f"biomni.tool.{field}"] = module.description
|
| 979 |
+
return module2api
|
| 980 |
+
|
| 981 |
+
|
| 982 |
+
def download_and_unzip(url: str, dest_dir: str) -> str:
|
| 983 |
+
"""Download a zip file from a URL and extract it to the destination directory.
|
| 984 |
+
|
| 985 |
+
Args:
|
| 986 |
+
url: The URL to download the zip file from.
|
| 987 |
+
dest_dir: The directory to extract the contents to.
|
| 988 |
+
|
| 989 |
+
Returns:
|
| 990 |
+
The path to the extracted directory, or an error message.
|
| 991 |
+
|
| 992 |
+
"""
|
| 993 |
+
try:
|
| 994 |
+
os.makedirs(dest_dir, exist_ok=True)
|
| 995 |
+
print(f"Downloading from {url} ...")
|
| 996 |
+
with requests.get(url, stream=True) as r:
|
| 997 |
+
r.raise_for_status()
|
| 998 |
+
total_size = int(r.headers.get("content-length", 0))
|
| 999 |
+
chunk_size = 8192
|
| 1000 |
+
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp_file:
|
| 1001 |
+
with tqdm.tqdm(
|
| 1002 |
+
total=total_size / (1024**3),
|
| 1003 |
+
unit="GB",
|
| 1004 |
+
unit_scale=True,
|
| 1005 |
+
desc="Downloading",
|
| 1006 |
+
ncols=80,
|
| 1007 |
+
) as pbar:
|
| 1008 |
+
for chunk in r.iter_content(chunk_size=chunk_size):
|
| 1009 |
+
if chunk:
|
| 1010 |
+
tmp_file.write(chunk)
|
| 1011 |
+
pbar.update(len(chunk) / (1024**3))
|
| 1012 |
+
tmp_zip_path = tmp_file.name
|
| 1013 |
+
print(f"Downloaded to {tmp_zip_path}. Extracting...")
|
| 1014 |
+
with zipfile.ZipFile(tmp_zip_path, "r") as zip_ref:
|
| 1015 |
+
zip_ref.extractall(dest_dir)
|
| 1016 |
+
os.unlink(tmp_zip_path)
|
| 1017 |
+
print(f"Extraction complete to {dest_dir}")
|
| 1018 |
+
return dest_dir
|
| 1019 |
+
except Exception as e:
|
| 1020 |
+
print(f"Error downloading or extracting zip: {e}")
|
| 1021 |
+
return f"Error: {e}"
|
| 1022 |
+
|
| 1023 |
+
|
| 1024 |
+
def check_and_download_s3_files(
|
| 1025 |
+
s3_bucket_url: str, local_data_lake_path: str, expected_files: list[str], folder: str = "data_lake"
|
| 1026 |
+
) -> dict[str, bool]:
|
| 1027 |
+
"""Check for missing files in the local data lake and download them from S3 bucket.
|
| 1028 |
+
|
| 1029 |
+
Args:
|
| 1030 |
+
s3_bucket_url: Base URL of the S3 bucket (e.g., "https://biomni-release.s3.amazonaws.com")
|
| 1031 |
+
local_data_lake_path: Local path to the data lake directory
|
| 1032 |
+
expected_files: List of expected file names in the data lake
|
| 1033 |
+
folder: S3 folder name ("data_lake" or "benchmark")
|
| 1034 |
+
|
| 1035 |
+
Returns:
|
| 1036 |
+
Dictionary mapping file names to download success status
|
| 1037 |
+
"""
|
| 1038 |
+
|
| 1039 |
+
os.makedirs(local_data_lake_path, exist_ok=True)
|
| 1040 |
+
download_results = {}
|
| 1041 |
+
|
| 1042 |
+
def download_with_progress(url: str, file_path: str, desc: str) -> bool:
|
| 1043 |
+
"""Download file with progress bar."""
|
| 1044 |
+
try:
|
| 1045 |
+
response = requests.get(url, stream=True)
|
| 1046 |
+
response.raise_for_status()
|
| 1047 |
+
|
| 1048 |
+
total_size = int(response.headers.get("content-length", 0))
|
| 1049 |
+
|
| 1050 |
+
with open(file_path, "wb") as f:
|
| 1051 |
+
if total_size > 0:
|
| 1052 |
+
with tqdm.tqdm(total=total_size, unit="B", unit_scale=True, desc=desc, ncols=80) as pbar:
|
| 1053 |
+
for chunk in response.iter_content(chunk_size=8192):
|
| 1054 |
+
if chunk:
|
| 1055 |
+
f.write(chunk)
|
| 1056 |
+
pbar.update(len(chunk))
|
| 1057 |
+
else:
|
| 1058 |
+
for chunk in response.iter_content(chunk_size=8192):
|
| 1059 |
+
if chunk:
|
| 1060 |
+
f.write(chunk)
|
| 1061 |
+
return True
|
| 1062 |
+
except Exception as e:
|
| 1063 |
+
print(f"✗ Failed to download {desc}: {e}")
|
| 1064 |
+
if os.path.exists(file_path):
|
| 1065 |
+
try:
|
| 1066 |
+
os.remove(file_path)
|
| 1067 |
+
except OSError:
|
| 1068 |
+
pass
|
| 1069 |
+
return False
|
| 1070 |
+
|
| 1071 |
+
def cleanup_file(file_path: str):
|
| 1072 |
+
"""Clean up file if it exists."""
|
| 1073 |
+
if os.path.exists(file_path):
|
| 1074 |
+
try:
|
| 1075 |
+
os.remove(file_path)
|
| 1076 |
+
except OSError:
|
| 1077 |
+
pass
|
| 1078 |
+
|
| 1079 |
+
# Handle benchmark folder (download as zip)
|
| 1080 |
+
if folder == "benchmark":
|
| 1081 |
+
print(f"Downloading entire {folder} folder structure...")
|
| 1082 |
+
s3_zip_url = urljoin(s3_bucket_url + "/", folder + ".zip")
|
| 1083 |
+
|
| 1084 |
+
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp_zip:
|
| 1085 |
+
tmp_zip_path = tmp_zip.name
|
| 1086 |
+
|
| 1087 |
+
if download_with_progress(s3_zip_url, tmp_zip_path, f"{folder}.zip"):
|
| 1088 |
+
print(f"Extracting {folder}.zip...")
|
| 1089 |
+
try:
|
| 1090 |
+
with zipfile.ZipFile(tmp_zip_path, "r") as zip_ref:
|
| 1091 |
+
zip_ref.extractall(local_data_lake_path)
|
| 1092 |
+
print(f"✓ Successfully downloaded and extracted {folder} folder")
|
| 1093 |
+
download_results = dict.fromkeys(expected_files, True)
|
| 1094 |
+
except Exception as e:
|
| 1095 |
+
print(f"✗ Error extracting {folder}.zip: {e}")
|
| 1096 |
+
download_results = dict.fromkeys(expected_files, False)
|
| 1097 |
+
finally:
|
| 1098 |
+
cleanup_file(tmp_zip_path)
|
| 1099 |
+
else:
|
| 1100 |
+
download_results = dict.fromkeys(expected_files, False)
|
| 1101 |
+
|
| 1102 |
+
return download_results
|
| 1103 |
+
|
| 1104 |
+
# Handle data_lake folder (download individual files)
|
| 1105 |
+
for filename in expected_files:
|
| 1106 |
+
local_file_path = os.path.join(local_data_lake_path, filename)
|
| 1107 |
+
|
| 1108 |
+
if os.path.exists(local_file_path):
|
| 1109 |
+
download_results[filename] = True
|
| 1110 |
+
continue
|
| 1111 |
+
|
| 1112 |
+
s3_file_url = urljoin(s3_bucket_url + "/" + folder + "/", filename)
|
| 1113 |
+
print(f"Downloading {filename} from {folder}...")
|
| 1114 |
+
|
| 1115 |
+
if download_with_progress(s3_file_url, local_file_path, filename):
|
| 1116 |
+
print(f"✓ Successfully downloaded: {filename}")
|
| 1117 |
+
download_results[filename] = True
|
| 1118 |
+
else:
|
| 1119 |
+
download_results[filename] = False
|
| 1120 |
+
|
| 1121 |
+
return download_results
|
| 1122 |
+
|
| 1123 |
+
|
| 1124 |
+
def clean_message_content(content: str) -> str:
|
| 1125 |
+
"""Clean message content by removing ANSI escape codes.
|
| 1126 |
+
|
| 1127 |
+
This function removes ANSI escape sequences (like color codes) from text content
|
| 1128 |
+
that might be present in terminal output or console messages. This ensures clean
|
| 1129 |
+
text for markdown generation and PDF conversion.
|
| 1130 |
+
|
| 1131 |
+
Args:
|
| 1132 |
+
content: The raw message content that may contain ANSI escape codes
|
| 1133 |
+
|
| 1134 |
+
Returns:
|
| 1135 |
+
Cleaned content with ANSI escape codes removed
|
| 1136 |
+
|
| 1137 |
+
Example:
|
| 1138 |
+
>>> clean_message_content("Hello \x1b[31mworld\x1b[0m!")
|
| 1139 |
+
"Hello world!"
|
| 1140 |
+
"""
|
| 1141 |
+
import re
|
| 1142 |
+
|
| 1143 |
+
return re.sub(r"\x1b\[[0-9;]*m", "", content)
|
| 1144 |
+
|
| 1145 |
+
|
| 1146 |
+
def should_skip_message(clean_output: str) -> bool:
|
| 1147 |
+
"""Check if message should be skipped during markdown generation.
|
| 1148 |
+
|
| 1149 |
+
This function determines whether a message should be excluded from the final
|
| 1150 |
+
markdown output. It skips empty or meaningless messages but preserves important
|
| 1151 |
+
error messages that should be displayed to users.
|
| 1152 |
+
|
| 1153 |
+
Args:
|
| 1154 |
+
clean_output: The cleaned message content to evaluate
|
| 1155 |
+
|
| 1156 |
+
Returns:
|
| 1157 |
+
True if the message should be skipped, False otherwise
|
| 1158 |
+
|
| 1159 |
+
Note:
|
| 1160 |
+
Parsing error messages are intentionally not skipped as they provide
|
| 1161 |
+
important feedback to users about conversation flow issues.
|
| 1162 |
+
"""
|
| 1163 |
+
return (
|
| 1164 |
+
clean_output.strip() in ["", "None", "null", "undefined"]
|
| 1165 |
+
# Don't skip parsing error messages - they should be displayed and increment step counter
|
| 1166 |
+
# or "There are no tags" in clean_output
|
| 1167 |
+
# or "Execution terminated due to repeated parsing errors" in clean_output
|
| 1168 |
+
)
|
| 1169 |
+
|
| 1170 |
+
|
| 1171 |
+
def has_execution_results(clean_output: str, execution_results) -> bool:
|
| 1172 |
+
"""Check if message contains code execution and has associated results.
|
| 1173 |
+
|
| 1174 |
+
This function determines whether a message contains executable code and has
|
| 1175 |
+
corresponding execution results available for display in the markdown output.
|
| 1176 |
+
|
| 1177 |
+
Args:
|
| 1178 |
+
clean_output: The cleaned message content to check for execute tags
|
| 1179 |
+
execution_results: List of execution results from the agent's execution history
|
| 1180 |
+
|
| 1181 |
+
Returns:
|
| 1182 |
+
True if the message contains <execute> tags and has execution results available
|
| 1183 |
+
"""
|
| 1184 |
+
return "<execute>" in clean_output and execution_results is not None and execution_results
|
| 1185 |
+
|
| 1186 |
+
|
| 1187 |
+
def find_matching_execution(clean_output: str, execution_results) -> dict | None:
|
| 1188 |
+
"""Find the execution result that matches the given message content.
|
| 1189 |
+
|
| 1190 |
+
This function searches through the execution results to find the one that
|
| 1191 |
+
corresponds to the current message. It matches based on the triggering message
|
| 1192 |
+
content to associate execution results with their originating AI messages.
|
| 1193 |
+
|
| 1194 |
+
Args:
|
| 1195 |
+
clean_output: The cleaned message content to match against
|
| 1196 |
+
execution_results: List of execution result dictionaries containing
|
| 1197 |
+
triggering messages and execution data
|
| 1198 |
+
|
| 1199 |
+
Returns:
|
| 1200 |
+
The matching execution result dictionary if found, None otherwise
|
| 1201 |
+
|
| 1202 |
+
Note:
|
| 1203 |
+
The matching is bidirectional - it checks if either the triggering message
|
| 1204 |
+
is contained in the current output or vice versa to handle partial matches.
|
| 1205 |
+
"""
|
| 1206 |
+
for exec_result in execution_results:
|
| 1207 |
+
if exec_result["triggering_message"] in clean_output or clean_output in exec_result["triggering_message"]:
|
| 1208 |
+
return exec_result
|
| 1209 |
+
return None
|
| 1210 |
+
|
| 1211 |
+
|
| 1212 |
+
def create_parsing_error_html() -> str:
|
| 1213 |
+
"""Create HTML markup for displaying parsing errors in markdown output.
|
| 1214 |
+
|
| 1215 |
+
This function generates a styled HTML block that displays parsing errors
|
| 1216 |
+
when the agent's response doesn't contain the required tags. The HTML
|
| 1217 |
+
uses CSS classes for consistent styling in the final PDF output.
|
| 1218 |
+
|
| 1219 |
+
Returns:
|
| 1220 |
+
HTML string containing a styled parsing error message box
|
| 1221 |
+
|
| 1222 |
+
Note:
|
| 1223 |
+
The returned HTML uses CSS classes defined in get_pdf_css_content()
|
| 1224 |
+
for consistent styling across the document.
|
| 1225 |
+
"""
|
| 1226 |
+
return """
|
| 1227 |
+
<div class="parsing-error-box">
|
| 1228 |
+
<div class="parsing-error-header">Parsing Error</div>
|
| 1229 |
+
<div class="parsing-error-content">Each response must include thinking process followed by either execute or solution tag. But there are no tags in the current response.</div>
|
| 1230 |
+
</div>
|
| 1231 |
+
"""
|
| 1232 |
+
|
| 1233 |
+
|
| 1234 |
+
def parse_tool_calls_from_code(code: str, module2api: dict, custom_functions: dict = None) -> list[str]:
|
| 1235 |
+
"""Parse code to detect imported tools by analyzing import statements.
|
| 1236 |
+
|
| 1237 |
+
This function analyzes Python code to identify which tools/functions are being
|
| 1238 |
+
imported and used. It extracts tool names from import statements and function
|
| 1239 |
+
calls, then returns a deduplicated list of detected tool names.
|
| 1240 |
+
|
| 1241 |
+
Args:
|
| 1242 |
+
code: The Python code string to analyze for tool imports
|
| 1243 |
+
module2api: Dictionary mapping module names to their available API tools
|
| 1244 |
+
custom_functions: Optional dictionary of custom functions that have been
|
| 1245 |
+
added to the agent
|
| 1246 |
+
|
| 1247 |
+
Returns:
|
| 1248 |
+
Sorted list of unique tool names detected in the code
|
| 1249 |
+
|
| 1250 |
+
Example:
|
| 1251 |
+
>>> code = "from biomni.tool import analyze_data\nimport pandas as pd"
|
| 1252 |
+
>>> parse_tool_calls_from_code(code, module2api)
|
| 1253 |
+
['analyze_data', 'pandas']
|
| 1254 |
+
"""
|
| 1255 |
+
tool_module_pairs = parse_tool_calls_with_modules(code, module2api, custom_functions)
|
| 1256 |
+
return sorted({pair[0] for pair in tool_module_pairs})
|
| 1257 |
+
|
| 1258 |
+
|
| 1259 |
+
def parse_tool_calls_with_modules(code: str, module2api: dict, custom_functions: dict = None) -> list[tuple[str, str]]:
|
| 1260 |
+
"""Parse code to detect imported tools and their associated modules.
|
| 1261 |
+
|
| 1262 |
+
This function performs detailed analysis of Python code to identify which
|
| 1263 |
+
tools/functions are being imported and which modules they belong to. It
|
| 1264 |
+
handles various import patterns including direct imports, from-imports,
|
| 1265 |
+
and module.function patterns.
|
| 1266 |
+
|
| 1267 |
+
Args:
|
| 1268 |
+
code: The Python code string to analyze for tool imports
|
| 1269 |
+
module2api: Dictionary mapping module names to their available API tools
|
| 1270 |
+
custom_functions: Optional dictionary of custom functions that have been
|
| 1271 |
+
added to the agent
|
| 1272 |
+
|
| 1273 |
+
Returns:
|
| 1274 |
+
List of tuples containing (tool_name, module_name) pairs for each
|
| 1275 |
+
detected tool and its associated module
|
| 1276 |
+
|
| 1277 |
+
Note:
|
| 1278 |
+
The function uses regex patterns to match various import statement
|
| 1279 |
+
formats and also detects direct function calls without explicit imports.
|
| 1280 |
+
"""
|
| 1281 |
+
import re
|
| 1282 |
+
|
| 1283 |
+
detected_tools = set()
|
| 1284 |
+
|
| 1285 |
+
# Get all available tools from module2api
|
| 1286 |
+
all_tools = {}
|
| 1287 |
+
for module_name, module_tools in module2api.items():
|
| 1288 |
+
for tool in module_tools:
|
| 1289 |
+
if isinstance(tool, dict) and "name" in tool:
|
| 1290 |
+
tool_name = tool["name"]
|
| 1291 |
+
if tool_name not in all_tools:
|
| 1292 |
+
all_tools[tool_name] = []
|
| 1293 |
+
all_tools[tool_name].append(module_name)
|
| 1294 |
+
|
| 1295 |
+
# Add custom tools
|
| 1296 |
+
if custom_functions:
|
| 1297 |
+
for tool_name in custom_functions.keys():
|
| 1298 |
+
if tool_name not in all_tools:
|
| 1299 |
+
all_tools[tool_name] = []
|
| 1300 |
+
all_tools[tool_name].append("custom_tools")
|
| 1301 |
+
|
| 1302 |
+
# Look for import statements in the code
|
| 1303 |
+
import_patterns = [
|
| 1304 |
+
r"from\s+([\w.]+)\s+import\s+([\w,\s]+)", # from module import tool1, tool2
|
| 1305 |
+
r"import\s+([\w.]+)", # import module
|
| 1306 |
+
]
|
| 1307 |
+
|
| 1308 |
+
for pattern in import_patterns:
|
| 1309 |
+
matches = re.findall(pattern, code)
|
| 1310 |
+
for match in matches:
|
| 1311 |
+
if len(match) == 2: # from module import tools
|
| 1312 |
+
module_name, tools_str = match
|
| 1313 |
+
# Split tools by comma and clean up
|
| 1314 |
+
tools = [tool.strip() for tool in tools_str.split(",")]
|
| 1315 |
+
|
| 1316 |
+
for tool in tools:
|
| 1317 |
+
# Check if this tool exists in any module
|
| 1318 |
+
if tool in all_tools:
|
| 1319 |
+
# Find the best matching module
|
| 1320 |
+
best_module = find_best_module_match(module_name, all_tools[tool])
|
| 1321 |
+
detected_tools.add((tool, best_module))
|
| 1322 |
+
# Also check if it's a module.function pattern
|
| 1323 |
+
elif "." in tool:
|
| 1324 |
+
parts = tool.split(".")
|
| 1325 |
+
if len(parts) == 2:
|
| 1326 |
+
module_part, func_part = parts
|
| 1327 |
+
if func_part in all_tools:
|
| 1328 |
+
best_module = find_best_module_match(module_part, all_tools[func_part])
|
| 1329 |
+
detected_tools.add((func_part, best_module))
|
| 1330 |
+
|
| 1331 |
+
elif len(match) == 1: # import module
|
| 1332 |
+
module_name = match[0]
|
| 1333 |
+
# Check if any tools from this module are used
|
| 1334 |
+
for tool_name, modules in all_tools.items():
|
| 1335 |
+
if any(module_name in mod for mod in modules):
|
| 1336 |
+
# Look for usage of this tool in the code
|
| 1337 |
+
if re.search(rf"\b{tool_name}\s*\(", code):
|
| 1338 |
+
best_module = find_best_module_match(module_name, modules)
|
| 1339 |
+
detected_tools.add((tool_name, best_module))
|
| 1340 |
+
|
| 1341 |
+
# Also look for direct function calls without imports
|
| 1342 |
+
function_call_pattern = r"(\w+)\s*\("
|
| 1343 |
+
function_calls = re.findall(function_call_pattern, code)
|
| 1344 |
+
|
| 1345 |
+
for func_call in function_calls:
|
| 1346 |
+
if func_call in all_tools:
|
| 1347 |
+
# For direct calls, use the first available module
|
| 1348 |
+
best_module = all_tools[func_call][0]
|
| 1349 |
+
detected_tools.add((func_call, best_module))
|
| 1350 |
+
|
| 1351 |
+
return sorted(detected_tools)
|
| 1352 |
+
|
| 1353 |
+
|
| 1354 |
+
def find_best_module_match(target_module: str, available_modules: list[str]) -> str:
|
| 1355 |
+
"""Find the best matching module from a list of available modules.
|
| 1356 |
+
|
| 1357 |
+
This function attempts to match a target module name against a list of
|
| 1358 |
+
available modules using various matching strategies: exact match, partial
|
| 1359 |
+
substring matches, and fallback to the first available module.
|
| 1360 |
+
|
| 1361 |
+
Args:
|
| 1362 |
+
target_module: The module name we're trying to match
|
| 1363 |
+
available_modules: List of available module names to search through
|
| 1364 |
+
|
| 1365 |
+
Returns:
|
| 1366 |
+
The best matching module name from the available modules list.
|
| 1367 |
+
Returns "unknown" if no modules are available.
|
| 1368 |
+
|
| 1369 |
+
Note:
|
| 1370 |
+
The matching strategy prioritizes exact matches, then partial matches
|
| 1371 |
+
(where either the target is contained in the module name or vice versa),
|
| 1372 |
+
and finally falls back to the first available module.
|
| 1373 |
+
"""
|
| 1374 |
+
# First try exact match
|
| 1375 |
+
if target_module in available_modules:
|
| 1376 |
+
return target_module
|
| 1377 |
+
|
| 1378 |
+
# Try partial matches
|
| 1379 |
+
for module in available_modules:
|
| 1380 |
+
if target_module in module or module in target_module:
|
| 1381 |
+
return module
|
| 1382 |
+
|
| 1383 |
+
# Return the first available module as fallback
|
| 1384 |
+
return available_modules[0] if available_modules else "unknown"
|
| 1385 |
+
|
| 1386 |
+
|
| 1387 |
+
def inject_custom_functions_to_repl(custom_functions: dict):
|
| 1388 |
+
"""Inject custom functions into the Python REPL execution environment.
|
| 1389 |
+
|
| 1390 |
+
This function makes custom tools available during code execution by injecting
|
| 1391 |
+
them into both the persistent execution namespace and the builtins module.
|
| 1392 |
+
This allows the agent to call custom functions that users have added via
|
| 1393 |
+
agent.add_tool() when executing Python code in <execute> blocks.
|
| 1394 |
+
|
| 1395 |
+
Args:
|
| 1396 |
+
custom_functions: Dictionary mapping function names to their callable objects
|
| 1397 |
+
|
| 1398 |
+
Note:
|
| 1399 |
+
The function modifies both the persistent namespace used by run_python_repl
|
| 1400 |
+
and the builtins module to ensure maximum compatibility and accessibility
|
| 1401 |
+
of custom functions during code execution.
|
| 1402 |
+
"""
|
| 1403 |
+
if custom_functions:
|
| 1404 |
+
# Access the persistent namespace used by run_python_repl
|
| 1405 |
+
from biomni.tool.support_tools import _persistent_namespace
|
| 1406 |
+
|
| 1407 |
+
# Inject all custom functions into the execution namespace
|
| 1408 |
+
for name, func in custom_functions.items():
|
| 1409 |
+
_persistent_namespace[name] = func
|
| 1410 |
+
|
| 1411 |
+
# Also make them available in builtins for broader access
|
| 1412 |
+
import builtins
|
| 1413 |
+
|
| 1414 |
+
if not hasattr(builtins, "_biomni_custom_functions"):
|
| 1415 |
+
builtins._biomni_custom_functions = {}
|
| 1416 |
+
builtins._biomni_custom_functions.update(custom_functions)
|
| 1417 |
+
|
| 1418 |
+
|
| 1419 |
+
def format_execute_tags_in_content(content: str, parse_tool_calls_with_modules_func) -> str:
|
| 1420 |
+
"""Format execute tags in content by extracting code and creating highlighted tool call blocks.
|
| 1421 |
+
|
| 1422 |
+
This function processes content that contains <execute>...</execute> tags and
|
| 1423 |
+
converts them into styled HTML blocks that display the code with syntax highlighting
|
| 1424 |
+
and information about which tools are being used.
|
| 1425 |
+
|
| 1426 |
+
Args:
|
| 1427 |
+
content: The content string that may contain <execute> tags
|
| 1428 |
+
parse_tool_calls_with_modules_func: Function to parse tool calls with modules
|
| 1429 |
+
(typically parse_tool_calls_with_modules)
|
| 1430 |
+
|
| 1431 |
+
Returns:
|
| 1432 |
+
Formatted content with execute tags converted to highlighted tool call blocks.
|
| 1433 |
+
Also processes <solution> tags in the same pass.
|
| 1434 |
+
|
| 1435 |
+
Note:
|
| 1436 |
+
The function also calls format_solution_tags_in_content() to handle
|
| 1437 |
+
solution tags in the same processing pass.
|
| 1438 |
+
"""
|
| 1439 |
+
import re
|
| 1440 |
+
|
| 1441 |
+
# Pattern to match <execute>...</execute> blocks
|
| 1442 |
+
execute_pattern = r"<execute>(.*?)</execute>"
|
| 1443 |
+
|
| 1444 |
+
def replace_execute_tag(match):
|
| 1445 |
+
code_content = match.group(1).strip()
|
| 1446 |
+
language, tool_name = detect_code_language_and_tool(code_content)
|
| 1447 |
+
code_content = clean_code_content(code_content, language)
|
| 1448 |
+
|
| 1449 |
+
# Parse tools from the code content with module information
|
| 1450 |
+
detected_tool_modules = parse_tool_calls_with_modules_func(code_content)
|
| 1451 |
+
|
| 1452 |
+
# Create the formatted block
|
| 1453 |
+
formatted_block = create_tool_call_block(code_content, language, tool_name, detected_tool_modules)
|
| 1454 |
+
return formatted_block
|
| 1455 |
+
|
| 1456 |
+
# Replace all execute tags with formatted tool call blocks
|
| 1457 |
+
formatted_content = re.sub(execute_pattern, replace_execute_tag, content, flags=re.DOTALL)
|
| 1458 |
+
|
| 1459 |
+
# Also format solution tags
|
| 1460 |
+
formatted_content = format_solution_tags_in_content(formatted_content)
|
| 1461 |
+
|
| 1462 |
+
return formatted_content
|
| 1463 |
+
|
| 1464 |
+
|
| 1465 |
+
def detect_code_language_and_tool(code_content: str) -> tuple[str, str]:
|
| 1466 |
+
"""Detect the programming language and tool name from code content.
|
| 1467 |
+
|
| 1468 |
+
This function analyzes code content to determine the programming language
|
| 1469 |
+
and appropriate tool name based on language markers at the beginning of
|
| 1470 |
+
the code block.
|
| 1471 |
+
|
| 1472 |
+
Args:
|
| 1473 |
+
code_content: The code content to analyze for language markers
|
| 1474 |
+
|
| 1475 |
+
Returns:
|
| 1476 |
+
Tuple containing (language, tool_name) where:
|
| 1477 |
+
- language: The detected programming language ("python", "r", "bash")
|
| 1478 |
+
- tool_name: The human-readable tool name for display
|
| 1479 |
+
|
| 1480 |
+
Example:
|
| 1481 |
+
>>> detect_code_language_and_tool("#!R\nlibrary(ggplot2)")
|
| 1482 |
+
("r", "R REPL")
|
| 1483 |
+
>>> detect_code_language_and_tool("#!BASH\necho 'hello'")
|
| 1484 |
+
("bash", "Bash Script")
|
| 1485 |
+
"""
|
| 1486 |
+
if code_content.startswith("#!R") or code_content.startswith("# R code") or code_content.startswith("# R script"):
|
| 1487 |
+
return "r", "R REPL"
|
| 1488 |
+
elif code_content.startswith("#!BASH") or code_content.startswith("# Bash script"):
|
| 1489 |
+
return "bash", "Bash Script"
|
| 1490 |
+
elif code_content.startswith("#!CLI"):
|
| 1491 |
+
return "bash", "CLI Command"
|
| 1492 |
+
else:
|
| 1493 |
+
return "python", "Python REPL"
|
| 1494 |
+
|
| 1495 |
+
|
| 1496 |
+
def clean_code_content(code_content: str, language: str) -> str:
|
| 1497 |
+
"""Clean code content by removing language markers.
|
| 1498 |
+
|
| 1499 |
+
This function removes language-specific markers from the beginning of code
|
| 1500 |
+
content to prepare it for display in code blocks. The markers are used
|
| 1501 |
+
internally for language detection but should not appear in the final output.
|
| 1502 |
+
|
| 1503 |
+
Args:
|
| 1504 |
+
code_content: The raw code content that may contain language markers
|
| 1505 |
+
language: The detected programming language ("python", "r", "bash")
|
| 1506 |
+
|
| 1507 |
+
Returns:
|
| 1508 |
+
Cleaned code content with language markers removed
|
| 1509 |
+
|
| 1510 |
+
Example:
|
| 1511 |
+
>>> clean_code_content("#!R\nlibrary(ggplot2)", "r")
|
| 1512 |
+
"library(ggplot2)"
|
| 1513 |
+
>>> clean_code_content("#!BASH\necho 'hello'", "bash")
|
| 1514 |
+
"echo 'hello'"
|
| 1515 |
+
"""
|
| 1516 |
+
import re
|
| 1517 |
+
|
| 1518 |
+
if language == "r":
|
| 1519 |
+
return re.sub(r"^#!R|^# R code|^# R script", "", code_content, count=1).strip()
|
| 1520 |
+
elif language == "bash":
|
| 1521 |
+
if code_content.startswith("#!BASH") or code_content.startswith("# Bash script"):
|
| 1522 |
+
return re.sub(r"^#!BASH|^# Bash script", "", code_content, count=1).strip()
|
| 1523 |
+
elif code_content.startswith("#!CLI"):
|
| 1524 |
+
return re.sub(r"^#!CLI", "", code_content, count=1).strip()
|
| 1525 |
+
return code_content
|
| 1526 |
+
|
| 1527 |
+
|
| 1528 |
+
def create_tool_call_block(code_content: str, language: str, tool_name: str, detected_tool_modules: list) -> str:
|
| 1529 |
+
"""Create the HTML block for tool call highlighting.
|
| 1530 |
+
|
| 1531 |
+
This function generates a styled HTML block that displays code execution
|
| 1532 |
+
information including the code itself, syntax highlighting, and a list of
|
| 1533 |
+
tools that were used during execution.
|
| 1534 |
+
|
| 1535 |
+
Args:
|
| 1536 |
+
code_content: The cleaned code content to display
|
| 1537 |
+
language: The programming language for syntax highlighting
|
| 1538 |
+
tool_name: The default tool name to display if no specific tools detected
|
| 1539 |
+
detected_tool_modules: List of (tool_name, module_name) tuples for tools used
|
| 1540 |
+
|
| 1541 |
+
Returns:
|
| 1542 |
+
HTML string containing a styled tool call block with code and tool information
|
| 1543 |
+
|
| 1544 |
+
Note:
|
| 1545 |
+
The HTML uses CSS classes defined in get_pdf_css_content() for styling.
|
| 1546 |
+
If no specific tools are detected, it falls back to a default tool name.
|
| 1547 |
+
"""
|
| 1548 |
+
# Create the formatted block with code and tools used
|
| 1549 |
+
formatted_block = f"""<div class="tool-call-highlight">
|
| 1550 |
+
<div class="tool-call-header">
|
| 1551 |
+
<strong>Code Execution</strong>
|
| 1552 |
+
</div>
|
| 1553 |
+
<div class="tool-call-input">
|
| 1554 |
+
```{language}
|
| 1555 |
+
{code_content}
|
| 1556 |
+
```
|
| 1557 |
+
</div>"""
|
| 1558 |
+
|
| 1559 |
+
# Add tools used section
|
| 1560 |
+
if detected_tool_modules:
|
| 1561 |
+
tools_list = format_detected_tools(detected_tool_modules)
|
| 1562 |
+
formatted_block += f"""
|
| 1563 |
+
<div class="tools-used">
|
| 1564 |
+
<strong>Tools Used:</strong> {tools_list}
|
| 1565 |
+
</div>"""
|
| 1566 |
+
else:
|
| 1567 |
+
formatted_block += format_default_tool_name(language, tool_name)
|
| 1568 |
+
|
| 1569 |
+
formatted_block += "</div>"
|
| 1570 |
+
return formatted_block
|
| 1571 |
+
|
| 1572 |
+
|
| 1573 |
+
def format_detected_tools(detected_tool_modules: list) -> str:
|
| 1574 |
+
"""Format detected tools with their modules for display.
|
| 1575 |
+
|
| 1576 |
+
This function takes a list of (tool_name, module_name) tuples and formats
|
| 1577 |
+
them into a human-readable string for display in the tool call blocks.
|
| 1578 |
+
It handles special cases for common tools and formats module names appropriately.
|
| 1579 |
+
|
| 1580 |
+
Args:
|
| 1581 |
+
detected_tool_modules: List of (tool_name, module_name) tuples
|
| 1582 |
+
|
| 1583 |
+
Returns:
|
| 1584 |
+
Comma-separated string of formatted tool descriptions
|
| 1585 |
+
|
| 1586 |
+
Example:
|
| 1587 |
+
>>> format_detected_tools([("analyze_data", "biomni.tool"), ("pandas", "pandas")])
|
| 1588 |
+
"biomni → analyze_data, pandas → pandas"
|
| 1589 |
+
"""
|
| 1590 |
+
tool_descriptions = []
|
| 1591 |
+
for tool_name, module_name in detected_tool_modules:
|
| 1592 |
+
if tool_name == "python_repl":
|
| 1593 |
+
tool_descriptions.append("Python REPL")
|
| 1594 |
+
elif tool_name == "r_repl":
|
| 1595 |
+
tool_descriptions.append("R REPL")
|
| 1596 |
+
elif "bash" in tool_name.lower():
|
| 1597 |
+
tool_descriptions.append("Bash Script")
|
| 1598 |
+
else:
|
| 1599 |
+
# Extract the last part of the module name for display
|
| 1600 |
+
display_module = module_name.split(".")[-1] if "." in module_name else module_name
|
| 1601 |
+
tool_descriptions.append(f"{display_module} → {tool_name}")
|
| 1602 |
+
|
| 1603 |
+
return ", ".join(sorted(tool_descriptions))
|
| 1604 |
+
|
| 1605 |
+
|
| 1606 |
+
def format_default_tool_name(language: str, tool_name: str) -> str:
|
| 1607 |
+
"""Format default tool name based on programming language.
|
| 1608 |
+
|
| 1609 |
+
This function generates HTML for displaying the default tool name when
|
| 1610 |
+
no specific tools are detected in the code. It maps programming languages
|
| 1611 |
+
to their appropriate default tool names.
|
| 1612 |
+
|
| 1613 |
+
Args:
|
| 1614 |
+
language: The programming language ("python", "r", "bash")
|
| 1615 |
+
tool_name: The detected tool name (used for bash CLI vs script distinction)
|
| 1616 |
+
|
| 1617 |
+
Returns:
|
| 1618 |
+
HTML string containing a styled tools-used section
|
| 1619 |
+
|
| 1620 |
+
Note:
|
| 1621 |
+
For bash, it distinguishes between CLI commands and bash scripts
|
| 1622 |
+
based on the tool_name parameter.
|
| 1623 |
+
"""
|
| 1624 |
+
if language == "r":
|
| 1625 |
+
return """
|
| 1626 |
+
<div class="tools-used">
|
| 1627 |
+
<strong>Tools Used:</strong> R REPL
|
| 1628 |
+
</div>"""
|
| 1629 |
+
elif language == "bash":
|
| 1630 |
+
if tool_name == "CLI Command":
|
| 1631 |
+
return """
|
| 1632 |
+
<div class="tools-used">
|
| 1633 |
+
<strong>Tools Used:</strong> CLI Command
|
| 1634 |
+
</div>"""
|
| 1635 |
+
else:
|
| 1636 |
+
return """
|
| 1637 |
+
<div class="tools-used">
|
| 1638 |
+
<strong>Tools Used:</strong> Bash Script
|
| 1639 |
+
</div>"""
|
| 1640 |
+
else:
|
| 1641 |
+
return """
|
| 1642 |
+
<div class="tools-used">
|
| 1643 |
+
<strong>Tools Used:</strong> Python REPL
|
| 1644 |
+
</div>"""
|
| 1645 |
+
|
| 1646 |
+
|
| 1647 |
+
def format_solution_tags_in_content(content: str) -> str:
|
| 1648 |
+
"""Format solution tags in content by extracting text and formatting as solution blocks.
|
| 1649 |
+
|
| 1650 |
+
This function processes content that contains <solution>...</solution> tags and
|
| 1651 |
+
converts them into styled HTML blocks that display solution content with appropriate
|
| 1652 |
+
formatting and CSS classes.
|
| 1653 |
+
|
| 1654 |
+
Args:
|
| 1655 |
+
content: The content string that may contain <solution> tags
|
| 1656 |
+
|
| 1657 |
+
Returns:
|
| 1658 |
+
Formatted content with solution tags converted to styled solution blocks
|
| 1659 |
+
|
| 1660 |
+
Note:
|
| 1661 |
+
The solution blocks use the "title-text summary" CSS class for consistent
|
| 1662 |
+
styling with other content blocks in the markdown output.
|
| 1663 |
+
"""
|
| 1664 |
+
import re
|
| 1665 |
+
|
| 1666 |
+
# Pattern to match <solution>...</solution> blocks
|
| 1667 |
+
solution_pattern = r"<solution>(.*?)</solution>"
|
| 1668 |
+
|
| 1669 |
+
def replace_solution_tag(match):
|
| 1670 |
+
solution_content = match.group(1).strip()
|
| 1671 |
+
# Format as regular text, not terminal
|
| 1672 |
+
return f"""<div class="title-text summary">
|
| 1673 |
+
<div class="title-text-header">
|
| 1674 |
+
<strong>Summary and Solution</strong>
|
| 1675 |
+
</div>
|
| 1676 |
+
<div class="title-text-content">
|
| 1677 |
+
{solution_content}
|
| 1678 |
+
</div>
|
| 1679 |
+
</div>"""
|
| 1680 |
+
|
| 1681 |
+
# Replace all solution tags with formatted solution blocks
|
| 1682 |
+
formatted_content = re.sub(solution_pattern, replace_solution_tag, content, flags=re.DOTALL)
|
| 1683 |
+
|
| 1684 |
+
return formatted_content
|
| 1685 |
+
|
| 1686 |
+
|
| 1687 |
+
def format_observation_as_terminal(content: str) -> str | None:
|
| 1688 |
+
"""Format observation content with terminal-like styling.
|
| 1689 |
+
|
| 1690 |
+
This function processes observation content from the agent's execution results
|
| 1691 |
+
and formats it as a styled terminal block. It handles both text and image content,
|
| 1692 |
+
with length limits to ensure the output fits within PDF page constraints.
|
| 1693 |
+
|
| 1694 |
+
Args:
|
| 1695 |
+
content: The observation content string, potentially containing <observation> tags
|
| 1696 |
+
|
| 1697 |
+
Returns:
|
| 1698 |
+
Formatted HTML content with terminal styling, or None if observation is
|
| 1699 |
+
empty, invalid, or contains only meaningless content
|
| 1700 |
+
|
| 1701 |
+
Note:
|
| 1702 |
+
- Content is limited to 10,000 characters to fit within 2 A4 pages
|
| 1703 |
+
- Handles both text and base64-encoded images
|
| 1704 |
+
- Uses CSS classes for consistent styling with other content blocks
|
| 1705 |
+
"""
|
| 1706 |
+
import re
|
| 1707 |
+
|
| 1708 |
+
# Character limit for 2 A4 pages (approximately 10,000 characters)
|
| 1709 |
+
MAX_OBSERVATION_LENGTH = 10000
|
| 1710 |
+
|
| 1711 |
+
# Remove the <observation> tags and extract the content
|
| 1712 |
+
observation_pattern = r"<observation>(.*?)</observation>"
|
| 1713 |
+
observation_match = re.search(observation_pattern, content, re.DOTALL)
|
| 1714 |
+
|
| 1715 |
+
if observation_match:
|
| 1716 |
+
observation_content = observation_match.group(1).strip()
|
| 1717 |
+
else:
|
| 1718 |
+
# Fallback if no observation tags found - check if content is meaningful
|
| 1719 |
+
if not (content.strip() and content.strip() not in ["", "None", "null", "undefined"]):
|
| 1720 |
+
return None
|
| 1721 |
+
observation_content = content.strip()
|
| 1722 |
+
|
| 1723 |
+
# Skip empty observations
|
| 1724 |
+
if not observation_content or observation_content in ["", "None", "null", "undefined"]:
|
| 1725 |
+
return None
|
| 1726 |
+
|
| 1727 |
+
# Check if observation is too long for 2 pages
|
| 1728 |
+
if len(observation_content) > MAX_OBSERVATION_LENGTH:
|
| 1729 |
+
cropped_content = observation_content[:MAX_OBSERVATION_LENGTH]
|
| 1730 |
+
truncation_notice = f"\n\n[Output truncated - content was too long to display here ({len(observation_content)} characters total)]"
|
| 1731 |
+
observation_content = cropped_content + truncation_notice
|
| 1732 |
+
|
| 1733 |
+
# Check if it contains plot data (base64 images)
|
| 1734 |
+
if "data:image/" in observation_content:
|
| 1735 |
+
content_html = process_observation_with_images(observation_content)
|
| 1736 |
+
else:
|
| 1737 |
+
# Regular text output - format as terminal output
|
| 1738 |
+
content_html = f"```terminal\n{observation_content}\n```"
|
| 1739 |
+
|
| 1740 |
+
return f"""<div class="title-text observation">
|
| 1741 |
+
<div class="title-text-header">
|
| 1742 |
+
<strong>Observation</strong>
|
| 1743 |
+
</div>
|
| 1744 |
+
<div class="title-text-content">
|
| 1745 |
+
{content_html}
|
| 1746 |
+
</div>
|
| 1747 |
+
</div>"""
|
| 1748 |
+
|
| 1749 |
+
|
| 1750 |
+
def process_observation_with_images(observation_content: str) -> str:
|
| 1751 |
+
"""Process observation content that contains both text and base64-encoded images.
|
| 1752 |
+
|
| 1753 |
+
This function handles observation content that includes both text output and
|
| 1754 |
+
base64-encoded images (typically plots from data analysis). It separates the
|
| 1755 |
+
text and image content and formats them appropriately for markdown display.
|
| 1756 |
+
|
| 1757 |
+
Args:
|
| 1758 |
+
observation_content: The observation content containing both text and images
|
| 1759 |
+
|
| 1760 |
+
Returns:
|
| 1761 |
+
HTML string containing formatted text (as terminal blocks) and images
|
| 1762 |
+
(as markdown image tags)
|
| 1763 |
+
|
| 1764 |
+
Note:
|
| 1765 |
+
The function uses "data:image/" as a delimiter to split content into
|
| 1766 |
+
text and image parts, then processes each part separately.
|
| 1767 |
+
"""
|
| 1768 |
+
# Split content into text and image parts
|
| 1769 |
+
parts = observation_content.split("data:image/")
|
| 1770 |
+
text_parts = []
|
| 1771 |
+
image_parts = []
|
| 1772 |
+
|
| 1773 |
+
for i, part in enumerate(parts):
|
| 1774 |
+
if i == 0:
|
| 1775 |
+
# First part is text only
|
| 1776 |
+
if part.strip():
|
| 1777 |
+
text_parts.append(part.strip())
|
| 1778 |
+
else:
|
| 1779 |
+
# Find the end of the base64 data
|
| 1780 |
+
end_markers = ["\n", "\r", " ", "\t", ">", "<", "]", ")", "}"]
|
| 1781 |
+
image_end = len(part)
|
| 1782 |
+
for marker in end_markers:
|
| 1783 |
+
marker_pos = part.find(marker)
|
| 1784 |
+
if marker_pos != -1 and marker_pos < image_end:
|
| 1785 |
+
image_end = marker_pos
|
| 1786 |
+
|
| 1787 |
+
# Extract image data
|
| 1788 |
+
image_data = "data:image/" + part[:image_end]
|
| 1789 |
+
image_parts.append(image_data)
|
| 1790 |
+
|
| 1791 |
+
# Extract remaining text
|
| 1792 |
+
remaining_text = part[image_end:].strip()
|
| 1793 |
+
if remaining_text:
|
| 1794 |
+
text_parts.append(remaining_text)
|
| 1795 |
+
|
| 1796 |
+
# Build the content
|
| 1797 |
+
content_html = ""
|
| 1798 |
+
if text_parts:
|
| 1799 |
+
# Add text content as terminal output
|
| 1800 |
+
text_content = "\n".join(text_parts)
|
| 1801 |
+
content_html += f"```terminal\n{text_content}\n```\n\n"
|
| 1802 |
+
|
| 1803 |
+
if image_parts:
|
| 1804 |
+
# Add image content
|
| 1805 |
+
for image_data in image_parts:
|
| 1806 |
+
content_html += f"\n\n"
|
| 1807 |
+
|
| 1808 |
+
return content_html
|
| 1809 |
+
|
| 1810 |
+
|
| 1811 |
+
def remove_emojis_from_text(text: str) -> str:
|
| 1812 |
+
"""Remove emojis from text for markdown/PDF output.
|
| 1813 |
+
|
| 1814 |
+
This function removes common emojis used in the system prompt and configuration
|
| 1815 |
+
display from text content before it's converted to markdown or PDF. This ensures
|
| 1816 |
+
clean, professional output while preserving emojis in the console display.
|
| 1817 |
+
|
| 1818 |
+
Args:
|
| 1819 |
+
text: The text content that may contain emojis
|
| 1820 |
+
|
| 1821 |
+
Returns:
|
| 1822 |
+
Text content with emojis removed
|
| 1823 |
+
|
| 1824 |
+
Note:
|
| 1825 |
+
The function targets specific emojis used in the Biomni system:
|
| 1826 |
+
- 🔧 for tools
|
| 1827 |
+
- 📊 for data
|
| 1828 |
+
- ⚙️ for software
|
| 1829 |
+
- 📋 for configuration
|
| 1830 |
+
- 🤖 for agent
|
| 1831 |
+
"""
|
| 1832 |
+
import re
|
| 1833 |
+
|
| 1834 |
+
# Remove common emojis used in the system prompt, this makes conversion simpler
|
| 1835 |
+
emoji_patterns = [
|
| 1836 |
+
r"🔧\s*", # Tool emoji
|
| 1837 |
+
r"📊\s*", # Data emoji
|
| 1838 |
+
r"⚙️\s*", # Software emoji
|
| 1839 |
+
r"📋\s*", # Config emoji
|
| 1840 |
+
r"🤖\s*", # Agent emoji
|
| 1841 |
+
]
|
| 1842 |
+
|
| 1843 |
+
for pattern in emoji_patterns:
|
| 1844 |
+
text = re.sub(pattern, "", text)
|
| 1845 |
+
|
| 1846 |
+
return text
|
| 1847 |
+
|
| 1848 |
+
|
| 1849 |
+
def format_lists_in_text(text: str) -> str:
|
| 1850 |
+
"""Format numbered lists and bullet points in text to proper markdown format.
|
| 1851 |
+
|
| 1852 |
+
This function processes text content to identify and format various types of lists,
|
| 1853 |
+
including numbered lists with checkboxes, regular lists, and plan structures.
|
| 1854 |
+
It also handles preprocessing tasks like removing bold formatting from plan titles
|
| 1855 |
+
and removing emojis for clean PDF output.
|
| 1856 |
+
|
| 1857 |
+
Args:
|
| 1858 |
+
text: The text content to process for list formatting
|
| 1859 |
+
|
| 1860 |
+
Returns:
|
| 1861 |
+
Formatted text with properly structured lists and cleaned formatting
|
| 1862 |
+
|
| 1863 |
+
Note:
|
| 1864 |
+
The function performs several preprocessing steps:
|
| 1865 |
+
- Removes bold formatting from plan titles
|
| 1866 |
+
- Removes emojis for PDF output
|
| 1867 |
+
- Identifies and formats checkbox lists
|
| 1868 |
+
- Processes regular text blocks
|
| 1869 |
+
"""
|
| 1870 |
+
import re
|
| 1871 |
+
|
| 1872 |
+
# Preprocess to remove bold formatting from plan titles
|
| 1873 |
+
# Remove **Plan:**, **Updated Plan:**, **Completed Plan:**, etc.
|
| 1874 |
+
text = re.sub(r"\*\*([Pp]lan|Updated [Pp]lan|Completed [Pp]lan|Final [Pp]lan):\*\*", r"\1:", text)
|
| 1875 |
+
# Also handle cases without colons
|
| 1876 |
+
text = re.sub(r"\*\*([Pp]lan|Updated [Pp]lan|Completed [Pp]lan|Final [Pp]lan)\*\*", r"\1", text)
|
| 1877 |
+
# Handle any other bold formatting patterns for plan titles
|
| 1878 |
+
text = re.sub(r"<strong>([Pp]lan|Updated [Pp]lan|Completed [Pp]lan|Final [Pp]lan):</strong>", r"\1:", text)
|
| 1879 |
+
text = re.sub(r"<strong>([Pp]lan|Updated [Pp]lan|Completed [Pp]lan|Final [Pp]lan)</strong>", r"\1", text)
|
| 1880 |
+
|
| 1881 |
+
# Remove emojis from the text for markdown/PDF output
|
| 1882 |
+
text = remove_emojis_from_text(text)
|
| 1883 |
+
|
| 1884 |
+
lines = text.split("\n")
|
| 1885 |
+
list_blocks = identify_list_blocks(lines)
|
| 1886 |
+
|
| 1887 |
+
# Process each block
|
| 1888 |
+
result_blocks = []
|
| 1889 |
+
for block_text, is_checkbox_list in list_blocks:
|
| 1890 |
+
if is_checkbox_list:
|
| 1891 |
+
result_blocks.append(format_single_list(block_text))
|
| 1892 |
+
else:
|
| 1893 |
+
result_blocks.append(block_text)
|
| 1894 |
+
|
| 1895 |
+
return "\n".join(result_blocks)
|
| 1896 |
+
|
| 1897 |
+
|
| 1898 |
+
def identify_list_blocks(lines: list) -> list[tuple[str, bool]]:
|
| 1899 |
+
"""Identify blocks of text that contain lists.
|
| 1900 |
+
|
| 1901 |
+
This function analyzes a list of text lines to identify contiguous blocks
|
| 1902 |
+
that contain numbered lists with checkboxes. It groups lines into blocks
|
| 1903 |
+
and marks whether each block contains a checkbox list or regular text.
|
| 1904 |
+
|
| 1905 |
+
Args:
|
| 1906 |
+
lines: List of text lines to analyze
|
| 1907 |
+
|
| 1908 |
+
Returns:
|
| 1909 |
+
List of tuples containing (block_text, is_checkbox_list) where:
|
| 1910 |
+
- block_text: The text content of the block
|
| 1911 |
+
- is_checkbox_list: True if the block contains numbered items with checkboxes
|
| 1912 |
+
|
| 1913 |
+
Note:
|
| 1914 |
+
The function looks for patterns like "1. [ ]", "2. [✓]", "3. [✗]" to
|
| 1915 |
+
identify checkbox sequences and groups them into separate blocks.
|
| 1916 |
+
"""
|
| 1917 |
+
import re
|
| 1918 |
+
|
| 1919 |
+
list_blocks = []
|
| 1920 |
+
current_block = []
|
| 1921 |
+
in_checkbox_sequence = False
|
| 1922 |
+
|
| 1923 |
+
for line in lines:
|
| 1924 |
+
line_stripped = line.strip()
|
| 1925 |
+
|
| 1926 |
+
# Check if this line starts a numbered item with checkbox
|
| 1927 |
+
if re.match(r"^\d+\.\s*\[[ ✓✗]\]", line_stripped):
|
| 1928 |
+
if not in_checkbox_sequence:
|
| 1929 |
+
# Start of a new checkbox sequence
|
| 1930 |
+
if current_block:
|
| 1931 |
+
list_blocks.append(("\n".join(current_block), False))
|
| 1932 |
+
current_block = [line]
|
| 1933 |
+
in_checkbox_sequence = True
|
| 1934 |
+
else:
|
| 1935 |
+
# Continue the sequence
|
| 1936 |
+
current_block.append(line)
|
| 1937 |
+
else:
|
| 1938 |
+
if in_checkbox_sequence:
|
| 1939 |
+
# End of checkbox sequence
|
| 1940 |
+
if current_block:
|
| 1941 |
+
list_blocks.append(("\n".join(current_block), True))
|
| 1942 |
+
current_block = []
|
| 1943 |
+
in_checkbox_sequence = False
|
| 1944 |
+
current_block.append(line)
|
| 1945 |
+
|
| 1946 |
+
# Handle the last block
|
| 1947 |
+
if current_block:
|
| 1948 |
+
if in_checkbox_sequence:
|
| 1949 |
+
list_blocks.append(("\n".join(current_block), True))
|
| 1950 |
+
else:
|
| 1951 |
+
list_blocks.append(("\n".join(current_block), False))
|
| 1952 |
+
|
| 1953 |
+
return list_blocks
|
| 1954 |
+
|
| 1955 |
+
|
| 1956 |
+
def format_single_list(text: str) -> str:
|
| 1957 |
+
"""Format a single list block with checkboxes and plan titles.
|
| 1958 |
+
|
| 1959 |
+
This function processes a text block that may contain numbered lists with
|
| 1960 |
+
checkboxes and plan titles. It converts checkbox symbols to HTML list items
|
| 1961 |
+
and wraps the content in a styled container with appropriate CSS classes.
|
| 1962 |
+
|
| 1963 |
+
Args:
|
| 1964 |
+
text: The text block to format, potentially containing numbered lists
|
| 1965 |
+
|
| 1966 |
+
Returns:
|
| 1967 |
+
HTML string containing either a formatted list with plan title or
|
| 1968 |
+
regular text if no list items are found
|
| 1969 |
+
|
| 1970 |
+
Note:
|
| 1971 |
+
The function recognizes plan titles like "Plan", "Updated Plan", "Completed Plan"
|
| 1972 |
+
and converts checkbox symbols (✓, ✗) to HTML format ([x], [ ]).
|
| 1973 |
+
"""
|
| 1974 |
+
import re
|
| 1975 |
+
|
| 1976 |
+
lines = text.split("\n")
|
| 1977 |
+
list_items = []
|
| 1978 |
+
has_list_items = False
|
| 1979 |
+
plan_title = "Plan" # Default title
|
| 1980 |
+
|
| 1981 |
+
for line in lines:
|
| 1982 |
+
line = line.strip()
|
| 1983 |
+
if not line:
|
| 1984 |
+
continue
|
| 1985 |
+
|
| 1986 |
+
# Check for plan title patterns
|
| 1987 |
+
if re.match(r"^(Plan|Updated Plan|Completed Plan)$", line, re.IGNORECASE):
|
| 1988 |
+
plan_title = line
|
| 1989 |
+
continue
|
| 1990 |
+
|
| 1991 |
+
# Check for numbered lists with checkboxes (1. [ ] or 1. [✓] or 1. [✗])
|
| 1992 |
+
if re.match(r"^\d+\.\s*\[[ ✓✗]\]", line):
|
| 1993 |
+
has_list_items = True
|
| 1994 |
+
# Extract the content after the checkbox
|
| 1995 |
+
content = re.sub(r"^\d+\.\s*\[[ ✓✗]\]\s*", "", line)
|
| 1996 |
+
|
| 1997 |
+
# Replace checkbox symbols with text format
|
| 1998 |
+
if "[✓]" in line:
|
| 1999 |
+
list_items.append(f"<li><strong>[x]</strong> {content}</li>")
|
| 2000 |
+
elif "[✗]" in line:
|
| 2001 |
+
list_items.append(f"<li><strong>[ ]</strong> {content}</li>")
|
| 2002 |
+
else:
|
| 2003 |
+
list_items.append(f"<li><strong>[ ]</strong> {content}</li>")
|
| 2004 |
+
else:
|
| 2005 |
+
# Regular text - add as is (don't convert to list items)
|
| 2006 |
+
list_items.append(line)
|
| 2007 |
+
|
| 2008 |
+
if has_list_items and list_items:
|
| 2009 |
+
# This is a list - return with container div and styled title
|
| 2010 |
+
return f"""<div class="title-text plan">
|
| 2011 |
+
<div class="title-text-header">
|
| 2012 |
+
<span class="plan-title">{plan_title}</span>
|
| 2013 |
+
</div>
|
| 2014 |
+
<div class="title-text-content">
|
| 2015 |
+
<ul>
|
| 2016 |
+
{chr(10).join(list_items)}
|
| 2017 |
+
</ul>
|
| 2018 |
+
</div>
|
| 2019 |
+
</div>"""
|
| 2020 |
+
else:
|
| 2021 |
+
# Regular text
|
| 2022 |
+
return "\n".join(list_items)
|
| 2023 |
+
|
| 2024 |
+
|
| 2025 |
+
def convert_markdown_to_pdf(markdown_path: str, pdf_path: str) -> None:
|
| 2026 |
+
"""Convert markdown file to PDF using weasyprint or fallback libraries.
|
| 2027 |
+
|
| 2028 |
+
This function converts a markdown file to PDF format using multiple fallback
|
| 2029 |
+
strategies. It prioritizes weasyprint for better layout control, then falls back
|
| 2030 |
+
to markdown2pdf and finally pandoc if the preferred libraries are not available.
|
| 2031 |
+
|
| 2032 |
+
Args:
|
| 2033 |
+
markdown_path: Path to the input markdown file
|
| 2034 |
+
pdf_path: Path where the output PDF file should be saved
|
| 2035 |
+
|
| 2036 |
+
Raises:
|
| 2037 |
+
ImportError: If no PDF conversion library is available
|
| 2038 |
+
Exception: If PDF conversion fails for any other reason
|
| 2039 |
+
|
| 2040 |
+
Note:
|
| 2041 |
+
The function uses minimal markdown extensions for better performance
|
| 2042 |
+
and applies custom CSS styling for consistent formatting.
|
| 2043 |
+
"""
|
| 2044 |
+
try:
|
| 2045 |
+
# Try weasyprint first (better for complex layouts)
|
| 2046 |
+
from weasyprint import HTML
|
| 2047 |
+
from weasyprint.text.fonts import FontConfiguration
|
| 2048 |
+
|
| 2049 |
+
# Read markdown content
|
| 2050 |
+
with open(markdown_path, encoding="utf-8") as f:
|
| 2051 |
+
markdown_content = f.read()
|
| 2052 |
+
|
| 2053 |
+
# Convert markdown to HTML with minimal extensions for better performance
|
| 2054 |
+
import markdown
|
| 2055 |
+
|
| 2056 |
+
# Use minimal extensions to improve performance
|
| 2057 |
+
html_content = markdown.markdown(
|
| 2058 |
+
markdown_content,
|
| 2059 |
+
extensions=["fenced_code"], # Removed codehilite for better performance
|
| 2060 |
+
)
|
| 2061 |
+
|
| 2062 |
+
# Add CSS styling
|
| 2063 |
+
css_content = get_pdf_css_content()
|
| 2064 |
+
|
| 2065 |
+
# Create HTML document
|
| 2066 |
+
html_doc = f"""
|
| 2067 |
+
<!DOCTYPE html>
|
| 2068 |
+
<html>
|
| 2069 |
+
<head>
|
| 2070 |
+
<meta charset="utf-8">
|
| 2071 |
+
<title>Biomni Conversation History</title>
|
| 2072 |
+
<style>{css_content}</style>
|
| 2073 |
+
</head>
|
| 2074 |
+
<body>
|
| 2075 |
+
{html_content}
|
| 2076 |
+
</body>
|
| 2077 |
+
</html>
|
| 2078 |
+
"""
|
| 2079 |
+
|
| 2080 |
+
# Convert to PDF with performance optimizations
|
| 2081 |
+
font_config = FontConfiguration()
|
| 2082 |
+
html_obj = HTML(string=html_doc)
|
| 2083 |
+
html_obj.write_pdf(pdf_path, font_config=font_config, optimize_images=True)
|
| 2084 |
+
|
| 2085 |
+
except ImportError:
|
| 2086 |
+
# Fallback to markdown2pdf if weasyprint is not available
|
| 2087 |
+
try:
|
| 2088 |
+
from markdown2pdf import markdown2pdf
|
| 2089 |
+
|
| 2090 |
+
markdown2pdf(markdown_path, pdf_path)
|
| 2091 |
+
except ImportError:
|
| 2092 |
+
# Final fallback - try using pandoc if available
|
| 2093 |
+
import subprocess
|
| 2094 |
+
|
| 2095 |
+
try:
|
| 2096 |
+
subprocess.run(["pandoc", markdown_path, "-o", pdf_path], check=True)
|
| 2097 |
+
except (subprocess.CalledProcessError, FileNotFoundError) as e:
|
| 2098 |
+
raise ImportError(
|
| 2099 |
+
"No PDF conversion library available. Please install weasyprint, markdown2pdf, or pandoc."
|
| 2100 |
+
) from e
|
| 2101 |
+
except Exception as e:
|
| 2102 |
+
raise Exception(f"PDF conversion failed: {e}") from e
|
| 2103 |
+
|
| 2104 |
+
|
| 2105 |
+
def get_pdf_css_content() -> str:
|
| 2106 |
+
"""Get the CSS content for PDF generation.
|
| 2107 |
+
|
| 2108 |
+
This function returns a comprehensive CSS stylesheet designed specifically
|
| 2109 |
+
for PDF generation from markdown content. It includes styling for all
|
| 2110 |
+
HTML elements that may appear in the converted markdown, with optimized
|
| 2111 |
+
typography, spacing, and layout for print media.
|
| 2112 |
+
|
| 2113 |
+
Returns:
|
| 2114 |
+
CSS string containing all styles needed for PDF generation
|
| 2115 |
+
|
| 2116 |
+
Note:
|
| 2117 |
+
The CSS includes styles for:
|
| 2118 |
+
- Typography and font families
|
| 2119 |
+
- Headings and text formatting
|
| 2120 |
+
- Code blocks and syntax highlighting
|
| 2121 |
+
- Tables and lists
|
| 2122 |
+
- Custom classes for tool calls, observations, and plans
|
| 2123 |
+
- Print-optimized spacing and layout
|
| 2124 |
+
"""
|
| 2125 |
+
return """
|
| 2126 |
+
body {
|
| 2127 |
+
/* Previously: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif, 'Noto Color Emoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Twemoji', 'EmojiOne Color' */
|
| 2128 |
+
font-family: sans-serif;
|
| 2129 |
+
font-size: 9pt;
|
| 2130 |
+
line-height: 1.4;
|
| 2131 |
+
max-width: 800px;
|
| 2132 |
+
margin: 0 auto;
|
| 2133 |
+
padding: 15px;
|
| 2134 |
+
color: #333;
|
| 2135 |
+
}
|
| 2136 |
+
h1, h2, h3, h4, h5, h6 {
|
| 2137 |
+
/* Previously: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif, 'Noto Color Emoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Twemoji', 'EmojiOne Color' */
|
| 2138 |
+
font-family: sans-serif;
|
| 2139 |
+
color: #2c3e50;
|
| 2140 |
+
margin-top: 1em;
|
| 2141 |
+
margin-bottom: 0.5em;
|
| 2142 |
+
}
|
| 2143 |
+
h1 {
|
| 2144 |
+
border-bottom: 2px solid #3498db;
|
| 2145 |
+
padding-bottom: 8px;
|
| 2146 |
+
font-size: 16pt;
|
| 2147 |
+
}
|
| 2148 |
+
h2 {
|
| 2149 |
+
border-bottom: 1px solid #bdc3c7;
|
| 2150 |
+
padding-bottom: 3px;
|
| 2151 |
+
font-size: 14pt;
|
| 2152 |
+
}
|
| 2153 |
+
h3 {
|
| 2154 |
+
font-size: 12pt;
|
| 2155 |
+
}
|
| 2156 |
+
h4 {
|
| 2157 |
+
font-size: 10pt;
|
| 2158 |
+
margin-top: 0.8em;
|
| 2159 |
+
margin-bottom: 0.3em;
|
| 2160 |
+
}
|
| 2161 |
+
h5, h6 {
|
| 2162 |
+
font-size: 9pt;
|
| 2163 |
+
margin-top: 0.6em;
|
| 2164 |
+
margin-bottom: 0.2em;
|
| 2165 |
+
}
|
| 2166 |
+
code {
|
| 2167 |
+
background-color: #f8f9fa;
|
| 2168 |
+
padding: 1px 3px;
|
| 2169 |
+
border-radius: 2px;
|
| 2170 |
+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
| 2171 |
+
font-size: 8pt;
|
| 2172 |
+
white-space: pre-wrap;
|
| 2173 |
+
word-wrap: break-word;
|
| 2174 |
+
}
|
| 2175 |
+
pre {
|
| 2176 |
+
background-color: #f8f9fa;
|
| 2177 |
+
padding: 10px;
|
| 2178 |
+
border-radius: 3px;
|
| 2179 |
+
overflow-x: auto;
|
| 2180 |
+
border-left: 3px solid #3498db;
|
| 2181 |
+
white-space: pre-wrap;
|
| 2182 |
+
word-wrap: break-word;
|
| 2183 |
+
font-size: 8pt;
|
| 2184 |
+
margin: 0.5em 0;
|
| 2185 |
+
}
|
| 2186 |
+
pre code {
|
| 2187 |
+
background-color: transparent;
|
| 2188 |
+
padding: 0;
|
| 2189 |
+
border-radius: 0;
|
| 2190 |
+
font-size: 8pt;
|
| 2191 |
+
}
|
| 2192 |
+
/* Code header styling */
|
| 2193 |
+
strong {
|
| 2194 |
+
font-size: 9pt;
|
| 2195 |
+
font-weight: normal;
|
| 2196 |
+
color: #6c757d;
|
| 2197 |
+
font-style: italic;
|
| 2198 |
+
}
|
| 2199 |
+
blockquote {
|
| 2200 |
+
border-left: 3px solid #bdc3c7;
|
| 2201 |
+
margin: 0.5em 0;
|
| 2202 |
+
padding-left: 15px;
|
| 2203 |
+
color: #7f8c8d;
|
| 2204 |
+
font-size: 8pt;
|
| 2205 |
+
}
|
| 2206 |
+
table {
|
| 2207 |
+
border-collapse: collapse;
|
| 2208 |
+
width: 100%;
|
| 2209 |
+
margin: 0.5em 0;
|
| 2210 |
+
font-size: 8pt;
|
| 2211 |
+
}
|
| 2212 |
+
th, td {
|
| 2213 |
+
border: 1px solid #bdc3c7;
|
| 2214 |
+
padding: 4px 8px;
|
| 2215 |
+
text-align: left;
|
| 2216 |
+
}
|
| 2217 |
+
th {
|
| 2218 |
+
background-color: #ecf0f1;
|
| 2219 |
+
font-weight: bold;
|
| 2220 |
+
}
|
| 2221 |
+
img {
|
| 2222 |
+
max-width: 100%;
|
| 2223 |
+
height: auto;
|
| 2224 |
+
display: block;
|
| 2225 |
+
margin: 10px auto;
|
| 2226 |
+
border: 1px solid #ddd;
|
| 2227 |
+
border-radius: 3px;
|
| 2228 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
| 2229 |
+
}
|
| 2230 |
+
p {
|
| 2231 |
+
/* Previously: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif, 'Noto Color Emoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Twemoji', 'EmojiOne Color' */
|
| 2232 |
+
font-family: sans-serif;
|
| 2233 |
+
margin: 0.3em 0;
|
| 2234 |
+
}
|
| 2235 |
+
/* Tool call highlighting - matching observation and code formatting */
|
| 2236 |
+
.tool-call-highlight {
|
| 2237 |
+
background-color: #f8f9fa;
|
| 2238 |
+
border: 1px solid #e9ecef;
|
| 2239 |
+
border-radius: 3px;
|
| 2240 |
+
padding: 0;
|
| 2241 |
+
margin: 10px 0;
|
| 2242 |
+
overflow: hidden;
|
| 2243 |
+
}
|
| 2244 |
+
.tool-call-header {
|
| 2245 |
+
background-color: #e9ecef;
|
| 2246 |
+
color: #495057;
|
| 2247 |
+
padding: 8px 12px;
|
| 2248 |
+
margin: 0;
|
| 2249 |
+
font-weight: normal;
|
| 2250 |
+
font-size: 9pt;
|
| 2251 |
+
font-style: italic;
|
| 2252 |
+
border-bottom: 1px solid #dee2e6;
|
| 2253 |
+
}
|
| 2254 |
+
.tool-call-input {
|
| 2255 |
+
background-color: #f8f9fa;
|
| 2256 |
+
border: none;
|
| 2257 |
+
border-radius: 0;
|
| 2258 |
+
padding: 10px 12px;
|
| 2259 |
+
margin: 0;
|
| 2260 |
+
color: #333;
|
| 2261 |
+
font-size: 8pt;
|
| 2262 |
+
line-height: 1.4;
|
| 2263 |
+
}
|
| 2264 |
+
.tool-call-input strong {
|
| 2265 |
+
color: #495057;
|
| 2266 |
+
font-weight: normal;
|
| 2267 |
+
font-size: 8pt;
|
| 2268 |
+
font-style: italic;
|
| 2269 |
+
}
|
| 2270 |
+
.tool-call-input pre {
|
| 2271 |
+
background-color: #f8f9fa;
|
| 2272 |
+
border: 1px solid #e9ecef;
|
| 2273 |
+
border-radius: 3px;
|
| 2274 |
+
padding: 10px;
|
| 2275 |
+
margin: 0;
|
| 2276 |
+
font-size: 8pt;
|
| 2277 |
+
line-height: 1.4;
|
| 2278 |
+
overflow-x: auto;
|
| 2279 |
+
white-space: pre-wrap;
|
| 2280 |
+
word-wrap: break-word;
|
| 2281 |
+
}
|
| 2282 |
+
.tool-call-input code {
|
| 2283 |
+
background-color: transparent;
|
| 2284 |
+
padding: 0;
|
| 2285 |
+
border-radius: 0;
|
| 2286 |
+
font-size: 8pt;
|
| 2287 |
+
color: #2c3e50;
|
| 2288 |
+
}
|
| 2289 |
+
.tools-used {
|
| 2290 |
+
background-color: #f8f9fa;
|
| 2291 |
+
border-top: 1px solid #dee2e6;
|
| 2292 |
+
padding: 8px 12px;
|
| 2293 |
+
margin: 0;
|
| 2294 |
+
font-size: 8pt;
|
| 2295 |
+
color: #6c757d;
|
| 2296 |
+
}
|
| 2297 |
+
.tools-used strong {
|
| 2298 |
+
color: #6c757d;
|
| 2299 |
+
font-weight: normal;
|
| 2300 |
+
font-size: 8pt;
|
| 2301 |
+
font-style: italic;
|
| 2302 |
+
}
|
| 2303 |
+
/* Title-text styling - unified for observations, plans, and solutions */
|
| 2304 |
+
.title-text {
|
| 2305 |
+
background-color: #f8f9fa;
|
| 2306 |
+
border: 1px solid #e9ecef;
|
| 2307 |
+
border-radius: 3px;
|
| 2308 |
+
padding: 0;
|
| 2309 |
+
margin: 10px 0;
|
| 2310 |
+
overflow: hidden;
|
| 2311 |
+
}
|
| 2312 |
+
.title-text-header {
|
| 2313 |
+
background-color: #e9ecef;
|
| 2314 |
+
color: #495057;
|
| 2315 |
+
padding: 8px 12px;
|
| 2316 |
+
margin: 0;
|
| 2317 |
+
font-weight: normal;
|
| 2318 |
+
font-size: 9pt;
|
| 2319 |
+
font-style: italic;
|
| 2320 |
+
border-bottom: 1px solid #dee2e6;
|
| 2321 |
+
}
|
| 2322 |
+
.title-text-header strong {
|
| 2323 |
+
color: #495057;
|
| 2324 |
+
font-weight: normal;
|
| 2325 |
+
font-size: 9pt;
|
| 2326 |
+
font-style: italic;
|
| 2327 |
+
}
|
| 2328 |
+
.title-text-content {
|
| 2329 |
+
background-color: #f8f9fa;
|
| 2330 |
+
border: none;
|
| 2331 |
+
border-radius: 0;
|
| 2332 |
+
padding: 10px 12px;
|
| 2333 |
+
margin: 0;
|
| 2334 |
+
color: #333;
|
| 2335 |
+
font-size: 8pt;
|
| 2336 |
+
line-height: 1.4;
|
| 2337 |
+
}
|
| 2338 |
+
/* Plan-specific styling - soft blue pastel */
|
| 2339 |
+
.title-text.plan {
|
| 2340 |
+
background-color: #e3f2fd;
|
| 2341 |
+
border-color: #bbdefb;
|
| 2342 |
+
}
|
| 2343 |
+
.title-text.plan .title-text-header {
|
| 2344 |
+
background-color: #bbdefb;
|
| 2345 |
+
color: #1976d2;
|
| 2346 |
+
}
|
| 2347 |
+
.title-text.plan .title-text-content {
|
| 2348 |
+
background-color: #e3f2fd;
|
| 2349 |
+
}
|
| 2350 |
+
.plan-title {
|
| 2351 |
+
font-style: italic;
|
| 2352 |
+
font-weight: normal;
|
| 2353 |
+
color: #1565c0;
|
| 2354 |
+
text-shadow: 0 1px 2px rgba(0,0,0,0.1);
|
| 2355 |
+
}
|
| 2356 |
+
.plan-title strong {
|
| 2357 |
+
font-weight: normal;
|
| 2358 |
+
}
|
| 2359 |
+
/* Code execution-specific styling - matching title-text styling */
|
| 2360 |
+
.tool-call-highlight {
|
| 2361 |
+
background-color: #f8f9fa;
|
| 2362 |
+
border-color: #e9ecef;
|
| 2363 |
+
}
|
| 2364 |
+
.tool-call-header {
|
| 2365 |
+
background-color: #e9ecef;
|
| 2366 |
+
color: #495057;
|
| 2367 |
+
}
|
| 2368 |
+
.tool-call-input {
|
| 2369 |
+
background-color: #f8f9fa;
|
| 2370 |
+
color: #333;
|
| 2371 |
+
}
|
| 2372 |
+
/* Observation-specific styling - soft purple pastel */
|
| 2373 |
+
.title-text.observation {
|
| 2374 |
+
background-color: #f3e5f5;
|
| 2375 |
+
border-color: #e1bee7;
|
| 2376 |
+
}
|
| 2377 |
+
.title-text.observation .title-text-header {
|
| 2378 |
+
background-color: #e1bee7;
|
| 2379 |
+
color: #7b1fa2;
|
| 2380 |
+
}
|
| 2381 |
+
.title-text.observation .title-text-content {
|
| 2382 |
+
background-color: #f3e5f5;
|
| 2383 |
+
}
|
| 2384 |
+
/* Summary and solution-specific styling - soft orange pastel, no overlay */
|
| 2385 |
+
.title-text.summary {
|
| 2386 |
+
background-color: #fff3e0;
|
| 2387 |
+
border-color: #ffcc02;
|
| 2388 |
+
}
|
| 2389 |
+
.title-text.summary .title-text-header {
|
| 2390 |
+
background-color: #ffcc02;
|
| 2391 |
+
color: #f57c00;
|
| 2392 |
+
}
|
| 2393 |
+
.title-text.summary .title-text-content {
|
| 2394 |
+
background-color: #fff3e0;
|
| 2395 |
+
}
|
| 2396 |
+
.title-text-content ul {
|
| 2397 |
+
background-color: transparent;
|
| 2398 |
+
border: none;
|
| 2399 |
+
border-radius: 0;
|
| 2400 |
+
padding: 0;
|
| 2401 |
+
margin: 0;
|
| 2402 |
+
color: #333;
|
| 2403 |
+
font-size: 8pt;
|
| 2404 |
+
line-height: 1.4;
|
| 2405 |
+
}
|
| 2406 |
+
.title-text-content li {
|
| 2407 |
+
margin: 3px 0;
|
| 2408 |
+
color: #333;
|
| 2409 |
+
}
|
| 2410 |
+
.title-text-content li strong {
|
| 2411 |
+
color: #495057;
|
| 2412 |
+
font-weight: normal;
|
| 2413 |
+
font-size: 8pt;
|
| 2414 |
+
font-style: italic;
|
| 2415 |
+
}
|
| 2416 |
+
.title-text-content li code {
|
| 2417 |
+
background-color: #e9ecef;
|
| 2418 |
+
color: #333;
|
| 2419 |
+
padding: 1px 3px;
|
| 2420 |
+
border-radius: 2px;
|
| 2421 |
+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
| 2422 |
+
font-size: 7pt;
|
| 2423 |
+
}
|
| 2424 |
+
.title-text-content pre {
|
| 2425 |
+
background-color: #f8f9fa;
|
| 2426 |
+
border: 1px solid #e9ecef;
|
| 2427 |
+
border-radius: 3px;
|
| 2428 |
+
padding: 10px;
|
| 2429 |
+
margin: 0;
|
| 2430 |
+
font-size: 8pt;
|
| 2431 |
+
line-height: 1.4;
|
| 2432 |
+
overflow-x: auto;
|
| 2433 |
+
white-space: pre-wrap;
|
| 2434 |
+
word-wrap: break-word;
|
| 2435 |
+
}
|
| 2436 |
+
.title-text-content code {
|
| 2437 |
+
background-color: transparent;
|
| 2438 |
+
padding: 0;
|
| 2439 |
+
border-radius: 0;
|
| 2440 |
+
font-size: 8pt;
|
| 2441 |
+
color: #2c3e50;
|
| 2442 |
+
}
|
| 2443 |
+
/* Parsing error display styling */
|
| 2444 |
+
.parsing-error-box {
|
| 2445 |
+
background-color: #ffebee;
|
| 2446 |
+
border: 1px solid #f44336;
|
| 2447 |
+
border-radius: 4px;
|
| 2448 |
+
padding: 8px 12px;
|
| 2449 |
+
margin: 8px 0;
|
| 2450 |
+
font-size: 9pt;
|
| 2451 |
+
color: #c62828;
|
| 2452 |
+
box-shadow: 0 2px 4px rgba(244, 67, 54, 0.1);
|
| 2453 |
+
}
|
| 2454 |
+
.parsing-error-header {
|
| 2455 |
+
font-weight: bold;
|
| 2456 |
+
margin-bottom: 4px;
|
| 2457 |
+
color: #d32f2f;
|
| 2458 |
+
}
|
| 2459 |
+
.parsing-error-content {
|
| 2460 |
+
font-family: 'Courier New', monospace;
|
| 2461 |
+
background-color: #ffcdd2;
|
| 2462 |
+
padding: 4px 6px;
|
| 2463 |
+
border-radius: 2px;
|
| 2464 |
+
margin-top: 4px;
|
| 2465 |
+
font-size: 8pt;
|
| 2466 |
+
white-space: pre-wrap;
|
| 2467 |
+
word-wrap: break-word;
|
| 2468 |
+
}
|
| 2469 |
+
"""
|
Biomni/biomni/version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
__version__ = "0.0.8" # pragma: no cover
|
Biomni/license_info.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Biomni Data Source License Information
|
| 2 |
+
|
| 3 |
+
This document provides an overview of the data sources used by Biomni, their licenses, and suitability for internal hosting and commercial use.
|
| 4 |
+
|
| 5 |
+
A significant portion of the data used in Biomni requires a commercial license for any commercial application. Several datasets are explicitly licensed for non-commercial use only, which would prohibit their use in a commercial product without a separate agreement. Before proceeding with any commercial use, a thorough legal review of the licenses for each dataset you intend to use is strongly recommended.
|
| 6 |
+
|
| 7 |
+
## Data License and Internal Hosting Analysis
|
| 8 |
+
|
| 9 |
+
| Data Source Category | Example Files | License | Internal Hosting | Source |
|
| 10 |
+
| :--- | :--- | :--- | :--- | :--- |
|
| 11 |
+
| **COSMIC** | `Cosmic_*.csv`, `Cosmic_*.parquet` | Requires commercial license for commercial use. | Yes, with a valid commercial license. | [Sanger Institute](https://cancer.sanger.ac.uk/cosmic) |
|
| 12 |
+
| **BindingDB** | `BindingDB_All_202409.tsv` | Custom, non-commercial use granted. Commercial use requires a license. | Yes, with a commercial license. | [BindingDB](https://www.bindingdb.org) |
|
| 13 |
+
| **Broad Repurposing Hub** | `broad_repurposing_hub_*.parquet` | CC BY 4.0 | Yes | [Broad Institute](https://www.broadinstitute.org/drug-repurposing-hub) |
|
| 14 |
+
| **DDInter** | `ddinter_*.csv` | CC BY-NC-SA 4.0 | No, non-commercial use only. | [DDInter](http://ddinter.scbdd.com/) |
|
| 15 |
+
| **DisGeNET** | `DisGeNET.parquet` | CC BY-NC-SA 4.0 | No, non-commercial use only. | [DisGeNET](https://www.disgenet.org/) |
|
| 16 |
+
| **Enamine** | `enamine_cloud_library_smiles.pkl` | Proprietary. Requires license for screening. | Yes, with a valid license. | [Enamine](https://enamine.net/) |
|
| 17 |
+
| **EveBio** | `evebio_*.csv` | Appears to be proprietary data from EveBio. | Requires permission from EveBio. | EveBio |
|
| 18 |
+
| **Gene Ontology (GO)** | `go-plus.json` | CC BY 4.0 | Yes | [Gene Ontology Consortium](http://geneontology.org/) |
|
| 19 |
+
| **GTEx** | `gtex_tissue_gene_tpm.parquet` | dbGaP controlled access. | Yes, with authorization. | [GTEx Portal](https://gtexportal.org/) |
|
| 20 |
+
| **Human Protein Atlas** | `proteinatlas.tsv` | CC BY-SA 3.0 | Yes | [Human Protein Atlas](https://www.proteinatlas.org/) |
|
| 21 |
+
| **MSigDB** | `msigdb_human_*.parquet` | Custom, requires license for commercial use. | Yes, with a license. | [Broad Institute](https://www.gsea-msigdb.org/gsea/msigdb) |
|
| 22 |
+
| **OMIM** | `omim.parquet` | Custom, requires license for commercial use. | Yes, with a license. | [OMIM](https://omim.org/) |
|
| 23 |
+
| **BioGRID** | `affinity_capture-ms.parquet`, etc. | OSL 3.0 | Yes | [BioGRID](https://thebiogrid.org/) |
|
| 24 |
+
| **CZI Cell Census** | `czi_census_datasets_v4.parquet` | CC BY 4.0 | Yes | [Chan Zuckerberg Initiative](https://cellxgene.cziscience.com/census) |
|
| 25 |
+
| **DepMap** | `DepMap_*.csv` | CC BY 4.0 | Yes | [Broad Institute DepMap](https://depmap.org/) |
|
| 26 |
+
| **Genebass** | `genebass_*.pkl` | ODC-By v1.0 | Yes | [Genebass](https://genebass.org/) |
|
| 27 |
+
| **GWAS Catalog** | `gwas_catalog.pkl` | Apache 2.0 | Yes | [EBI GWAS Catalog](https://www.ebi.ac.uk/gwas/) |
|
| 28 |
+
| **HPO** | `hp.obo` | Custom, free for all uses. | Yes | [Human Phenotype Ontology](https://hpo.jax.org/) |
|
| 29 |
+
| **McPAS-TCR** | `McPAS-TCR.parquet` | CC BY-NC-SA 4.0 | No, non-commercial use only. | [McPAS-TCR](http://friedmanlab.weizmann.ac.il/McPAS-TCR/) |
|
| 30 |
+
| **miRDB** | `miRDB_v6.0_results.parquet` | Custom, free for non-commercial use. | No, non-commercial use only. | [miRDB](http://mirdb.org/) |
|
| 31 |
+
| **miRTarBase** | `miRTarBase_*.parquet` | CC BY-NC 4.0 | No, non-commercial use only. | [miRTarBase](https://mirtarbase.cuhk.edu.cn/) |
|
| 32 |
+
| **MouseMine** | `mousemine_*.parquet` | CC BY 4.0 | Yes | [MouseMine](http://www.mousemine.org/) |
|
| 33 |
+
| **P-HIPSTER** | `Virus-Host_PPI_P-HIPSTER_2020.parquet` | CC BY 4.0 | Yes | [P-HIPSTER](http://phipster.org) |
|
| 34 |
+
| **TXGNN** | `txgnn_*.pkl` | MIT License | Yes | - |
|
| 35 |
+
|
| 36 |
+
## Configuration for Commercial Use
|
| 37 |
+
|
| 38 |
+
To manage which datasets are used based on licensing, Biomni provides a configuration option. You can set the `commercial_mode` flag to `True` in your configuration to automatically exclude datasets that are not licensed for commercial use.
|
| 39 |
+
|
| 40 |
+
### Usage
|
| 41 |
+
|
| 42 |
+
```python
|
| 43 |
+
from biomni.agent import A1
|
| 44 |
+
|
| 45 |
+
# For commercial use (excludes non-commercial datasets)
|
| 46 |
+
agent = A1(commercial_mode=True)
|
| 47 |
+
|
| 48 |
+
# For academic/research use (includes all datasets)
|
| 49 |
+
agent = A1(commercial_mode=False) # default
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
This configuration automatically selects the appropriate data environment description file and ensures compliance with licensing requirements.
|
Biomni/pyproject.toml
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[build-system]
|
| 2 |
+
requires = ["setuptools>=61.0", "wheel"]
|
| 3 |
+
build-backend = "setuptools.build_meta"
|
| 4 |
+
|
| 5 |
+
[project]
|
| 6 |
+
name = "biomni"
|
| 7 |
+
dynamic = ["version"]
|
| 8 |
+
description = "Biomni"
|
| 9 |
+
readme = "README.md"
|
| 10 |
+
license = "Apache-2.0"
|
| 11 |
+
license-files = ["LICENSE"]
|
| 12 |
+
authors = [
|
| 13 |
+
{name = "Biomni Team", email = "kexinh@cs.stanford.edu"}
|
| 14 |
+
]
|
| 15 |
+
requires-python = ">=3.11"
|
| 16 |
+
dependencies = ["pydantic", "langchain", "python-dotenv"]
|
| 17 |
+
|
| 18 |
+
[project.urls]
|
| 19 |
+
Homepage = "https://github.com/snap-stanford/biomni"
|
| 20 |
+
Repository = "https://github.com/snap-stanford/biomni"
|
| 21 |
+
|
| 22 |
+
[project.optional-dependencies]
|
| 23 |
+
gradio = ["gradio>=5.0,<6.0"]
|
| 24 |
+
|
| 25 |
+
[tool.setuptools]
|
| 26 |
+
include-package-data = true
|
| 27 |
+
|
| 28 |
+
[tool.setuptools.dynamic]
|
| 29 |
+
version = {attr = "biomni.version.__version__"}
|
| 30 |
+
|
| 31 |
+
[tool.setuptools.packages.find]
|
| 32 |
+
exclude = ["test*", "tutorials*"]
|
| 33 |
+
|
| 34 |
+
[tool.ruff]
|
| 35 |
+
src = ["src"]
|
| 36 |
+
line-length = 120
|
| 37 |
+
|
| 38 |
+
[tool.ruff.format]
|
| 39 |
+
docstring-code-format = true
|
| 40 |
+
|
| 41 |
+
[tool.ruff.lint]
|
| 42 |
+
select = [
|
| 43 |
+
"F", # Errors detected by Pyflakes
|
| 44 |
+
"E", # Error detected by Pycodestyle
|
| 45 |
+
"W", # Warning detected by Pycodestyle
|
| 46 |
+
"I", # isort
|
| 47 |
+
#"D", # pydocstyle
|
| 48 |
+
"B", # flake8-bugbear
|
| 49 |
+
"TID", # flake8-tidy-imports
|
| 50 |
+
"C4", # flake8-comprehensions
|
| 51 |
+
"BLE", # flake8-blind-except
|
| 52 |
+
"UP", # pyupgrade
|
| 53 |
+
"RUF100", # Report unused noqa directives
|
| 54 |
+
"TCH", # Typing imports
|
| 55 |
+
#"NPY", # Numpy specific rules
|
| 56 |
+
#"PTH", # Use pathlib
|
| 57 |
+
#"PL" # pylint
|
| 58 |
+
]
|
| 59 |
+
ignore = [
|
| 60 |
+
# line too long -> we accept long comment lines; black gets rid of long code lines
|
| 61 |
+
"E501",
|
| 62 |
+
# Do not assign a lambda expression, use a def -> lambda expression assignments are convenient
|
| 63 |
+
"E731",
|
| 64 |
+
# allow I, O, l as variable names -> I is the identity matrix
|
| 65 |
+
"E741",
|
| 66 |
+
# Missing docstring in public package
|
| 67 |
+
"D104",
|
| 68 |
+
# Missing docstring in public module
|
| 69 |
+
"D100",
|
| 70 |
+
# Missing docstring in __init__
|
| 71 |
+
"D107",
|
| 72 |
+
# Errors from function calls in argument defaults. These are fine when the result is immutable.
|
| 73 |
+
"B008",
|
| 74 |
+
# __magic__ methods are are often self-explanatory, allow missing docstrings
|
| 75 |
+
"D105",
|
| 76 |
+
# first line should end with a period [Bug: doesn't work with single-line docstrings]
|
| 77 |
+
"D400",
|
| 78 |
+
# First line should be in imperative mood; try rephrasing
|
| 79 |
+
"D401",
|
| 80 |
+
## Disable one in each pair of mutually incompatible rules
|
| 81 |
+
# We don’t want a blank line before a class docstring
|
| 82 |
+
"D203",
|
| 83 |
+
# We want docstrings to start immediately after the opening triple quote
|
| 84 |
+
"D213",
|
| 85 |
+
# camcelcase imported as lowercase
|
| 86 |
+
"N813",
|
| 87 |
+
# module import not at top level of file
|
| 88 |
+
"E402",
|
| 89 |
+
# Too many arguments in function definition
|
| 90 |
+
"PLR0913",
|
| 91 |
+
# Too many branches
|
| 92 |
+
"PLR0912",
|
| 93 |
+
# magic value used in comparison
|
| 94 |
+
"PLR2004",
|
| 95 |
+
# Too many statements
|
| 96 |
+
"PLR0915",
|
| 97 |
+
# Extracting value from dictionary without calling `.items()` - clashes with sim
|
| 98 |
+
"PLC0206",
|
| 99 |
+
# import should be at top of file
|
| 100 |
+
"PLC0415",
|
| 101 |
+
# do not catch blind exception
|
| 102 |
+
"BLE001",
|
| 103 |
+
# strip with multi characters
|
| 104 |
+
"B005"
|
| 105 |
+
]
|
cloningscenarios_all_models_cloning_20260524_163903.log
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
RUN_ID=cloning_20260524_163903
|
| 2 |
+
Logs: /225040511/project/cloningscenarios_logs/cloning_20260524_163903
|
| 3 |
+
Biomanus PID: 115392
|
| 4 |
+
Biomni scales PID: 115393
|
| 5 |
+
Biomni-ReAct PID: 115394
|
| 6 |
+
React+code PID: 115395
|
| 7 |
+
All CloningScenarios jobs finished for RUN_ID=cloning_20260524_163903
|
ollama.log
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
nohup: ignoring input
|
| 2 |
+
time=2025-12-21T13:03:10.395Z level=INFO source=routes.go:1554 msg="server config" env="map[CUDA_VISIBLE_DEVICES: GGML_VK_VISIBLE_DEVICES: GPU_DEVICE_ORDINAL: HIP_VISIBLE_DEVICES: HSA_OVERRIDE_GFX_VERSION: HTTPS_PROXY: HTTP_PROXY: NO_PROXY: OLLAMA_CONTEXT_LENGTH:4096 OLLAMA_DEBUG:INFO OLLAMA_FLASH_ATTENTION:false OLLAMA_GPU_OVERHEAD:0 OLLAMA_HOST:http://127.0.0.1:11434 OLLAMA_KEEP_ALIVE:5m0s OLLAMA_KV_CACHE_TYPE: OLLAMA_LLM_LIBRARY: OLLAMA_LOAD_TIMEOUT:5m0s OLLAMA_MAX_LOADED_MODELS:0 OLLAMA_MAX_QUEUE:512 OLLAMA_MODELS:/225040511/project/ollama/models OLLAMA_MULTIUSER_CACHE:false OLLAMA_NEW_ENGINE:false OLLAMA_NOHISTORY:false OLLAMA_NOPRUNE:false OLLAMA_NUM_PARALLEL:1 OLLAMA_ORIGINS:[http://localhost https://localhost http://localhost:* https://localhost:* http://127.0.0.1 https://127.0.0.1 http://127.0.0.1:* https://127.0.0.1:* http://0.0.0.0 https://0.0.0.0 http://0.0.0.0:* https://0.0.0.0:* app://* file://* tauri://* vscode-webview://* vscode-file://*] OLLAMA_REMOTES:[ollama.com] OLLAMA_SCHED_SPREAD:false OLLAMA_VULKAN:false ROCR_VISIBLE_DEVICES: http_proxy: https_proxy: no_proxy:]"
|
| 3 |
+
time=2025-12-21T13:03:10.395Z level=INFO source=images.go:493 msg="total blobs: 0"
|
| 4 |
+
time=2025-12-21T13:03:10.396Z level=INFO source=images.go:500 msg="total unused blobs removed: 0"
|
| 5 |
+
time=2025-12-21T13:03:10.396Z level=INFO source=routes.go:1607 msg="Listening on 127.0.0.1:11434 (version 0.13.5)"
|
| 6 |
+
time=2025-12-21T13:03:10.396Z level=INFO source=runner.go:67 msg="discovering available GPUs..."
|
| 7 |
+
time=2025-12-21T13:03:10.397Z level=INFO source=server.go:429 msg="starting runner" cmd="/usr/local/bin/ollama runner --ollama-engine --port 41421"
|
| 8 |
+
time=2025-12-21T13:03:10.589Z level=INFO source=server.go:429 msg="starting runner" cmd="/usr/local/bin/ollama runner --ollama-engine --port 39047"
|
| 9 |
+
time=2025-12-21T13:03:10.687Z level=INFO source=runner.go:106 msg="experimental Vulkan support disabled. To enable, set OLLAMA_VULKAN=1"
|
| 10 |
+
time=2025-12-21T13:03:10.687Z level=INFO source=server.go:429 msg="starting runner" cmd="/usr/local/bin/ollama runner --ollama-engine --port 46809"
|
| 11 |
+
time=2025-12-21T13:03:10.863Z level=INFO source=types.go:42 msg="inference compute" id=GPU-a680880b-cb7f-062f-fa43-501d14dbbd89 filter_id="" library=CUDA compute=8.9 name=CUDA0 description="NVIDIA GeForce RTX 4090" libdirs=ollama,cuda_v12 driver=12.8 pci_id=0000:27:00.0 type=discrete total="48.0 GiB" available="47.4 GiB"
|
| 12 |
+
[GIN] 2025/12/21 - 13:03:21 | 200 | 66.064µs | 127.0.0.1 | HEAD "/"
|
| 13 |
+
[GIN] 2025/12/21 - 13:03:21 | 200 | 522.445µs | 127.0.0.1 | GET "/api/tags"
|
rerun_base_deepseek_cloningscenarios_50shards.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import os
|
| 5 |
+
import subprocess
|
| 6 |
+
import time
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
PROJECT_ROOT = Path("/225040511/project")
|
| 11 |
+
PYTHON_BIN = Path(os.getenv("LAB_BENCH_RUNNER_PYTHON", "/225040511/miniconda3/envs/biomni_e1/bin/python"))
|
| 12 |
+
RUNNER = PROJECT_ROOT / "react_code_bioagent_deepseek/react_code_bioagent/labbench_runner.py"
|
| 13 |
+
OUT = Path(
|
| 14 |
+
os.getenv(
|
| 15 |
+
"OUT",
|
| 16 |
+
str(PROJECT_ROOT / "base_llm_labbench/results/cloningscenarios_base_deepseek_cloning_50shards_20260524_174548"),
|
| 17 |
+
)
|
| 18 |
+
)
|
| 19 |
+
SHARD_COUNT = int(os.getenv("SHARD_COUNT", "50"))
|
| 20 |
+
SHARD_CONCURRENCY = int(os.getenv("SHARD_CONCURRENCY", "5"))
|
| 21 |
+
SEED = os.getenv("SEED", "20260514")
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def load_env_file(path: Path) -> None:
|
| 25 |
+
if not path.exists():
|
| 26 |
+
return
|
| 27 |
+
for raw in path.read_text(encoding="utf-8", errors="replace").splitlines():
|
| 28 |
+
line = raw.strip()
|
| 29 |
+
if not line or line.startswith("#") or "=" not in line:
|
| 30 |
+
continue
|
| 31 |
+
if line.startswith("export "):
|
| 32 |
+
line = line[len("export ") :].strip()
|
| 33 |
+
key, value = line.split("=", 1)
|
| 34 |
+
os.environ.setdefault(key.strip(), value.strip().strip('"').strip("'"))
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def count_rows(path: Path) -> int:
|
| 38 |
+
if not path.exists():
|
| 39 |
+
return 0
|
| 40 |
+
return sum(1 for _ in path.open("r", encoding="utf-8", errors="replace"))
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def main() -> int:
|
| 44 |
+
for env_file in [
|
| 45 |
+
PROJECT_ROOT / ".env",
|
| 46 |
+
PROJECT_ROOT / "LAB-Bench/.env",
|
| 47 |
+
PROJECT_ROOT / "base_llm_labbench/.env",
|
| 48 |
+
]:
|
| 49 |
+
load_env_file(env_file)
|
| 50 |
+
|
| 51 |
+
env = os.environ.copy()
|
| 52 |
+
env.setdefault("DEEPSEEK_BASE_URL", "https://api.deepseek.com/v1")
|
| 53 |
+
env.setdefault("DEEPSEEK_MODEL_NAME", "deepseek-chat")
|
| 54 |
+
|
| 55 |
+
OUT.mkdir(parents=True, exist_ok=True)
|
| 56 |
+
shard_log_dir = OUT / "shard_logs"
|
| 57 |
+
shard_log_dir.mkdir(parents=True, exist_ok=True)
|
| 58 |
+
|
| 59 |
+
result_file = OUT / "cloningscenarios_results.jsonl"
|
| 60 |
+
reasoning_log = OUT / "cloningscenarios_reasoning.log"
|
| 61 |
+
result_file.write_text("", encoding="utf-8")
|
| 62 |
+
reasoning_log.write_text("", encoding="utf-8")
|
| 63 |
+
for old_log in shard_log_dir.glob("base_deepseek_cloningscenarios_shard*.log"):
|
| 64 |
+
old_log.unlink()
|
| 65 |
+
|
| 66 |
+
(OUT / "run_config.txt").write_text(
|
| 67 |
+
"\n".join(
|
| 68 |
+
[
|
| 69 |
+
"model=base DeepSeek",
|
| 70 |
+
f"deepseek_model={env['DEEPSEEK_MODEL_NAME']}",
|
| 71 |
+
"eval=CloningScenarios",
|
| 72 |
+
"dev_size=0",
|
| 73 |
+
"test_size=33",
|
| 74 |
+
f"shard_count={SHARD_COUNT}",
|
| 75 |
+
f"shard_concurrency={SHARD_CONCURRENCY}",
|
| 76 |
+
f"seed={SEED}",
|
| 77 |
+
"",
|
| 78 |
+
]
|
| 79 |
+
),
|
| 80 |
+
encoding="utf-8",
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
print(f"output={OUT}", flush=True)
|
| 84 |
+
print(f"model={env['DEEPSEEK_MODEL_NAME']}", flush=True)
|
| 85 |
+
print(f"shard_count={SHARD_COUNT}", flush=True)
|
| 86 |
+
print(f"shard_concurrency={SHARD_CONCURRENCY}", flush=True)
|
| 87 |
+
|
| 88 |
+
running: list[tuple[int, subprocess.Popen[bytes], object]] = []
|
| 89 |
+
failures = 0
|
| 90 |
+
next_shard = 0
|
| 91 |
+
|
| 92 |
+
while next_shard < SHARD_COUNT or running:
|
| 93 |
+
while next_shard < SHARD_COUNT and len(running) < SHARD_CONCURRENCY:
|
| 94 |
+
shard = next_shard
|
| 95 |
+
next_shard += 1
|
| 96 |
+
label = f"base_deepseek_cloningscenarios_shard{shard + 1:02d}of{SHARD_COUNT:02d}"
|
| 97 |
+
log_handle = (shard_log_dir / f"{label}.log").open("wb")
|
| 98 |
+
cmd = [
|
| 99 |
+
str(PYTHON_BIN),
|
| 100 |
+
str(RUNNER),
|
| 101 |
+
"--evals",
|
| 102 |
+
"CloningScenarios",
|
| 103 |
+
"--splits",
|
| 104 |
+
"test",
|
| 105 |
+
"--dev-size",
|
| 106 |
+
"0",
|
| 107 |
+
"--test-size",
|
| 108 |
+
"33",
|
| 109 |
+
"--seed",
|
| 110 |
+
SEED,
|
| 111 |
+
"--n-threads",
|
| 112 |
+
"1",
|
| 113 |
+
"--shard-index",
|
| 114 |
+
str(shard),
|
| 115 |
+
"--shard-count",
|
| 116 |
+
str(SHARD_COUNT),
|
| 117 |
+
"--output-root",
|
| 118 |
+
str(OUT),
|
| 119 |
+
"--compact-results-path",
|
| 120 |
+
str(result_file),
|
| 121 |
+
"--reasoning-log-path",
|
| 122 |
+
str(reasoning_log),
|
| 123 |
+
"--skip-existing-results",
|
| 124 |
+
"--model",
|
| 125 |
+
env["DEEPSEEK_MODEL_NAME"],
|
| 126 |
+
"--base-url",
|
| 127 |
+
env["DEEPSEEK_BASE_URL"],
|
| 128 |
+
]
|
| 129 |
+
proc = subprocess.Popen(cmd, stdout=log_handle, stderr=subprocess.STDOUT, env=env)
|
| 130 |
+
running.append((shard, proc, log_handle))
|
| 131 |
+
print(f"started shard={shard + 1}/{SHARD_COUNT} pid={proc.pid}", flush=True)
|
| 132 |
+
|
| 133 |
+
time.sleep(2)
|
| 134 |
+
still_running: list[tuple[int, subprocess.Popen[bytes], object]] = []
|
| 135 |
+
for shard, proc, log_handle in running:
|
| 136 |
+
rc = proc.poll()
|
| 137 |
+
if rc is None:
|
| 138 |
+
still_running.append((shard, proc, log_handle))
|
| 139 |
+
continue
|
| 140 |
+
log_handle.close()
|
| 141 |
+
if rc != 0:
|
| 142 |
+
failures += 1
|
| 143 |
+
print(f"finished shard={shard + 1}/{SHARD_COUNT} rc={rc}", flush=True)
|
| 144 |
+
running = still_running
|
| 145 |
+
print(f"progress rows={count_rows(result_file)} active={len(running)} failures={failures}", flush=True)
|
| 146 |
+
|
| 147 |
+
print(f"after_rows={count_rows(result_file)}", flush=True)
|
| 148 |
+
print(f"failures={failures}", flush=True)
|
| 149 |
+
return 0
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
if __name__ == "__main__":
|
| 153 |
+
raise SystemExit(main())
|
rerun_base_deepseek_cloningscenarios_50shards.sh
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
set -u
|
| 3 |
+
|
| 4 |
+
PROJECT_ROOT="/225040511/project"
|
| 5 |
+
PYTHON_BIN="${LAB_BENCH_RUNNER_PYTHON:-/225040511/miniconda3/envs/biomni_e1/bin/python}"
|
| 6 |
+
RUNNER="${PROJECT_ROOT}/react_code_bioagent_deepseek/react_code_bioagent/labbench_runner.py"
|
| 7 |
+
OUT="${OUT:-${PROJECT_ROOT}/base_llm_labbench/results/cloningscenarios_base_deepseek_cloning_50shards_20260524_174548}"
|
| 8 |
+
SHARD_COUNT="${SHARD_COUNT:-50}"
|
| 9 |
+
SHARD_CONCURRENCY="${SHARD_CONCURRENCY:-5}"
|
| 10 |
+
SEED="${SEED:-20260514}"
|
| 11 |
+
|
| 12 |
+
for ENV_FILE in "${PROJECT_ROOT}/.env" "${PROJECT_ROOT}/LAB-Bench/.env" "${PROJECT_ROOT}/base_llm_labbench/.env"; do
|
| 13 |
+
if [[ -f "${ENV_FILE}" ]]; then
|
| 14 |
+
set -a
|
| 15 |
+
# shellcheck disable=SC1090
|
| 16 |
+
source "${ENV_FILE}"
|
| 17 |
+
set +a
|
| 18 |
+
fi
|
| 19 |
+
done
|
| 20 |
+
|
| 21 |
+
export DEEPSEEK_BASE_URL="${DEEPSEEK_BASE_URL:-https://api.deepseek.com/v1}"
|
| 22 |
+
export DEEPSEEK_MODEL_NAME="${DEEPSEEK_MODEL_NAME:-deepseek-chat}"
|
| 23 |
+
|
| 24 |
+
mkdir -p "${OUT}/shard_logs"
|
| 25 |
+
: > "${OUT}/cloningscenarios_results.jsonl"
|
| 26 |
+
: > "${OUT}/cloningscenarios_reasoning.log"
|
| 27 |
+
rm -f "${OUT}/shard_logs"/base_deepseek_cloningscenarios_shard*.log
|
| 28 |
+
|
| 29 |
+
{
|
| 30 |
+
echo "model=base DeepSeek"
|
| 31 |
+
echo "deepseek_model=${DEEPSEEK_MODEL_NAME}"
|
| 32 |
+
echo "eval=CloningScenarios"
|
| 33 |
+
echo "dev_size=0"
|
| 34 |
+
echo "test_size=33"
|
| 35 |
+
echo "shard_count=${SHARD_COUNT}"
|
| 36 |
+
echo "shard_concurrency=${SHARD_CONCURRENCY}"
|
| 37 |
+
echo "seed=${SEED}"
|
| 38 |
+
} > "${OUT}/run_config.txt"
|
| 39 |
+
|
| 40 |
+
echo "output=${OUT}"
|
| 41 |
+
echo "model=${DEEPSEEK_MODEL_NAME}"
|
| 42 |
+
echo "shard_count=${SHARD_COUNT}"
|
| 43 |
+
echo "shard_concurrency=${SHARD_CONCURRENCY}"
|
| 44 |
+
|
| 45 |
+
active=0
|
| 46 |
+
for ((shard=0; shard<SHARD_COUNT; shard++)); do
|
| 47 |
+
label="$(printf "base_deepseek_cloningscenarios_shard%02dof%02d" "$((shard + 1))" "${SHARD_COUNT}")"
|
| 48 |
+
"${PYTHON_BIN}" "${RUNNER}" \
|
| 49 |
+
--evals CloningScenarios \
|
| 50 |
+
--splits test \
|
| 51 |
+
--dev-size 0 \
|
| 52 |
+
--test-size 33 \
|
| 53 |
+
--seed "${SEED}" \
|
| 54 |
+
--n-threads 1 \
|
| 55 |
+
--shard-index "${shard}" \
|
| 56 |
+
--shard-count "${SHARD_COUNT}" \
|
| 57 |
+
--output-root "${OUT}" \
|
| 58 |
+
--compact-results-path "${OUT}/cloningscenarios_results.jsonl" \
|
| 59 |
+
--reasoning-log-path "${OUT}/cloningscenarios_reasoning.log" \
|
| 60 |
+
--skip-existing-results \
|
| 61 |
+
--model "${DEEPSEEK_MODEL_NAME}" \
|
| 62 |
+
--base-url "${DEEPSEEK_BASE_URL}" \
|
| 63 |
+
> "${OUT}/shard_logs/${label}.log" 2>&1 &
|
| 64 |
+
active=$((active + 1))
|
| 65 |
+
if [[ "${active}" -ge "${SHARD_CONCURRENCY}" ]]; then
|
| 66 |
+
wait -n || true
|
| 67 |
+
active=$((active - 1))
|
| 68 |
+
fi
|
| 69 |
+
done
|
| 70 |
+
|
| 71 |
+
while [[ "${active}" -gt 0 ]]; do
|
| 72 |
+
wait -n || true
|
| 73 |
+
active=$((active - 1))
|
| 74 |
+
done
|
| 75 |
+
|
| 76 |
+
rows=0
|
| 77 |
+
if [[ -f "${OUT}/cloningscenarios_results.jsonl" ]]; then
|
| 78 |
+
rows="$(wc -l < "${OUT}/cloningscenarios_results.jsonl")"
|
| 79 |
+
fi
|
| 80 |
+
echo "after_rows=${rows}"
|
run_base_openai_labbench_three_tasks_50shards.sh
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
set -u
|
| 3 |
+
|
| 4 |
+
RUN_ID="${RUN_ID:-openai_labbench_$(date -u +%Y%m%d_%H%M%S)}"
|
| 5 |
+
PROJECT_ROOT="/225040511/project"
|
| 6 |
+
PYTHON_BIN="${LAB_BENCH_RUNNER_PYTHON:-/225040511/miniconda3/envs/biomni_e1/bin/python}"
|
| 7 |
+
RUNNER="${PROJECT_ROOT}/react_code_bioagent_deepseek/react_code_bioagent/labbench_runner.py"
|
| 8 |
+
RESULT_ROOT="${PROJECT_ROOT}/base_llm_labbench/results/base_openai_${RUN_ID}"
|
| 9 |
+
LOG_ROOT="${PROJECT_ROOT}/base_llm_labbench/logs/base_openai_${RUN_ID}"
|
| 10 |
+
SHARD_COUNT="${LABBENCH_SHARD_COUNT:-50}"
|
| 11 |
+
SHARD_CONCURRENCY="${LABBENCH_SHARD_CONCURRENCY:-5}"
|
| 12 |
+
SEED="${LABBENCH_SEED:-20260514}"
|
| 13 |
+
OPENAI_MODEL_NAME="${OPENAI_MODEL_NAME:-${OPENAI_MODEL:-gpt-4o-mini}}"
|
| 14 |
+
OPENAI_BASE_URL="${OPENAI_BASE_URL:-https://api.openai.com/v1}"
|
| 15 |
+
|
| 16 |
+
mkdir -p "${RESULT_ROOT}" "${LOG_ROOT}"
|
| 17 |
+
|
| 18 |
+
for ENV_FILE in "${PROJECT_ROOT}/.env" "${PROJECT_ROOT}/LAB-Bench/.env" "${PROJECT_ROOT}/base_llm_labbench/.env"; do
|
| 19 |
+
if [[ -f "${ENV_FILE}" ]]; then
|
| 20 |
+
set -a
|
| 21 |
+
# shellcheck disable=SC1090
|
| 22 |
+
source "${ENV_FILE}"
|
| 23 |
+
set +a
|
| 24 |
+
fi
|
| 25 |
+
done
|
| 26 |
+
|
| 27 |
+
if [[ -z "${OPENAI_API_KEY:-}" ]]; then
|
| 28 |
+
echo "Missing OPENAI_API_KEY. Put it in ${PROJECT_ROOT}/.env or export it before launching." >&2
|
| 29 |
+
exit 2
|
| 30 |
+
fi
|
| 31 |
+
|
| 32 |
+
wait_limited() {
|
| 33 |
+
local -n pid_array="$1"
|
| 34 |
+
if [[ "${#pid_array[@]}" -lt "${SHARD_CONCURRENCY}" ]]; then
|
| 35 |
+
return
|
| 36 |
+
fi
|
| 37 |
+
local pid="${pid_array[0]}"
|
| 38 |
+
wait "${pid}" || true
|
| 39 |
+
pid_array=("${pid_array[@]:1}")
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
count_rows() {
|
| 43 |
+
local file="$1"
|
| 44 |
+
if [[ -f "${file}" ]]; then
|
| 45 |
+
wc -l < "${file}"
|
| 46 |
+
else
|
| 47 |
+
echo 0
|
| 48 |
+
fi
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
run_eval() {
|
| 52 |
+
local eval_name="$1"
|
| 53 |
+
local dev_size="$2"
|
| 54 |
+
local test_size="$3"
|
| 55 |
+
local eval_lower
|
| 56 |
+
eval_lower="$(echo "${eval_name}" | tr '[:upper:]' '[:lower:]')"
|
| 57 |
+
local out="${RESULT_ROOT}/${eval_lower}"
|
| 58 |
+
local result="${out}/${eval_lower}_results.jsonl"
|
| 59 |
+
local reasoning="${out}/${eval_lower}_reasoning.log"
|
| 60 |
+
local shard_log_dir="${out}/shard_logs"
|
| 61 |
+
mkdir -p "${shard_log_dir}"
|
| 62 |
+
touch "${result}" "${reasoning}"
|
| 63 |
+
|
| 64 |
+
{
|
| 65 |
+
echo "run_id=${RUN_ID}"
|
| 66 |
+
echo "model=baseLLM OpenAI"
|
| 67 |
+
echo "openai_model=${OPENAI_MODEL_NAME}"
|
| 68 |
+
echo "eval=${eval_name}"
|
| 69 |
+
echo "dev_size=${dev_size}"
|
| 70 |
+
echo "test_size=${test_size}"
|
| 71 |
+
echo "shard_count=${SHARD_COUNT}"
|
| 72 |
+
echo "shard_concurrency=${SHARD_CONCURRENCY}"
|
| 73 |
+
echo "seed=${SEED}"
|
| 74 |
+
} > "${out}/run_config.txt"
|
| 75 |
+
|
| 76 |
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] eval=${eval_name} before_rows=$(count_rows "${result}")"
|
| 77 |
+
local pids=()
|
| 78 |
+
for ((shard=0; shard<SHARD_COUNT; shard++)); do
|
| 79 |
+
local label
|
| 80 |
+
label="$(printf "base_openai_%s_shard%02dof%02d" "${eval_lower}" "$((shard + 1))" "${SHARD_COUNT}")"
|
| 81 |
+
"${PYTHON_BIN}" "${RUNNER}" \
|
| 82 |
+
--evals "${eval_name}" \
|
| 83 |
+
--splits test \
|
| 84 |
+
--dev-size "${dev_size}" \
|
| 85 |
+
--test-size "${test_size}" \
|
| 86 |
+
--seed "${SEED}" \
|
| 87 |
+
--n-threads 1 \
|
| 88 |
+
--shard-index "${shard}" \
|
| 89 |
+
--shard-count "${SHARD_COUNT}" \
|
| 90 |
+
--output-root "${out}" \
|
| 91 |
+
--compact-results-path "${result}" \
|
| 92 |
+
--reasoning-log-path "${reasoning}" \
|
| 93 |
+
--skip-existing-results \
|
| 94 |
+
--model "${OPENAI_MODEL_NAME}" \
|
| 95 |
+
--base-url "${OPENAI_BASE_URL}" \
|
| 96 |
+
--api-key "${OPENAI_API_KEY}" \
|
| 97 |
+
> "${shard_log_dir}/${label}.log" 2>&1 &
|
| 98 |
+
pids+=("$!")
|
| 99 |
+
wait_limited pids
|
| 100 |
+
done
|
| 101 |
+
for pid in "${pids[@]}"; do wait "${pid}" || true; done
|
| 102 |
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] eval=${eval_name} after_rows=$(count_rows "${result}")"
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
echo "RUN_ID=${RUN_ID}"
|
| 106 |
+
echo "RESULT_ROOT=${RESULT_ROOT}"
|
| 107 |
+
echo "LOG_ROOT=${LOG_ROOT}"
|
| 108 |
+
echo "OPENAI_MODEL_NAME=${OPENAI_MODEL_NAME}"
|
| 109 |
+
echo "SHARD_COUNT=${SHARD_COUNT}"
|
| 110 |
+
echo "SHARD_CONCURRENCY=${SHARD_CONCURRENCY}"
|
| 111 |
+
|
| 112 |
+
run_eval DbQA 45 315 > "${LOG_ROOT}/dbqa.log" 2>&1 &
|
| 113 |
+
pid_dbqa=$!
|
| 114 |
+
run_eval SeqQA 45 315 > "${LOG_ROOT}/seqqa.log" 2>&1 &
|
| 115 |
+
pid_seqqa=$!
|
| 116 |
+
run_eval CloningScenarios 0 33 > "${LOG_ROOT}/cloningscenarios.log" 2>&1 &
|
| 117 |
+
pid_cloning=$!
|
| 118 |
+
|
| 119 |
+
echo "dbqa_pid=${pid_dbqa}"
|
| 120 |
+
echo "seqqa_pid=${pid_seqqa}"
|
| 121 |
+
echo "cloningscenarios_pid=${pid_cloning}"
|
| 122 |
+
|
| 123 |
+
wait "${pid_dbqa}" || true
|
| 124 |
+
wait "${pid_seqqa}" || true
|
| 125 |
+
wait "${pid_cloning}" || true
|
| 126 |
+
|
| 127 |
+
echo "finished RUN_ID=${RUN_ID}"
|
run_biomni_nomcp_cloningscenarios.sh
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
set -u
|
| 3 |
+
|
| 4 |
+
RUN_ID="${RUN_ID:-cloning_nomcp_$(date -u +%Y%m%d_%H%M%S)}"
|
| 5 |
+
PYTHON_BIN="${LAB_BENCH_RUNNER_PYTHON:-${BIOMNI_EXECUTION_PYTHON:-/225040511/miniconda3/envs/biomni_e1/bin/python}}"
|
| 6 |
+
BIOMNI_ROOT="/225040511/project/Biomni"
|
| 7 |
+
LAB_BENCH_ROOT="/225040511/project/LAB-Bench"
|
| 8 |
+
RUNNER="${BIOMNI_ROOT}/experiments/lab_bench/run_labbench_with_biomni.py"
|
| 9 |
+
OUT_ROOT="${BIOMNI_ROOT}/experiments/lab_bench/results/biomni_no_mcp_cloningscenarios_${RUN_ID}"
|
| 10 |
+
LOG_ROOT="/225040511/project/cloningscenarios_logs/${RUN_ID}"
|
| 11 |
+
EVAL_NAME="CloningScenarios"
|
| 12 |
+
EVAL_LOWER="cloningscenarios"
|
| 13 |
+
DEV_SIZE="${CLONING_DEV_SIZE:-0}"
|
| 14 |
+
TEST_SIZE="${CLONING_TEST_SIZE:-33}"
|
| 15 |
+
SHARD_COUNT="${CLONING_SHARD_COUNT:-33}"
|
| 16 |
+
SHARD_CONCURRENCY="${CLONING_SHARD_CONCURRENCY:-5}"
|
| 17 |
+
SEED="${CLONING_SEED:-20260514}"
|
| 18 |
+
|
| 19 |
+
mkdir -p "${OUT_ROOT}/agent_runtime" "${OUT_ROOT}/shard_logs/${EVAL_LOWER}" "${LOG_ROOT}"
|
| 20 |
+
|
| 21 |
+
for ENV_FILE in "/225040511/project/.env" "${BIOMNI_ROOT}/.env" "${LAB_BENCH_ROOT}/.env"; do
|
| 22 |
+
if [[ -f "${ENV_FILE}" ]]; then
|
| 23 |
+
set -a
|
| 24 |
+
# shellcheck disable=SC1090
|
| 25 |
+
source "${ENV_FILE}"
|
| 26 |
+
set +a
|
| 27 |
+
fi
|
| 28 |
+
done
|
| 29 |
+
|
| 30 |
+
export BIOMNI_CUSTOM_API_KEY="${BIOMNI_CUSTOM_API_KEY:-${DEEPSEEK_API_KEY:-}}"
|
| 31 |
+
export BIOMNI_CUSTOM_BASE_URL="${BIOMNI_CUSTOM_BASE_URL:-${DEEPSEEK_BASE_URL:-https://api.deepseek.com/v1}}"
|
| 32 |
+
export BIOMNI_LLM="${BIOMNI_LLM:-${DEEPSEEK_MODEL_NAME:-deepseek-chat}}"
|
| 33 |
+
export BIOMNI_SOURCE="${BIOMNI_SOURCE:-Custom}"
|
| 34 |
+
|
| 35 |
+
{
|
| 36 |
+
echo "run_id=${RUN_ID}"
|
| 37 |
+
echo "model=Biomni no MCP"
|
| 38 |
+
echo "eval=${EVAL_NAME}"
|
| 39 |
+
echo "dev_size=${DEV_SIZE}"
|
| 40 |
+
echo "test_size=${TEST_SIZE}"
|
| 41 |
+
echo "shard_count=${SHARD_COUNT}"
|
| 42 |
+
echo "shard_concurrency=${SHARD_CONCURRENCY}"
|
| 43 |
+
echo "seed=${SEED}"
|
| 44 |
+
echo "use_mcp=false"
|
| 45 |
+
echo "use_tool_retriever=false"
|
| 46 |
+
} > "${OUT_ROOT}/run_config.txt"
|
| 47 |
+
|
| 48 |
+
count_rows() {
|
| 49 |
+
local file="$1"
|
| 50 |
+
if [[ -f "${file}" ]]; then
|
| 51 |
+
wc -l < "${file}"
|
| 52 |
+
else
|
| 53 |
+
echo 0
|
| 54 |
+
fi
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
wait_limited() {
|
| 58 |
+
local -n pid_array="$1"
|
| 59 |
+
if [[ "${#pid_array[@]}" -lt "${SHARD_CONCURRENCY}" ]]; then
|
| 60 |
+
return
|
| 61 |
+
fi
|
| 62 |
+
local pid="${pid_array[0]}"
|
| 63 |
+
wait "${pid}" || true
|
| 64 |
+
pid_array=("${pid_array[@]:1}")
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
RESULT_FILE="${OUT_ROOT}/${EVAL_LOWER}_results.jsonl"
|
| 68 |
+
REASONING_LOG="${OUT_ROOT}/${EVAL_LOWER}_reasoning.log"
|
| 69 |
+
touch "${RESULT_FILE}" "${REASONING_LOG}"
|
| 70 |
+
|
| 71 |
+
echo "RUN_ID=${RUN_ID}"
|
| 72 |
+
echo "output=${OUT_ROOT}"
|
| 73 |
+
echo "log_root=${LOG_ROOT}"
|
| 74 |
+
echo "before_rows=$(count_rows "${RESULT_FILE}")"
|
| 75 |
+
|
| 76 |
+
pids=()
|
| 77 |
+
for ((shard=0; shard<SHARD_COUNT; shard++)); do
|
| 78 |
+
label="$(printf "biomni_nomcp_%s_shard%02dof%02d" "${EVAL_LOWER}" "$((shard + 1))" "${SHARD_COUNT}")"
|
| 79 |
+
"${PYTHON_BIN}" "${RUNNER}" \
|
| 80 |
+
--eval "${EVAL_NAME}" \
|
| 81 |
+
--split test \
|
| 82 |
+
--dev-size "${DEV_SIZE}" \
|
| 83 |
+
--test-size "${TEST_SIZE}" \
|
| 84 |
+
--seed "${SEED}" \
|
| 85 |
+
--repeat 1 \
|
| 86 |
+
--n_threads 1 \
|
| 87 |
+
--output-root "${OUT_ROOT}" \
|
| 88 |
+
--agent-root "${OUT_ROOT}/agent_runtime" \
|
| 89 |
+
--run-label "${label}" \
|
| 90 |
+
--shard-index "${shard}" \
|
| 91 |
+
--shard-count "${SHARD_COUNT}" \
|
| 92 |
+
--reasoning-log-path "${REASONING_LOG}" \
|
| 93 |
+
--compact-results-path "${RESULT_FILE}" \
|
| 94 |
+
--compact-output-only \
|
| 95 |
+
--skip-existing-results \
|
| 96 |
+
--no-mcp \
|
| 97 |
+
--no-use-tool-retriever \
|
| 98 |
+
> "${OUT_ROOT}/shard_logs/${EVAL_LOWER}/${label}.log" 2>&1 &
|
| 99 |
+
pids+=("$!")
|
| 100 |
+
wait_limited pids
|
| 101 |
+
done
|
| 102 |
+
|
| 103 |
+
for pid in "${pids[@]}"; do
|
| 104 |
+
wait "${pid}" || true
|
| 105 |
+
done
|
| 106 |
+
|
| 107 |
+
echo "after_rows=$(count_rows "${RESULT_FILE}")"
|
| 108 |
+
echo "finished output=${OUT_ROOT}"
|
run_cloningscenarios_all_models.sh
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
set -euo pipefail
|
| 3 |
+
|
| 4 |
+
RUN_ID="${RUN_ID:-cloning_$(date -u +%Y%m%d_%H%M%S)}"
|
| 5 |
+
PYTHON_BIN="${LAB_BENCH_RUNNER_PYTHON:-${BIOMNI_EXECUTION_PYTHON:-/225040511/miniconda3/envs/biomni_e1/bin/python}}"
|
| 6 |
+
EVAL_NAME="CloningScenarios"
|
| 7 |
+
EVAL_LOWER="cloningscenarios"
|
| 8 |
+
DEV_SIZE="${CLONING_DEV_SIZE:-0}"
|
| 9 |
+
TEST_SIZE="${CLONING_TEST_SIZE:-33}"
|
| 10 |
+
SHARD_COUNT="${CLONING_SHARD_COUNT:-33}"
|
| 11 |
+
SHARD_CONCURRENCY="${CLONING_SHARD_CONCURRENCY:-33}"
|
| 12 |
+
SEED="${CLONING_SEED:-20260514}"
|
| 13 |
+
SCALES_TEXT="${CLONING_TOOL_SCALES:-100 500 1000 2000}"
|
| 14 |
+
|
| 15 |
+
PROJECT_ROOT="/225040511/project"
|
| 16 |
+
LAB_BENCH_ROOT="${PROJECT_ROOT}/LAB-Bench"
|
| 17 |
+
BIOMANUS_ROOT="${PROJECT_ROOT}/Biomanus"
|
| 18 |
+
BIOMNI_ROOT="${PROJECT_ROOT}/Biomni"
|
| 19 |
+
BIOMNI_REACT_ROOT="${PROJECT_ROOT}/Biomni-ReAct"
|
| 20 |
+
REACT_CODE_ROOT="${PROJECT_ROOT}/React+code_labbench"
|
| 21 |
+
LOG_ROOT="${PROJECT_ROOT}/cloningscenarios_logs/${RUN_ID}"
|
| 22 |
+
|
| 23 |
+
mkdir -p "${LOG_ROOT}"
|
| 24 |
+
|
| 25 |
+
for ENV_FILE in "${PROJECT_ROOT}/.env" "${BIOMANUS_ROOT}/.env" "${BIOMNI_ROOT}/.env" "${BIOMNI_REACT_ROOT}/.env" "${LAB_BENCH_ROOT}/.env"; do
|
| 26 |
+
if [[ -f "${ENV_FILE}" ]]; then
|
| 27 |
+
set -a
|
| 28 |
+
# shellcheck disable=SC1090
|
| 29 |
+
source "${ENV_FILE}"
|
| 30 |
+
set +a
|
| 31 |
+
fi
|
| 32 |
+
done
|
| 33 |
+
|
| 34 |
+
export DEEPSEEK_BASE_URL="${DEEPSEEK_BASE_URL:-https://api.deepseek.com/v1}"
|
| 35 |
+
export DEEPSEEK_MODEL_NAME="${DEEPSEEK_MODEL_NAME:-deepseek-chat}"
|
| 36 |
+
export BIOMNI_CUSTOM_API_KEY="${BIOMNI_CUSTOM_API_KEY:-${DEEPSEEK_API_KEY:-}}"
|
| 37 |
+
export BIOMNI_CUSTOM_BASE_URL="${BIOMNI_CUSTOM_BASE_URL:-${DEEPSEEK_BASE_URL}}"
|
| 38 |
+
export BIOMNI_LLM="${BIOMNI_LLM:-${DEEPSEEK_MODEL_NAME}}"
|
| 39 |
+
export BIOMNI_SOURCE="${BIOMNI_SOURCE:-Custom}"
|
| 40 |
+
export BIOMNI_REACT_API_KEY="${BIOMNI_REACT_API_KEY:-${DEEPSEEK_API_KEY:-${BIOMNI_CUSTOM_API_KEY:-}}}"
|
| 41 |
+
export BIOMNI_REACT_BASE_URL="${BIOMNI_REACT_BASE_URL:-${DEEPSEEK_BASE_URL}}"
|
| 42 |
+
export BIOMNI_REACT_MODEL="${BIOMNI_REACT_MODEL:-${DEEPSEEK_MODEL_NAME}}"
|
| 43 |
+
export REACT_CODE_API_KEY="${REACT_CODE_API_KEY:-${DEEPSEEK_API_KEY:-${BIOMNI_CUSTOM_API_KEY:-}}}"
|
| 44 |
+
export REACT_CODE_BASE_URL="${REACT_CODE_BASE_URL:-${DEEPSEEK_BASE_URL}}"
|
| 45 |
+
export REACT_CODE_MODEL="${REACT_CODE_MODEL:-${DEEPSEEK_MODEL_NAME}}"
|
| 46 |
+
|
| 47 |
+
if [[ -z "${DEEPSEEK_API_KEY:-}" && -z "${BIOMNI_CUSTOM_API_KEY:-}" && -z "${OPENAI_API_KEY:-}" && -z "${ANTHROPIC_API_KEY:-}" ]]; then
|
| 48 |
+
echo "Missing API key. Set DEEPSEEK_API_KEY, BIOMNI_CUSTOM_API_KEY, OPENAI_API_KEY, or ANTHROPIC_API_KEY." >&2
|
| 49 |
+
exit 2
|
| 50 |
+
fi
|
| 51 |
+
|
| 52 |
+
latest_biomanus_graph() {
|
| 53 |
+
local graph
|
| 54 |
+
graph="$(find "${BIOMANUS_ROOT}/graph_outputs" -maxdepth 1 -type d -name 'mcp_generated_graph_all_*' 2>/dev/null | sort | tail -n 1 || true)"
|
| 55 |
+
if [[ -n "${graph}" && -d "${graph}" ]]; then
|
| 56 |
+
echo "${graph}"
|
| 57 |
+
else
|
| 58 |
+
echo "${BIOMANUS_ROOT}/graph_outputs/labbench_dbqa_seqqa_subgraph"
|
| 59 |
+
fi
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
wait_limited() {
|
| 63 |
+
local -n pid_array="$1"
|
| 64 |
+
if [[ "${#pid_array[@]}" -lt "${SHARD_CONCURRENCY}" ]]; then
|
| 65 |
+
return
|
| 66 |
+
fi
|
| 67 |
+
local pid
|
| 68 |
+
pid="${pid_array[0]}"
|
| 69 |
+
wait "${pid}" || true
|
| 70 |
+
pid_array=("${pid_array[@]:1}")
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
run_biomanus() {
|
| 74 |
+
local out_root="${BIOMANUS_ROOT}/experiments/lab_bench/results/cloningscenarios_biomanus_${RUN_ID}"
|
| 75 |
+
local agent_root="${out_root}/agent_runtime"
|
| 76 |
+
local shard_log_dir="${out_root}/shard_logs"
|
| 77 |
+
local result_file="${out_root}/${EVAL_LOWER}_results.jsonl"
|
| 78 |
+
local reasoning_log="${out_root}/${EVAL_LOWER}_reasoning.log"
|
| 79 |
+
local graph_dir
|
| 80 |
+
graph_dir="$(latest_biomanus_graph)"
|
| 81 |
+
mkdir -p "${out_root}" "${agent_root}" "${shard_log_dir}"
|
| 82 |
+
{
|
| 83 |
+
echo "run_id=${RUN_ID}"
|
| 84 |
+
echo "model=Biomanus"
|
| 85 |
+
echo "eval=${EVAL_NAME}"
|
| 86 |
+
echo "dev_size=${DEV_SIZE}"
|
| 87 |
+
echo "test_size=${TEST_SIZE}"
|
| 88 |
+
echo "shard_count=${SHARD_COUNT}"
|
| 89 |
+
echo "shard_concurrency=${SHARD_CONCURRENCY}"
|
| 90 |
+
echo "graph_dir=${graph_dir}"
|
| 91 |
+
} > "${out_root}/run_config.txt"
|
| 92 |
+
local pids=()
|
| 93 |
+
for ((shard=0; shard<SHARD_COUNT; shard++)); do
|
| 94 |
+
local label
|
| 95 |
+
label="$(printf "biomanus_%s_shard%02dof%02d" "${EVAL_LOWER}" "$((shard + 1))" "${SHARD_COUNT}")"
|
| 96 |
+
"${PYTHON_BIN}" "${BIOMANUS_ROOT}/experiments/lab_bench/scripts/run_labbench_with_hypobioos.py" \
|
| 97 |
+
--evals "${EVAL_NAME}" \
|
| 98 |
+
--splits test \
|
| 99 |
+
--dev-size "${DEV_SIZE}" \
|
| 100 |
+
--test-size "${TEST_SIZE}" \
|
| 101 |
+
--seed "${SEED}" \
|
| 102 |
+
--repeat 1 \
|
| 103 |
+
--n_threads 1 \
|
| 104 |
+
--output-root "${out_root}" \
|
| 105 |
+
--agent-root "${agent_root}" \
|
| 106 |
+
--graph-dir "${graph_dir}" \
|
| 107 |
+
--run-label "${label}" \
|
| 108 |
+
--shard-index "${shard}" \
|
| 109 |
+
--shard-count "${SHARD_COUNT}" \
|
| 110 |
+
--reasoning-log-path "${reasoning_log}" \
|
| 111 |
+
--compact-results-path "${result_file}" \
|
| 112 |
+
--compact-output-only \
|
| 113 |
+
--skip-existing-results \
|
| 114 |
+
--use-graph-retriever \
|
| 115 |
+
--use-mcp \
|
| 116 |
+
> "${shard_log_dir}/${label}.log" 2>&1 &
|
| 117 |
+
pids+=("$!")
|
| 118 |
+
wait_limited pids
|
| 119 |
+
done
|
| 120 |
+
for pid in "${pids[@]}"; do wait "${pid}" || true; done
|
| 121 |
+
echo "Biomanus done: ${out_root}"
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
run_biomni_scale() {
|
| 125 |
+
local scale="$1"
|
| 126 |
+
local log="${LOG_ROOT}/biomni_scale_${scale}.log"
|
| 127 |
+
RUN_ID="${RUN_ID}" \
|
| 128 |
+
LAB_BENCH_TOOL_SCALES="${scale}" \
|
| 129 |
+
LAB_BENCH_EVALS="${EVAL_NAME}" \
|
| 130 |
+
LAB_BENCH_DEV_SIZE="${DEV_SIZE}" \
|
| 131 |
+
LAB_BENCH_TEST_SIZE="${TEST_SIZE}" \
|
| 132 |
+
LAB_BENCH_TEST_SHARD_COUNT="${SHARD_COUNT}" \
|
| 133 |
+
LAB_BENCH_SHARD_CONCURRENCY="${SHARD_CONCURRENCY}" \
|
| 134 |
+
WAIT_FOR_EXISTING_LABBENCH=0 \
|
| 135 |
+
"${BIOMNI_ROOT}/experiments/lab_bench/run_labbench_tool_scales_biomni.sh" > "${log}" 2>&1
|
| 136 |
+
echo "Biomni scale ${scale} done"
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
run_biomni_scales() {
|
| 140 |
+
local scale_pids=()
|
| 141 |
+
local scale
|
| 142 |
+
read -r -a scales <<< "${SCALES_TEXT}"
|
| 143 |
+
for scale in "${scales[@]}"; do
|
| 144 |
+
run_biomni_scale "${scale}" &
|
| 145 |
+
scale_pids+=("$!")
|
| 146 |
+
done
|
| 147 |
+
for pid in "${scale_pids[@]}"; do wait "${pid}" || true; done
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
run_biomni_react() {
|
| 151 |
+
local out_root="${BIOMNI_REACT_ROOT}/LAB-bench/cloningscenarios_${RUN_ID}"
|
| 152 |
+
local shard_log_dir="${out_root}/shard_logs"
|
| 153 |
+
local result_file="${out_root}/${EVAL_LOWER}_results.jsonl"
|
| 154 |
+
local reasoning_log="${out_root}/${EVAL_LOWER}_reasoning.log"
|
| 155 |
+
mkdir -p "${out_root}" "${shard_log_dir}"
|
| 156 |
+
local pids=()
|
| 157 |
+
for ((shard=0; shard<SHARD_COUNT; shard++)); do
|
| 158 |
+
local label
|
| 159 |
+
label="$(printf "biomni_react_%s_shard%02dof%02d" "${EVAL_LOWER}" "$((shard + 1))" "${SHARD_COUNT}")"
|
| 160 |
+
"${PYTHON_BIN}" "${BIOMNI_REACT_ROOT}/scripts/run_labbench_dbqa.py" \
|
| 161 |
+
--eval "${EVAL_NAME}" \
|
| 162 |
+
--split test \
|
| 163 |
+
--dev-size "${DEV_SIZE}" \
|
| 164 |
+
--test-size "${TEST_SIZE}" \
|
| 165 |
+
--seed "${SEED}" \
|
| 166 |
+
--shard-index "${shard}" \
|
| 167 |
+
--shard-count "${SHARD_COUNT}" \
|
| 168 |
+
--output-root "${out_root}" \
|
| 169 |
+
--result-file "${result_file}" \
|
| 170 |
+
--reasoning-log "${reasoning_log}" \
|
| 171 |
+
--skip-existing-results \
|
| 172 |
+
> "${shard_log_dir}/${label}.log" 2>&1 &
|
| 173 |
+
pids+=("$!")
|
| 174 |
+
wait_limited pids
|
| 175 |
+
done
|
| 176 |
+
for pid in "${pids[@]}"; do wait "${pid}" || true; done
|
| 177 |
+
echo "Biomni-ReAct done: ${out_root}"
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
run_react_code() {
|
| 181 |
+
local out_root="${REACT_CODE_ROOT}/results/cloningscenarios_${RUN_ID}"
|
| 182 |
+
local shard_log_dir="${out_root}/shard_logs"
|
| 183 |
+
local result_file="${out_root}/${EVAL_LOWER}_results.jsonl"
|
| 184 |
+
local reasoning_log="${out_root}/${EVAL_LOWER}_reasoning.log"
|
| 185 |
+
mkdir -p "${out_root}" "${shard_log_dir}"
|
| 186 |
+
local pids=()
|
| 187 |
+
for ((shard=0; shard<SHARD_COUNT; shard++)); do
|
| 188 |
+
local label
|
| 189 |
+
label="$(printf "react_code_%s_shard%02dof%02d" "${EVAL_LOWER}" "$((shard + 1))" "${SHARD_COUNT}")"
|
| 190 |
+
"${PYTHON_BIN}" "${REACT_CODE_ROOT}/run_labbench_react_code.py" \
|
| 191 |
+
--eval "${EVAL_NAME}" \
|
| 192 |
+
--split test \
|
| 193 |
+
--dev-size "${DEV_SIZE}" \
|
| 194 |
+
--test-size "${TEST_SIZE}" \
|
| 195 |
+
--seed "${SEED}" \
|
| 196 |
+
--shard-index "${shard}" \
|
| 197 |
+
--shard-count "${SHARD_COUNT}" \
|
| 198 |
+
--output-root "${out_root}" \
|
| 199 |
+
--result-file "${result_file}" \
|
| 200 |
+
--reasoning-log "${reasoning_log}" \
|
| 201 |
+
--skip-existing-results \
|
| 202 |
+
> "${shard_log_dir}/${label}.log" 2>&1 &
|
| 203 |
+
pids+=("$!")
|
| 204 |
+
wait_limited pids
|
| 205 |
+
done
|
| 206 |
+
for pid in "${pids[@]}"; do wait "${pid}" || true; done
|
| 207 |
+
echo "React+code done: ${out_root}"
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
echo "RUN_ID=${RUN_ID}"
|
| 211 |
+
echo "Logs: ${LOG_ROOT}"
|
| 212 |
+
|
| 213 |
+
run_biomanus > "${LOG_ROOT}/biomanus.log" 2>&1 &
|
| 214 |
+
pid_biomanus=$!
|
| 215 |
+
run_biomni_scales > "${LOG_ROOT}/biomni_scales.log" 2>&1 &
|
| 216 |
+
pid_biomni=$!
|
| 217 |
+
run_biomni_react > "${LOG_ROOT}/biomni_react.log" 2>&1 &
|
| 218 |
+
pid_biomni_react=$!
|
| 219 |
+
run_react_code > "${LOG_ROOT}/react_code.log" 2>&1 &
|
| 220 |
+
pid_react_code=$!
|
| 221 |
+
|
| 222 |
+
echo "Biomanus PID: ${pid_biomanus}"
|
| 223 |
+
echo "Biomni scales PID: ${pid_biomni}"
|
| 224 |
+
echo "Biomni-ReAct PID: ${pid_biomni_react}"
|
| 225 |
+
echo "React+code PID: ${pid_react_code}"
|
| 226 |
+
|
| 227 |
+
wait "${pid_biomanus}" || true
|
| 228 |
+
wait "${pid_biomni}" || true
|
| 229 |
+
wait "${pid_biomni_react}" || true
|
| 230 |
+
wait "${pid_react_code}" || true
|
| 231 |
+
|
| 232 |
+
echo "All CloningScenarios jobs finished for RUN_ID=${RUN_ID}"
|
run_cloningscenarios_reactcode_and_base_50shards.sh
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
set -u
|
| 3 |
+
|
| 4 |
+
RUN_ID="${RUN_ID:-cloning_50shards_$(date -u +%Y%m%d_%H%M%S)}"
|
| 5 |
+
PROJECT_ROOT="/225040511/project"
|
| 6 |
+
PYTHON_BIN="${LAB_BENCH_RUNNER_PYTHON:-/225040511/miniconda3/envs/biomni_e1/bin/python}"
|
| 7 |
+
LOG_ROOT="${PROJECT_ROOT}/cloningscenarios_logs/${RUN_ID}"
|
| 8 |
+
SHARD_COUNT="${CLONING_SHARD_COUNT:-50}"
|
| 9 |
+
SHARD_CONCURRENCY="${CLONING_SHARD_CONCURRENCY:-5}"
|
| 10 |
+
DEV_SIZE="${CLONING_DEV_SIZE:-0}"
|
| 11 |
+
TEST_SIZE="${CLONING_TEST_SIZE:-33}"
|
| 12 |
+
SEED="${CLONING_SEED:-20260514}"
|
| 13 |
+
|
| 14 |
+
mkdir -p "${LOG_ROOT}"
|
| 15 |
+
|
| 16 |
+
for ENV_FILE in "${PROJECT_ROOT}/.env" "${PROJECT_ROOT}/react_code_bioagent_deepseek/.env" "${PROJECT_ROOT}/LAB-Bench/.env"; do
|
| 17 |
+
if [[ -f "${ENV_FILE}" ]]; then
|
| 18 |
+
set -a
|
| 19 |
+
# shellcheck disable=SC1090
|
| 20 |
+
source "${ENV_FILE}"
|
| 21 |
+
set +a
|
| 22 |
+
fi
|
| 23 |
+
done
|
| 24 |
+
|
| 25 |
+
export DEEPSEEK_BASE_URL="${DEEPSEEK_BASE_URL:-https://api.deepseek.com/v1}"
|
| 26 |
+
export DEEPSEEK_MODEL_NAME="${DEEPSEEK_MODEL_NAME:-deepseek-chat}"
|
| 27 |
+
export REACT_CODE_API_KEY="${REACT_CODE_API_KEY:-${DEEPSEEK_API_KEY:-${BIOMNI_CUSTOM_API_KEY:-}}}"
|
| 28 |
+
export REACT_CODE_BASE_URL="${REACT_CODE_BASE_URL:-${DEEPSEEK_BASE_URL}}"
|
| 29 |
+
export REACT_CODE_MODEL="${REACT_CODE_MODEL:-${DEEPSEEK_MODEL_NAME}}"
|
| 30 |
+
|
| 31 |
+
wait_limited() {
|
| 32 |
+
local -n pid_array="$1"
|
| 33 |
+
if [[ "${#pid_array[@]}" -lt "${SHARD_CONCURRENCY}" ]]; then
|
| 34 |
+
return
|
| 35 |
+
fi
|
| 36 |
+
local pid="${pid_array[0]}"
|
| 37 |
+
wait "${pid}" || true
|
| 38 |
+
pid_array=("${pid_array[@]:1}")
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
count_rows() {
|
| 42 |
+
local file="$1"
|
| 43 |
+
if [[ -f "${file}" ]]; then
|
| 44 |
+
wc -l < "${file}"
|
| 45 |
+
else
|
| 46 |
+
echo 0
|
| 47 |
+
fi
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
run_react_code_bioagent() {
|
| 51 |
+
local runner="${PROJECT_ROOT}/React+code_labbench/run_labbench_react_code.py"
|
| 52 |
+
local out="${PROJECT_ROOT}/react_code_bioagent_deepseek/labbench_runs/react_code_cloningscenarios_${RUN_ID}"
|
| 53 |
+
local result="${out}/cloningscenarios_results.jsonl"
|
| 54 |
+
local reasoning="${out}/cloningscenarios_reasoning.log"
|
| 55 |
+
mkdir -p "${out}/shard_logs"
|
| 56 |
+
touch "${result}" "${reasoning}"
|
| 57 |
+
echo "react_code_bioagent output=${out}"
|
| 58 |
+
echo "react_code_bioagent before_rows=$(count_rows "${result}")"
|
| 59 |
+
local pids=()
|
| 60 |
+
for ((shard=0; shard<SHARD_COUNT; shard++)); do
|
| 61 |
+
local label
|
| 62 |
+
label="$(printf "react_code_bioagent_cloningscenarios_shard%02dof%02d" "$((shard + 1))" "${SHARD_COUNT}")"
|
| 63 |
+
"${PYTHON_BIN}" "${runner}" \
|
| 64 |
+
--eval CloningScenarios \
|
| 65 |
+
--split test \
|
| 66 |
+
--dev-size "${DEV_SIZE}" \
|
| 67 |
+
--test-size "${TEST_SIZE}" \
|
| 68 |
+
--seed "${SEED}" \
|
| 69 |
+
--shard-index "${shard}" \
|
| 70 |
+
--shard-count "${SHARD_COUNT}" \
|
| 71 |
+
--output-root "${out}" \
|
| 72 |
+
--result-file "${result}" \
|
| 73 |
+
--reasoning-log "${reasoning}" \
|
| 74 |
+
--skip-existing-results \
|
| 75 |
+
> "${out}/shard_logs/${label}.log" 2>&1 &
|
| 76 |
+
pids+=("$!")
|
| 77 |
+
wait_limited pids
|
| 78 |
+
done
|
| 79 |
+
for pid in "${pids[@]}"; do wait "${pid}" || true; done
|
| 80 |
+
echo "react_code_bioagent after_rows=$(count_rows "${result}")"
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
run_base_deepseek() {
|
| 84 |
+
local runner="${PROJECT_ROOT}/react_code_bioagent_deepseek/react_code_bioagent/labbench_runner.py"
|
| 85 |
+
local out="${PROJECT_ROOT}/base_llm_labbench/results/cloningscenarios_base_deepseek_${RUN_ID}"
|
| 86 |
+
local result="${out}/cloningscenarios_results.jsonl"
|
| 87 |
+
local reasoning="${out}/cloningscenarios_reasoning.log"
|
| 88 |
+
mkdir -p "${out}/shard_logs"
|
| 89 |
+
touch "${result}" "${reasoning}"
|
| 90 |
+
echo "base_deepseek output=${out}"
|
| 91 |
+
echo "base_deepseek before_rows=$(count_rows "${result}")"
|
| 92 |
+
local pids=()
|
| 93 |
+
for ((shard=0; shard<SHARD_COUNT; shard++)); do
|
| 94 |
+
local label
|
| 95 |
+
label="$(printf "base_deepseek_cloningscenarios_shard%02dof%02d" "$((shard + 1))" "${SHARD_COUNT}")"
|
| 96 |
+
"${PYTHON_BIN}" "${runner}" \
|
| 97 |
+
--evals CloningScenarios \
|
| 98 |
+
--splits test \
|
| 99 |
+
--dev-size "${DEV_SIZE}" \
|
| 100 |
+
--test-size "${TEST_SIZE}" \
|
| 101 |
+
--seed "${SEED}" \
|
| 102 |
+
--n-threads 1 \
|
| 103 |
+
--shard-index "${shard}" \
|
| 104 |
+
--shard-count "${SHARD_COUNT}" \
|
| 105 |
+
--output-root "${out}" \
|
| 106 |
+
--compact-results-path "${result}" \
|
| 107 |
+
--reasoning-log-path "${reasoning}" \
|
| 108 |
+
--skip-existing-results \
|
| 109 |
+
> "${out}/shard_logs/${label}.log" 2>&1 &
|
| 110 |
+
pids+=("$!")
|
| 111 |
+
wait_limited pids
|
| 112 |
+
done
|
| 113 |
+
for pid in "${pids[@]}"; do wait "${pid}" || true; done
|
| 114 |
+
echo "base_deepseek after_rows=$(count_rows "${result}")"
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
echo "RUN_ID=${RUN_ID}"
|
| 118 |
+
echo "SHARD_COUNT=${SHARD_COUNT}"
|
| 119 |
+
echo "SHARD_CONCURRENCY=${SHARD_CONCURRENCY}"
|
| 120 |
+
|
| 121 |
+
run_react_code_bioagent > "${LOG_ROOT}/react_code_bioagent_deepseek.log" 2>&1 &
|
| 122 |
+
pid_react=$!
|
| 123 |
+
run_base_deepseek > "${LOG_ROOT}/base_deepseek.log" 2>&1 &
|
| 124 |
+
pid_base=$!
|
| 125 |
+
|
| 126 |
+
echo "react_code_bioagent_pid=${pid_react}"
|
| 127 |
+
echo "base_deepseek_pid=${pid_base}"
|
| 128 |
+
echo "log_root=${LOG_ROOT}"
|
| 129 |
+
|
| 130 |
+
wait "${pid_react}" || true
|
| 131 |
+
wait "${pid_base}" || true
|
| 132 |
+
echo "finished RUN_ID=${RUN_ID}"
|